一, 问题:

模型格式如下图 ,  需要将此类型文件进行解析关键数据, 并在地图上展示

java 把经纬度转换成坐标 java经纬度计算_java

二, 涨姿势:

目前我们所见的所有地图底图服务都是瓦片地图的方式发布的。瓦片地图金字塔模型是一种多分辨率层次模型,从瓦片金字塔的底层到顶层,分辨率越来越低,但表示的地理范围不变。

当我们建立好了影像金字塔后,前端再请求地图时,则将只是在切好的瓦片缓存中,找到对应级别里对应的瓦片即可。然后在前端将这些请求到的瓦片拼接出来,便可以得到用户需要的级别下的可视范围内的瓦片了。

java 把经纬度转换成坐标 java经纬度计算_Math_02


最小的地图等级是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