React-Leafletで無料でブラウザに地図を載せてみよう

2022年12月7日水曜日

react

t f B! P L

Leafletは Webブラウザ上に地図を載せられるオープンソースのJavaScriptライブラリです。そして今回紹介する「React-Leaflet」は、Leafletを Reactからコンポーネント思考で使えるようにした拡張ライブラリです。

この記事では「React-Leaflet」を使って、地図をブラウザ上に表示させることや、簡単なイベント処理について紹介します。

スポンサーリンク

インストール

npmの人

npm install react-leaflet

yarnの人

yarn add react-leaflet

TypeScriptの場合

TypeScirptを使用している場合は、追加で以下もインストール

npm install -D @types/leaflet
yarn add -D @types/leaflet

基本のマップ表示

TypeScriptのコード例で、基本のマップ表示をしてみる。

まず、MapContainerタグに地図中央に表示する緯度・経度や、ズーム率や style で要素のサイズなどを指定していく。

その下には、TileLayer タグを置き、表示する地図タイルを指定する。メジャーなところで、地図タイルには「OpenStreetMap」や「日本地理院」などのがある。(以下の例では OpenStreetMapの地図タイルを使用している)

import { MapContainer, TileLayer } from 'react-leaflet';
import { LatLngTuple } from 'leaflet';

function App() {

  //地図の中央に表示する緯度・経度
  const position: LatLngTuple = [35.710179001728534, 139.8107304222906]
        
  return (
    <MapContainer center={position} zoom={17} scrollWheelZoom={false} style={{ height: "100vh" }}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
    </MapContainer>
  )
}

export default App;

■実行結果
enter image description here

マーカーを表示する

指定した緯度・経度の場所に、マーカーを表示してみます。

<Marker>タグを挿入し、position 属性にマーカーを表示する緯度・経度を指定します。また、<Popup>タグには、マーカーがクリックされた際に表示するポップアップメッセージを設定します。

function App() {
  const position: LatLngTuple = [35.710179001728534, 139.8107304222906]

  return (
    <MapContainer center={position} zoom={17} scrollWheelZoom={false} style={{ height: "100vh" }}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      {/* マーカーの表示 */}
      <Marker position={[35.71042994800952, 139.80907162872504]}>
        <Popup>
          A pretty CSS3 popup. <br /> Easily customizable.
        </Popup>
      </Marker>
    </MapContainer>
  )
}

■実行結果
enter image description here

あれっ、マーカーアイコンの画像がリンクエラーになり表示されていません。。。

この現象がバグなのかは不明ですが、「React-Leaflet」ではフォルトのマーカーアイコンを設定しておく必要があるため、次のようにコンポーネントを宣言する前に、デフォルトのマーカーアイコンを設定する処理を追加します。

//マーカーのデフォルトアイコンを設定
let DefaultIcon = Leaflet.icon({
  iconUrl: icon,
  shadowUrl: iconShadow,
});
Leaflet.Marker.prototype.options.icon = DefaultIcon;

function App() {
  const position: LatLngTuple = [35.710179001728534, 139.8107304222906]

  return (
    <MapContainer center={position} zoom={17} scrollWheelZoom={false} style={{ height: "100vh" }}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      {/* マーカーの表示 */}
      <Marker position={[35.71042994800952, 139.80907162872504]}>
        <Popup>
          A pretty CSS3 popup. <br /> Easily customizable.
        </Popup>
      </Marker>
    </MapContainer>
  )
}

無事、マーカーアイコンが表示されました。

■実行結果
enter image description here

スポンサーリンク

マップのクリックイベントを実装

地図上でクリックした所に、マーカーを表示するサンプルを作ってみます。

React-Leafletでは、マウス操作など地図上で発生するイベントは useMapEvents フックを使って取得します。

最初に、useMapEventsMapContainer のコンテキスト内でのみ使用可能であるため、クリックイベントを受け付けるサブコンポーネントを作ります。

import { LatLng } from 'leaflet'
import { useState } from 'react'
import { Marker, Popup, useMapEvents } from 'react-leaflet'

function ClickMaker() {
  const [position, setPosition] = useState<LatLng | null>(null)

  const map = useMapEvents({
    click(e) {
      setPosition(e.latlng)
    }
  })

  return (
    <>
      {position && (
        <Marker position={position}>
          <Popup>You are here</Popup>
        </Marker>
      )}
    </>
  )
}

export default ClickMaker

上で作成した ClickMaker を親の App コンポーネントに配置します。

function App() {
  const position: LatLngTuple = [35.710179001728534, 139.8107304222906]

  return (
    <MapContainer center={position} zoom={17} scrollWheelZoom={false} style={{ height: "100vh" }}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      {/* マーカーの表示 */}
      <ClickMaker/>
    </MapContainer>
  )
}

■実行結果
enter image description here

図形を書く

地図上に図形を書くサンプルコードを作って見ます。

四角形(Rectangle)

<MapContainer center={position} zoom={17} scrollWheelZoom={false} style={{ height: "100vh" }}>
  <TileLayer
    attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
  />
  <Rectangle 
    bounds={[
      [35.710925125032475, 139.8085439015381],
      [35.709940717542715, 139.81317875874197]
    ]} 
    pathOptions={ { color: "red" }} 
  />
</MapContainer>

■実行結果
enter image description here

円(Circle)

center に円の中央の緯度・経度を指定。radius には円の半径をメートル単位で指定する。

<MapContainer center={position} zoom={17} scrollWheelZoom={false} style={{ height: "100vh" }}>
  <TileLayer
    attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
  />
  <Circle 
    center={[35.710925125032475, 139.8085439015381]} 
    pathOptions={ {fillColor: 'blue'} } 
    radius={100} 
  />
</MapContainer>

■実行結果
enter image description here

ポリライン

positions に一連の線で結ぶ緯度・経度を配列で指定する。

<MapContainer center={position} zoom={17} scrollWheelZoom={false} style={{ height: "100vh" }}>
  <TileLayer
    attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
  />
  <Polyline 
    pathOptions={{ color: 'purple', weight: 10 }} 
    positions={[
        [35.70970550333407, 139.80904279241705],
        [35.70961838678436, 139.8098903704621],
        [35.709940717542715, 139.81296954639802],
        [35.71013237305134, 139.81329141147842],
        [35.710820586761024, 139.81380639560706],
        [35.710419855450226, 139.81458960063608]
    ]} />
</MapContainer>

謎の線になってしまいましたが、こんな感じで自由に座標を指定して線を結ぶことができます。

■実行結果
enter image description here

ポリゴン(Polygon)

positions に多角形の頂点となる緯度・経度を指定する。

<MapContainer center={position} zoom={17} scrollWheelZoom={false} style={{ height: "100vh" }}>
  <TileLayer
    attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
  />
  <Polygon 
    pathOptions={{ color: "red" }} 
    positions={[
      [35.71108053847581, 139.80874263954215],
      [35.712050935021, 139.81003687451388],
      [35.71173578472511, 139.8123484064776],
      [35.71087720566655, 139.81086432991802]
    ]} 
  />
</MapContainer>

■実行結果

enter image description here

スポンサーリンク

住所検索

住所・施設名などのキーワードから位置情報を検索し、見つかった場所を地図に表示するサンプルコードを作ってみます。「React-Leaflet」自体に住所検索をする機能はないため、今回、国土地理院のジオコーディング APIを使ってキーワードから緯度・経度を取得します。

まず、見つかった緯度・経度の場所に地図の表示を切り替えるコンポーネントを作成します。

/**
 * マップの表示位置をパラメータで指定した緯度・経度に切り替えるためのコンポーネント 
 */
function MapViewControl(prop: {position: LatLngTuple}) {
  const map = useMap()
  useLayoutEffect(() => {
    map.setView(prop.position)
  }, [prop.position])
  return (null)
}

メインのコンポーネントには、住所検索エリアと、マップ表示の MapContainer を置き、その下にタイルと先ほど作成した MapViewControl コンポーネントを配置します。

function App() {
  const [position, setPosition] = useState<LatLngTuple>([35.710304502694505, 139.81029660578832])
  const [address, setAddress] = useState("")

  //住所検索
  const onSearch = async () => {
     //処理内容は後述
  }

  return (
    <>
      {/* 住所検索エリアを追加 */}
      <div>
        <input 
          type="text" 
          style={ {width: "30rem"} }
          value={address}
          onChange={ e => setAddress(e.target.value) }
        />
        <button onClick={onSearch}>住所検索</button>
      </div>
      <MapContainer center={position} zoom={17} scrollWheelZoom={false} style={{ height: "100vh" }}>
        <TileLayer
          attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        {/* マップの表示位置を切り替えるコンポーネントを配置 */}
        <MapViewControl position={position}/> 
      </MapContainer>
    </>
  )
}

最後に、上の onSearch 関数の中身を完成させます。

国土地理院のジオコーディング APIを使って、キーワードにヒットした住所の緯度・経度に、地図の表示を切り替える処理を実装します。

  const onSearch = async () => {
    //「国土地理院API」でキーワードから緯度・経度を含む住所情報を取得
    const url = `https://msearch.gsi.go.jp/address-search/AddressSearch?q=${encodeURIComponent(address)}`
    const response = await fetch(url);
    const results = await response.json()
  
    if (Array.isArray(results) && results.length > 0) {
      //見つかった住所(施設)の位置を表示
      const coordinates = results[0].geometry.coordinates
      setPosition([coordinates[1], coordinates[0]])
    } else {
      alert("Not Found")
    }
  }

これで完成です。実際に動かして見ます。

【実行結果】

住所検索前の表示(スカイツリーを表示)
enter image description here

「ディズニーランド」で検索
enter image description here

うん、いいですね。

ちなみに、国土地理院のジオコーディング APIは、“渋谷3丁目” のような基本的な住所検索と、キーワード検索であれば、官公庁やディズニーランドのような有名な施設であれば検索が可能である。ただし、“セブンイレブン渋谷3丁目明治通り店” のようなローカルな施設の検索には対応していないため、精度の高い住所検索を求めるのなら、有料の Google Maps API を使う必要がある。

ルート表示

指定した地点間のルート表示を「react-leaflet-routing-machine」を使ってお手軽に作ってみます。

以下をインストールします。

npm i react-leaflet-routing-machine 

TypeScriptの人はこちらもインストール

npm i @types/leaflet-routing-machine

ルート表示実装

まず、2つの地点間のルート表示をするコンポーネントを作ります。

今回のサンプルコードでは、浅草神社からスカイツリーまでのルートを求めています。

import Leaflet from "leaflet";
import { createControlComponent } from "@react-leaflet/core";
import "leaflet-routing-machine";
import "leaflet-routing-machine/dist/leaflet-routing-machine.css";

/**
 * ルート表示用のコントール
 */
const createRoutineMachineLayer = (props: any) => {
  console.log(props)
  const instance = Leaflet.Routing.control({
    waypoints: [
      Leaflet.latLng(35.71498168439901, 139.79663181249592),
      Leaflet.latLng(35.71020351730888, 139.81066512955994)
    ],
    lineOptions: {
      styles: [
        {
          color: "blue",
          opacity: 0.6,
          weight: 4
        },
      ],
      extendToWaypoints: true,
      missingRouteTolerance: 1,
    }
  });
  return instance;
};

const RoutingMachine = createControlComponent(createRoutineMachineLayer);

次にメインのコンポーネントに、上で作成した RoutingMachine を配置します。

function App() {
  const [position, setPosition] = useState<LatLngTuple>([35.710304502694505, 139.81029660578832])

  return (
    <MapContainer center={position} zoom={17} scrollWheelZoom={false} style={{ height: "100vh" }}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      {/* ルート表示用のコントロール */}
      <RoutingMachine/>
    </MapContainer>
  )
}

実行すると、次のようにルート上にラインが引かれ、右側に道順の案内が表示されます。

■実行結果
enter image description here

ルートの表示は問題なさそうだが、右側に表示される道順の案内の日本語がぶっ壊れていますね。
これは「react-leaflet-routing-machine」のデフォルトのルート探索エンジンが OSRM (Open Source Routing Machine) であるからでしょう。

スポンサーリンク
スポンサーリンク

このブログを検索

Profile

自分の写真
Webアプリエンジニア。 日々新しい技術を追い求めてブログでアウトプットしています。
プロフィール画像は、猫村ゆゆこ様に書いてもらいました。

仕事募集もしていたり、していなかったり。

QooQ