31. トポロジ

PostGIS は postgis_topology というエクステンションを介して SQL/MM SQL-MM 3 Topo-Geo と Topo-Net 3 仕様に対応しています。マニュアル PostGIS トポロジ でエクステンションで地峡される全ての関数とタイプに関して学習することができます。postgis_topology エクステンションには、もうひとつの中核的な空間型である topogeometry があります。topogeometry 空間型に加えて、topologies の構築とトポロジの投入の関数があります。

トポロジの使用を開始する前に、postgis_topology エクステンションをインストールします。次のようにします:

CREATE EXTENSION postgis_topology;

エクステンションをインストールすると、インストールしたデータベースに topology というスキーマができています。データベース内の topology スキーマは全てのトポロジをカタログ化しています。

topology スキーマには、二つのテーブルと全てのトポロジの補助関数とがあります。

  • topology - データベース内の全てのトポロジ覧と、それが格納されていスキーマの一覧です

  • layer - データベースでとぽジオメトリを保持する全てのテーブルカラムの一覧です

layer テーブルは、これより前に学習した raster_columnsgeometry_columnsgeography_columns カタログによく似ていますが、トポジオメトリ専用です。

31.1. トポロジの作成

トポロジとトポジオメトリとは、正確には何であって、この二つのどういう関係にあるのでしょう? その説明の前に、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. トポロジとトポジオメトリの格納

トポロジは PostgreSQL データベース内のスキーマとして実装されています。nyc_topo スキーマを調べると、これらのテーブルとビューが見えます:

  • edge - edge_data に対して作られたビューで、主に SQL/MM 準拠のためにあります。

    edge_data テーブルのカラムの一部を持っています。

  • edge_data - トポロジを構成する全てのラインストリングを含みます

  • face - edge_data から形成できる閉じたサーフェスの全ての一覧です。

    実際のジオメトリはありませんが、その代わりにジオメトリのバウンディングボックスを持っています。

  • node - 全てのエッジの始点終点と接続されていない点 (孤立ノード) の全てを持ちます

  • relation - トポロジのどの要素がトポジオメトリを構成するかを定義します。

ではトポジオメトリは何でしょう? トポジオメトリは、トポロジ内のエッジ、フェイス、ノードや他のトポジオメトリから形成されるジオメトリの表現です。

トポジオメトリはどこにあるでしょう? *relation*テーブル を介してトポロジの要素を参照する、別のどこかにあります。nyc_topo スキーマ内にトポジオメトリを投げることもできますが、一般的な規則では、トポジオメトリを保持し、また、他の興味がありそうな種類のデータも保持する、他のスキーマ内の他のテーブルを定義することになります。

31.3. なぜトポジオメトリを使うか?

トポジオメトリの利用でデータが整然として、接続もされています。トポジオメトリは土地台帳作成作業に非常に便利です。土地台帳では、境界を変更しても二つの土地区画が相互にオーバラップしないように保証できることが必要だったり、これらを構成するジオメトリを変更する時に道路が接続されていることが保証される必要があります。ジオメトリは自身の島に存在し、これらを複製したり、変形したりできます。ジオメトリは心配がなく、空間共有する他のジオメトリに関して気にしません。対照的に、トポジオメトリは、これらを定義するエッジ、ノード、フェイスや他のトポジオメトリが無い場合には存在してはいけないという規則に従います。トポジオメトリは唯一のトポロジに属します。トポジオメトリは、ジオメトリや個々の要素といったもの (エッジ/フェイス/ノード) が動かされたり追加されたりする時に、一つのトポジオメトリだけの形状が変更されるのでなく、共通する要素を持つ全てのジオメトリが変更される関係モデルです。

データのない nyc_topo トポロジがあります。ニューヨーク市のデータを投入してみましょう。トポロジのエッジ、フェイス、ノードは、二つの主要な方法で作成できます。

  • エッジ、フェイス、ノードはトポロジ基本関数で直接生成できます。

  • エッジ、フェイス、ノードはトポジオメトリの生成によって形成することができます。トポジオメトリがジオメトリから形成して、その座標のエッジ、フェイス、ノードのいずれかが欠落している場合には、処理の一部として、新しいエッジ、フェイス、ノードが生成されます。

31.4. トポジオメトリカラムの定義とトポジオメトリの生成

トポロジ投入の最も一般的な方法は、トポジオメトリの生成です。地区を保持するテーブルの作成から始めましょう。AddTopoGeometryColumn 関数を使ってトポジオメトリカラムを追加します。

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

テーブルに投入する準備ができています。追加前にジオメトリが妥当であるかを確認するのが最善です。さもないと、SQL/MM ジオメトリが単純でないようなエラーが出ます。

では妥当なものを追加することろから始めましょう。ここで使われてる 1 は前のクエリで生成された layer_id を参照するものです。レイヤID が分からない場合には、後の例で使用する FindLayer 関数を使って検索します。

これらの例では toTopoGeom 関数を使って、ジオメトリをトポジオメトリに変換します。toTopoGeom 関数は多数の帳簿を処理します。

toTopoGeom 関数は、渡されたジオメトリを調査して、トポロジにジオメトリの形状の形成に必要なノード、エッジ、フェイスを挿入します。この新しいトポジオメトリが、トポロジの新規要素や既存要素とどのように関係しているかを定義する relation テーブルに関係を追加します。場合によっては、ジオメトリの断片が存在することがあり、または、新しいジオメトリの形成に、既存の断片を分割する必要があります。

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)

行政区は、POLYGON タイプであり、かつ nyc_neighborhoods_t.topo カラムからの他のトポジオメトリのコレクションである nyc_boros_t テーブルの topo カラムを定義することで地区のコレクションから形成されることが、宣言的に表現されます。

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 は、新しいトポジオメトリを定義するために、プリミティブである可能性があるか、他のトポジオメトリであるような既存のトポロジ要素から始めます。

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 以上を使っている場合には、新しいキャストでトポジオメトリからトポエレメントにキャストすることができます。上の例の`topology.TopoElementArray_Agg( ARRAY[ (n.topo).id, (n.topo).layer_id ]::topoelement ) )` を、より短い topology.TopoElementArray_Agg( n.topo::topoelement ) に置き換えることができます

これらを pgAdmin で見るには、次のように、トポジオメトリからジオメトリにキャストします:

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 関数を使って宣言的に、行政区をまたいで共有する要素を調査します。

トポエレメントの集合が返されます。トポエレメントは二つの整数の配列として表現されます。一つ目は要素識別子、二つ目は要素のレイヤ (またはプリミティブタイプ) です。PostGIS GetTopoElements はタイプ番号 1-3 (1:ノード, 2:エッジ, 3:フェイス) でトポジオメトリのプリミティブを返します。全ての地区と行政区のトポエレメントのタイプ番号は、フェイスに対応する 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;

行政区は地区を集約したものですので、地区の境界紛争を訂正することで、行政区の問題が訂正できます。

訂正する方法は多数あります。外に出て、人々にどの区域にいると思いますか? と質問できるでしょう。あるいは、面積が最も少ない地区や、最も高額を提示した入札者に、小区画の土地を割当てることも可能です。

トポジオメトリからの要素の削除は 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地区 が更新されました。地区や行政区の境界紛争のクエリを実行すると、もはや境界紛争がなくなっていることが分かります。

集中的な単純化によってできた地区間の隙間がまだあります。このような問題は、 トポロジエディタ の関数ファミリ を使ってトポロジを直接編集したり、穴を埋めたり、地区に割り当てるといった方法での訂正が可能です。