24

地图坐标转换

LBS,基于位置的服务(Location Based Service),近年来已经无处不在,尤其是我们前端,相信或多或少都有接触一些地图API服务,比如高德、百度啊、谷歌啊~
但是用的时候可能看到下面这些字眼:比如BD09、火星坐标、WGS84……不由得还是蒙圈了啊?

那么接下来,我们就来了解一下,关于当前用到的一些互联网地图的基础坐标转换知识~

0、首先给大家出一个题

地图上的经纬度转换到平面坐标时,和平面坐标的XY的对应关系是什么,就是经度(longitude)和维度(latitude)分别给对应X,Y中的谁?

这是在实际中经常会用到的一个知识点,我之前没有想太多,反正就把数值往里尝试,因为位置差异很大,正确还是错误一眼就看出来了的,但是这样其实很不好,被师兄说了,我一个GISer的连这个都弄不明白不应该,哈哈哈。不求甚解是可以的,但是专业性还是要强化的。

clipboard.png

来看看上面的图:
经纬度大家都知道,地球上横线是纬度,纵线是经度。
这也导致了我们下意识就会觉得,横线是X,纵线是Y。这样的认知显然是错误的。

但其实,横线是刻画了Y轴上的刻度,纵线是刻画了X轴上的刻度,这里要用到投影的角度来看问题。

clipboard.png

所以大家要记住经纬XY
经度 (longitude) —— 对应 X
维度 ( latitude ) —— 对应 Y

一、当前互联网地图的坐标系现状

1.地球坐标 (WGS84)

国际标准,从专业GPS 设备中取出的数据的坐标系
国际地图提供商使用的坐标系

2.火星坐标 (GCJ-02)也叫国测局坐标系

中国标准,从国行移动设备中定位获取的坐标数据使用这个坐标系
国家规定: 国内出版的各种地图系统(包括电子形式),必须至少采用GCJ-02对地理位置进行首次加密。

3.百度坐标 (BD-09)

百度标准,百度 SDK,百度地图,Geocoding 使用
(百度在火星坐标上的二次加密)

二、互联网在线地图使用的坐标系

火星坐标系:

    iOS 地图(其实是高德)
    Gogole地图
    搜搜、阿里云、高德地图

百度坐标系:

    当然只有百度地图

WGS84坐标系:

    国际标准,谷歌国外地图、osm地图等国外的地图一般都是这个

三、从设备获取经纬度(GPS)坐标

  如果使用的是百度sdk那么可以获得百度坐标(bd09)或者火星坐标(GCJ02),默认是bd09
  如果使用的是ios的原生定位库,那么获得的坐标是WGS84
  如果使用的是高德sdk,那么获取的坐标是GCJ02

四、坐标转换方法--JS版本

我在之前的一篇文章里,基于Ionic框架的使用讲到了地图定位:ionic2入门教程(六)地图服务(谷歌、高德、百度定位),现在重新写一个小demo来实现我们的坐标转换。
关于方法,我找到了应该是最通用的一种,源码地址——作者wandergis,大部分的转换方式应该都是基于他的这个版本,相关说明也是最清楚的。
实际中我们可能会用到不同的地图,那么就对应到不同坐标系的转换,比如说,你有一份wgs84的数据服务,你要展现在百度或者高德地图上,这时候你就需要转换了。
我这里的例子是,我用到百度搜索地名,得到经纬度,但是我要将它绘制在以84为坐标系的地图leaflet之上,这时候我就需要将返回的经纬度进行转换。
我们先用百度搜索广州塔,定位中心

clipboard.png

基于我们选择的OpenStreetMap,未转换之前,我们用百度搜索广州塔返回的值画点,可以看出很明显是偏移了的:

clipboard.png

    <h1>百度地名搜索</h1>
    <input type="text" id="searchVal">
    <button id="searchBtn">广州市内搜索</button>
    <div id="map1">

    </div>
    <script>
        var searchBtn = document.getElementById('searchBtn');

        var mymap = L.map('map1').setView([39.897445, 116.331398], 13);

        L.tileLayer(
            'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
                maxZoom: 18,
                attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, ' +
                    '<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, ' +
                    'Imagery © <a href="http://mapbox.com">Mapbox</a>',
                id: 'mapbox.streets'
            }).addTo(mymap);

        // 创建地址解析器实例
        searchBtn.onclick = function () {
            var searchVal = document.getElementById('searchVal').value;
            var myGeo = new BMap.Geocoder();
            // 将地址解析结果显示在地图上,并调整地图视野
            myGeo.getPoint(searchVal, function (point) {
                if (point) {
                    console.log(point);
                    L.marker([point.lat,point.lng]).addTo(mymap);
                    mymap.setView([point.lat,point.lng],15);
                } else {
                    alert("您选择地址没有解析到结果!");
                }
            }, "广州市");
        }

转换代码如下:

/**
 * copy from https://github.com/wandergis/coordtransform
 * Created by Wandergis on 2015/7/8.
 * 提供了百度坐标(BD09)、国测局坐标(火星坐标,GCJ02)、和WGS84坐标系之间的转换
 */
//UMD魔法代码
// if the module has no dependencies, the above pattern can be simplified to
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
      // AMD. Register as an anonymous module.
      define([], factory);
    } else if (typeof module === 'object' && module.exports) {
      // Node. Does not work with strict CommonJS, but
      // only CommonJS-like environments that support module.exports,
      // like Node.
      module.exports = factory();
    } else {
      // Browser globals (root is window)
      root.coordtransform = factory();
    }
  }(this, function () {
    //定义一些常量
    var x_PI = 3.14159265358979324 * 3000.0 / 180.0;
    var PI = 3.1415926535897932384626;
    var a = 6378245.0;
    var ee = 0.00669342162296594323;
    /**
     * 百度坐标系 (BD-09) 与 火星坐标系 (GCJ-02)的转换
     * 即 百度 转 谷歌、高德
     * @param bd_lon
     * @param bd_lat
     * @returns {*[]}
     */
    var bd09togcj02 = function bd09togcj02(bd_lon, bd_lat) {
      var bd_lon = +bd_lon;
      var bd_lat = +bd_lat;
      var x = bd_lon - 0.0065;
      var y = bd_lat - 0.006;
      var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_PI);
      var theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_PI);
      var gg_lng = z * Math.cos(theta);
      var gg_lat = z * Math.sin(theta);
      return [gg_lng, gg_lat]
    };
  
    /**
     * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换
     * 即谷歌、高德 转 百度
     * @param lng
     * @param lat
     * @returns {*[]}
     */
    var gcj02tobd09 = function gcj02tobd09(lng, lat) {
      var lat = +lat;
      var lng = +lng;
      var z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * x_PI);
      var theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * x_PI);
      var bd_lng = z * Math.cos(theta) + 0.0065;
      var bd_lat = z * Math.sin(theta) + 0.006;
      return [bd_lng, bd_lat]
    };
  
    /**
     * WGS84转GCj02
     * @param lng
     * @param lat
     * @returns {*[]}
     */
    var wgs84togcj02 = function wgs84togcj02(lng, lat) {
      var lat = +lat;
      var lng = +lng;
      if (out_of_china(lng, lat)) {
        return [lng, lat]
      } else {
        var dlat = transformlat(lng - 105.0, lat - 35.0);
        var dlng = transformlng(lng - 105.0, lat - 35.0);
        var radlat = lat / 180.0 * PI;
        var magic = Math.sin(radlat);
        magic = 1 - ee * magic * magic;
        var sqrtmagic = Math.sqrt(magic);
        dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);
        dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI);
        var mglat = lat + dlat;
        var mglng = lng + dlng;
        return [mglng, mglat]
      }
    };
  
    /**
     * GCJ02 转换为 WGS84
     * @param lng
     * @param lat
     * @returns {*[]}
     */
    var gcj02towgs84 = function gcj02towgs84(lng, lat) {
      var lat = +lat;
      var lng = +lng;
      if (out_of_china(lng, lat)) {
        return [lng, lat]
      } else {
        var dlat = transformlat(lng - 105.0, lat - 35.0);
        var dlng = transformlng(lng - 105.0, lat - 35.0);
        var radlat = lat / 180.0 * PI;
        var magic = Math.sin(radlat);
        magic = 1 - ee * magic * magic;
        var sqrtmagic = Math.sqrt(magic);
        dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);
        dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI);
        var mglat = lat + dlat;
        var mglng = lng + dlng;
        return [lng * 2 - mglng, lat * 2 - mglat]
      }
    };
  
    var transformlat = function transformlat(lng, lat) {
      var lat = +lat;
      var lng = +lng;
      var ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
      ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
      ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0;
      ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0;
      return ret
    };
  
    var transformlng = function transformlng(lng, lat) {
      var lat = +lat;
      var lng = +lng;
      var ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
      ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
      ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0;
      ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0;
      return ret
    };
  
    /**
     * 判断是否在国内,不在国内则不做偏移
     * @param lng
     * @param lat
     * @returns {boolean}
     */
    var out_of_china = function out_of_china(lng, lat) {
      var lat = +lat;
      var lng = +lng;
      // 纬度3.86~53.55,经度73.66~135.05 
      return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55);
    };
  
    return {
      bd09togcj02: bd09togcj02,
      gcj02tobd09: gcj02tobd09,
      wgs84togcj02: wgs84togcj02,
      gcj02towgs84: gcj02towgs84
    }
  }));

坐标转换后显示如下,我们需要将百度坐标转成火星坐标再转成wgs84,因为我们用的地图是openstreet,是wgs84坐标系
可以看到,下面中三个点中,有一个已经是正确了的
clipboard.png

        myGeo.getPoint(searchVal, function (point) {
                if (point) {
                    console.log(point);
                    // bd09->gcj02
                    var myPoint = coordtransform.bd09togcj02(point.lng, point.lat);
                    console.log(myPoint);
                    // gcj02->wgs84
                    var myPoint2 = coordtransform.gcj02towgs84(myPoint[0], myPoint[1]);
                    console.log(myPoint2);
                    var latlng = L.latLng([myPoint[1], myPoint[0]]);
                    var latlng2 = L.latLng([myPoint2[1], myPoint2[0]]);
                    // 画点
                    L.marker(point).addTo(mymap);
                    L.marker(latlng).addTo(mymap);
                    L.marker(latlng2).addTo(mymap);
                    // 设置中心
                    mymap.setView([point.lat, point.lng], 13);
                } else {
                    alert("您选择地址没有解析到结果!");
                }
            }, "广州市");

源码https://github.com/JiaXinYi/i...

五、EPSG:3857

如果你用到了leaflet/openlayers/arcgis jsAPI的话,应该还有一个点需要了解
这个算是题外话,因为一般都是学gis的才会用到这些,一般情况下百度高德这些大概都能够满足需求了。
像用到这些地图的情况,经常会涉及到EPSG:3857 或者OpenLayers:900913,acrgis: 102100(3857)。

EPSG:3857 其实是EPSG协会(European Petroleum Survey Group)为 Web Wercator 最终设立的WKID,也就是现在我们常用的Web 地图的坐标系,并且给定官方命名 “WGS 84 / Pseudo-Mercator“。

Web Mercator 是一个投影坐标系统,其基准面是 WGS 1984 。

WGS 1984 是一个长半轴(a)为6378137,短半轴(b)为6356752.314245179 的椭球体,扁率(f)为298.257223563,f=(a-b)/a 。

clipboard.png

但是,Web Mercator 坐标系使用的投影方法不是严格意义的墨卡托投影而是一个被 EPSG称为伪墨卡托的投影方法,这个伪墨卡托投影方法的大名是 Popular Visualization Pseudo Mercator,PVPM。

Google 最先发明了这套系统,在投影过程中,将表示地球的参考椭球体近似的作为正球体处理(正球体半径 R = 椭球体半长轴 a)。

后来,Web Mercator 在 Web 地图领域被广泛使用,这个坐标系就名声大噪。尽管这个坐标系由于精度问题一度不被GIS专业人士接受,但最终 EPSG 还是给了 WKID:3857。

所以其实看到EPSG:3857,就知道,当前的坐标系是wgs84,而这个属性,通常在地图的默认设置中。就是说,如果你不改,这些地图就应该是wgs84坐标系。

六、坐标转换方法--TS版本(待完成)

请输入代码

如果对你有帮助的话,能否考虑打赏a cup of coffee,比心

clipboard.png

参考:
http://wandergis.com/coordtra...
http://blog.csdn.net/kikitamo...
https://www.sogou.com/link?ur...
http://developer.baidu.com/ma...


JiaXinYi
840 声望342 粉丝