17

I’m porting an iOS app to Android, and using Google Maps Android API v2. The application needs to draw a heatmap overlay onto the map.

So far, it looks like the best option is to use a TileOverlay and implement a custom TileProvider. In the method getTile, my method is given x, y, and zoom, and needs to return a bitmap in the form of a Tile. So far, so good.

I have an array of heatmap items that I will use to draw radial gradients onto the bitmap, each with a lat/long. I am having trouble with the following two tasks:

  1. How do I determine if the tile represented by x, y, and zoom contains the lat/long of the heatmap item?
  2. How do I translate the lat/long of the heatmap item to x/y coordinates of the bitmap.

Thank you for your help!

UPDATE

Thanks to MaciejGórski's answer below, and marcin's implementation I was able to get the 1st half of my question answered, but I still need help with the 2nd part. To clarify, I need a function to return the x/y coordinates of the tile for a specified lat/long. I've tried reversing the calculations of MaciejGórski's and marcin's answer with no luck.

public static Point fromLatLng(LatLng latlng, int zoom){
    int noTiles = (1 << zoom);
    double longitudeSpan = 360.0 / noTiles;
    double mercator = fromLatitude(latlng.latitude);
    int y = ((int)(mercator / 360 * noTiles)) + 180;
    int x = (int)(latlng.longitude / longitudeSpan) + 180;
    return new Point(x, y);
}

Any help is appreciated!

azcoastal
  • 411
  • 5
  • 11
  • have you found a way to convert LatLng Z to X Y? i found a function that partly works but it gives me the wrong y. – Hugo Alves Nov 08 '13 at 10:15

3 Answers3

9

On zoom level 0, there is only one tile (x=0,y=0). On next zoom level number of tiles are quadrupled (doubled on x and y).

This means on zoom level W, x may be a value in range <0, 1 << W).

From documentation:

The coordinates of the tiles are measured from the top left (northwest) corner of the map. At zoom level N, the x values of the tile coordinates range from 0 to 2N - 1 and increase from west to east and the y values range from 0 to 2N - 1 and increase from north to south.

You can achieve this using simple calculations.

For longitude this is straightforward :

double longitudeMin = (((double) x) / (1 << zoom)) * 360 - 180;
double longitudeMax = (((double) x + 1) / (1 << zoom)) * 360 - 180;
longitudeMax = Double.longBitsToDouble(Double.doubleToLongBits(longitudeMax) - 1); // adjust

Here x is first scaled into <0,1), then into <-180,180).

The max value is adjusted, so it doesn't overlap with the next area. You may skip this.

For latitude this will be a bit harder, because Google Maps use Mercator projection.

First you scale y just like it was in range <-180,180). Note that the values need to be reversed.

double mercatorMax = 180 - (((double) y) / (1 << zoom)) * 360;
double mercatorMin = 180 - (((double) y + 1) / (1 << zoom)) * 360;

Now you use a magical function that does Mercator projection (from SphericalMercator.java):

public static double toLatitude(double mercator) {
    double radians = Math.atan(Math.exp(Math.toRadians(mercator)));
    return Math.toDegrees(2 * radians) - 90;
}

latitudeMax = SphericalMercator.toLatitude(mercatorMax);
latitudeMin = SphericalMercator.toLatitude(mercatorMin);
latitudeMin = Double.longBitsToDouble(Double.doubleToLongBits(latitudeMin) + 1);

This was was typed from memory and was not tested in any way, so if there is an error there, please put a comment and I will fix it.

MaciejGórski
  • 22,187
  • 7
  • 70
  • 94
  • 1
    Ho lee crap! I was kinda expecting a built-in function that I was unaware of. If it takes this much code then perhaps the TileOverlay is not the correct way to draw on the map. Google Maps Javascript v3 has this built in, and so does iOS MKOverlayView. – azcoastal Jun 04 '13 at 18:58
  • In reviewing your answer, I noticed that you are calculating longitude from "x", as well as latitude. Is that a typo? I'm guessing you meant: double mercatorMax = 180 - (((double) y) / (1 << zoom)) * 360; Also, is there a similar way to convert a lat/long to x/y? – azcoastal Jun 04 '13 at 19:04
  • @azcoastal Yes. It was a copy-paste error. I've fixed that. For lat/lng back you would have just to call everything in the reverse order, starting with `SphericalMercator.fromLatitude` under the link I put in the answer. It's not that much code actually. – MaciejGórski Jun 04 '13 at 19:21
  • I hate to ask, but would you mind showing what you mean by "reverse order"? With your help I should be able to turn it around quickly and accept the answer. – azcoastal Jun 05 '13 at 21:22
  • @azcoastal It means the same as if you want to calculate x from `y = x + 1`, you take x to the left and everything else to the right: `-x = -y + 1`; `x = y - 1` and do that from the last line of original code to the first. – MaciejGórski Jun 06 '13 at 07:11
  • does this look correct to you? public static Point fromLatLng(LatLng latlng, int zoom){ int noTiles = (1 << zoom); double longitudeSpan = 360.0 / noTiles; double mercator = fromLatitude(latlng.latitude); int y = ((int)(mercator * noTiles / 360)) + 180; int x = (int)(latlng.longitude / longitudeSpan) + 180; return new Point(x, y); } – azcoastal Jun 06 '13 at 14:22
  • @azcoastal Sorry, can't read code in comments. It's best to create unit tests for it, then write code and see if they fail. – MaciejGórski Jun 06 '13 at 15:04
9

This worked for me:

double n = Math.pow(2, zoom);
double longitudeMin = x/n * 360 -180;
double lat_rad = Math.atan(Math.sinh(Math.PI * (1 - 2 * y/n)));
double latitudeMin = lat_rad * 180/Math.PI;

double longitudeMax = (x + 1)/n * 360 -180;
lat_rad = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1)/n)));
double latitudeMax = lat_rad * 180/Math.PI;

References: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames

Swati Pardeshi
  • 599
  • 9
  • 12
8

MaciejGórski if you don't mind (if you do, I will remove this post) I compiled your code into ready to use method:

private LatLngBounds boundsOfTile(int x, int y, int zoom) {
    int noTiles = (1 << zoom);
    double longitudeSpan = 360.0 / noTiles;
    double longitudeMin = -180.0 + x * longitudeSpan;

    double mercatorMax = 180 - (((double) y) / noTiles) * 360;
    double mercatorMin = 180 - (((double) y + 1) / noTiles) * 360;
    double latitudeMax = toLatitude(mercatorMax);
    double latitudeMin = toLatitude(mercatorMin);

    LatLngBounds bounds = new LatLngBounds(new LatLng(latitudeMin, longitudeMin), new LatLng(latitudeMax, longitudeMin + longitudeSpan));
    return bounds;
}
marcin
  • 2,945
  • 2
  • 12
  • 13
  • 1
    That's fine. Does the code return correct values? I haven't has chance to test it yet. – MaciejGórski Jun 05 '13 at 10:10
  • As far as I know: yes it does :) – marcin Jun 05 '13 at 10:17
  • @marcin, I may have this backwards, so correct me if I'm wrong... Since the constructor for LatLngBounds is looking for the southwest corner as the first param, should it be 'new LatLng(latitudeMax, longitudeMin)' as the 1st param, then 'new LatLng(latitudeMin, longitudeMin + longitudeSpan)' as the 2nd param? I was thinking that x & y represent the NW corner of the tile. – azcoastal Jun 05 '13 at 22:22
  • latitude (in degrees) increases from west to east and from south to north - so southwest point will be `new LatLng(latitudeMin, longitudeMin)` and northeast will be `new LatLng(latitudeMax, longitudeMax)` where `longitudeMax=longitudeMin+longitudeSpan` – marcin Jun 06 '13 at 07:37
  • @azcoastal `x & y represent the NW corner of the tile` No. x, y & zoom represent the tile as a whole. It's like an identifier. – MaciejGórski Jun 06 '13 at 07:53
  • Something to beware of: `LatLng` converts a longitude value of `180` to `-180`, so if you try to represent tile [0,0,0] with `LatLngBounds`, its longitude range is `-180` to `-180`. Such a `LatLngBounds` has zero width; `contains(LatLng)` returns false for any location. – Kevin Krumwiede Jan 03 '17 at 20:31