はじめに
これは FOSS4G Advent Calendar 2024 24日目の記事です。
投稿日はすっかり年の瀬 12/30 ですが、FOSS4G Advent Calendar シリーズ 2 に枠が空いておりましたので、せっかくですので 登録させていただきました。
もう4年半も前になりますが、地理院地図Vector のベクトルタイルから取り出したデータに疑似的に標高値を与え、deck.gl+Mapbox GL JS を用いて可視化するという実験をしていたことがあります。
最近、国土地理院から「3 次元点群を用いた地図データへの標高値付与ツールの作成」という調査結果が公開されましたが、データへの標高値付与方法として、下記の図のように「面データの属性に標高値を付与」「中心線頂点に標高値を付与」という方針が取られています。
実は、私が行っていた当時の実験の際にも同様の方法をとっていました。自分の取り組みは捨てたものではなかったなと思いまして、今更ながら、当時のレポジトリを掘り起こして、どのような試行をしていたのか記事に残してみたいと思います。
該当レポジトリはこちら
コンセプト
三次元地図と言えば、(対象領域内で)シームレスな地形の上に、(ある程度)リアルな形状をした建物や構造物が立ち並ぶ、まるで写真のようなイメージをすることでしょう。一方、このようなデータを整備するのは非常にコストがかかります。
当時は地理院地図Vector により、ベクトルタイルが提供され始めた頃でした。ベクトルタイルは当然二次元用のデータで、リアルな三次元地図を作れるような情報はありません。例えば、道路データは「中心線」にすぎず、道路の形状を示したポリゴンデータではありません。
一方、高コストでリッチな三次元地図は無理でも、二次元のデータを「多少立ち上がらせる」くらいはできるのではないかと考えました。そこで、あくまで形状は二次元由来の「線や面」という図形に過ぎないですが、これらを高さを変えて表示した、多少三次元らしい地図を作成してみたわけです。
利用したライブラリ
ライブラリは、deck.gl と Mapbox GL JS を利用しました。レポジトリを見ると、以下のようなバージョンの組み合わせであったようです。
- deck.gl v8.1.0
- Mapbox GL JS v1.11.1
こんな感じで設定していました。なお、deck.gl の最新版だと動きません(機能がなくなったわけではなく、取り出し方等が変わった?)。
<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)
の配列で表現されたラインデータを渡すことで、三次元表現を行います。
// 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);
冒頭の国土地理院の調査研究年報では、道路データについては幅員からさらに面データを作成していますが、ここでは、あくまで道路は中心線からなるラインデータとして取り扱っています。
ポリゴンについては、当時から Mapbox GL JS のスタイルレイヤに fill-extrusion
という type があり、ポリゴンを立ち上がらせる表現ができましたので、deck.gl は介さずに、これを利用していました。レイヤ設定の fill-extrusion-height
と fill-extrusion-base
を用いて、三次元表現を行います。
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 から維持しています。
[
{
"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 由来なのはラインデータと同様です。
[
{
"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 ファイルにまとめるにあたり、以下のように各データ名を割り当てています。対象にしているデータの種類は 等高線、道路、鉄道、建物です。
{
"contour": [...], // 等高線の Line 型のデータ
"road": [...], // 道路の Line 型のデータ
"railway": [...], // 鉄道の Line 型のデータ
"building": {
"type": "FeatureCollection",
"features": [...], // 建物の Polygon 型のデータ
}
}
データの作成
あくまで参照実装でありましたので、かなり雑に標高値を付与しております。もとにしたのは地理院地図Vector に入っている等高線データで、この等高線データの頂点を「疑似点群」のように用いて、ラインデータの頂点やポリゴンデータに標高を割り当てました。
- 等高線:データに含まれる
alti
属性を使用。 - その他ラインデータ:LineString の頂点ごとに最近傍の等高線頂点を探し、その
alti
属性を LineRtring の頂点に付与。 - ポリゴンデータ:Polygon の代表点を計算し、その代表点と最近傍の等高線頂点を探し、その
alti
属性を Polygon の属性値として付与。
なお、建物については、高さデータはどうしても取得できない(地表の標高ではなく、「表層」の高さデータが必要)ので、建物種別に応じて、適当な数字を付与しています。
実際の表示(デモサイト)
上記の設計を用いて作成したのが以下のサイトです。好きな場所で地理院地図Vector のデータから(なんちゃって)三次元データを表示できるサイトとなります。
使い方は以下の通りです。
- まずは3次元表示したい範囲を決めます。 ZL15 のタイル6枚分までの範囲に画面を絞らないと次のステップで「範囲が広すぎる」旨表示されます。
- 左上の「3D」ボタンを押すと変換が始まります。何も反応が起こりませんが、中では変換が進んでいます。負荷が大きいので、たびたび下記のように応答しなくなりますが、「待機」で様子を見てください。
- 変換が終了すると、データが地図上に表示されます。地図を自由に動かして見ることができます。
なお、見た目の関係で、優先度に応じて少し高さを調整しています(表示の際に、道路は 5 m、鉄道は 5.3 m 高さを加えています)。このあたりも、実在の世界を写真のように表現する 3D のイメージと異なり、記号化・転位・総錨等の地図編集が行われる二次元地図よりの発想になりますね。
また、標高データの付与方法が雑というのはありますが、水平方向に比べて高さ方向の感度が低いため、見た目を重視しようとすると、調整する標高の数字はどうしても思い切ったものになりますね。
課題
課題も何もあったもんではない雑さ加減ですが、課題を列挙してみます。冒頭の国土地理院の調査研究年報に記載されている課題もありますが、自分の実験の場合、主に
- 地形の標高データしか利用できない
- 元データがタイルであるためデータが分割されている
- タイル内でも断片化されているデータが多い
というところを原因とする不具合が多いです。
細かいところだと、以下のような課題があります。
- ラインデータに不整合が生じる(特にタイル境界)
- 橋梁については、途中に頂点があると谷底のデータを拾ってしまい、不整合が生じる
- トンネルについては、正確な表示はできない(表面にはりつく)
- タイル境界で変な線が発生する
- 建物データが実際の高さではない(データ種別ごとに一律の高さを付与している)
- 水面のような勾配のあるポリゴンデータの表示が行えていない
また、今回のデータは JSON で作成していますが、非常に重いです。一応、タイルデータに分けて読み込む方法なども考えてはみましたが、4タイルずつくらいの表示にとどめないとしんどいです。
例:筑波山周辺の三次元表示(1度に表示されるデータを表示位置周辺の ZL15 のタイル4枚相当に制限)※初期表示から少し近づくように動くとデータが表示されます
どのような方法がベストかはまだ研究が足りませんが、以下のようなアイデアが考えられます。
-
Mapbox Vector Tile を拡張し、三次元座標をとれるようにする
- 今回の取り組みは、ベクトルタイルの座標値に z 値を付け足すものでしたので、同様に Mapbox Vector Tile に z 値 を入れるのでも良いのでは?という発想です。ただ、独自に活用するとガラパゴス化する可能性は高いです。
- 実は、Mapbox Vector Tile の次世代(V3)で z 値をサポートするとかしないとか話が出ていたような気がするのですが、V3 の作業自体があまり動いていなさそうです。
- なお、MapLibre では MapLibre Tiles (MLT) というプロジェクトが動いているようで、そこでは三次元座標のサポートも謳われています。
-
三次元データに適する形式(.obj 等)に変換の上、タイルで配信する
- これは、Mapbox GL JS v3 リリースの際に追加された新スタイルで行われている方法です。各都市トレードマークとなる建物データがタイル状に分割された b3dm 形式のデータとして配信されています(詳細は以下の記事をご覧ください)。この方法ならば、例えば勾配のある水面や複雑な形状の建物データも表現できるかもしれません。
また、当時はベクトルタイルの等高線のデータをもとに標高値を付与しましたが、普通に標高タイルを用いる方が精度や速度が向上する、ということも考えられます。
おまけ(地形表現)
実は deck.gl には、標高データを用いて地形を表現できる TerrainLayer なるものがあります。今回紹介したレポジトリでは、国土地理院の標高タイルと全国最新写真(シームレス)を用いて、地形の表現を行っています。なお、国土地理院の標高タイルのデコード方法には対応していないため、標高が無効又はマイナスである地点では表示がおかしくなります。
また、上記のラインデータ・ポリゴンデータの三次元表示と組み合わせて表示すると、埋まって見える等の齟齬があります。
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);
おわりに
「二次元の図形に座標値を付与して三次元らしく見せる」というコンセプトについて掘り起こしてみました。
なお、当時の実装そのままに紹介しておりますが、標高値の付与の方法だったり、データの形式だったり、もう少し色々と工夫ができそうな気がします。
当時からは状況が進んで、Mapbox GL JS でも v3 で三次元表示に関わるようなスタイル設定が増えていっているように見受けられます。MapLibre GL JS の方も開発は活発そうですし、MapLibre Tiles の動きも今後が楽しみです。
今後、Web 地図における三次元表示はますます簡便になりつつ、求められることもより高度になっていくかもしれません。今後の動きにも注目していきたいです。