140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
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<MapLibreMap, "setFeatureState" | "removeFeatureState">;
|
|
type FeatureReference = Pick<MapFeatureInteractionState, "source" | "sourceLayer" | "id">;
|
|
|
|
export function toMapFeatureReference(
|
|
feature: Pick<DetailFeature, "id" | "layer"> | 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<Pick<MapFeatureInteractionState, "hovered" | "selected">>
|
|
) {
|
|
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<MapLibreMap | null>;
|
|
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 };
|
|
}
|