PostGIS 完全指南 / 第 11 章:拓扑模型
第 11 章:拓扑模型
11.1 什么是拓扑
拓扑(Topology)描述了几何对象之间的空间关系,而不关心具体的坐标。拓扑模型强调"共享边界"的概念——相邻的两个地块共享同一条边界线,而不是各自存储独立的多边形。
简单特征 vs 拓扑模型
| 特性 |
Simple Feature |
Topology |
| 边界存储 |
每个多边形独立存储 |
相邻面共享边 |
| 数据冗余 |
高(共享边界重复存储) |
低(边只存一次) |
| 编辑影响 |
只影响当前要素 |
自动更新相邻要素 |
| 适用场景 |
通用 |
行政区划、地籍管理 |
| 一致性 |
需要手动保证 |
规则自动保证 |
11.2 PostGIS Topology 架构
PostGIS Topology 使用三层数据结构:
Topology Layer (拓扑层)
├── Node (节点) ──────── 空间中的点
├── Edge (边) ─────────── 连接两个节点的线段
└── Face (面) ─────────── 由边围成的封闭区域
核心表
| 表名 |
说明 |
topology.topology |
拓扑元数据 |
topology.topology_id_seq |
拓扑 ID 序列 |
{topo_name}.node |
节点表 |
{topo_name}.edge_data |
边表(含几何) |
{topo_name}.face |
面表 |
{topo_name}.relation |
拓扑要素与拓扑对象的关系 |
11.3 创建拓扑
-- 启用拓扑扩展
CREATE EXTENSION IF NOT EXISTS postgis_topology;
-- 创建拓扑模式
-- 参数:拓扑名称, SRID, 容差(容差内的点视为同一个点)
SELECT topology.CreateTopology('admin_topo', 4326, 0.00001);
-- 查看拓扑信息
SELECT * FROM topology.topology WHERE name = 'admin_topo';
将简单要素导入拓扑
-- 假设已有行政区划表 districts (id, name, geom)
-- 创建拓扑层
SELECT topology.AddTopoGeometryColumn('admin_topo', 'public', 'districts', 'topo_geom', 'MULTIPOLYGON');
-- 将几何数据导入拓扑
UPDATE districts
SET topo_geom = topology.toTopoGeom(geom, 'admin_topo', 1);
-- 查看拓扑要素数量
SELECT
'nodes' AS type, count(*) FROM admin_topo.node
UNION ALL
SELECT
'edges' AS type, count(*) FROM admin_topo.edge_data
UNION ALL
SELECT
'faces' AS type, count(*) FROM admin_topo.face;
11.4 拓扑编辑
添加拓扑要素
-- 添加新的行政区划
INSERT INTO districts (name, topo_geom)
VALUES (
'新城区',
topology.toTopoGeom(
ST_GeomFromText('POLYGON((116.5 39.9, 116.6 39.9, 116.6 40.0, 116.5 40.0, 116.5 39.9))', 4326),
'admin_topo',
1
)
);
编辑拓扑边界
-- 移动共享边界(自动更新相邻面)
-- 步骤 1: 找到要编辑的边
SELECT edge_id, ST_AsText(geom)
FROM admin_topo.edge_data
WHERE ST_Intersects(geom, ST_GeomFromText('POINT(116.55 39.95)', 4326));
-- 步骤 2: 使用 ST_MoveIsoEdge 或直接编辑节点
-- 注意: 拓扑编辑通常通过拓扑编辑工具(如 QGIS Topology Editor)完成
删除拓扑要素
-- 删除一个行政区划
DELETE FROM districts WHERE name = '新城区';
-- 清理孤立的拓扑对象
SELECT topology.ST_RemoveIsoNode('admin_topo', node_id)
FROM admin_topo.node n
WHERE NOT EXISTS (
SELECT 1 FROM admin_topo.relation r
WHERE r.topogeo_id = n.node_id AND r.layer_id = 1
);
11.5 拓扑查询
从拓扑获取几何
-- 从拓扑要素获取几何
SELECT
d.name,
d.topo_geom,
topology.ST_GetFaceGeometry('admin_topo', face_id) AS face_geom
FROM districts d;
-- 拓扑关系查询
SELECT a.name AS district_a, b.name AS district_b
FROM districts a, districts b
WHERE a.id < b.id
AND ST_Intersects(
topology.ST_GetFaceGeometry('admin_topo', (a.topo_geom).topogeo_id),
topology.ST_GetFaceGeometry('admin_topo', (b.topo_geom).topogeo_id)
);
查看共享边
-- 查看两个相邻面共享的边
SELECT
e.edge_id,
ST_AsText(e.geom) AS edge_geom,
e.start_node,
e.end_node
FROM admin_topo.edge_data e
WHERE EXISTS (
SELECT 1 FROM admin_topo.relation r1
WHERE r1.element_id = e.edge_id AND r1.element_type = 2
AND r1.topogeo_id = (SELECT (topo_geom).topogeo_id FROM districts WHERE name = '东城区')
)
AND EXISTS (
SELECT 1 FROM admin_topo.relation r2
WHERE r2.element_id = e.edge_id AND r2.element_type = 2
AND r2.topogeo_id = (SELECT (topo_geom).topogeo_id FROM districts WHERE name = '西城区')
);
11.6 拓扑规则
定义拓扑规则
PostGIS Topology 支持以下核心拓扑规则:
| 规则 |
说明 |
检查函数 |
| 无重叠 (No Overlap) |
同层面要素不重叠 |
ST_Overlaps |
| 无间隙 (No Gaps) |
同层面要素之间无间隙 |
ST_Touches 或自定义 |
| 无悬挂点 (No Dangles) |
线要素无悬挂端点 |
ST_StartPoint / ST_EndPoint |
| 线连通 (Line Connectivity) |
线要素必须连通 |
ST_IsSimple |
| 面覆盖 (Area Coverage) |
面要素完全覆盖区域 |
ST_Covers |
重叠检测
-- 检查同层面要素是否重叠
WITH pairs AS (
SELECT
a.id AS id_a, a.name AS name_a, a.geom AS geom_a,
b.id AS id_b, b.name AS name_b, b.geom AS geom_b
FROM districts a, districts b
WHERE a.id < b.id
)
SELECT
name_a, name_b,
ST_Area(ST_Intersection(geom_a, geom_b)::geography) / 1000000 AS overlap_km2,
ST_Intersection(geom_a, geom_b) AS intersection_geom
FROM pairs
WHERE ST_Overlaps(geom_a, geom_b);
间隙检测
-- 检查面要素之间的间隙
WITH union_geom AS (
SELECT ST_Union(geom) AS geom FROM districts
),
boundary AS (
SELECT ST_Boundary(
ST_Envelope(geom)
) AS geom FROM union_geom
)
SELECT ST_Difference(b.geom, u.geom) AS gap_geom
FROM boundary b, union_geom u
WHERE NOT ST_IsEmpty(ST_Difference(b.geom, u.geom));
自动修复
-- 修复重叠:将重叠部分分配给面积较大的要素
CREATE OR REPLACE FUNCTION fix_overlaps() RETURNS void AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT a.id AS id_a, b.id AS id_b,
ST_Intersection(a.geom, b.geom) AS overlap_geom
FROM districts a, districts b
WHERE a.id < b.id AND ST_Overlaps(a.geom, b.geom)
LOOP
-- 从较小的要素中减去重叠部分
UPDATE districts
SET geom = ST_Difference(geom, rec.overlap_geom)
WHERE id = rec.id_b;
END LOOP;
END;
$$ LANGUAGE plpgsql;
11.7 拓扑验证
-- 验证拓扑的有效性
SELECT * FROM topology.ValidateTopology('admin_topo');
-- 验证结果包含:
-- 1. 无效的边(自相交等)
-- 2. 无效的面(不闭合等)
-- 3. 孤立的节点/边
-- 4. 不一致的关系
-- 验证单个拓扑几何的有效性
SELECT
name,
topology.ST_GetFaceGeometry('admin_topo', (topo_geom).topogeo_id) AS geom,
ST_IsValid(topology.ST_GetFaceGeometry('admin_topo', (topo_geom).topogeo_id)) AS is_valid
FROM districts;
11.8 拓扑与非拓扑数据互转
从拓扑导出简单要素
-- 导出为普通几何表
CREATE TABLE districts_simple AS
SELECT
id,
name,
(topo_geom)::geometry AS geom
FROM districts;
-- 创建空间索引
CREATE INDEX idx_districts_simple_geom ON districts_simple USING GIST(geom);
从简单要素导入拓扑
-- 清空拓扑后重新导入
-- 1. 清空
SELECT topology.ClearTopoGeomColumn('public', 'districts', 'topo_geom');
-- 2. 删除旧拓扑
SELECT topology.DropTopology('admin_topo');
-- 3. 重新创建
SELECT topology.CreateTopology('admin_topo', 4326, 0.00001);
SELECT topology.AddTopoGeometryColumn('admin_topo', 'public', 'districts', 'topo_geom', 'MULTIPOLYGON');
-- 4. 重新导入
UPDATE districts
SET topo_geom = topology.toTopoGeom(geom, 'admin_topo', 1);
11.9 业务场景
场景:地籍管理
-- 创建地籍拓扑
SELECT topology.CreateTopology('cadastral_topo', 4490, 0.001);
CREATE TABLE land_parcels (
parcel_id SERIAL PRIMARY KEY,
owner_name TEXT,
land_use VARCHAR(50),
area_m2 NUMERIC(12,2),
topo_geom topology.topogeometry
);
SELECT topology.AddTopoGeometryColumn('cadastral_topo', 'public', 'land_parcels', 'topo_geom', 'MULTIPOLYGON');
-- 导入地籍数据
INSERT INTO land_parcels (owner_name, land_use, topo_geom)
SELECT
owner_name,
land_use,
topology.toTopoGeom(geom, 'cadastral_topo', 1)
FROM raw_parcels;
-- 查询相邻地块
WITH parcel AS (
SELECT topo_geom FROM land_parcels WHERE parcel_id = 100
)
SELECT lp.parcel_id, lp.owner_name
FROM land_parcels lp, parcel p
WHERE lp.parcel_id != 100
AND ST_Touches(
(lp.topo_geom)::geometry,
(p.topo_geom)::geometry
);
11.10 性能考虑
| 操作 |
性能影响 |
建议 |
| 创建拓扑 |
慢(需要构建节点/边/面) |
批量导入时一次性创建 |
| toTopoGeom |
比直接插入慢 5-10 倍 |
大数据量时分批处理 |
| 拓扑查询 |
取决于拓扑复杂度 |
使用拓扑关系表而非几何运算 |
| 拓扑编辑 |
需要更新关系表 |
使用事务保证一致性 |
11.11 本章小结
| 要点 |
说明 |
| 拓扑模型 |
共享边界,消除冗余,保证一致性 |
| Node/Edge/Face |
拓扑三层结构 |
| CreateTopology |
创建拓扑空间 |
| toTopoGeom |
将简单要素转为拓扑要素 |
| ValidateTopology |
验证拓扑一致性 |
| 适用场景 |
行政区划、地籍管理、管网系统 |
扩展阅读