feat(map): refine SCADA feature experience

This commit is contained in:
2026-07-14 10:48:24 +08:00
parent 0b7e827024
commit 1c85d938a6
35 changed files with 1020 additions and 842 deletions
@@ -10,23 +10,48 @@ import {
toDetailFeature
} from "../map/feature-adapter";
import { INTERACTIVE_HIT_LAYER_IDS } from "../map/layers";
import type { WaterNetworkSourceId } from "../map/sources";
import { SOURCE_LAYERS, type WaterNetworkSourceId } from "../map/sources";
import { SCADA_HIT_LAYER_ID } from "../map/scada";
const HOVER_LAYER_IDS: Record<WaterNetworkSourceId, readonly string[]> = {
conduits: ["pipes-hover-outline", "pipes-hover"],
junctions: ["junctions-hover"],
orifices: ["orifices-hover-outline", "orifices-hover"],
pumps: ["pumps-hover-outline", "pumps-hover"],
outfalls: ["outfalls-hover"]
export type MapFeatureInteractionState = {
source: WaterNetworkSourceId;
sourceLayer: string;
id: string;
hovered: boolean;
selected: boolean;
};
const SELECTED_LAYER_IDS: Record<WaterNetworkSourceId, readonly string[]> = {
conduits: ["pipes-selected-halo", "pipes-selected-outline", "pipes-selected"],
junctions: ["junctions-selected-halo", "junctions-selected"],
orifices: ["orifices-selected-halo", "orifices-selected-outline", "orifices-selected"],
pumps: ["pumps-selected-halo", "pumps-selected-outline", "pumps-selected"],
outfalls: ["outfalls-selected-halo", "outfalls-selected"]
};
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 { 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(
{ 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<MapLibreMap | null>;
@@ -43,88 +68,65 @@ export function useMapInteractions({
}: UseMapInteractionsOptions) {
useEffect(() => {
const map = mapRef.current;
if (!mapReady || !map) {
return;
}
if (!mapReady || !map) return;
const activeMap = map;
let hoveredFeature: { id: string; sourceId: WaterNetworkSourceId } | null = null;
let hoveredFeature: FeatureReference | null = null;
function clearHoveredFeature() {
if (!hoveredFeature) {
return;
}
HOVER_LAYER_IDS[hoveredFeature.sourceId].forEach((layerId) => {
activeMap.setFilter(layerId, ["==", ["get", "id"], ""]);
});
const clearHoveredFeature = () => {
if (!hoveredFeature) return;
clearMapFeatureInteractionState(map, hoveredFeature, "hovered");
hoveredFeature = null;
}
};
function handleMouseMove(event: MapLayerMouseEvent) {
const handleMouseMove = (event: MapLayerMouseEvent) => {
const feature = event.features?.[0];
if (!feature) {
return;
}
const source = feature && getWaterNetworkSourceId(feature);
const id = feature && getFeatureId(feature);
if (!source || !id) return;
activeMap.getCanvas().style.cursor = "pointer";
const id = getFeatureId(feature);
const sourceId = getWaterNetworkSourceId(feature);
if (!id || !sourceId) {
const next = { source, sourceLayer: SOURCE_LAYERS[source], id };
if (selectedFeature?.id === id && selectedFeature.layer === source) {
clearHoveredFeature();
return;
}
if (hoveredFeature?.id === id && hoveredFeature.sourceId === sourceId) {
return;
}
if (hoveredFeature?.id === id && hoveredFeature.source === source) return;
clearHoveredFeature();
HOVER_LAYER_IDS[sourceId].forEach((layerId) => {
activeMap.setFilter(layerId, ["==", ["get", "id"], id]);
});
hoveredFeature = { id, sourceId };
}
setMapFeatureInteractionState(map, next, { hovered: true });
hoveredFeature = next;
map.getCanvas().style.cursor = "pointer";
};
function handleMouseLeave() {
activeMap.getCanvas().style.cursor = "";
const handleMouseLeave = () => {
clearHoveredFeature();
}
map.getCanvas().style.cursor = "";
};
function handleClick(event: MapLayerMouseEvent) {
const handleClick = (event: MapLayerMouseEvent) => {
const feature = event.features?.[0];
if (feature) {
onSelectFeature(toDetailFeature(feature));
}
}
if (feature) onSelectFeature(toDetailFeature(feature));
};
activeMap.on("mousemove", INTERACTIVE_HIT_LAYER_IDS, handleMouseMove);
activeMap.on("mouseleave", INTERACTIVE_HIT_LAYER_IDS, handleMouseLeave);
activeMap.on("click", INTERACTIVE_HIT_LAYER_IDS, handleClick);
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 () => {
activeMap.off("mousemove", INTERACTIVE_HIT_LAYER_IDS, handleMouseMove);
activeMap.off("mouseleave", INTERACTIVE_HIT_LAYER_IDS, handleMouseLeave);
activeMap.off("click", INTERACTIVE_HIT_LAYER_IDS, handleClick);
clearHoveredFeature();
map.off("mousemove", hitLayerIds, handleMouseMove);
map.off("mouseleave", hitLayerIds, handleMouseLeave);
map.off("click", hitLayerIds, handleClick);
};
}, [mapRef, mapReady, onSelectFeature]);
}, [mapRef, mapReady, onSelectFeature, selectedFeature]);
useEffect(() => {
const map = mapRef.current;
if (!mapReady || !map) {
return;
}
Object.values(SELECTED_LAYER_IDS).flat().forEach((layerId) => {
map.setFilter(layerId, ["==", ["get", "id"], ""]);
});
if (!selectedFeature?.id) {
return;
}
SELECTED_LAYER_IDS[selectedFeature.layer].forEach((layerId) => {
map.setFilter(layerId, ["==", ["get", "id"], selectedFeature.id]);
});
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]);
}