Posted June 17, 2017 by mikulas.florek
First, we need to decide where we get the data (height maps and satellite maps) from. This part took most of the time I worked on this feature. After some extensive searching I chose:
Now, we need to:
I will not talk about the 3rd point since image saving is an old feature from the editor.
I implemented simple Slippy Map in imgui so user can navigate to place he wants to export.
To download a heith map from Amazon, we just need to construct the correct web address like this:
http://s3.amazonaws.com/elevation-tiles-prod/terrarium/
http://s3.amazonaws.com/elevation-tiles-prod/terrarium/4/
http://s3.amazonaws.com/elevation-tiles-prod/terrarium/4/1/
http://s3.amazonaws.com/elevation-tiles-prod/terrarium/4/1/2.png
There is a png image at this address. It contains elevation data in meters, split into RGB value:
height_in_meters = (red * 256 + green + blue / 256) - 32768
Zoom parameter is an integer between 0 and 18. Although higher zoom levels might not be available.
Zoom level N
contains 1 << N x 1 << N tiles.
X
and Y
go from 0 to (1 << N) - 1
.
Left edge (X == 0
) is 180 degrees west.
Top edge (Y == 0
) is 85.0511 degrees north.
Similar to Amazon, we just need to construct the correct web address to get an image from Google:
http://maps.googleapis.com/maps/api/staticmap?center=
http://maps.googleapis.com/maps/api/staticmap?center=0,0
http://maps.googleapis.com/maps/api/staticmap?center=0,0&zoom=4
http://maps.googleapis.com/maps/api/staticmap?center=0,0&zoom=4&size=256x256&maptype=satellite
http://maps.googleapis.com/maps/api/staticmap?center=0,0&zoom=4&size=256x256&maptype=satellite&key=YOUR_API_KEY
Since Google uses different coordinate system than Amazon, we need to convert from one to another to be able to download matching data. We already know how Amazon’s XYZ data are related to latitude and longitude, so it should not be a problem. Note that latitude and longitude parameters mark center of the tile.
double tilex2long(float x, int z)
{
return x / pow(2.0, z) * 360.0 - 180;
}
double tiley2lat(float y, int z)
{
double n = M_PI - 2.0 * M_PI * y / pow(2.0, z);
return 180.0 / M_PI * atan(0.5 * (exp(n) - exp(-n)));
}
Since there is no simple library to do this I decided to use Winsock to download the images.
Before we can use socket, we have to initialize Winsock.
#include <WinSock2.h>
#include <Windows.h>
#include <cmath>
#include <cstdlib>
#pragma comment(lib, "wininet.lib")
#pragma comment(lib, "Ws2_32.lib")
WORD sockVer;
WSADATA wsaData;
sockVer = 2 | (2 << 8);
if (WSAStartup(sockVer, &wsaData) != 0) return error("failed to initialize Winsock");
SOCKET socket = ::socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (socket == INVALID_SOCKET) return error("failed to create socket");
const char* host = "s3.amazonaws.com"
SOCKADDR_IN sin;
setMemory(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(80);
hostent* hostname = gethostbyname(host);
if (!hostname) return false;
const char* ip = inet_ntoa(**(in_addr**)hostname->h_addr_list);
sin.sin_addr.s_addr = ip ? ::inet_addr(ip) : INADDR_ANY;
if (connect(socket, (LPSOCKADDR)&sin, sizeof(sin)) != 0)
{
closesocket(socket);
return error("failed to connect");
}
When we are done with the socket, clean up
closesocket(socket);
and when we are done with the whole application
WSACleanup();
The servers communicate in HTTP. First, we have to send HTTP GET request to the server and then we can receive an answer.
static bool writeRawString(SOCKET socket, const char* str)
{
int len = (int)strlen(str);
int send = ::send(socket, str, len, 0);
return send == len;
}
writeRawString(socket, "GET ");
writeRawString(socket, path);
writeRawString(socket, " HTTP/1.1\r\nHost: ");
writeRawString(socket, host);
writeRawString(socket, "\r\nConnection: close\r\n\r\n");
So the request looks like this:
GET /elevation-tiles-prod/terrarium/4/1/2.png HTTP/1.1\r\n
Host: s3.amazonaws.com\r\n
Connection: close\r\n\r\n
After that server should send us its response and we can read it
Array<u8> data(allocator);
data.reserve(16 * 1024);
u8 buf[1024];
while (int r = ::recv(socket, (char*)buf, sizeof(buf), 0))
{
if (r > 0)
{
data.resize(data.size() + r);
copyMemory(&data[data.size() - r], buf, r);
}
}
Note: we should handle errors here, but I am not doing it because I am lazy.
Response from the server should contain HTTP header and png image right after the header.