地理院のベクトルタイルを 3D っぽく見せる（2025 新年版）

## はじめに

昨年末に以下のような記事を書きました。

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

こちらは４年半前の成果を記事にまとめてみたものですが、勢いで、最新の deck.gl (v9)、MapLibre GL JS (v5)を利用して、PMTiles で提供されている国土地理院[最適化ベクトルタイル](https://github.com/gsi-cyberjapan/optimal_bvmap)の三次元風の表現を試してみました。

![箱根強羅_標高タイル由来.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/e1588fd4-60f6-d59b-056e-aa27f4845144.png)

:::note
[2025/01/06 追記] 水域（PathLayer）や記号（IconLayer）、注記（TextLayer）を追加しました。注記については、PathLayer を用いて、ピン刺しするようなイメージで、指し示す地点を結んでみました。後述の[標高タイルベースのデモサイト](https://mghs15.github.io/deckgl-3dmap-maplibre/demtile-base/)をご覧ください。
:::

（水域、記号、注記も表示したサンプル）
![箱根強羅サンプル_標高タイルベース_水域注記追加版.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/d2651fe0-9abc-e453-9126-a17363e14be2.png)


## 作成したデモサイトとレポジトリ

以下の通り、2種類作成してみました。

* 従来通り、標高の取得に等高線を用いたもの 

https://mghs15.github.io/deckgl-3dmap-maplibre/contour-base/

* 標高の取得に標高タイルを用いたもの 

https://mghs15.github.io/deckgl-3dmap-maplibre/demtile-base/

レポジトリはこちら。

https://github.com/mghs15/deckgl-3dmap-maplibre


## 使用したライブラリ

まずメインは以下の２つです。以前の Mapbox GL JS の代わりに MapLibre GL JS を利用しています。MapLibre もしっかり deck.gl の[ドキュメントでサポート](https://deck.gl/docs/developer-guide/base-maps/using-with-maplibre)されています。

* [deck.gl (v9)](https://deck.gl/)
* [MapLibre GL JS (v5)](https://github.com/maplibre/maplibre-gl-js)

また、PMTiles を利用するために、[PMTiles](https://github.com/protomaps/PMTiles/tree/main/js)、各種処理に便利な [turf.js](https://turfjs.org/) も利用しています。最終的には以下のようになりました。

```HTML
<!-- deck.gl -->
<script src="https://unpkg.com/deck.gl@^9.0.0/dist.min.js"></script>
<!-- MapLibre -->
<script src="https://unpkg.com/maplibre-gl@5.0.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@5.0.0/dist/maplibre-gl.css" rel='stylesheet' />
<!-- PMTiles -->
<script src="https://unpkg.com/pmtiles@4.1/dist/pmtiles.js"></script>
<!-- turf.js -->
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script>
```

## 主な改良点

### 標高タイルを用いた z 座標付与

今回新たに **国土地理院の[標高タイル](https://maps.gsi.go.jp/development/demtile.html)を用いて各座標に z 値を付与するサイトも追加** しています。あくまで 10 m 刻みの等高線データに比べて、標高タイルを用いたほうがなめらかで見栄えの良い絵に仕上がります。

（等高線データ由来）
![青ヶ島_等高線由来.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/7ca27c8f-9bc1-3d28-ffc1-5812c295649f.png)
（標高タイル由来）
![青ヶ島_標高タイル由来.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/bd79005f-eaec-78d0-4d95-73790e41bdd2.png)

パフォーマンスは、等高線データの速度向上がありますので、標高タイルの方が時間はかかります。等高線使用 ver は、頂点数の他、等高線の密度に応じて速度が遅くなりましたが、標高タイルの場合は、タイルのピクセルを取ってくるだけなので、単純に頂点数に応じて遅くなると考えられます。

なお、座標値の数だけ標高タイルを叩きますので、サーバへのアクセス数が不安でしたが、表示範囲に応じた ZL の標高タイルを使うこととしていますので、観察してみるとブラウザキャッシュが効いており、思ったほどは負荷はかけてないのではないかと思います……。

### 等高線の間引き

従来通りのベクトルタイル内の等高線の頂点から各地物に z 座標を与える方式についてですが、以下の通り改良しています。

* 等高線データに対して、turf.js の `simplify()` を利用して頂点数を削減することで、**品質を犠牲に**パフォーマンスを向上

### 標高の強調機能等

その他、共通ですが、以下のような機能を追加しています。
* ZL15 に限らず、どの ZL でもデータを取得できるように変更
* 高さの強調機能を実装

（高さを５倍に強調した例）
![青ヶ島_高さの強調表示.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/460133e6-0875-e4ab-d0f8-820fe10e65af.png)

:::note
なんか、ダンジョンぽくて見えてかっこいいなと自画自賛しています。青ヶ島の地形は見事ですね。
:::

### GeoJSON 準拠のデータ構造

これまでの実装では、z 座標を付与したラインデータは、以下のとおり deck.gl に渡しやすいようなオリジナルのデータ形式としていましたが、標高タイルを用いる実装の場合、データ構造を GeoJSON に準じたものへ変更しています。

```javascript:オリジナルのデータ構造
[
  {
    "path": [
      [139.09853875637054, 35.807888219716645, 340],
      ...
      [139.0976858139038,  35.807864292219804,  340]
    ],
    "properties": {...},
    ...
  },
  ...
]
```
```JavaScript:GeoJSON に準じた構造
[
  {
    "type": "Feature",
    "geometry": {
      "type": "LineString",
      "coordinates": [
        [139.09853875637054, 35.807888219716645, 340],
        ...
        [139.0976858139038,  35.807864292219804,  340]
      ],
    }
    "properties": {...},
    ...
  },
  ...
]
```

こちらを deck.gl の PathLayer へ渡す際は、以下の通り、`d.path` の代わりに `d.geometry.coordinates` を渡せば OK です。

```JavaScript:PathLayer 実装例
const myRoadDeckLayer = new deck.PathLayer({
  id: 'path-layer-road',
  data: road,
  ...
  getPath: d => d.geometry.coordinates,
  getColor: d => d.properties["color"],
  getWidth: d => d.properties["width"]
});
```



## deck.gl v9 の破壊的変更と対応

### MapboxLayer の廃止

以前の実装もそうでしたが、deck.gl のレイヤを Mapbox GL JS/MapLibre GL JS へ組み込む形で利用しています。以前の deck.gl の v8 以前は、以下の通り、３通りの方法で組み込むことができました。

https://qiita.com/shi-works/items/cd99cabae941facab6e4

> 1. MapboxOverlayを使ったオーバーレイ
> 2. MapboxOverlayを使ったインターリーブ
> 3. MapboxLayerを使ったインターリーブ

以前の実装でも上記の記事でも MapboxLayer を利用していましたが、deck.gl の v9 では、MapboxLayer が廃止されてしまい、MapboxOverlay を利用する必要があります。

https://deck.gl/docs/upgrade-guide#deckglmapbox

> `MapboxLayer` has been removed. Use `MapboxOverlay` instead.

MapboxLayer の場合、Mapbox GL JS/MapLibre GL JS のレイヤと同じようにレイヤの追加・更新・削除ができたので便利でした……。

### MapboxOverlay の利用

MapboxOverlay は、以下の通り Mapbox GL JS/MapLibre GL JS の `addControl()` で管理する必要がありますが、少々厄介でした。とりあえず最低限の動きをするところまで来ました。

```JavaScript
// deck.gl 由来の情報を格納するためのグローバル変数
const g = {};

// MapboxOverlay で deck.gl のレイヤを追加する関数
const addLayer = (scale) => {
  // 引数 scale は 高さの強調倍率

  // MapboxOverlay の作成
  g.deckOverlay = new deck.MapboxOverlay({
    interleaved: true, layers: []
  }); 
  //道路データ
  const myRoadDeckLayer = new deck.PathLayer({
    id: 'path-layer-road',
    data: road,
    pickable: true,
    widthScale: 3,
    widthMinPixels: 1,
    // z 値に scale を掛ける
    getPath: d => d.path.map(v => [v[0], v[1], v[2] * scale]),
    getColor: d => d.properties["color"],
    getWidth: d => d.properties["width"]
  });
  //鉄道
  const myRailwayDeckLayer = new deck.PathLayer({
    /* 省略 */
  });
  //等高線
  const myContourDeckLayer = new deck.PathLayer({
    /* 省略 */
  });
  // MapboxOverlay を MapLibre GL JS の map へ追加
  if(!g.deckOverlay){
    g.deckOverlay = new deck.MapboxOverlay({
      interleaved: true, layers: []
    });
    map.addControl(g.deckOverlay);
  }
  g.deckOverlay.setProps({
    layers: deckLayers
  });
}
```

### 課題

現在、以下のような課題にぶつかっています。レイヤを追加するだけならいいのですが、これらを更新しようとすると、ドキュメントが少なく、困っています。

* 別々に管理したい複数の MapboxOverlay の制御がうまくいかない。
  * 上記のコードは、３種類の PathLayer を追加するものですが、これとは別に TerrainLayer を追加したく、別の MapboxOverlay を立ててみたのですが、なぜか連動してしまいます。

:::note
投稿当初「以前の MapboxOverlay を完全に削除しないと、更新が反映されない。`MapboxOverlay.setProps()` ではうまくレイヤの更新ができない」としていたのですが、３月に試してみたところ、期待通りに更新ができました。当時は何か変なことをしてしまっていたのか分かりませんが、今のところ、上記で例示したようなコードで動いています。
:::

Mapbox GL JS/MapLibre GL JS の `addControl()` の知識も乏しいので、今後勉強しないといけませんね。

何か本件、良い方法などあれば、ご教示いただけますと幸いです……。

## おわりに

４年半前の実装が最新版でも実現可能か確認してみました。いろいろと機能は移り変わるので、勉強は続けないといけませんね……。また、良い方法を見つけ次第、追記したいと思います。

また、実装に当たっては、ChatGPT の支援を受けたほか、以下のレポジトリを参考にさせていただきました。

https://github.com/shiwaku/gsi-vt-railtrcl-3d-maplibre