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 | 验证拓扑一致性 |
| 适用场景 | 行政区划、地籍管理、管网系统 |
扩展阅读