做了几年地图相关的开发我最大的感受是地理数据的交换到最后基本都能收敛成一种格式——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[[[多边形环]], ...]多块不相邻区域GeometryCollectiongeometries数组混合几何从表格能看出坐标的嵌套深度直接反映几何复杂度。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, encodingutf-8) as f: json.dump(feature_collection, f, ensure_asciiFalse)注意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给了一个相对稳定的基座剩下的是流程纪律。
阅读完成 · 觉得有帮助?