mapboxで描画した3Dマップ上にビジュアライゼーションを投影する

# はじめに

Reactで作ったWebアプリに描画されたmapboxの地図上でビジュアライゼーションの表示を行います。
ビジュアライゼーションの描画にはdeck.glを利用します。
同時に3Dモデルも地図上に表示します。

# deck.glについて

mapboxでのdeck.glの使い方は何パターンかあるのですが、今回はmapbox-glをベースにdeck.glのレイヤーを乗せて表示する形を試します。
このパターンだと`@deck.gl/mapbox`というサブモジュールを使うパターンになるのですが、[ドキュメント](https://deck.gl/docs/api-reference/mapbox/overview)には

```
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](https://deck.gl/docs/api-reference/geo-layers/terrain-layer)
このパターンだとmapbox-glで3Dモデルの表示に使っている`Three.js`は、使うことができなくなります。
その場合は代わりに`luma.gl`を使うようです。

## 最終的な完成形

- 地形を有効にした状態
  ![完成形](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/ed20b909-5c32-e46f-efcc-b57b07e74ea3.png)

  3D 地形表示を有効にした状態でもビジュアライゼーションは表示できましたが、高さ情報を意識できていないように見えました。

- 平面の状態
  ![完成形2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/3a6c151c-e4cd-6e78-3591-ef5201ff5724.png)


# 環境構築

こちらの記事の初期セットアップをベースにします
[【React/mapboxの地図上で3D モデルを動かす】(2) mapboxの地図上に3Dモデルを表示する](https://qiita.com/itouoti/items/90550c8d529d63596d96)


## package install

```bash
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](https://deck.gl/docs/api-reference/mapbox/mapbox-layer)を使ってmapbox-glにレイヤーとしてビジュアライゼーションを追加しています。
- typescriptの場合は`typed`ディレクトリの中をimportします。([Using deck.gl with TypeScript](https://deck.gl/docs/get-started/using-with-typescript))
- 今回使用したビジュアライゼーションは[ArcLayer](https://deck.gl/docs/api-reference/layers/arc-layer)と[IconLayer](https://deck.gl/docs/api-reference/layers/icon-layer)です。

```App.tsx
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](https://deck.gl/)
[【React/mapboxの地図上で3D モデルを動かす】(2) mapboxの地図上に3Dモデルを表示する](https://qiita.com/itouoti/items/90550c8d529d63596d96)