🌎

Amplify UI Geoとreact-map-glの併用時、mapがないと言われた時にした小細工について書く

2024/10/17に公開

記事概要

  • 狭い用途の備忘録。
  • AWSもといAmplifyとAmplify UI GeoでWeb地図使って何か作りたい人向け。その中でもNext.js or Reactを利用したい人へ、事前にコレやっておくといいかもみたいな記事。
  • MapboxやGoogle Maps Platform使いたい人はスルーしていい。

登場するもの

  • AWS Amplify: AWSが提供するホスティングサービス。AWSの各種サービスを裏側で使ってVercelっぽいことができる。
    • ViteやNext.jsで作ったコードのリポジトリ連携させたら勝手にCI/CDする仕組みや、HTTPヘッダー書き換えるような仕組みをマネージドでやるなど、Webサイトやシステム建てる時に大体必要なことやってくれる。
  • Amplify UI Library: AWSが提供している、Amplify + 他のAWSサービスが連携しやすくなるようなUI Kit。ReactやVue、他にもFlutterなど向けのUIを提供している。
    • 過去のAmplify(v1)と2024年10月現在のAmplify(v2)はサービス提供している内容がかなり異なり、過去のバージョンのAmplifyでも利用できるようにするため制限が強め。ただし、AWSにがっつりロックインされるつもりがあるならそれなりに使いやすい。
      • たとえば、全部CSRコンポーネントなので、Next.jsの14以降で使うときはuse clientを宣言しないと使えない。
    • Amplify UI Geo: Amplify UI Libraryのコンポーネントの一つ。地図を提供するコンポーネントで、隠蔽されているが、バックエンドにはAmazon Location Serviceを利用している。Cognito(AWSのユーザーアクセスコントロールとかやってくれるやつ)、および、内部的にCognitoを利用しているAmplify Authと併用するときは、下記のMaplibreを直で利用するより楽なので利用してる。
  • Maplibre GL JS: 地図表示のためのライブラリ。Amplify UI Geoでは、コレにいくつかAWS Backendのサポートなどを入れているっぽい。
  • react-map-gl: ReactでMapLibre GL JSを触るときにの選択肢の一つ。地図を利用したビジュアライゼーションでは大手プロジェクトであるvis.gl(立ち上げはUber社)の名義で保守されており、他のReactラッパーはほぼ個人開発のような形になっているので、安定性をとるならこれ一択だと思う。

バージョン

  • @aws-amplify/ui-react: 6.5.x
  • @aws-amplify/ui-react-geo: 2.0.x
  • react: 18.2.x
  • react-map-gl: 7.1.x

併用した時の問題点

react-map-glのサンプルにあるコードを「中身だいたい同じなんだからコピペでも行けるっしょ」と思って使ったらエラーを吐くようになった。
具体的には地図を構成する要素を記載したSourceや、Sourceの属性等によって投影されるLayerを、Amplify UI Geoの地図を構成するコンポーネントであるMapViewのchildrenとしても、上手く動いてくれない。

以下は、react-map-glのサンプルコードを元に「こう書いたらいけるんじゃね?」で書いた動かないコード。


function App() {
  const geojson: FeatureCollection = {
    type: 'FeatureCollection',
    features: [
      {type: 'Feature', geometry: {type: 'Point', coordinates: [-122.4, 37.8]}}
    ]
  };

  const layerStyle: CircleLayer = {
    id: 'point',
    type: 'circle',
    paint: {
      'circle-radius': 10,
      'circle-color': '#007cbf'
    }
  };

  // サンプルコードのreact-map-gl/Mapをamplify-ui/geo/MapViewに差し替えただけ。
  // 動かない。mapが無いよと言われる。
  return <MapView
    initialViewState={{
      longitude: -122.4,
      latitude: 37.8,
      zoom: 14
    }}
  >
    <Source id="my-data" type="geojson" data={geojson}>
      <Layer {...layerStyle} />
    </Source>
  </MapView>;
}

原因

  • SourceLayerなど、地図の子コンポーネントとして使われることを意識しているreact-map-glの各種コンポーネントは、特定のReact Contextが存在することを期待している。
  • コンテキスト内の地図への参照は、デフォルトのMapRefで取得している。
  • MapLibre GL JSはMapbox GL JSのv1からフォークされたライブラリだが、Mapboxのv3,v4に伴うAPI変更で、Mapboxの形式に合わせ手作られたインターフェースと、MapLibreの実態のインターフェースがズレてしまっている。
    • MapRefも同様に、互換性を持たない形になってしまっている。そのため、デフォルトのMapRefを利用しているMapViewコンポーネントでは、うまくコンテキストが構成できていない。

というような現象が起きているっぽい。

パッと見MapViewのプロパティであるmapLibにmaplibre-glを指定すればイケそうだが、実際には無理だった。

対応

「インポート先を分岐させるとおかしいことになるなら、無理やり固定すればいいじゃない」

というわけで、MapViewにあるreact-map-glに関するインポートを、すべてmaplibre-gl専用のものに書き換えて利用する。
地図を使う時点でもう地理情報のプロバイダーや地図ライブラリは決め打ちになってしまっていると思うので、ヘタに分岐を書くよりはこちらの方が手っ取り早い。
もし分岐が必要なら、mapLibプロパティの値によって動的にインポートする先を切り替えるようにするのが良いと思うが試してない。

// MapViewのコードをほぼそのままコピペし、Context込みのMapViewコンポーネントにして利用する。
// https://github.com/aws-amplify/amplify-ui/blob/main/packages/react-geo/src/components/MapView/MapView.tsx

// ...
// react-map-glがMapboxにも対応しており、Mapbox側のAPIに変更があったため、明示的にMapLibreは専用のコンポーネントに分けているっぽい。
// 今回のケースはmaplibre決め打ちなので、react-map-glをimport先にしているものは明示的にmaplibreのコンポーネントを指定している。
import ReactMapGL from 'react-map-gl/maplibre';
import type { MapProps, MapRef, TransformRequestFunction } from 'react-map-gl/maplibre';
// ...

const MapViewWithMapLibre = forwardRef<MapRef, MapViewProps>(
  ({ mapLib, mapStyle, style, ...props }, ref) => {
    // ...

    return transformRequest ? (
        <ReactMapGL
          {...props}
          mapLib={mapLib ?? maplibregl}
          mapStyle={mapStyle ?? geoConfig?.maps?.default}
          ref={ref}
          style={styleProps}
          transformRequest={transformRequest}
          fog={props.fog}
          terrain={props.terrain}
        />
    ) : null;

  });

// ...

コンポーネント利用側でも、react-map-glのコンポーネントは明示的にmaplibreの方を利用する。


// ...
// maplibreを明示してインポート
import { Layer, Source, CircleLayer } from 'react-map-gl/maplibre';

// ...

function App() {
  const geojson: FeatureCollection = {
    type: 'FeatureCollection',
    features: [
      {type: 'Feature', geometry: {type: 'Point', coordinates: [-122.4, 37.8]}}
    ]
  };

  const layerStyle: CircleLayer = {
    id: 'point',
    type: 'circle',
    paint: {
      'circle-radius': 10,
      'circle-color': '#007cbf'
    }
  };

  // 上記コンポーネントに差し替え
  return <MapViewWithMapLibre
    initialViewState={{
      longitude: -122.4,
      latitude: 37.8,
      zoom: 14
    }}
  >
    <Source id="my-data" type="geojson" data={geojson}>
      <Layer {...layerStyle} />
    </Source>
  </MapViewWithMapLibre>;
}

まとめ

2024/10現在、MapboxやMapLibre、react-map-glのバージョンによるインターフェースの違いがかなりの混乱を呼んでいるように感じる。
この対処法自体はアップデートに伴い必要なくなったり、上記のようなMapLibreとMapboxを明確に分けたコンポーネントをreact-map-glが出してくるかもしれないが、それまでは対処療法としては役立つと思う。

Discussion