import "maplibre-gl/dist/maplibre-gl.css"; import type { FeatureCollection, Geometry, Point } from "geojson"; import * as maplibregl from "maplibre-gl"; import type { GeoJSONSource, Map as MapLibreMap, MapSourceDataEvent, StyleSpecification } from "maplibre-gl"; import { useCallback, useEffect, useRef, useState, type RefObject } from "react"; import type { WorkbenchAccess } from "../api/request-context"; import { env } from "@/shared/config/env"; import { fetchScadaSnapshot, type ScadaDevice } from "../api/scada-client"; import type { DetailFeature } from "../types"; import { SIMULATION_SOURCE_IDS, simulationAnnotationLayers, simulationSources } from "../map/annotation-layers"; import { fitNetworkBounds, type NetworkViewParams } 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"; import { AVAILABLE_WATER_NETWORK_SOURCE_IDS, GEOSERVER_WATER_NETWORK_SOURCE_IDS, createBaseStyle, createWaterNetworkSources, WATER_NETWORK_GLOBAL_VIEW, type GeoServerWaterNetworkSourceId, type WaterNetworkSourceId } from "../map/sources"; import { SCADA_SOURCE_ID, registerScadaImages, scadaBusinessLayers, scadaFallbackBusinessLayers, scadaHitLayers, scadaInteractionLayers } from "../map/scada"; import { registerSupplyAssetImages } from "../map/supply-icons"; import { installCityScene } from "../map/city-scene-control"; import { useMapInteractions } from "./use-map-interactions"; import { useSimulationLayerVisibility } from "./use-simulation-layer-visibility"; declare global { interface Window { __waterNetworkMap?: MapLibreMap; } } type UseWorkbenchMapOptions = { featureInteractionsEnabled?: boolean; containerRef: RefObject; impactVisible: boolean; layerVisibility: Record; onClearSelection: () => void; onSelectFeature: (feature: DetailFeature) => void; selectedFeature: DetailFeature | null; simulationOverlay: FeatureCollection | null; allowDemoSimulation: boolean; networkView?: NetworkViewParams | null; getAccessToken?: WorkbenchAccess; geoserverWorkspace?: string; }; type UseWorkbenchMapResult = { mapRef: RefObject; mapReady: boolean; mapError: string | null; sourceStatuses: WorkbenchSourceStatus[]; availableSourceIds: WaterNetworkSourceId[]; sourceFeatureCounts: Partial>; getScadaFeatures: ( deviceIds?: string[], signal?: AbortSignal ) => Promise>; scadaObservedAt: string | null; refreshGeoServerLayers: () => boolean; fitNetworkBounds: () => void; }; export type WorkbenchSourceStatusValue = "loading" | "online" | "degraded" | "offline"; export type WorkbenchSourceStatus = { id: string; sourceName: string; status: WorkbenchSourceStatusValue; message: string; }; const SOURCE_STATUS_LABELS: Record = { "mapbox-base": "Mapbox 底图", "tianditu-base": "天地图底图", "geoserver-mvt": "GeoServer MVT", "geoserver-catalog": "GeoServer 图层目录", "scada-api": "SCADA API", "scada-icons": "SCADA 图标", "annotation-source": "业务标注源" }; const SOURCE_GROUP_BY_ID: Record = { "mapbox-light": "mapbox-base", "mapbox-satellite": "mapbox-base", "tianditu-vector": "tianditu-base", "tianditu-vector-annotation": "tianditu-base", "tianditu-image": "tianditu-base", "tianditu-image-annotation": "tianditu-base", ...Object.fromEntries( GEOSERVER_WATER_NETWORK_SOURCE_IDS.map((sourceId) => [sourceId, "geoserver-mvt"]) ), [SIMULATION_SOURCE_IDS.impactArea]: "annotation-source", [SIMULATION_SOURCE_IDS.annotations]: "annotation-source", [SIMULATION_SOURCE_IDS.backendAnalysis]: "annotation-source" }; const GEOSERVER_LAYER_PROBE_TIMEOUT_MS = 8_000; const SCADA_REFRESH_INTERVAL_MS = 15_000; const EMPTY_SIMULATION_COLLECTION: FeatureCollection = { type: "FeatureCollection", features: [] }; export function useWorkbenchMap({ featureInteractionsEnabled = true, containerRef, impactVisible, layerVisibility, onClearSelection, onSelectFeature, selectedFeature, simulationOverlay, allowDemoSimulation, networkView, geoserverWorkspace, getAccessToken }: UseWorkbenchMapOptions): UseWorkbenchMapResult { const mapRef = useRef(null); const impactVisibleRef = useRef(impactVisible); const simulationOverlayRef = useRef(simulationOverlay); const allowDemoSimulationRef = useRef(allowDemoSimulation); const networkViewRef = useRef(networkView ?? WATER_NETWORK_GLOBAL_VIEW); const fittedNetworkViewRef = useRef(null); simulationOverlayRef.current = simulationOverlay; allowDemoSimulationRef.current = allowDemoSimulation; networkViewRef.current = networkView ?? WATER_NETWORK_GLOBAL_VIEW; const [mapReady, setMapReady] = useState(false); const [mapError, setMapError] = useState(null); const [sourceStatuses, setSourceStatuses] = useState>({}); const [scadaObservedAt, setScadaObservedAt] = useState(null); const [availableGeoServerSourceIds, setAvailableGeoServerSourceIds] = useState< GeoServerWaterNetworkSourceId[] >(() => [...REQUIRED_GEOSERVER_SOURCE_IDS]); const [sourceFeatureCounts, setSourceFeatureCounts] = useState< Partial> >({}); const [geoServerLayerRefreshRevision, setGeoServerLayerRefreshRevision] = useState(0); const geoServerLayerProbeInFlightRef = useRef(false); const scadaCollectionRef = useRef>({ type: "FeatureCollection", features: [] }); const scadaLoadPromiseRef = useRef>>( Promise.resolve(scadaCollectionRef.current) ); const getScadaFeatures = useCallback(async (deviceIds?: string[], signal?: AbortSignal) => { const collection = await scadaLoadPromiseRef.current; if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError"); if (!deviceIds) return collection; const selectedIds = new Set(deviceIds); return { type: "FeatureCollection" as const, features: collection.features.filter((feature) => selectedIds.has(String(feature.id))) }; }, []); 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, geoserverWorkspace) .then((availability) => { if (lifecycleAbortController.signal.aborted) return; setAvailableGeoServerSourceIds((current) => current?.join("|") === availability.availableSourceIds.join("|") ? current : availability.availableSourceIds ); setSourceFeatureCounts((current) => areFeatureCountsEqual(current, availability.featureCounts) ? current : availability.featureCounts ); 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, geoserverWorkspace]); 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 tiandituToken = env.TJWATER_TIANDITU_TOKEN || undefined; const scadaAbortController = new AbortController(); let scadaRefreshTimeoutId: number | undefined; updateSourceStatus(setSourceStatuses, "scada-api", "loading", "正在通过API加载SCADA设备。"); const map = new maplibregl.Map({ container: containerRef.current, style: createBaseStyle(mapboxToken, tiandituToken), pitch: 0, bearing: 0, dragRotate: false, touchPitch: false, maxZoom: MAP_MAX_ZOOM, canvasContextAttributes: { antialias: true, preserveDrawingBuffer: true }, trackResize: false, attributionControl: false }); // MapLibre throttles automatic resizing to 50ms, which lags behind the dock edge. const resizeObserver = new ResizeObserver(() => { const container = map.getContainer(); const canvas = map.getCanvas(); if (!container.clientWidth || !container.clientHeight) return; if ( canvas.clientWidth === container.clientWidth && canvas.clientHeight === container.clientHeight ) return; map.resize(); // Resizing clears the drawing buffer; repaint in the same layout cycle. map.redraw(); }); resizeObserver.observe(map.getContainer()); window.__waterNetworkMap = map; let removeCityScene: (() => void) | undefined; map.on("load", async () => { map.resize(); (map.getSource(SCADA_SOURCE_ID) as GeoJSONSource | undefined)?.setData( scadaCollectionRef.current ); const sources = createWaterNetworkSources(REQUIRED_GEOSERVER_SOURCE_IDS, geoserverWorkspace); 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); map.addSource(SIMULATION_SOURCE_IDS.backendAnalysis, simulationSources.backendAnalysis); syncSimulationSourceData(map, simulationOverlayRef.current, allowDemoSimulationRef.current); await registerSupplyAssetImages(map, scadaAbortController.signal); if (scadaAbortController.signal.aborted) return; const scadaRegistration = await registerScadaImages(map, scadaAbortController.signal); if (scadaAbortController.signal.aborted) return; const activeScadaBusinessLayers = scadaRegistration.status === "unavailable" ? scadaFallbackBusinessLayers : scadaBusinessLayers; simulationAnnotationLayers .filter((layer) => layer.id === "simulation-impact-fill") .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)); filterWaterNetworkLayersBySourceIds( waterNetworkInteractionLayers, REQUIRED_GEOSERVER_SOURCE_IDS ).forEach((layer) => map.addLayer(layer)); scadaInteractionLayers.forEach((layer) => map.addLayer(layer)); filterWaterNetworkLayersBySourceIds( createValueLabelLayers(), REQUIRED_GEOSERVER_SOURCE_IDS ).forEach((layer) => map.addLayer(layer)); simulationAnnotationLayers .filter((layer) => layer.type === "symbol") .forEach((layer) => map.addLayer(layer)); filterWaterNetworkLayersBySourceIds( waterNetworkHitLayers, REQUIRED_GEOSERVER_SOURCE_IDS ).forEach((layer) => map.addLayer(layer)); scadaHitLayers.forEach((layer) => map.addLayer(layer)); removeCityScene = installCityScene(map); setSimulationLayersVisibility(map, impactVisibleRef.current, false); setMapReady(true); updateSourceStatus( setSourceStatuses, "scada-icons", scadaRegistration.status === "ready" ? "online" : scadaRegistration.status === "degraded" ? "degraded" : "offline", scadaRegistration.status === "ready" ? "SCADA 图标已就绪。" : scadaRegistration.status === "degraded" ? "部分 SCADA 图标加载失败,已使用默认图标。" : "SCADA 图标不可用,已使用圆点图层降级显示。" ); updateSourceStatus(setSourceStatuses, "annotation-source", "online", "业务标注源已就绪。"); fitNetworkBounds(map, networkViewRef.current); fittedNetworkViewRef.current = networkViewKey(networkViewRef.current); }); const refreshScada = async () => { const snapshotPromise = fetchScadaSnapshot(getAccessToken, scadaAbortController.signal); scadaLoadPromiseRef.current = snapshotPromise .then((snapshot) => snapshot.collection) .catch(() => scadaCollectionRef.current); try { const snapshot = await snapshotPromise; if (scadaAbortController.signal.aborted) return; scadaCollectionRef.current = snapshot.collection; setScadaObservedAt(snapshot.observedAt); (map.getSource(SCADA_SOURCE_ID) as GeoJSONSource | undefined)?.setData(snapshot.collection); updateSourceStatus( setSourceStatuses, "scada-api", snapshot.telemetryStatus === "ready" ? "online" : "degraded", getScadaStatusMessage(snapshot) ); } catch (error) { if (scadaAbortController.signal.aborted) return; setScadaObservedAt(null); updateSourceStatus( setSourceStatuses, "scada-api", scadaCollectionRef.current.features.length > 0 ? "degraded" : "offline", error instanceof Error ? error.message : "SCADA API加载失败。" ); } finally { if (!scadaAbortController.signal.aborted) { scadaRefreshTimeoutId = window.setTimeout(refreshScada, SCADA_REFRESH_INTERVAL_MS); } } }; void refreshScada(); map.on("sourcedata", (event) => { updateStatusFromSourceEvent(event, "online", setSourceStatuses); }); map.on("sourcedataabort", (event) => { updateStatusFromSourceEvent(event, "degraded", setSourceStatuses); }); map.on("error", (event) => { const message = event.error?.message ?? "地图渲染异常"; if (!message.includes("AbortError")) { const sourceGroupId = getSourceGroupFromErrorEvent(event); if (sourceGroupId) { updateSourceStatus( setSourceStatuses, sourceGroupId, "offline", getSourceErrorMessage(sourceGroupId, message) ); } else { setMapError(message); } } }); mapRef.current = map; return () => { resizeObserver.disconnect(); scadaAbortController.abort(); if (scadaRefreshTimeoutId !== undefined) { window.clearTimeout(scadaRefreshTimeoutId); } removeCityScene?.(); map.remove(); mapRef.current = null; delete window.__waterNetworkMap; }; }, [containerRef, getAccessToken, geoserverWorkspace]); useEffect(() => { const map = mapRef.current; if (!mapReady || !map) return; syncDataDependentGeoServerLayers(map, availableGeoServerSourceIds, layerVisibility, geoserverWorkspace); }, [availableGeoServerSourceIds, layerVisibility, mapReady, geoserverWorkspace]); useEffect(() => { const map = mapRef.current; if (!mapReady || !map || !networkView) return; const key = networkViewKey(networkView); if (fittedNetworkViewRef.current === key) return; fitNetworkBounds(map, networkView); fittedNetworkViewRef.current = key; }, [mapReady, networkView]); useMapInteractions({ enabled: featureInteractionsEnabled, availableSourceIds: availableGeoServerSourceIds, mapRef, mapReady, onSelectFeature, selectedFeature }); useSimulationLayerVisibility({ mapRef, mapReady, visible: impactVisible }); useEffect(() => { const map = mapRef.current; if (!map || !mapReady) return; syncSimulationSourceData(map, simulationOverlay, allowDemoSimulation); }, [allowDemoSimulation, mapReady, simulationOverlay]); function fitToNetworkBounds() { const map = mapRef.current; if (map) { fitNetworkBounds(map, networkViewRef.current); } } return { mapRef, mapReady, mapError, sourceStatuses: Object.values(sourceStatuses), availableSourceIds: AVAILABLE_WATER_NETWORK_SOURCE_IDS.filter( (sourceId) => sourceId === "scada" || availableGeoServerSourceIds.includes(sourceId as GeoServerWaterNetworkSourceId) ), sourceFeatureCounts, getScadaFeatures, scadaObservedAt, refreshGeoServerLayers, fitNetworkBounds: fitToNetworkBounds }; } function areFeatureCountsEqual( current: Partial>, next: Partial> ) { const currentEntries = Object.entries(current); const nextEntries = Object.entries(next); return ( currentEntries.length === nextEntries.length && nextEntries.every(([sourceId, count]) => current[sourceId as WaterNetworkSourceId] === count) ); } function networkViewKey(view: NetworkViewParams) { return view.bbox3857.join(","); } function getScadaStatusMessage(snapshot: Awaited>) { const deviceSummary = `已通过API加载 ${snapshot.collection.features.length} 个SCADA设备`; if (snapshot.telemetryStatus === "ready") { return `${deviceSummary},其中 ${snapshot.observedDeviceCount} 个带有最近15分钟监测值。`; } if (snapshot.telemetryStatus === "unavailable") { return `${deviceSummary};实时监测值暂不可用,保留设备元数据。`; } return `${deviceSummary};最近15分钟没有监测值,保留设备元数据。`; } function syncSimulationSourceData( map: MapLibreMap, backendOverlay: FeatureCollection | null, allowDemoSimulation: boolean ) { const useBackendOverlay = Boolean(backendOverlay?.features.length); (map.getSource(SIMULATION_SOURCE_IDS.backendAnalysis) as GeoJSONSource | undefined)?.setData( useBackendOverlay ? backendOverlay! : EMPTY_SIMULATION_COLLECTION ); (map.getSource(SIMULATION_SOURCE_IDS.impactArea) as GeoJSONSource | undefined)?.setData( useBackendOverlay || !allowDemoSimulation ? EMPTY_SIMULATION_COLLECTION : simulationSources.impactArea.data ); (map.getSource(SIMULATION_SOURCE_IDS.annotations) as GeoJSONSource | undefined)?.setData( useBackendOverlay || !allowDemoSimulation ? EMPTY_SIMULATION_COLLECTION : simulationSources.annotations.data ); } function syncDataDependentGeoServerLayers( map: MapLibreMap, availableSourceIds: readonly GeoServerWaterNetworkSourceId[], layerVisibility: Record, workspace?: string ) { const availableSourceIdSet = new Set(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, workspace); 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, createValueLabelLayers(), [ ...simulationAnnotationLayers.filter((layer) => layer.type === "symbol"), ...waterNetworkHitLayers, ...scadaHitLayers ].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, layerVisibility: Record, workspace?: string ) { 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, setSourceStatuses: ( updater: ( current: Record ) => Record ) => void ) { const sourceGroupId = SOURCE_GROUP_BY_ID[event.sourceId]; if (!sourceGroupId) { return; } if (status === "online" && !event.isSourceLoaded) { return; } updateSourceStatus( setSourceStatuses, sourceGroupId, status, getSourceStatusMessage(sourceGroupId, status) ); } function updateSourceStatus( setSourceStatuses: ( updater: ( current: Record ) => Record ) => void, sourceGroupId: string, status: WorkbenchSourceStatusValue, message: string ) { setSourceStatuses((current) => { const previous = current[sourceGroupId]; if (previous?.status === status && previous.message === message) { return current; } return { ...current, [sourceGroupId]: { id: sourceGroupId, sourceName: SOURCE_STATUS_LABELS[sourceGroupId] ?? sourceGroupId, status, message } }; }); } function clearSourceStatus( setSourceStatuses: ( updater: ( current: Record ) => Record ) => 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]; } const message = event.error?.message ?? ""; if (message.includes("geoserver.waternetwork.cn")) { return "geoserver-mvt"; } if (message.includes("api.mapbox.com") || message.includes("mapbox")) { return "mapbox-base"; } if (message.includes("tianditu.gov.cn") || message.includes("tianditu")) { return "tianditu-base"; } return null; } function getSourceStatusMessage(sourceGroupId: string, status: WorkbenchSourceStatusValue) { if (["mapbox-base", "tianditu-base"].includes(sourceGroupId)) { return status === "online" ? "底图瓦片已恢复正常。" : "底图瓦片请求中断,业务图层仍可继续使用。"; } if (sourceGroupId === "geoserver-mvt") { return status === "online" ? "供水管网矢量瓦片已加载。" : "供水管网瓦片请求中断,当前视图可能不完整。"; } if (sourceGroupId === "annotation-source") { return status === "online" ? "业务标注和影响范围已加载。" : "业务标注源加载中断,模拟标注可能暂不可见。"; } if (sourceGroupId === "scada-api") { return status === "online" ? "SCADA设备已通过API加载。" : "SCADA API请求中断,管网图层仍可继续使用。"; } return status === "online" ? "地图数据源已恢复正常。" : "地图数据源请求中断。"; } function getSourceErrorMessage(sourceGroupId: string, errorMessage: string) { if (sourceGroupId === "mapbox-base") { return `底图瓦片请求失败:${errorMessage}`; } if (sourceGroupId === "geoserver-mvt") { return `GeoServer 矢量瓦片请求失败:${errorMessage}`; } if (sourceGroupId === "annotation-source") { return `业务标注源请求失败:${errorMessage}`; } if (sourceGroupId === "scada-api") { return `SCADA API请求失败:${errorMessage}`; } return errorMessage; }