1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GeoParquetとPMTilesで全国1kmメッシュの防災Web地図を作る ― 5段階に分けて実装 ―

1
Posted at

GeoParquetとPMTilesで全国1kmメッシュの防災Web地図を作る ― 5段階に分けて実装 ―

日本全国の1kmメッシュを土台にして、洪水、土砂災害、津波、内水、地震などの防災データを重ねるWeb地図を作りたいと考えています。大量データを高速に表示する方法も考えています。

最初は、データ変換からWeb表示までを1つのDocker Composeにまとめるつもりでした。ただ、全国データを扱う処理では、どこかで失敗したときの切り分けが難しくなります。Shapefileの読み込みに失敗したのか、空間集計に時間がかかっているのか、Tippecanoeの設定が合っていないのか、PMTilesの配信設定が悪いのかを、エラーの切り分けができずに最初からデバッグする必要があるためです。

そこで、実装を次の5段階に分けることにしました。

実装の手順

  • 初回 全体構成と、Step間で受け渡すファイルを決める
  • Step 1 e-Statの全国1kmメッシュをGeoParquetへ変換する
  • Step 2 防災区域を正規化し、1kmメッシュへ集計する
  • Step 3 TippecanoeでPMTilesを作る
  • Step 4 NginxとOpenLayersで静的表示する
  • Step 5 DuckDBとFastAPIで詳細・統計APIを追加する
  • 最終回 Step 1~5の成果物を1つのDocker Compose環境へまとめる

この記事は初回です。まだ全国データそのものは変換しません。先に、各Stepがどこまで担当するか、前後のStepへ何を渡すか、どの時点で検査するかを決めます。


最終的に作る構成

全体は、データを作る処理と、普段動かしておくサービスに分けます。

ChatGPT Image 2026年8月4日 14_57_13.png

それぞれの役割は次のとおりです。

役割 使用するもの 主な用途
原本・中間成果物 GeoParquet 再集計、属性確認、別形式への変換
空間集計・API用DB DuckDB Spatial 詳細検索、統計、FastAPIからの参照
固定レイヤーの表示 Tippecanoe / PMTiles 全国1kmメッシュ、浸水想定区域などの配信
静的配信 Nginx OpenLayersとPMTilesのRange配信
詳細・統計API FastAPI メッシュ詳細、データセット情報、集計値
Web地図 OpenLayers 背景地図、凡例、レイヤー切り替え、クリック

全国1kmメッシュや浸水想定区域のように、頻繁には変わらないレイヤーはPMTilesにして静的配信します。地図を移動するたびにFastAPIでMVTを生成する構成にはしません。

一方で、メッシュの詳しい属性、データの出典、基準日、集計値などはFastAPIから返します。雨量、水位、警報のように更新頻度が高い情報も、将来はFastAPI側へ追加します。


1kmメッシュは共通の索引として使う

防災データは、すべて同じ形ではありません。

洪水浸水想定区域       ポリゴン
土砂災害警戒区域       ポリゴン
避難施設              ポイント
J-SHIS地震ハザード    250mメッシュ
DEM                  ラスター
雨量・水位            時系列

これらを最初からすべて1kmメッシュだけにしてしまうと、道路沿いや斜面沿いの細かな区域が見えなくなります。そのため、1kmメッシュは全国を横断して検索・集計するための共通索引として使い、元の形状も別に残します。

元データの形状
  詳細表示、再集計、検証に使用

1kmメッシュ集計
  全国表示、検索、統計、データセット間の比較に使用

たとえば洪水データなら、次の2種類を作ります。

flood_native.parquet
  元の浸水想定区域ポリゴン

flood_summary_1km.parquet
  1kmメッシュごとの最大区分、影響面積、面積率

Stepごとに分ける理由

各Stepは、できるだけ単独で実行できる小さなプロジェクトにします。

ここでいう「単独で実行できる」は、前段の成果物が不要という意味ではありません。必要なファイルを所定の場所へ置けば、前段のコンテナやPythonソースを起動しなくても処理できる、という意味です。

たとえばStep 2は、Step 1のPythonモジュールをimportしません。読み込むのは、Step 1が作った次のファイルだけです。
また、機能を追加して、表示するデータが増えてもstepを増やせば良いので、特に問題はありません。

例えば、

workspace/artifacts/step1/mesh_master_1km.parquet
workspace/artifacts/step1/manifest.json
workspace/artifacts/step1/validation_report.json

この形にしておくと、次の作業がしやすくなります。

  • Step 1だけをやり直してメッシュ件数を比較する
  • Step 2で洪水だけ、土砂災害だけを処理する
  • Step 3のズーム設定だけを変えてPMTilesを作り直す
  • Step 4を架空のPMTilesで先に確認する
  • Step 5をフロントなしでテストする
  • 最終統合で、どの成果物に問題があるかを追う

フォルダー構成

シリーズ全体は、次の構成で進めます。

japan_disaster_pmtiles_series_overview_r002/
├─ README.md
├─ Makefile
├─ .env.example
│
├─ 00_overview/
│  ├─ README.md
│  ├─ qiita_overview_r002.md
│  ├─ architecture.md
│  ├─ data_contracts.md
│  ├─ development_rules.md
│  └─ execution_plan.md
│
├─ 01_step1_mesh_geoparquet/
│  ├─ README.md
│  ├─ config/
│  ├─ docker/
│  ├─ scripts/
│  ├─ src/
│  └─ tests/
│
├─ 02_step2_hazard_aggregation/
│  ├─ README.md
│  ├─ config/
│  ├─ docker/
│  ├─ scripts/
│  ├─ src/
│  └─ tests/
│
├─ 03_step3_pmtiles/
│  ├─ README.md
│  ├─ config/
│  ├─ docker/
│  ├─ scripts/
│  ├─ src/
│  └─ tests/
│
├─ 04_step4_static_web/
│  ├─ README.md
│  ├─ frontend/
│  ├─ nginx/
│  ├─ scripts/
│  └─ tests/
│
├─ 05_step5_fastapi_duckdb/
│  ├─ README.md
│  ├─ backend/
│  ├─ config/
│  ├─ docker/
│  ├─ scripts/
│  └─ tests/
│
├─ 99_final_integration/
│  ├─ README.md
│  ├─ backend/
│  ├─ frontend/
│  ├─ nginx/
│  ├─ scripts/
│  └─ tests/
│
├─ contracts/
│  ├─ artifact_manifest.schema.json
│  ├─ dataset_catalog.schema.json
│  └─ examples/
│
├─ workspace/
│  ├─ raw/
│  │  ├─ mesh/
│  │  └─ hazards/
│  ├─ artifacts/
│  │  ├─ step1/
│  │  ├─ step2/
│  │  ├─ step3/
│  │  ├─ step4/
│  │  └─ step5/
│  ├─ published/
│  │  └─ tiles/
│  ├─ runtime/
│  └─ logs/
│
└─ scripts/
   ├─ show_pipeline.py
   ├─ validate_structure.py
   └─ check_overview.sh

01_から05_までは、記事もDocker環境も個別に作ります。99_final_integrationでは、各Stepの処理を書き直すのではなく、検査済みの成果物を組み合わせます。


入力データと成果物をソースから分ける

元データや生成ファイルは、各Stepのソースフォルダーへ置きません。トップレベルのworkspaceへまとめます。

workspace/raw
  e-Statや国土数値情報から取得した原本。
  展開後も内容は変更しない。

workspace/artifacts/step1~step5
  各Stepが作った検査中の成果物。

workspace/published
  表示確認と検査が終わったPMTilesやcatalog.json。

workspace/runtime
  FastAPIが読み取るDuckDB。

workspace/logs
  実行ログ、件数比較、検査結果。

コンテナへマウントするときも、前段の成果物は読み取り専用にします。

Step 2の例
  workspace/artifacts/step1 : read only
  workspace/raw/hazards     : read only
  workspace/artifacts/step2 : read / write

処理ミスでStep 1の成果物を書き換えないためです。


Step間で受け渡すファイル

Step 1

workspace/artifacts/step1/
├─ mesh_master_1km.parquet
├─ manifest.json
└─ validation_report.json

mesh_master_1km.parquetには、少なくとも次の列を持たせます。

内容
feature_id Web表示や内部処理で使う数値ID
mesh_code 8桁の3次メッシュコード
mesh1_code 1次メッシュコード
mesh2_code 2次メッシュコード
center_lon メッシュ中心経度
center_lat メッシュ中心緯度
geometry 標準地域メッシュのポリゴン
bbox BBOX部分読み込み用の範囲列

メッシュコードは計算対象ではなく識別コードなので、整数ではなく文字列で保存します。

Step 2

workspace/artifacts/step2/
├─ native/
│  └─ {dataset_id}.parquet
├─ summary_1km/
│  └─ {dataset_id}__{scenario_id}.parquet
├─ dataset_catalog_fragment.json
├─ manifest.json
└─ validation_report.json

1km集計には次のような列を持たせます。

内容
mesh_code Step 1のメッシュと結合する8桁コード
dataset_id データセットの識別子
scenario_id 計画規模、想定最大規模などの条件
has_hazard 対象区域と交差するか
max_class メッシュ内の最大区分
affected_area_m2 重複を除いた影響面積
affected_area_ratio メッシュ面積に対する影響面積率
feature_count 交差した元地物数

Step 3

workspace/artifacts/step3/
├─ tiles/
│  └─ {dataset_id}__{scenario_id}__{version}.pmtiles
├─ pmtiles_catalog_fragment.json
├─ manifest.json
└─ validation_report.json

PMTilesへ入れる属性は、地図表示とクリックに必要なものだけに絞ります。

feature_id
mesh_code
class_id
value
source_layer

長い説明文、利用条件、複数年分の統計値はPMTilesへ詰め込まず、Step 5のAPIから返します。

Step 4

workspace/artifacts/step4/
├─ frontend_dist/
├─ nginx.conf
├─ range_check_report.json
├─ browser_smoke_report.json
└─ manifest.json

Step 4はPMTilesを作りません。Step 3の成果物をNginxから配信し、OpenLayersで問題なく読めるかを確認します。

Step 5

workspace/artifacts/step5/
├─ disaster.duckdb
├─ openapi.json
├─ api_test_report.xml
├─ manifest.json
└─ validation_report.json

DuckDBには、メッシュ詳細、データセット情報、集計値を登録します。PMTilesへ含めなかった属性を、地物をクリックしたときに取得するためのデータベースです。


manifest.jsonを残す

生成物だけが残っていても、どの入力ファイルと設定から作ったのか分からなければ再現できません。各Stepでは、成果物と一緒にmanifest.jsonを出力します。

{
  "schema_version": "1.0",
  "step": "step2",
  "artifact_version": "2026-08-04_r001",
  "created_at": "2026-08-04T10:00:00+09:00",
  "inputs": [
    {
      "role": "mesh_master",
      "path": "workspace/artifacts/step1/mesh_master_1km.parquet",
      "sha256": "..."
    }
  ],
  "outputs": [
    {
      "role": "hazard_summary_1km",
      "path": "workspace/artifacts/step2/summary_1km/flood__maximum.parquet",
      "sha256": "..."
    }
  ],
  "parameters": {
    "dataset_id": "flood",
    "scenario_id": "maximum"
  },
  "validation": {
    "status": "passed",
    "report": "workspace/artifacts/step2/validation_report.json"
  }
}

最終統合では、validation.statuspassedになっている成果物だけを公開対象にします。JSON Schemaはcontracts/へ置き、各Stepで同じ形式を使います。


Step 1:全国1kmメッシュをGeoParquetへ変換する

最初に作るのは、後続処理の基準になる全国1kmメッシュです。

入力には、e-Statの3次メッシュ境界Shapefileを使います。全国分のファイルを読み込み、KEY_CODEを8桁のmesh_codeへ統一し、GeoParquetへまとめます。

この段階で行う処理は次のとおりです。

Shapefileの再帰検索
必須ファイルとCRSの確認
メッシュコードの文字列化
全国分の結合
重複コードの確認
欠損・不正geometryの確認
GeoParquet 1.1への保存
manifestと検査レポートの作成

GeoParquetは、互換性を優先して次の条件から始めます。

GeoParquet 1.1
geometry encoding : WKB
compression       : ZSTD
covering bbox      : あり

Step 1では、防災区域の集計やPMTiles作成までは行いません。まず共通メッシュだけを安定して作れるようにします。

終わりの目安は次のとおりです。

メッシュ件数が0件ではない
mesh_codeが8桁の数字である
mesh_codeに重複がない
geometryに欠損がない
geometryが有効である
CRSが記録されている
出力したGeoParquetを再読込できる

Step 2:防災データを正規化して1kmメッシュへ集計する

防災データは、提供元やデータ種別によって属性名が異なります。Step 2では設定JSONを用意し、元の列名を共通の列へ置き換えます。

元の属性名
  ↓ 設定JSONで対応付け
共通列
  dataset_id
  scenario_id
  hazard_class
  source_name
  source_version
  reference_date

元ポリゴンはGeoParquetとして残し、別に1kmメッシュ集計を作ります。集計では、最大区分、影響面積、面積率、交差した地物数を計算します。

防災データを扱うときは、次の点を外さないようにします。

元形状を捨てない
計画規模と想定最大規模を混ぜない
区域外を「安全」と記録しない
EPSG:3857で面積を計算しない
重複ポリゴンの面積を二重計上しない

面積計算に使うCRSは対象地域に合わせて決めます。全国を一度に処理する場合でも、Web表示用のEPSG:3857を面積算定に流用しません。

終わりの目安は次のとおりです。

元形状GeoParquetを再読込できる
dataset_idとscenario_idが空でない
面積率が0以上1以下である
最大区分が設定した範囲内である
対象外・区域外・データなしを区別できる
出典と版がcatalog断片へ記録される

Step 3:TippecanoeでPMTilesを作る

Step 2のGeoParquetから、表示に必要な列だけを取り出してPMTilesを作ります。

GeoParquet
  ↓ ogr2ogr
FlatGeobuf
  ↓ Tippecanoe
PMTiles

TippecanoeへGeoParquetを直接渡すのではなく、FlatGeobufを中間形式にします。変換時に列を絞ることで、タイルへ不要な属性が入るのを防ぎます。

ズームレベルは、最初から全国1kmメッシュだけで押し切らず、表示縮尺に合わせて分けます。

Zoom 4~6    都道府県または広域集計
Zoom 7~9    10km・20km集計
Zoom 10~14  1kmメッシュ
Zoom 12以上  元の詳細区域

防災メッシュでは、タイルサイズを小さくするための自動間引きを安易に使いません。地物が落ちると、「危険度が低い」のではなく「タイル作成時に消えた」状態になるためです。低ズームには、あらかじめ粗く集計した別レイヤーを用意します。

終わりの目安は次のとおりです。

PMTilesの構造検査に通る
想定したsource layerが存在する
必須属性がタイル内に残っている
代表地点のタイルを取得できる
ファイル名にデータ版が含まれている
公開中の同名ファイルを上書きしていない

Step 4:NginxとOpenLayersで静的表示する

Step 4では、FastAPIを使わずにPMTilesを表示します。これにより、表示側の問題とAPI側の問題を分けて確認できます。

構成は次のとおりです。

PMTiles
  ↓ HTTP Range Request
Nginx
  ↓
ol-pmtiles
  ↓
OpenLayers

Nginxでは、PMTilesのMIME Type、CORS、長期キャッシュ、Range Requestを設定します。PMTilesファイルへHTTPレベルのgzip圧縮は重ねません。

確認する応答は次のとおりです。

206 Partial Content
Accept-Ranges: bytes
Content-Range
Content-Length
ETag

OpenLayers側では、背景地図切り替え、凡例、レイヤーON/OFF、地物クリックまで作ります。クリック時にはmesh_codefeature_idを取得できるようにしておきます。

終わりの目安は次のとおりです。

Web画面を表示できる
Range Requestが206を返す
地図移動時に必要部分だけが取得される
凡例と表示色が一致する
クリックした地物からmesh_codeを取得できる
FastAPIが停止していても固定レイヤーは表示できる

Step 5:DuckDBとFastAPIを追加する

最後に、詳細情報と統計を返すAPIを追加します。

FastAPIはPMTiles本体を配信しません。固定地図はNginxへ任せ、APIはJSONの取得に絞ります。

最初に用意するエンドポイントは次のとおりです。

GET /api/v1/health
GET /api/v1/datasets
GET /api/v1/datasets/{dataset_id}/stats
GET /api/v1/meshes/{mesh_code}?dataset_id=...

DuckDBには、Step 1のメッシュ属性、Step 2の集計値、データセット情報を登録します。APIからは読み取り専用で開き、データ更新処理と参照処理を分けます。

終わりの目安は次のとおりです。

DuckDBを読み取り専用で開ける
health APIが200を返す
datasets APIがcatalogと一致する
mesh_codeで詳細を1件取得できる
存在しないmesh_codeが404になる
統計件数がStep 2の検査結果と一致する
pytestが通る

最終回:検査済みの成果物をまとめる

最終統合では、各Stepの処理を1つの巨大なPythonファイルへまとめません。検査済みの成果物を公開領域へ移し、WebとAPIを起動します。

ChatGPT Image 2026年8月4日 14_58_55.png

個別実行用のスクリプトは、次の名前へそろえます。

./scripts/run_step1.sh
./scripts/run_step2.sh
./scripts/run_step3.sh
./scripts/run_step4.sh
./scripts/run_step5.sh
./scripts/run_final.sh

全工程を順番に確認するために、別途build_all.shも用意します。

./scripts/build_all.sh

build_all.shは、単に各スクリプトを呼ぶだけにはしません。各Stepのmanifest.jsonと検査結果を確認し、失敗した時点で止めます。


Dockerコンテナの分け方

データ構築用のコンテナは常駐させません。

段階 コンテナ 通常運用で常駐
Step 1 mesh-builder しない
Step 2 hazard-builder しない
Step 3 tile-builder しない
Step 4 web する
Step 5 api する
最終統合 web + api する

データを更新するときだけbuilderを起動し、普段はwebapiだけを動かします。


ソースコードの書き方

このシリーズのPythonソースは、次の方針でそろえます。

Python            3.13
文字コード        UTF-8
改行              LF
型ヒント          公開関数と主要な内部関数へ付ける
docstring         日本語でParameters / Returns / Raisesを記載
パス              pathlib.Pathを使用
ログ              loggingを使用
設定              JSON、環境変数、CLI引数へ分離
終了コード        成功は0、検査失敗は非0
テスト            pytest

コメントは、コードを日本語に言い換えるだけではなく、処理の理由が後から分かるように書きます。

# EPSG:3857はWeb表示には便利ですが、全国の面積計算には向きません。
# 影響面積を求める処理では、対象地域に合った投影座標系へ変換します。
# メッシュコードは算術計算に使う数値ではなく識別子です。
# 先頭ゼロや桁数を保つため、整数型へ変換せず文字列として保存します。

すべての関数へ長い説明を書くのではなく、入出力、例外条件、GIS処理で間違えやすい点を優先してdocstringとコメントへ残します。


公開ファイルは版を付ける

PMTilesは同じ名前で上書きしません。データ条件と版をファイル名へ含めます。

{dataset_id}__{scenario_id}__{source_version}__{build_revision}.pmtiles

例です。

flood__maximum__2025__r001.pmtiles
landslide__warning_zone__2026__r001.pmtiles
mesh_1km__base__estat__r001.pmtiles

更新時は、新しいファイルを作成して検査し、catalog.jsonの参照先を切り替えます。公開中のファイルを直接置き換えないため、問題があった場合は前の版へ戻せます。


防災データとして気を付けること

区域外を「安全」と表示しない

浸水想定区域や警戒区域に含まれないことと、災害が起きないことは同じではありません。画面では「区域外」「対象データなし」「未整備」を分けて表示し、「安全」という表現は使いません。

出典と基準日を表示する

少なくとも次の情報は、データセット情報またはポップアップから確認できるようにします。

データ名称
作成機関
基準日・基準年度
取得日
シナリオ
原典URL
利用条件
処理版

固定データとリアルタイムデータを混ぜない

PMTilesへ向くのは、更新頻度が低い境界や想定区域です。雨量、水位、地震情報、警報、避難情報はFastAPI側へ追加します。10分ごとに変わる値まで、その都度全国PMTilesへ作り直す構成にはしません。


今回の骨格に含めた確認用ソース

全体構成版には、GIS処理の代わりに、実装順とフォルダー構成を確認する軽量スクリプトを入れています。

python3 scripts/show_pipeline.py
python3 scripts/validate_structure.py

まとめて実行する場合は次です。

make check

show_pipeline.pyは各Stepの入力と出力を表示します。validate_structure.pyは、必須パスが存在するかだけでなく、想定したファイルまたはディレクトリになっているかも確認します。

この段階ではGeoPandas、DuckDB、Dockerは不要です。Step 1以降で、それぞれの処理に必要な環境を追加します。


これからの記事

次回からは、次の順番で実装します。

まずはStep 1で、e-Statの3次メッシュShapefileを全国分まとめ、GeoParquetとして再利用できる状態まで作ります。
長丁場になりますが、頑張って作成して行きましょう。

第1回
  e-Stat Shapefileの配置
  Docker環境
  全国ファイルの読み込み
  mesh_codeの正規化
  GeoParquet出力
  件数・CRS・geometry検査

第2回
  防災データ設定JSON
  元形状の正規化
  CRSとgeometryの確認・修復
  1kmメッシュとの空間結合
  影響面積率の計算
  出典と版の管理

第3回
  GeoParquetからFlatGeobufへ変換
  Tippecanoeの設定
  ズーム別レイヤー
  PMTiles作成
  pmtiles verify

第4回
  Vite + OpenLayers
  ol-pmtiles
  Nginx Range Request
  凡例とレイヤー切り替え
  クリック選択

第5回
  DuckDB構築
  FastAPI
  メッシュ詳細API
  統計API
  pytest
  OpenLayersとの連携

最終回
  成果物の昇格
  Docker Compose統合
  一括構築スクリプト
  スモークテスト
  更新・ロールバック手順

参考資料


1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?