23. 妥当性

「なぜクエリで 'TopologyException' エラーが発生するのでしょうか?」という質問の90%は「一つ以上の入力が不正だったから」となります。ここから疑問が投げかけられます: 不正であるという意味は何か? またなぜ注意すべきか?

23.1. 妥当性とは

妥当性は、範囲の境界を定義し、大量の構造が必要となるポリゴンにとって最も重要なものです。ラインは非常に単純で不正にはなりません。ポイントも不正にはなりません。

ポリゴンの妥当性の規則には明白に感じるものもあり、恣意的に感じることがあります (実際、恣意的です)。

  • ポリゴンのリングは閉じていなければなりません。

  • 穴を定義するリングは外側の境界を定義するリングの内側に置かれます。

  • リングは自己交差できません (自身に接触又はクロスできません)。

  • リングは、ポイント以外では、他のリングに接触できません。

  • マルチポリゴンの要素は相互に接触できません。

最後の三つの規則は任意カテゴリに含まれます。自己一貫性があるポリゴンを定義する他の方法がありますが、上の規則は、PostGIS が準拠する OGC SFSQL 標準で使われるものです。

規則が重要であるのは、ジオメトリ計算用のアルゴリズムが、入力の一貫した構造に依存しているためです。構造についての仮定を持たないアルゴリズムの構築は可能です。しかし、あらゆる構造自由なルーティンの最初に行うことが「入力の解析と構造の組み込み」になるため、これらのルーティンは非常に遅くなる傾向にあります。

構造が重要である理由の例を示します。このポリゴンは不正です:

POLYGON((0 0, 0 1, 2 1, 2 2, 1 2, 1 0, 0 0));

この図では、もう少しはっきりと不正性が分かります:

_images/figure_eight.png

外側リングは実際に算用数字の8の字になっていて、真ん中で自己接触しています。描画ルーティンはポリゴンの内部の色塗りに成功して、二つの1面積単位の正方形からなり、合計2面積単位となる「領域」が見えていることに注意して下さい。

データベースがこのポリゴンの面積をどう考えるか見てみましょう:

SELECT ST_Area(ST_GeometryFromText(
         'POLYGON((0 0, 0 1, 1 1, 2 1, 2 2, 1 2, 1 1, 1 0, 0 0))'
       ));
 st_area
---------
       0

ここで何が起こっているのでしょう? 面積計算のアルゴリズムはリングは自己インタセクトしない前提で計算しています。行儀の良いリングは常に境界線の一方側 (どちらが内部でもよくて、*一方*でいいです)に (内部に) 区切られた領域を持ちます。しかしながら、行儀の悪い8の字ポリゴン内では、二つある区切られた領域の一つはラインの右側で区切られ、もう一つは左側で区切られます。これによって、両方でキャンセルしあうような面積計算になり (一方が 1 で、もう一方が -1 です)、故に「面積 0」の結果となります。

23.2. 妥当性検出

前の例では、不正だと**知っていた**一つのポリゴンを使いました。テーブル内の数百のジオメトリについて不正を検知するにはどうすればいいでしょうか? ST_IsValid(geometry) 関数を使います。8の字ポリゴンに対して使うと、すぐに答が得られます:

SELECT ST_IsValid(ST_GeometryFromText(
         'POLYGON((0 0, 0 1, 1 1, 2 1, 2 2, 1 2, 1 1, 1 0, 0 0))'
       ));
f

この地物が不正なのは知っていますが、なぜ不正なのかは知りません。ST_IsValidReason(geometry) 関数を使うと、不正の原因を見つけることができます:

SELECT ST_IsValidReason(ST_GeometryFromText(
         'POLYGON((0 0, 0 1, 1 1, 2 1, 2 2, 1 2, 1 1, 1 0, 0 0))'
       ));
Self-intersection[1 1]

理由 (自己交差) に加えて、不正位置 ((1 1)座標) も返されることに注意して下さい。

テーブルのテストにも ST_IsValid(geometry) 関数を使えます:

-- Find all the invalid polygons and what their problem is
SELECT name, boroname, ST_IsValidReason(geom)
FROM nyc_neighborhoods
WHERE NOT ST_IsValid(geom);
          name           |   boroname    |          st_isvalidreason
-------------------------+---------------+-----------------------------------------
 Howard Beach            | Queens        | Self-intersection[597264.08 4499924.54]
 Corona                  | Queens        | Self-intersection[595483.05 4513817.95]
 Steinway                | Queens        | Self-intersection[593545.57 4514735.20]
 Red Hook                | Brooklyn      | Self-intersection[584306.82 4502360.51]

23.3. 不正なものの修復

ポリゴンを単純な構造 (リング) に分解して、リングが妥当性の規則に従うことを確実にして、リング同士の規則に従った新しいポリゴンを生成することで、不正を修復できます。多くの場合、結果は直感的ですが、非常に行儀の悪い入力の場合には、妥当な出力なのに、どのように見えるかの直感を満たさない場合があります。PostGIS の最近のバージョンでは、ジオメトリの修復のための異なるアルゴリズムがあります: マニュアルページ を注意深く読んで、お好みのものを採用して下さい。

たとえば、不正の古典的な例の「バナナポリゴン」があります。単一のリングで一つの領域を囲みますが、リング自身に接触するように曲がっていて、実際には穴でない「穴」が残ったものです。

POLYGON((0 0, 2 0, 1 1, 2 2, 3 1, 2 0, 4 0, 4 4, 0 4, 0 0))
_images/banana.png

ポリゴンで ST_MakeValid を実行すると、妥当な OGC ポリゴンであって、外側リングと内側リングとが一つの点で接触するポリゴンが得られます。

SELECT ST_AsText(
         ST_MakeValid(
           ST_GeometryFromText('POLYGON((0 0, 2 0, 1 1, 2 2, 3 1, 2 0, 4 0, 4 4, 0 4, 0 0))')
         )
       );
POLYGON((0 0,0 4,4 4,4 0,2 0,0 0),(2 0,3 1,2 2,1 1,2 0))

注釈

「バナナポリゴン」 (あるいは「逆さシェル」) は、妥当なジオメトリのための OGC トポロジモデルと、ESRI が内部で使用するモデルが異なります。ESRI モデルは、接触するリングを無効と考えられ、この種の形状にはバナナ形が好まれます。OGC もで鵜は逆です。両方とも「正しい」というわけでなく、同じ状況をモデル化するための異なる方法です。

23.4. 大規模妥当性修復

修復されたものをテーブルに追加する間に再調査用としてジメトリにフラグを立てるSQLの例を示します。

-- Column for old invalid form
ALTER TABLE nyc_neighborhoods
  ADD COLUMN geom_invalid geometry
  DEFAULT NULL;

-- Fix invalid and save the original
UPDATE nyc_neighborhoods
  SET geom = ST_MakeValid(geom),
      geom_invalid = geom
  WHERE NOT ST_IsValid(geom);

-- Review the invalid cases
SELECT geom, ST_IsValidReason(geom_invalid)
  FROM nyc_neighborhoods
  WHERE geom_invalid IS NOT NULL;

不正ジオメトリの修復に関して視覚的に良いツールは OpenJump (http://openjump.org) です。Tools->QA->Validate Selected Layers に評価ルーチンを持っています。

23.5. 関数リスト

ST_IsValid(geometry A): ジオメトリが妥当かどうかを示す真偽値を返します。

ST_IsValidReason(geometry A): 不正の理由と不正箇所の座標値を示す文字列を返します。

ST_MakeValid(geometry A): 妥当性規則に従って再構築したジオメトリを返します。