"use client"; import "maplibre-gl/dist/maplibre-gl.css"; import maplibregl, { type Map as MapLibreMap, type MapSourceDataEvent } from "maplibre-gl"; import { useEffect, useRef, useState, type RefObject } from "react"; import type { DetailFeature } from "../types"; import { SIMULATION_SOURCE_IDS, simulationAnnotationLayers, simulationSources } from "../map/annotation-layers"; import { fitNetworkBounds } from "../map/camera"; import { waterNetworkBusinessLayers, waterNetworkHitLayers, waterNetworkInteractionLayers } from "../map/layers"; import { MAP_MAX_ZOOM } from "../map/map-layer-visuals"; import { setSimulationLayersVisibility } from "../map/simulation-layers"; import { registerScadaImages, scadaFallbackLayers, scadaLayers, type ScadaImageRegistrationResult } from "../map/scada"; import { createBaseStyle, createWaterNetworkSources, WATER_NETWORK_GLOBAL_VIEW } from "../map/sources"; import { useMapInteractions } from "./use-map-interactions"; import { useSimulationLayerVisibility } from "./use-simulation-layer-visibility"; declare global { interface Window { __waterNetworkMap?: MapLibreMap; } } type UseWorkbenchMapOptions = { containerRef: RefObject; leftPanelOpen: boolean; rightPanelOpen: boolean; impactVisible: boolean; onSelectFeature: (feature: DetailFeature) => void; selectedFeature: DetailFeature | null; }; type UseWorkbenchMapResult = { mapRef: RefObject; mapReady: boolean; mapError: string | null; sourceStatuses: WorkbenchSourceStatus[]; 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 底图", "geoserver-mvt": "GeoServer MVT", "scada-icons": "SCADA 图标", "annotation-source": "业务标注源" }; const SOURCE_GROUP_BY_ID: Record = { "mapbox-light": "mapbox-base", "mapbox-satellite": "mapbox-base", conduits: "geoserver-mvt", junctions: "geoserver-mvt", orifices: "geoserver-mvt", outfalls: "geoserver-mvt", pumps: "geoserver-mvt", scada: "geoserver-mvt", [SIMULATION_SOURCE_IDS.impactArea]: "annotation-source", [SIMULATION_SOURCE_IDS.annotations]: "annotation-source" }; export function useWorkbenchMap({ containerRef, leftPanelOpen, rightPanelOpen, impactVisible, onSelectFeature, selectedFeature }: UseWorkbenchMapOptions): UseWorkbenchMapResult { const mapRef = useRef(null); const impactVisibleRef = useRef(impactVisible); const layoutOpenRef = useRef({ leftPanelOpen, rightPanelOpen }); const [mapReady, setMapReady] = useState(false); const [mapError, setMapError] = useState(null); const [sourceStatuses, setSourceStatuses] = useState>({}); useEffect(() => { layoutOpenRef.current = { leftPanelOpen, rightPanelOpen }; }, [leftPanelOpen, rightPanelOpen]); useEffect(() => { impactVisibleRef.current = impactVisible; }, [impactVisible]); useEffect(() => { if (!containerRef.current || mapRef.current) { return; } const mapboxToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN; const map = new maplibregl.Map({ container: containerRef.current, style: createBaseStyle(mapboxToken), pitch: 0, bearing: 0, maxZoom: MAP_MAX_ZOOM, preserveDrawingBuffer: true, attributionControl: false }); window.__waterNetworkMap = map; map.on("load", async () => { map.resize(); const sources = createWaterNetworkSources(); map.addSource("conduits", sources.conduits); map.addSource("junctions", sources.junctions); map.addSource("orifices", sources.orifices); map.addSource("outfalls", sources.outfalls); map.addSource("pumps", sources.pumps); map.addSource("scada", sources.scada); map.addSource(SIMULATION_SOURCE_IDS.impactArea, simulationSources.impactArea); map.addSource(SIMULATION_SOURCE_IDS.annotations, simulationSources.annotations); simulationAnnotationLayers.filter((layer) => layer.id === "simulation-impact-fill").forEach((layer) => map.addLayer(layer)); waterNetworkBusinessLayers.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)); let scadaRegistration: ScadaImageRegistrationResult = { status: "unavailable", failedImageIds: [] }; try { scadaRegistration = await registerScadaImages(map); } finally { try { const presentationLayers = scadaRegistration.status === "unavailable" ? scadaFallbackLayers : scadaLayers; presentationLayers.slice(0, -1).forEach((layer) => map.addLayer(layer)); simulationAnnotationLayers.filter((layer) => layer.type === "symbol").forEach((layer) => map.addLayer(layer)); waterNetworkHitLayers.forEach((layer) => map.addLayer(layer)); map.addLayer(presentationLayers[presentationLayers.length - 1]); } finally { setSimulationLayersVisibility(map, impactVisibleRef.current); setMapReady(true); updateSourceStatus(setSourceStatuses, "annotation-source", "online", "业务标注源已就绪。"); fitNetworkBounds(map, WATER_NETWORK_GLOBAL_VIEW); } } if (scadaRegistration.status !== "ready") { updateSourceStatus( setSourceStatuses, "scada-icons", "degraded", scadaRegistration.status === "degraded" ? "部分 SCADA 分类图标加载失败,已使用通用图标。" : "SCADA 图标加载失败,已使用通用点位显示。" ); } }); const resizeMap = () => map.resize(); window.addEventListener("resize", resizeMap); window.setTimeout(resizeMap, 0); window.setTimeout(resizeMap, 300); window.setTimeout(resizeMap, 1000); 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 () => { window.removeEventListener("resize", resizeMap); map.remove(); mapRef.current = null; delete window.__waterNetworkMap; }; }, [containerRef]); useMapInteractions({ mapRef, mapReady, onSelectFeature, selectedFeature }); useSimulationLayerVisibility({ mapRef, mapReady, visible: impactVisible }); function fitToNetworkBounds() { const map = mapRef.current; if (map) { fitNetworkBounds(map, WATER_NETWORK_GLOBAL_VIEW); } } return { mapRef, mapReady, mapError, sourceStatuses: Object.values(sourceStatuses), fitNetworkBounds: fitToNetworkBounds }; } 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 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"; } return null; } function getSourceStatusMessage(sourceGroupId: string, status: WorkbenchSourceStatusValue) { if (sourceGroupId === "mapbox-base") { return status === "online" ? "底图瓦片已恢复正常。" : "底图瓦片请求中断,业务图层仍可继续使用。"; } if (sourceGroupId === "geoserver-mvt") { return status === "online" ? "排水管网矢量瓦片已加载。" : "排水管网瓦片请求中断,当前视图可能不完整。"; } if (sourceGroupId === "annotation-source") { return status === "online" ? "业务标注和影响范围已加载。" : "业务标注源加载中断,模拟标注可能暂不可见。"; } 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}`; } return errorMessage; }