🌉

【React】Mapboxを使用して検索機能付きの地図を表示してみる

2022/04/14に公開

概要

Mapboxは、GoogleMapと同様の地図を表示するサービスです。王者Googleマップにライバル登場!「Mapbox」はどこまで使えるか?の記事に、概要や値段等紹介されていますが、GoogleMapと比べて無料枠も多いし安く使えるようなサービスになっています。
今回はReactでこのMapboxを使用し、検索機能付きの地図を表示してみます。

実装する内容

以下のイメージの内容のものを表示します。

実装時に考慮する内容としては、主に以下の3点です。

  • 地図の検索ボックスが表示されるようにする。
  • 検索結果の地点にピンを表示する。
  • 地名が日本語で表示されるようにする。

前準備や参考にしたものなど

実装サンプル

まずは地図本体の表示と、言語切り替えの実装部分です。
緯度経度の初期値については、propから渡すことを想定しています。

MapboxComponent.js
import Map, { Marker, NavigationControl } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";
import MapboxLanguage from "@mapbox/mapbox-gl-language";

import GeocoderControl from "./GeocoderControl";
import MarkerPin from "./MarkerPin";

export default function MapboxComponent(prop) {
  const [markerPin, setMarkerPin] = useState({
    latitude: prop?.initialGeographicPoint?.latitude,
    longitude: prop?.initialGeographicPoint?.longitude,
  });
  function onDragEnd(e) {
    const viewState = e.viewState;
    if (viewState) {
      setMarkerPin({
        latitude: viewState.latitude,
        longitude: viewState.longitude,
      });
    }
  }

  function onLoadMap(e) {
    const map = e?.target;
    if (map) {
      // 言語設定
      const language = new MapboxLanguage({
        defaultLanguage: "ja",
      });
      map.addControl(language);
      language._initialStyleUpdate();
    }
  }

  return (
    <Map
      initialViewState={{
        latitude: markerPin.latitude,
        longitude: markerPin.longitude,
        zoom: 13,
      }}
      mapStyle="mapbox://styles/mapbox/streets-v11"
      mapboxAccessToken={process.env.MAPBOX_TOKEN}
      style={{ height: "350px", width: "100%" }}
      attributionControl={false}
      onDragEnd={onDragEnd}
      onLoad={onLoadMap}
    >
      <Marker
        latitude={markerPin.latitude}
        longitude={markerPin.longitude}
      >
        <MarkerPin />
      </Marker>
      <NavigationControl />
      <GeocoderControl setMarkerPin={setMarkerPin} position="top-left" />
    </Map>
  );
}

以下がピンの表示部分です。上記で紹介したExample: Draggable Markerのピンの表示部分のコードと、ほぼ同様です。

MarkerPin.js
export default function MarkerPin() {
  const ICON = `M20.2,15.7L20.2,15.7c1.1-1.6,1.8-3.6,1.8-5.7c0-5.6-4.5-10-10-10S2,4.5,2,10c0,2,0.6,3.9,1.6,5.4c0,0.1,0.1,0.2,0.2,0.3
  c0,0,0.1,0.1,0.1,0.2c0.2,0.3,0.4,0.6,0.7,0.9c2.6,3.1,7.4,7.6,7.4,7.6s4.8-4.5,7.4-7.5c0.2-0.3,0.5-0.6,0.7-0.9
  C20.1,15.8,20.2,15.8,20.2,15.7z`;

  const pinStyle = {
    fill: "#d00",
    stroke: "none",
  };
  return (
    <svg height={20} viewBox="0 0 24 24" style={pinStyle}>
      <path d={ICON} />
    </svg>
  );
}

最後に検索ボックスの表示部分です。上記で紹介したExample: Geocoderのほぼそのままですが、検索結果のMarkerについては親からpropで渡したものにセットしています。

GeocoderControl.js
import { useControl } from "react-map-gl";
import MapboxGeocoder from "@mapbox/mapbox-gl-geocoder";
import "@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css";

export default function GeocoderControl(prop) {
  const noop = () => {};

  const geocoder = useControl(
    () => {
      const ctrl = new MapboxGeocoder({
        ...prop,
        accessToken: process.env.MAPBOX_TOKEN,
      });
      ctrl.on("loading", noop);
      ctrl.on("results", noop);
      ctrl.on("result", (evt) => {
        const { result } = evt;
        const location =
          result &&
          (result.center ||
            (result.geometry?.type === "Point" && result.geometry.coordinates));
        if (location) {
          prop.setMarkerPin({ longitude: location[0], latitude: location[1] });
        }
      });
      ctrl.on("error", noop);
      return ctrl;
    },
    {
      position: prop.position,
    }
  );

  if (geocoder._map) {
    if (
      geocoder.getProximity() !== prop.proximity &&
      prop.proximity !== undefined
    ) {
      geocoder.setProximity(prop.proximity);
    }
    if (
      geocoder.getRenderFunction() !== prop.render &&
      prop.render !== undefined
    ) {
      geocoder.setRenderFunction(prop.render);
    }
    if (
      geocoder.getLanguage() !== prop.language &&
      prop.language !== undefined
    ) {
      geocoder.setLanguage(prop.language);
    }
    if (geocoder.getZoom() !== prop.zoom && prop.zoom !== undefined) {
      geocoder.setZoom(prop.zoom);
    }
    if (geocoder.getFlyTo() !== prop.flyTo && prop.flyTo !== undefined) {
      geocoder.setFlyTo(prop.zoom);
    }
    if (
      geocoder.getPlaceholder() !== prop.placeholder &&
      prop.placeholder !== undefined
    ) {
      geocoder.setPlaceholder(prop.zoom);
    }
    if (
      geocoder.getCountries() !== prop.countries &&
      prop.countries !== undefined
    ) {
      geocoder.setCountries(prop.zoom);
    }
    if (geocoder.getTypes() !== prop.types && prop.types !== undefined) {
      geocoder.setTypes(prop.zoom);
    }
    if (
      geocoder.getMinLength() !== prop.minLength &&
      prop.minLength !== undefined
    ) {
      geocoder.setMinLength(prop.zoom);
    }
    if (geocoder.getLimit() !== prop.limit && prop.limit !== undefined) {
      geocoder.setLimit(prop.zoom);
    }
    if (geocoder.getFilter() !== prop.filter && prop.filter !== undefined) {
      geocoder.setFilter(prop.zoom);
    }
    if (geocoder.getOrigin() !== prop.origin && prop.origin !== undefined) {
      geocoder.setOrigin(prop.zoom);
    }
    if (
      geocoder.getAutocomplete() !== prop.autocomplete &&
      prop.autocomplete !== undefined
    ) {
      geocoder.setAutocomplete(prop.zoom);
    }
    if (
      geocoder.getFuzzyMatch() !== prop.fuzzyMatch &&
      prop.fuzzyMatch !== undefined
    ) {
      geocoder.setFuzzyMatch(prop.zoom);
    }
    if (geocoder.getRouting() !== prop.routing && prop.routing !== undefined) {
      geocoder.setRouting(prop.zoom);
    }
    if (
      geocoder.getWorldview() !== prop.worldview &&
      prop.worldview !== undefined
    ) {
      geocoder.setWorldview(prop.zoom);
    }
  }
  return <></>;
}

Discussion