かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

OpenStreetMap + React Leaflet で地図を表示したメモ

最近は地図関係のアプリを開発してて、ライブラリも多く実装したものを定期的にメモに残しておこうと思う

Google Map の API が高額でいろいろなレイヤーを重ね合わせるのが少し面倒そうだったので、OpenStreetMap を使う方法を探索した

OpenSteetMap を使えるライブラリはかなりたくさんあり色々試したが、本記事では React Leaflet でシンプルな地図を表示させたメモです

環境
  • React 18.2.0
  • leaflet 1.9.4
  • react-leaflet: 4.2.1

React Leaflet を使う準備

$ npm i react-leaflet leaflet
$ npm i -D @types/leaflet

React Leafret で OpenStreetMap を表示する

// Map.tsx
import { latLng } from "leaflet";
import { MapContainer, TileLayer } from "react-leaflet";
// leaflet のスタイルがないと地図が崩れる
import "leaflet/dist/leaflet.css";
// 地図を表示させるコンテナの height が必要
import "./map.css";

const position = latLng([35.3607411, 138.727262]);
const zoom = 5;

export const Map = (): JSX.Element => {
  return (
    <MapContainer center={position} zoom={zoom}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
        {/* OpenStreetMap のスタイルを選択して指定 */}
        url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
        maxZoom={13}
        minZoom={2}
      />
    </MapContainer>
  );
};
/* map.css */
.leaflet-container {
  width: 100%;
  height: 100vh;
}

React Leaflet + OpenStreetMap

これでシンプルな地図が表示できた。
ハマりどころは Leaflet の CSS (leaflet/dist/leaflet.css) が必要なことと、Leaflet が出力するコンテナの高さがないと地図が表示されないこと。TypeScript だと position を latLng 関数を挟まないと Type Error になることくらい

クリックした箇所に marker を表示させる

useMapEvent hook で地図上のイベントを取得できるので、クリックされた lat, long を取得して Marker として表示させる

// LocationMarkers.tsx
import { useState } from "react";
import { LatLng, latLng, icon } from "leaflet";
import { Marker, Popup, useMapEvent } from "react-leaflet";

const ICON = icon({
  iconUrl: "/images/leaflet/marker-icon.png",
  iconSize: [25, 41],
  iconAnchor: [12, 41],
  popupAnchor: [0, -30],
});

export const LocationMarkers = (): JSX.Element => {
const [markers, setMarkers] = useState<LatLng[]>([]);
  // useMapEvent は `<MapContainer>` の内側からしか呼び出せない
  const map = useMapEvent("click", (e) => {
    const { lat, lng } = e.latlng;
    setMarkers((markers) => [...markers, latLng(lat, lng)]);
  });

  return (
    <>
      {markers.map((marker) => (
        <Marker key={marker.toString()} position={marker} icon={ICON}>
          <Popup>{marker.toString()}</Popup>
        </Marker>
      ))}
    </>
  );
};

<MapContainer> 内に作成した <LocationMarkers /> コンポーネントを追加すれば OK

// Map.tsx
export const Map = (): JSX.Element => {
  return (
    <MapContainer center={position} zoom={zoom}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
        url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
        maxZoom={13}
        minZoom={2}
      />
      <LocationMarkers />
    </MapContainer>
  );
};

markers

地図のクリックした箇所にマーカーが表示されマーカーをクリックするとポップアップが表示されるようになった!

🐞 デフォルトのマーカー画像が表示されない問題

Leaflet.Marker.prototype.options.iconLeaflet.icon を渡す方法・Leaflet.Icon.Default.mergeOptions にアイコン画像を設定する方法を issue でみつけたが leaflet v1.9.4, react-leaflet v4.2.1 の環境では画像が壊れたままになってしまったので、カスタムアイコンを作って表示させた

h3-js で取得したエリアを React Leaflet で表示させる

h3geo.org h3-js は Uber の開発した世界を六角形のグリッドで表すJavaScript のライブラリ。
h3-js で取得した六角形のエリアを lat, long の矩形 (polygon) として地図上に表示させてみます

$ npm i h3-js

h3 では世界をどのサイズの六角形のグリッド化しているのかというサイズを resolution という数字で表しています。
今回はマーカーと同じようにクリックした位置が含まれる resolution を Polygon 化して地図上にプロットします。

// HexLayer.tsx
import { FC, useState } from "react";
import { LatLng } from "leaflet";
import { Polygon, useMapEvent } from "react-leaflet";
import { cellToBoundary, CoordPair, latLngToCell } from "h3-js";

const resolution = 3;

export const HexLayer: FC = () => {
  const [hexagons, setHexagons] = useState<{index: string, polygon: CoordPair[]}[]>([]);
  const map = useMapEvent("click", (e) => {
    const { lat, lng } = e.latlng;
    const h3Index = latLngToCell(lat, lng, resolution);
    const boundary = cellToBoundary(h3Index);

    setHexagons((hexagons) => [...hexagons, {
      index: h3Index,
      polygon: boundary,
    }]);
  });

  return (
    <>
      {hexagons.map(({ index, polygon }) => (
        <Polygon key={index} positions={polygon} />
      ))}
    </>
  );
}

h3 hex

h3-js で取得したエリアを Polygon として表示させることが出来ました!
※ Leaflet は左右に地図を移動させると経度が -600 や 1000 のように -180 - 180 の範囲を超えてしまうようです。h3-js の cellToBoundary で変換した座標は -180 〜 180 の間に収まる数値に変換されるため、地図を移動させまくると Ploygon が表示されないようにみえてしまいます

Sample

所管

React-Leaflet の example には実例が色々とコードと共に載っているので、扱いやすいライブラリだなと感じました。
一方で Leaflet に依存しているので、ちょっとした問題があると Leaflet 側まで見に行かなければならない事もあるのは注意が必要そうです (経度が延々大きくも小さくもなる問題とか)

また React-Leaflet は地図上に表示させているマーカーや矩形を 画像や SVG といった HTML 要素で表現しているので表示量が多くなると Canvas ベースのものより処理が重くなるのではないかな〜という気がしています。 単体でできることが豊富なのでサクッと地図を使ったモックなどを作るのには向いてそうですね!

おわり


[参考]

13歳からの地政学 サクッと読めて面白かった!