maplibre-gl-js の addProtocol で緯線経線を描く

これは [MapLibre Advent Calendar 2024](https://qiita.com/advent-calendar/2024/maplibre) の7日目の記事です。

# はじめに

[maplibre-gl-js](https://maplibre.org/maplibre-gl-js/docs/) で緯度経度のグリッドを描いてみます。

# できあがり

Globe view でも高ズームレベルでも使えそうなものができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/2fc16e46-7a42-24c1-6ac4-7d320076b72d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/8b55f7f5-cd3e-a4bf-6f39-4f4fab597d99.png)


ワーキングデモは <https://frogcat.github.io/maplibre-gl-grid-protocol/> よりどうぞ。

レポジトリはこちらです <https://github.com/frogcat/maplibre-gl-grid-protocol> (英語)

# 解説

## 実装方針

MapTiler の [maplibre-grid](https://github.com/maptiler/maplibre-grid) のような先行事例があります。ただ、[IControl](https://maplibre.org/maplibre-gl-js/docs/API/interfaces/IControl/) の拡張で作られているので Globe view への対応は課題です。 

理論上は **緯線・経線の MVT** を世界のだれかが整備してくれれば、あとは style.json の記述でなんとかなるはずです。ただ、緯線・経線の間隔に対するカスタマイズ要求、項目ラベルのローカライズなど、誰もが納得する MVT を整備するのはなかなか難しそうです。ズームレベルを大きくする場合には [覚悟](https://qiita.com/mg_kudo/items/b3ae4071a69d4026550c) もいります。

今回は [maplibregl.addProtocol](https://maplibre.org/maplibre-gl-js/docs/API/functions/addProtocol/) でカスタムプロトコルを作り、クライアントサイドで動的に生成した MVT を返す仕組みを考えてみました。

## 実装

たとえば [こちらの記事](https://qiita.com/mg_kudo/items/cc88be1ed46fa77871fd) を参照しつつ、以下のような段取りの関数を作ればよさそうです。

1. 関数に渡される引数から地図タイルの x,y,z 座標を取得
2. 当該タイルと交差する緯線・経線を用意する（z に応じて緯線・経線の間隔を変化させる）
3. 緯線・経線を PBF バイナリとしてエンコードする（必要に応じて id や 属性を付与する）
4. 結果を返す (古い API なら callback に渡す、新しい API なら Promise を解決する)

ブラウザ側で PBF バイナリをエンコードするところがやや特殊ですが、あとは難しいところはないはずです。
詳しくはレポジトリのほうを参照してください。


## スタイリング

各 Feature には id (`[news]dddmmss`, 8桁固定整数)と property としての label (`[NEWS] ddd mm ss`, 11文字固定文字列) がセットされるので、 style.json の中で自由にスタイリングが可能です。

grid の生成する MVT には比較的高密度の緯線・経線が含まれるので、style.json 側でズームレベルに応じて filter 句を使った「間引き」をするのが基本的な使用方法になるかと思います。

```json
"layers" : [
   {
      "id": "grid-line",
      "type": "line",
      "source": "grid",
      "source-layer": "line",
      "minzoom": 0,
      "maxzoom": 20,
      "filter": [
        "==",
        [
          "%",
          ["id"],
          [
            "case",
            ["<", ["zoom"], 4],  200000,
            ["<", ["zoom"], 5],  100000,
            ["<", ["zoom"], 6],  50000,
            ["<", ["zoom"], 7],  20000,
            ["<", ["zoom"], 8],  10000,
            ["<", ["zoom"], 9],  3000,
            ["<", ["zoom"], 10], 2000,
            ["<", ["zoom"], 11], 1000,
            ["<", ["zoom"], 12], 500,
            ["<", ["zoom"], 13], 200,
            ["<", ["zoom"], 14], 100,
            ["<", ["zoom"], 15], 30,
            ["<", ["zoom"], 16], 20,
            10
          ]
        ],
        0
      ],
      "paint": {
        "line-color": ["case", ["<", ["id"], 30000000], "#006000", "#600000"],
        "line-width": 20
      }
    },
```

## 問題

以上のやり方でうまくいくケースもあるのですが、こんな問題もあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/e19eda3c-6876-9be5-65ce-9bd4c9e9513f.png)


画像では緯線・経線の密度が南北で異なっていますね。
Globe view ではスクリーン内にズームレベルの異なるタイルがロードされるケースがあるようで、ちょっと注意が必要かと思いました。

- Globe view の場合の `["zoom"]` はどういう意味になるのか？
- `["zoom"]` を使わずに minzoom / maxzoom / filter で書き換えればうまくいく？
- タイル生成側で必要十分な緯線経線を出力 → style.json での filter に頼らないようにする、とか？
- 最終手段は `setFeatureState()` か？

このへんは気になりますが、 maplibre version 5.x の進展と合わせてウォッチしていきたいです。

## 拡張性

スタイル全般は style.json に任せるとして、MVT生成側でカスタマイズしたいであろう項目はこんなところでしょうか。

- 緯度経度の間隔
- ラベルのフォーマット

現状 `GridProtocol.createLoadFn(options)` のようにオプションを渡してカスタマイズする方法は用意しているのですが、以下のように QueryParameter として渡すこともできそうです。絶対に JavaScript のコードを書きたくない人にとってはよい方法かもしれません。

```json
{
  "sources": {
    "grid": {
      "type": "vector",
      "tiles": ["grid://{z}/{x}/{y}?interval=3600"],
      "minZoom": 1,
      "maxZoom": 24
    }
  }
}
```

そもそもこのようなプラグインの需要があるかも定かではないのですが、なにかご希望があれば [Issues](https://github.com/frogcat/maplibre-gl-grid-protocol/issues) にどうぞ。

# まとめ

クライアントサイドでの MVT 生成技術と addProtocol による、style.json でスタイリングが可能な緯線・経線のソリューションの紹介でした。