独自形式 3D データ用のタイルローダーを作った（deck.gl+MapLibre 用）

## はじめに

先の年末年始に地理院地図Vector/最適化ベクトルタイルに標高値を与えて、deck.gl+Mapbox GL JS/MapLibre GL JS で表示させるという取り組みをしていました。

https://qiita.com/mg_kudo/items/6079ac10b7994e391228

https://qiita.com/mg_kudo/items/f6ea34938d5d0f3d326a

こちらの記事で紹介しているサイトは、その場で 3D 用データを作成して表示することにフォーカスしていましたが、今回は、その 3D の表示を予め **タイル分割** された **独自形式** のデータ（JSON）として作っておいて、それを読み込むためのローダーを作ったのでメモしておきます。

なお、以前（４年前）も同じような試行をしていましたが、今回は、ChatGPT を活用し、自分が考えるよりも性能のいいやつをすぐに作ってもらえました。

### デモサイトはこちら

 https://mghs15.github.io/deckgl-tiledjson-loader/

※神戸市新開地駅～谷上駅付近、尾道周辺のサンプルデータを表示可能（ZL13 以上で表示）
※尾道周辺は[三次元電子国土基本図も表示してみました](https://qiita.com/mg_kudo/items/722431f979f846f426e0)

![サンプルイメージ（神戸市すずらん台付近）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/9c038e49-66c8-4769-8dc5-24dd0707b75d.png)

## 機能等の説明

### 前提とする情報

今回のサイトでは、ZL 14 のタイル単位に分割した独自形式の JSON を、地図の動きに合わせて読み込み、deck.gl の PathLayer 等へ設定の上、MapLibre 上で表示させています。

利用しているライブラリは、[MapLibre v5+deck.gl v9](https://qiita.com/mg_kudo/items/f6ea34938d5d0f3d326a) の組み合わせです。

参考ですが、独自形式のデータは、GeoJSON を拡張したようなものとなっています。以下の通り、３次元座標を持つ GeoJSON を、データ種別ごとにメンバーとしてまとめています。（※ Polygon のみ各頂点は２次元で、高さは属性値で代用しています。）

```javascript:（参考）今回利用する独自形式の 3D 用 JSON
{
  "road": [
    {
      "type": "Feature",
      "geometry": {
         "type": "LineString",
         "coordinates": [
           [135.08746, 34.75884, 290],
           ...
           [135.08721, 34.75875, 320]
         ],
      },
      "properties": {...}
    },
    ...
  ], // 道路の Line 型の GoeJSON (Feature の配列)
  "railway": [...], // 鉄道の Line 型の GoeJSON (Feature の配列)
  "symbol": [...], // 記号・注記の Point 型の GoeJSON (Feature の配列)
  ...
  "building": {...} // 建物の Polygon 型の GoeJSON (FeatureCollection)
}
```

### タイルローダー

今回のメイン機能である、地図の動きに合わせてタイルを取得するためのローダーの機能については、地図の動きが止まった時（`moveend` イベント発生時）に使われることを想定し、以下のような流れになっています。

* 地図の範囲から必要なタイル番号を割り出し、
* JSON データを必要な分だけ取得してい、
* 一式データをマージして次の工程へ渡す

これは、MapLibre v5+deck.gl v9 の組み合わせでは、deck.gl の MapboxOverlay と Mapbox GL JS/MapLibre GL JS の `addControl()` を用いる必要があるのですが、MapboxOverlay を更新するには、いったん削除する必要があるので、ローダーにおいても、データを都度追加するのではなく、一式取得してマージしなおす形に割り切っています。

なお、タイルデータ一式を取得しなおすのは無駄であるため、キャッシュ機能も有しています（ChatGPT にオーダーした結果をそのまま実装しています……）。

### 高縮尺用差分データ

また、デモサイトにおいては、3D 表示におけるちょっとした工夫として、ズームを上げると詳細なデータの差分がロードされるという工夫もしています。これは、[3D Tiles の実装](https://cesium.com/blog/2015/08/10/introducing-3d-tiles/#flexible)なども参考にしています。

具体的には、今回のデモサイトでは、基本的に「多くの建物が省略されているデータ」（最適化ベクトルタイル ZL14 由来）を表示していますが、一部地域（すずらん台駅周辺）については、ZL 16 以上にズームした際に、「詳細な建物データ」（最適化ベクトルタイル  ZL 16 由来）を **差分として追加** で読み込み、表示するようにしています。既に ZL 16 未満から表示されている等高線や道路等のデータは差分データには含まれておらず、ZL 16 未満用のものをそのまま流用することで全体のデータ量を抑えています。

* ZL 16 未満では、建物データは省略している
![ZL14のデータのみ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/85c157da-19d4-44c4-a4af-79c66ae0ffeb.png)
* ZL 16 以上では、詳細な建物データを差分として追加で読み込み、表示している（建物データ以外は、ZL 16 未満用で読み込んだものをそのまま用いている）
![ZL14に加えてZL16の建物データの追加表示.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/8388a6bf-eb88-4ba0-8c72-9a329a328206.png)


## 課題

:::note
いったん[解消](https://qiita.com/mg_kudo/items/f6ea34938d5d0f3d326a#%E8%AA%B2%E9%A1%8C)しました（2025/03/25）。
:::

~~以下のような課題を認識しています。~~

* ~~注記の文字が、建物（MapLibre のネイティブレイヤ（fill-extrusion）使用）よりも下に表示されてしまう~~
* ~~動くたびにレイヤを一式作成しなおしているため、動かすたびにデータが一瞬消えてしまってうるさい~~

## レポジトリ

https://github.com/mghs15/deckgl-tiledjson-loader/tree/main

メインのローダー機能は `TileLoader` クラスに集約されています。

## 感想

ChatGPT の力を借りることで、タイルローダーをかなり簡単に実装できました。デモサイトのパフォーマンスとしては、データ形式がかなり雑で肥大化したものでありながら、（データの重さを考慮すれば）意外とさくさく動いているような気がします。