Static Google Maps coordinate projection

1 years ago

Static Google Maps coordinate projection

Do you want to project coordinates to the screen when using the Google Maps Static API?

Google Maps already provide customizing of static maps to an extent and in most cases, I would say what's available is more than enough. For example, you can render: different map types, markers (also with custom icons!), and even paths!

But say you want to introduce interaction in your static map, then in most cases you'd actually need the JavaScript API.

However, there are cases where you might still want/need to use the Static API:

  • You want to conceal the coordinates.
  • Maybe for a minimalistic geoguesser game where you don't want the users to be able to find the coordinates through the JavaScript API.

  • You want nothing but simplicity.
  • Maybe the Static Maps API has all you actually need but all you want to do is anchor a button or interactable label at a specific coordinate.

    Or maybe you don't want any external sources in your website, like the JavaScript API.

Click anywhere on the map to open the coordinate.

So, if you do want to project a world coordinate to a screen coordinate, there's three steps we have to take:

  1. Calculating coordinates to world coordinates
  2. First we need to translate our latitude and longitude coordinates into world coordinates according to the Mercator projection.

    The Mercator projection is what Google uses to translate all coordinates input into the API to world coordinates.

  3. Calculating world coordinates to pixel coordinates
  4. Before we can apply our coordinates, they need to be scaled to our zoom level - which is what pixel coordinates are.

  5. Calculating and applying the pixel coordinates
  6. With all calculations at hand, we need to apply them for our scenario!

This is great for when we want to anchor an overlaying element on our map, but what about calculating coordinates from a mouse click?

Then we have to reverse the entire process, starting with reversing the pixel coordinates to world coordinates to coordinates.

Calculating the pixel coordinates

Given that on a regular request we can only input a search string or a coordinate - as the center of the map, the zoom level, and the canvas size; we have to use these 3 properties to make a calculation that aligns with the API's.

If you're working with search inputs, you should do the geocoding yourself, so that you're only working with coordinates with the Static Maps API. This is to ensure both the API and you are working with the same center.

This is the request example I'll be working with in this article:

https://maps.googleapis.com/maps/api/staticmap?center=58.3797273637364,12.324802043063467&zoom=15&size=250x250&key=API_KEY

It's a 250x250 map of Vänersborg, Sweden at zoom level 15.

Google provides fantastic transparent documentation on how their coordinates are calculated and I'll be using this as a reference going forwards.

Understanding the viewport

Not to be confused with the visible viewport definition.

Before we can start, it's important that we understand how the viewport of the map works with dependency to the size parameter.

For example, this is a hypothetical example of a 100x100 map:

And this is the same hypothetical map but sized 200x100:

As you can tell, the map expands by the size, anchored by the center.

This is because the map projection that Google Maps uses for translating coordinates to world coordinates is the Mercator projection.

With the Mercator projection, Google Maps then uses tile sizes relative to the zoom level to expand the map centered by our coordinates by the size.

We can calculate what tile size Google uses at what zoom level by multiplying the zoom level by 256:

function getTileSize(zoomLevel) { return zoomLevel * 256; };

Calculating coordinates to world coordinates

We need to convert our coordinates to world coordinates using the Mercator projection ourselves, this is so that we can use the Mercator world coordinates to get our pixel coordinates.

Using the Mercator projection, we can input our coordinates and get the world coordinates based on our zoom level's tile size:

function getMercatorWorldCoordinateProjection(zoomLevel, latitude, longitude) { const tileSize = getTileSize(zoomLevel); const latitudeToRadians = ((latitude * Math.PI) / 180); const northing = Math.log(Math.tan((Math.PI / 4) + (latitudeToRadians / 2))); return { left: ((longitude + 180) * (tileSize / 360)), top: ((tileSize / 2) - ((tileSize * northing) / (2 * Math.PI))) }; }

Calculating world coordinates to pixel coordinates

Now that we have our world coordinates, we have to take the zoom level into account. Using the expression given to us in the Google Maps documentation:

pixelCoordinate = worldCoordinate * (2 ** zoomLevel)

We can apply this to our world coordinates through an expontential function:

function getPixelCoordinates(zoomLevel, leftWorldCoordinate, topWorldCoordinate) { return { left: leftWorldCoordinate * (2 ** zoomLevel), top: topWorldCoordinate * (2 ** zoomLevel) }; }

Calculating and applying the pixel coordinates

With the pixel coordinates at hand, we can subtract them with the center pixel coordinates and then add our map's half size:

const width = 250; const height = 250; const offsetLeft = (width / 2) + (targetPixelCoordinates.left - centerPixelCoordinates.left); const offsetTop = (height / 2) + (targetPixelCoordinates.top - centerPixelCoordinates.top);

Final result

With all 3 sections combined, this would be the full code:

const centerWorldCoordinates = getMercatorWorldCoordinateProjection(zoomLevel, center.latitude, center.longitude); const targetWorldCoordinates = getMercatorWorldCoordinateProjection(zoomLevel, target.latitude, target.longitude); const centerPixelCoordinates = getPixelCoordinates(zoomLevel, centerWorldCoordinates.left, centerWorldCoordinates.top); const targetPixelCoordinates = getPixelCoordinates(zoomLevel, targetWorldCoordinates.left, targetWorldCoordinates.top); const offsetLeft = (width / 2) + (targetPixelCoordinates.left - centerPixelCoordinates.left); const offsetTop = (height / 2) + (targetPixelCoordinates.top - centerPixelCoordinates.top);

Now offsetLeft and offsetTop is our offsets from the top left corner of our map, with the projected coordinates.

Reversing screen coordinates to coordinates

If we want to do the opposit - translate a pixel on the map to coordinates, we have to go through the same process as above, but in the reversed order.

This is useful if for example, you only want to capture a latitude and longitude click on your static map.

Reverting screen coordinates to pixel coordinates

First we need to revert our click event's offset with our map's half width and height, which would give us the relative pixel coordinates.

Going forwards, we'll assume the screen coordinates we want to reverse is coming from a mouse event: offsetX and offsetY.

function getRelativePixelCoordinates(width, height, offsetLeft, offsetTop) { return { left: offsetLeft - (width / 2), top: offsetTop - (height / 2) }; }

Now that we have the relative pixel coordinate - based on the pixel coordinates of the center of our map, we need to calculate our map's center pixel coordinates again.

If we now add our relative pixel coordinates to the center pixel coordinates, we now have an absolute pixel coordinate of our click event:

function getAbsolutePixelCoordinates(pixelCoordinates, relativePixelCoordinates) { return { left: pixelCoordinates.left + relativePixelCoordinates.left, top: pixelCoordinates.top + relativePixelCoordinates.top }; }

We can use the exact same code as before to get the center pixel coordinates and now use our 2 new functions:

const centerWorldCoordinates = getMercatorWorldCoordinateProjection(zoomLevel, center.latitude, center.longitude); const centerPixelCoordinates = getPixelCoordinates(zoomLevel, centerWorldCoordinates.left, centerWorldCoordinates.top); const relativePixelCoordinates = getRelativePixelCoordinates(width, height, event.offsetX, event.offsetY); const pixelCoordinates = getAbsolutePixelCoordinates(centerPixelCoordinates, relativePixelCoordinates);

Calculating screen coordinates to world coordinates

Reusing the formula given to us in the Google Maps documentation, we can use it again but this time to divide our pixel coordinates with the scale:

function getWorldCoordinates(zoomLevel, pixelCoordinates) { return { left: pixelCoordinates.left / (2 ** zoomLevel), top: pixelCoordinates.top / (2 ** zoomLevel) }; }

Calculating world coordinates to coordinates

Now that we have the world coordinates of our click, we need to reverse the Mercator world coordinate projection to get the actual coordinates.

function getCoordinatesFromMercatorWorldCoordinates(zoomLevel, worldCoordinates) { const tileSize = getTileSize(zoomLevel); const northing = (2 * Math.atan(Math.exp(((tileSize / 2) - worldCoordinates.top) * (2 * Math.PI) / tileSize))) - (Math.PI / 2); return { latitude: northing * 180 / Math.PI, longitude: (worldCoordinates.left * 360 / tileSize) - 180 }; }

With all the pieces put together, this is how you'd calculate the screen coordinates to coordinates:

const relativePixelCoordinates = getRelativePixelCoordinates(width, height, event.offsetX, event.offsetY); const pixelCoordinates = getAbsolutePixelCoordinates(centerPixelCoordinates, relativePixelCoordinates); const worldCoordinates = getWorldCoordinates(zoomLevel, pixelCoordinates); const coordinates = getCoordinatesFromMercatorWorldCoordinates(zoomLevel, worldCoordinates);

Now coordinates contains our clicked latitude and longitude!