Merged in feat/SW-1616-clustering (pull request #1330)
feat(SW-1616): Added clustering on destination country/city pages * feat(SW-1616): Added clustering on destination country/city pages Approved-by: Fredrik Thorsson Approved-by: Matilda Landström
This commit is contained in:
53
hooks/maps/use-map-viewport.ts
Normal file
53
hooks/maps/use-map-viewport.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// Hook to use supercluster with @visgl/react-google-map.
|
||||
// Implemented according to https://github.com/visgl/react-google-maps/tree/main/examples/custom-marker-clustering
|
||||
|
||||
import { useMap } from "@vis.gl/react-google-maps"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import type { BBox } from "geojson"
|
||||
|
||||
type MapViewportOptions = {
|
||||
padding?: number
|
||||
}
|
||||
|
||||
export function useMapViewport({ padding = 0 }: MapViewportOptions = {}) {
|
||||
const map = useMap()
|
||||
const [bbox, setBbox] = useState<BBox>([-180, -90, 180, 90])
|
||||
const [zoom, setZoom] = useState(0)
|
||||
|
||||
// observe the map to get current bounds
|
||||
useEffect(() => {
|
||||
if (!map) return
|
||||
|
||||
const listener = map.addListener("bounds_changed", () => {
|
||||
const bounds = map.getBounds()
|
||||
const zoom = map.getZoom()
|
||||
const projection = map.getProjection()
|
||||
|
||||
if (!bounds || !zoom || !projection) return
|
||||
|
||||
const sw = bounds.getSouthWest()
|
||||
const ne = bounds.getNorthEast()
|
||||
|
||||
const paddingDegrees = degreesPerPixel(zoom) * padding
|
||||
|
||||
const n = Math.min(90, ne.lat() + paddingDegrees)
|
||||
const s = Math.max(-90, sw.lat() - paddingDegrees)
|
||||
|
||||
const w = sw.lng() - paddingDegrees
|
||||
const e = ne.lng() + paddingDegrees
|
||||
|
||||
setBbox([w, s, e, n])
|
||||
setZoom(zoom)
|
||||
})
|
||||
|
||||
return () => listener.remove()
|
||||
}, [map, padding])
|
||||
|
||||
return { bbox, zoom }
|
||||
}
|
||||
|
||||
function degreesPerPixel(zoomLevel: number) {
|
||||
// 360° divided by the number of pixels at the zoom-level
|
||||
return 360 / (Math.pow(2, zoomLevel) * 256)
|
||||
}
|
||||
42
hooks/maps/use-supercluster.ts
Normal file
42
hooks/maps/use-supercluster.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useMemo, useReducer } from "react"
|
||||
import Supercluster, {type ClusterProperties } from "supercluster"
|
||||
|
||||
import { useMapViewport } from "./use-map-viewport"
|
||||
|
||||
import type { FeatureCollection, GeoJsonProperties, Point } from "geojson"
|
||||
|
||||
export function useSupercluster<T extends GeoJsonProperties>(
|
||||
geojson: FeatureCollection<Point, T>,
|
||||
superclusterOptions: Supercluster.Options<T, ClusterProperties>
|
||||
) {
|
||||
// create the clusterer and keep it
|
||||
const clusterer = useMemo(() => {
|
||||
return new Supercluster(superclusterOptions)
|
||||
}, [superclusterOptions])
|
||||
|
||||
// version-number for the data loaded into the clusterer
|
||||
// (this is needed to trigger updating the clusters when data was changed)
|
||||
const [version, dataWasUpdated] = useReducer((x: number) => x + 1, 0)
|
||||
|
||||
// when data changes, load it into the clusterer
|
||||
useEffect(() => {
|
||||
clusterer.load(geojson.features)
|
||||
dataWasUpdated()
|
||||
}, [clusterer, geojson])
|
||||
|
||||
// get bounding-box and zoomlevel from the map
|
||||
const { bbox, zoom } = useMapViewport({ padding: 100 })
|
||||
|
||||
// retrieve the clusters within the current viewport
|
||||
const clusters = useMemo(() => {
|
||||
// don't try to read clusters before data was loaded into the clusterer (version===0),
|
||||
// otherwise getClusters will crash
|
||||
if (!clusterer || version === 0) return []
|
||||
|
||||
return clusterer.getClusters(bbox, zoom)
|
||||
}, [version, clusterer, bbox, zoom])
|
||||
|
||||
return {
|
||||
clusters,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user