"use client"; import type { Map as MapLibreMap, MapLayerMouseEvent } from "maplibre-gl"; import type { RefObject } from "react"; import { useEffect } from "react"; import type { DetailFeature } from "../types"; import { getFeatureId, getWaterNetworkSourceId, toDetailFeature } from "../map/feature-adapter"; import { INTERACTIVE_HIT_LAYER_IDS } from "../map/layers"; import { SOURCE_LAYERS, type WaterNetworkSourceId } from "../map/sources"; import { SCADA_HIT_LAYER_ID } from "../map/scada"; export type MapFeatureInteractionState = { source: WaterNetworkSourceId; sourceLayer: string; id: string; hovered: boolean; selected: boolean; }; type FeatureStateMap = Pick; type FeatureReference = Pick; export function toMapFeatureReference( feature: Pick | null ): FeatureReference | null { if (!feature?.id) return null; return { source: feature.layer, sourceLayer: SOURCE_LAYERS[feature.layer], id: feature.id }; } export function setMapFeatureInteractionState( map: FeatureStateMap, feature: FeatureReference, state: Partial> ) { map.setFeatureState( { source: feature.source, sourceLayer: feature.sourceLayer, id: feature.id }, state ); } export function clearMapFeatureInteractionState( map: FeatureStateMap, feature: FeatureReference, key?: "hovered" | "selected" ) { map.removeFeatureState( { source: feature.source, sourceLayer: feature.sourceLayer, id: feature.id }, key ); } type UseMapInteractionsOptions = { mapRef: RefObject; mapReady: boolean; onSelectFeature: (feature: DetailFeature) => void; selectedFeature: DetailFeature | null; }; export function useMapInteractions({ mapRef, mapReady, onSelectFeature, selectedFeature }: UseMapInteractionsOptions) { useEffect(() => { const map = mapRef.current; if (!mapReady || !map) return; let hoveredFeature: FeatureReference | null = null; const clearHoveredFeature = () => { if (!hoveredFeature) return; clearMapFeatureInteractionState(map, hoveredFeature, "hovered"); hoveredFeature = null; }; const handleMouseMove = (event: MapLayerMouseEvent) => { const feature = event.features?.[0]; const source = feature && getWaterNetworkSourceId(feature); const id = feature && getFeatureId(feature); if (!source || !id) return; const next = { source, sourceLayer: SOURCE_LAYERS[source], id }; if (selectedFeature?.id === id && selectedFeature.layer === source) { clearHoveredFeature(); return; } if (hoveredFeature?.id === id && hoveredFeature.source === source) return; clearHoveredFeature(); setMapFeatureInteractionState(map, next, { hovered: true }); hoveredFeature = next; map.getCanvas().style.cursor = "pointer"; }; const handleMouseLeave = () => { clearHoveredFeature(); map.getCanvas().style.cursor = ""; }; const handleClick = (event: MapLayerMouseEvent) => { const feature = event.features?.[0]; if (feature) onSelectFeature(toDetailFeature(feature)); }; const hitLayerIds = [SCADA_HIT_LAYER_ID, ...INTERACTIVE_HIT_LAYER_IDS]; map.on("mousemove", hitLayerIds, handleMouseMove); map.on("mouseleave", hitLayerIds, handleMouseLeave); map.on("click", hitLayerIds, handleClick); return () => { clearHoveredFeature(); map.off("mousemove", hitLayerIds, handleMouseMove); map.off("mouseleave", hitLayerIds, handleMouseLeave); map.off("click", hitLayerIds, handleClick); }; }, [mapRef, mapReady, onSelectFeature, selectedFeature]); useEffect(() => { const map = mapRef.current; if (!mapReady || !map) return; const next = toMapFeatureReference(selectedFeature); if (next) setMapFeatureInteractionState(map, next, { selected: true, hovered: false }); return () => { if (next) clearMapFeatureInteractionState(map, next, "selected"); }; }, [mapReady, mapRef, selectedFeature]); }