1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-01-05

はじめに

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

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

箱根強羅_標高タイル由来.png

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

(水域、記号、注記も表示したサンプル)
箱根強羅サンプル_標高タイルベース_水域注記追加版.png

作成したデモサイト

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

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

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

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

(等高線データ由来)
青ヶ島_等高線由来.png
(標高タイル由来)
青ヶ島_標高タイル由来.png

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

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

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

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

その他、共通ですが、以下のような機能を追加しています。

  • ZL15 に限らず、どの ZL でもデータを取得できるように変更
  • 高さの強調機能を実装

(高さを5倍に強調した例)
青ヶ島_高さの強調表示.png

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

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

オリジナルのデータ構造
[
  {
    "path": [
      [139.09853875637054, 35.807888219716645, 340],
      ...
      [139.0976858139038,  35.807864292219804,  340]
    ],
    "properties": {...},
    ...
  },
  ...
]
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 です。

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"]
});

レポジトリ

使用したライブラリ

まずメインは以下の2つです。以前の Mapbox GL JS の代わりに MapLibre GL JS を利用しています。MapLibre もしっかり deck.gl のドキュメントでサポートされています。

また、PMTiles を利用するために、PMTiles、各種処理に便利な turf.js も利用しています。最終的には以下のようになりました。

<!-- 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>

破壊的変更と対応

MapboxLayer の廃止

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

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

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

MapboxLayer has been removed. Use MapboxOverlay instead.

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

MapboxOverlay の利用

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

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

// MapboxOverlay で deck.gl のレイヤを追加する関数
const addLayer = (scale) => {
  // 引数 scale は 高さの強調倍率
  
  // まず、以前の情報を完全に削除する
  // これをしないとレイヤの更新がうまくいかない
  if(g.deckOverlay){
    map.removeControl(g.deckOverlay);
    delete g.deckOverlay;
  }
  // 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 へ追加
  g.deckOverlay.setProps({
    layers: [
      myRoadDeckLayer,
      myRailwayDeckLayer,
      myContourDeckLayer
    ]
  });
  map.addControl(g.deckOverlay);
}

課題

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

  • 以前の MapboxOverlay を完全に削除しないと、更新が反映されない。
    • たとえば、以下のコードで scale を変更する場合、最後の MapboxOverlay.setProps() を行えば済むように見えますが、これではなぜか既存のレイヤが削除できず、結局既存の MapboxOverlay を削除する必要があります。
  • 別々に管理したい複数の MapboxOverlay の制御がうまくいかない。
    • 以下のコードは、3種類の PathLayer を追加するものですが、これとは別に TerrainLayer を追加したく、別の MapboxOverlay を立ててみたのですが、なぜか連動してしまいます。
    • 上記のとおり、MapboxOverlay.setProps() によるアップデートがうまくいかないため、ひとつの MapboxOverlay を setProps() で更新しながら利用するのも難しく、現状良い手立ては見つけられていません。

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

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

おわりに

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

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?