31. 拓扑¶
PostGIS 通过名为 postgis_topology 的扩展支持 SQL/MM SQL-MM 3 Topo-Geo 和 Topo-Net 3 规范。您可以在手册:PostGIS 拓扑中了解此扩展提供的所有函数和类型。postgis_topology 扩展包含另一种核心空间类型,称为 topogeometry。除了 topogeometry 空间类型外,您还会找到用于构建拓扑和填充拓扑的函数。
在您开始使用拓扑之前,您必须按如下方式安装 postgis_topology 扩展:
CREATE EXTENSION postgis_topology;
安装扩展后,您将在数据库中看到一个名为 topology 的新模式。topology 模式会编录数据库中的所有拓扑。
topology 模式包含两个表和所有用于拓扑的辅助函数。
topology - 列出数据库中的所有拓扑以及它们存储在哪个模式中
layer - 列出数据库中所有包含 topogeometries 的表列
layer 表与我们之前学到的 raster_columns、geometry_columns 和 geography_columns 目录非常相似,但专门用于 topogeometries。
31.1. 创建拓扑¶
到底什么是拓扑和 topogeometry,它们之间有什么关系?在解释之前,让我们首先使用 CreateTopology 函数创建一个拓扑来容纳我们纽约市的拓扑完美数据,并将容差设置为 0.5 米。请注意,0.5 以米为单位,因为我们的空间参考系统是纽约州平面米。
SELECT topology.CreateTopology('nyc_topo', 26918, 0.5);
输出结果为:
1
这是它为新拓扑分配的 ID。运行上述命令后,您将在数据库中看到一个名为 nyc_topo 的新模式。您可以将拓扑命名为任何您想要的名称。我的惯例是在末尾添加 _topo 以将其与数据库中的其他模式区分开来。
如果您浏览 topology.topology 表,
SELECT * FROM topology.topology;
您会看到
id | name | srid | precision | hasz
----+----------+-------+-----------+------
1 | nyc_topo | 26918 | 0 | f
(1 row)
31.2. 拓扑和 topogeometries 的存储¶
拓扑是在 PostgreSQL 数据库中以模式形式实现的。如果您浏览 nyc_topo 模式,您会看到这些表和视图:
- edge - 这是针对 edge_data 构建的视图,主要用于符合 SQL/MM 标准。
它具有 edge_data 表中的部分列。
edge_data - 包含构成拓扑的所有线串
- face - 包含可以从 edge_data 形成的所有闭合曲面的列表。
它不包含实际的几何图形,而只包含几何图形的边界框。
node - 包含所有边的起点和终点,以及未连接到任何内容的点(孤立节点)
relation - 这定义了拓扑中的哪些元素构成 topogeometry。
那么什么是 topogeometry?topogeometry 是由拓扑中的边、面、节点和其他 topogeometry 形成的几何图形的表示形式。
topogeometry 位于何处?它位于其他地方,通过 relation 表引用拓扑的元素。虽然我们可以将 topogeometries 放入我们的 nyc_topo 模式中,但通常的惯例是在其他模式中定义具有 topogeometry 的其他表,并且还具有您可能感兴趣跟踪的任何其他类型的数据。
31.3. 为什么要使用 topogeometries?¶
使用 topogeometries 可以使您的数据保持整洁和连接。Topogeometries 对于地籍工作非常有用,您希望确保两块土地不会相互重叠,即使您更改其中一块土地的边界,或者您希望确保道路在您更改形成它们的几何图形时保持连接。几何图形存在于它们自己的岛屿上,您可以复制它们、变形它们。几何图形是无忧无虑的,不关心与它们共享空间的其他几何图形。相比之下,topogeometries 遵循其拓扑的规则;除非存在定义它们的边、节点、面或其他 topogeometry,否则它们无法存在。一个 topogeometry 属于一个且仅属于一个拓扑。一个 topogeometry 是一个几何图形的关系模型,因此,当每个组件(边/面/节点)被移动、添加等时,它们不会更改一个 topogeometry 的形状,而是会更改所有具有公共组件的 topogeometries。
我们有一个没有任何数据的 nyc_topo 拓扑。让我们用我们的纽约市数据填充它。拓扑边、面和节点可以通过两种主要方式创建:
可以直接使用拓扑基本函数创建边、面和节点。
可以通过创建 topogeometries 来形成边、面和节点。当从几何图形创建 topogeometry 时,并且缺少与其坐标匹配的边、面或节点时,则会在该过程中创建新的边、面和节点。
31.4. 定义 topogeometry 列和创建 topogeometries¶
填充拓扑最常见的方法是创建 topogeometries。让我们首先创建一个表来保存社区,然后使用 AddTopoGeometryColumn 函数添加一个 topogeometry 列。
CREATE TABLE nyc_neighborhoods_t(boroname varchar(43), name varchar(67),
CONSTRAINT pk_nyc_neighborhoods_t PRIMARY KEY(boroname,name) );
SELECT topology.AddTopoGeometryColumn('nyc_topo', 'public', 'nyc_neighborhoods_t',
'topo', 'POLYGON') As layer_id;
以上输出的结果为:
layer_id
--------
1
现在我们准备填充我们的表。最好在添加之前确保您的几何图形有效,否则您会收到诸如 SQLMM 几何图形不简单的错误。
因此,让我们从添加有效的几何图形开始。这里使用的 1 是指先前查询生成的 layer_id。如果您不知道 layer id,您可以使用 FindLayer 函数查找它,我们将在后面的示例中使用它。
在这些示例中,您将使用 toTopoGeom 函数将几何图形转换为其等效的 topogeometry。toTopoGeom 函数会为您处理大量记账工作。
toTopoGeom 函数会检查传入的几何图形,并根据需要将节点、边和面注入到您的拓扑中,以形成几何图形的形状。然后,它会将关系添加到 relation 表中,该表定义了此新的 topogeometry 如何与这些新的和现有的拓扑元素相关。在某些情况下,几何图形的某些部分可能存在,或者需要拆分现有部分以形成新的几何图形。
INSERT INTO nyc_neighborhoods_t(boroname,name, topo)
SELECT boroname, name, topology.toTopoGeom(geom, 'nyc_topo', 1)
FROM nyc_neighborhoods
WHERE ST_ISvalid(geom);
上述步骤应花费 3-4 秒。现在让我们添加无效的几何图形
INSERT INTO nyc_neighborhoods_t(boroname,name, topo)
SELECT boroname, name, topology.toTopoGeom(
ST_UnaryUnion(
ST_CollectionExtract(
ST_MakeValid(geom), 3)
), 'nyc_topo', 1)
FROM nyc_neighborhoods
WHERE NOT ST_ISvalid(geom);
以上步骤应该花费大约 300-400 毫秒。
现在我们的拓扑中有数据了。快速检查将显示 nyc_topo.edge、nyc_topo.node 和 nyc_topo.face 中都有数据
SELECT 'edge' AS name, count(*)
FROM nyc_topo.edge
UNION ALL
SELECT 'node' AS name, count(*)
FROM nyc_topo.node
UNION ALL
SELECT 'face' AS name, count(*)
FROM nyc_topo.face;
输出为:
name | count
------+-------
edge | 580
node | 396
face | 218
(3 rows)
现在,我们可以声明性地表达 boros 是由一系列社区组成的,方法是在 nyc_boros_t 表中定义一个名为 topo 的列,该列的类型为 POLYGON,并且是来自 nyc_neighborhoods_t.topo 列的其他 topogeometries 的集合。
CREATE TABLE nyc_boros_t(boroname varchar(43),
CONSTRAINT pk_nyc_boros_t PRIMARY KEY(boroname) );
SELECT topology.AddTopoGeometryColumn('nyc_topo', 'public', 'nyc_boros_t',
'topo', 'POLYGON',
(topology.FindLayer('public', 'nyc_neighborhoods_t', 'topo')).layer_id
) AS layer_id;
输出结果为:
为了填充这个新表,我们将使用 CreateTopoGeom 函数。CreateTopoGeom 不是从几何图形开始形成新的 topogeometry,而是从现有的拓扑元素(可能是基本元素或其他 topogeometries)开始来定义新的 topogeometry。
INSERT INTO nyc_boros_t(boroname, topo)
SELECT n.boroname,
topology.CreateTopoGeom('nyc_topo',
3, (topology.FindLayer('public', 'nyc_boros_t', 'topo')).layer_id ,
topology.TopoElementArray_Agg( ARRAY[ (n.topo).id, (n.topo).layer_id ]::topoelement ) )
FROM nyc_neighborhoods_t AS n
GROUP BY n.boroname;
这将插入 5 条记录,对应于纽约的行政区。
注意
如果您使用的是 PostGIS 3.4 或更高版本,则可以使用新的强制转换将 topogeometry 转换为 topoelement,并将上面的示例中的 topology.TopoElementArray_Agg( ARRAY[ (n.topo).id, (n.topo).layer_id ]::topoelement ) ) 替换为较短的 topology.TopoElementArray_Agg( n.topo::topoelement )
要在 pgAdmin 中查看这些内容,您可以按如下方式将 topogeometry 转换为几何图形:
SELECT boroname, topo::geometry AS geom
FROM nyc_boros_t;
输出将如下所示:
如果您认为这是一团糟,是的,这确实是一团糟。这是在经历了无数次简化和其他几何图形处理过程后发生的事情,其中每个几何图形都被视为一个单独的单元。您会得到间隙、悬空的岛屿,以及社区侵占彼此的领土。
幸运的是,我们可以使用拓扑来清理这种混乱,并帮助我们维护良好、干净的连接数据。
让我们戴上我们的土地测量师的帽子,并提出这样一个问题:如果我们正在将我们的土地划分为区域(boros 或社区),以便每个区域可能与其他区域相邻,但不应共享任何公共区域,那么区域是否应该具有公共区域才有意义?不,这没有意义。现在,我们的数据指出,某些区域属于多个社区或多个行政区。
让我们首先看看 boros,并查找共享公共元素的社区
SELECT te, array_agg(DISTINCT b.boroname)
FROM nyc_boros_t AS b, topology.GetTopoGeomelements(topo) AS te
GROUP BY te
HAVING count(DISTINCT b.boroname) > 1;
输出结果为:
te | array_agg
--------+-------------------
{44,3} | {Brooklyn,Queens}
{51,3} | {Brooklyn,Queens}
{76,3} | {Brooklyn,Queens}
{114,3} | {Brooklyn,Queens}
{117,3} | {Brooklyn,Queens}
(5 rows)
这告诉我们皇后区和布鲁克林正处于边界战争中。在此查询中,我们使用 GetTopoGeomElements 函数来声明性地检查行政区之间共享哪些组件。
返回的是一组 topoelements。topoelement 表示为一个包含 2 个整数的数组,其中第一个数字是元素的 id,第二个数字是元素的图层(或基本类型)。PostGIS GetTopoElements 返回 topogeometry 的基本元素,类型编号 1-3 对应于(1:节点,2:边和 3:面)。社区和行政区的所有 topoelements 的类型均为 3,这对应于拓扑面。我们可以使用 ST_GetFaceGeometry 来获得这些共享面的可视化表示,如下所示:
SELECT te, t.geom, ST_Area(t.geom) AS area, array_agg(DISTINCT d.boroname) AS shared_boros
FROM nyc_boros_t AS d, topology.GetTopoGeomelements(topo) AS te
, topology.ST_GetFaceGeometry('nyc_topo',te[1]) AS t(geom)
GROUP BY te, t.geom
HAVING count(DISTINCT d.boroname) > 1
ORDER BY area;
结果将是 5 行,对应于皇后区和布鲁克林之间的边界争端。
如果我们查看我们的社区,我们将看到类似的情况,但有 44 起边界争端
SELECT te, t.geom, ST_Area(t.geom) AS area, array_agg(DISTINCT d.name) AS shared_d
FROM nyc_neighborhoods_t AS d, topology.GetTopoGeomelements(d.topo) AS te
, topology.ST_GetFaceGeometry('nyc_topo',te[1]) AS t(geom)
GROUP BY te, t.geom
HAVING count(DISTINCT d.name) > 1
ORDER BY area;
由于行政区是社区的聚合,因此我们可以通过解决社区边界争端来解决行政区问题。
我们可以通过多种方式解决此问题。我们可以出去调查,询问人们他们认为自己站在哪个社区。或者,我们可以将少量的土地分配给面积最小的社区或出价最高的社区。
从 Topogeometries 中删除元素是使用 TopoGeom_remElement 函数处理的。因此,让我们开始吧,从面积最大的社区中删除重复元素,如下所示:
WITH to_remove AS (SELECT te, MAX( ST_Area(d.topo::geometry) ) AS max_area, array_agg(DISTINCT d.name) AS shared_d
FROM nyc_neighborhoods_t AS d, topology.GetTopoGeomelements(d.topo) AS te
, topology.ST_GetFaceGeometry('nyc_topo',te[1]) AS t(geom)
GROUP BY te
HAVING count(DISTINCT d.name) > 1)
UPDATE nyc_neighborhoods_t AS d SET topo = TopoGeom_remElement(topo, te)
FROM to_remove
WHERE d.name = ANY(to_remove.shared_d)
AND ST_Area(d.topo::geometry) = to_remove.max_area;
以上结果是更新了 29 个社区。如果您为社区和行政区重新运行边界争端查询,您会发现您不再有边界争端。
我们仍然在社区之间存在由于密集简化而导致的空旷空间间隙。可以通过使用拓扑编辑器函数系列直接编辑拓扑和/或填充孔并将这些孔分配给社区来解决此类问题。