MapLibreでPMTiles形式の町丁目ベクトルタイルを表示してみる

## はじめに
[TerraMap API](https://www.mapmarketing.co.jp/terramap-api?grid=qiita&utm_source=qiita.com&utm_medium=referral&utm_campaign=qiita_pmtiles_on_maplibre_f7614c3e3e5d11a59bbe_01)でも利用している町丁目ポリゴンを、tippecanoeでPMTiles形式に変換し、Web地図上に表示してみることにしました。作成したPMTilesファイルについては、MapLibreでOpenStreetMap上に表示しています。
### PMTilesとは
単一ファイル形式のタイルデータです。種類には背景地図、ベクトルタイル、画像等があり、S3のようなストレージサービスにホストすることができます。サーバープログラムは不要で、低コストでタイルデータを提供することができるとされています。

https://docs.protomaps.com/pmtiles/

## PMTilesファイル作成
以下の手順で、町丁目ポリゴンのGeoJSONファイルからベクトルタイルのPMTilesファイルを作成しました。作業OSはWindowsで、GitやDockerが使えることが前提の手順になります。
1. tippecanoeのソースを取得
最新tippecanoeのDockerイメージが共有されているかは分からなかったので、まず現在も更新され続けている以下のソースを取得しました。
https://github.com/felt/tippecanoe

1. Dockerイメージのビルド
READMEに従って、Dockerイメージのビルドから実行していきます。
    ```
    docker build -t tippecanoe:latest .
    ```
1. コンテナの起動
    ```
    docker run --name pmtiles-conv -it ^
    -v {ホスト側の作業ディレクトリパス}:/data ^
    tippecanoe:latest
    ```
1. GeoJSONをPMTilesへ変換
    ```
    tippecanoe -zg -pk -pn -l chomoku-layer -o /data/chomoku.pmtiles /data/chomoku.geojson
    ```
    tippecanoeのオプションについてはケースバイケースと思いますが、今回は以下を使用しています。

    | オプション | 概要 |
    |:-:|:-|
    | -zg  | ズームレベル0から妥当な最大ズームレベルまでのタイルを作成  |
    | -pk  | タイルを500Kバイトに制限しない  |
    | -pn  | フィーチャ間で共有しているノードは簡素化しない  |
    | -l  | `-l {レイヤー名}` でレイヤー名を指定<br>※ 省略した場合はGeoJSONのファイル名になる  |
    | -o  | `-o {ファイル名}` で出力ファイル名を指定 |

   処理の結果、980MBあったGeoJSONが、77MBのPMTilesに変換されました。 
   
## MapLibreでのPMTiles表示処理
この記事ではMapLibreでPMTilesを表示させる方法を知りたいので、ローカルにPMTilesファイルを置いて確認しました。ソースは以下のようになりました。

```index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>MapLibreで町丁目ポリゴンのPMTilesを表示</title>
    <script src="https://unpkg.com/maplibre-gl@4.7.0/dist/maplibre-gl.js"></script>
    <link
      href="https://unpkg.com/maplibre-gl@4.7.0/dist/maplibre-gl.css"
      rel="stylesheet"
    />
    <script src="https://unpkg.com/pmtiles@3.2.0/dist/pmtiles.js"></script>
  </head>
  <body>
    <div id="map" style="width: 100%; height: 95vh"></div>
    <script>
      const protocol = new pmtiles.Protocol();
      maplibregl.addProtocol("pmtiles", protocol.tile);

      const map = new maplibregl.Map({
        container: "map",
        center: [139.66204291, 35.67260863],
        zoom: 10,
        maxZoom: 17,
        style: {
          version: 8,
          sources: {
            osm: {
              type: "raster",
              tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
              tileSize: 256,
              attribution:
                '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
            },
          },
          layers: [
            {
              id: "osm-layer",
              source: "osm",
              type: "raster",
            },
          ],
        },
      });

      map.addControl(new maplibregl.NavigationControl());

      map.on("load", function () {
        map.addSource("mvt_data", {
          type: "vector",
          tiles: [
            `pmtiles://chomoku.pmtiles/{z}/{x}/{y}`,
          ],
          minzoom: 0,
          maxzoom: 11,    // タイルがある最大ズームレベルを設定
        });

        map.addLayer({
          id: "mvt_polygon",
          type: "fill",
          source: "mvt_data",
          "source-layer": "chomoku-layer",   // PMTilesのレイヤー名
          layout: {},
          paint: {
            "fill-color": "#7fffd4",
            "fill-opacity": 0.2,
          },
        });

        map.addLayer({
          id: "mvt_line",
          type: "line",
          source: "mvt_data",
          "source-layer": "chomoku-layer",   // PMTilesのレイヤー名
          layout: {},
          paint: {
            "line-color": "#2e8b57",
            "line-width": 0.5,
          },
        });
      });
    </script>
  </body>
</html>
```
:::note
**詳細①**　PMTilesを扱うにはpmtiles.jsを利用します。
```
<script src="https://unpkg.com/pmtiles@3.2.0/dist/pmtiles.js"></script>
```
PMTilesをロードするためのプロトコルを追加します。
```
const protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
```
:::
:::note
**詳細②**　tilesのURLは、プロトコル名pmtilesを先頭に付与し、`pmtiles:PMTilesファイルのURL/{z}/{x}/{y}`という形式で指定します。
```
tiles: [
  `pmtiles://chomoku.pmtiles/{z}/{x}/{y}`,
],
```
:::

## 町丁目ポリゴンの表示結果
実際に町丁目ポリゴンを表示した例が以下になります。
![pmtiles_polygons.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2893989/07db9e73-5f14-9df8-e3b1-d298b5e70268.gif)
ローカル環境ではありますが、静的なファイルなのでパフォーマンスに優れているのが分かると思います。
[町丁目ポリゴンをFlatGeobuf形式に変換](https://qiita.com/y-mto/items/185af00a224f4c9730a0)したこともありますが、それと比べても高速でメモリ使用量の心配もほとんどありません。これはtippecanoeによるズームレベルに応じたポリゴンの簡素化に起因していると思われます。

## その他の参考情報

https://maplibre.org/maplibre-gl-js/docs/examples/pmtiles/

https://github.com/protomaps/PMTiles/blob/main/js/examples/maplibre.html

https://qiita.com/mg_kudo/items/6fa899f4532ba2fe879b