GCJ-02 火星坐标:WGS84 互转代码与位移实测
你从 GPS 设备、高德、OSM 导出的经纬度经常对不上,差几百米是常态。根子在坐标系:我国要求公开地图服务使用 GCJ-02,而 GPS 和绝大多数 GeoJSON 数据是 WGS84。它跟 WGS84 之间没法用旋转平移描述,本质是一次非线性加密偏移。
两套坐标系的关系
WGS84 是全球 GPS 采用的基准,经纬度即地球表面的真实位置。GCJ-02(俗称火星坐标)是在 WGS84 基础上按经纬度做的一次非线性加偏,自 2002 年起作为我国公开地图的强制标准。没有公开的反向参数,所以高德、腾讯的经纬度不能直接喂给 Leaflet 去叠加 WGS84 底图。
RFC 7946 第 4 节明确规定 GeoJSON 坐标必须基于 WGS84,并删除了旧的 crs 成员。规范的 GeoJSON 理论上不该带 GCJ-02 坐标,但现实里混用太多,导入前一定要确认来源。坐标系与规范的交叉点在 RFC 7946 的隐蔽坑 里讲过。
正向转换代码
GCJ-02 的加偏算法是公开的,基于克拉索夫斯基椭球,下面这段直接能用:
const PI = Math.PI;
const a = 6378245.0; // 克拉索夫斯基椭球长半轴
const ee = 0.00669342162296594323; // 第一偏心率平方
function outOfChina(lng, lat) {
return (lng < 72.004 || lng > 137.8347) ||
(lat < 0.8293 || lat > 55.8271);
}
function tLat(x, y) {
let r = -100 + 2*x + 3*y + 0.2*y*y + 0.1*x*y + 0.2*Math.sqrt(Math.abs(x));
r += (20*Math.sin(6*x*PI) + 20*Math.sin(2*x*PI)) * 2/3;
r += (20*Math.sin(y*PI) + 40*Math.sin(y/3*PI)) * 2/3;
r += (160*Math.sin(y/12*PI) + 320*Math.sin(y/30*PI)) * 2/3;
return r;
}
function tLng(x, y) {
let r = 300 + x + 2*y + 0.1*x*x + 0.1*x*y + 0.1*Math.sqrt(Math.abs(x));
r += (20*Math.sin(6*x*PI) + 20*Math.sin(2*x*PI)) * 2/3;
r += (20*Math.sin(x*PI) + 40*Math.sin(x/3*PI)) * 2/3;
r += (150*Math.sin(x/12*PI) + 300*Math.sin(x/30*PI)) * 2/3;
return r;
}
// WGS84 -> GCJ-02
function wgs84ToGcj02(lng, lat) {
if (outOfChina(lng, lat)) return [lng, lat];
let dLat = tLat(lng - 105, lat - 35);
let dLng = tLng(lng - 105, lat - 35);
const radLat = lat / 180 * PI;
let magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
const sq = Math.sqrt(magic);
dLat = (dLat * 180) / ((a * (1 - ee)) / (magic * sq) * PI);
dLng = (dLng * 180) / (a / sq * Math.cos(radLat) * PI);
return [lng + dLng, lat + dLat];
}
偏移量跟位置强相关。几个代表点的换算结果:
| 地点 | WGS84 经度 | WGS84 纬度 | GCJ-02 经度 | GCJ-02 纬度 | 水平位移 |
|---|---|---|---|---|---|
| 天安门 | 116.397428 | 39.90923 | 116.403672 | 39.910634 | 约 555 米 |
| 广州 | 113.264385 | 23.129112 | 113.269715 | 23.126435 | 约 621 米 |
| 哈尔滨 | 126.534967 | 45.803775 | 126.541007 | 45.805757 | 约 517 米 |
| 喀什 | 75.989261 | 39.467663 | 75.992214 | 39.467888 | 约 255 米 |
天安门从 WGS84(116.397428, 39.90923)换到 GCJ-02 落到 116.403672, 39.910634,水平位移约 555 米。这就是地图"飘"几百米的来源。
逆向转换:没有闭式解
GCJ-02 转回 WGS84 没有解析公式。一次近似是把它当对称偏移减回去,误差 1 到 2 米:
// GCJ-02 -> WGS84(一次近似)
function gcj02ToWgs84(lng, lat) {
if (outOfChina(lng, lat)) return [lng, lat];
const [glng, glat] = wgs84ToGcj02(lng, lat);
return [lng - (glng - lng), lat - (glat - lat)];
}
想要更高精度就迭代两次,第二次用第一次的残差再减一次,误差能压到亚米级。绝大多数可视化场景一次近似已经够用。
顺手处理 BD-09
百度地图在 GCJ-02 之上又叠了一层 BD-09,所以百度坐标要先转回 GCJ-02 再换 WGS84:
// GCJ-02 -> BD-09
function gcj02ToBd09(lng, lat) {
const r = Math.sqrt(lng*lng + lat*lat) + 0.00002*Math.sin(lat*PI);
const t = Math.atan2(lat, lng) + 0.000003*Math.cos(lng*PI);
return [r*Math.cos(t) + 0.0065, r*Math.sin(t) + 0.006];
}
三个坑
偏移只对国境内生效。outOfChina 把境外直接透传,但 72.004 到 137.8347 这个框是按我国陆地轮廓粗划的,中越、中缅边界附近的零散点会算错,要严就自己加边界判定。坐标系对齐、把 GCJ-02 点正确叠加到 WGS84 面上做缓冲,在 GeoJSON 缓冲区分析 里展开过。
批量转换别在循环里反复 new 对象。几百万个点的轨迹,把 transform 内联、复用数组,能快不少。EPSG:4490 转 4326 这种投影级转换是另一回事,属于重投影而非加偏,做法在 Shapefile 转 GeoJSON 实战 里。
底图坐标系要配对。天地图用的是 CGCS2000,和 WGS84 差到厘米级,通常可忽略;OSM 是纯 WGS84。拿 GCJ-02 的点去叠 OSM 底图却不转换,偏差就是真实的火星偏移,这部分在 天地图与 OSM 底图对比 里对照过。
小结
GCJ-02 本质是一次位置相关的非线性加偏,谈不上投影变换。正向往外偏移有公开公式,逆向只能逼近。处理 GeoJSON 前先确认坐标来源:GPS 和标准 GeoJSON 是 WGS84,高德腾讯是 GCJ-02,百度是 BD-09,按链路逐层还原,叠加到 WGS84 底图才不会飘。全国各级行政区划的 WGS84 GeoJSON 数据可在 geojsoncn.com 获取。