一, 问题:
模型格式如下图 , 需要将此类型文件进行解析关键数据, 并在地图上展示
二, 涨姿势:
目前我们所见的所有地图底图服务都是瓦片地图的方式发布的。瓦片地图金字塔模型是一种多分辨率层次模型,从瓦片金字塔的底层到顶层,分辨率越来越低,但表示的地理范围不变。
当我们建立好了影像金字塔后,前端再请求地图时,则将只是在切好的瓦片缓存中,找到对应级别里对应的瓦片即可。然后在前端将这些请求到的瓦片拼接出来,便可以得到用户需要的级别下的可视范围内的瓦片了。
最小的地图等级是0,此时世界地图只由一张瓦片组成
具有唯一的瓦片等级(Z)和瓦片行列坐标编号(X, Y)
瓦片等级越高,组成世界地图的瓦片数越多,可以展示的地图越详细
某一瓦片等级地图的瓦片是由低一级的各瓦片切割成的4个瓦片组成,四叉树结构形成了瓦片金字塔
对于地图瓦片文件的坐标表示,通常瓦片系统使用的是XYZ坐标系统,其中X和Y分别表示瓦片的列和行,Z表示缩放级别(也就是层级)。在Web Mercator投影中,每个瓦片代表地图上的一个固定大小的矩形区域。
SO 给定一个瓦片文件的路径,如/16/53901/24785.png,我们可以这样解析坐标:
缩放级别(Z): 16
列(X): 53901
行(Y): 24785
请注意,这里的Y坐标通常是从地图的顶部(北)开始计算的,但在某些情况下,Y坐标可能是从底部(南)开始计算的,这取决于瓦片系统的具体实现。
要计算一个瓦片文件集合的最大和最小坐标,你需要遍历集合中的所有文件,并提取每个文件的X、Y和Z坐标。然后,你可以找出X和Y坐标的最大值和最小值,以及Z坐标的最大值和最小值。这些值将分别代表整个文件集合在地图上的最大和最小边界。
三, Coding
public class TileBoundsCalculator3 {
// 计算瓦片经纬度范围的函数
public static Bounds calculateTileBounds(int tileX, int tileY, int zoomLevel) {
double tileRes = 360.0 / (1 << zoomLevel);
double minLat = (tileY * tileRes - 90);
double maxLat = ((tileY + 1) * tileRes - 90);
double minLon = (tileX * tileRes - 180);
double maxLon = ((tileX + 1) * tileRes - 180);
return new Bounds(minLon, minLat, maxLon, maxLat);
}
// 表示经纬度范围的类
public static class Bounds {
public double minLon;
public double minLat;
public double maxLon;
public double maxLat;
public Bounds(double minLon, double minLat, double maxLon, double maxLat) {
this.minLon = minLon;
this.minLat = minLat;
this.maxLon = maxLon;
this.maxLat = maxLat;
}
// 输出函数,方便打印查看
@Override
public String toString() {
return "Bounds{" +
"minLon=" + minLon +
", minLat=" + minLat +
", maxLon=" + maxLon +
", maxLat=" + maxLat +
'}';
}
}
// 使用示例
public static void main(String[] args) {
int tileX = 53901;
int tileY = 24786;
int zoomLevel = 16;
Bounds bounds = calculateTileBounds(tileX, tileY, zoomLevel);
System.out.println(bounds);
System.out.println((tileX + 1) * (360 / (2^zoomLevel)));
}
}
四, 未完, 为什么有误差? 经度对了,纬度差老远?
这个方法假设地球是一个完美的球体,并且使用了简化的三角学公式来进行转换。,它直接基于缩放级别和瓦片坐标来计算瓦片的边界。它假设地球表面在瓦片尺寸上是均匀划分的,并且忽略了墨卡托投影的非线性特性。因此,这种方法计算起来更简单、更快,但在高纬度地区可能会导致较大的误差。在实际应用中,你可能需要使用更精确的地球模型(如WGS84)以及更复杂的算法来得到更准确的经纬度坐标。
请注意,这个方法只适用于XYZ瓦片坐标系统。如果你使用的是其他类型的瓦片坐标系统(如QuadKey、QuadTree或其他自定义系统),那么你需要根据该系统的规范来实现相应的转换逻辑。
此外,如果你正在使用特定的地图服务(如Google Maps API),那么通常该服务会提供自己的API或库来简化瓦片坐标与经纬度之间的转换过程。在这种情况下,你应该查阅该服务的文档,以了解如何使用其提供的工具来完成转换。
五, 正解
基于墨卡托投影(Web Mercator projection)的公式,将瓦片的像素坐标转换为经纬度。pixelToLatLng 函数首先计算出一个中间值 n,然后根据这个值计算纬度和经度。calculateTileBounds 函数则利用 pixelToLatLng 函数来找出瓦片四个角(或至少三个角)的经纬度,然后确定瓦片的边界范围。
特别是在纬度上。它通常能够提供更准确的瓦片边界经纬度,特别是在高纬度地区。
public class TileToLatLongConverter2 {
// 瓦片的像素尺寸(常见的Web Mercator瓦片大小为256x256像素)
private static final int TILE_SIZE = 256;
// 地球半径(单位:米)
private static final double EARTH_RADIUS = 6378137.0;
// 初始偏移量(用于将经纬度转换为像素坐标)
private static final double INITIAL_RESOLUTION = TILE_SIZE * 0.5 / (Math.PI * EARTH_RADIUS);
/**
* 将经纬度转换为像素坐标。
*
* @param lat 纬度(范围:-90到90)
* @param lng 经度(范围:-180到180)
* @param zoom 缩放级别
* @return 像素坐标(px, py)
*/
private static double[] latLngToPixel(double lat, double lng, int zoom) {
double sinLat = Math.sin(lat * Math.PI / 180);
// 墨卡托投影的公式
double pixelX = ((lng + 180) / 360) * TILE_SIZE * Math.pow(2, zoom);
double pixelY = (1 + Math.sin(lat * Math.PI / 180)) / 2 * TILE_SIZE * Math.pow(2, zoom);
return new double[]{pixelX, pixelY};
}
/**
* 将像素坐标转换为经纬度。
*
* @param px 像素X坐标
* @param py 像素Y坐标
* @param zoom 缩放级别
* @return 经纬度(lat, lng)
*/
private static double[] pixelToLatLng(double px, double py, int zoom) {
double n = Math.PI - (2 * Math.PI * py) / (TILE_SIZE * Math.pow(2, zoom));
double lat = (Math.toDegrees(Math.atan(Math.sinh(n))));
double lng = (px / (TILE_SIZE * Math.pow(2, zoom))) * 360 - 180;
// System.out.println(lat +" "+ lng);
return new double[]{lat, lng};
}
/**
* 计算瓦片边界的经纬度范围。
*
* @param tileX 瓦片X坐标
* @param tileY 瓦片Y坐标
* @param zoom 缩放级别
* @return 瓦片边界的经纬度范围(minLat, minLng, maxLat, maxLng)
*/
public static double[] calculateTileBounds(int tileX, int tileY, int zoom) {
// 计算瓦片左上角的经纬度
double[] topLeftLatLng = pixelToLatLng(tileX * TILE_SIZE, tileY * TILE_SIZE, zoom);
double minLat = topLeftLatLng[0];
double minLng = topLeftLatLng[1];
// 计算瓦片右上角的经纬度
double[] topRightLatLng = pixelToLatLng((tileX + 1) * TILE_SIZE, tileY * TILE_SIZE, zoom);
double maxLat = topRightLatLng[0];
double maxLng = topRightLatLng[1];
// 计算瓦片左下角的经纬度
double[] bottomLeftLatLng = pixelToLatLng(tileX * TILE_SIZE, (tileY + 1) * TILE_SIZE, zoom);
double bottomLat = bottomLeftLatLng[0];
// 瓦片在纬度上不是等宽的,因此我们需要取左下角和左上角的纬度中的最小值作为minLat
minLat = Math.min(minLat, bottomLat);
// 瓦片在经度上是等宽的,所以maxLng已经在计算右上角时得到
// 返回瓦片边界的经纬度范围
return new double[]{minLat, minLng, maxLat, maxLng};
}
public static void main(String[] args) {
sout(3368, 1549, 12,calculateTileBounds(3368, 1549, 12));
sout(6737, 3098, 13,calculateTileBounds(6737, 3098, 13));
sout(53901, 24785, 16,calculateTileBounds(53901, 24785, 16));
sout(1724854, 793148, 21,calculateTileBounds(53901, 24785, 16));
}
private static void sout(int tileX, int tileY, int zoom, double[] bounds) {
System.out.println(tileX+" "+tileY+" "+zoom+" "+bounds[0]+" "+bounds[1]+" "+bounds[2]+" "+bounds[3]);
}
// 转回去
public static String getTileNumber(final double lat, final double lon, final int zoom) {
int xtile = (int) Math.floor((lon + 180) / 360 * (1 << zoom));
int ytile = (int) Math.floor((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2 * (1 << zoom));
if (xtile < 0)
xtile = 0;
if (xtile >= (1 << zoom))
xtile = ((1 << zoom) - 1);
if (ytile < 0)
ytile = 0;
if (ytile >= (1 << zoom))
ytile = ((1 << zoom) - 1);
return ("" + zoom + "/" + xtile + "/" + ytile);
}
}
3368 1549 12 40.044437584608566 116.015625 40.11168866559596 116.103515625
6737 3098 13 40.07807142745009 116.0595703125 40.11168866559596 116.103515625
53901 24785 16 40.1032859129344 116.0870361328125 40.107487419012415 116.092529296875
1724854 793148 21 40.1032859129344 116.0870361328125 40.107487419012415 116.092529296875