TileProvider method getTile - need to translate x and y to lat/long
Asked Answered
F

3

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!

Frontage answered 2/6, 2013 at 15:3 Comment(1)
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.Cheekpiece
P
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.

Partridge answered 4/6, 2013 at 17:10 Comment(7)
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.Frontage
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?Frontage
@Frontage 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.Gragg
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.Frontage
@Frontage 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.Gragg
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); }Frontage
@Frontage Sorry, can't read code in comments. It's best to create unit tests for it, then write code and see if they fail.Gragg
D
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

Divergence answered 1/10, 2013 at 12:27 Comment(1)
Thank you for the link, which provide code snippets to do both operations in most common languages !Damicke
M
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;
}
Misadvise answered 5/6, 2013 at 9:41 Comment(6)
That's fine. Does the code return correct values? I haven't has chance to test it yet.Gragg
As far as I know: yes it does :)Misadvise
@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.Frontage
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+longitudeSpanMisadvise
@Frontage x & y represent the NW corner of the tile No. x, y & zoom represent the tile as a whole. It's like an identifier.Gragg
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.Bakken

© 2022 - 2024 — McMap. All rights reserved.