fix: hide unavailable GeoServer layers
This commit is contained in:
@@ -47,6 +47,7 @@ export function clearMapFeatureInteractionState(
|
||||
}
|
||||
|
||||
type UseMapInteractionsOptions = {
|
||||
availableSourceIds: readonly WaterNetworkSourceId[];
|
||||
mapRef: RefObject<MapLibreMap | null>;
|
||||
mapReady: boolean;
|
||||
onSelectFeature: (feature: DetailFeature) => void;
|
||||
@@ -54,6 +55,7 @@ type UseMapInteractionsOptions = {
|
||||
};
|
||||
|
||||
export function useMapInteractions({
|
||||
availableSourceIds,
|
||||
mapRef,
|
||||
mapReady,
|
||||
onSelectFeature,
|
||||
@@ -102,7 +104,7 @@ export function useMapInteractions({
|
||||
if (feature) onSelectFeature(toDetailFeature(feature));
|
||||
};
|
||||
|
||||
const hitLayerIds = [...INTERACTIVE_HIT_LAYER_IDS];
|
||||
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);
|
||||
@@ -113,17 +115,21 @@ export function useMapInteractions({
|
||||
map.off("mouseleave", hitLayerIds, handleMouseLeave);
|
||||
map.off("click", hitLayerIds, handleClick);
|
||||
};
|
||||
}, [mapRef, mapReady, onSelectFeature, selectedFeature]);
|
||||
}, [availableSourceIds, 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 });
|
||||
if (next && map.getSource(next.source)) {
|
||||
setMapFeatureInteractionState(map, next, { selected: true, hovered: false });
|
||||
}
|
||||
return () => {
|
||||
if (next) clearMapFeatureInteractionState(map, next, "selected");
|
||||
if (next && map.getSource(next.source)) {
|
||||
clearMapFeatureInteractionState(map, next, "selected");
|
||||
}
|
||||
};
|
||||
}, [mapReady, mapRef, selectedFeature]);
|
||||
}, [availableSourceIds, mapReady, mapRef, selectedFeature]);
|
||||
}
|
||||
|
||||
function toFeatureStateTarget(feature: FeatureReference) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import type { ScadaDevice } from "../api/scada-client";
|
||||
import { useEffect, useRef, useSyncExternalStore, type RefObject } from "react";
|
||||
import { getResponsiveWorkbenchPadding } from "../map/camera";
|
||||
import type { WaterNetworkSourceId } from "../map/sources";
|
||||
import { WorkbenchMapController } from "../map/workbench-map-controller";
|
||||
|
||||
export function useWorkbenchMapController({
|
||||
@@ -12,7 +13,8 @@ export function useWorkbenchMapController({
|
||||
rightPanelOpen,
|
||||
conditionPanelExpanded = false,
|
||||
agentPanelWidth,
|
||||
getScadaFeatures
|
||||
getScadaFeatures,
|
||||
availableSourceIds
|
||||
}: {
|
||||
mapRef: RefObject<MapLibreMap | null>;
|
||||
mapReady: boolean;
|
||||
@@ -21,6 +23,7 @@ export function useWorkbenchMapController({
|
||||
conditionPanelExpanded?: boolean;
|
||||
agentPanelWidth?: number;
|
||||
getScadaFeatures?: (deviceIds?: string[], signal?: AbortSignal) => Promise<FeatureCollection<Point, ScadaDevice>>;
|
||||
availableSourceIds?: readonly WaterNetworkSourceId[];
|
||||
}) {
|
||||
const valuesRef = useRef({
|
||||
mapReady,
|
||||
@@ -63,6 +66,12 @@ export function useWorkbenchMapController({
|
||||
controller.getSnapshot,
|
||||
controller.getSnapshot
|
||||
);
|
||||
useEffect(() => {
|
||||
const target = state.target;
|
||||
if (target && availableSourceIds && !availableSourceIds.includes(target.sourceId)) {
|
||||
controller.clearHighlight();
|
||||
}
|
||||
}, [availableSourceIds, controller, state.target]);
|
||||
useEffect(() => () => controller.destroy(), [controller]);
|
||||
return { controller, state };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
import type { FeatureCollection, Point } from "geojson";
|
||||
import maplibregl, { type GeoJSONSource, type Map as MapLibreMap, type MapSourceDataEvent } from "maplibre-gl";
|
||||
import maplibregl, {
|
||||
type GeoJSONSource,
|
||||
type Map as MapLibreMap,
|
||||
type MapSourceDataEvent,
|
||||
type StyleSpecification
|
||||
} from "maplibre-gl";
|
||||
import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
||||
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
|
||||
import { env } from "@/shared/config/env";
|
||||
@@ -14,10 +19,16 @@ import {
|
||||
} from "../map/annotation-layers";
|
||||
import { fitNetworkBounds } from "../map/camera";
|
||||
import {
|
||||
filterWaterNetworkLayersBySourceIds,
|
||||
waterNetworkBusinessLayers,
|
||||
waterNetworkHitLayers,
|
||||
waterNetworkInteractionLayers
|
||||
} from "../map/layers";
|
||||
import {
|
||||
DATA_DEPENDENT_GEOSERVER_SOURCE_IDS,
|
||||
REQUIRED_GEOSERVER_SOURCE_IDS,
|
||||
resolveGeoServerLayerAvailability
|
||||
} from "../map/geoserver-layer-availability";
|
||||
import { MAP_MAX_ZOOM } from "../map/map-layer-visuals";
|
||||
import { createValueLabelLayers } from "../map/value-label";
|
||||
import { setSimulationLayersVisibility } from "../map/simulation-layers";
|
||||
@@ -26,7 +37,9 @@ import {
|
||||
GEOSERVER_WATER_NETWORK_SOURCE_IDS,
|
||||
createBaseStyle,
|
||||
createWaterNetworkSources,
|
||||
WATER_NETWORK_GLOBAL_VIEW
|
||||
WATER_NETWORK_GLOBAL_VIEW,
|
||||
type GeoServerWaterNetworkSourceId,
|
||||
type WaterNetworkSourceId
|
||||
} from "../map/sources";
|
||||
import {
|
||||
SCADA_SOURCE_ID,
|
||||
@@ -49,6 +62,8 @@ declare global {
|
||||
type UseWorkbenchMapOptions = {
|
||||
containerRef: RefObject<HTMLDivElement | null>;
|
||||
impactVisible: boolean;
|
||||
layerVisibility: Record<string, boolean>;
|
||||
onClearSelection: () => void;
|
||||
onSelectFeature: (feature: DetailFeature) => void;
|
||||
selectedFeature: DetailFeature | null;
|
||||
getAccessToken?: AccessTokenProvider;
|
||||
@@ -59,7 +74,9 @@ type UseWorkbenchMapResult = {
|
||||
mapReady: boolean;
|
||||
mapError: string | null;
|
||||
sourceStatuses: WorkbenchSourceStatus[];
|
||||
availableSourceIds: WaterNetworkSourceId[];
|
||||
getScadaFeatures: (deviceIds?: string[], signal?: AbortSignal) => Promise<FeatureCollection<Point, ScadaDevice>>;
|
||||
refreshGeoServerLayers: () => boolean;
|
||||
fitNetworkBounds: () => void;
|
||||
};
|
||||
|
||||
@@ -75,6 +92,7 @@ export type WorkbenchSourceStatus = {
|
||||
const SOURCE_STATUS_LABELS: Record<string, string> = {
|
||||
"mapbox-base": "Mapbox 底图",
|
||||
"geoserver-mvt": "GeoServer MVT",
|
||||
"geoserver-catalog": "GeoServer 图层目录",
|
||||
"scada-api": "SCADA API",
|
||||
"scada-icons": "SCADA 图标",
|
||||
"annotation-source": "业务标注源"
|
||||
@@ -90,9 +108,13 @@ const SOURCE_GROUP_BY_ID: Record<string, string> = {
|
||||
[SIMULATION_SOURCE_IDS.annotations]: "annotation-source"
|
||||
};
|
||||
|
||||
const GEOSERVER_LAYER_PROBE_TIMEOUT_MS = 8_000;
|
||||
|
||||
export function useWorkbenchMap({
|
||||
containerRef,
|
||||
impactVisible,
|
||||
layerVisibility,
|
||||
onClearSelection,
|
||||
onSelectFeature,
|
||||
selectedFeature,
|
||||
getAccessToken
|
||||
@@ -102,6 +124,11 @@ export function useWorkbenchMap({
|
||||
const [mapReady, setMapReady] = useState(false);
|
||||
const [mapError, setMapError] = useState<string | null>(null);
|
||||
const [sourceStatuses, setSourceStatuses] = useState<Record<string, WorkbenchSourceStatus>>({});
|
||||
const [availableGeoServerSourceIds, setAvailableGeoServerSourceIds] = useState<
|
||||
GeoServerWaterNetworkSourceId[]
|
||||
>(() => [...REQUIRED_GEOSERVER_SOURCE_IDS]);
|
||||
const [geoServerLayerRefreshRevision, setGeoServerLayerRefreshRevision] = useState(0);
|
||||
const geoServerLayerProbeInFlightRef = useRef(false);
|
||||
const scadaCollectionRef = useRef<FeatureCollection<Point, ScadaDevice>>({
|
||||
type: "FeatureCollection",
|
||||
features: []
|
||||
@@ -121,15 +148,80 @@ export function useWorkbenchMap({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshGeoServerLayers = useCallback(() => {
|
||||
if (geoServerLayerProbeInFlightRef.current) return false;
|
||||
geoServerLayerProbeInFlightRef.current = true;
|
||||
setGeoServerLayerRefreshRevision((revision) => revision + 1);
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const lifecycleAbortController = new AbortController();
|
||||
const requestAbortController = new AbortController();
|
||||
const probeTimeout = window.setTimeout(
|
||||
() => requestAbortController.abort(),
|
||||
GEOSERVER_LAYER_PROBE_TIMEOUT_MS
|
||||
);
|
||||
geoServerLayerProbeInFlightRef.current = true;
|
||||
|
||||
void resolveGeoServerLayerAvailability(requestAbortController.signal)
|
||||
.then((availability) => {
|
||||
if (lifecycleAbortController.signal.aborted) return;
|
||||
setAvailableGeoServerSourceIds((current) =>
|
||||
current?.join("|") === availability.availableSourceIds.join("|")
|
||||
? current
|
||||
: availability.availableSourceIds
|
||||
);
|
||||
|
||||
if (availability.failedSourceIds.length > 0) {
|
||||
updateSourceStatus(
|
||||
setSourceStatuses,
|
||||
"geoserver-catalog",
|
||||
"degraded",
|
||||
`无法确认 ${availability.failedSourceIds.join("、")} 图层是否有数据,已隐藏对应入口。`
|
||||
);
|
||||
} else {
|
||||
clearSourceStatus(setSourceStatuses, "geoserver-catalog");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
window.clearTimeout(probeTimeout);
|
||||
if (!lifecycleAbortController.signal.aborted) {
|
||||
geoServerLayerProbeInFlightRef.current = false;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
lifecycleAbortController.abort();
|
||||
requestAbortController.abort();
|
||||
geoServerLayerProbeInFlightRef.current = false;
|
||||
window.clearTimeout(probeTimeout);
|
||||
};
|
||||
}, [geoServerLayerRefreshRevision]);
|
||||
|
||||
useEffect(() => {
|
||||
impactVisibleRef.current = impactVisible;
|
||||
}, [impactVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedFeature &&
|
||||
DATA_DEPENDENT_GEOSERVER_SOURCE_IDS.includes(
|
||||
selectedFeature.layer as (typeof DATA_DEPENDENT_GEOSERVER_SOURCE_IDS)[number]
|
||||
) &&
|
||||
!availableGeoServerSourceIds.includes(selectedFeature.layer as GeoServerWaterNetworkSourceId)
|
||||
) {
|
||||
onClearSelection();
|
||||
}
|
||||
}, [availableGeoServerSourceIds, onClearSelection, selectedFeature]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || mapRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMapReady(false);
|
||||
setMapError(null);
|
||||
const mapboxToken = env.TJWATER_MAPBOX_ACCESS_TOKEN || undefined;
|
||||
const scadaAbortController = new AbortController();
|
||||
updateSourceStatus(setSourceStatuses, "scada-api", "loading", "正在通过API加载SCADA设备。");
|
||||
@@ -155,10 +247,11 @@ export function useWorkbenchMap({
|
||||
(map.getSource(SCADA_SOURCE_ID) as GeoJSONSource | undefined)?.setData(
|
||||
scadaCollectionRef.current
|
||||
);
|
||||
const sources = createWaterNetworkSources();
|
||||
GEOSERVER_WATER_NETWORK_SOURCE_IDS.forEach((sourceId) =>
|
||||
map.addSource(sourceId, sources[sourceId])
|
||||
);
|
||||
const sources = createWaterNetworkSources(REQUIRED_GEOSERVER_SOURCE_IDS);
|
||||
REQUIRED_GEOSERVER_SOURCE_IDS.forEach((sourceId) => {
|
||||
const source = sources[sourceId];
|
||||
if (source) map.addSource(sourceId, source);
|
||||
});
|
||||
map.addSource(SIMULATION_SOURCE_IDS.impactArea, simulationSources.impactArea);
|
||||
map.addSource(SIMULATION_SOURCE_IDS.annotations, simulationSources.annotations);
|
||||
await registerSupplyAssetImages(map);
|
||||
@@ -170,18 +263,27 @@ export function useWorkbenchMap({
|
||||
simulationAnnotationLayers
|
||||
.filter((layer) => layer.id === "simulation-impact-fill")
|
||||
.forEach((layer) => map.addLayer(layer));
|
||||
waterNetworkBusinessLayers.forEach((layer) => map.addLayer(layer));
|
||||
filterWaterNetworkLayersBySourceIds(
|
||||
waterNetworkBusinessLayers,
|
||||
REQUIRED_GEOSERVER_SOURCE_IDS
|
||||
).forEach((layer) => map.addLayer(layer));
|
||||
activeScadaBusinessLayers.forEach((layer) => map.addLayer(layer));
|
||||
simulationAnnotationLayers
|
||||
.filter((layer) => layer.type !== "symbol" && layer.id !== "simulation-impact-fill")
|
||||
.forEach((layer) => map.addLayer(layer));
|
||||
waterNetworkInteractionLayers.forEach((layer) => map.addLayer(layer));
|
||||
filterWaterNetworkLayersBySourceIds(
|
||||
waterNetworkInteractionLayers,
|
||||
REQUIRED_GEOSERVER_SOURCE_IDS
|
||||
).forEach((layer) => map.addLayer(layer));
|
||||
scadaInteractionLayers.forEach((layer) => map.addLayer(layer));
|
||||
createValueLabelLayers().forEach((layer) => map.addLayer(layer));
|
||||
simulationAnnotationLayers
|
||||
.filter((layer) => layer.type === "symbol")
|
||||
.forEach((layer) => map.addLayer(layer));
|
||||
waterNetworkHitLayers.forEach((layer) => map.addLayer(layer));
|
||||
filterWaterNetworkLayersBySourceIds(
|
||||
waterNetworkHitLayers,
|
||||
REQUIRED_GEOSERVER_SOURCE_IDS
|
||||
).forEach((layer) => map.addLayer(layer));
|
||||
scadaHitLayers.forEach((layer) => map.addLayer(layer));
|
||||
setSimulationLayersVisibility(map, impactVisibleRef.current);
|
||||
setMapReady(true);
|
||||
@@ -276,7 +378,14 @@ export function useWorkbenchMap({
|
||||
};
|
||||
}, [containerRef, getAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!mapReady || !map) return;
|
||||
syncDataDependentGeoServerLayers(map, availableGeoServerSourceIds, layerVisibility);
|
||||
}, [availableGeoServerSourceIds, layerVisibility, mapReady]);
|
||||
|
||||
useMapInteractions({
|
||||
availableSourceIds: availableGeoServerSourceIds,
|
||||
mapRef,
|
||||
mapReady,
|
||||
onSelectFeature,
|
||||
@@ -301,11 +410,93 @@ export function useWorkbenchMap({
|
||||
mapReady,
|
||||
mapError,
|
||||
sourceStatuses: Object.values(sourceStatuses),
|
||||
availableSourceIds: AVAILABLE_WATER_NETWORK_SOURCE_IDS.filter(
|
||||
(sourceId) =>
|
||||
sourceId === "scada" ||
|
||||
availableGeoServerSourceIds.includes(sourceId as GeoServerWaterNetworkSourceId)
|
||||
),
|
||||
getScadaFeatures,
|
||||
refreshGeoServerLayers,
|
||||
fitNetworkBounds: fitToNetworkBounds
|
||||
};
|
||||
}
|
||||
|
||||
function syncDataDependentGeoServerLayers(
|
||||
map: MapLibreMap,
|
||||
availableSourceIds: readonly GeoServerWaterNetworkSourceId[],
|
||||
layerVisibility: Record<string, boolean>
|
||||
) {
|
||||
const availableSourceIdSet = new Set<GeoServerWaterNetworkSourceId>(availableSourceIds);
|
||||
|
||||
DATA_DEPENDENT_GEOSERVER_SOURCE_IDS.forEach((sourceId) => {
|
||||
if (availableSourceIdSet.has(sourceId)) return;
|
||||
map.getStyle().layers
|
||||
.filter((layer) => "source" in layer && layer.source === sourceId)
|
||||
.reverse()
|
||||
.forEach((layer) => map.removeLayer(layer.id));
|
||||
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
||||
});
|
||||
|
||||
const requestedOptionalSourceIds = DATA_DEPENDENT_GEOSERVER_SOURCE_IDS.filter((sourceId) =>
|
||||
availableSourceIdSet.has(sourceId)
|
||||
);
|
||||
const sources = createWaterNetworkSources(requestedOptionalSourceIds);
|
||||
requestedOptionalSourceIds.forEach((sourceId) => {
|
||||
if (!map.getSource(sourceId) && sources[sourceId]) {
|
||||
map.addSource(sourceId, sources[sourceId]);
|
||||
}
|
||||
});
|
||||
|
||||
addMissingDataDependentLayers(
|
||||
map,
|
||||
waterNetworkBusinessLayers,
|
||||
[...scadaBusinessLayers, ...scadaFallbackBusinessLayers].map((layer) => layer.id),
|
||||
availableSourceIdSet,
|
||||
layerVisibility
|
||||
);
|
||||
addMissingDataDependentLayers(
|
||||
map,
|
||||
waterNetworkInteractionLayers,
|
||||
scadaInteractionLayers.map((layer) => layer.id),
|
||||
availableSourceIdSet,
|
||||
layerVisibility
|
||||
);
|
||||
addMissingDataDependentLayers(
|
||||
map,
|
||||
waterNetworkHitLayers,
|
||||
scadaHitLayers.map((layer) => layer.id),
|
||||
availableSourceIdSet,
|
||||
layerVisibility
|
||||
);
|
||||
}
|
||||
|
||||
function addMissingDataDependentLayers(
|
||||
map: MapLibreMap,
|
||||
layers: StyleSpecification["layers"],
|
||||
beforeLayerCandidates: readonly string[],
|
||||
availableSourceIdSet: ReadonlySet<GeoServerWaterNetworkSourceId>,
|
||||
layerVisibility: Record<string, boolean>
|
||||
) {
|
||||
const beforeLayerId = beforeLayerCandidates.find((layerId) => map.getLayer(layerId));
|
||||
layers.forEach((layer) => {
|
||||
if (!("source" in layer) || typeof layer.source !== "string") return;
|
||||
const sourceId = layer.source as GeoServerWaterNetworkSourceId;
|
||||
if (
|
||||
!DATA_DEPENDENT_GEOSERVER_SOURCE_IDS.includes(
|
||||
sourceId as (typeof DATA_DEPENDENT_GEOSERVER_SOURCE_IDS)[number]
|
||||
) ||
|
||||
!availableSourceIdSet.has(sourceId) ||
|
||||
map.getLayer(layer.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
map.addLayer(layer, beforeLayerId);
|
||||
if (layerVisibility[sourceId] === false) {
|
||||
map.setLayoutProperty(layer.id, "visibility", "none");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatusFromSourceEvent(
|
||||
event: MapSourceDataEvent,
|
||||
status: WorkbenchSourceStatusValue,
|
||||
@@ -360,6 +551,22 @@ function updateSourceStatus(
|
||||
});
|
||||
}
|
||||
|
||||
function clearSourceStatus(
|
||||
setSourceStatuses: (
|
||||
updater: (
|
||||
current: Record<string, WorkbenchSourceStatus>
|
||||
) => Record<string, WorkbenchSourceStatus>
|
||||
) => void,
|
||||
sourceGroupId: string
|
||||
) {
|
||||
setSourceStatuses((current) => {
|
||||
if (!current[sourceGroupId]) return current;
|
||||
const next = { ...current };
|
||||
delete next[sourceGroupId];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function getSourceGroupFromErrorEvent(event: { sourceId?: string; error?: { message?: string } }) {
|
||||
if (event.sourceId && SOURCE_GROUP_BY_ID[event.sourceId]) {
|
||||
return SOURCE_GROUP_BY_ID[event.sourceId];
|
||||
|
||||
Reference in New Issue
Block a user