经纬度转换说明书

1,881 阅读10分钟

1617348601938.png

前言

这是一份关于经纬度转换的详细说明书。如果有小伙伴正在或者打算从事地图相关开发任务的,它会是帮你理解和解决经纬度转换的秘籍。

1.常见的地图坐标系

WGS84

地图: google(国外)、osm、arcgisonline

阐述: 大地坐标系,也是目前广泛使用的GPS全球定位系统使用的坐标系。

CGCS2000

地图: 天地图

阐述: 国家大地坐标系,很多时候可以直接用WGS84坐标代替CGCS2000坐标,因为两者的定义一样,采用的参考椭球非常接近。扁率差异引起椭球面上的纬度和高度变化最大达0.1mm。在当前的测量精度范围内,是可以忽略这点差异的。

GCJ-02

地图: 高德地图、腾讯地图、google(国内)

阐述: 火星坐标系,是由中国国家测绘局定制的地理坐标系统,由WGS84坐标一次加密后获得。

BD-09

地图: 百度地图

阐述: 百度坐标系,百度就和人家不一样,在GCJ-02坐标系的基础上又进行了一次加密,牛批!其中,BD-09LL表示百度经纬度坐标,BD-09MC表示百度web墨卡托投影坐标。

关系图

snipaste_20210401_182916.png

清楚这些,是进行坐标转换的前提条件。

2.基本概念

投影

将地理坐标转换到投影坐标的过程,即将不规则的地球曲面转换成平面。

地理坐标系

为球面坐标,参考平面是椭球面,坐标单位:经纬度。

投影坐标系

为平面坐标,参考平面是水平面,坐标单位:米、千米等。

EPSG

The European Petroleum Survey Group,欧洲石油调查组织。维护空间参考系统,OGC以其为标准。其中所有的独立内容都有相应的ID。比如下面的“EPSG:4326”、“EPSG:3857”。

EPSG:4326

一种地理坐标系,默认指WGS84坐标系

EPSG:3857

一种投影坐标系,默认指google的墨卡托坐标系

当然,还有更多的标准,比如EPSG:4479,EPSG:4480等等。

3.Openlayers中的坐标转换

Proj4

在openlayers中,默认使用Proj4库工具进行坐标转换。这个过程是:源坐标系(地理或投影)--->WGS84--->目标坐标系(投影或地理)。由此可见WGS84在这里充当了桥梁的作用,但实际转换过程中可能会丢失精度。

需要注意的是,由于GCJ-02、BD-09坐标系并没有收录到EPSG系统中,所以Proj4是默认没有这两个坐标系的转换方法的。也就是说在使用openlayers时,如果不对其转换规则进行改动,则需要将GCJ-02、BD-09坐标手动转换为WGS84坐标

内置转换方法

  • toLonLat(coordinate,projection)

    coordinate: 投影坐标数组;

    projection:坐标投影,默认值为Web Mercator,即"EPSG:3857"。

  • fromLonLat(coordinate,projection)

    coordinate:经纬度坐标数组;

    projection:目标投影,默认值为Web Mercator,即"EPSG:3857"。

  • transform(coordinate,source,destination)

    coordinate:坐标数组;

    source:源投影;

    destination:目标投影。

  • transformExtent(extent,source,destination,stops)

    extent:范围数组,类似[120,30,120,30];

    source:源投影;

    destination:目标投影;

    stops: Number of stops per side used for the transform. By default only the corners are used,看不懂。

当然,在不修改和定义新的转换规则的情况下,上述的内置转换方法并不足以满足我们的使用,针对高德、百度等地图的坐标数据,我们需要对其进行转换纠偏之后才可以使用,否则会产生较大的误差。

区别

  • 数据存储 通常选择EPSG:4326坐标进行存储,因为EPSG:3857坐标的数据可读性较差,并且由于其数值大导致存储比较占用内存。
  • 渲染 通常使用EPSG:3857进行渲染,因为EPSG:4326会导致页面变形(如下图,EPSG:4326坐标系下地图会被压缩)。

20200706101338652222.png

右上角为墨卡托投影(EPSG:3857),右下角为等距投影(EPSG:4326)

通常,数据存储在 EPSG:4326 中,显示在 EPSG:3857 中。

4.转换算法

经纬度 转 经纬度

BD-09LL 转 GCJ-02

适用范围: 百度坐标火星坐标(高德、腾讯、google国内等)

算法代码:

bdToGcj(lon, lat) {
    let x_pi = 3.14159265358979324 * 3000.0 / 180.0;
    let x = lon - 0.0065;
    let y = lat - 0.006;
    let z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi);
    let theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi);
    return [z * Math.cos(theta), z * Math.sin(theta)];
}

GCJ-02 转 BD-09LL

适用范围: 火星坐标(高德、腾讯、google国内等)百度坐标

算法代码:

gcjToBd(lon, lat) {
    let x_pi = 3.14159265358979324 * 3000.0 / 180.0;
    const latlng = [];
    const x = lon;
    const y = lat;
    const z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * x_pi);
    const theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * x_pi);
    latlng[0] = z * Math.cos(theta) + 0.0065;
    latlng[1] = z * Math.sin(theta) + 0.006;
    return latlng;
}

GCJ-02 转 WGS84

适用范围: 火星坐标(高德、腾讯、google国内等)大地坐标(google国外、osm、arcgisonline等)

算法代码:

gcjToWgs(lon, lat) {
    let PI = 3.14159265358979324;
    function transformGCJ2WGS(gcjLon, gcjLat) {
        let d = delta(gcjLon, gcjLat)
        return [gcjLon - d.lon, gcjLat - d.lat]
    }

    function delta(x, y) {
        let a = 6378245.0 //  a: 卫星椭球坐标投影到平面地图坐标系的投影因子。
        let ee = 0.00669342162296594323 //  ee: 椭球的偏心率。
        let dLat = transformLat(x - 105.0, y - 35.0)
        let dLon = transformLon(x - 105.0, y - 35.0)
        let radLat = y / 180.0 * PI
        let magic = Math.sin(radLat)
        magic = 1 - ee * magic * magic
        let sqrtMagic = Math.sqrt(magic)
        dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * PI)
        dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * PI)
        return {
            'lat': dLat,
            'lon': dLon
        }
    }
    function transformLat(x, y) {
        let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x))
        ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0
        ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0
        ret += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0
        return ret
    }
    function transformLon(x, y) {
        let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x))
        ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0
        ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0
        ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0
        return ret
    }

    return transformGCJ2WGS(lon, lat);
}

WGS84 转 GCJ-02

适用范围: 大地坐标(google国外、osm、arcgisonline等)火星坐标(高德、腾讯、google国内等)

算法代码:

wgsToGcj(lon, lat) {
    let pi = 3.14159265358979324;
    let a = 6378245.0;
    let ee = 0.00669342162296594323;

    /*判断是否在国内,不在国内则不做偏移,因为国外的谷歌地图坐标系是符合WGS84的*/
    function outOfChina(lon, lat) {
        if ((lon < 72.004 || lon > 137.8347) && (lat < 0.8293 || lat > 55.8271)) {
            return true;
        } else {
            return false;
        }
    }

    function transformLat(x, y) {
        let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
        ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
        return ret;
    }

    function transformLon(x, y) {
        let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
        ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
        return ret;
    }

    function transformLonLat(wgLon, wgLat) {
        // 若在国外,不做处理直接返回
        if (outOfChina(wgLon, wgLat)) {
            return [wgLon, wgLat];
        }
        let dLat = transformLat(wgLon - 105.0, wgLat - 35.0);
        let dLon = transformLon(wgLon - 105.0, wgLat - 35.0);
        let radLat = wgLat / 180.0 * pi;
        let magic = Math.sin(radLat);
        magic = 1 - ee * magic * magic;
        let sqrtMagic = Math.sqrt(magic);
        dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
        dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
        return [wgLon + dLon, wgLat + dLat]
    }
    return transformLonLat(lon, lat)
}

经纬度 转 墨卡托

经纬度 转 墨卡托

适用范围: 火星经纬度火星墨卡托WGS84经纬度WGS84墨卡托

算法代码:

lonlatToMercator(lon, lat) {
    let mercator = [0, 0]
    let x = lon * 20037508.34 / 180;
    let y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
    y = y * 20037508.34 / 180;

    mercator = [x, y]
    return mercator;
}

墨卡托 转 经纬度

适用范围: 火星墨卡托火星经纬度WGS84墨卡托WGS84经纬度

算法代码:

mercatorToLonlat(mercatorX, mercatorY) {
    let lonlat = [0, 0]
    var x = mercatorX / 20037508.34 * 180;
    var y = mercatorY / 20037508.34 * 180;
    y = 180 / Math.PI * (2 * Math.atan(Math.exp(y * Math.PI / 180)) - Math.PI / 2);
    lonlat = [x, y]
    return lonlat;
}

BD-09LL 转 BD-09MC

适用范围: 百度经纬度百度墨卡托

算法代码:

bdllToBdmc(lon, lat) {
    let LLBAND = [75, 60, 45, 30, 15, 0];
    let LL2MC = [
        [-0.0015702102444, 111320.7020616939, 1704480524535203, -10338987376042340, 26112667856603880, -35149669176653700, 26595700718403920, -10725012454188240, 1800819912950474, 82.5],
        [0.0008277824516172526, 111320.7020463578, 647795574.6671607, -4082003173.641316, 10774905663.51142, -15171875531.51559, 12053065338.62167, -5124939663.577472, 913311935.9512032, 67.5],
        [0.00337398766765, 111320.7020202162, 4481351.045890365, -23393751.19931662, 79682215.47186455, -115964993.2797253, 97236711.15602145, -43661946.33752821, 8477230.501135234, 52.5],
        [0.00220636496208, 111320.7020209128, 51751.86112841131, 3796837.749470245, 992013.7397791013, -1221952.21711287, 1340652.697009075, -620943.6990984312, 144416.9293806241, 37.5],
        [-0.0003441963504368392, 111320.7020576856, 278.2353980772752, 2485758.690035394, 6070.750963243378, 54821.18345352118, 9540.606633304236, -2710.55326746645, 1405.483844121726, 22.5],
        [-0.0003218135878613132, 111320.7020701615, 0.00369383431289, 823725.6402795718, 0.46104986909093, 2351.343141331292, 1.58060784298199, 8.77738589078284, 0.37238884252424, 7.45]
    ];
    function getRange(cC, cB, T) {
        if (cB != null) {
            cC = Math.max(cC, cB);
        }
        if (T != null) {
            cC = Math.min(cC, T);
        }
        return cC;
    }
    function getLoop(cC, cB, T) {
        while (cC > T) {
            cC -= T - cB;
        }
        while (cC < cB) {
            cC += T - cB;
        }
        return cC;
    }
    function convertor(cC, cD) {
        if (!cC || !cD) {
            return null;
        }
        let T = cD[0] + cD[1] * Math.abs(cC.x);
        const cB = Math.abs(cC.y) / cD[9];
        let cE = cD[2] + cD[3] * cB + cD[4] * cB * cB +
            cD[5] * cB * cB * cB + cD[6] * cB * cB * cB * cB +
            cD[7] * cB * cB * cB * cB * cB +
            cD[8] * cB * cB * cB * cB * cB * cB;
        T *= (cC.x < 0 ? -1 : 1);
        cE *= (cC.y < 0 ? -1 : 1);
        return [T, cE];
    }
    function convertLL2MC(T) {
        let cD, cC, len;
        T.x = getLoop(T.x, -180, 180);
        T.y = getRange(T.y, -74, 74);
        const cB = T;
        for (cC = 0, len = LLBAND.length; cC < len; cC++) {
            if (cB.y >= LLBAND[cC]) {
                cD = LL2MC[cC];
                break;
            }
        }
        if (!cD) {
            for (cC = LLBAND.length - 1; cC >= 0; cC--) {
                if (cB.y <= -LLBAND[cC]) {
                    cD = LL2MC[cC];
                    break;
                }
            }
        }
        const cE = convertor(T, cD);
        return cE;
    }
    return convertLL2MC({
        x: lon,
        y: lat
    });
}

BD-09MC 转 BD-09LL

适用范围: 百度墨卡托百度经纬度

算法代码:

bdmcToBdll(lon, lat) {
    let MCBAND = [12890594.86, 8362377.87, 5591021, 3481989.83, 1678043.12, 0]
    let MC2LL = [
        [1.410526172116255e-8, 0.00000898305509648872, -1.9939833816331, 200.9824383106796, -187.2403703815547, 91.6087516669843, -23.38765649603339, 2.57121317296198, -0.03801003308653, 17337981.2],
        [-7.435856389565537e-9, 0.000008983055097726239, -0.78625201886289, 96.32687599759846, -1.85204757529826, -59.36935905485877, 47.40033549296737, -16.50741931063887, 2.28786674699375, 10260144.86],
        [-3.030883460898826e-8, 0.00000898305509983578, 0.30071316287616, 59.74293618442277, 7.357984074871, -25.38371002664745, 13.45380521110908, -3.29883767235584, 0.32710905363475, 6856817.37],
        [-1.981981304930552e-8, 0.000008983055099779535, 0.03278182852591, 40.31678527705744, 0.65659298677277, -4.44255534477492, 0.85341911805263, 0.12923347998204, -0.04625736007561, 4482777.06],
        [3.09191371068437e-9, 0.000008983055096812155, 0.00006995724062, 23.10934304144901, -0.00023663490511, -0.6321817810242, -0.00663494467273, 0.03430082397953, -0.00466043876332, 2555164.4],
        [2.890871144776878e-9, 0.000008983055095805407, -3.068298e-8, 7.47137025468032, -0.00000353937994, -0.02145144861037, -0.00001234426596, 0.00010322952773, -0.00000323890364, 826088.5]
    ]
    function convertor(cC, cD) {
        if (!cC || !cD) {
            return null;
        }
        let T = cD[0] + cD[1] * Math.abs(cC.x);
        const cB = Math.abs(cC.y) / cD[9];
        let cE = cD[2] + cD[3] * cB + cD[4] * cB * cB +
            cD[5] * cB * cB * cB + cD[6] * cB * cB * cB * cB +
            cD[7] * cB * cB * cB * cB * cB +
            cD[8] * cB * cB * cB * cB * cB * cB;
        T *= (cC.x < 0 ? -1 : 1);
        cE *= (cC.y < 0 ? -1 : 1);
        return [T, cE];
    }

    function convertMC2LL(cB) {
        const cC = {
            x: Math.abs(cB.x),
            y: Math.abs(cB.y)
        };
        let cE;
        for (let cD = 0, len = MCBAND.length; cD < len; cD++) {
            if (cC.y >= MCBAND[cD]) {
                cE = MC2LL[cD];
                break;
            }
        }
        const T = convertor(cB, cE);
        return T;
    }
    return convertMC2LL({
        x: lon,
        y: lat
    });
}

5.一张图

通过一张图清晰自己需要将当前坐标值进行怎样的转换:

1617348601938.png

干货!!!

6.示例

关键点

在一个项目中,首先要明确几个关键点:

  1. 数据的存储。

    首先要明确和统一后台存储的坐标数据是哪种类型的,推荐使用WGS84经纬度坐标,等同于CGCS2000经纬度坐标

    一般来讲后台统一存储的都是WGS84经纬度坐标或者CGCS2000经纬度坐标(天地图)

  2. 使用的地图。

    明确当前使用的是哪种地图,比如高德、google等,此条件并不需要明确选用哪种坐标系

    允许使用任何的地图类型,可以有效使用的地图为天地图、高德、百度、google等。

  3. 选用的坐标系。

    明确当前使用的坐标系,是经纬度坐标系还是墨卡托坐标系。也可以理解为是4326还是3857。

    一般情况下使用墨卡托坐标系较多。

  4. 实际的操作。

    除了上述3个点外,另一个影响坐标转换的还有实际的地图操作。存在两种情况:

    • 根据经纬度坐标进行渲染。例如:点位渲染。

    • 通过地图操作获取经纬度坐标。例如:圈选框选。

    此关键点需要根据第3点对数据进行不同的处理。比如我选择了使用墨卡托坐标系,那么我通过框选获取的就是墨卡托坐标;如果我选择了经纬度坐标系,那么框选获取到的就是经纬度坐标。

具体示例

下面来举个例子哈。

确定关键点:

  1. 数据存储为天地图经纬度坐标;
  2. 使用百度地图;
  3. 选用墨卡托坐标系;
  4. 操作:点位渲染、框选圈选。

点位渲染

将获取的天地图经纬度坐标转换成百度墨卡托坐标

snipaste_20210402_170301.png

由图对号入座可知,坐标转换顺序为1->2->3->6,调用的方法依次为:wgsToGcj(lon,loat),gcjToBd(lon,loat),bdllToBdmc(lon,lat)。

框选圈选

该功能是通过在地图上画矩形或圆形,然后获取到该图形的边界范围,通常需要拿到的是经纬度数据。由于选用的是墨卡托坐标系,所以在进行画图形的操作时,首先拿到的是选用地图的墨卡托坐标,这里就是百度的墨卡托坐标了。

分步骤阐述就是:

  1. 进行画图操作;

  2. 获取到选用地图的墨卡托坐标,这里就是百度的墨卡托坐标;

  3. 将百度的墨卡托坐标转换为天地图的经纬度坐标。

    一般来讲,最后得到的坐标是与存储数据格式相同的,这样方便后台使用。当然,具体情况也要具体分析,比如说部分需求就是通过圈选框选拿到坐标,给后台让其进行数据筛选。

1617351994528.png

由图对号入座可知,坐标转换顺序为6->3->2->1,调用的方法依次为:bdmcToBdll(lon,lat),bdToGcj(lon,loat),gcjToWgs(lon,loat)。

7.总结

整理不易,但允许白嫖~

一起努力啊,陌生人!