LoginSignup
42
35

More than 3 years have passed since last update.

React-leafletの使い方メモ

Last updated at Posted at 2020-11-26

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
mkdir react-leaflet
cd react-leaflet
npx create-react-app my-app

2. インストール

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

開発版をインストールする場合
npm install leaflet react-leaflet@next --save
安定版をインストールする場合
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触ったことがある方はイメージしやすいと思います。

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クラスを呼ぶように変更

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コンテナの表示幅を指定

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

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

サーバーを起動して確認

サーバーを起動します

terminal
npm start

以上でhttp://localhost:3000/ に地図が表示される筈です
image.png

4. イベント管理

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

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>
    )
  }
}
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

5. ポリゴンを表示

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

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

6. SVG overlay

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

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

7. LayerGroupとFeatureGroup

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

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

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

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

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

9. LayersControl

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

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

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

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

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

11. Drag可能なMarker

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

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

12. View Bounds

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

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

13. Animated Panning

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

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の情報をコンポーネント外で使うことも出来ます

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

まとめ

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

42
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
35