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?

全国の津波浸水想定(生1.8GB)を“静的サイト+ブラウザだけ”で動かす

1
Posted at

全国対応の津波避難マップ( https://tsunami-hinan.it-study.jp )を個人開発しています。
このアプリはバックエンドなし(静的ファイルをFFFTPで上げるだけ)で動いていますが、扱うデータは重い。中でも津波浸水想定区域は、生データで合計1.8GB超ありました。

これをどうやってブラウザに載せたか、という話です。要点は3つ。

  1. データを 県ごとに分割し、bbox索引で「今見ている/現在地の県」だけ遅延読込する
  2. 重いポリゴン/点群を ラスター化 → ポリゴン化 → 簡素化 で各県数MBに圧縮する
  3. 「浸水域の判定」と「未収録地域の判定」をブラウザ側の point-in-polygon でやる

課題:全国の浸水想定は素直に載せると即死

津波浸水想定は国土数値情報の A40(沿岸都道府県ごとのポリゴン)で配布されています。が、

  • GeoJSONにすると合計 約1.8GB(1ポリゴン=約10mセルの極小ポリゴンが数百万個)
  • 宮城だけは別ソースの 10mメッシュ点群(約391万点・投影座標)

サーバーは静的ホスティング(FFFTPで上げるだけ)。バックエンドで間引けない。当然、1ファイルでブラウザに渡せばモバイルはメモリ不足で落ちます。

そこで「必要な分だけ・軽くして・その場で」の3点で攻めました。

1. 県別分割 + bbox索引で遅延読込

浸水域は県ごとに data/zones/<県コード>.json に分割。さらに県→bboxの索引を1つ持ちます。

// data/zones/index.json (起動時に読むのはこれだけ・約1.7KB)
{ "04": [140.74, 37.77, 141.68, 38.97], "39": [132.70, 32.70, 134.31, 33.89], ... }

フロントは、表示範囲や現在地とbboxが交差する県だけを fetch します。

// 矩形(bounds)と交差する県コード
function prefsForBounds(b, index) {
  const codes = [];
  const w = b.getWest(), s = b.getSouth(), e = b.getEast(), n = b.getNorth();
  for (const code in index) {
    const [minLon, minLat, maxLon, maxLat] = index[code];
    if (!(maxLon < w || minLon > e || maxLat < s || minLat > n)) codes.push(code);
  }
  return codes;
}

// 県ファイルを必要時にだけ取得して蓄積(多重取得も防ぐ)
const loaded = {}, inflight = {};
const zones = { type: 'FeatureCollection', features: [] };
function ensureZones(codes) {
  return Promise.all(codes.map(code => {
    if (loaded[code]) return;
    if (inflight[code]) return inflight[code];
    return inflight[code] = fetch(`data/zones/${code}.json`)
      .then(r => r.json())
      .then(fc => {
        loaded[code] = true;
        fc.features.forEach(f => zones.features.push(f));
        zoneLayer.addData(fc);   // Leaflet レイヤに追加
      }).catch(() => { delete inflight[code]; });
  }));
}

// 地図移動で、その範囲の県を読み込む(広域すぎる時は読まない)
map.on('moveend', () => {
  if (map.getZoom() < 9) return;
  ensureZones(prefsForBounds(map.getBounds(), zoneIndex));
});

起動時に読むのは索引(1.7KB)だけ。実データは**見た県・行った県の分(各0.2〜4MB)**しか落ちてきません。全国49MBあっても、ユーザーが実際に転送するのはごく一部です。

2. ラスター化 → ポリゴン化で各県を数MBへ

極小ポリゴンが数百万個、では重い。そこで一度ラスター(格子)に焼き直してから、輪郭をポリゴンに起こすと劇的に軽く・速くなります。rasterio を使います。

import numpy as np
from rasterio import features
from affine import Affine
from shapely.geometry import shape, mapping, Polygon, MultiPolygon
from shapely.ops import unary_union

RES = 0.00022  # 解像度(度)≒ 約20m

# geoms: A40の各ポリゴン(経度緯度)。bboxから格子サイズを決める
transform = Affine(RES, 0, minx, 0, RES, miny)
mask = features.rasterize(
    ((g, 1) for g in geoms),
    out_shape=(nrows, ncols), transform=transform,
    fill=0, all_touched=True, dtype="uint8",
)

# 格子 → ポリゴン化(C実装で高速)
polys = [shape(geom) for geom, val
         in features.shapes(mask, mask=mask.astype(bool), transform=transform)
         if val == 1]

merged = unary_union(polys)
# 川・湖など内側の穴を埋める(後述)
merged = MultiPolygon([Polygon(g.exterior) for g in merged.geoms])
merged = merged.simplify(0.00018, preserve_topology=True)   # 約16m

ポイントは、rasterizefeatures.shapes も C実装なので、ポリゴン数が数百万でも数秒〜十数秒で終わること。簡素化まで入れて、

  • 宮城(10mメッシュ点群 532MB相当)→ 約2〜4MB
  • 各県 → 0.2〜4MB、全国合計 約49MB

まで圧縮できました。点群だった宮城は、座標を投影座標から緯度経度へ再投影してから同じパイプラインに乗せています。

穴埋めについて:浸水データは川・湖が「対象外=穴」になっており、素直に使うと避難経路探索が川の中で止まることがあります。Polygon(g.exterior) で内側リングを落として水域を浸水域に取り込む(=危険側に倒す)ことで、現実的な経路だけ出るようにしています(詳細は別記事)。

3. 判定もブラウザで:浸水域内/外+「未収録」

読み込んだ県の MultiPolygon に対して、レイキャスティングの point-in-polygon で内外を判定します。

function pointInRings(lng, lat, rings) {
  let inside = false, r = rings[0];
  for (let i = 0, j = r.length - 1; i < r.length; j = i++) {
    const xi = r[i][0], yi = r[i][1], xj = r[j][0], yj = r[j][1];
    if (((yi > lat) !== (yj > lat)) &&
        (lng < (xj - xi) * (lat - yi) / (yj - yi) + xi)) inside = !inside;
  }
  return inside;
}

ここで地味に大事なのが「データが無い地域を“安全”と誤判定しない」こと。香川県などはA40のベクターが非公開で、浸水域データがありません。索引に無い=判定できない、を「域外=安全」と出すのは危険です。

そこで簡素化した都道府県境界prefectures.json・約720KB)を持ち、起点がどの県かを point-in-polygon で求め、その県の浸水域データが無ければ「浸水想定データ未収録」と明示します。

const code = prefectureAt(lat, lng);        // 県を判定
if (code && !zoneIndex[code]) showNoDataNotice();   // 未収録 → 警告
else if (isInsideZone(lat, lng)) showEvacuation();  // 域内
else showSafeNotice();                              // 域外

まとめ

  • 重い地理データは「県別分割+bbox索引+遅延読込」でブラウザに優しくなる
  • 極小ポリゴン/点群は「ラスター化→ポリゴン化→簡素化」で桁違いに軽くなる(rasterio が強い)
  • 判定はクライアントの point-in-polygon で完結。バックエンド不要・静的ホスティングだけで全国対応できる

この仕組みで、全国の津波浸水想定をのせた避難マップが、サーバーレスで動いています。
👉 津波避難マップ(無料・登録不要・オフライン対応): https://tsunami-hinan.it-study.jp

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?