From 8aff43697f846e387a1168551f382ac3ed53d261 Mon Sep 17 00:00:00 2001 From: Samitha Widanage Date: Tue, 11 Aug 2026 23:35:55 +0800 Subject: [PATCH] chore: remove unused clustering hooks from src `useSuperclusterWorker` and `useMapViewport` were added to `src/hooks` alongside the worker clustering example in #891, but the example imports its own copies under `examples/worker-marker-clustering/src/hooks`, so the ones in `src` were never used. They are not exported from `src/index.ts`, not imported anywhere inside `src`, and have no tests, so they never reached `dist`. `./src` is part of `files`, so the sources did ship in the tarball, but the `exports` map has no entry that would let anything import them. `supercluster` is not a dependency of the library either. Removing them is not a breaking change. Refs #1049 --- src/hooks/use-map-viewport.ts | 92 ------ src/hooks/use-supercluster-worker.ts | 403 --------------------------- 2 files changed, 495 deletions(-) delete mode 100644 src/hooks/use-map-viewport.ts delete mode 100644 src/hooks/use-supercluster-worker.ts diff --git a/src/hooks/use-map-viewport.ts b/src/hooks/use-map-viewport.ts deleted file mode 100644 index 6685e1fe..00000000 --- a/src/hooks/use-map-viewport.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * useMapViewport - Hook to track map viewport bounds and zoom level - * - * Returns the current bounding box and zoom level of the map, updating - * whenever the map becomes idle after panning or zooming. - * - * @example - * ```tsx - * const { bbox, zoom } = useMapViewport({ padding: 100 }); - * const { clusters } = useSuperclusterWorker(geojson, options, { bbox, zoom }, workerUrl); - * ``` - */ - -import {useEffect, useState} from 'react'; -import {useMap} from './use-map'; - -/** Bounding box [west, south, east, north] */ -export type ViewportBBox = [number, number, number, number]; - -export interface MapViewportOptions { - /** - * Padding in pixels to extend the bounding box beyond the visible viewport. - * Useful for pre-loading markers that are just outside the view. - * @default 0 - */ - padding?: number; -} - -export interface MapViewport { - /** Bounding box [west, south, east, north] */ - bbox: ViewportBBox; - /** Current zoom level */ - zoom: number; -} - -/** - * Calculates degrees per pixel at a given zoom level. - * Used to convert pixel padding to geographic distance. - */ -function degreesPerPixel(zoomLevel: number): number { - // 360° divided by the number of pixels at the zoom-level - return 360 / (Math.pow(2, zoomLevel) * 256); -} - -/** - * Hook to track map viewport (bounding box and zoom) - * - * @param options - Configuration options - * @returns Current viewport with bbox and zoom - */ -export function useMapViewport(options: MapViewportOptions = {}): MapViewport { - const {padding = 0} = options; - const map = useMap(); - const [bbox, setBbox] = useState([-180, -90, 180, 90]); - const [zoom, setZoom] = useState(0); - - useEffect(() => { - if (!map) return; - - const updateViewport = () => { - const bounds = map.getBounds(); - const currentZoom = map.getZoom(); - const projection = map.getProjection(); - - if (!bounds || currentZoom === undefined || !projection) return; - - const sw = bounds.getSouthWest(); - const ne = bounds.getNorthEast(); - - const paddingDegrees = degreesPerPixel(currentZoom) * 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(currentZoom); - }; - - // Update on map idle (after pan/zoom completes) - const listener = map.addListener('idle', updateViewport); - - // Initial update - updateViewport(); - - return () => listener.remove(); - }, [map, padding]); - - return {bbox, zoom}; -} diff --git a/src/hooks/use-supercluster-worker.ts b/src/hooks/use-supercluster-worker.ts deleted file mode 100644 index 9b856586..00000000 --- a/src/hooks/use-supercluster-worker.ts +++ /dev/null @@ -1,403 +0,0 @@ -/** - * useSuperclusterWorker - Web Worker-based clustering hook - * - * This hook provides an interface for running Supercluster in a Web Worker, - * preventing main thread blocking when clustering large datasets (10k+ markers). - * - * @remarks - * Usage requires: - * 1. Install supercluster: `npm install supercluster @types/supercluster` - * 2. Create a worker file in your app (see worker-marker-clustering example) - * 3. Pass the worker URL to this hook - * - * @see {@link https://github.com/visgl/react-google-maps/tree/main/examples/worker-marker-clustering} - * - * @example - * ```tsx - * const workerUrl = new URL('./clustering.worker.ts', import.meta.url); - * const { bbox, zoom } = useMapViewport({ padding: 100 }); - * const { clusters, isLoading } = useSuperclusterWorker( - * geojson, - * { radius: 80, maxZoom: 16 }, - * { bbox, zoom }, - * workerUrl - * ); - * ``` - */ - -import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; - -// ============================================================================ -// GeoJSON Types (inline to avoid external dependency) -// ============================================================================ - -/** GeoJSON Bounding Box [west, south, east, north] */ -export type BBox = [number, number, number, number]; - -/** GeoJSON Point geometry */ -export interface PointGeometry { - type: 'Point'; - coordinates: [number, number]; -} - -/** GeoJSON Feature */ -export interface GeoFeature

> { - type: 'Feature'; - id?: string | number; - geometry: PointGeometry; - properties: P; -} - -/** GeoJSON FeatureCollection */ -export interface GeoFeatureCollection

> { - type: 'FeatureCollection'; - features: GeoFeature

[]; -} - -// ============================================================================ -// Supercluster Types (inline to avoid external dependency) -// ============================================================================ - -/** Supercluster options */ -export interface SuperclusterOptions { - /** Min zoom level to generate clusters */ - minZoom?: number; - /** Max zoom level to cluster points */ - maxZoom?: number; - /** Minimum points to form a cluster */ - minPoints?: number; - /** Cluster radius in pixels */ - radius?: number; - /** Tile extent (radius is calculated relative to it) */ - extent?: number; - /** Whether to generate numeric ids for clusters */ - generateId?: boolean; -} - -/** Properties added to cluster features by Supercluster */ -export interface ClusterProperties { - cluster: true; - cluster_id: number; - point_count: number; - point_count_abbreviated: string | number; -} - -/** A cluster or point feature returned by Supercluster */ -export type ClusterFeature

> = - GeoFeature

| GeoFeature; - -// ============================================================================ -// Worker Message Types -// ============================================================================ - -type WorkerMessage = - | {type: 'init'; options: SuperclusterOptions} - | {type: 'load'; features: GeoFeature[]} - | {type: 'getClusters'; bbox: BBox; zoom: number; requestId: number} - | {type: 'getLeaves'; clusterId: number; requestId: number; limit?: number} - | {type: 'getChildren'; clusterId: number; requestId: number} - | {type: 'getClusterExpansionZoom'; clusterId: number; requestId: number}; - -type WorkerResponse = - | {type: 'ready'} - | {type: 'loaded'; count: number} - | {type: 'clusters'; clusters: ClusterFeature[]; requestId: number} - | {type: 'leaves'; leaves: GeoFeature[]; requestId: number} - | {type: 'children'; children: ClusterFeature[]; requestId: number} - | {type: 'expansionZoom'; zoom: number; requestId: number} - | {type: 'error'; message: string; requestId?: number}; - -// ============================================================================ -// Hook Types -// ============================================================================ - -export interface SuperclusterViewport { - /** Bounding box [west, south, east, north] */ - bbox: BBox; - /** Zoom level (will be floored to integer) */ - zoom: number; -} - -export interface UseSuperclusterWorkerResult

> { - /** Current clusters/markers for the viewport */ - clusters: ClusterFeature

[]; - /** True while loading data or calculating clusters */ - isLoading: boolean; - /** Error message if worker failed */ - error: string | null; - /** Get all leaf features in a cluster */ - getLeaves: (clusterId: number, limit?: number) => Promise[]>; - /** Get immediate children of a cluster */ - getChildren: (clusterId: number) => Promise[]>; - /** Get zoom level at which a cluster expands */ - getClusterExpansionZoom: (clusterId: number) => Promise; -} - -// ============================================================================ -// Hook Implementation -// ============================================================================ - -// Check if Web Workers are supported -const supportsWorker = typeof Worker !== 'undefined'; - -/** - * Hook for running Supercluster in a Web Worker - * - * @param geojson - GeoJSON FeatureCollection with Point features - * @param options - Supercluster configuration options - * @param viewport - Current map viewport (bbox and zoom) - * @param workerUrl - URL to the clustering worker file - * @returns Clustering results and utility functions - */ -export function useSuperclusterWorker

>( - geojson: GeoFeatureCollection

| null, - options: SuperclusterOptions, - viewport: SuperclusterViewport, - workerUrl: URL | string -): UseSuperclusterWorkerResult

{ - // Initialize state with environment check - const initialError = useMemo( - () => - supportsWorker ? null : 'Web Workers not supported in this environment', - [] - ); - - const [clusters, setClusters] = useState[]>([]); - const [isLoading, setIsLoading] = useState(supportsWorker); - const [error, setError] = useState(initialError); - - const workerRef = useRef(null); - const requestIdRef = useRef(0); - const pendingRequestsRef = useRef< - Map< - number, - {resolve: (value: unknown) => void; reject: (error: Error) => void} - > - >(new Map()); - const isReadyRef = useRef(false); - const dataLoadedRef = useRef(false); - const optionsRef = useRef(options); - const loadingDataRef = useRef(false); - - // Update options ref in effect to avoid accessing during render - useEffect(() => { - optionsRef.current = options; - }, [options]); - - // Initialize worker - useEffect(() => { - if (!supportsWorker) return; - - let worker: Worker; - try { - worker = new Worker(workerUrl, {type: 'module'}); - } catch (e) { - // Worker creation can fail synchronously, we need to report this error - // eslint-disable-next-line react-hooks/set-state-in-effect - setError( - `Failed to create worker: ${e instanceof Error ? e.message : 'Unknown error'}` - ); - setIsLoading(false); - return; - } - - workerRef.current = worker; - - // Capture ref values for cleanup - const pendingRequests = pendingRequestsRef.current; - - worker.onmessage = (event: MessageEvent) => { - const response = event.data; - - switch (response.type) { - case 'ready': - isReadyRef.current = true; - break; - - case 'loaded': - dataLoadedRef.current = true; - loadingDataRef.current = false; - break; - - case 'clusters': - setClusters(response.clusters as ClusterFeature

[]); - setIsLoading(false); - break; - - case 'leaves': - case 'children': - case 'expansionZoom': { - const pending = pendingRequests.get(response.requestId); - if (pending) { - pendingRequests.delete(response.requestId); - if (response.type === 'leaves') { - pending.resolve(response.leaves); - } else if (response.type === 'children') { - pending.resolve(response.children); - } else { - pending.resolve(response.zoom); - } - } - break; - } - - case 'error': - setError(response.message); - setIsLoading(false); - if (response.requestId !== undefined) { - const pending = pendingRequests.get(response.requestId); - if (pending) { - pendingRequests.delete(response.requestId); - pending.reject(new Error(response.message)); - } - } - break; - } - }; - - worker.onerror = err => { - setError(err.message || 'Worker error'); - setIsLoading(false); - }; - - // Initialize with options - const initMessage: WorkerMessage = { - type: 'init', - options: optionsRef.current - }; - worker.postMessage(initMessage); - - return () => { - worker.terminate(); - workerRef.current = null; - isReadyRef.current = false; - dataLoadedRef.current = false; - pendingRequests.clear(); - }; - }, [workerUrl]); - - // Load data when geojson changes - useEffect(() => { - const worker = workerRef.current; - if (!worker || !geojson) return; - - // Mark as loading via ref to avoid effect issues - loadingDataRef.current = true; - dataLoadedRef.current = false; - - const loadMessage: WorkerMessage = { - type: 'load', - features: geojson.features as GeoFeature[] - }; - worker.postMessage(loadMessage); - }, [geojson]); - - // Get clusters when viewport or data changes - useEffect(() => { - const worker = workerRef.current; - if (!worker || !geojson) return; - - // Wait a tick to ensure data is loaded - const timeoutId = setTimeout(() => { - const requestId = ++requestIdRef.current; - - const message: WorkerMessage = { - type: 'getClusters', - bbox: viewport.bbox, - zoom: Math.floor(viewport.zoom), - requestId - }; - worker.postMessage(message); - }, 0); - - return () => clearTimeout(timeoutId); - }, [viewport, geojson]); - - const getLeaves = useCallback( - (clusterId: number, limit?: number): Promise[]> => { - return new Promise((resolve, reject) => { - const worker = workerRef.current; - if (!worker) { - reject(new Error('Worker not initialized')); - return; - } - - const requestId = ++requestIdRef.current; - pendingRequestsRef.current.set(requestId, { - resolve: resolve as (value: unknown) => void, - reject - }); - - const message: WorkerMessage = { - type: 'getLeaves', - clusterId, - requestId, - limit - }; - worker.postMessage(message); - }); - }, - [] - ); - - const getChildren = useCallback( - (clusterId: number): Promise[]> => { - return new Promise((resolve, reject) => { - const worker = workerRef.current; - if (!worker) { - reject(new Error('Worker not initialized')); - return; - } - - const requestId = ++requestIdRef.current; - pendingRequestsRef.current.set(requestId, { - resolve: resolve as (value: unknown) => void, - reject - }); - - const message: WorkerMessage = { - type: 'getChildren', - clusterId, - requestId - }; - worker.postMessage(message); - }); - }, - [] - ); - - const getClusterExpansionZoom = useCallback( - (clusterId: number): Promise => { - return new Promise((resolve, reject) => { - const worker = workerRef.current; - if (!worker) { - reject(new Error('Worker not initialized')); - return; - } - - const requestId = ++requestIdRef.current; - pendingRequestsRef.current.set(requestId, { - resolve: resolve as (value: unknown) => void, - reject - }); - - const message: WorkerMessage = { - type: 'getClusterExpansionZoom', - clusterId, - requestId - }; - worker.postMessage(message); - }); - }, - [] - ); - - return { - clusters, - isLoading, - error, - getLeaves, - getChildren, - getClusterExpansionZoom - }; -}