MapLibreでFlatGeobuf形式の町丁目ポリゴンを表示してみる

## はじめに
FlatGeobufファイルがどんなものなのか？気になっていたので、[TerraMap API](https://www.mapmarketing.co.jp/terramap-api?grid=qiita&utm_source=qiita.com&utm_medium=referral&utm_campaign=qiita_flatgeobuf_on_maplibre_185af00a224f4c9730a0_01)でも利用している町丁目ポリゴンをFlatGeobuf形式に変換することで、実際に試すことにしました。作成したFlatGeobufファイルについては、MapLibreでOpenStreetMap上に表示してみました。
### FlatGeobufとは
FlatGeobufとは、GeoJSONを[flatbuffers](https://flatbuffers.dev/)でシリアライズ（バイナリ化）することで圧縮するファイルフォーマットを指します。シェープファイルと異なりファイルサイズやフィールド名の文字数の制限が無いことや、GeoJSONには無い空間インデックスを持つといったメリットがあり、クラウドに最適化（Cloud Optimized）されたものとしても期待されている規格です。

https://flatgeobuf.org/

その他の参考情報
https://gunmagisgeek.com/blog/data/7222
https://www.slideshare.net/slideshow/foss4g-japan-2021-flatgeobuf/250787048

## FlatGeobufファイル作成
FlatGeobufファイルは、QGIS Desktopのベクタレイヤからのエクスポート機能で出力することができ、大量なデータでもQGIS Desktopではかなり軽快に利用することができます。

ちなみに弊社が提供できる行政界ポリゴンについて、同じくエクスポート機能で出力したGeoJSONと比較すると、以下のような圧縮効果がみられました。

| 行政界 | ポリゴン数 | GeoJSON | FlatGeobuf | 圧縮率 |
|:-|-:|-:|-:|-:|
| 都道府県 | 47  | 75MB  | 27MB  | 36%  |
| 市区町村 | 1,896  | 195MB  | 71MB  | 36%  |
| 大字 | 113,116  | 884MB  | 331MB  | 37%  |
| 町丁目 | 182,625  | 964MB  | 368MB  | 38%  |

参考情報
https://www.aeroasahi.co.jp/fun/column/48/

## MapLibreでのポリゴン表示処理
Web地図での使い勝手をみたいので、MapLibreでOpenStreetMap上に町丁目ポリゴンを表示させることにしました。この記事ではMapLibreでFlatGeobufを表示させる方法を知りたいので、ローカルにFlatGeobufファイルを置いて確認しました。

[flatgeobuf.org/examples/maplibre/](https://flatgeobuf.org/examples/maplibre/)には3つのサンプルプログラムが用意されています。町丁目ポリゴンは比較的大きいデータなので、今回は[Filtering a Large Dataset](https://flatgeobuf.org/examples/maplibre/large.html)のソースをアレンジし、以下のHTMLファイルを作成してみました。

```index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>MapLibreでFlatGeobuf形式の町丁目ポリゴンを表示してみる</title>
    <link
      href="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.css"
      rel="stylesheet"
    />
    <script src="https://unpkg.com/maplibre-gl@3.3.1/dist/maplibre-gl.js"></script>
    <script src="https://unpkg.com/underscore@1.13.1/underscore-min.js"></script>
    <script src="https://unpkg.com/flatgeobuf@3.32.0/dist/flatgeobuf-geojson.min.js"></script>
  </head>
  <body>
    <div id="map" style="width: 100%; height: 95vh"></div>
    <script>
      const mapMaxZoom = 18;
      const fgbMinZoom = 9;

      document.addEventListener("DOMContentLoaded", async () => {
        // 背景地図
        const map = new maplibregl.Map({
          container: "map",
          style:
            "https://tile.openstreetmap.jp/styles/osm-bright-ja/style.json",
          center: [139.662, 35.6727],
          zoom: 10,
          maxZoom: mapMaxZoom,
        });

        // バウンディングボックスをFlatGeobufで期待される形式に変換
        function fgbBoundingBox() {
          const { _sw, _ne } = map.getBounds();
          return {
            minX: _sw.lng,
            minY: _sw.lat,
            maxX: _ne.lng,
            maxY: _ne.lat,
          };
        }

        // バウンディングボックス内のポリゴンデータを取得してソースにセット
        async function updateResults() {
          if (map.getZoom() < fgbMinZoom) {
            return;
          };

          let i = 0;
          const fc = { type: "FeatureCollection", features: [] };
          let iter = flatgeobuf.deserialize(
            "chomoku.fgb",     //町丁目ポリゴンのFlatGeobufファイルURL
            fgbBoundingBox()
          );
          for await (let feature of iter) {
            fc.features.push({ ...feature, id: i });
            i += 1;
          }
          map.getSource("polygons").setData(fc);
        }

        map.on("load", () => {
          map.addSource("polygons", {
            type: "geojson",
            data: { type: "FeatureCollection", features: [] },
          });

          map.addLayer({
            id: "polygons-fill",
            type: "fill",
            source: "polygons",
            minzoom: fgbMinZoom,        // 最小ズームレベル
            maxzoom: mapMaxZoom + 1,    // 最大ズームレベル
            paint: {
              "fill-color": "#0000FF",
              "fill-opacity": 0.5,
            },
          });
          map.addLayer({
            id: "polygons-line",
            type: "line",
            source: "polygons",
            minzoom: fgbMinZoom,        // 最小ズームレベル
            maxzoom: mapMaxZoom + 1,    // 最大ズームレベル
            paint: {
              "line-color": "#0000FF",
              "line-opacity": 0.9,
              "line-width": 1,
            },
          });

          // マップが移動する度にデータを更新すると重くなるので、1秒に1回にする
          updateResults = _.throttle(updateResults, 1000);

          // 初期状態でデータを表示
          updateResults();

          // マップが移動する度にデータを更新する
          map.on("moveend", function (s) {
            updateResults();
          });
        });
      });
    </script>
  </body>
</html>
```
:::note
**詳細①**　flatgeobuf-geojson.min.js がFlatGeobufを扱うスクリプトで、以下の箇所ではバウンディングボックスごとにデシリアライズしてます。
```
let iter = flatgeobuf.deserialize(
  "chomoku.fgb",     //町丁目ポリゴンのFlatGeobufファイルURL
  fgbBoundingBox()
);
```
:::
:::note
**詳細②**　FlatGeobufのデシリアライズやポリゴンレイヤの表示に関しては、ズームレベルで制限をかけています。制限が無いと、メモリ使用量がすぐに膨大となりエラーになってしまいます。
```
// バウンディングボックス内のポリゴンデータを取得してソースにセット
async function updateResults() {
  if (map.getZoom() < fgbMinZoom) {
    return;
  };
```
```
map.addLayer({
  id: "polygons-fill",
  type: "fill",
  source: "polygons",
  minzoom: fgbMinZoom,        // 最小ズームレベル
  maxzoom: mapMaxZoom + 1,    // 最大ズームレベル
```
またポリゴンレイヤのmaxzoom値は、背景地図のmaxzoom値 + 1 にすると最大拡大範囲でもポリゴンが表示されます。
:::
## 町丁目ポリゴンの表示結果
実際に町丁目ポリゴンを表示した例が以下になります。
![flatgeobuf_polygons.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2893989/9d40c9ce-f091-6ab6-0df8-480a08ba9e72.gif)
このように、ズームレベルを限定させることにはなりますが、ベクトルタイルのような使い方ができました。また表示されるポリゴンについては、単純化されることは無いのがFlatGeobufのメリットの一つと思われます。