"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, getResponsiveWorkbenchPadding } from "../map/camera"; import { waterNetworkLayers } from "../map/layers"; import { setSimulationLayersVisibility } from "../map/simulation-layers"; 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; }; 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", "annotation-source": "业务标注源" }; const SOURCE_GROUP_BY_ID: Record = { "mapbox-light": "mapbox-base", "mapbox-satellite": "mapbox-base", pipes: "geoserver-mvt", junctions: "geoserver-mvt", [SIMULATION_SOURCE_IDS.impactArea]: "annotation-source", [SIMULATION_SOURCE_IDS.annotations]: "annotation-source" }; export function useWorkbenchMap({ containerRef, leftPanelOpen, rightPanelOpen, impactVisible, onSelectFeature }: UseWorkbenchMapOptions): UseWorkbenchMapResult { const mapRef = useRef(null); const impactVisibleRef = useRef(impactVisible); const layoutOpenRef = useRef({ leftPanelOpen, rightPanelOpen }); const appliedLayoutPaddingRef = useRef<{ leftPanelOpen: boolean; rightPanelOpen: boolean } | null>(null); 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, preserveDrawingBuffer: true, attributionControl: false }); window.__waterNetworkMap = map; map.on("load", () => { map.resize(); const sources = createWaterNetworkSources(); map.addSource("pipes", sources.pipes); map.addSource("junctions", sources.junctions); map.addSource(SIMULATION_SOURCE_IDS.impactArea, simulationSources.impactArea); map.addSource(SIMULATION_SOURCE_IDS.annotations, simulationSources.annotations); waterNetworkLayers.forEach((layer) => map.addLayer(layer)); simulationAnnotationLayers.forEach((layer) => map.addLayer(layer)); setSimulationLayersVisibility(map, impactVisibleRef.current); setMapReady(true); updateSourceStatus(setSourceStatuses, "annotation-source", "online", "业务标注源已就绪。"); fitNetworkBounds(map, WATER_NETWORK_GLOBAL_VIEW); }); 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 }); useEffect(() => { const map = mapRef.current; if (!mapReady || !map) { return; } const previousLayout = appliedLayoutPaddingRef.current; const currentLayout = { leftPanelOpen, rightPanelOpen }; appliedLayoutPaddingRef.current = currentLayout; if ( !previousLayout || (previousLayout.leftPanelOpen === currentLayout.leftPanelOpen && previousLayout.rightPanelOpen === currentLayout.rightPanelOpen) ) { return; } map.easeTo({ padding: getResponsiveWorkbenchPadding(map, leftPanelOpen, rightPanelOpen), duration: 350 }); }, [leftPanelOpen, rightPanelOpen, mapReady]); 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; }