Leaflet 地図上の緯度経度情報を React Redux で状態管理する

先日は React で OpenStreetMap を表示することが出来る Leaflet をご紹介しました。外部データを地図上に表示したいというユースケースがあると思います。React の状態管理で広く使われている Redux と Leaflet を連携させる方法を解説します。

https://qiita.com/t-kurasawa/items/03e5bc9c9a07d8ff99b7

## Leaflet から Redux へデータを渡す

### Leaflet

[MapContainer](https://react-leaflet.js.org/docs/api-map/)  はマップを表示・操作するコンポーネントです。このコンポーネント配下では Leaflet の地図を操作することが出来ます。
MapController は地図操作処理を記述したコンポーネントです。
TileLayer は表示する地図を定義しています。
LocationMarker は後述します。

```OpenStreetMap.tsx
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='&copy; <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 に渡しています。


```MapController.tsx
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 で管理しています。


```openstreetmap-slice.ts
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 に表示する際には以下の手順になります。
１．API 通信して redux でオープンデータを管理（※ stores/opendata-slice 部分）
２．Leaflet 地図上に redux のデータをセットした React コンポーネントを描画


```LocationMarker.tsx
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;

```

以上です。どなたかのお役に立てれば嬉しいです。