做了几年地图相关的开发,我最大的感受是:地理数据的交换,到最后基本都能收敛成一种格式——GeoJSON。它不是什么复杂的新技术,就是一套用JSON描述点、线、面的公共约定。以前各个系统之间互相传位置数据,要么自己发明字段,要么导出成乱七八糟的文本,到了前端还得写一堆解析器。GeoJSON把坐标、几何形状和业务属性打包成一个结构清晰的对象,从而让后端接口、数据库、前端地图组件都能直接识别。刚接触GIS的前端可以看,后端想给地图接口出数据的同学也能直接上手,用不着去啃完整规范,按这篇文章的思路走两遍基本就能应付日常开发。
1. GeoJSON到底在解决什么问题
1.1 为什么还需要另一种JSON格式
一个普通的JSON接口可以很好地传输用户、订单、商品,因为它表达的是结构化业务数据。但你让它表达“某条地铁线路沿线经过哪些点”就麻烦了。你可以自己设计一个字段叫path,内容是数组;别人也可以叫route,数组里再塞两层对象。字段名不同、嵌套层级不同,前端对接起来就是一场灾难。GeoJSON做的事情就是把空间数据最常用的表达方式固定下来:点用Point,线用LineString,面用Polygon,每个几何对象都自带coordinates。与此同时,它保持了JSON最大的优点——人类可读,且任何语言都能解析。
JSON本身没有空间语义,GeoJSON给JSON加了空间语义。这个“语义”才是关键,它让多个团队可以不用开会对齐字段格式,直接按照同一个规范输出数据。举个例子,后端要返回一个门店铺位,如果自己写“{“lon”: 12.34, “lat”: 56.78}”,前端拿到还要确认lon是longitude还是long;而用GeoJSON标准写“{“type”: “Point”, “coordinates”: [12.34, 56.78]}”,任何地图库都能直接识别。规范统一之后,接口设计、数据校验、可视化落地的成本都会明显下降。
1.2 几何模型和坐标体系
理解GeoJSON,首先要理解它的几何模型。无论一条数据是某个门店的点、一条河流的线,还是一个小区的面,最终都要落到几何类型上。最开始容易搞混的是坐标体系:GeoJSON标准规定坐标使用地理坐标,即经度在前,纬度在后,单位是十进制度。我接手过不少来自第三方平台的数据,它们习惯纬度在前、经度在后,导入GeoJSON之后地图上的点全部跑到不知名位置。原因不是坐标系错误,而是坐标顺序只换了一半。
GeoJSON本身并没有强制规定必须在哪个投影坐标系下使用,但实际生态里几乎默认使用全球通用的经纬度坐标,也就是常说的EPSG:4326。当应用需要做面积、长度计算时,再在后台转换成适合当地的投影坐标系,GeoJSON只负责对外交换。如果你拿着一个已经投影成平面坐标的数据直接转GeoJSON,前端地图库读到的数字意义就变了。因此我习惯把所有对外数据统一成经纬度,计算时再另做投影。这个习惯帮我挡掉了非常多后续排查。
2. 一分钟看懂GeoJSON的核心数据结构
2.1 FeatureCollection与Feature的关系
在最常见的GeoJSON文件中,最外层通常是FeatureCollection。它像一个文件夹,里面放很多Feature;一个Feature就是一份空间要素,例如一栋建筑的坐标多边形和它的名称、用途等属性。FeatureCollection里只有features数组,每个元素是一个Feature。为什么文件夹不能直接放几何?因为真实业务往往需要几何加属性。Feature把二者粘在一起,geometry保存几何数据,properties保存业务属性。地图库的图层加载器,看到FeatureCollection就能把每个Feature映射成一条可点击、可选中的记录。
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [12.34, 56.78] }, "properties": { "id": 1001, "name": "演示点位", "category": "sample" } } ] }这个例子虽然简单,但已经包含了GeoJSON的全部核心结构。properties不限字段,你可以放名称、人口、温度甚至一个对象;但值必须是JSON可序列化的类型,日期和undefined都不建议直接放。如果你需要批量返回几千个点,就把它们全部塞进features数组,前端拿到后循环处理即可。
2.2 几种常见几何类型详解
GeoJSON并不是只有点线面这么简单,它把几何类型拆得比较细。多数日常项目会用到Point、LineString和Polygon,偶尔会遇到MultiPolygon来表达多块不相邻区域。
| 类型 | coordinates结构 | 典型用途 |
|---|---|---|
| Point | [经度, 纬度] | 门店、事件点 |
| LineString | [[经度, 纬度], ...] | 路线、河流 |
| Polygon | [[外层环坐标], [内洞环坐标], ...] | 小区、行政区 |
| MultiPoint | [[经度, 纬度], ...] | 一组散点 |
| MultiLineString | [[[坐标], ...], ...] | 多条线路 |
| MultiPolygon | [[[多边形环]], ...] | 多块不相邻区域 |
| GeometryCollection | geometries数组 | 混合几何 |
从表格能看出坐标的嵌套深度直接反映几何复杂度。Point只有一层数组,Point的coordinates是[数字,数字];LineString是数组的数组;Polygon则变成数组的数组的数组。很多实际报错都来自这里:把Polygon写成了两层,地图渲染时只画出一条线或干脆什么都不显示。新手常犯的错误还包括Polygon没有闭合,也就是第一个坐标点和最后一个坐标点不重合。标准要求线性环必须是闭合的,前端地图库一般会自己补闭合,但后端做空间计算时会直接判定无效。所以在脚本里输出Polygon前,最好自己把坐标闭合。
Multi前缀类型还有一层容易踩的嵌套:MultiPolygon的coordinates最外层是几何数组,每个元素是一个Polygon,因此整体会是四层数组。这个嵌套对新手不太友好,但拆开看其实每个元素就是独立Polygon。如果你只是在做可视化,几何类型越简单越好;实在需要复杂几何,建议先用工具做一次转换和预览,别直接手写。
2.3 properties字段承载业务信息
一个GeoJSON若只有几何,只能画形状,没法做交互。properties就是给形状附上业务灵魂的部分。想在地图上根据状态显示不同颜色,我可以在properties里放status字段;想点击要素弹出面板,就放title、description;要聚合统计,也可以放population或count。GeoJSON规范没有限制字段类型,但实际项目中如果属性值里面有特别大的数组或者长文本,会影响加载速度。我更建议在properties中只放展示和基础交互需要的字段,完整业务详情通过要素id再调一次接口获取。
另一种常见需求是给每个Feature加独立id,很多地图库用它来更新动态图层。规范里id不是必需的,但它对增量更新很重要;最好保证同一次输出的集合里id不重复。我见过有人在properties里放一个很大的JSON对象,前端代码需要深拷贝到状态里,后期刷新效率明显变差。更合理的做法是保持properties扁平,例如直接放id、name、category、status这类标量字段。字段名要稳定,前端样式表达式和弹窗绑定都依赖它,改一个字段名就要同步改前端代码,容易漏。
3. 实操过程与核心环节实现
3.1 从零手写一个最小GeoJSON
在项目里我们不一定每次都生成完整文件,但理解最小结构是调试的基础。我经常用纯文本编辑器创建demo.geojson,内容尽量精简,用一个LineString做测试。这里故意用演示坐标,不指向任何真实地点。
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "路段A", "status": "closed" }, "geometry": { "type": "LineString", "coordinates": [ [12.0, 34.0], [12.1, 34.1], [12.2, 34.2] ] } } ] }写完以后我先做两件事:第一,确认JSON合法;第二,确认要素类型正确。最简单的方式是打开浏览器开发者工具,执行一次JSON.parse并打印,没有报错就说明语法过关。然后我会把文件拖到地图预览页面里,一个能实际显示的地图例,比我反复检查文档强太多。因为每天要处理很多GeoJSON,我习惯保存一份最小模板作为片段,要新增要素时直接复制改坐标,效率高,也不容易丢大括号。第一次接触GeoJSON的人,手写一遍还会发现一个细节:每个Feature必须有type字段,而且geometry内部也要有type和coordinates,少一个地图库都会给出不太友好的报错。
3.2 用脚本生成与解析GeoJSON
业务中手工写GeoJSON不现实,大多数情况是从数据库或者已有矢量数据转出来。我会用脚本做这件事,思路很固定:准备好坐标数组,准备属性字典,然后组装成Python对象,用标准库输出。以下代码不依赖额外地图库,用Python自带的json就能完成。
import json coords = [[12.0, 34.0], [12.1, 34.1], [12.2, 34.2]] properties = { "name": "路段B", "level": 2 } feature = { "type": "Feature", "geometry": { "type": "LineString", "coordinates": coords }, "properties": properties } feature_collection = { "type": "FeatureCollection", "features": [feature] } with open("output.geojson", "w", encoding="utf-8") as f: json.dump(feature_collection, f, ensure_ascii=False)注意json.dump的ensure_ascii要设为False,否则中文会变成一堆\u转义,虽然不违反规范,但可读性差。解析时用json.load读回来。遇到坐标需要转换,我会把经纬度翻转的逻辑写成一个递归函数,遍历coordinates数组,逐个判断如果当前元素是数字则交换,否则继续深入。递归对嵌套层数不确定的MultiPolygon尤其重要。如果项目里已经用了成熟的地理计算库,可以考虑直接调用它来构建几何对象再做serialize,但标准库手工组装对于简单场景完全够用。
3.3 坐标精度、坐标参考与文件体量
写GeoJSON时,我习惯把坐标四舍五入到小数点后6位。换算下来,这个精度大约对应地面的十分之一米,显示和绝大多数业务场景都足够。再往后的小数位只是增加字符数,不会带来可见的精度提升,反而会让文件变大。但有一条原则:计算过程保留原始全精度,只对对外输出的GeoJSON做格式化。文件体量和坐标数量呈线性关系,几万个Point的FeatureCollection轻松超过几十MB。我会使用gzip压缩,线上传输可以降到原来的五分之一甚至更低。
对于超大区域数据集,可以考虑减少顶点密度、抽稀或再转换成更节省空间的瓦片格式。此外,文件里如果写入了坐标系字段,对外应用通常只认WGS84经纬度,所以输出前统一用EPSG:4326作为交换标准。不同区域需要投影坐标时,只用于本地计算,不用于GeoJSON交换。我还遇到过有人把测量坐标直接写进GeoJSON,数字倒是很大,但地图上怎么都不显示。本质是参考系错了,不是格式错了。GeoJSON的协作价值就在于大家都用同一把尺子,尺子不统一,后面的处理链条全乱。
4. 应用场景与实践落地
4.1 前端可视化:把GeoJSON变成地图上的图形
后端返回GeoJSON,前端要做的事情很简单:把数据交给地图库,让地图库负责逐要素绘制。一个轻量级的前端地图库加载FeatureCollection时,几乎不需要额外配置,默认把所有要素画在同一图层上。我的经验是,对于点要素,给它设置一个基于属性值的圆形样式,比如用status字段控制颜色;对于线和面,单独设置描边与填充透明度。这个阶段的坑多数在数据不在代码,比如要素只有geometry但没有properties,地图库可以显示,但点击弹窗时拿不到内容。所以我在后端生成GeoJSON时,一定会保证每个Feature都有properties;哪怕里面只有一个id。
属性字段命名也要保持稳定,前端样式表达式依赖字段名。另一个容易被忽略的点是坐标数量:一个面要素如果包含几千个顶点,拖动地图时会明显卡顿。可视化之前可以做一次顶点抽稀,保留边界特征即可。地图上叠加多个GeoJSON图层时,我会给每个图层起清晰的名字,方便控制显示开关。总之,前端只负责消费标准数据,不要在前端做复杂的坐标拼接和属性重组,那样只会把问题隐藏起来。
4.2 后端存储与格式转换
GeoJSON不仅是传输格式,很多时候也直接作为存储格式。我见过团队把FeatureCollection整包塞进文档数据库,也见过将其解析后导入带有空间索引的数据库。前者的优点是写入零成本,缺点是修改其中一条要素必须重写整个集合,所以我更推荐空间数据库。导入时要注意坐标系,最佳实践是先建立带有空间参考的字段,再把GeoJSON的coordinates数组映射进去。从其他格式转换到GeoJSON同理,经典流程分三步:第一步检查原始数据的坐标系;第二步把原始数据重投影到WGS84经纬度;第三步导出为GeoJSON。
凡是跳过第一步的,后期发现位置偏移,基本都要返工。如果原始数据是Shapefile或CAD,转换工具可以选择能查看要素属性和预览几何的开源GIS软件,右键导出即可。导出前我会顺手看一眼属性表,剔除那些带非法日期或空值的字段,避免前端拿到不可序列化的值。后端接口返回GeoJSON时,最好设置正确的Content-Type,并允许前端跨域访问;如果数据量大,加上范围过滤参数,让前端只请求当前视野内的要素。这样一个接口可以同时服务多个地图页面,减少重复开发。
4.3 地图对接与在线协作
GeoJSON的另一个优势是它可以直接拖进许多地图平台的编辑器中。我做模拟项目X时,需要把巡检路线分享给同事,做法很简单:生成一个GeoJSON文件,传给同事后,他直接拖到某个在线地图数据页面,路线立刻覆盖在上面,还能导出不同样式。这种基于标准格式的协作,省掉了连环截图和反复描述坐标的过程。企业级系统还可以用GeoJSON做后端渲染:地图服务器读取空间数据库,按照请求范围输出矢量要素,前端只负责画,后端负责按需裁剪。这套方案能支撑较大的数据量,核心仍然是GeoJSON作为交换标准。
5. 常见问题与排查技巧实录
5.1 坐标顺序颠倒:最常见也最隐蔽
这个坑我至少见过三次,每次症状都很一致:点跑到地图上的海里或某个错误方向,用鼠标点击位置看到的数字和数据库里明显对不上。GeoJSON标准明确是经度、纬度,但很多人受坐标文本里“lat, lon”习惯的影响,写成了纬度、经度。排查方法很简单,打印一个点的原始坐标,对照经度是否在正常区间。大多数正常经度范围在-180到180,纬度在-90到90。如果看到数据第一个数字是“30.5”,第二个是“120.2”,那大概率反了。
解决时可以写一个递归转换函数,把第一层和第二层数字交换,再输出调整后的数据。我个人的习惯是后端接口在出口统一加一个转换开关:默认输出GeoJSON时强制坐标顺序,同时提供一个兼容查询参数允许外部传入不同顺序。这样既能保证内部规范,又不耽误对接上游数据。坐标顺序问题看起来小,实际破坏力极强,因为它不会报错,只会让结果显示得离谱,排查起来比较费时间。
5.2 文件不显示或一片空白
地图上不显示要素,九个原因里有八个出在数据结构而不是地图配置。先加载一个硬编码的Point Feature,如果这个能显示,再逐步替换成真实数据。若最小Point都显示不出来,多半是坐标无效或者地图容器高度为0。真实数据替换后仍然一片空白,就检查文件是不是只有单个Feature而没有外包FeatureCollection,或者geometry为null。在前端控制台打印报错最能说明问题。
还有一批原因是JSON语法不标准:尾逗号、单引号、注释。这些在JavaScript对象里能过,但严格JSON解析器会拒绝。比较快的排查方式是把文件内容放进编辑器格式化,语法错误会立刻标红。此外,要素样式透明度或描边为0也会造成看不见,但这是业务配置问题,不是数据问题。我建议把“最小可复现文件”作为排查第一步,不要直接在几千行的GeoJSON里翻找,那会很痛苦。
5.3 大文件加载卡顿与优化策略
一次展示几千个Feature还好,上万甚至十万个点直接加载时,浏览器可能卡到无法操作。我给出四个递进方案:第一,前端只保留当前视野内的要素,用地图库的边界判断方法过滤;第二,后端提供空间查询参数,按坐标范围动态返回;第三,对线面做抽稀处理,减少顶点数;第四,如果数据是静态热力场景,后端直接聚合成网格,不必返回每个点。压缩传输也值得做,开启gzip就能让带宽占用明显下降。
在优化之前,先量化瓶颈。别急着上复杂方案,用控制台看接口返回耗时、解析JSON耗时、渲染耗时。很多时候瓶颈是渲染而不是解析,优化步骤于是集中在减少图层元素和样式重绘上。如果数据还要做点击交互,我会先做空间索引。大体量点数据的前端渲染本身就有上限,与其硬扛,不如从数据源头就按区域分块输出。GeoJSON的优势是标准开放,缺点是纯文本冗余度高,做到一定量之后一定要配合裁剪、压缩和抽稀,否则体验数据再规范也很难用。
5.4 边界、拓扑和空间计算
GeoJSON天然不保证相邻要素共享边界。两个相邻小区,各自画了一个Polygon,理论上公共线段应该完全重合,但实际数据中一边坐标略微偏移或者反向绘制,就会形成裂缝或重叠。做数据发布前,我建议在GIS软件里做一次拓扑检查,把公共边对齐。如果只是做可视化,裂缝肉眼可能看不到;但如果做叠加分析,就会产生错误的相交面积。
另一个容易忽视的问题是空间计算单位。经纬度坐标不能直接拿来算长度和面积,必须转换到合适的投影坐标系,或者使用球面距离公式。否则在低纬度地区还算接近,到了高纬度误差会大到离谱。GeoJSON本身只是文件格式,不负责空间分析,理解这一点,很多所谓精度错误都能定位到“算错投影”而不是“GeoJSON写错”。我做数据分析时,通常把GeoJSON作为输入,转成带投影坐标的临时数据后计算,算完再转回经纬度输出,这样两头都干净。
回顾整个使用经历,我最想分享的一句经验是:GeoJSON不难,真正难的是团队里每个人对坐标顺序、坐标参考和要素结构的理解保持相同。刚开始做地图数据时,我在一个模拟项目里反复遇到位置偏移,后来发现是同事导出时用了纬度经度排序,而前端默认按经度纬度解析。从那以后,我在所有接口文档和代码注释里都加了一条醒目的约定。这个约定长得像一行注释,但它让我们在后续数据对接时再没有出现过因为坐标顺序导致的集体返工。一个格式规范如果细节走样,带来的技术债往往几十个文件一起偿还。GeoJSON给了一个相对稳定的基座,剩下的是流程纪律。