import { useEffect, useMemo, useRef, useState } from 'react' import { VECTOR_VIEWPORT_DEBOUNCE_MS, VECTOR_VIEWPORT_FEATURE_LIMIT, VECTOR_VIEWPORT_FEATURE_THRESHOLD, VECTOR_VIEWPORT_MIN_ZOOM, } from '../config/vectorDelivery' import { formatError } from '../lib/formatError' import { datasetsApi } from '../services/api/datasets' import type { DatasetCreateResponse, MapViewportState } from '../types' interface ViewportVectorLayerOptions { selectedProjectId: string | null selectedDataset: DatasetCreateResponse | null featureCount: number | null isVectorDatasetType: (datasetType: string) => boolean } export function useViewportVectorLayer({ selectedProjectId, selectedDataset, featureCount, isVectorDatasetType, }: ViewportVectorLayerOptions) { const [viewport, setViewport] = useState(null) const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [truncated, setTruncated] = useState(false) const [loadedFeatureCount, setLoadedFeatureCount] = useState(0) const requestSequence = useRef(0) const enabled = Boolean( selectedProjectId && selectedDataset && isVectorDatasetType(selectedDataset.dataset_type) && (featureCount ?? 0) > VECTOR_VIEWPORT_FEATURE_THRESHOLD, ) const zoom = viewport?.zoom ?? null const zoomRequired = enabled && (zoom === null || zoom < VECTOR_VIEWPORT_MIN_ZOOM) useEffect(() => { requestSequence.current += 1 setData(null) setLoading(false) setError(null) setTruncated(false) setLoadedFeatureCount(0) }, [enabled, selectedDataset?.id, selectedProjectId]) useEffect(() => { if (!enabled || !selectedProjectId || !selectedDataset || !viewport) { return } if (viewport.zoom < VECTOR_VIEWPORT_MIN_ZOOM) { requestSequence.current += 1 setData(null) setLoading(false) setError(null) setTruncated(false) setLoadedFeatureCount(0) return } const sequence = requestSequence.current + 1 requestSequence.current = sequence const timer = window.setTimeout(async () => { setLoading(true) setError(null) try { const response = await datasetsApi.selectVectorFeatures(selectedProjectId, selectedDataset.id, { bbox: viewport.bbox, limit: VECTOR_VIEWPORT_FEATURE_LIMIT, }) if (requestSequence.current !== sequence) { return } setData(response.geojson) setLoadedFeatureCount(response.feature_count) setTruncated(response.truncated) } catch (requestError) { if (requestSequence.current !== sequence) { return } setData(null) setLoadedFeatureCount(0) setTruncated(false) setError(formatError(requestError, 'Unable to load visible vector features')) } finally { if (requestSequence.current === sequence) { setLoading(false) } } }, VECTOR_VIEWPORT_DEBOUNCE_MS) return () => { window.clearTimeout(timer) } }, [enabled, selectedDataset, selectedProjectId, viewport]) const statusMessage = useMemo(() => { if (!enabled) { return null } if (zoomRequired) { return `Zoom in to level ${VECTOR_VIEWPORT_MIN_ZOOM} to load buildings from PostGIS.` } if (loading) { return 'Loading visible features from PostGIS...' } if (error) { return error } if (truncated) { return `${loadedFeatureCount.toLocaleString()} visible features loaded; zoom in further because this view exceeds the ${VECTOR_VIEWPORT_FEATURE_LIMIT.toLocaleString()} feature limit.` } return `${loadedFeatureCount.toLocaleString()} visible of ${(featureCount ?? 0).toLocaleString()} total features loaded from PostGIS.` }, [enabled, error, featureCount, loadedFeatureCount, loading, truncated, zoomRequired]) return { enabled, data, loading, error, truncated, loadedFeatureCount, featureLimit: VECTOR_VIEWPORT_FEATURE_LIMIT, minZoom: VECTOR_VIEWPORT_MIN_ZOOM, zoom, zoomRequired, statusMessage, setViewport, } }