前言
这是一份关于经纬度转换的详细说明书。如果有小伙伴正在或者打算从事地图相关开发任务的,它会是帮你理解和解决经纬度转换的秘籍。
1.常见的地图坐标系
WGS84
地图: google(国外)、osm、arcgisonline。
阐述: 大地坐标系,也是目前广泛使用的GPS全球定位系统使用的坐标系。
CGCS2000
地图: 天地图。
阐述: 国家大地坐标系,很多时候可以直接用WGS84坐标代替CGCS2000坐标,因为两者的定义一样,采用的参考椭球非常接近。扁率差异引起椭球面上的纬度和高度变化最大达0.1mm。在当前的测量精度范围内,是可以忽略这点差异的。
GCJ-02
地图: 高德地图、腾讯地图、google(国内)。
阐述: 火星坐标系,是由中国国家测绘局定制的地理坐标系统,由WGS84坐标一次加密后获得。
BD-09
地图: 百度地图。
阐述: 百度坐标系,百度就和人家不一样,在GCJ-02坐标系的基础上又进行了一次加密,牛批!其中,BD-09LL表示百度经纬度坐标,BD-09MC表示百度web墨卡托投影坐标。
关系图
清楚这些,是进行坐标转换的前提条件。
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坐标系下地图会被压缩)。
右上角为墨卡托投影(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.一张图
通过一张图清晰自己需要将当前坐标值进行怎样的转换:
干货!!!
6.示例
关键点
在一个项目中,首先要明确几个关键点:
-
数据的存储。
首先要明确和统一后台存储的坐标数据是哪种类型的,推荐使用WGS84经纬度坐标,等同于CGCS2000经纬度坐标。
一般来讲后台统一存储的都是WGS84经纬度坐标或者CGCS2000经纬度坐标(天地图)。
-
使用的地图。
明确当前使用的是哪种地图,比如高德、google等,此条件并不需要明确选用哪种坐标系。
允许使用任何的地图类型,可以有效使用的地图为天地图、高德、百度、google等。
-
选用的坐标系。
明确当前使用的坐标系,是经纬度坐标系还是墨卡托坐标系。也可以理解为是4326还是3857。
一般情况下使用墨卡托坐标系较多。
-
实际的操作。
除了上述3个点外,另一个影响坐标转换的还有实际的地图操作。存在两种情况:
- 根据经纬度坐标进行渲染。例如:点位渲染。
- 通过地图操作获取经纬度坐标。例如:圈选框选。
此关键点需要根据第3点对数据进行不同的处理。比如我选择了使用墨卡托坐标系,那么我通过框选获取的就是墨卡托坐标;如果我选择了经纬度坐标系,那么框选获取到的就是经纬度坐标。
具体示例
下面来举个例子哈。
确定关键点:
- 数据存储为天地图经纬度坐标;
- 使用百度地图;
- 选用墨卡托坐标系;
- 操作:点位渲染、框选圈选。
点位渲染
将获取的天地图经纬度坐标转换成百度墨卡托坐标。
由图对号入座可知,坐标转换顺序为1->2->3->6,调用的方法依次为:wgsToGcj(lon,loat),gcjToBd(lon,loat),bdllToBdmc(lon,lat)。
框选圈选
该功能是通过在地图上画矩形或圆形,然后获取到该图形的边界范围,通常需要拿到的是经纬度数据。由于选用的是墨卡托坐标系,所以在进行画图形的操作时,首先拿到的是选用地图的墨卡托坐标,这里就是百度的墨卡托坐标了。
分步骤阐述就是:
-
进行画图操作;
-
获取到选用地图的墨卡托坐标,这里就是百度的墨卡托坐标;
-
将百度的墨卡托坐标转换为天地图的经纬度坐标。
一般来讲,最后得到的坐标是与存储数据格式相同的,这样方便后台使用。当然,具体情况也要具体分析,比如说部分需求就是通过圈选框选拿到坐标,给后台让其进行数据筛选。
由图对号入座可知,坐标转换顺序为6->3->2->1,调用的方法依次为:bdmcToBdll(lon,lat),bdToGcj(lon,loat),gcjToWgs(lon,loat)。
7.总结
整理不易,但允许白嫖~
一起努力啊,陌生人!