Amazon Location Serviceの地図上にdeck.glで3Dモデルを表示してみる

## はじめに

実は、以前にも試したことがあるのですが、
Amazon Location Serviceの地図上にdeck.glで3Dモデルを重畳表示させてみました。

https://github.com/smallriver0316/deckgl-on-amazon-location-service

使用している技術スタックは、主にAmplify+React+Typescript+deck.glになります。

- Amplify
- Amazon Location Service
- React
- Typescript
- Vite
- Maplibre
- react-map-gl(amplify/ui-react-geo)
- deck.gl

また、データはお馴染みのPateau 3D tilesを使っています。
久しぶりに試してみて、いろいろ変更点があったのでまとめてみたいと思います。

## Amazon Location Serviceとは

Amazon Location ServiceはAWSが提供するGIS APIサービスです。

https://aws.amazon.com/jp/location/

AWSのAPIを介してマップタイルやジオコーディング、ルート検索機能を利用できます。開発者視点ではこれらの機能を比較的低価格で利用できること、OSSのMaplibreを使って地図データのレンダリングできることが特徴です。

## deck.glとは

Uberが開発したマップビジュアライゼーションフレームワークです。WebGLを使って地図上にデータを重畳表示できます。

https://deck.gl/

deck.glの素晴らしいところは、Reactコンポーネントによって直感的に地図上に3Dモデルやポリゴンの表示を実装できるところです。

このとき地図タイルの表示はreact-map-glを使ってできます。

https://deck.gl/docs/get-started/using-with-react

Amazon Location Serviceにこだわらなければ、deck.glのTile3DLayerのサンプル通りに実装するだけで簡単に3Dモデルの表示ができます。

```.ts
// App.tsx
import { Map } from "react-map-gl/maplibre";
import { DeckGL, Tile3DLayer, MapViewState } from "deck.gl";
import { Tiles3DLoader } from "@loaders.gl/3d-tiles";
import type { Tile3D, Tileset3D } from "@loaders.gl/tiles";
import { Vector3 } from "math.gl";
import "maplibre-gl/dist/maplibre-gl.css";

const INITIAL_VIEW_STATE: MapViewState = {
  longitude: 139.7674681227469,
  latitude: 35.68111419325676,
  zoom: 11,
  bearing: 0,
  pitch: 0,
};

function App() {
  const layers = [
    new Tile3DLayer({
      id: "tile-3d-layer",
      pointSize: 1,
      data: "https://assets.cms.plateau.reearth.io/assets/97/0b3db1-d1d5-441a-8aa0-6d6d63361e20/13100_tokyo23-ku_2022_3dtiles_1_1_op_bldg_13101_chiyoda-ku_lod2/tileset.json",
      loader: Tiles3DLoader,
      onTilesetLoad: (tile: Tileset3D) => {
        const { cartographicCenter } = tile;
        if (cartographicCenter) {
          console.log(cartographicCenter);
        }
      },
      onTileLoad: (tileHeader: Tile3D) => {
        tileHeader.content.cartographicOrigin = new Vector3(
          tileHeader.content.cartographicOrigin.x,
          tileHeader.content.cartographicOrigin.y,
          tileHeader.content.cartographicOrigin.z - 40
        );
      },
    }),
  ];

  return (
    <DeckGL initialViewState={INITIAL_VIEW_STATE} controller layers={layers}>
      <Map mapStyle="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json" />
    </DeckGL>
  );
}

export default App;
```

以前は、react-map-glがmapboxにしか対応していなかったため、maplibreをmapboxであるように見せかける対応が必要でした。しかし現在はmaplibreにも対応しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173520/828463d9-26c9-430b-1326-6fe260c9f235.png)


## セットアップ

### Amplifyプロジェクトの立ち上げ

Amplifyプロジェクトを立ち上げます。立ち上げ手順は基本的には公式の説明通りです。

https://ui.docs.amplify.aws/react/getting-started/usage/vite

今回、reactフレームワークとしてViteを選択しました。以下のコマンドでTypescriptベースのプロジェクトのボイラープレートを作ります。

```bash
npm create vite@latest amplify-deckgl-sample -- --template react-ts
```

次にパッケージをインストールします。

```bash
cd amplify-deckgl-sample
npm install @aws-amplify/ui-react aws-amplify
```

そして、`amplify init`を実行して、Amplify環境を作ります。この辺のコマンドはAmplifyのクイックスタートと同じなので、詳しい説明は割愛します。
ここで一つ注意点があり、作成されるaws-exports.jsはそのままだとTypescriptにインポートできません。aws-exports.tsにリネームする必要があります。

```ts
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import { Amplify } from 'aws-amplify';
import awsconfig from './aws-exports.ts';

Amplify.configure(awsconfig);

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```

さらにもう一つ注意することがあり、AmplifyでViteプロジェクトを立ち上げる際、vite.config.tsに以下の追記をする必要があります。これをしておかないと、`global is not defined`というエラーが出ます。

```ts
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  define: {
    global: {},
  },
  resolve: {
    alias: {
      './runtimeConfig': './runtimeConfig.browser',
    },
  },
});
```

一度、`npm run dev`を実行して、webページが立ち上がることを確認しておくといいです。

### Amazon Location Serviceの導入

Amazon Location Serviceはamplifyコマンドで導入します。

```bash
$ amplify add auth
##　ここでdefaultのCognitoを選択
Using service: Cognito, provided by: awscloudformation

 The current configured provider is Amazon Cognito.

 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections.
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.

$ amplify add geo
? Select which capability you want to add: Map (visualize the geospatial data)
✔ Provide a name for the Map: · xxxxx
✔ Who can access this Map? · Authorized and Guest users ## Guest userのアクセスも許可
Available advanced settings:
- Map style & Map data provider (default: Streets provided by Esri)

✔ Do you want to configure advanced settings? (y/N) · yes ## Map styleはESRIのNavigationを選択
✔ Specify the map style. Refer https://docs.aws.amazon.com/location-maps/latest/APIReference/API_MapConfiguration.html · Navigation (data provided by ESRI)

⚠️ Auth configuration is required to allow unauthenticated users, but it is not configured properly.

## 設定をクラウドに反映
$ amplify push
```

アクセスの際の認証ではゲストユーザーのアクセスを許可することで、IAM RoleのUnauthenticated Roleが作成されます。地図を表示するだけなら、これで認証なしで表示が可能になります。

Map styleでESRIを選択したのは、Map styleによってPlateauの3Dモデルを表示できない場合があるからです。これについては後述します。

次に地図の表示に必要なパッケージをインストールします。

```bash
npm install maplibre-gl-js-amplify @aws-amplify/ui-react-geo
```

Amazon Location Serviceの地図表示はreact-map-glなしでも可能です。詳しい説明は[AWSの公式ドキュメント](https://aws.amazon.com/jp/builders-flash/202111/amplify-geo-map/)に記載があるので、そちらを参照してください。
既に書いた通りdeck.glと共にレンダリングするためにreact-map-glを導入するところなのですが、AWSがカスタマイズした@aws-amplify/ui-react-geoを使う方が簡単です。

```ts
// App.tsx
import { MapView } from '@aws-amplify/ui-react-geo';
import 'maplibre-gl/dist/maplibre-gl.css';

const INITIAL_VIEW_STATE: MapViewState = {
  longitude: 139.7674681227469,
  latitude: 35.68111419325676,
  zoom: 11,
  bearing: 0,
  pitch: 0,
};

function App() {
  return <MapView initialViewState={INITIAL_VIEW_STATE} />;
}

export default App;
```

`npm run dev`を実行し、localhost:5173で地図を表示できれば成功です。

### deck.glの導入

いよいよdeck.glを使って3Dモデルをレンダリングします。
まずは必要なパッケージをインストールします。

```bash
npm install deck.gl@8.9.35 loader.gl math.gl
```

ここでdeck.glのバージョンを指定しているのには訳があります。
以下にあるように、現在、v9系のdeck.glではPlateau 3D tilesを表示することができません。以下のdiscussionにあるように、FMEを使っているCityGMLは最新のdeck.glで表示するとエラーになるようです。Plateau 3D tilesを表示する場合は、v8.9.35を指定しましょう。

https://github.com/visgl/deck.gl/discussions/8815

Plateau 3D tilesの表示にはもう一つ難があり、特定の地図ではエラーになることがあります。Map styleでHEREの地図を選択すると、以下のように3Dモデルは表示されませんでした。(これ分かる人がいたらコメントいただけると嬉しいです。)

![スクリーンショット 2024-05-29 061611.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173520/b9147092-328f-b4bd-d061-49f62ccff3af.jpeg)

@amplify/ui-react-geoを使うと便利なのですが、deck.glのレイヤーを表示する場合は以下のようにスタイルを調整する必要があります。このpositionとzIndexはui-react-geoのソースコードで明示的に変更されていて、そのままではdeck.glのレイヤーより上にレンダリングされてしまいます。これをreact-map-glのdefaultの設定に合わせるようにstyleを指定します。

```tsx
      <MapView
        initialViewState={INITIAL_VIEW_STATE}
        style={{
          position: 'absolute',
          zIndex: -1,
        }}
      />
```

また、このままだとdeck.glのレイヤーと背景地図を一致させたまま動かせないので、deck.glとui-react-geoが同じviewstateを参照するように、以下のように修正します。

```tsx
// App.tsx
  const [viewport, setViewport] = useState<MapViewState>(INITIAL_VIEW_STATE);

  return (
    <DeckGL
      initialViewState={INITIAL_VIEW_STATE}
      controller
      layers={layers}
      onViewStateChange={({ viewState }: { viewState: MapViewState }) =>
        setViewport(viewState)
      }
    >
      <MapView
        {...viewport}
        initialViewState={INITIAL_VIEW_STATE}
        style={{
          position: 'absolute',
          zIndex: -1,
        }}
      />
    </DeckGL>
  );
}
```

ビルドする際に、deck.glのインポート箇所で`Could not find a declaration file for module 'deck.gl'`のエラーが出るので、tsconfig.jsonに`noImplicityAny: false`を追記します。

```json
// tsconfig.json
{
  "compilerOptions": {
    ...
    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": false // <--- 追加
  },
  ...
}
```

これで完成です。`npm run dev`を実行して、3Dモデルが表示されることを確認できます。`amplify publish`を実行して簡単にプロビジョニングすることもできます。

![スクリーンショット 2024-05-31 062818.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173520/e181d82c-4d1f-38dd-98bc-97f347900831.jpeg)

## おわりに

完全に自己満足ですが、最新のamplify UIでもdeck.glを使った表示ができて嬉しいです。

実際には、deck.glを使う場合、表示される3Dモデルやポリゴンが目立つようにすると思うので、Amazon Location Serviceの地図を使うことはほとんど無いと思います。冒頭に書いたように、CARTOのbasemapを使った方がはるかに簡単に実現できます。

とはいえ、Amazon Location Serviceにも、OpenDataのVisualization Dark, ESRIのDarkgray Campusなど、deck.glのビジュアライゼーションとマッチしそうな地図タイルもあるので、いろいろ試していきたいです。

## 参考文献

https://qiita.com/Kanahiro/items/4ae702c575176501a157

https://qiita.com/shi-works/items/cd99cabae941facab6e4

https://aws.amazon.com/jp/builders-flash/202301/use-map-library-location-service/