🌉
【React】Mapboxを使用して検索機能付きの地図を表示してみる
概要
Mapboxは、GoogleMapと同様の地図を表示するサービスです。王者Googleマップにライバル登場!「Mapbox」はどこまで使えるか?の記事に、概要や値段等紹介されていますが、GoogleMapと比べて無料枠も多いし安く使えるようなサービスになっています。
今回はReactでこのMapboxを使用し、検索機能付きの地図を表示してみます。
実装する内容
以下のイメージの内容のものを表示します。
実装時に考慮する内容としては、主に以下の3点です。
- 地図の検索ボックスが表示されるようにする。
- 検索結果の地点にピンを表示する。
- 地名が日本語で表示されるようにする。
前準備や参考にしたものなど
- MapBoxでアカウントを作成し、APIキーを取得します。
- Reactで以下のライブラリをインストールします。
- 地図検索はExample: Geocoderのコードを参考にしています。
- ピンの表示はExample: Draggable Markerを参考にしています。
実装サンプル
まずは地図本体の表示と、言語切り替えの実装部分です。
緯度経度の初期値については、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