TL;DR
- firestore でサービスを開発しており、ある場所の地理的な近傍を検索したい。
- 近傍検索において GeoHash は「十分に効率的に」処理できる手法である。
- select の選択肢として使用する場合のカスタムフックを作成した。
前提
クライアント
- react-admin を使用する。
- DataProvider として react-admin-firebase を使用する。
データ
-
firestore を使用する。
-
レコードには、以下が入っていることとする
フィールド名 型 内容 geo [number, number] [緯度, 経度] geohash string GeoHash -
サンプル データ登録例
import { collection, addDoc } from "firebase/firestore";
import * as geofire from "geofire-common";
const cities = [
['秋葉原', 35.698715, 139.774021],
['上野', 35.714171, 139.775220],
];
for(const city of cities) {
const [name, lat, lng] = city;
await addDoc(collection(db, "cities"), {
name,
geohash: geofire.geohashForLocation([lat, lng]),
geo: [lat, lng],
});
}
カスタムフック
import * as geofire from "geofire-common";
import { useEffect, useState } from "react";
import firebase from "firebase/compat/app";
import { useDataProvider } from "react-admin";
type CollectionReference = firebase.firestore.CollectionReference;
export const useNearBy = (
resource: string,
center: [number, number],
radius: number // Km
) => {
const dp = useDataProvider();
const [choices, setChoices] = useState<unknown[]>([]);
useEffect(() => {
if (!resource) return;
if (!center) return;
if (!radius) return;
const bounds = geofire.geohashQueryBounds(center, radius * 1000);
const params = bounds.map(([s, e]) => ({
pagination: { page: 1, perPage: 1000 },
sort: { field: "geohash", order: "asc" },
filter: {
collectionQuery: (cr: CollectionReference) =>
cr.orderBy("geohash").startAt(s).endAt(e),
},
}));
const jobs = params.map((p) => dp.getList(resource, p));
Promise.all(jobs).then((all) => {
const result = all
.map(({ data }) => data)
.flat()
.filter(({ geo }) => geo)
.map((record) => ({
record,
distance: geofire.distanceBetween(center, record.geo),
}))
.filter(({ distance }) => distance < radius)
.sort((a, b) => a.distance - b.distance)
.map(({ record }) => record);
setChoices(result);
});
}, [dp, resource, center, radius]);
return choices;
};
使い方
const NearByInput = (props) => {
const center = [35.6812362, 139.7649361]; // 東京駅
const choices = useNearBy("cities", center, 10); // 10 Km 範囲
return <AutocompleteInput {...props} choices={choices} optionText="name" />;
};
おまけ
- geofire-common ライブラリがとても有用。今回使用した関数は以下。
関数名 入力 出力 geohashForLocation 緯度経度 GeoHash geohashQueryBounds 中心(緯度経度)
半径(Km)円を覆う適度な GeoHash (9 個以内) distanceBetween 地点A
地点B二地点間の距離(Km)