在国内地图开发中,经常会遇到这样一个问题:
GPS 获取到的经纬度,直接放到高德地图或腾讯地图上,位置会出现明显偏移。
这通常不是 GPS 定位错误,而是因为两边使用的坐标体系不同。
常见情况是:
-
GPS、部分 GIS 数据:WGS84
-
高德地图:中国大陆范围内通常使用 GCJ-02
-
腾讯地图:中国大陆范围内通常使用 GCJ-02
-
百度地图:通常使用 BD-09
因此,如果手里拿到的是 GPS 的 WGS84 经纬度,而最终需要显示在高德、腾讯等国内地图服务中,就需要先完成:
WGS84
↓
GCJ-02
本文介绍 WGS84 与 GCJ-02 的区别,并给出一份可以直接使用的 JavaScript 转换代码。
一、WGS84 是什么?
WGS84,全称:
World Geodetic System 1984
是一套全球广泛使用的地理坐标参考系统。
我们日常通过 GPS、GNSS 设备获取到的经纬度,很多情况下就是基于 WGS84。
例如:
经度:116.397428
纬度:39.909230
这种坐标可以用于:
-
GPS 定位
-
GIS 数据处理
-
GPS 轨迹
-
国际地图数据
-
OpenStreetMap 等地理数据场景
-
户外设备定位数据
但如果直接把 WGS84 坐标用于部分中国大陆互联网地图服务,就可能看到位置出现偏移。
二、GCJ-02 是什么?
GCJ-02 经常被开发者称为:
火星坐标系
在中国大陆的互联网地图开发场景中非常常见。
例如高德地图、腾讯地图等服务,在中国大陆范围内通常涉及 GCJ-02 坐标。
因此常见的数据流程是:
GPS设备
↓
WGS84
↓
坐标转换
↓
GCJ-02
↓
高德 / 腾讯地图
如果没有进行转换:
WGS84坐标
↓
直接放入GCJ02地图
↓
出现位置偏移
所以开发地图项目时,第一件需要确认的事情往往不是:
经纬度是多少?
而是:
这个经纬度到底属于什么坐标系?
三、为什么两个坐标看起来很接近,却不能直接混用?
例如某个位置经过坐标转换后,可能出现类似:
WGS84
116.391xxx,39.907xxx
GCJ02
116.397xxx,39.908xxx
两组数字看起来差别不算特别大。
但经纬度的小数变化映射到真实地理位置以后,可能已经是数百米级别的位置差异。
所以:
WGS84 ≠ GCJ02
不能因为:
"经纬度看起来差不多"
就直接当成同一种坐标使用。
这也是很多地图"定位偏了"的真正原因。
四、JavaScript 实现 WGS84 → GCJ02
下面实现一个基础的 JavaScript 坐标转换模块。
const PI = Math.PI;
const AXIS = 6378245.0;
const OFFSET = 0.00669342162296594323;
function outOfChina(lng, lat) {
return (
lng < 72.004 ||
lng > 137.8347 ||
lat < 0.8293 ||
lat > 55.8271
);
}
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 transformLng(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 wgs84ToGcj02(lng, lat) {
if (outOfChina(lng, lat)) {
return [lng, lat];
}
let dLat = transformLat(lng - 105.0, lat - 35.0);
let dLng = transformLng(lng - 105.0, lat - 35.0);
const radLat = (lat / 180.0) * PI;
let magic = Math.sin(radLat);
magic = 1 - OFFSET * magic * magic;
const sqrtMagic = Math.sqrt(magic);
dLat =
(dLat * 180.0) /
(((AXIS * (1 - OFFSET)) / (magic * sqrtMagic)) * PI);
dLng =
(dLng * 180.0) /
((AXIS / sqrtMagic) * Math.cos(radLat) * PI);
const gcjLat = lat + dLat;
const gcjLng = lng + dLng;
return [gcjLng, gcjLat];
}
调用方式:
const result = wgs84ToGcj02(
116.397428,
39.909230
);
console.log(result);
返回的是:
[GCJ02 经度, GCJ02 纬度]
五、实际项目中建议封装成对象返回
数组虽然简单,但业务项目中容易搞混经纬度顺序。
function convertWgs84ToGcj02(lng, lat) {
const [gcjLng, gcjLat] = wgs84ToGcj02(lng, lat);
return {
longitude: gcjLng,
latitude: gcjLat
};
}
使用:
const location = convertWgs84ToGcj02(
116.397428,
39.909230
);
console.log(location.longitude);
console.log(location.latitude);
这样代码会更清晰。
六、一定要统一"经度、纬度"的顺序
例如:
116.397428,39.909230
这里:
116.397428 = 经度 longitude
39.909230 = 纬度 latitude
建议整个项目统一约定:
longitude, latitude
即:
经度,纬度
代码同样统一:
convert(lng, lat);
不要有的模块使用 (lat, lng),另一些模块使用 (lng, lat)。
七、为什么需要 outOfChina 判断?
代码中的 outOfChina 用于判断坐标是否明显处于 GCJ-02 常见适用区域之外。
例如纽约、伦敦、东京、巴黎等坐标,一般没有必要按照中国大陆 GCJ-02 的转换逻辑处理。
需要注意:
这种经纬度边界判断属于工程上的近似范围判断,并不等同于精确国界多边形判断。
如果业务对边界地区要求非常严格,应使用更精确的地理范围数据。
八、前端输入一定要进行参数校验
经度合法范围:
-180 ~ 180
纬度合法范围:
-90 ~ 90
可以封装:
function isValidCoordinate(lng, lat) {
return (
Number.isFinite(lng) &&
Number.isFinite(lat) &&
lng >= -180 &&
lng <= 180 &&
lat >= -90 &&
lat <= 90
);
}
尤其要注意:
Number("")
结果是:
0
所以表单中最好先判断输入是否为空。
九、批量转换怎么实现?
如果需要处理:
116.397428,39.909230
121.473701,31.230416
113.264385,23.129112
可以按行处理:
function batchConvert(text) {
return text
.split(/\r?\n/)
.filter(Boolean)
.map((line, index) => {
const [lngText, latText] = line.split(",");
const lng = Number(lngText);
const lat = Number(latText);
if (!isValidCoordinate(lng, lat)) {
return {
line: index + 1,
success: false,
error: "经纬度格式错误"
};
}
const [gcjLng, gcjLat] =
wgs84ToGcj02(lng, lat);
return {
line: index + 1,
success: true,
sourceLng: lng,
sourceLat: lat,
gcjLng,
gcjLat
};
});
}
这样即使某一行错误,也不会导致整个批量任务失败。
十、GCJ02 转换常见错误
1. 把 WGS84 当成 GCJ02
GPS 获取的数据应先确认坐标体系,再决定是否转换。
2. 经纬度顺序写反
错误:
wgs84ToGcj02(lat, lng);
正确:
wgs84ToGcj02(lng, lat);
3. 重复转换
建议业务数据明确记录坐标体系:
{
longitude: 116.39,
latitude: 39.90,
coordinateSystem: "GCJ02"
}
这样可以避免后续重复转换。
十一、建议项目内部建立统一坐标类型
const CoordinateSystem = {
WGS84: "WGS84",
GCJ02: "GCJ02",
BD09: "BD09"
};
坐标对象:
const point = {
longitude: 116.397428,
latitude: 39.909230,
coordinateSystem: CoordinateSystem.WGS84
};
当项目逐渐涉及:
WGS84
GCJ02
BD09
Web Mercator
这种统一设计会比在业务代码中到处直接调用转换函数更容易维护。
十二、坐标转换结果能做到完全无误差吗?
不建议把任何坐标转换工具理解成"绝对准确"。
实际效果还可能受到:
-
原始 GPS 数据精度
-
手机定位精度
-
GNSS 设备质量
-
地图数据源
-
坐标转换算法
-
原始数据本身是否已经转换
-
地图平台实际接口要求
等因素影响。
开发过程中最重要的是先确认:
数据来源是什么坐标系?
↓
目标地图使用什么坐标系?
↓
是否已经被转换过?
↓
最终统一坐标体系
十三、推荐的数据处理流程
如果是 GPS 数据展示到国内地图:
GPS / GNSS
↓
WGS84
↓
判断目标地图
↓
GCJ02 转换
↓
高德 / 腾讯地图展示
如果后端保存的是原始 GPS 数据,更推荐:
数据库保存原始 WGS84
↓
业务层根据目标平台转换
↓
前端地图展示
这样以后做 GIS 分析、国际地图或其他坐标体系转换时,仍然保留原始数据。
十四、在线测试 WGS84 → GCJ02
如果只是临时转换一个坐标,或者想快速验证自己的代码结果,可以直接使用在线 Demo:
在线 Demo: WGS84转GCJ02 - GPS坐标转高德火星坐标在线工具
支持:
-
WGS84 → GCJ02
-
单个坐标转换
-
经纬度快速粘贴
-
批量坐标处理
-
转换结果复制
-
其他地图坐标工具
总结
WGS84 与 GCJ02 的核心关系,可以简单理解为:
WGS84
GPS / GNSS / 国际地理数据常见
↓
坐标转换
↓
GCJ02
中国大陆部分互联网地图服务常见
地图开发中最重要的并不是记住某一段转换代码,而是建立一个习惯:
任何经纬度数据进入系统之前,先确认坐标系。
只要把下面四件事情理清:
坐标来源
目标地图
坐标体系
转换方向
大部分国内地图定位偏移问题都会容易处理很多。