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"; 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 feature.layer === "scada" ? { source: feature.layer, id: feature.id } : { source: feature.layer, sourceLayer: SOURCE_LAYERS[feature.layer], id: feature.id }; } export function setMapFeatureInteractionState( map: FeatureStateMap, feature: FeatureReference, state: Partial> ) { map.setFeatureState(toFeatureStateTarget(feature), state); } export function clearMapFeatureInteractionState( map: FeatureStateMap, feature: FeatureReference, key?: "hovered" | "selected" ) { map.removeFeatureState(toFeatureStateTarget(feature), key); } type UseMapInteractionsOptions = { availableSourceIds: readonly WaterNetworkSourceId[]; mapRef: RefObject; mapReady: boolean; onSelectFeature: (feature: DetailFeature) => void; selectedFeature: DetailFeature | null; }; export function useMapInteractions({ availableSourceIds, 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 === "scada" ? { source, id } : { 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 = INTERACTIVE_HIT_LAYER_IDS.filter((layerId) => map.getLayer(layerId)); 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); }; }, [availableSourceIds, mapRef, mapReady, onSelectFeature, selectedFeature]); useEffect(() => { const map = mapRef.current; if (!mapReady || !map) return; const next = toMapFeatureReference(selectedFeature); if (next && map.getSource(next.source)) { setMapFeatureInteractionState(map, next, { selected: true, hovered: false }); } return () => { if (next && map.getSource(next.source)) { clearMapFeatureInteractionState(map, next, "selected"); } }; }, [availableSourceIds, mapReady, mapRef, selectedFeature]); } function toFeatureStateTarget(feature: FeatureReference) { return feature.sourceLayer ? { source: feature.source, sourceLayer: feature.sourceLayer, id: feature.id } : { source: feature.source, id: feature.id }; }