Google Mapでハザードマップを表示するまで２ - GeoJSONをラスタータイルに変換する


『Google Mapでハザードマップを表示するまで』の第２回の記事です

第１回は[こちら](https://qiita.com/mjusui/items/9940a6abeb16e3fc2f59)
第３回は[こちら](https://qiita.com/mjusui/items/e6d2090c307eab87670a)

# データセットの形式と変換

まずは洪水情報を地図に表示できるようにしようということで、以下の２つのデータセットを表示したいのですが

- [国土数値情報 - 洪水浸水想定区域データ](https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-A31-v3_0.html)
- [東京都オープンデータカタログサイト - 浸水予想区域図](https://catalog.data.metro.tokyo.lg.jp/dataset/t000014d0000000029)

この２つのデータセットは、フォーマットが異なるので、フォーマットを統一して表示する必要があります

「国土数値情報」には **GeoJSON** 形式のデータが提供されており、**Google Maps API** には **GeoJSON** をそのままレンダリングする機能があるのですが、実際やってみたところ重くて使い物になりませんでした(データが多すぎたんでしょうね)

そこで **GIS(地理情報システム)** 関係の技術を少し調べたところ、どうやら **ラスタータイル** という形式が速いらしいということで、こちらに変換することにしました

また東京都の公開しているデータの方は、経緯度ごとの浸水深や標高の、単なる **CSV** なので、こちらも同様の形式に変換していきます


# ラスタータイルとは？

ラスタータイルの厳密な定義は他のサイトに譲るとして、簡単に言うと、ズーム倍率に応じて、あらかじめ地図をマス目状に区切って静的な画像にしておくことで、表示時の動的なレンダリング負荷を減らすというもののようです

今回はGoogleマップが表示してくれる地図画像はそのまま使うので、地図の上に浸水深に応じて塗り分ける色のタイルだけ生成すればよいことになります

タイルを細かくすればするほど、浸水が想定される範囲の境界線はよりなめらかになりますが、境界線付近が危険なことに変わりはないので、表示速度とのトレードオフを考慮して **50m x 50m** でタイルを生成することにしました


# GeoJSONについて

GeoJSONの規格は[RFC7946](https://datatracker.ietf.org/doc/html/rfc7946)で決まっています。この規格では、地図上の図形を、頂点座標(経緯度)の配列で表し(ベクター画像の仕組みと同じですね)、図形に関連するメタ情報( **Property** )などをあわせてJSON形式で表現します

GeoJSONでは、点や線、多角形( **Polygon** )など、いくつかの図形を表現するオブジェクトが定義されているのですが、「国土数値情報」では、地図上の範囲を**Polygon** オプジェクトで指定し、その範囲の浸水深を **Property** として付加しています

また、この **Polygon** オブジェクトは、配列の先頭で一番外側の境界線を表現し、配列の２つ目以降で、先頭で指定した範囲の内側に穴( **Hole** )を指定することもできるので、この穴の中は浸水深が変わるので注意が必要です


# ラスタータイルへの変換アルゴリズム

地図上のある点が、図形の範囲内に含まれるかどうかを判定する方法として **Crossing Number Algorithm** という方法があります

これを使って **タイルの中心点が、Polygonに含まれるかどうか** を判定して、タイルの色を決めていきます

JavaScriptで書いたコードの一部を掲載します

<details><summary>Crossing Number Algorithmの実装例</summary>

```js
      const item={};
      feats.forEach((feat, idx)=>{ // feature(ひとつの図形オブジェクトと考えて良い)ごとに処理
        const coors=feat.geometry.coordinates; // 図形の頂点配列のリスト
        const exter=coors[0]; // 外側の境界線
        const holes=coors.slice(1); // 内側の穴(Hole)のリスト

        const props=feat.properties; // 図形のProperty
        let code=(props.A31_201 || props.A31_101); // 国土数値情報で定義された河川コード
        const river=rivers.find( // 河川コードから河川名が取得できるか確認(ある河川では間違ったコードが入っていたので検証しています)
          river => river.code === code
            || river.aliases.includes(code)
        ) || null;
        code=river.code;
        const rank=(props.A31_205 || props.A31_105); // 浸水深のランク(1-6の６段階)

        // 外側の境界の東西南北の端の座標を取り出す
        const extera=exter[0];
        const lng=extera[0];
        const lat=extera[1];
        let west=lng;
        let east=lng;
        let south=lat;
        let north=lat;
        exter.slice(1, -1).forEach(p =>{
          const plng=p[0];
          const plat=p[1];
          if(plng < west){ west=plng; }
          if(east < plng){ east=plng; }
          if(plat < south){ south=plat; }
          if(north < plat){ north=plat; }
        });

        // 上記の東西南北の端を辺とする四角形を50m x 50mのタイルに分割して、タイルごとの色を判定する
        const xmin=Math.floor(west/wid);
        const xmax=Math.ceil(east/wid);
        const ymin=Math.floor(south/hei);
        const ymax=Math.ceil(north/hei);
        for(let x=xmin; x < xmax; x++){
        for(let y=ymin; y < ymax; y++){
          // タイルの東西南北の座標と、中心点の座標を計算する
          const west=(x * wid);
          const east=(west + wid);
          const south=(y * hei);
          const north=(south + hei);
          const ax=(west + (wid / 2) );
          const ay=(south + (hei / 2) );

          // Crossing Number Algorithm
          // タイルの中心点がPolygonに含まれれば1を、含まれなければ0を返す
          const including=(poly)=>{
            let collis=0;
            poly.slice(1).forEach((q, i)=>{
              const p=poly[i];
              const px=p[0];
              const py=p[1];
              const qx=q[0];
              const qy=q[1];

              if(px < ax && qx < ax){
                return;
              }

              if( (py <= ay && ay < qy)
              || (qy <= ay && ay < py) ){
                if(ax < (px + ((qx - px) * (ay - py) / (qy - py)) ) ){
                  collis+=1;
                }
              }
            });
            return (collis % 2);
          };

          // タイルの中心点がPolygonに含まれないとき、タイルは無色(表示されない)なのでタイルを生成しない
          if( !including(exter) ){
            continue;
          }

          // タイルの中心点がHoleに含まれるとき、タイルは無色(表示されない)なのでタイルを生成しない
          if(holes.find(including) ){
            continue;
          }

          // タイルを200 x 200個のブロックにまとめる(検索効率を高めるため、タイルを階層化して保存している)
          const x200=Math.floor(x / 200);
          const y200=Math.floor(y / 200);
          const key=`x200y200-${x200}-${y200}`;

          const tiles=item[key] || [];
          item[key]=tiles;

          let tile=tiles.find(
            tile => tile.x === x
              && tile.y === y
          ) || null;

          // 過去に生成したタイルと同じ座標の場合は、浸水深がより深い方を優先する
          if(tile){
            if(tile.rank < rank){
              tile.rank=rank;
            }
            continue;
          }

          // 過去に生成したタイルと同じ座標のでない場合は、新しくタイルを配列に保存する
          tile={ x, y, x200, y200, west, east, south, north, rank, prio, };
          item[key]=[...tiles, tile, ];
        }}

```

</details>

# 東京都オープンデータの変換

東京都の提供しているデータは、以下のように一行が一つの点に対応するようなCSVになっています

```
図郭No, 浸水深, 地盤高, 緯度, 経度
...
```

これを配列として読み込み、各点が地図上のどのタイルに含まれるかをまず判定し、浸水深から国土数値情報の６段階の色分けと同じ基準でタイルの色を決めていきます

複数の点が一つのタイルに含まれる場合は、[前回の記事で定めた基準](https://qiita.com/mjusui/items/9940a6abeb16e3fc2f59#%E3%83%87%E3%83%BC%E3%82%BF%E8%A1%A8%E7%A4%BA%E3%81%AE%E6%96%B9%E9%87%9D)にしたがって、どの色を採用すべきか判定します

### データ表示の方針(再掲)

- 1.浸水予想区域図(東京都)のデータを最優先
  - 1-1.データが重複する場合は、浸水深がより大きい方を優先
- 2.洪水浸水想定区域図(国)のデータは、年度の新しいものを優先
  - 2-1.年度が同じ場合は、想定最大規模を計画規模より優先
  - 2-2.年度も規模も同じ場合は、浸水深がより大きい方を優先


## 色分けの基準

浸水深を６段階に色分けする際の基準については、国が標準化してくれているので、その基準に合わせて決めます

- [水害ハザードマップ作成の手引き](https://www.mlit.go.jp/river/basic_info/jigyo_keikaku/saigai/tisiki/hazardmap/)

国土数値情報も、この基準に準拠しています

![色分け基準.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416178/30715032-d91f-0db7-aba1-ef71b4fa2b2c.png)「水害ハザードマップ作成の手引き」より抜粋

ちなみに、なぜこの色かというと色覚障がいのある方に配慮して、コントラストの違いが分かるようになっているのだそうです


# おわりに

第２回の記事では、各データをラスタータイルに変化する方法を書きました

次はGoogle Maps APIにタイルを表示する方法を書いて完結する予定です