React MapLibre で AWS の地形データを表示する!
⛳ 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
- react-map-gl を使って MapLibre の地図を表示させる
- AWS が公開している 地形データ (Terrain タイル) を表示させる
- 地形タイル (Terrain Layer) を 3D 表示にする
1. 🗺️ react-map-gl を使って MapLibre の地図を表示させる
"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
コンポーネントとして作成する
+ 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**
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;
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 を指定 -
Source
のtype="raster-dem"
,encoding="terrarium"
とする - タイルは
256x256
,260x260
,512x512
,516x516
が配信されている - zoom level は 0 〜 15 のデータが配信されているので
minzoom={0}
,maxzoom={15}
を指定- 地形用事する必要がないズームレベルはカットしてしまっても良いと思う
-
Tiles are available for zooms 0 through 15 and are available
cf. https://github.com/tilezen/joerd/blob/master/docs/index.md
react-map-gl
の Layer だけでは 3D になってない
⚠️ Next Action 地形レイヤー (Terrain Layer) が表示でき一見すると、地形が表示出ているようにみえるが、地図の傾きをキツくすると陰影のある平面のタイルが張り付いているだけの状態になっている
平面の地形タイルが張り付いている状態
地図を傾けて使用しないアプリなら、このままも陰影があるので十分地形が分かるが折角なので3D 表示させてみることにした
3. 🗻 地形タイル (Terrain Layer) を 3D 表示にする
react-map-gl/maplibre
の <Map>
コンポーネントに terrain
プロパティを指定すると 3D 表示にすることができた
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>
);
};
+ 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/maplibre-native/ios/latest/documentation/maplibre/mlnhillshadestylelayer/hillshadeilluminationanchor/
- https://docs.mapbox.com/ios/maps/api/11.6.0-beta.1/documentation/mapboxmaps/hillshadelayer/hillshadeilluminationdirection/
-
https://maplibre.org/maplibre-native/ios/latest/documentation/maplibre/mlnhillshadestylelayer/hillshadeexaggeration/
https://memomemokun.hateblo.jp/entry/2019/03/16/152958
https://maps.gsi.go.jp/development/ichiran.html
AWS Terrain Tiles
https://github.com/tilezen/joerd/blob/master/docs/attribution.md
Discussion