「Software Design 2024年02月号 位置情報エンジニアリングのすすめ 第7回」をMapbox GL JSで実装してみる
はじめに
現在、Software Designで連載中の「位置情報エンジニアリングのすすめ」を毎月楽しく拝読しております。第7回「防災マップの作成② 地図の3D表現と土砂災害警戒区域の可視化」ではMapLibre GL JSを用いて地理情報を表示していました。今回の紙面の内容もMapbox GL JSで実装可能なので、試してみようと思います。
建物ポリゴンの3D表示
紙面ではソースとして国土地理院Vector、レイヤーとしてfill-extrusion
を用いて建物を表示していました。Mapbox GL JSでも同じ手法で表示が可能です。ただし、Mapboxの場合はMapbox Streets v8タイルセットに建物のポリゴンおよびfill-extrusion
レイヤーで使用する高さ情報が含まれているのでこちらを利用するのが便利です。
また紙面のようにコード上で実現してもよいですが、Mapbox Studioを使用すればボタン一つで表示可能です。
Mapbox Studio上での操作
- https://studio.mapbox.com/ にアクセス
- 「New style」をクリック
- 「Classic template」をクリック
- 「Streets」を選択し、「Customize Streets」をクリック
- 編集画面が表示されたら「Layers」をクリック
- Streetsスタイルのレイヤー一覧が表示されるので、「Buildings」をクリック
- 「Building style」で「3D」を選択
これで都市部をズームすると以下のように建物が3D表示されているのがわかります。
レイヤー一覧を再度確認すると、Buildingsに少し変化があり、buliding-extrusionというfill-extrusion
レイヤーが作成されているのがわかります。つまり、紙面でコード上で作成していたのと同じことが実現できているということになります。
地形の3D表示
Mapbox GL JSではMap#setTerrain
というメソッドを使って地形の3D表示を行います。また、MapLibre GL JSでもMap#setTerrain
で設定するようです。ただし、Terrain(地形)の機能はMapbox GL JS v1.13.0にはなかった機能なので、それぞれが個別に実装した別機能と考えるのが良いかと思います(MapLibre GL JSはMapbox GL JS v1.13.0からフォークしたのでした)。
Mapbox GL JSのMap#setTerrain
の使い方は以下のサンプルをご参照ください。
また紙面で紹介されているようにMapLibre GL JSにはTerrainControl
というMap#setTerrain
を呼んでTerrainをオン・オフするコントロールがあります。しかし、残念ながらMapbox GL JSには同様のコントロールはデフォルトでは存在しません。
そこで、ここではTerrainを常に表示した状態にします。そして、この機能もMapbox Studioを使用すればボタン一つで表示可能です。
Mapbox Studio上での操作
- 「Global」をクリックし、「3D Terrain」をクリック
- 「Enable terrain」を「on」にする
これで山間部を表示すると、地形が3D表示されているのがわかります。Exaggerationの値を変更すると、高さを強調できます。
また、紙面ではhillshadeレイヤーを作成していました。Streetsスタイルでは以下のように最初からhillshadeレイヤーがあるので、改めて作成する必要はないです。
土砂災害区域の可視化
ここからは紙面と同じ様にレイヤーを作成していきます。紙面ではスタイルの中で定義していますが、ここではコード上でaddSource
・addLayer
を使用します。Studio上でレイヤーを作成できると簡単で良いのですが、StudioはMapbox以外のサーバにあるソースを利用できないため、この方法を使用します。
Mapbox Studioで作成したスタイルをPublish
変更を加えたスタイルを使用するためにはPublishする必要があります。
- 画面右上の「Publish...」ボタンをクリック
- ダイアログに変更前・後の地図が表示されているので、問題なければ「Publish」ボタンをクリック
- 画面右上の「Share...」ボタンをクリック
- ダイアログ上の「Style URL」をコピーしておく
サンプル全体
まず今回作成したサンプルです。
import { Map, NavigationControl } from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
// @ts-ignore
import OpacityControl from 'maplibre-gl-opacity';
import 'maplibre-gl-opacity/dist/maplibre-gl-opacity.css'
const map = new Map({
accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
container: 'map',
style: 'mapbox://styles/yochi/clrn88308001z01pud4930a9q',
center: [139.768435, 35.681054],
zoom: 15,
});
map.on('style.load', () => {
map.addSource('doseki', {
type: 'raster',
tiles: ['https://disaportaldata.gsi.go.jp/raster/05_dosekiryukeikaikuiki/{z}/{x}/{y}.png'],
minzoom: 2,
maxzoom: 17,
tileSize: 256,
attribution: '<a href="https://disaportaldata.gsi.go.jp/hazardmap/copyright/opendata.html">ハザードマップポータルサイト</a>',
});
map.addSource('kyukeisha', {
type: 'raster',
tiles: ['https://disaportaldata.gsi.go.jp/raster/05_kyukeishakeikaikuiki/{z}/{x}/{y}.png'],
minzoom: 2,
maxzoom: 17,
tileSize: 256,
attribution: '<a href="https://disaportaldata.gsi.go.jp/hazardmap/copyright/opendata.html">ハザードマップポータルサイト</a>',
});
map.addSource('jisuberi', {
type: 'raster',
tiles: ['https://disaportaldata.gsi.go.jp/raster/05_jisuberikeikaikuiki/{z}/{x}/{y}.png'],
minzoom: 2,
maxzoom: 17,
tileSize: 256,
attribution: '<a href="https://disaportaldata.gsi.go.jp/hazardmap/copyright/opendata.html">ハザードマップポータルサイト</a>',
});
map.addLayer({
id: 'doseki_layer',
source: 'doseki',
type: 'raster',
paint: { 'raster-opacity': 0.8 },
});
map.addLayer({
id: 'kyukeisha_layer',
source: 'kyukeisha',
type: 'raster',
paint: { 'raster-opacity': 0.8 },
});
map.addLayer({
id: 'jisuberi_layer',
source: 'jisuberi',
type: 'raster',
paint: { 'raster-opacity': 0.8 },
});
const hazardLayers = new OpacityControl({
baseLayers: {
doseki_layer: '土石流',
kyukeisha_layer: '急傾斜',
jisuberi_layer: '地滑り',
},
});
map.addControl(hazardLayers, 'top-left');
hazardLayers._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';
map.addControl(new NavigationControl());
});
Mapオブジェクトの作成
基本的にいつも通りですが、style
だけ異なります。自分で編集したスタイルを使用するので、先程コピーしたものを指定してください。
const map = new Map({
accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
container: 'map',
style: 'mapbox://styles/yochi/clrn88308001z01pud4930a9q',
center: [139.768435, 35.681054],
zoom: 15,
});
ソース・レイヤーの作成
Map#addSource
Map#addLayer
は地図の読み込み後に実行する必要があります。そこで、map.on('style.load', () => {/* ここ */}
の「ここ」の部分に以下のように記述します。
map.addSource('doseki', {
type: 'raster',
tiles: ['https://disaportaldata.gsi.go.jp/raster/05_dosekiryukeikaikuiki/{z}/{x}/{y}.png'],
minzoom: 2,
maxzoom: 17,
tileSize: 256,
attribution: '<a href="https://disaportaldata.gsi.go.jp/hazardmap/copyright/opendata.html">ハザードマップポータルサイト</a>',
});
中略
map.addLayer({
id: 'doseki_layer',
source: 'doseki',
type: 'raster',
paint: { 'raster-opacity': 0.8 },
});
以下略
maplibre-gl-opacityの作成
前回確認した通り、ちょっとしたハックでmaplibre-gl-opacityが使えます。作成したレイヤーをbaseLayers
に指定します。
const hazardLayers = new OpacityControl({
baseLayers: {
doseki_layer: '土石流',
kyukeisha_layer: '急傾斜',
jisuberi_layer: '地滑り',
},
});
map.addControl(hazardLayers, 'top-left');
hazardLayers._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';
紙面ではレイヤーの初期状態を非表示にするコード(リスト9)がありますが、load.style
のタイミングでOpacityControl
を作成すればbaseLayers
のレイヤーが一瞬描画されることはありません。
カメラコントロール機能の追加
MapLibre GL JSと同じ感じですね。
map.addControl(new NavigationControl());
結果
以下のように表示されます。
デモは以下のサイトで試せます。
おまけ
スタイル作成時に「Start with Standard」をクリックすると、最新のStandardスタイルが使用されます。
Standardスタイルはデフォルトで建物が3D表示されています。地形の3D表示はStreetsのときと同じ様に「Global」「3D Terrain」「Enable terrain」で設定します。
ExaggerationがStreets v12とは異なる設定なっています。ここでは同じ設定にします。
このスタイルを用いた場合のデモは以下のサイトで試せます。
おまけ2
ぜひ過去の記事もご参照ください。
Discussion