🗻

React MapLibre で AWS の地形データを表示する!

2024/12/28に公開

⛳ Goal

Next.js で作ったアプリ上に react-map-gl を使い MapLibre で地形データを3Dで表示させたい

環境

  • Next.js: 15.1.3
  • React: 19.0.0
  • TypeScrip: 5.7.2
  • maplibre-gl: 4.7.1
  • react-map-gl: 7.1.8

🐕 Step

  1. react-map-gl を使って MapLibre の地図を表示させる
  2. AWS が公開している 地形データ (Terrain タイル) を表示させる
  3. 地形タイル (Terrain Layer) を 3D 表示にする

1. 🗺️ react-map-gl を使って MapLibre の地図を表示させる

TerrainMap.tsx
"use client";
import { FC } from "react";
import * as maplibregl from "maplibre-gl";
import Map, { ViewState } from "react-map-gl/maplibre";
import "maplibre-gl/dist/maplibre-gl.css";

const InitialViewState: Partial<ViewState> = {
  longitude: 135.8,
  latitude: 37.5,
  zoom: 5,
  pitch: 45, // マップの初期ピッチ (傾き)
  bearing: 0, // マップの初期ベアリング (回転)
};

const MAX_PITCH = 85 as const; // マップの最大ピッチ角度
const MAX_ZOOM = 15 as const;
const MIN_ZOOM = 1 as const;

export const TerrainMap: FC = () => {
  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        mapLib={maplibregl}
        mapStyle="https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json"
        initialViewState={InitialViewState}
        maxPitch={MAX_PITCH}
        maxZoom={MAX_ZOOM}
        minZoom={MIN_ZOOM}
        attributionControl={true}
      />
    </div>
  );
};

地形データを表示させるので pitch (傾き) を付けて地図を表示させた
今回は Next.js で作成しており地図タイルの取得があるのでクライアントコンポーネントで作成した

📝 Next.js + MapLibre クライアントコンポーネントでないとエラーになる

MapLibre を扱っているコンポーネントに "use client" が無いと下記のようなエラーになる

  2 |
  3 | import * as maplibregl from "maplibre-gl";
> 4 | import Map, { ViewState } from "react-map-gl/maplibre";
    | ^
  5 |
  6 | import "maplibre-gl/dist/maplibre-gl.css";
  7 |

TypeError: createContext only works in Client Components. Add the "use client" directive at the top of the file to use it.

2. 🗾 AWS が公開している 地形データ (Terrain タイル) を表示させる

MapTerrainLayer コンポーネントとして作成する

TerrainMap.tsx
+ import { MapTerrainLayer } from "./MapTerrainLayer";

export const TerrainMap: FC = () => {
  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        mapLib={maplibregl}
        mapStyle="https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json"
        initialViewState={InitialViewState}
        maxPitch={MAX_PITCH}
        maxZoom={MAX_ZOOM}
        minZoom={MIN_ZOOM}
        attributionControl={true}
      >
+       <MapTerrainLayer />
      </Map>
    </div>
  );
};
**./attribution.ts**
attribution.ts
export const AWS_TERRAIN_TILES_ATTRIBUTION = `* ArcticDEM terrain data DEM(s) were created from DigitalGlobe, Inc., imagery and
  funded under National Science Foundation awards 1043681, 1559691, and 1542736;
* Australia terrain data © Commonwealth of Australia (Geoscience Australia) 2017;
* Austria terrain data © offene Daten Österreichs – Digitales Geländemodell (DGM)
  Österreich;
* Canada terrain data contains information licensed under the Open Government
  Licence – Canada;
* Europe terrain data produced using Copernicus data and information funded by the
  European Union - EU-DEM layers;
* Global ETOPO1 terrain data U.S. National Oceanic and Atmospheric Administration
* Mexico terrain data source: INEGI, Continental relief, 2016;
* New Zealand terrain data Copyright 2011 Crown copyright (c) Land Information New
  Zealand and the New Zealand Government (All rights reserved);
* Norway terrain data © Kartverket;
* United Kingdom terrain data © Environment Agency copyright and/or database right
  2015. All rights reserved;
* United States 3DEP (formerly NED) and global GMTED2010 and SRTM terrain data
  courtesy of the U.S. Geological Survey.` as const;
MapTerrainLayer.tsx
import { FC } from "react";
import { Layer, Source } from "react-map-gl/maplibre";
import { AWS_TERRAIN_TILES_ATTRIBUTION } from "./attribution";

const AWS_TERRAIN_TILES =
  "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png" as const;

export const MapTerrainLayer: FC = () => {
  const sourceID = "awsTerrainTile";
  return (
    <Source
      id={sourceID}
      type="raster-dem"
      tiles={[AWS_TERRAIN_TILES]}
      encoding="terrarium"
      minzoom={0} 
      maxzoom={15}
      // ⚠️ Attribution をちゃんと入れること
      attribution={AWS_TERRAIN_TILES_ATTRIBUTION}
    >
      <Layer
        id="mapTerrainLayer"
        type="hillshade"
        source={sourceID}
        paint={{
          "hillshade-illumination-anchor": "map", // 陰影の光源は地図の北を基準にする
          // "hillshade-illumination-direction": 315, // 高原を数値で指定する場合
          "hillshade-exaggeration": 0.5, // 陰影の強さ default 0.5 1〜0
        }}
      />
    </Source>
  );
};
  • Source コンポーネントの tiles に配信されているタイルの URL を指定
  • Sourcetype="raster-dem", encoding="terrarium" とする
  • タイルは 256x256, 260x260, 512x512, 516x516 が配信されている
  • zoom level は 0 〜 15 のデータが配信されているので minzoom={0}, maxzoom={15} を指定

⚠️ Next Action react-map-gl の Layer だけでは 3D になってない

地形レイヤー (Terrain Layer) が表示でき一見すると、地形が表示出ているようにみえるが、地図の傾きをキツくすると陰影のある平面のタイルが張り付いているだけの状態になっている


平面の地形タイルが張り付いている状態

地図を傾けて使用しないアプリなら、このままも陰影があるので十分地形が分かるが折角なので3D 表示させてみることにした

3. 🗻 地形タイル (Terrain Layer) を 3D 表示にする

react-map-gl/maplibre<Map> コンポーネントに terrain プロパティを指定すると 3D 表示にすることができた

TerrainMap.tsx
export const TerrainMap: FC = () => {
+ const terrainSourceID = "awsTerrainTile";

  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Map
        mapLib={maplibregl}
        mapStyle="https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json"
        initialViewState={InitialViewState}
        maxPitch={MAX_PITCH}
        maxZoom={MAX_ZOOM}
        minZoom={MIN_ZOOM}
        attributionControl={true}
+       terrain={{
+         source: terrainSourceID,
+         exaggeration: 1, // 立体の強調度合い
+       }}
      >
-       <MapTerrainLayer />
+       <MapTerrainLayer sourceID={terrainSourceID} />
      </Map>
    </div>
  );
};
MapTerrainLayer.tsx
+ MapTerrainLayerProps = {
+  sourceID?: string;
+ };

- export const MapTerrainLayer: FC = () => {
+ export const MapTerrainLayer: FC<MapTerrainLayerProps> = ({
+  sourceID = "awsTerrainTile",
+ }) => {
- const sourceID = "awsTerrainTile";
  return (
    <Source
      id={sourceID}
      type="raster-dem"
      tiles={[AWS_TERRAIN_TILES]}
      encoding="terrarium"
      minzoom={0} 
      maxzoom={15}
      attribution={AWS_TERRAIN_TILES_ATTRIBUTION}
    >
      <Layer
        id="mapTerrainLayer"
        type="hillshade"
        source={sourceID}
        paint={{
          "hillshade-illumination-anchor": "map",
          "hillshade-exaggeration": 0.5,
        }}
      />
    </Source>
  );
};

💡 Terrain Source の id を同じにすること

<Map> コンポーネントの terrain プロパティの source には Terrain Source の id を指定すること
変数や props にしておけば間違いが防げる

まとめ

これで react-map-gl を使って MapLibre 上に地形データ (Terrain Tiles) を 3D で表示することができました!

zoom level が低い時に 3D 表示にしているとぬるっとしてて見た目があまり良くないと感じたので、minzoom を調整して不要なときには terrain layer を表示しないようにしても良い気がする。

⚠️ zoom level に応じて 3D 表示を切り替えたかったはうまくいかなかった ⚠️
  • zoom level に応じて <Map>terrain 属性 を on/off しようとしたが、切り替わったところで 地図がフリーズしてしまった
  • zoom level に応じて source id を変更すればフリーズしないが、id が切り替わった直後に Error: source id changed というエラーでアプリがクラッシュしてしまった
Error: source id changed
The above error occurred in the <Source> component. It was handled by the <ReactDevOverlay> error boundary. Error Component Stack
    at Source (source.ts:90:25)
    at MapTerrainLayer (MapTerrainLayer.tsx)

おわり


参考

MapLibre + react-map-gl

https://maplibre.org/
https://visgl.github.io/react-map-gl/docs/api-reference/map

地形データの表示

https://zenn.dev/asahina820/books/071ba23476fdc4/viewer/e3d359
https://qiita.com/T-ubu/items/c35023e1df2362bd8e7f
https://www.docswell.com/s/smellman/5YWQR3-maplibre-meetup-japan-2023-11-09
https://qiita.com/Kanahiro/items/1e9c1a4ad6be76b27f0f
https://zenn.dev/mierune_inc/books/location-engineering/viewer/part7

AWS Terrain Tiles

https://aws.amazon.com/marketplace/pp/prodview-x7vtai3hasf26
https://registry.opendata.aws/terrain-tiles/
License
https://github.com/tilezen/joerd/blob/master/docs/attribution.md

Discussion