先聊点实在的。我最早接触GeoJSON,是被一个项目逼的:后端给了个KML文件,要我在网页地图上画出来,前端地图库读KML又慢又别扭,最后绕了一圈,把所有数据统一转成GeoJSON,问题一下解决了。从那以后,凡是涉及地理数据的传输、展示、交换,我第一个想到的就是它。这篇GeoJSON快速入门教程,就是把我这几年用这个格式总结下来的要点、工具、坑位一次说清楚。不管你是前端开发、GIS初学者,还是手头有一批带坐标的数据想画成地图,这篇文章都适用,而且我会尽量讲得直白,不整太多虚的。
1. 先搞懂GeoJSON是干什么的,再动手学
1.1 它解决的是什么样的“格式沟通”问题
很多人第一次听到GeoJSON,会以为它是一个软件或者某个库。其实不是,它就是一个基于JSON的地理空间数据交换格式,2016年8月成了RFC 7946标准。你可以把它理解成地理信息领域的“通用话术”:大家语言不同、系统不同、数据库不同,但只要都按这个格式说话,就能互相听懂。
举个例子。你在数据库里存了一堆门店地址,每个店有名字、经纬度、营业时间。要想在网页上把这些门店标出来,传统方案是把数据导成表格,再写代码逐行解析;要是数据来源是个旧的GIS系统,导出来是Shapefile,前端根本没法直接用。GeoJSON的方案是统一包一层:把经纬度坐标和业务属性放在同一个JSON文件里,前端拿到之后直接解析渲染,后端也能直接读写接口传参。它天然继承了JSON的优点:纯文本、结构清晰、任何语言都能解析,而且体积比Shapefile那种二进制格式更容易在Web场景下传输。
所以它核心解决的是多系统之间地理数据的标准化传递问题。你不需要关心数据原本存在Oracle Spatial还是PostGIS,也不需要关心对方用的是QGIS还是ArcGIS,只要导出成GeoJSON,剩下的事就是读取和展示了。
1.2 一张图看懂三层核心结构
GeoJSON的数据结构看起来有一堆嵌套,但拆开看其实只有三个层级,想清楚这三个概念,后面的内容就顺了。
第一层叫Geometry(几何对象),它只描述“形状在哪”,不带任何业务含义。比如一个Point对象,里面就一个coordinates数组,告诉你某个点的经纬度。它就像一个没有名字、没有任何背景的坐标点。
第二层叫Feature(要素),它把一个几何对象和一个属性表绑在一起。结构上分成两个字段:geometry存形状,properties存属性。比如一个Feature可以是“西湖边某个公园入口”,geometry是那个入口的坐标,properties里可以写name、type、open_time等等,想带多少字段就带多少。
第三层叫FeatureCollection(要素集合),它就是把一堆Feature打包成一个数组。传输的时候通常用这一层作为最外层,一个文件里装几百个点、几千条线都没问题。
我用一个生活化类比帮你记:Geometry是一块“积木”,Feature是“贴了标签的积木”,FeatureCollection就是“一盒贴好标签、可以直接交货的积木套装”。前端拿到这盒积木,拆开就能拼。
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "示例点" }, "geometry": { "type": "Point", "coordinates": [120.1521, 30.2587] } } ] }1.3 六种Geometry类型:先知道它们长什么样
GeoJSON一共定义了六种几何类型,绝大多数使用场景都覆盖了。我先快速列一遍,有个印象就行,后面实操会反复用到。
- Point(点):单个坐标点,coordinates是一个二元数组,比如[120.15, 30.26]。
- MultiPoint(多点):多个点的集合,coordinates是点的数组。
- LineString(线):一串有序坐标连成的线,比如一条骑行路线,coordinates是坐标数组。
- MultiLineString(多线):多条线的集合。
- Polygon(面):由闭合环围成的区域,coordinates是“环的数组”,每个环又是坐标数组。第一个环是外边界,后面的环是“洞”。
- MultiPolygon(多面):多个面的集合。
还有一个GeometryCollection(几何集合),允许在同一个geometry里混合多种形状,比如一片区域里既有面又有线。实际用得相对少,但要知道它存在。
这一串类型不需要死记,你只需要记住:Point、LineString、Polygon是老三样,Multi前缀就是“多个”,Polygon由闭合环构成。后面写文件的时候你还得注意一个细节——Polygon是数组套数组的结构,因为每个多边形可能带洞。这个坑很多人第一次写都会踩,我会在第2部分详细说。
2. 亲手写一个GeoJSON文件,并找到趁手的打开工具
2.1 从最简单的点要素开始手写
学习这个方法最直接的办法就是打开文本编辑器,手写一个文件。不用怕写错,JSON格式本来就是给人看的,写错了马上就能看出来。
打开任意编辑器,新建一个文件,保存为first.geojson。注意两个点:第一,文件编码必须是UTF-8,否则中文属性名会乱码;第二,GeoJSON本身就是JSON,所以不允许写注释。
写一个点要素:
{ "type": "Feature", "properties": { "name": "示例公园入口", "category": "park" }, "geometry": { "type": "Point", "coordinates": [120.1521, 30.2587] } }这个文件就表达了一个意思:有一个公园入口,位置在经度120.1521、纬度30.2587。
这里我要特意强调一个非常关键的细节:GeoJSON的坐标顺序是“经度在前,纬度在后”,也就是[longitude, latitude],不是我们口语中常说的“纬度,经度”。为什么要这样定?因为地理学上习惯用(x, y)表达平面位置,x对应经度,y对应纬度,和数学坐标系保持一致。这个顺序是RFC 7946标准里明确定死的。我第一次写的时候就习惯性写成了[30.2587, 120.1521],结果点位直接跑到了非洲附近,排查了半天才发现是坐标顺序反了。
写完这个文件之后,你可以在浏览器地址栏里输入file:///你的路径/first.geojson,但浏览器大概率会直接把它当文本显示,因为浏览器不内置GeoJSON渲染器。想要看到实际效果,得靠下面的工具。
2.2 线和面怎么写:闭合环与内洞
搞定点之后,再写一个线要素和面要素,这一节的难点集中在Polygon上。
线要素就是LineString,coordinates是坐标点的数组,每个点按顺序连接成一条线:
{ "type": "Feature", "properties": { "name": "示例骑行路线" }, "geometry": { "type": "LineString", "coordinates": [ [120.1485, 30.2550], [120.1560, 30.2585], [120.1680, 30.2610] ] } }面要素就要多啰嗦两句。Polygon的coordinates是一个三维数组:第一层是一个数组,里面放了若干个环,每个环又是一个坐标数组。第一个环是外边界,后面的环是内环,也就是“洞”。而且每个环都必须闭合——首尾两个坐标必须完全相等,这样才形成一个封闭区域。
一个简单矩形面:
{ "type": "Feature", "properties": { "name": "示例绿地" }, "geometry": { "type": "Polygon", "coordinates": [ [ [120.1200, 30.2700], [120.1300, 30.2700], [120.1300, 30.2800], [120.1200, 30.2800], [120.1200, 30.2700] ] ] } }看到最后一个点和第一个点一样了吗?这个闭合是硬性要求。如果你画了一个不闭合的Polygon,严格来说它不符合规范,很多渲染引擎会画出奇怪的形状或直接不渲染。
如果要带一个“洞”,就在外环后面再塞一个环:
"coordinates": [ [ [120.1200, 30.2700], [120.1300, 30.2700], [120.1300, 30.2800], [120.1200, 30.2800], [120.1200, 30.2700] ], [ [120.1230, 30.2730], [120.1230, 30.2750], [120.1250, 30.2750], [120.1250, 30.2730], [120.1230, 30.2730] ] ]这个结构相当于一个绿地里挖了一个小池塘。注意“洞”的环也必须闭合,而且“洞”必须完全落在“外环”内部,否则就是一个非法多边形。关于环的方向(顺时针还是逆时针),RFC 7946里建议外环逆时针、内环顺时针,但很多渲染工具并不严格校验。不过为了兼容性,建议你按规范来写,避免在某些严格的引擎上出错。
2.3 用在线工具画图、校验一条龙
手写文件适合理解结构,但真正干活的时候,没人愿意手动敲几千个坐标。这里推荐两个在线工具,是我每次调试GeoJSON都会用的:
第一个是geojson.io。这个网站相当于一个小型在线地图编辑器,左边是地图,右边是GeoJSON源码。你可以在左边的地图上直接点一下画一个点,点几下画一条线,或者用绘图工具拉一个面。每次操作,右边的代码会实时更新;反过来,你在右边粘贴一段GeoJSON,左边也会立刻显示图形。它还支持直接拖入本地文件打开,编辑完可以导出成GeoJSON或CSV。做数据调试、快速预览,用它最省事。
第二个是geojsonlint.com。这个网站专门做格式校验。你把GeoJSON贴进去,它会告诉你格式对不对、哪里不符合RFC标准。很多时候你看代码半天看不出问题,往这上面一贴,错误行号直接标出来。
我常用的工作流是:先在geojson.io里手工画草稿,导出文件;再在geojsonlint里检查一遍;没问题之后再丢给前端或后端使用。这个流程基本能过滤掉90%的低级格式错误。
2.4 GeoJSON用什么软件打开?工具清单
“geojson用什么软件打开”是我经常被问到的问题。GeoJSON是纯文本格式,理论上记事本就能“打开”,但你大概率想看的是图形化展示。我把常用的几类工具整理成一张表:
| 工具/平台 | 类型 | 适合场景 | 操作说明 |
|---|---|---|---|
| geojson.io | 在线工具 | 快速预览、临摹画图 | 打开网页,拖入.geojson文件即可显示 |
| QGIS | 桌面GIS软件 | 复杂地理分析、图层叠加 | 菜单Layer -> Add Layer -> Add Vector Layer,选择GeoJSON文件 |
| Kepler.gl | 在线可视化工具 | 大规模数据点、轨迹可视化 | 打开官网,Upload Data选择文件,支持上千个点流畅渲染 |
| Mapshaper | 在线工具 | 数据简化、格式转换 | 直接把文件拖进浏览器,可以查看和简化坐标 |
| Leaflet + 自定义JS | 前端开发 | 嵌入自己网页的地图 | 用L.geoJSON()方法加载,后面第3部分细讲 |
| Tableau / Power BI | 商业智能工具 | 业务分析场景结合地图 | 连接数据时选择GeoJSON或把坐标字段放入地图维度 |
如果你只是想“看一眼文件长什么样”,直接双击用默认文本编辑器打开也行,代码会以纯文本形式展示。但想看到地图效果、叠加底图,我还是推荐geojson.io,因为它不需要安装任何东西,浏览器一开就能用,而且对新手最友好。
3. 把GeoJSON用起来:前端渲染、格式转换与数据入库
3.1 用Leaflet 10行代码把数据画上地图
GeoJSON最大的优势在于Web端——JSON是JavaScript的原生格式,前端拿到数据几乎不需要二次转换。这里我用Leaflet做demo,因为它体积小、上手快,全世界的开发者在网上留下过大量示例。
先搭一个最简页面,引入Leaflet的CSS和JS:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> </head> <body> <div id="map" style="height: 600px;"></div> </body> </html>然后写JavaScript,创建地图并加载GeoJSON:
const map = L.map('map').setView([30.25, 120.15], 12); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map); fetch('first.geojson') .then(response => response.json()) .then(data => { L.geoJSON(data).addTo(map); });这段代码干了几件事:创建地图实例,加载一份OpenStreetMap的瓦片底图,然后通过网络请求读取GeoJSON文件,最后用L.geoJSON(data)把文件里所有要素画到地图上。整个过程不需要写任何循环,因为Leaflet已经处理了FeatureCollection的遍历和渲染。
如果你不想折腾本地服务器,也可以直接把GeoJSON变量写在页面里:
const geoJsonData = { "type": "Feature", "properties": { "name": "示例点" }, "geometry": { "type": "Point", "coordinates": [120.1521, 30.2587] } }; L.geoJSON(geoJsonData).addTo(map);3.2 按属性渲染样式:让地图会说话
把点画上地图只是第一步,实际项目中,你常常希望“不同属性的点显示不同颜色”。比如公园显示绿色、学校显示蓝色,或者按人流量从高到低用红黄绿渐变区分。
Leaflet的L.geoJSON()支持传入一个options对象,在style函数里针对每个Feature动态返回样式,在onEachFeature里给每个要素绑定事件或弹窗:
L.geoJSON(data, { pointToLayer: function (feature, latlng) { return L.circleMarker(latlng, { radius: 8, fillOpacity: 0.8 }); }, style: function (feature) { const category = feature.properties.category; return { color: category === 'park' ? '#2ecc71' : '#3498db', fillColor: category === 'park' ? '#2ecc71' : '#3498db', weight: 1 }; }, onEachFeature: function (feature, layer) { layer.bindPopup( '名称:' + feature.properties.name + '<br>类别:' + feature.properties.category ); } }).addTo(map);这段代码的pointToLayer把点要素渲染成圆点而不是默认的图标,style根据properties里的category字段切换颜色,onEachFeature给每个要素绑定了点击弹窗。这里的关键思路是:GeoJSON里的properties就是你的业务数据,它的自由度决定了地图图层能表达多丰富的信息。所以你在设计GeoJSON文件时,不要吝啬属性字段,尽量把展示需要用到的信息都放到properties里。
需要提醒一个细节:不同渲染引擎对样式的字段名要求不同。Leaflet用的是color、weight、fillColor这一套;Mapbox GL则用line-color、line-width、fill-color。如果你在Mapbox里用了Leaflet的字段名,样式会不生效。通用做法是——先查对应引擎的文档,或者用feature-state这类机制动态设置。
3.3 从Shapefile转GeoJSON:ogr2ogr与Docker环境
现实工作里,很多坐标数据不是在GeoJSON里,而是在Shapefile、KML、GPX甚至CSV里。转换最常用的工具是GDAL全家桶里的ogr2ogr命令行,它是社区公认的地理数据格式转换瑞士军刀。
基本转换命令非常简单:
ogr2ogr -f GeoJSON output.geojson input.shp这条命令会把input.shp转成output.geojson。如果你有一个CSV文件,里面有经度、纬度两列,也可以这样操作:
ogr2ogr -f GeoJSON output.geojson input.csv -oo X_POSSIBLE_NAMES=lng -oo Y_POSSIBLE_NAMES=lat-oo X_POSSIBLE_NAMES和-oo Y_POSSIBLE_NAMES指定CSV哪一列是经度、哪一列是纬度。
不过GDAL的安装有时比较折腾,不同系统依赖不同。这里有个偷懒方案:用Docker拉一个gdal镜像,直接跑容器命令,本地连安装都省了。
docker pull ghcr.io/osgeo/gdal:latest docker run --rm -v $(pwd):/data ghcr.io/osgeo/gdal:latest ogr2ogr -f GeoJSON /data/output.geojson /data/input.shp-v $(pwd):/data把当前目录挂载进容器,这样容器里就能访问到本地的input.shp了。这个方法对Windows、macOS、Linux通用,而且环境始终保持一致,不会出现“我电脑上能跑,你电脑上报错”的问题。
还有一个更轻量的替代方案:QGIS桌面软件自带“导出图层为GeoJSON”的功能,如果你习惯了图形界面,鼠标点几下就能完成转换,不需要碰命令行。我一般是这样分工的:批量转换、自动化脚本用ogr2ogr,临时单文件转换就直接在QGIS里导出。
3.4 用DuckDB和Python读GeoJSON做分析
GeoJSON不仅是拿来“看”的,也可以拿来“算”。当你的GeoJSON文件有成百上千个要素时,用代码做统计、按属性筛选、算范围覆盖之类的操作就变得很有必要。
先说说最近热度很高的DuckDB。它本身是一个轻量级分析型数据库,安装简单,单文件运行,而且官方提供了spatial扩展,可以直接读取GeoJSON文件,把它当普通表查询。这也是DuckDB入门教程里常常出现GeoJSON的原因——数据分析师终于不用先把坐标数据导入PostGIS,再跑SQL了。
INSTALL spatial; LOAD spatial; SELECT properties.name, ST_NPoints(geometry) AS point_count FROM ST_Read('first.geojson');这段SQL里,ST_Read函数直接读取GeoJSON文件,返回一个关系表;properties.name可以直接作为列名访问;ST_NPoints是计算一个几何对象包含多少个点的空间函数。你还可以在SQL里写WHERE properties.category = 'park'筛选公园,或者用ST_Area计算面积。DuckDB这类分析数据库的好处是:你不用写复杂的解析代码,SQL就能把文件里的各种信息榨出来,对数据分析场景特别友好。
如果你更习惯Python生态,直接用GeoPandas读文件,然后就是一个DataFrame,后续处理怎么顺手怎么来:
import geopandas as gpd gdf = gpd.read_file('first.geojson') print(gdf.head()) print(gdf.crs)GeoPandas会返回一个GeoDataFrame,每一行是一个Feature,geometry列存的是几何对象,其他列是properties里的属性。你可以用gdf[gdf['category'] == 'park']做筛选,也可以调用gdf.plot()直接出一个静态地图图。实测下来,几千个点的GeoJSON文件在Python里做分析基本无压力,计算和筛选都是毫秒级。
顺带一提,在阿里DataV这类可视化平台做数据接入时,GeoJSON也是前端传递地理数据的常用格式,它会配合组件读取GeoJSON里的properties和coordinates来渲染地图图层。本质上和前端Leaflet的思路一致:数据结构约定好了,剩下的就是各取所需。
4. 避坑手册:我踩过的GeoJSON常见问题
4.1 坐标顺序反了,点位跑到海里
这是新手最高频的问题,也是我自己摔得最惨的一次。现象是:明明数据看起来没问题,画到地图上点位却跑到了完全不对的地方,比如一个北京的坐标画出来跑到了非洲西海岸。
根因就是经纬度顺序写反了。GeoJSON要求[经度, 纬度],但很多数据源、接口、表格里存储的顺序可能是[纬度, 经度]。尤其某些从国内数据源拿到的数据,习惯用“纬度,经度”记录,你没留意就直接丢进GeoJSON,结果自然全乱。
排查方法很简单:看坐标数值区间。经度有效范围是-180到180,纬度是-90到90。如果看到一个坐标是[39.9, 116.4],那基本可以断定是反的,因为39.9明显是北京的纬度,116.4是经度。写成[116.4, 39.9]才对。
如果你用Python处理这类数据,可以写个判断自动纠正:
def fix_lng_lat(coord): lng, lat = coord if -90 <= lng <= 90 and -180 <= lat <= 180: return [lat, lng] return [lng, lat]4.2 坐标系不统一,偏移几百米
另一个频繁踩坑的问题是“偏移”。你拿到的坐标明明很准确,画到网页地图上却偏了几百米。大多数情况下是坐标系基准不一致导致的。
制图过程中常见的坐标系包括WGS84以及用于国内地图服务商加密坐标体系等。不同体系的坐标差异在小范围看是几百米的平移偏差,视觉上非常明显。底层原因我并不展开讲,你只需要记住一个实操经验:先确认你的数据源和底图到底属于哪个坐标系。
比如你在数据文件里标注的是WGS84经纬度,但底图用的是加密坐标,点位就会偏移。反过来也一样。处理方法有两个:一种是在数据源头转换坐标系,输出统一基准的坐标;另一种是数据文件不动,在渲染代码里调用坐标纠偏工具,把坐标转换后再画到地图上。这类工具在Python生态里很好找,前端也有现成实现。但有个原则:同一份GeoJSON文件里,所有坐标的坐标系必须一致,绝不混用,否则你画出来的图一定乱成一团。
4.3 多边形画出来一团乱
多边形渲染出现乱象,一般是下面几个原因:
第一,环没有闭合。前面说过,外环和内环都要首尾坐标相同。如果漏了最后一个重复点,不同渲染引擎的处理方式不一样,有的会帮你补上,有的会画一条隐形的边,看起来形状就怪了。
第二,多个多边形的首尾顺序错了。Polygon的coordinates第一层是多边形列表,如果你需要表示两个不相邻的独立区域,应该用MultiPolygon而不是一个Polygon里塞两个外环。一个Polygon里如果有两个外环,引擎会默认后一个环是“洞”,结果就是你本来要画两块地,显示出来变成一块地上挖了块洞。
第三,环自相交。自相交在多边形里非常隐蔽,坐标看着没问题,但两条边交叉了,填充区域会出现不规则的“蝴蝶结”形状。修正常用工具是Mapshaper,把GeoJSON拖进去以后,控制台执行fix相关命令,可以自动修复部分拓扑错误。
最关键的还是验证:先把多边形贴到geojsonlint这类工具里检查,再渲染。不要等到前端画出来才发现问题,那个排查成本就高了。
4.4 文件太大,加载卡成PPT
GeoJSON是纯文本,坐标一多体积就膨胀。一个包含几万个点的FeatureCollection,文件动辄几十MB,前端加载后地图拖拽会明显卡顿。
解决办法有三个方向。第一是简化坐标。对于线面数据,用Douglas-Peucker这类算法抽稀,减少坐标点数量,肉眼几乎看不出差别。Mapshaper里simplify功能就是干这个的,拖进去调一下百分比就能导出精简版。第二是换格式。如果数据量确实大,可以把GeoJSON转成TopoJSON,后者利用弧线共享可以大幅压缩体积,尤其是大量相邻多边形的情况,体积能缩小一半以上。第三是分块加载。不要一次性把整个文件丢给前端,而是按瓦片或按区域动态请求,配合后端做裁剪。实际项目中,我会先用规则判断数据量:少于1万个点用完整GeoJSON,10万以上点就必须考虑服务端分片或改用矢量瓦片方案。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 快速解决办法 |
|---|---|---|
| 点位置偏到另一个大洲 | 经纬度顺序写反 | 检查数值范围,确保经度在前 |
| 点位整体偏移几百米 | 坐标系基准不一致 | 统一数据与底图的坐标系,必要时做坐标转换 |
| 多边形显示成有洞或乱块 | 环未闭合、自相交、独立区域错误放入同一Polygon | 用geojsonlint/Mapshaper校验修复,独立区域改用MultiPolygon |
| 文件大、加载卡 | 坐标点过多、无简化 | 用Mapshaper简化,或转TopoJSON,或做分片加载 |
| 中文属性乱码 | 文件编码不是UTF-8 | 保存时指定UTF-8,可用geojson.io重新保存 |
| 前端样式不生效 | 使用了错误的样式字段名 | 查询对应渲染引擎的样式规范,按字段名填写 |
我个人在实际操作中的体会是:GeoJSON这个格式最大的价值不是某个工具,而是把地理数据拉到了一个“任何开发者都能轻松操作”的层面。你不需要成为GIS专家,也能在网页上做出一张还不错的交互地图。学习路线其实很简单:先从手写一个点开始,然后用geojson.io画图,再跑通Leaflet渲染,最后掌握ogr2ogr做转换,四步走完基本就入门了。
最后再分享一个小技巧:开发调试阶段,尽量用本地小文件,不要一上来就接全量数据;把数据简化到能复现问题的程度,排查会快很多。等你把整个链路跑顺了,再上全量数据也不迟。