GeoJSON 与 WKT 互转:把前端边界数据送进空间数据库
--- title: GeoJSON 与 WKT 互转:把前端边界数据送进空间数据库 ---
前端地图几乎都用 GeoJSON 描述边界,但一旦要把数据落库做空间查询,主流数据库(PostGIS、MySQL 空间扩展、SQLite 的 SpatiaLite)认的不是 GeoJSON,而是 WKT(Well-Known Text,OGC 制定的空间文本标准)。这篇聊聊 GeoJSON 和 WKT 怎么互相转,并给出能直接跑的 JavaScript 代码。
WKT 到底是什么
WKT 是一种用纯文本表示几何的规范,由 OGC 制定。和 GeoJSON 不同,它不包含任何属性,只描述"形状":点、线、面怎么摆。一个 Polygon 在 WKT 里长这样:
POLYGON ((116 39, 116 40, 117 40, 116 39))
坐标写法是 经度 纬度(空格分隔),外面套两层括号。GeoJSON 的三层结构和坐标顺序,可以回顾 GeoJSON 到底是什么 那篇。
为什么要互转
两个常见场景:
1. 采集入库。用户在地图上画了一个区,前端拿到的是 GeoJSON,要写进 PostGIS 得先变成 WKT。 2. 查询出库。数据库里用 ST_AsText(geom) 取出的也是 WKT,前端渲染前得转回 GeoJSON。
本系列讲过的 GeoJSON 格式转换(KML/SVG/CSV/PNG)是"出图格式",WKT 是"数据库格式"这一支,正好补上。
GeoJSON → WKT
一个函数覆盖 Point、LineString、Polygon、MultiPolygon,也认 Feature 和 FeatureCollection:
// GeoJSON 几何 → WKT 字符串
function coordsToWkt(type, coordinates) {
const pair = c => `${c[0]} ${c[1]}`; // [lon, lat] → "lon lat"
const ring = r => '(' + r.map(pair).join(', ') + ')';
switch (type) {
case 'Point': return `POINT (${pair(coordinates)})`;
case 'LineString': return `LINESTRING (${coordinates.map(pair).join(', ')})`;
case 'Polygon': return `POLYGON (${coordinates.map(ring).join(', ')})`;
case 'MultiPolygon': return `MULTIPOLYGON (${coordinates.map(p => '(' + p.map(ring).join(', ') + ')').join(', ')})`;
default: throw new Error('暂不支持的几何类型: ' + type);
}
}
function geojsonToWkt(geojson) {
if (geojson.type === 'Feature') return coordsToWkt(geojson.geometry.type, geojson.geometry.coordinates);
if (geojson.type === 'FeatureCollection') {
return geojson.features.map(f => coordsToWkt(f.geometry.type, f.geometry.coordinates)).join('; ');
}
return coordsToWkt(geojson.type, geojson.coordinates);
}
// 天安门一个点
console.log(geojsonToWkt({ type: 'Point', coordinates: [116.397428, 39.90923] }));
// → POINT (116.397428 39.90923)
WKT → GeoJSON
反向更绕,因为 WKT 的括号是嵌套的。下面用一个"按顶层逗号切分"的小函数处理 Polygon 和 MultiPolygon:
function wktToGeojson(wkt) {
const m = wkt.trim().match(/^(\w+)\s*\(([\s\S]*)\)$/);
if (!m) throw new Error('无法解析的 WKT: ' + wkt);
const tag = m[1].toUpperCase();
const body = m[2];
// 按顶层逗号切分(忽略括号内的逗号)
const splitTop = s => {
const out = []; let depth = 0, cur = '';
for (const ch of s) {
if (ch === '(') depth++;
if (ch === ')') depth--;
if (ch === ',' && depth === 0) { out.push(cur); cur = ''; }
else cur += ch;
}
if (cur) out.push(cur);
return out;
};
const toCoord = s => s.trim().split(/\s+/).map(Number); // "lon lat" → [lon, lat]
const toRing = s => s.trim().replace(/^\(|\)$/g, '').split(',').map(toCoord);
if (tag === 'POINT') return { type: 'Point', coordinates: toCoord(body) };
if (tag === 'LINESTRING') return { type: 'LineString', coordinates: splitTop(body).map(toCoord) };
if (tag === 'POLYGON') return { type: 'Polygon', coordinates: splitTop(body).map(toRing) };
if (tag === 'MULTIPOLYGON') {
return { type: 'MultiPolygon', coordinates: splitTop(body).map(poly => poly.trim().slice(1, -1).split('),(').map(toRing)) };
}
throw new Error('暂不支持的几何类型: ' + tag);
}
落到 PostGIS
WKT 不带坐标系信息(没有 SRID),所以建表和写入时一定把 4326(WGS84)显式写上,否则数据库不知道这是经纬度:
-- 建表:geometry 列指定 SRID 4326(WGS84)
CREATE TABLE districts (
id serial PRIMARY KEY,
name text,
geom geometry(Polygon, 4326)
);
-- 把 GeoJSON→WKT 的结果写进去(务必带 SRID)
INSERT INTO districts (name, geom)
VALUES ('示例区', ST_GeomFromText('POLYGON ((116 39, 116 40, 117 40, 116 39))', 4326));
-- 反向:从库里取 WKT 交给前端
SELECT name, ST_AsText(geom) FROM districts;
坐标系的事,GeoJSONcn 的坐标系文章 专门写过:边界统一存 WGS84,落库时 SRID 也别丢。
性能提醒
如果你手上是 TopoJSON(本系列讲过的瘦身方案),得先还原成 GeoJSON 再走上面的转换。另外,全国 4.6 万页的行政区划边界,别在请求里现转,生成期转好存进数据库,查询时直接 ST_Within 做空间关系判断,配合 点在多边形内判定 就能做围栏。
WKT 是纯文本,体积小、传输省,做数据库之间的几何交换格式很合适。