地理院地図Vector を 3D っぽく見せる

## はじめに

:::note
これは [FOSS4G Advent Calendar 2024](https://qiita.com/advent-calendar/2024/foss4g) 24日目の記事です。
投稿日はすっかり年の瀬 12/30 ですが、FOSS4G Advent Calendar シリーズ 2 に枠が空いておりましたので、せっかくですので 登録させていただきました。
:::


もう４年半も前になりますが、地理院地図Vector のベクトルタイルから取り出したデータに疑似的に標高値を与え、deck.gl+Mapbox GL JS を用いて可視化するという実験をしていたことがあります。

実験で作った成果のサンプル
![サンプル（箱根強羅）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/a9a35d31-1b08-0ef0-4755-5c22e0a1fa03.png)

最近、国土地理院から「[3 次元点群を用いた地図データへの標高値付与ツールの作成](https://www.gsi.go.jp/common/000261092.pdf)」という調査結果が公開されましたが、データへの標高値付与方法として、下記の図のように「面データの属性に標高値を付与」「中心線頂点に標高値を付与」という方針が取られています。

![建築物・中心線への標高値付与と3次元化の構想についての概略図（国土地理院）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/3bc43b70-f64f-b91c-9728-14251175a022.png)
※国土地理院の資料より引用

実は、私が行っていた当時の実験の際にも同様の方法をとっていました。自分の取り組みは捨てたものではなかったなと思いまして、今更ながら、当時のレポジトリを掘り起こして、どのような試行をしていたのか記事に残してみたいと思います。

該当レポジトリはこちら

https://github.com/mghs15/deckgl_3dseamlessmap

:::note
[2025/01/05 追記] 本記事の内容を、各ライブラリ最新版で実装してみた記事を公開しました。標高タイルを用いた実装も追加していますので、こちらも参考にどうぞ。

[地理院のベクトルタイルを 3D っぽく見せる（2025 新年版）](https://qiita.com/mg_kudo/items/f6ea34938d5d0f3d326a)

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

## コンセプト

三次元地図と言えば、（対象領域内で）シームレスな地形の上に、（ある程度）リアルな形状をした建物や構造物が立ち並ぶ、まるで写真のようなイメージをすることでしょう。一方、このようなデータを整備するのは非常にコストがかかります。

当時は地理院地図Vector により、ベクトルタイルが提供され始めた頃でした。ベクトルタイルは当然二次元用のデータで、リアルな三次元地図を作れるような情報はありません。例えば、道路データは「中心線」にすぎず、道路の形状を示したポリゴンデータではありません。

一方、高コストでリッチな三次元地図は無理でも、二次元のデータを「多少立ち上がらせる」くらいはできるのではないかと考えました。そこで、あくまで形状は二次元由来の「線や面」という図形に過ぎないですが、これらを高さを変えて表示した、多少三次元らしい地図を作成してみたわけです。

## 利用したライブラリ

ライブラリは、deck.gl と Mapbox GL JS を利用しました。レポジトリを見ると、以下のようなバージョンの組み合わせであったようです。

* deck.gl v8.1.0
* Mapbox GL JS v1.11.1

こんな感じで設定していました。なお、deck.gl の最新版だと動きません（機能がなくなったわけではなく、取り出し方等が変わった？）。

```HTML: deck.gl と Mapbox GL JS のロード
<script src="https://unpkg.com/deck.gl@^8.1.0/dist.min.js"></script>
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.11.1/mapbox-gl.js'></script>
<script type="text/javascript">
  const {MapboxLayer, ScatterplotLayer, PathLayer, TerrainLayer, PolygonLayer} = deck;
</script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.11.1/mapbox-gl.css' rel='stylesheet' />
```

方法としては、ラインデータについては、deck.gl で PathLayer を作って、Mapbox GL JS へ渡していたようです。設定の `getPath` に三次元座標値 `(x,y,z)` の配列で表現されたラインデータを渡すことで、三次元表現を行います。

```JavaScript: PathLayer の作成と map への追加
// deck.gl の PathLayer を作成する
const myRoadDeckLayer = new MapboxLayer({
  type: PathLayer,
  id: roadLayerID,
  data: road,
  pickable: true,
  widthScale: 3,
  widthMinPixels: 1,
  getPath: d => d.path, // 高さも含めたデータを渡す
  getColor: d => d.properties["color"],
  getWidth: d => d.properties["width"]
});
// Mapbox GL JS の map へレイヤ追加
map.addLayer(myRoadDeckLayer); 
```

:::note 
冒頭の国土地理院の調査研究年報では、道路データについては幅員からさらに面データを作成していますが、ここでは、あくまで道路は中心線からなるラインデータとして取り扱っています。
:::

ポリゴンについては、当時から Mapbox GL JS のスタイルレイヤに `fill-extrusion` という type があり、ポリゴンを立ち上がらせる表現ができましたので、deck.gl は介さずに、これを利用していました。レイヤ設定の `fill-extrusion-height` と `fill-extrusion-base` を用いて、三次元表現を行います。

```JavaScript: fill-extrusion レイヤの map への追加
map.addLayer({
  id: buildingLayerID,
  type: 'fill-extrusion',
  source: buildingSourceID,
  paint: {
    "fill-extrusion-color": ["get", "color"],
    "fill-extrusion-height": ["get", "height"],
    "fill-extrusion-base": ["get", "baseHeight"]
  },
  layout: {}
});
```

## データ構造

データは GeoJSON ベースに独自拡張した JSON 形式のものを準備しました。ターゲットはラインデータとポリゴンデータとなります。

ただでさえ地理院地図Vector のタイルはリッチであるのに加え、三次元表示用の z 値が追加されているため、たいそう重いデータとなりますが、当時は試行錯誤であったため、取り回ししやすい JSON 形式を採用しました。

ラインデータの構造は以下の通りです。GeoJSON では、`geometory` の座標値に z 値 を含んだ `[lng, lat, alti]` という形式をとることができますが、ここでは、deck.gl で利用することを前提に、`path` というメンバーに三次元座標値 `(x,y,z)` の配列を持たせています。属性値等は由来となる地理院地図Vector から維持しています。

```JavaScript:Line 型のデータ
[
  {
    "path": [
      [139.09853875637054, 35.807888219716645, 340],
      ...
      [139.0976858139038,  35.807864292219804,  340]
    ],
    "sourceLayer": "contour",
    "properties": {
      "orgGILvl": "25000",
      "ftCode": 7351,
      "alti": 340,
      "altiFlag": 1
    }
  },
  ...
]
```

ポリゴンデータの構造は以下の通り、GeoJSON に完全準拠しています。座標値も二次元です。三次元表示のために重要なのは、`properties` に持っている `alti` という属性値になります。その他の属性値等は、地理院地図Vector 由来なのはラインデータと同様です。

```JavaScript:Polygon 型のデータ
[
  {
    "geometry": {
      "type": "Polygon",
      "coordinates": [
        [
          [139.09691333770752, 35.80896930000682],
          ...
          [139.09691333770752, 35.80896930000682]
        ]
      ]
    },
    "type": "Feature",
    "properties": {
      "orgGILvl": "2500",
      "ftCode": 3101,
      "lvOrder": 0,
      "alti": 340
    },
    "tile": {"z": 15,"x": 29044,"y": 12888}
  },
  ...
]
```

最後に、データをひとつの JSON ファイルにまとめるにあたり、以下のように各データ名を割り当てています。対象にしているデータの種類は 等高線、道路、鉄道、建物です。

```JavaScript
{
  "contour": [...], // 等高線の Line 型のデータ
  "road": [...], // 道路の Line 型のデータ
  "railway": [...], // 鉄道の Line 型のデータ
  "building": {
    "type": "FeatureCollection",
    "features": [...], // 建物の Polygon 型のデータ
  }
}
```

## データの作成

あくまで参照実装でありましたので、かなり雑に標高値を付与しております。もとにしたのは地理院地図Vector に入っている等高線データで、この等高線データの頂点を「疑似点群」のように用いて、ラインデータの頂点やポリゴンデータに標高を割り当てました。
* 等高線：データに含まれる `alti` 属性を使用。
* その他ラインデータ：LineString の頂点ごとに最近傍の等高線頂点を探し、その `alti` 属性を LineRtring の頂点に付与。
* ポリゴンデータ：Polygon の代表点を計算し、その代表点と最近傍の等高線頂点を探し、その `alti` 属性を Polygon の属性値として付与。


なお、建物については、高さデータはどうしても取得できない（地表の標高ではなく、「表層」の高さデータが必要）ので、建物種別に応じて、適当な数字を付与しています。

## 実際の表示（デモサイト）

上記の設計を用いて作成したのが以下のサイトです。好きな場所で地理院地図Vector のデータから（なんちゃって）三次元データを表示できるサイトとなります。

https://mghs15.github.io/deckgl_3dseamlessmap/indexGet.html

使い方は以下の通りです。

1. まずは3次元表示したい範囲を決めます。 ZL15 のタイル6枚分までの範囲に画面を絞らないと次のステップで「範囲が広すぎる」旨表示されます。
![三次元表示したい範囲を決める.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/cc4ce898-133b-b920-216e-93f12ecf837a.png)
2. 左上の「3D」ボタンを押すと変換が始まります。何も反応が起こりませんが、中では変換が進んでいます。負荷が大きいので、たびたび下記のように応答しなくなりますが、「待機」で様子を見てください。
![ページが応答しなくても待つpng.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/7839a3e6-e2c3-25eb-2f40-8f8197f898dc.png)
3. 変換が終了すると、データが地図上に表示されます。地図を自由に動かして見ることができます。
![しばらく待つと変換されたデータが表示される.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/2e6a1efc-e78f-5270-7981-dd2b85e822d8.png)
![完成した様子.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/d0c3a470-0697-90b7-887b-2bdf23d012ef.png)

:::note
なお、見た目の関係で、優先度に応じて少し高さを調整しています（表示の際に、道路は 5 m、鉄道は 5.3 m 高さを加えています）。このあたりも、実在の世界を写真のように表現する 3D のイメージと異なり、記号化・転位・総錨等の地図編集が行われる二次元地図よりの発想になりますね。
また、標高データの付与方法が雑というのはありますが、水平方向に比べて高さ方向の感度が低いため、見た目を重視しようとすると、調整する標高の数字はどうしても思い切ったものになりますね。
:::

## 課題

課題も何もあったもんではない雑さ加減ですが、課題を列挙してみます。冒頭の国土地理院の調査研究年報に記載されている課題もありますが、自分の実験の場合、主に

* 地形の標高データしか利用できない
* 元データがタイルであるためデータが分割されている
* タイル内でも断片化されているデータが多い

というところを原因とする不具合が多いです。

細かいところだと、以下のような課題があります。

* ラインデータに不整合が生じる（特にタイル境界）
* 橋梁については、途中に頂点があると谷底のデータを拾ってしまい、不整合が生じる
* トンネルについては、正確な表示はできない（表面にはりつく）
* タイル境界で変な線が発生する
* 建物データが実際の高さではない（データ種別ごとに一律の高さを付与している）
* 水面のような勾配のあるポリゴンデータの表示が行えていない

また、今回のデータは JSON で作成していますが、非常に重いです。一応、タイルデータに分けて読み込む方法なども考えてはみましたが、4タイルずつくらいの表示にとどめないとしんどいです。

例：筑波山周辺の三次元表示（１度に表示されるデータを表示位置周辺の ZL15 のタイル４枚相当に制限）※初期表示から少し近づくように動くとデータが表示されます

https://mghs15.github.io/deckgl_3dseamlessmap/index_limit.html

どのような方法がベストかはまだ研究が足りませんが、以下のようなアイデアが考えられます。

* **Mapbox Vector Tile を拡張し、三次元座標をとれるようにする**
  * 今回の取り組みは、ベクトルタイルの座標値に z 値を付け足すものでしたので、同様に Mapbox Vector Tile に z 値 を入れるのでも良いのでは？という発想です。ただ、独自に活用するとガラパゴス化する可能性は高いです。
  * 実は、[Mapbox Vector Tile](https://github.com/mapbox/vector-tile-spec) の次世代（V3）で z 値をサポートするとかしないとか話が出ていたような気がするのですが、V3 の作業自体があまり動いていなさそうです。
  * なお、MapLibre では [MapLibre Tiles (MLT)](https://github.com/maplibre/maplibre-tile-spec) というプロジェクトが動いているようで、そこでは三次元座標のサポートも謳われています。
* **三次元データに適する形式（.obj 等）に変換の上、タイルで配信する**
  * これは、Mapbox GL JS v3 リリースの際に追加された新スタイルで行われている方法です。各都市トレードマークとなる建物データがタイル状に分割された b3dm 形式のデータとして配信されています（詳細は以下の記事をご覧ください）。この方法ならば、例えば勾配のある水面や複雑な形状の建物データも表現できるかもしれません。

https://qiita.com/mg_kudo/items/92283d017f7528f06c5e

また、当時はベクトルタイルの等高線のデータをもとに標高値を付与しましたが、普通に標高タイルを用いる方が精度や速度が向上する、ということも考えられます。

冒頭でも記載しましたが、各ライブラリ最新版で標高タイルを用いた実装を試してみたので、改めてリンクをご紹介します。

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

## おまけ（地形表現）

実は deck.gl には、標高データを用いて地形を表現できる TerrainLayer なるものがあります。今回紹介したレポジトリでは、国土地理院の標高タイルと全国最新写真（シームレス）を用いて、地形の表現を行っています。なお、国土地理院の標高タイルのデコード方法には対応していないため、標高が無効又はマイナスである地点では表示がおかしくなります。

また、上記のラインデータ・ポリゴンデータの三次元表示と組み合わせて表示すると、埋まって見える等の齟齬があります。

```JavaScript: TerrainLayer の作成と map への追加
const myTerrainDeckLayer = new MapboxLayer({
  id: 'terrain-layer',
  type: TerrainLayer,
  elevationDecoder: {
    rScaler: Math.pow(2,16)*0.01,
    gScaler: Math.pow(2,8)*0.01,
    bScaler: 0.01,
    offset: 0
  },
  elevationData: 'https://cyberjapandata.gsi.go.jp/xyz/dem_png/{z}/{x}/{y}.png',
  texture: 'https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg',
  color: [100,155,100]
});    
map.addLayer(myTerrainDeckLayer);
```
![Terrainを表示した様子.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/d6583e28-201b-43ab-79c7-c4cc80d5f0bf.png)

## おわりに

「二次元の図形に座標値を付与して三次元らしく見せる」というコンセプトについて掘り起こしてみました。

なお、当時の実装そのままに紹介しておりますが、標高値の付与の方法だったり、データの形式だったり、もう少し色々と工夫ができそうな気がします。

当時からは状況が進んで、Mapbox GL JS でも v3 で三次元表示に関わるようなスタイル設定が増えていっているように見受けられます。MapLibre GL JS の方も開発は活発そうですし、MapLibre Tiles の動きも今後が楽しみです。

今後、Web 地図における三次元表示はますます簡便になりつつ、求められることもより高度になっていくかもしれません。今後の動きにも注目していきたいです。