React-leafletの使い方メモ

# React-leafletとは

Open Source Mapを表示するJavaScriptライブラリであるleafletを、React.js上で使えるように拡張するライブラリです。

React Leaflet公式
https://react-leaflet.js.org/

本記事は公式のチュートリアルを順番にやっていくだけですので詳しくは公式サイトを参照してください
https://react-leaflet.js.org/docs/start-introduction


# 1. プロジェクトの作成



```terminal:terminal
mkdir react-leaflet
cd react-leaflet
npx create-react-app my-app
```


# 2. インストール


今回は開発版をインストールしました

```terminal:開発版をインストールする場合
npm install leaflet react-leaflet@next --save
```

```terminal:安定版をインストールする場合
npm install leaflet react-leaflet --save
```


# 3. 地図を表示


こちらのコードを参考にやっていきます
https://qiita.com/sugasaki/items/d225cf548e9a787dbd9c


#### 地図を表示する為のクラスを作成


MapContainerで地図オブジェクトをつくり、TileLayerで地図タイルを取得できるようにすると地図が表示されます。

MapContainerがL.map()、TileLayerがL.tileLayer()、MarkerがL.marker()に対応していますので、leaflet触ったことがある方はイメージしやすいと思います。

```javascript:my-app/component/simple.js
import React, { Component } from 'react'
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet'

export default class SimpleExample extends Component {
  render() {
    const position = [51.505, -0.09];
    return (
      <MapContainer center={position} zoom={13} scrollWheelZoom={false}>
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <Marker position={position}>
          <Popup>
            A pretty CSS3 popup. <br /> Easily customizable.
          </Popup>
        </Marker>
      </MapContainer>
    )
  }
}
```


#### App.jsでSimpleExampleクラスを呼ぶように変更


```javascript:my-app/App.js
import Leaflet from 'leaflet'
import React, { Component } from 'react';
import './App.css';
import 'leaflet/dist/leaflet.css';
import SimpleExample from './components/simple'

Leaflet.Icon.Default.imagePath =
  '//cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/images/'

class App extends Component {
  render() {
    return (
      <SimpleExample />
    );
  }
}

export default App;
```


#### leafletコンテナの表示幅を指定


指定し忘れると高さゼロ(＝表示されない)になってしまうので必ず設定します

```css:my-app/index.js
.leaflet-container {
  width: 600px;
  height: 300px;
  margin: 10px;
```


#### サーバーを起動して確認


サーバーを起動します

```terminal:terminal
npm start
```

以上でhttp://localhost:3000/ に地図が表示される筈です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/e31c40e4-f226-7068-7937-30932d713f06.png)


# 4. イベント管理


useMapEvents()によりleafletのイベントハンドラを使うことができます。以下ではclick()イベントが発生したら端末の現在位置を取得してMarkerを置くという動作を追加しています。

```javascript:my-app/components/event.js
import React, { Component, useState } from 'react'
import { MapContainer, TileLayer, useMapEvents, Marker, Popup } from 'react-leaflet'


function LocationMarker() {
    const [position, setPosition] = useState(null)
    const map = useMapEvents({
      click() {
        map.locate()
      },
      locationfound(e) {
        setPosition(e.latlng)
        map.flyTo(e.latlng, map.getZoom())
      },
    })
  
    return position === null ? null : (
      <Marker position={position}>
        <Popup>You are here</Popup>
      </Marker>
    )
  }

export default class EventsExample extends Component {
  render() {
    const position = [51.505, -0.09];
    return (
      <MapContainer center={position} zoom={13} scrollWheelZoom={false}>
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <LocationMarker />
      </MapContainer>
    )
  }
}
```

```javascript:my-app/App.js
import Leaflet from 'leaflet'
import React, { Component } from 'react';
import './App.css';
import 'leaflet/dist/leaflet.css';
import SimpleExample from './components/simple'
import EventsExample from './components/events'

Leaflet.Icon.Default.imagePath =
  '//cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/images/'

class App extends Component {
  render() {
    return (
      <div>
        SimpleExample
        <SimpleExample />

        EventsExample
        <EventsExample />
      </div>
    );
  }
}

export default App;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/ffef4678-ddcf-21ff-9d4a-bb12fa853df6.png)


# 5. ポリゴンを表示


ポリゴンも簡単に追加できます

```javascript:my-app/src/components/polygon.js
import React, { Component } from 'react'
import { MapContainer, TileLayer, Popup, Circle, CircleMarker, Polyline, Polygon, Rectangle } from 'react-leaflet'

export default class PolygonExample extends Component {
  render() {
    const center = [51.505, -0.09];

    const polyline = [
      [51.505, -0.09],
      [51.51, -0.1],
      [51.51, -0.12],
    ];

    const multiPolyline = [
      [
        [51.5, -0.1],
        [51.5, -0.12],
        [51.52, -0.12],
      ],
      [
        [51.5, -0.05],
        [51.5, -0.06],
        [51.52, -0.06],
      ],
    ];

    const polygon = [
      [51.515, -0.09],
      [51.52, -0.1],
      [51.52, -0.12],
    ];

    const multiPolygon = [
      [
        [51.51, -0.12],
        [51.51, -0.13],
        [51.53, -0.13],
      ],
      [
        [51.51, -0.05],
        [51.51, -0.07],
        [51.53, -0.07],
      ],
    ];

    const rectangle = [
      [51.49, -0.08],
      [51.5, -0.06],
    ];

    const fillBlueOptions = { fillColor: 'blue' };
    const blackOptions = { color: 'black' };
    const limeOptions = { color: 'lime' };
    const purpleOptions = { color: 'purple' };
    const redOptions = { color: 'red' };

    return (
      <MapContainer center={center} zoom={13} scrollWheelZoom={false}>
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <Circle center={center} pathOptions={fillBlueOptions} radius={200} />
        <CircleMarker center={[51.51, -0.12]} pathOptions={redOptions} radius={20}>
          <Popup>Popup in CircleMarker</Popup>
        </CircleMarker>
        <Polyline pathOptions={limeOptions} positions={polyline} />
        <Polyline pathOptions={limeOptions} positions={multiPolyline} />
        <Polygon pathOptions={purpleOptions} positions={polygon} />
        <Polygon pathOptions={purpleOptions} positions={multiPolygon} />
        <Rectangle bounds={rectangle} pathOptions={blackOptions} />
      </MapContainer>
    )
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/2b6ee912-5305-d87a-c181-c0d46a8484b6.png)


# 6. SVG overlay


SVGを上に重ねることもできます


```javascript:my-app/src/components/svg.js
import React, { Component } from 'react'
import { MapContainer, TileLayer, SVGOverlay } from 'react-leaflet'

export default class SVGExample extends Component {
  render() {
    const position = [51.505, -0.09];
    const bounds = [
      [51.49, -0.08],
      [51.5, -0.06],
    ];
    return (
      <MapContainer center={position} zoom={13} scrollWheelZoom={false}>
      <TileLayer
        attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <SVGOverlay attributes={{ stroke: 'red' }} bounds={bounds}>
        <rect x="0" y="0" width="100%" height="100%" fill="blue" />
        <circle r="5" cx="10" cy="10" fill="red" />
        <text x="50%" y="50%" stroke="white">
          text
        </text>
      </SVGOverlay>
    </MapContainer>
    )
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/14616b0b-740b-f90c-2aad-027b1e906cad.png)


# 7. LayerGroupとFeatureGroup


leafletにあるLayerGroupとかFeatureGroupも使えます


```javascript:my-app/src/components/group.js
import React, { Component } from 'react'
import { MapContainer, TileLayer, Popup, LayerGroup, Circle, FeatureGroup, Rectangle } from 'react-leaflet'

export default class LayerGroupExample extends Component {
  render() {
    const center = [51.505, -0.09];
    const rectangle = [
      [51.49, -0.08],
      [51.5, -0.06],
    ];
    const fillBlueOptions = { fillColor: 'blue' };
    const fillRedOptions = { fillColor: 'red' };
    const greenOptions = { color: 'green', fillColor: 'green' };
    const purpleOptions = { color: 'purple' };

    return (
      <MapContainer center={center} zoom={13} scrollWheelZoom={false}>
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <LayerGroup>
          <Circle center={center} pathOptions={fillBlueOptions} radius={200} />
          <Circle
            center={center}
            pathOptions={fillRedOptions}
            radius={100}
            stroke={false}
          />
          <LayerGroup>
            <Circle
              center={[51.51, -0.08]}
              pathOptions={greenOptions}
              radius={100}
            />
          </LayerGroup>
        </LayerGroup>
        <FeatureGroup pathOptions={purpleOptions}>
          <Popup>Popup in FeatureGroup</Popup>
          <Circle center={[51.51, -0.06]} radius={200} />
          <Rectangle bounds={rectangle} />
        </FeatureGroup>
      </MapContainer>
    )
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/1b0ff659-6b90-6b24-62ef-210ed89d09fc.png)


# 8. マウスオーバーで注釈を出す


MarkerやCircleにTooltipを追加しておくと注釈を出すことが出来ます


```javascript:my-app/src/components/tooltips.js
import React, { Component } from 'react'
import { MapContainer, TileLayer, Marker, Popup, Circle, CircleMarker, Polygon, Rectangle, Tooltip } from 'react-leaflet'
import { useState, useMemo } from 'react'

const center = [51.505, -0.09]

const multiPolygon = [
  [
    [51.51, -0.12],
    [51.51, -0.13],
    [51.53, -0.13],
  ],
  [
    [51.51, -0.05],
    [51.51, -0.07],
    [51.53, -0.07],
  ],
];

const rectangle = [
  [51.49, -0.08],
  [51.5, -0.06],
];

function TooltipCircle() {
  const [clickedCount, setClickedCount] = useState(0)
  const eventHandlers = useMemo(
    () => ({
      click() {
        setClickedCount((count) => count + 1)
      },
    }),
    [],
  )

  const clickedText =
    clickedCount === 0
      ? 'Click this Circle to change the Tooltip text'
      : `Circle click: ${clickedCount}`

  return (
    <Circle
      center={center}
      eventHandlers={eventHandlers}
      pathOptions={{ fillColor: 'blue' }}
      radius={200}>
      <Tooltip>{clickedText}</Tooltip>
    </Circle>
  )
}

export default class ToolTipsExample extends Component {
  render() {
    return (
      <MapContainer center={center} zoom={13} scrollWheelZoom={false}>
      <TileLayer
        attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <TooltipCircle />
      <CircleMarker
        center={[51.51, -0.12]}
        pathOptions={{ color: 'red' }}
        radius={20}>
        <Tooltip>Tooltip for CircleMarker</Tooltip>
      </CircleMarker>
      <Marker position={[51.51, -0.09]}>
        <Popup>Popup for Marker</Popup>
        <Tooltip>Tooltip for Marker</Tooltip>
      </Marker>
      <Polygon pathOptions={{ color: 'purple' }} positions={multiPolygon}>
        <Tooltip sticky>sticky Tooltip for Polygon</Tooltip>
      </Polygon>
      <Rectangle bounds={rectangle} pathOptions={{ color: 'black' }}>
        <Tooltip direction="bottom" offset={[0, 20]} opacity={1} permanent>
          permanent Tooltip for Rectangle
        </Tooltip>
      </Rectangle>
      </MapContainer>
    )
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/2adc8e9f-b929-0860-da7e-2c8996acfcee.png)


# 9. LayersControl


レイヤーを選んで表示するようにすることもできます

```javascript:my-app/src/components/leyersControl.js
import React, { Component } from 'react'
import { MapContainer, TileLayer, Marker, Popup, LayersControl, LayerGroup, Circle, FeatureGroup, Rectangle } from 'react-leaflet'

export default class LayersControlExample extends Component {
  render() {
    const center = [51.505, -0.09];
    const rectangle = [
      [51.49, -0.08],
      [51.5, -0.06],
    ];

    return (
      <MapContainer center={center} zoom={13} scrollWheelZoom={false}>
        <LayersControl position="topright">
          <LayersControl.BaseLayer checked name="OpenStreetMap.Mapnik">
            <TileLayer
              attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
              url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
            />
          </LayersControl.BaseLayer>
          <LayersControl.BaseLayer name="OpenStreetMap.BlackAndWhite">
            <TileLayer
              attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
              url="https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png"
            />
          </LayersControl.BaseLayer>
          <LayersControl.Overlay name="Marker with popup">
            <Marker position={center}>
              <Popup>
                A pretty CSS3 popup. <br /> Easily customizable.
              </Popup>
            </Marker>
          </LayersControl.Overlay>
          <LayersControl.Overlay checked name="Layer group with circles">
            <LayerGroup>
              <Circle
                center={center}
                pathOptions={{ fillColor: 'blue' }}
                radius={200}
              />
              <Circle
                center={center}
                pathOptions={{ fillColor: 'red' }}
                radius={100}
                stroke={false}
              />
              <LayerGroup>
                <Circle
                  center={[51.51, -0.08]}
                  pathOptions={{ color: 'green', fillColor: 'green' }}
                  radius={100}
                />
              </LayerGroup>
            </LayerGroup>
          </LayersControl.Overlay>
          <LayersControl.Overlay name="Feature group">
            <FeatureGroup pathOptions={{ color: 'purple' }}>
              <Popup>Popup in FeatureGroup</Popup>
              <Circle center={[51.51, -0.06]} radius={200} />
              <Rectangle bounds={rectangle} />
            </FeatureGroup>
          </LayersControl.Overlay>
        </LayersControl>
      </MapContainer>
    )
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/c33ba959-eb44-9079-f129-beaed961dc79.png)


# 10. stateの値でRectangleの色を変える


この辺からleaflet単独ではなくReactからleafletを操作する手順になります。ここではEffectフックでRectangleの色を変えています。


```javascript:my-app/src/components/panes.js
import React, { Component, useState, useRef, useEffect } from 'react'
import { MapContainer, TileLayer, Pane, Rectangle } from 'react-leaflet'

const outer = [
  [50.505, -29.09],
  [52.505, 29.09],
]
const inner = [
  [49.505, -2.09],
  [53.505, 2.09],
]

function BlinkingPane() {
  const [render, setRender] = useState(true)
  const timerRef = useRef()
  useEffect(() => {
    timerRef.current = setInterval(() => {
      setRender((r) => !r)
    }, 1000)
    return () => {
      clearInterval(timerRef.current)
    }
  }, [])

  return render ? (
    <Pane name="cyan-rectangle" style={{ zIndex: 500 }}>
      <Rectangle bounds={outer} pathOptions={{ color: 'cyan' }} />
    </Pane>
  ) : null
}

export default class PanesExample extends Component {
  render() {
    return (
      <MapContainer bounds={outer} scrollWheelZoom={false}>
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <BlinkingPane />
        <Pane name="yellow-rectangle" style={{ zIndex: 499 }}>
          <Rectangle bounds={inner} pathOptions={{ color: 'yellow' }} />
          <Pane name="purple-rectangle">
            <Rectangle bounds={outer} pathOptions={{ color: 'purple' }} />
          </Pane>
        </Pane>
      </MapContainer>
    )
  }
}
```
横に長いRectangleが紫⇔青と交互に色を変えながら表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/9f57b337-d18b-a124-e0f9-b3be9a063f1f.png)


# 11. Drag可能なMarker


MarkerをDraggableにするかどうかをReact stateにしておいて、途中でDrag可能にしたりDragできなくしたりする例です

```javascript:my-app/src/components/draggable.js
import React, { Component, useState, useRef, useMemo, useCallback } from 'react'
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet'

const center = {
  lat: 51.505,
  lng: -0.09,
}

function DraggableMarker() {
  const [draggable, setDraggable] = useState(false)
  const [position, setPosition] = useState(center)
  const markerRef = useRef(null)
  const eventHandlers = useMemo(
    () => ({
      dragend() {
        const marker = markerRef.current
        if (marker != null) {
          setPosition(marker.getLatLng())
        }
      },
    }),
    [],
  )
  const toggleDraggable = useCallback(() => {
    setDraggable((d) => !d)
  }, [])

  return (
    <Marker
      draggable={draggable}
      eventHandlers={eventHandlers}
      position={position}
      ref={markerRef}>
      <Popup minWidth={90}>
        <span onClick={toggleDraggable}>
          {draggable
            ? 'Marker is draggable'
            : 'Click here to make marker draggable'}
        </span>
      </Popup>
    </Marker>
  )
}

export default class DraggableMarkerExample extends Component {
  render() {
    return (
      <MapContainer center={center} zoom={13} scrollWheelZoom={false}>
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <DraggableMarker />
      </MapContainer>
    )
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/cb32aa98-a4a6-34ae-5949-7ca323c12550.png)


# 12. View Bounds


innerBoundsまたはouterBoundsの内側をクリックすると、クリックした側をinnerHandler、クリックしていない側をouterHandlerに登録して、その範囲が大きく表示されるように

```javascript:my-app/src/components/viewBounds.js
import React, { Component, useState, useMemo } from 'react'
import { MapContainer, TileLayer, Rectangle, useMap } from 'react-leaflet'

const innerBounds = [
  [49.505, -2.09],
  [53.505, 2.09],
]
const outerBounds = [
  [50.505, -29.09],
  [52.505, 29.09],
]

const redColor = { color: 'red' }
const whiteColor = { color: 'white' }

function SetBoundsRectangles() {
  const [bounds, setBounds] = useState(outerBounds)
  const map = useMap()

  const innerHandlers = useMemo(
    () => ({
      click() {
        setBounds(innerBounds)
        map.fitBounds(innerBounds)
      },
    }),
    [map],
  )
  const outerHandlers = useMemo(
    () => ({
      click() {
        setBounds(outerBounds)
        map.fitBounds(outerBounds)
      },
    }),
    [map],
  )

  return (
    <>
      <Rectangle
        bounds={outerBounds}
        eventHandlers={outerHandlers}
        pathOptions={bounds === outerBounds ? redColor : whiteColor}
      />
      <Rectangle
        bounds={innerBounds}
        eventHandlers={innerHandlers}
        pathOptions={bounds === innerBounds ? redColor : whiteColor}
      />
    </>
  )
}

export default class ViewBoundsExample extends Component {
  render() {
    return (
      <MapContainer bounds={outerBounds} scrollWheelZoom={false}>
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <SetBoundsRectangles />
      </MapContainer>
    )
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/6e9fcb61-3d17-1ee9-f5e4-99eeea62b08b.png)



# 13. Animated Panning


checkboxの値でsetViewのanimateを有効にしたり無効にしたりできます

```javascript:my-app/src/components/animatedPanning.js
import React, { Component, useRef } from 'react'
import { MapContainer, TileLayer, useMapEvent } from 'react-leaflet'

function SetViewOnClick({ animateRef }) {
  const map = useMapEvent('click', (e) => {
    map.setView(e.latlng, map.getZoom(), {
      animate: animateRef.current || false,
    })
  })

  return null
}

function AnimateExample() {
  const animateRef = useRef(false)

  return (
    <>
      <p>
        <label>
          <input
            type="checkbox"
            onChange={() => {
              animateRef.current = !animateRef.current
            }}
          />
          Animate panning
        </label>
      </p>
      <MapContainer center={[51.505, -0.09]} zoom={13} scrollWheelZoom={false}>
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <SetViewOnClick animateRef={animateRef} />
      </MapContainer>
    </>
  )
}

export default class AnimatedPanningExample extends Component {
  render() {
    return (
      <AnimateExample />
    )
  }
}
```



# 14. External State


逆にleafletの情報をコンポーネント外で使うことも出来ます

```javascript:my-app/src/components/
import React, {useState, useCallback, useEffect, useMemo} from 'react'
import { MapContainer, TileLayer } from 'react-leaflet'

const center = [51.505, -0.09]
const zoom = 13

function DisplayPosition({ map }) {
  const [position, setPosition] = useState(map.getCenter())

  const onClick = useCallback(() => {
    map.setView(center, zoom)
  }, [map])

  const onMove = useCallback(() => {
    setPosition(map.getCenter())
  }, [map])

  useEffect(() => {
    map.on('move', onMove)
    return () => {
      map.off('move', onMove)
    }
  }, [map, onMove])

  return (
    <p>
      latitude: {position.lat.toFixed(4)}, longitude: {position.lng.toFixed(4)}{' '}
      <button onClick={onClick}>reset</button>
    </p>
  )
}

function ExternalStateExample() {
  const [map, setMap] = useState(null)

  const displayMap = useMemo(
    () => (
      <MapContainer
        center={center}
        zoom={zoom}
        scrollWheelZoom={false}
        whenCreated={setMap}>
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
      </MapContainer>
    ),
    [],
  )

  return (
    <div>
      {map ? <DisplayPosition map={map} /> : null}
      {displayMap}
    </div>
  )
}

export default ExternalStateExample;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/df4f7199-0938-d9ea-3f32-a870ef4400a8.png)



# まとめ

Reactを使う環境でleafletを使うなら単独で使うよりこちらを使った方が良さげです