google-map-reactとuse-superclusterを使ったGoogleマップのクラスタリング実装
React でGoogleマップのクラスタリングを実装しようとしたら、日本語記事が全然なくて苦労したので記事を書きました。英語読める人は素直にこちらの記事を読んで下さい。
デモ
導入
クラスタリングの前段となるGoogleマップ自体の描画は他の記事に譲りますが、ライブラリとしては google-map-react or @react-google-maps/api がメジャーなようです。
本記事ではgoogle-map-react を使用しています。
適当なマーカーを表示させてスタート。
import GoogleMapReact from "google-map-react";
import styled from "@emotion/styled";
const MapCanvasElement = styled.div`
top: 0;
left: 0;
width: 100%;
height: 100%;
`;
const MarkerElement = styled.div`
background: orange;
color: white;
padding: 15px 10px;
display: inline-flex;
border-radius: 100%;
transform: translate(-50%, -50%);
`;
export const MapComponent = () => {
const Marker = ({ text }) => <MarkerElement>{text}</MarkerElement>;
return (
<MapCanvasElement>
<GoogleMapReact
bootstrapURLKeys={{ key: "キー入れる" }}
center={{ lat: 35, lng: 135 }}
zoom={5}
>
<Marker lat={35.8} lng={135} text={"test1"} />
<Marker lat={35.5} lng={140} text={"test2"} />
<Marker lat={34} lng={134} text={"test3"} />
</GoogleMapReact>
</MapCanvasElement>
);
};
use-superclusterとGeoJSON
今回はクラスタリング機能の実装にuse-supercluster を使用しました。これは、クラスタリング用ライブラリ supercluster のラッパーライブラリで、フック機能により React で使いやすくしてくれています。
use-superclusterは、マーカーの情報をGeoJSONのFeatureオブジェクトの配列で渡す必要があります。GeoJSONはざっくり言うと地理情報を表現するためのjsonフォーマット形式で、こちらの記事が詳しいです。
// 1マーカー表示に最低限必要な項目
{
"type": "Feature",
"properties": {
"cluster": false
},
"geometry": {
"type": "Point",
"coordinates": [135, 35.8] //経度、緯度の順。注意。
}
}
マーカーのテキストやidといった追加情報を持たせたいときは、propertiesの中に定義します。
test1~3のマーカー情報を持った配列を定義します。
const MarkerArray = [
{
type: "Feature",
properties: {
cluster: false,
id: 1,
label: "test1",
},
geometry: {
type: "Point",
coordinates: [135, 35.8],
},
},
{
type: "Feature",
properties: {
cluster: false,
id: 2,
label: "test2",
},
geometry: {
type: "Point",
coordinates: [140, 35.5],
},
},
{
type: "Feature",
properties: {
cluster: false,
id: 3,
label: "test3",
},
geometry: {
type: "Point",
coordinates: [134, 34],
},
},
];
use-superclusterの導入
useSuperclusterを定義します。
export const MapComponent = () => {
const [bounds, setBounds] = useState<any | null>(null);
const [zoom, setZoom] = useState<number>(5);
const MarkerArray = (略)
const { clusters, supercluster } = useSupercluster({
points: MarkerArray,
bounds: bounds,
zoom: zoom,
options: { radius: 50, maxZoom: 20 },
});
(中略)
<GoogleMapReact
bootstrapURLKeys={{ key: "キー入れる" }}
center={{ lat: 35, lng: 135 }}
zoom={zoom}
onChange={({ zoom, bounds }) => {
setZoom(zoom);
setBounds([
bounds.nw.lng,
bounds.se.lat,
bounds.se.lng,
bounds.nw.lat,
]);
}}
>
pointsにマーカーの情報、boundsにマップの境界値、zoomに拡大率を渡します。
GoogleMapReactコンポーネントのonChangeのコールバックとしてboundsとzoomを取得できるので、useStateに保存します。
optionsに使える項目は superclusterのOptions に記載があります。
useSuperclusterの返り値のうち、clustersは、クラスターまたはマーカーのGeoJSON Featureオブジェクトの配列です。
例えばtest1とtest3マーカーがまとまってクラスターになり、test2がまとまってない(マーカー)時、clustersの中身は以下のようになります。
[
{
"type": "Feature",
"properties": {
"cluster": true,
"cluster_id": 8, //idは自動で振られる
"point_count": 2, //まとまったマーカーの数
"point_count_abbreviated": 2
},
"geometry": {
"type": "Point",
"coordinates": [34.9, 134.5]
}
},
{
"type": "Feature",
"properties": {
"cluster": false,
"label": "test2"
},
"geometry": {
"type": "Point",
"coordinates": [35.5, 140]
}
}
]
クラスター、マーカの表示
clustersにクラスター(マーカー)の情報が入ってくるので、クラスター用のスタイルを作成して、後はよしなに展開してあげます。
const ClusterElement = styled.div`
background: green;
min-width: 50px;
min-height: 50px;
display: inline-flex;
border-radius: 100%;
transform: translate(-50%, -50%);
`;
const Marker = ({ text }) => <MarkerElement>{text}</MarkerElement>;
const Cluster = () => <ClusterElement />;
return (
<MapCanvasElement>
<GoogleMapReact
(略)
>
{clusters.map((cluster) => {
const [longitude, latitude] = cluster.geometry.coordinates;
const { cluster: isCluster } = cluster.properties;
if (isCluster) {
return (
<Cluster
key={`cluster-${cluster.id}`}
lat={latitude}
lng={longitude}
/>
);
}
return (
<Marker
key={cluster.properties.id}
lat={latitude}
lng={longitude}
text={cluster.properties.label}
/>
);
})}
</GoogleMapReact>
</MapCanvasElement>
こうなっていれば成功です。
Optional:クラスターをクリックしてズームする
このままでもクラスタリングとしては十分ですが、クラスターをクリックするとズームインする機能を入れてみます。
useSuperclusterの返り値であるsuperclusterは superclusterのメソッド にアクセスできるので、getClusterExpansionZoomを呼び出します。
getClusterExpansionZoomで計算された拡大率をsetZoomに渡してマップを拡大します。
if (isCluster) {
return (
<Cluster
key={`cluster-${cluster.id}`}
lat={latitude}
lng={longitude}
onClick={() => {
const expansionZoom = Math.min(
supercluster.getClusterExpansionZoom(cluster.id),
20
);
setZoom(expansionZoom);
}}
/>
);
}
ズーム機能も含めた全体のソースはこちらです。
import GoogleMapReact from "google-map-react";
import styled from "@emotion/styled";
import useSupercluster from "use-supercluster";
import React, { useState } from "react";
const MapCanvasElement = styled.div`
top: 0;
left: 0;
width: 100%;
height: 100%;
`;
const MarkerElement = styled.div`
background: orange;
color: white;
padding: 15px 10px;
display: inline-flex;
border-radius: 100%;
transform: translate(-50%, -50%);
`;
const ClusterElement = styled.div`
background: green;
min-width: 50px;
min-height: 50px;
display: inline-flex;
border-radius: 100%;
transform: translate(-50%, -50%);
`;
export const MapComponent = () => {
const [bounds, setBounds] = useState<any | null>(null);
const [zoom, setZoom] = useState<number>(5);
const MarkerArray = [
{
type: "Feature",
properties: {
cluster: false,
id: 1,
label: "test1",
},
geometry: {
type: "Point",
coordinates: [135, 35.8],
},
},
{
type: "Feature",
properties: {
cluster: false,
id: 2,
label: "test2",
},
geometry: {
type: "Point",
coordinates: [140, 35.5],
},
},
{
type: "Feature",
properties: {
cluster: false,
id: 3,
label: "test3",
},
geometry: {
type: "Point",
coordinates: [134, 34],
},
},
];
const { clusters, supercluster } = useSupercluster({
points: MarkerArray,
bounds: bounds,
zoom: zoom,
options: { radius: 100, maxZoom: 20 },
});
const Marker = ({ text }) => <MarkerElement>{text}</MarkerElement>;
const Cluster = () => <ClusterElement />;
return (
<MapCanvasElement>
<GoogleMapReact
bootstrapURLKeys={{ key: "キー入れる" }}
center={{ lat: 35, lng: 135 }}
zoom={zoom}
yesIWantToUseGoogleMapApiInternals
onChange={({ zoom, bounds }) => {
setZoom(zoom);
setBounds([
bounds.nw.lng,
bounds.se.lat,
bounds.se.lng,
bounds.nw.lat,
]);
}}
>
{clusters.map((cluster) => {
const [longitude, latitude] = cluster.geometry.coordinates;
const { cluster: isCluster } = cluster.properties;
if (isCluster) {
return (
<Cluster
key={`cluster-${cluster.id}`}
lat={latitude}
lng={longitude}
onClick={() => {
const expansionZoom = Math.min(
supercluster.getClusterExpansionZoom(cluster.id),
20
);
setZoom(expansionZoom);
}}
/>
);
}
return (
<Marker
key={cluster.properties.id}
lat={latitude}
lng={longitude}
text={cluster.properties.label}
/>
);
})}
</GoogleMapReact>
</MapCanvasElement>
);
};
おわりに
冒頭のデモは、今回解説したクラスタリング機能を使った実際のサービス動画です。
どこまで行けるマップ(仮)
出発地の駅と所要時間(n分以内)を指定して、行ける範囲を検索できます。
デモ目的歓迎なので、ぜひ見に来て下さい!
この記事が気に入ったらサポートをしてみませんか?