PostGIS の LineString と Polygon で路線と公園を描きながら学んでみた

PostGIS の LineString と Polygon で路線と公園を描きながら学んでみた

2026.03.09

はじめに

GIS 初学者の筆者が PostGIS の LineString(線)Polygon(面) を試してみました。地下鉄路線を線で、公園の敷地を面で表現し、長さや面積を計算してみます。

前回の記事では Point(点)で 2 点間の距離計算を扱いました。今回はその続きとして線と面を試してみました。知っておいた方が良いこととしてgeometry 型と geography 型の違い についても整理しました。

試したこと早見

今回手を動かして理解できたポイントは以下の通りです。

  • LineString で地下鉄路線(南北線・東西線)を線として作成し、長さや頂点数を取得した
  • Polygon で公園(大通公園・中島公園)を面として作成し、面積や周長を取得した
  • geojson.io でジオメトリを地図上に可視化する方法を試した
  • geometry 型と geography 型の違いを整理し、::geography キャストでメートル単位に変換できることを確認した
  • 前回の ST_DistanceSphereST_Distance + geography 型の関係を比較した
  • ST_Collect / ST_Dump で Multi 系ジオメトリの集約・分解を試した

geojson_io___powered_by_Mapbox.png

環境の準備

前回の記事で構築した Docker 環境をそのまま使用します。

# 前回のディレクトリに移動
cd learn-gis

# コンテナが起動していなければ起動
docker compose up -d

# psqlで接続
docker compose exec db psql -U gisuser -d gis_db

前回作成した landmarks テーブルが残っている状態で進めます。

LineString で線を扱う

LineString とは

まず LineString(ラインストリング) について調べました。2 つ以上の点を順に結んだ線を表すジオメトリ型で、道路、鉄道路線、河川など「線的な地物」の表現に使います。

WKT(Well-Known Text)での表記は以下の通りです。

LINESTRING(経度1 緯度1, 経度2 緯度2, 経度3 緯度3, ...)

ラインテーブルの作成

テーブルを作ります。GEOMETRY(LineString, 4326) で LineString 型かつ SRID 4326(WGS84)のみ受け付けるカラムを定義します。

CREATE TABLE subway_lines (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    line_name TEXT NOT NULL,
    geom GEOMETRY(LineString, 4326)
);

札幌地下鉄南北線を LineString で作成する

前回使った札幌の座標を流用して、地下鉄南北線の中心部区間を LineString で表現してみました。さっぽろ駅から中島公園駅までの 4 駅を線で結びます。

INSERT INTO subway_lines (name, line_name, geom) VALUES
    ('南北線(さっぽろ〜中島公園)', '南北線',
     ST_SetSRID(ST_GeomFromText(
         'LINESTRING(141.3507 43.0687, 141.3544 43.0607, 141.3533 43.0555, 141.3549 43.0487)'
     ), 4326));
--               さっぽろ駅      大通駅          すすきの駅      中島公園駅

もう 1 本追加して東西線を作成する

INSERT INTO subway_lines (name, line_name, geom) VALUES
    ('東西線(西11丁目〜バスセンター前)', '東西線',
     ST_SetSRID(ST_GeomFromText(
         'LINESTRING(141.3383 43.0588, 141.3509 43.0596, 141.3563 43.0603, 141.3634 43.0610)'
     ), 4326));
--               西11丁目駅      西4丁目付近     大通駅          バスセンター前駅

WKT で確認してみる

SELECT name, ST_AsText(geom) AS wkt FROM subway_lines;
                name                |                                       wkt
------------------------------------+---------------------------------------------------------------------------------
 南北線(さっぽろ〜中島公園)       | LINESTRING(141.3507 43.0687,141.3544 43.0607,141.3533 43.0555,141.3549 43.0487)
 東西線(西11丁目〜バスセンター前) | LINESTRING(141.3383 43.0588,141.3509 43.0596,141.3563 43.0603,141.3634 43.061)
(2 rows)

LineString の基本操作を試す

関数 意味
ST_NPoints LineString の頂点数
ST_StartPoint 始点(最初の頂点)
ST_EndPoint 終点(最後の頂点)
ST_Length 線の長さ
SELECT
    name,
    ST_NPoints(geom)               AS num_points,
    ST_AsText(ST_StartPoint(geom)) AS start_point,
    ST_AsText(ST_EndPoint(geom))   AS end_point,
    ST_Length(geom)                 AS length_degree
FROM subway_lines;

ここで length_degree0.021... のような小さな値になっていて意味がわかりませんでした。これは「度」単位の数値であり、「geometry 型と geography 型」セクションで整理します。

                name                | num_points |       start_point       |        end_point        |    length_degree
------------------------------------+------------+-------------------------+-------------------------+----------------------
 南北線(さっぽろ〜中島公園)       |          4 | POINT(141.3507 43.0687) | POINT(141.3549 43.0487) |  0.02111496568477214
 東西線(西11丁目〜バスセンター前) |          4 | POINT(141.3383 43.0588) | POINT(141.3634 43.061)  | 0.025204976230397646
(2 rows)

Polygon で面を扱う

Polygon とは

次に Polygon(ポリゴン) を試しました。閉じた領域を表すジオメトリ型で、行政区域、建物の敷地、公園の範囲など「面的な地物」の表現に使います。

WKT での表記は以下の通りです。

POLYGON((経度1 緯度1, 経度2 緯度2, 経度3 緯度3, ..., 経度1 緯度1))

ポリゴンテーブルの作成

CREATE TABLE parks (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    geom GEOMETRY(Polygon, 4326)
);

大通公園を Polygon で作成してみる

大通公園は東西に約 1.5km 延びる細長い公園です。概略的な矩形で表現してみます。

INSERT INTO parks (name, geom) VALUES
    ('大通公園',
     ST_SetSRID(ST_GeomFromText(
         'POLYGON((141.3410 43.0600, 141.3565 43.0608, 141.3565 43.0598, 141.3410 43.0590, 141.3410 43.0600))'
     ), 4326));
--              ↑ NW             ↑ NE              ↑ SE              ↑ SW              ↑ 始点=終点(閉じる)

もう 1 つ追加して中島公園を作成する

中島公園は好きなのでキャプチャも貼っておきます。中島公園の座標は Google Maps で公園の四隅をクリックして取得しました。

中島公園_-_Google_マップ.png

実際に地図上で 4 つの角をクリックしてみると、「始点 = 終点で閉じる」という Polygon のルールが腑に落ちました。地図では 4 箇所をクリックするだけですが、WKT では最初の座標を末尾にもう一度書いて閉じます。そのため座標は 5 つになります。

INSERT INTO parks (name, geom) VALUES
    ('中島公園',
     ST_SetSRID(ST_GeomFromText(
         'POLYGON((141.3567 43.0476, 141.3565 43.0402, 141.3518 43.0401, 141.3512 43.0480, 141.3567 43.0476))'
     ), 4326));

穴あきポリゴンでドーナツ型を表現する

公園の中に池や建物がある場合、穴あき Polygon で表現できます。複数のリングを定義し、1 番目を外周、2 番目以降を穴とします。中島公園の池をくり抜こうかと思ったのですが、最初はシンプルに試したかったので座学で済ませました。

リング 回転方向 役割
1 番目のリング 反時計回り 外周(ポリゴンの境界)
2 番目以降のリング 時計回り 穴(くり抜く領域)
-- ドーナツ型Polygonの例(投入はしない。構文の解説のみ)
SELECT ST_AsText(
    ST_GeomFromText(
        'POLYGON(
            (141.3567 43.0476, 141.3565 43.0402, 141.3518 43.0401, 141.3512 43.0480, 141.3567 43.0476),
            (141.3530 43.0460, 141.3528 43.0430, 141.3520 43.0430, 141.3522 43.0460, 141.3530 43.0460)
        )', 4326)
) AS donut_polygon;
--  ↑ 外周リング(反時計回り)     ↑ 穴リング(時計回り)

Polygon の基本操作を試す

関数 意味
ST_NPoints 頂点数(始点=終点を含むため、4 頂点の矩形は 5 になる)
ST_Centroid ポリゴンの重心(中心点)
ST_Area 面積
ST_Perimeter 周長(外周の長さ)
SELECT
    name,
    ST_NPoints(geom)              AS num_points,
    ST_AsText(ST_Centroid(geom))  AS centroid,
    ST_Area(geom)                 AS area_degree,
    ST_Perimeter(geom)            AS perimeter_degree
FROM parks;

ここでも面積が 0.0000... という小さな値、周長も度単位になっています。

   name   | num_points |                  centroid                   |      area_degree       |   perimeter_degree
----------+------------+---------------------------------------------+------------------------+----------------------
 大通公園 |          5 | POINT(141.34874999999997 43.0599)           | 1.5500000000021898e-05 |  0.03304126286091674
 中島公園 |          5 | POINT(141.3540209354025 43.044074220854164) | 3.8984999999998055e-05 | 0.025541044237441385
(2 rows)

geojson.io でジオメトリを地図上に表示する

Polygon や LineString は座標の羅列だけでは形状がまったく想像できませんでした。調べたところ、geojson.io を使えばブラウザ上で地図表示できることがわかりました。

手順

  1. PostGIS で GeoJSON を出力する
-- 中島公園のPolygonをGeoJSONとして出力
SELECT json_build_object(
    'type', 'FeatureCollection',
    'features', json_agg(
        json_build_object(
            'type', 'Feature',
            'properties', json_build_object('name', name),
            'geometry', ST_AsGeoJSON(geom)::json
        )
    )
) AS geojson
FROM parks
WHERE name = '中島公園';
  1. 出力された JSON をコピーする
  2. https://geojson.io にアクセスし、右側の JSON エディターに貼り付ける
  3. 左側の地図に Polygon が表示される

灰色の網掛けで中島公園が囲われていることを確認できました。

geojson_io___powered_by_Mapbox.png

geojson.io はインストール不要で、GeoJSON を貼り付けるだけで地図表示できます。座標の入力ミス(lon/lat の逆転、Polygon が閉じていない等)にもすぐ気づけます。PostGIS のデバッグツールとして覚えておくと便利そうです。

geometry 型と geography 型について

前回 ST_DistanceSphere を使ったとき、結果は「メートル」で返ってきました。しかし今回 ST_LengthST_Area を使うと「度」で返ってきます。調べてみると geometry 型と geography 型の違いが原因でした。

geometry 型 geography 型
計算方式 平面(デカルト座標) として計算 球面(地球の曲面) として計算
EPSG:4326 での単位 (経度・緯度の数値差) メートル
関数の対応 ほぼすべての ST_* 関数が使える 一部の関数のみ対応
パフォーマンス 高速 geometry 型より遅い
用途 局所的な解析、投影座標系のデータ 広域の距離・面積計算

ST_Length の違いを実際に確認してみる

同じ LineString に対して、geometry 型と geography 型で ST_Length を比較してみました。

SELECT
    name,
    -- geometry型: 平面計算 → 度が返る
    ST_Length(geom)                                   AS length_degree,
    -- geography型: 球面計算 → メートルが返る
    ROUND(ST_Length(geom::geography)::numeric, 0)     AS length_meter
FROM subway_lines;
  • 0.021度: 数値としては正しいが、人間には意味がわからない
  • 2290メートル: 約 2.3km でさっぽろ駅から中島公園駅までの距離はこのくらいです
               name                |    length_degree     | length_meter
------------------------------------+----------------------+--------------
 南北線(さっぽろ〜中島公園)       |  0.02111496568477214 |         2290
 東西線(西11丁目〜バスセンター前) | 0.025204976230397646 |         2061

ST_Area の違いも確認してみる

Polygon の面積でも同じなのか試してみました。

SELECT
    name,
    -- geometry型: 度² が返る(人間にはわからない値)
    ST_Area(geom)                                          AS area_degree2,
    -- geography型: 平方メートルが返る
    ROUND(ST_Area(geom::geography)::numeric, 0)            AS area_m2,
    -- 平方メートル → ヘクタール(1ha = 10,000m²)
    ROUND((ST_Area(geom::geography) / 10000)::numeric, 2)  AS area_ha
FROM parks;

同じですね。

   name   |      area_degree2      | area_m2 | area_ha
----------+------------------------+---------+---------
 大通公園 | 1.5500000000021898e-05 |  140273 |   14.03
 中島公園 | 3.8984999999998055e-05 |  352899 |   35.29

ST_DistanceSphere との関係

ここで前回使った ST_DistanceSphere との関係が気になったので調べました。ST_DistanceSphere は geometry 型を受け取りつつ内部で球面計算する特殊な関数です。2 つの方法を比較してみます。

方法 書き方 内部の計算
ST_DistanceSphere(geom, geom) geometry 型のまま渡す 球体近似で距離計算
ST_Distance(geog, geog) geography 型にキャスト 回転楕円体で距離計算(より精密)
-- 前回の方法: ST_DistanceSphere(geometry型を渡すが球面計算する)
SELECT ROUND(ST_DistanceSphere(
    ST_SetSRID(ST_MakePoint(141.3507, 43.0687), 4326),
    ST_SetSRID(ST_MakePoint(141.3549, 43.0487), 4326)
)::numeric, 0) AS method1_m;

-- 今回の方法: geography型にキャストしてST_Distance
SELECT ROUND(ST_Distance(
    ST_SetSRID(ST_MakePoint(141.3507, 43.0687), 4326)::geography,
    ST_SetSRID(ST_MakePoint(141.3549, 43.0487), 4326)::geography
)::numeric, 0) AS method2_m;

どちらもメートルを返しますが、ST_Distance + geography 型のほうが高精度です。ST_DistanceSphere は手軽に球面距離を出したいときの便利関数です。

なぜ geometry 型が基本なのか整理してみた

「度」で返るのが不便なら geography 型だけ使えばよいのでは?と思いました。調べてみると、geometry 型が基本になっている理由がありました。実際はほとんどの空間操作では「度」が問題にならないようです。

操作の種類 単位が影響するか
含まれるか? ST_Contains(区域, 地点) しない
交差するか? ST_Intersects(路線, 区域) しない
最寄りを探す ORDER BY geom <-> target LIMIT 1 しない
バウンディングボックス検索 geom && ST_MakeEnvelope(...) しない
距離を測る ST_Length, ST_Distance する
面積を測る ST_Area する

「含まれるか」「交差するか」といったトポロジ判定は、日本国内の局所的なデータであれば geometry 型でも実用上問題ないようです。GIS の実務では距離・面積の計測よりトポロジ判定のほうが多いようです。

Multi 系ジオメトリで複数パーツを表現する

現実の地物は 1 つのジオメトリで表現できないケースがあることを知りました。

問題 解決
沖縄県(本島 + 離島群) 1 つの Polygon では表現不可 MultiPolygon
分岐する河川 1 本の LineString では表現不可 MultiLineString
同じ名前の複数拠点 1 つの Point では表現不可 MultiPoint

WKT での表記

MultiPoint:
  MULTIPOINT((141.35 43.06), (141.36 43.07))

MultiLineString:
  MULTILINESTRING((141.35 43.06, 141.36 43.07), (141.37 43.08, 141.38 43.09))

MultiPolygon:
  MULTIPOLYGON(
    ((141.34 43.05, 141.35 43.05, 141.35 43.06, 141.34 43.06, 141.34 43.05)),
    ((141.36 43.05, 141.37 43.05, 141.37 43.06, 141.36 43.06, 141.36 43.05))
  )

ST_Collect で複数ジオメトリを 1 つにまとめる

ST_Collect は集約関数(SUM, COUNT 等と同様)で、複数のジオメトリを 1 つにまとめます。同じ型同士なら Multi 系に、異なる型が混在する場合は GeometryCollection になります。GROUP BY と組み合わせて「区ごとにポリゴンを集約」といった用途にも使えます。

-- 2つの公園を1つのMultiPolygonにまとめる
SELECT
    ST_GeometryType(ST_Collect(geom)) AS collected_type,
    ST_NumGeometries(ST_Collect(geom)) AS num_parts
FROM parks;
    collected_type    | num_parts
----------------------+-----------
 ST_MultiPolygon      |         2

ST_Dump で Multi 系を個々に分解する

ST_DumpST_Collect の逆で、Multi 系ジオメトリを個々のジオメトリに分解します。

関数 方向 用途
ST_Collect 個別 → Multi 複数ジオメトリを 1 つにまとめる
ST_Dump Multi → 個別 Multi 系を個々のジオメトリに分解する
ST_NumGeometries Multi 系に含まれるジオメトリ数を返す
ST_GeometryN(geom, n) n 番目のジオメトリを取り出す(1-indexed)
SELECT
    (dump).path[1]           AS part_number,
    ST_AsText((dump).geom)   AS individual_geom
FROM (
    SELECT ST_Dump(ST_Collect(geom)) AS dump FROM parks
) AS sub;
 part_number |                                     individual_geom
-------------+----------------------------------------------------------------------------------------
           1 | POLYGON((141.341 43.06,141.3565 43.0608,141.3565 43.0598,141.341 43.059,141.341 43.06))
           2 | POLYGON((141.3505 43.05,141.358 43.05,141.358 43.0455,141.3505 43.0455,141.3505 43.05))

まとめ

地下鉄南北線・東西線を題材に、LINESTRING(lon lat, ...) で線を表現する方法を学びました。ST_NPointsST_StartPointST_EndPointST_Length で頂点数や始点・終点・長さを取得できます。

大通公園・中島公園を題材に、POLYGON((lon lat, ...)) で面を表現する方法を学びました。始点と終点を同一座標で閉じるルールがあります。ST_CentroidST_AreaST_Perimeter で重心・面積・周長を取得できます。

座標の羅列だけでは形状がわからないため、ST_AsGeoJSON で GeoJSON を出力し geojson.io に貼り付けて地図上で確認する方法も試しました。

EPSG:4326 の geometry 型で ST_Length / ST_Area を使うと「度」が返ります。::geography キャストでメートル単位に変換できます。前回使った ST_DistanceSphere は geometry 型を渡しつつ内部で球面計算する便利関数で、ST_Distance + geography 型のほうが高精度です。

Multi 系ジオメトリでは、ST_Collect で複数ジオメトリを 1 つに集約し、ST_Dump で個々に分解できます。2 つの公園を MultiPolygon にまとめる例で試しました。

おわりに

今回は手入力の座標で LineString と Polygon の基本を学びました。次回は国土数値情報などの実データを PostGIS にインポートし、ST_ContainsST_Intersects を使った空間検索をやってみようと思います。

この記事をシェアする

FacebookHatena blogX

関連記事