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_columnsgeometry_columnsgeography_columns 目錄非常相似,但專用於 topogeometries。

31.1. 建立拓樸

拓樸和 topogeometry 究竟是什麼,它們之間有什麼關聯?在解釋之前,我們先使用 CreateTopology 函數建立一個拓樸,以容納我們拓樸上完美紐約市資料,並將容差設定為 0.5 公尺。請注意,由於我們的空間參考系統為 State Plany NY 公尺,因此 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 拓樸。讓我們用我們的紐約市資料填充它。拓樸邊、面和節點可以透過 2 種主要方式建立。

  • 可以使用拓樸基本函數直接建立邊、面和節點。

  • 可以透過建立 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。如果您不知道圖層 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;

輸出如下所示

_images/boros_topogeom.png

如果您認為這完全是一團糟,是的,它完全是一團糟。這是在多次簡化和其他幾何圖形處理週期後發生的情況,在這些週期中,每個幾何圖形都被視為一個單獨的單元。您會產生間隙、懸空島嶼和鄰里侵占彼此的領土。

幸運的是,我們可以利用拓樸來清理這種混亂,並幫助我們維護良好且連線的資料。

讓我們戴上土地測量員的帽子,並提出一個問題,如果我們將土地劃分為地塊(行政區或鄰里),以便每個地塊可能與其他地塊相鄰,但不應共用任何共同區域,那麼地塊共用共同區域是否有意義?不,這沒有意義。而現在,我們的資料指出某些區域屬於多個鄰里或多個行政區。

首先,讓我們看看行政區,並尋找共用共同元素的鄰里

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 個鄰里。如果您重新執行鄰里和行政區的邊界爭議查詢,您會發現您不再有任何邊界爭議。

由於密集簡化,我們在鄰里之間仍然存在空曠空間的間隙。可以透過使用 拓樸編輯器函數系列直接編輯拓樸和/或填補空隙並將這些空隙分配給鄰里來修正此類問題。