先日は React で OpenStreetMap を表示することが出来る Leaflet をご紹介しました。外部データを地図上に表示したいというユースケースがあると思います。React の状態管理で広く使われている Redux と Leaflet を連携させる方法を解説します。
Leaflet から Redux へデータを渡す
Leaflet
MapContainer はマップを表示・操作するコンポーネントです。このコンポーネント配下では Leaflet の地図を操作することが出来ます。
MapController は地図操作処理を記述したコンポーネントです。
TileLayer は表示する地図を定義しています。
LocationMarker は後述します。
import { MapContainer, MapContainerProps, TileLayer } from 'react-leaflet';
import MapController from './MapController';
import 'leaflet/dist/leaflet.css';
import 'assets/css/OpenStreetMap.css';
export const OpenStreetMap = (props: MapContainerProps) => {
const initialPosition = {
lat: 35.6809591,
lng: 139.7673068,
};
return (
<MapContainer
center={initialPosition}
zoom={17}
scrollWheelZoom={true}
whenReady={() => {
console.log('This function will fire once the map is created');
}}
whenCreated={(map) => {
console.log('The underlying leaflet map instance:', map);
// デバイスの現在地の緯度経度を取得する
map.locate();
}}
>
<TileLayer
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapController />
<LocationMarker />
</MapContainer>
);
};
export default OpenStreetMap;
useMapEvents は Leaflet の関数です。
locationfound(e) はデバイスの現在地の緯度経度情報を見つけた時に処理されます。e には現在地の緯度経度情報が入っています。
moveend(e) は地図を移動し終えた時に処理されます。e には地図の中心の緯度経度情報が入っています。
setCurrentLocation と setMapCenterPosition で各々の緯度経度情報を redux に渡しています。
import { useAppDispatch } from 'hooks';
import {
setCurrentLocation,
setMapCenterPosition,
} from 'stores/openstreetmap-slice';
import { useMapEvents } from 'react-leaflet';
export const MapController = (props: any) => {
const dispatch = useAppDispatch();
const map = useMapEvents({
locationfound(e) {
// デバイスの現在地の緯度経度情報を redux に渡す
dispatch(
setCurrentLocation({
lat: e.latlng.lat,
lng: e.latlng.lng
})
);
// デバイスの現在地の緯度経度に地図の中心を移動する
map.flyTo(e.latlng, map.getZoom());
},
moveend(e) {
// 地図の中心の緯度経度情報を redux に渡す
dispatch(
setMapCenterPosition({
lat: e.target._lastCenter.lat,
lng: e.target._lastCenter.lng,
})
);
},
});
return <></>;
};
export default MapController;
Redux Toolkit で状態管理
npx create-react-app my-app --template redux-typescript
redux 周りは上記で生成したソースコードをベースに作っています。
Leaflet で取得できる現在地(currentLocation)と地図上の位置情報(mapCenterPosition)を redux で管理しています。
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState } from 'store';
import { LatLngLiteral } from 'leaflet';
const STORE_NAME = 'openstreetmap';
export interface OpenStreetMapState {
status: 'idle' | 'loading' | 'failed';
currentLocation: LatLngLiteral;
mapCenterPosition: LatLngLiteral;
}
const initialPosition: LatLngLiteral = {
lat: 35.6809591,
lng: 139.7673068,
};
const initialState: OpenStreetMapState = {
status: 'idle',
currentLocation: initialPosition,
mapCenterPosition: initialPosition,
};
export const openstreetmapSlice = createSlice({
name: STORE_NAME,
initialState,
reducers: {
setCurrentLocation: (state, action: PayloadAction<LatLngLiteral>) => {
state.currentLocation = action.payload;
},
setMapCenterPosition: (state, action: PayloadAction<LatLngLiteral>) => {
state.mapCenterPosition = action.payload;
},
},
extraReducers: (builder) => {},
});
export const selectCurrentLocation = (state: RootState) => state.openstreetmap.currentLocation;
export const selectMapCenterPosition = (state: RootState) => state.openstreetmap.mapCenterPosition;
export const { setCurrentLocation, setMapCenterPosition } = openstreetmapSlice.actions;
export default openstreetmapSlice.reducer;
Redux から Leaflet へデータを渡す
オープンデータを Leaflet に表示する際には以下の手順になります。
1.API 通信して redux でオープンデータを管理(※ stores/opendata-slice 部分)
2.Leaflet 地図上に redux のデータをセットした React コンポーネントを描画
import { useAppSelector } from 'hooks';
import { selectCurrentLocation } from 'stores/openstreetmap-slice';
import { selectOpenData} from 'stores/opendata-slice';
import { LatLng } from 'leaflet';
import { Circle, FeatureGroup, Popup } from 'react-leaflet';
import { Avatar, Button, Card, CardActions, CardHeader } from '@mui/material';
import DirectionsIcon from '@mui/icons-material/Directions';
export const LocationMarker = () => {
const currentLocation = useAppSelector(selectCurrentLocation);
const openDataList = useAppSelector(selectOpenData);
const OpenDataCircle = openDataList.map(
(openData, index) => {
return (
<Circle
key={index.toString()}
pathOptions={{ color: 'orange' }}
center={new LatLng(openData.lat, openData.lng)}
radius={50}
eventHandlers={{
click: () => {
// console.log('Circle clicked');
},
}}
>
<Popup closeButton={false}>
<Card sx={{ maxWidth: 'auto' }}>
<CardHeader
avatar={<Avatar src={<DirectionsIcon />} variant="square" />}
title={openData.name}
}
/>
<CardActions>
<Button variant="outlined" href={openData.id} >詳細を見る</Button>
</CardActions>
</Card>
</Popup>
</Circle>
);
}
);
return currentLocation === null ? null : (
<FeatureGroup>
{OpenDataCircle}
</FeatureGroup>
);
};
export default LocationMarker;
以上です。どなたかのお役に立てれば嬉しいです。