前段时间接了个需求,要把一批从某地方业务平台导出的天地图坐标点位,展示到高德地图上。负责导入的同事一开始觉得这事很简单,经纬度嘛,直接拿过来打点就行。结果点位一上去,几乎每个都跟真实位置对不上,有的往北偏,有的往东偏,最远的相差将近一公里。
排查到最后,问题出在最基础的地方:天地图坐标和高德坐标根本不是同一种坐标系。这篇内容我不讲太多虚的,就把“天地图坐标转高德坐标系”的原理、转换算法、JS实现和我在业务里遇到的坑完整写一遍,希望正在被坐标折腾的你能快速摆脱困境。
1. 为什么天地图坐标放到高德地图上会偏移
1.1 现场还原:从坐标错位到确定坐标系问题
我先把现场还原一下。那天同事把坐标点导进去之后,地图上的标记点跟实际位置完全对不上,放大到道路级别,点落在了街道对面,缩小到城区级别,点落在了相邻片区。我第一反应是“数据解析出了问题”,就把打印出来的经纬度拿到地图平台的在线坐标拾取工具上,找到同一真实位置再拾取一次坐标,两套数据放在一起对比,发现小数点后的数值存在明显差异。
当时做了个简单统计:几十个点位的偏差方向不统一,有的偏东,有的偏西,距离从几十米到几百米不等。这个现象其实很关键,如果是固定常数平移,说明可能是底图偏移;但方向不统一,基本可以断定是坐标系算法差异。查文档之后确认,天地图公开服务使用 CGCS2000 坐标系,而高德互联网地图使用 GCJ-02 坐标系,两者之间有非线性偏移,必须经过算法换算才能对齐。
1.2 三种坐标系快速扫盲
先说清楚最基础的三套坐标,理解了这套东西,后面所有代码都是水到渠成。
| 坐标系 | 通俗叫法 | 主要应用场景 | 和真实地球位置的关系 |
|---|---|---|---|
| CGCS2000 | 2000国家大地坐标系 | 天地图、测绘成果、很多政务平台 | 接近真实位置,基于地心参考框架 |
| WGS84 | GPS经纬度 | GPS设备、国际标准、部分GIS软件 | 全球统一,真实经纬度 |
| GCJ-02 | 火星坐标 | 高德地图、腾讯地图等互联网地图 | 在WGS84基础上做了非线性偏移 |
可以这么理解:WGS84 是“不带滤镜”的全球经纬度标准,GPS 量出来的原始结果就是它。CGCS2000 和 WGS84 很接近,在普通应用里差别可以忽略。而 GCJ-02 相当于在 WGS84 坐标上套了一层“偏移滤镜”,能把真实位置挪动几十到几百米。高德地图为了让互联网地图符合管理要求,底图和标注都是基于 GCJ-02 来展示的,所以你直接传原始经纬度给高德,它会把原始经纬度当作 GCJ-02 来用,位置自然就偏了。
1.3 偏移不是固定值,不能靠加常数解决
曾经有同事问过我:“既然有偏移,那我在经纬度上统一加一个修正值行不行?”我的答案是:不行。
GCJ-02 的偏移不是一个固定平移量,而是由一套多项式算法生成的。你可以简单理解为一张照片上叠加了一层不规则扭曲的玻璃,同一个修正量不可能同时修正照片中心和边缘的变形。这也就是为什么同一个城市里,有些点向东偏 50 米,有些点向西偏 300 米,方向差异很大。处理这种情况,必须使用标准的坐标系转换算法,让每个点都经过同样的数学变换。
明确“不能加常数”之后,接下来就是怎么转换的问题。
2. 转换链路与方案设计
2.1 换算路径:CGCS2000 → WGS84 → GCJ-02
天地图坐标转高德坐标,走的链路是:先把 CGCS2000 当作 WGS84 处理,再从 WGS84 转成 GCJ-02。
为什么中间要插一步 WGS84?因为公开的 GCJ-02 偏移算法,输入前提是 WGS84 经纬度。虽然 CGCS2000 和 WGS84 不是完全相等的两个坐标系,但它们在民用互联网应用中的差距很小,多数情况下只有厘米级到亚米级,对前端展示、地图标注、业务分析来说完全够用。
严格地说,CGCS2000 和 WGS84 的椭球参数存在微小差异,同一点在两个坐标系下的坐标差通常在 0.1 米左右,即使在全国大范围场景也不会超过 1 米。这个精度在地图上根本显示不出来,如果你把“转成高德坐标”这件事放在高德地图 Web 端去做,点标记的像素误差远大于这个系统差。所以工程上最合理的方案就是:不额外做七参数转换,直接把 CGCS2000 经纬度近似成 WGS84,再走标准算法转 GCJ-02。
2.2 先弄清楚你的数据到底是什么坐标系
拿到数据之后别急着写转换代码,先确认坐标系来源。这一步做错了,后面所有步骤都是白费。
| 数据来源 | 常见坐标系 | 接入高德前的处理 |
|---|---|---|
| 天地图 Web API 返回经纬度 | CGCS2000 | 近似按 WGS84,再转 GCJ-02 |
| GPS 设备采集 | WGS84 | 直接转 GCJ-02 |
| 高德/腾讯地图拾取器取点 | GCJ-02 | 无需转换 |
| 百度地图取点 | BD-09 | 先转 WGS84,再转 GCJ-02 |
| 测绘部门导出 Shapefile/GeoJSON | 可能是投影坐标(EPSG:4547 等) | 先反投到经纬度,再走转换 |
最容易被坑的是 GeoJSON 和 Shapefile。很多文件看起来里面是经纬度,但投影信息里写的可能是 CGCS2000 / Gauss-Kruger 投影,单位是米而不是度。如果你拿“米制坐标”直接当作经纬度去喂给转换公式,算出来的结果会完全错乱。判断方法很简单:看坐标数值大小。经度应该在 70 到 140 左右,纬度应该在 0 到 55 左右,如果出现 300000 这种六位数,几乎可以肯定是投影坐标。
2.3 精度需求决定方案取舍
我处理这个项目时,最终选了“CGCS2000 近似 WGS84 + 标准 GCJ-02 算法”的方案,就是因为业务对精度要求属于地图可视化级别。
如果你的场景也是把点位展示在地图上、做区域统计、做轨迹回放,那这个方案是最合适的:简单、稳定、不依赖专业软件。如果你的场景是施工放样、不动产确权这类需要厘米级精度的测绘业务,那就不能在前端做近似转换了,应该由测绘专业人员用七参数转换工具把成果统一到目标坐标系,再进入地图系统展示。还有一种中间场景,比如做片区级分析和路网匹配,CGCS2000 与 WGS84 的亚米级差异不影响结论,重点仍然是把 GCJ-02 的几百米偏移消掉。
一句话总结:前端 JS 负责把“互联网地图的坐标系不一致”这个问题解决掉,真正的测绘级精度替换不在前端职责范围内。
3. 核心算法与JS实现
3.1 WGS84转GCJ02的标准算法原理
现在进入核心环节。先上 WGS84 转 GCJ-02 的算法,这是整个转换链路里最关键的一段,网上流传的版本很多,但底层逻辑是同一套:通过两个多项式函数计算纬度偏移量 dLat 和经度偏移量 dLng,然后基于地球椭球参数修正,最终加到原始坐标上。
这段代码里的参数是公开算法里固定的常量,不需要自己调。你可以把它理解成一个“偏移查询函数”:输入某个位置的经纬度,输出这个位置应该偏移多少。每个点都要走一遍三角函数和多项式计算,所以它天然是逐点处理的。
这是标准实现:
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 * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0; ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.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 * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0; ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0; return ret; } function wgs84ToGcj02(lng, lat) { if (outOfChina(lng, lat)) { return { lng, lat }; } const a = 6378245.0; const ee = 0.006693421622965943; let dLat = transformLat(lng - 105.0, lat - 35.0); let dLng = transformLng(lng - 105.0, lat - 35.0); const radLat = (lat / 180.0) * Math.PI; let magic = Math.sin(radLat); magic = 1 - ee * magic * magic; const sqrtMagic = Math.sqrt(magic); dLat = (dLat * 180.0) / (((a * (1 - ee)) / (magic * sqrtMagic)) * Math.PI); dLng = (dLng * 180.0) / ((a / sqrtMagic) * Math.cos(radLat) * Math.PI); return { lng: lng + dLng, lat: lat + dLat }; }这里的a = 6378245.0是克拉索夫斯基椭球的长半轴,ee = 0.006693421622965943是偏心率平方。这两个常量是算法自带的,不要在不知道原理的情况下去改。网上有些版本把常量写错,导致结果偏差,所以复制代码时尽量找流传较广的标准版。outOfChina的作用是判断点位是否在算法覆盖范围之外,如果你处理的是境外位置,直接返回原始坐标。
3.2 封装完整的天地图坐标转高德坐标模块
为了让业务代码干净,我把转换逻辑封装成一个独立模块。模块里除了正算函数,我还额外加了一个 GCJ-02 转 WGS84 的反算函数。很多人只需要天地图转高德,但实际开发中你大概率会遇到从高德拿坐标给其他图层用的反向需求,一次封装好更省事。
反算法我用的是迭代逼近,思路很简单:先猜一个 WGS84 坐标,把它转成 GCJ-02,拿结果和真实目标比较,把差值反向加回去,重复几次就收敛了。这个思路在转换没有直接反函数表达式时很实用。
// coordinate-transform.js const PI = Math.PI; 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; } export 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 a = 6378245.0; const ee = 0.006693421622965943; const radLat = (lat / 180.0) * PI; let magic = Math.sin(radLat); magic = 1 - ee * magic * magic; const sqrtMagic = Math.sqrt(magic); dLat = (dLat * 180.0) / (((a * (1 - ee)) / (magic * sqrtMagic)) * PI); dLng = (dLng * 180.0) / ((a / sqrtMagic) * Math.cos(radLat) * PI); return { lng: lng + dLng, lat: lat + dLat }; } export function gcj02ToWgs84(lng, lat) { if (outOfChina(lng, lat)) { return { lng, lat }; } let mx = lng; let my = lat; let dx = 0.01; let dy = 0.01; while (Math.abs(dx) > 1e-9 && Math.abs(dy) > 1e-9) { const g = wgs84ToGcj02(mx, my); dx = lng - g.lng; dy = lat - g.lat; mx += dx; my += dy; } return { lng: mx, lat: my }; } // 天地图坐标转高德坐标 // 天地图的 CGCS2000 在互联网场景下近似 WGS84,因此直接走 wgs84ToGcj02 export function tiandituToGaode(lng, lat) { return wgs84ToGcj02(lng, lat); } // 高德坐标转天地图坐标,反向使用 export function gaodeToTianditu(lng, lat) { return gcj02ToWgs84(lng, lat); }实际业务中,如果后端在入库时已经做过坐标统一,前端只需要展示,那转换工具就很简单。但如果像我这次一样,上游给的是历史积累的天地图坐标,下游又要高德展示,那这个模块就是全项目的“地基”。
需要注意一点,gcj02ToWgs84的迭代初始步长会影响收敛速度,但通常运行 5 到 10 次就能把误差压到厘米级,对前端完全足够。
3.3 自测:用往返转换验证工具函数
写完工具函数不能直接上业务,先做一轮自测。我的习惯是:用高德地图的在线坐标拾取工具随便取一个点,当作 GCJ-02 目标坐标,然后先反算成 WGS84/天地图坐标,再正算回高德坐标,最后对比还原后的坐标与原始 GCJ-02 坐标的差距。
这个测试能同时验证正算、反算两个函数有没有写错,也能验证算法覆盖范围判断是否正确。
const target = { lng: 113.271241, lat: 23.129114 }; // 示例:高德拾取的点 const wgs = gcj02ToWgs84(target.lng, target.lat); const back = wgs84ToGcj02(wgs.lng, wgs.lat); const deltaLng = Math.abs(back.lng - target.lng); const deltaLat = Math.abs(back.lat - target.lat); console.log(`还原误差(经度): ${deltaLng}`); console.log(`还原误差(纬度): ${deltaLat}`);如果算法正确,还原误差应该在0.000001度级别,大约相当于 0.1 米,几乎可以忽略。如果误差达到0.001度、也就是上百米,说明函数实现有问题,常见的原因包括:常量写错、outOfChina条件写反、或者经纬度顺序搞混。
另一个自测方法是取一个你自己熟悉的固定点位,比如公司楼下,从高德地图上拾取坐标记为 G,从天地图上拾取同一个点记为 T。把 T 传入tiandituToGaode,如果得到的结果接近 G,说明转换链路是通的。这个测试建议放到代码上线前跑一遍,不要等到地图上出现几百个错位点才去返工。
4. 在业务系统里落地的实操细节
4.1 接入高德JS API:转换后打点的标准姿势
转换函数就绪后,接入高德地图非常简单。开发中最容易犯的错误是:拿到原始天地图坐标后,一边循环一边调用转换函数,一边再创建地图标注,功能上没问题,但代码逻辑混乱,后面排查困难。
我推荐的做法分成两步,第一步先批量转换坐标,第二步再统一渲染。这样你可以随时打印转换后的数组,确认坐标正确后再上地图。
import AMapLoader from '@amap/amap-jsapi-loader'; import { tiandituToGaode } from './coordinate-transform.js'; const rawPoints = [ { lng: 113.123456, lat: 23.654321 }, { lng: 113.234567, lat: 23.765432 } ]; // 先转换 const convertedPoints = rawPoints.map(p => { const g = tiandituToGaode(p.lng, p.lat); return { lng: g.lng, lat: g.lat }; }); AMapLoader.load({ key: 'YOUR_AMAP_KEY', version: '2.0' }).then(AMap => { const map = new AMap.Map('map', { center: convertedPoints[0], zoom: 12 }); convertedPoints.forEach(p => { new AMap.Marker({ position: [p.lng, p.lat], map }); }); });这里有个高德 API 的小细节:AMap.Marker的position属性是经度在前、纬度在后,也就是[lng, lat],千万别倒过来。如果倒过来,点位会跳到完全相反的位置,而且这种错误调试起来很伤脑筋。
4.2 批量点位转换与渲染性能优化
有人可能会担心转换函数在循环里做三角函数计算,点位多了会不会卡。我之前测试过,万级点位的坐标转换耗时通常在几十毫秒到一两百毫秒,跟地图打点相比几乎可以忽略。真正的瓶颈在高德地图创建的 Marker 数量上,几千个普通AMap.Marker就能让页面明显卡顿。
如果你有大量点位要展示,不要用普通 Marker 硬扛,改用高德的AMap.MassMarks,也就是海量点类,它针对大数据量渲染做了优化。对MassMarks来说,转换后的坐标数组可以直接作为数据源,每一条记录带上经纬度和业务字段即可。
const massData = convertedPoints.map(p => ({ lnglat: [p.lng, p.lat], name: '示例点' })); const mass = new AMap.MassMarks(massData, { opacity: 0.8, zIndex: 111, cursor: 'pointer', style: { url: '//example.com/marker.png', anchor: { x: 16, y: 16 }, size: { width: 32, height: 32 } } }); mass.setMap(map);从这段代码可以看到,坐标转换和渲染优化是两个独立的事情。先保证坐标正确,再考虑渲染效率,顺序不能反。我见过有的项目为了让海量点显示得更流畅,把转换逻辑搬到循环里加了很多条件判断,结果性能没提升,代码还变得很难维护。
4.3 与React/Vue工程集成的注意事项
如果你在 React 或 Vue 项目里做这件事,建议把坐标转换函数放到独立的工具文件里,不要写在组件内部。因为地图相关的组件生命周期比较复杂,地图实例创建、销毁、重新渲染都会发生,如果转换函数定义在组件内部,很容易出现闭包引用旧数据的问题。
在 Vue 3 组合式 API 里,我习惯这样组织:
// src/utils/coordinate-transform.js // 存放 3.2 节里的全部转换函数 // MapComponent.vue <script setup> import { onMounted, ref } from 'vue'; import AMapLoader from '@amap/amap-jsapi-loader'; import { tiandituToGaode } from '@/utils/coordinate-transform'; const rawPoints = ref([]); const mapRef = ref(null); onMounted(async () => { const points = rawPoints.value.map(p => { const g = tiandituToGaode(p.lng, p.lat); return [g.lng, g.lat]; }); const AMap = await AMapLoader.load({ key: 'YOUR_AMAP_KEY', version: '2.0' }); const map = new AMap.Map(mapRef.value, { center: points[0], zoom: 10 }); points.forEach(pos => { new AMap.Marker({ position: pos, map }); }); }); </script>React 里思路也类似,只是在useEffect里执行,依赖项要处理好。一个很常见的坑是:组件重复渲染时,地图实例被重复创建,导致点位不断叠加。解决办法是在useEffect的清理函数里销毁地图实例,或者用一个 ref 保存地图实例。
其实不管用什么框架,原则都是一样的:坐标转换是纯函数,放在数据进入组件之前完成;地图实例是昂贵的对象,只创建一次;渲染逻辑用生命周期钩子控制。
4.4 投影坐标先做反算
前面提到过,有些数据源导出的不是经纬度,而是 CGCS2000 投影坐标,数值是米级单位。这种数据如果直接拿到转换函数里算,结果会错到离谱。
以 CGCS2000 高斯-克吕格 3 度带投影为例,坐标形式可能是x=519283.71, y=3320124.56。你需要先用投影库把它反算成 CGCS2000 经纬度,再走tiandituToGaode。
我通常用proj4这个库:
npm install proj4import proj4 from 'proj4'; // 以中央经线 111 度的 CGCS2000 3 度带投影为例 // 实际使用时请换成你的投影定义,尤其是中央经线 proj4.defs('CGCS2000_3_DEGREE_CM_111', '+proj=tmerc +lat_0=0 +lon_0=111 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs'); // EPSG:4490 是 CGCS2000 地理坐标系,即经纬度 const [lng, lat] = proj4('CGCS2000_3_DEGREE_CM_111', 'EPSG:4490', [519283.71, 3320124.56]); const gaode = tiandituToGaode(lng, lat); console.log(gaode);需要注意:投影定义里的中央经线、假东偏移量、椭球体参数必须和你的数据完全一致,否则反算出来的经纬度会沿着某个方向整体偏移。如果你不知道数据的投影定义,就从数据源的基础信息里查,Shapefile 通常会有.prj文件,GeoJSON 一般带crs字段。
5. 常见问题与避坑清单
5.1 典型问题速查表
我把常见问题和解决办法整理成了表格,方便你直接对照排查。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 点位整体偏移 300 到 500 米 | 高德把原始坐标当 GCJ-02 使用,而你传的是 WGS84/天地图坐标 | 先转 GCJ-02 再传入 |
| 偏移方向不固定、距离不等 | 坐标系差异属于非线性偏移,不能加固定常数 | 使用标准 WGS84 转 GCJ-02 算法 |
| 转换后还是偏移几十米 | 原始数据实际是投影坐标,被误当经纬度 | 先做投影反算,得到真正经纬度 |
| 点位到了完全错误的大洲 | 经纬度顺序填反,高德要求[lng, lat] | 检查position参数顺序 |
| 个别境外点位不偏移 | outOfChina判断走境外逻辑,直接返回原坐标 | 确认算法覆盖范围,境外数据用 WGS84 展示 |
| 部分点位转换后与高德拾取坐标差 1 米以内 | 算法正确,属于相邻坐标系之间的正常误差 | 不需要处理,视觉对齐 |
| 转换函数返回 NaN | 输入坐标存在非法值,或函数未做非数字防护 | 对输入做Number.isFinite判断 |
这个表格基本涵盖了我遇到过的八类问题。其中“经纬度顺序”看起来是最低级的问题,但前端、后端、地图 API 对经纬度顺序的定义并不统一,排查时一定要先确认顺序。
5.2 一个完整的排查顺序
如果按照上面的方法转换之后,点位还是不对,不要着急改算法,按这个顺序排查。
- 第一步:确认原始数据的坐标系。打印五个坐标点,和大范围估判法对比,判断是经纬度还是投影坐标。
- 第二步:确认高德地图展示层是否已经使用 GCJ-02。如果你用的是第三方封装,它可能内部已经做过一次转换,你再转一次就造成二次偏移。
- 第三步:确认代码路径。检查是页面初始化时就转换,还是异步请求回来后又转了一次,避免同一个数组被多次原地修改。
- 第四步:用 3.3 节的自测代码验证转换函数本身是否正确,排除工具函数写错的可能。
- 第五步:在地图上选一个明显地标,同时用两套坐标打点,直接看重合度。
上面这五步走完,绝大多数偏移问题都能定位到原因。我见过最诡异的一次,是线上代码和本地代码不一致,本地上传的数据正常,线上却偏移,最后发现是部署分支里混入了一个旧版本的转换函数,跟坐标系算法没什么关系。
5.3 几个容易忽略的小细节
最后一个部分,分享几个很容易忽略但影响很大的小细节。
- 批量转换时不要原地修改源数组,否则你不知道什么时候数据已经从“天地图坐标”变成了“高德坐标”,后续再想反向转换就乱套了。
- 如果是从后端接口拿坐标,建议让后端在返回数据结构里直接标注坐标系字段,例如
"crs": "cgcs2000",避免前端猜测。 - 高德地图的坐标拾取工具取出来的点,本身就是 GCJ-02,不要用它去验证“天地图坐标是否正确”,要用它验证转换后的结果。
- 当项目里同时存在 Leaflet、OpenLayers、高德等多个地图库时,每个库对坐标的处理方式不一样,最好在数据层统一转成目标坐标系,不在各库内部各自转换。
- 坐标转换函数是纯计算逻辑,不依赖 DOM,可以放在 Web Worker 里做大量点位的预处理,但在实际项目中通常没这个必要。
5.4 合规与服务条款提醒
地图是涉及公共数据的重要载体,使用地图服务和坐标算法时必须遵守平台服务条款和适用的法律法规。本文提供的坐标转换代码,仅用于解决合法的数据兼容开发场景,例如把政务平台提供的规范数据在互联网地图上做可视化展示。我不讨论、也不支持任何规避偏移算法、未授权数据采集、侵犯地图数据版权等用途。大家在项目里使用地图数据时,务必确认数据来源和用途合规,这是地图开发最不能忽视的底线。
结尾
搞过几个地图项目之后,我的体会是:坐标系问题看起来只是“换算一下”,但它往往是集成开发里最隐蔽的拦路虎。天地图坐标和高德坐标之间那几百米的差距,会让一个功能看起来彻底失败,但定位清楚之后,解决方案反而就那几十行 JS。
最后再分享一个小技巧:做地图类的数据接入,一定要养成“先验证坐标系,再大规模处理”的习惯。先用两三个已知点位做往返测试,确认转换关系正确后再处理全量数据。这个习惯帮我省下了很多次返工时间,也希望它能帮到你。