TileProvider方法getTile - 需要将x和y转换为纬度/经度。

17

我正在将一款iOS应用程序移植到Android,并使用Google Maps Android API v2。该应用程序需要在地图上绘制热力图层叠加。

到目前为止,看起来最好的选择是使用一个TileOverlay并实现一个自定义的TileProvider。在getTile方法中,我的方法会给出x、y和zoom,然后需要返回一张以Tile形式的位图。到此为止,一切都好。

我有一个热图项目数组,我将使用它们在位图上绘制径向渐变,每个坐标都有一个经度/纬度。我在以下两个任务中遇到了麻烦:

  1. 如何确定由x、y和zoom表示的瓦片是否包含热图项的经度/纬度?
  2. 如何将热图项目的经度/纬度转换为位图的x/y坐标。

谢谢您的帮助!

更新

多亏了MaciejGórski下面的答案和marcin的实现,我得到了问题的前半部分的答案,但我仍然需要第二部分的帮助。为了澄清,我需要一个函数来返回指定经度/纬度所对应的瓦片的x/y坐标。我尝试了MaciejGórski和marcin答案的计算方法,但没有成功。

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);
}

非常感谢您的帮助!


你找到将经纬度Z转换为XY的方法了吗?我找到了一个部分工作的函数,但它给我错误的y。 - Hugo Alves
3个回答

9
这是我的解决方案:

这对我起作用了:

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


1
谢谢您提供的链接,其中提供了大多数常见语言中执行这两个操作的代码片段! - Tim Autin

9
在缩放级别0上,只有一个瓦片(x=0,y=0)。在下一个缩放级别上,瓦片数量增加了四倍(x和y各增加一倍)。
这意味着在缩放级别W上,x的值可能在范围内(0,1 << W)之间取值。
根据文档:
瓦片的坐标是从地图左上角(西北角)开始测量的。在缩放级别N上,瓦片坐标的x值范围从0到2N-1,并且从西向东递增,y值范围从0到2N-1,并且从北向南递增。
您可以使用简单的计算来实现此操作。
对于经度而言,这很简单:
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

这里的x首先被缩放到<0,1)区间,然后再缩放到<-180,180)区间。

最大值进行了调整,以避免与下一个区域重叠。你可以跳过这一步。

对于纬度来说,这会有点难,因为Google Maps使用墨卡托投影。

首先,您需要像在<-180,180)范围内那样缩放y值。请注意,值需要反转。

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

现在您可以使用一个神奇的函数,该函数执行墨卡托投影(来自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);

此文档由记忆中打出,未经任何测试。如有错误,请在评论中指出,我会进行修正。


1
天啊!我有点期待一个我不知道的内置函数。如果需要这么多代码,那么TileOverlay可能不是在地图上绘制的正确方式。Google Maps Javascript v3已经内置了此功能,iOS MKOverlayView也是如此。 - azcoastal
很抱歉要问,但您能否展示一下您所说的“反向顺序”是什么意思?有了您的帮助,我应该能够快速地解决问题并接受答案。 - azcoastal
@azcoastal 这意味着如果你想从 y = x + 1 计算出 x,你需要将 x 移到左边,将其余部分移到右边:-x = -y + 1x = y - 1,并且要从原始代码的最后一行开始逐行进行操作。 - MaciejGórski
你觉得这段代码正确吗?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
@azcoastal 抱歉,无法在评论中阅读代码。最好为其创建单元测试,然后编写代码并查看是否失败。 - MaciejGórski
显示剩余2条评论

8

MaciejGórski,如果您不介意的话(如果您介意,我会删除这篇文章),我将您的代码编译成了可直接使用的方法:

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;
}

1
没问题。这段代码返回正确的值吗?我还没有测试过它。 - MaciejGórski
据我所知,是的,它可以 :) - marcin
@marcin,如果我错了,请纠正我...由于LatLngBounds的构造函数正在寻找第一个参数作为西南角,因此第一个参数应该是'new LatLng(latitudeMax, longitudeMin)',第二个参数应该是'new LatLng(latitudeMin, longitudeMin + longitudeSpan)'?我认为x和y代表瓦片的西北角。 - azcoastal
纬度(以度为单位)从西向东,从南向北增加 - 因此,西南点将是“new LatLng(latitudeMin,longitudeMin)”,而东北点将是“new LatLng(latitudeMax,longitudeMax)”,其中“longitudeMax = longitudeMin + longitudeSpan”。 - marcin
@azcoastal x和y代表瓦片的西北角。x、y和缩放级别zoom代表整个瓦片,就像一个标识符。 - MaciejGórski
需要注意的是:LatLng 将经度值 180 转换为 -180,因此如果您尝试使用 LatLngBounds 表示瓦片 [0,0,0],其经度范围为 -180-180。这样的 LatLngBounds 宽度为零;对于任何位置,contains(LatLng) 都会返回 false。 - Kevin Krumwiede

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接