はじめに
Reactで作ったWebアプリに描画されたmapboxの地図上でビジュアライゼーションの表示を行います。
ビジュアライゼーションの描画にはdeck.glを利用します。
同時に3Dモデルも地図上に表示します。
deck.glについて
mapboxでのdeck.glの使い方は何パターンかあるのですが、今回はmapbox-glをベースにdeck.glのレイヤーを乗せて表示する形を試します。
このパターンだと@deck.gl/mapbox
というサブモジュールを使うパターンになるのですが、ドキュメントには
Mapbox 2.0's terrain feature is currently not supported.
と注意書きがあり、3D地形表示には対応していないようです。
ですが今回は3D地形表示状態でもdeck.glでのレイヤー表示ができるかも含めて試しました。
本来の3D地形表示をサポートするdeck.glの利用パターンは、deck.glをベースに表示対象のマップを mapbox にするという使い方になります。
(つまりmapbox-glベースではなく、deck.glベースで実装する)
TerrainLayer
このパターンだとmapbox-glで3Dモデルの表示に使っているThree.js
は、使うことができなくなります。
その場合は代わりにluma.gl
を使うようです。
最終的な完成形
環境構築
こちらの記事の初期セットアップをベースにします
【React/mapboxの地図上で3D モデルを動かす】(2) mapboxの地図上に3Dモデルを表示する
package install
yarn add three @mapbox/mapbox-gl-language mapbox-gl @deck.gl/mapbox deck.gl
yarn add -D @types/three @types/mapbox-gl
実装
- deck.glのライブラリのMapboxLayerを使ってmapbox-glにレイヤーとしてビジュアライゼーションを追加しています。
- typescriptの場合は
typed
ディレクトリの中をimportします。(Using deck.gl with TypeScript) - 今回使用したビジュアライゼーションはArcLayerとIconLayerです。
import { useEffect, useRef, useState } from "react";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";
import "./App.css";
import { droneLayer } from "./droneLayer";
import { MapboxLayer } from "@deck.gl/mapbox/typed";
import { ArcLayer, IconLayer } from "@deck.gl/layers/typed";
mapboxgl.accessToken = "YOUR MAPBOX TOKEN";
const App = () => {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<mapboxgl.Map | null>(null);
const [lng, setLng] = useState(6.5873);
const [lat, setLat] = useState(45.3967);
const [zoom, setZoom] = useState(13.85);
const exaggeration = 1.0;
let dronePosition: mapboxgl.LngLatLike = [6.56674, 45.39881];
let droneHeight = 100;
// この中でアニメーションを描画していく
const animation = (frame: number) => {
if (!map.current) return;
// ドローンの現在位置の標高を取得
const elevation =
Math.floor(
map.current.queryTerrainElevation(dronePosition, {
exaggerated: false,
}) || 0
) * exaggeration;
if (map.current.getLayer("drone-model")) {
let height =
elevation + droneHeight + Math.sin(droneHeight + frame * 0.01) * 0.5;
// 上下の揺れだけ表現
droneLayer.updateLngLat({ altitude: height });
}
requestAnimationFrame(animation);
};
useEffect(() => {
// マップの初期セットアップ
if (map.current) return;
map.current = new mapboxgl.Map({
container: mapContainer.current as HTMLElement,
style: "mapbox://styles/mapbox/streets-v11",
center: [lng, lat],
zoom: zoom,
pitch: 76,
bearing: 150,
antialias: true,
});
animation(0);
}, []);
useEffect(() => {
// マップ読み込み後
if (!map.current) return;
map.current.on("move", () => {
if (map.current) {
setLng(Number(map.current.getCenter().lng.toFixed(4)));
setLat(Number(map.current.getCenter().lat.toFixed(4)));
setZoom(Number(map.current.getZoom().toFixed(2)));
}
});
map.current.on("load", () => {
// モデルを描画する
if (map.current) {
if (!map.current.getLayer("drone-model")) {
map.current.addLayer(droneLayer);
}
if (!map.current.getSource("mapbox-dem")) {
// 標高データの追加
map.current.addSource("mapbox-dem", {
type: "raster-dem",
url: "mapbox://mapbox.terrain-rgb",
tileSize: 512,
maxzoom: 14,
});
map.current.setTerrain({ source: "mapbox-dem", exaggeration });
}
if (!map.current.getLayer("my-archlayer")) {
const myArcLayer = new MapboxLayer<ArcLayer>({
id: "my-archlayer",
// @ts-ignore
type: ArcLayer,
data: [
{
position: [6.56674, 45.39881],
size: 5,
},
{
position: [6.6025, 45.40753],
size: 5,
},
],
getSourcePosition: (d) => [6.56674, 45.39881],
getTargetPosition: (d) => [6.6025, 45.40753],
getSourceColor: [0, 128, 200],
getTargetColor: [200, 0, 80],
getWidth: 1,
});
map.current.addLayer(myArcLayer);
}
if (!map.current.getLayer("my-iconlayer")) {
const ICON_MAPPING = {
marker: { x: 0, y: 0, width: 128, height: 128, mask: true },
};
const myIconLayer = new MapboxLayer<IconLayer>({
id: "my-iconlayer",
// @ts-ignore
type: IconLayer,
data: [
{
name: "ふもと",
exits: 4214,
coordinates: [6.56674, 45.39881],
},
{
name: "頂上",
exits: 3002,
coordinates: [6.6025, 45.40753],
},
],
pickable: false,
iconAtlas:
"https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/icon-atlas.png",
iconMapping: ICON_MAPPING,
getIcon: (d) => "marker",
sizeScale: 10,
getPosition: (d) => d.coordinates,
getSize: (d) => 5,
getColor: (d) => [Math.sqrt(d.exits), 140, 0],
});
map.current.addLayer(myIconLayer);
}
}
});
});
return (
<div>
<div style={{ position: "relative" }}>
<div className="sidebar">
Longitude: {lng} | Latitude: {lat} | Zoom: {zoom}
</div>
<div ref={mapContainer} className="map-container" />
</div>
</div>
);
};
export default App;
まとめ
mapboxの3D地図上でビジュアライゼーションを表示することができました。
参考
deck.gl
【React/mapboxの地図上で3D モデルを動かす】(2) mapboxの地図上に3Dモデルを表示する