"use client"; import { config } from "@/config/config"; import { useProject } from "@/contexts/ProjectContext"; import React, { createContext, useContext, useState, useEffect, useMemo, useCallback, useRef, } from "react"; import { Map as OlMap } from "ol"; import View from "ol/View.js"; import "ol/ol.css"; import MapTools from "./MapTools"; // 导入 DeckLayer import { DeckLayer } from "@utils/layers"; import { toLonLat } from "ol/proj"; import { along, bearing, lineString, length } from "@turf/turf"; import { Deck } from "@deck.gl/core"; import { TextLayer } from "@deck.gl/layers"; import { TripsLayer } from "@deck.gl/geo-layers"; import { CollisionFilterExtension } from "@deck.gl/extensions"; import { ContourLayer } from "deck.gl"; import { isLpsFlowProperty, toM3h } from "@utils/units"; import { usePathname } from "next/navigation"; import { cleanupTransientMapResources, disposeMapResources, markMapResourcePersistent, } from "./mapLifecycle"; import { createOperationalMapResources, createOperationalMapSources, } from "./operationalLayers"; import { getRoundedCurrentTimelineMinutes } from "./Controls/timelineTime"; import { useTimelineTimeConfig } from "./Controls/useTimelineTimeConfig"; import { TileFeatureIndex, clipLineStringPartsToExtent, coordinatesToLonLat, lineStringFromFlatCoordinates, type TileFeatureInstance, } from "./tileFeatureIndex"; interface MapComponentProps { children?: React.ReactNode; } const COMPARE_OPEN_START_MARK = "tjwater:compare-open-start"; const COMPARE_OPEN_READY_MARK = "tjwater:compare-open-ready"; const COMPARE_OPEN_MEASURE = "tjwater:compare-open"; const markCompareOpenStart = () => { performance.clearMarks(COMPARE_OPEN_START_MARK); performance.clearMarks(COMPARE_OPEN_READY_MARK); performance.clearMeasures(COMPARE_OPEN_MEASURE); performance.mark(COMPARE_OPEN_START_MARK); }; const markCompareOpenReady = () => { if (performance.getEntriesByName(COMPARE_OPEN_START_MARK).length === 0) { return; } performance.mark(COMPARE_OPEN_READY_MARK); performance.measure( COMPARE_OPEN_MEASURE, COMPARE_OPEN_START_MARK, COMPARE_OPEN_READY_MARK, ); }; interface DataContextType { currentTime?: number; // 当前时间 setCurrentTime?: React.Dispatch>; selectedDate?: Date; // 选择的日期 schemeName?: string; // 当前方案名称 setSchemeName?: React.Dispatch>; setSelectedDate?: React.Dispatch>; currentJunctionCalData?: any[]; // 当前计算结果 setCurrentJunctionCalData?: React.Dispatch>; currentPipeCalData?: any[]; // 当前计算结果 setCurrentPipeCalData?: React.Dispatch>; compareJunctionCalData?: any[]; setCompareJunctionCalData?: React.Dispatch>; comparePipeCalData?: any[]; setComparePipeCalData?: React.Dispatch>; isCompareMode?: boolean; setCompareMode?: React.Dispatch>; toggleCompareMode?: () => void; showJunctionText?: boolean; // 是否显示节点文本 showPipeText?: boolean; // 是否显示管道文本 showJunctionId?: boolean; // 是否显示节点ID showPipeId?: boolean; // 是否显示管道ID setShowJunctionTextLayer?: React.Dispatch>; setShowPipeTextLayer?: React.Dispatch>; setShowJunctionId?: React.Dispatch>; setShowPipeId?: React.Dispatch>; showContourLayer?: boolean; setShowContourLayer?: React.Dispatch>; isContourLayerAvailable?: boolean; showWaterflowLayer?: boolean; setShowWaterflowLayer?: React.Dispatch>; setContourLayerAvailable?: React.Dispatch>; isWaterflowLayerAvailable?: boolean; setWaterflowLayerAvailable?: React.Dispatch>; junctionText: string; pipeText: string; setJunctionText?: React.Dispatch>; setPipeText?: React.Dispatch>; setContours?: React.Dispatch>; deckLayer?: DeckLayer; compareDeckLayer?: DeckLayer; deckLayers?: DeckLayer[]; compareMap?: OlMap; maps?: OlMap[]; diameterRange?: [number, number]; elevationRange?: [number, number]; forceStyleAutoApplyVersion?: number; setForceStyleAutoApplyVersion?: React.Dispatch>; } // 跨组件传递 const MapContext = createContext(undefined); const DataContext = createContext(undefined); // 添加防抖函数 type DebouncedFunction any> = (( ...args: Parameters ) => void) & { cancel: () => void; }; function debounce any>( func: F, waitFor: number ): DebouncedFunction { let timeout: ReturnType | null = null; const debounced = (...args: Parameters): void => { if (timeout !== null) { clearTimeout(timeout); } timeout = setTimeout(() => func(...args), waitFor); }; debounced.cancel = () => { if (timeout !== null) { clearTimeout(timeout); timeout = null; } }; return debounced; } const indexCalculationRecords = (records: any[]) => new Map(records.map((record) => [String(record.ID), record])); const mergeJunctionValues = ( features: any[], records: any[], property: string, ) => { const recordsById = indexCalculationRecords(records); return features.map((feature) => { const record = recordsById.get(String(feature.id)); if (!record) return feature; const value = isLpsFlowProperty(property) ? toM3h(record.value, "lps") : record.value; return { ...feature, [property]: value }; }); }; const mergePipeValues = ( features: any[], records: any[], property: string, ) => { const recordsById = indexCalculationRecords(records); const isFlow = property === "flow"; return features.map((feature) => { const record = recordsById.get(String(feature.id)); if (!record) return feature; const value = isFlow ? toM3h(record.value, "lps") : record.value; const reverseFlow = isFlow && record.value < 0; return { ...feature, [property]: isFlow ? Math.abs(value) : value, flowFlag: reverseFlow ? -1 : 1, path: reverseFlow ? [...feature.path].reverse() : feature.path, }; }); }; const getNumericRange = (values: number[]): [number, number] | undefined => { let min = Infinity; let max = -Infinity; values.forEach((value) => { if (!Number.isFinite(value)) return; min = Math.min(min, value); max = Math.max(max, value); }); return min === Infinity ? undefined : [min, max]; }; export const useMap = () => { return useContext(MapContext); }; export const useData = () => { return useContext(DataContext); }; const MapComponent: React.FC = ({ children }) => { const pathname = usePathname(); const project = useProject(); const MAP_WORKSPACE = project?.workspace || config.MAP_WORKSPACE; const MAP_EXTENT = (project?.extent || config.MAP_EXTENT) as [ number, number, number, number, ]; const MAP_URL = config.MAP_URL; const MAP_VIEW_STORAGE_KEY = `${MAP_WORKSPACE}_map_view`; // 持久化 key const { durationMinutes, stepMinutes } = useTimelineTimeConfig(); const mapRef = useRef(null); const canvasRef = useRef(null); const compareMapRef = useRef(null); const compareCanvasRef = useRef(null); const deckLayerRef = useRef(null); const compareDeckLayerRef = useRef(null); const isDisposingRef = useRef(false); const isCompareDisposingRef = useRef(false); const [map, setMap] = useState(); const [deckLayer, setDeckLayer] = useState(); const [compareMap, setCompareMap] = useState(); const [compareDeckLayer, setCompareDeckLayer] = useState(); // currentCalData 用于存储当前计算结果 const [currentTime, setCurrentTime] = useState( getRoundedCurrentTimelineMinutes, ); // 默认选择当前时间 // const [selectedDate, setSelectedDate] = useState(new Date("2025-9-17")); const [selectedDate, setSelectedDate] = useState(new Date()); // 默认今天 const [schemeName, setSchemeName] = useState(""); // 当前方案名称 // 记录 id、对应属性的计算值 const [currentJunctionCalData, setCurrentJunctionCalData] = useState( [], ); const [currentPipeCalData, setCurrentPipeCalData] = useState([]); const [compareJunctionCalData, setCompareJunctionCalData] = useState( [], ); const [comparePipeCalData, setComparePipeCalData] = useState([]); const [isCompareMode, setCompareMode] = useState(false); // junctionData 为当前层级和视口内按 ID 去重的节点数据;pipeData 为管道标签数据。 // pipeFragments 保留当前层级和视口内每个瓦片管道片段,水流动画按 instanceKey 渲染,不能按业务 ID 去重。 const [junctionData, setJunctionDataState] = useState([]); const [pipeData, setPipeDataState] = useState([]); const [pipeFragments, setPipeFragments] = useState([]); const junctionIndexRef = useRef(null); const pipeIndexRef = useRef(null); const [showJunctionTextLayer, setShowJunctionTextLayer] = useState(false); // 控制节点文本图层显示 const [showPipeTextLayer, setShowPipeTextLayer] = useState(false); // 控制管道文本图层显示 const [showJunctionId, setShowJunctionId] = useState(false); // 控制节点ID显示 const [showPipeId, setShowPipeId] = useState(false); // 控制管道ID显示 const [showContourLayer, setShowContourLayer] = useState(false); // 控制等高线图层显示 const [junctionText, setJunctionText] = useState("pressure"); const [pipeText, setPipeText] = useState("velocity"); const [contours, setContours] = useState([]); const [isContourLayerAvailable, setContourLayerAvailable] = useState(false); // 控制等高线图层显示 const [isWaterflowLayerAvailable, setWaterflowLayerAvailable] = useState(false); // 控制等高线图层显示 const [showWaterflowLayer, setShowWaterflowLayer] = useState(false); // 控制等高线图层显示 const [currentZoom, setCurrentZoom] = useState(11); // 当前缩放级别 // 实时合并计算结果到基础地理数据中 const mergedJunctionData = useMemo( () => mergeJunctionValues(junctionData, currentJunctionCalData, junctionText), [junctionData, currentJunctionCalData, junctionText], ); const mergedPipeData = useMemo( () => mergePipeValues(pipeData, currentPipeCalData, pipeText), [pipeData, currentPipeCalData, pipeText], ); const mergedPipeFragments = useMemo( () => mergePipeValues(pipeFragments, currentPipeCalData, pipeText), [pipeFragments, currentPipeCalData, pipeText], ); const mergedCompareJunctionData = useMemo( () => isCompareMode ? mergeJunctionValues(junctionData, compareJunctionCalData, junctionText) : [], [isCompareMode, junctionData, compareJunctionCalData, junctionText], ); const mergedComparePipeData = useMemo( () => isCompareMode ? mergePipeValues(pipeData, comparePipeCalData, pipeText) : [], [isCompareMode, pipeData, comparePipeCalData, pipeText], ); const mergedComparePipeFragments = useMemo( () => isCompareMode ? mergePipeValues(pipeFragments, comparePipeCalData, pipeText) : [], [isCompareMode, pipeFragments, comparePipeCalData, pipeText], ); const [diameterRange, setDiameterRange] = useState< [number, number] | undefined >(); const [elevationRange, setElevationRange] = useState< [number, number] | undefined >(); const [forceStyleAutoApplyVersion, setForceStyleAutoApplyVersion] = useState(0); const toggleCompareMode = useCallback(() => { setCompareMode((prev) => { if (!prev) markCompareOpenStart(); return !prev; }); }, []); const maps = useMemo( () => [map, isCompareMode ? compareMap : undefined].filter(Boolean) as OlMap[], [compareMap, isCompareMode, map], ); const deckLayers = useMemo( () => [deckLayer, isCompareMode ? compareDeckLayer : undefined].filter( Boolean, ) as DeckLayer[], [compareDeckLayer, deckLayer, isCompareMode], ); const buildPipeFragments = useCallback((instance: TileFeatureInstance) => { const tileCoordinates = lineStringFromFlatCoordinates( instance.flatCoordinates, instance.stride, ); const clippedParts = clipLineStringPartsToExtent( tileCoordinates, instance.tileExtent, ); return clippedParts.flatMap((clippedCoordinates, partIndex) => { const path = coordinatesToLonLat(clippedCoordinates); const lineStringFeature = lineString(path); const fragmentLength = length(lineStringFeature); if (fragmentLength <= 0) return []; const timestamps = [0]; let cumulativeLength = 0; for (let i = 1; i < path.length; i += 1) { cumulativeLength += length(lineString([path[i - 1], path[i]])); timestamps.push((cumulativeLength / fragmentLength) * 10); } const midPoint = along(lineStringFeature, fragmentLength / 2).geometry .coordinates; const prevPoint = along(lineStringFeature, fragmentLength * 0.49).geometry .coordinates; const nextPoint = along(lineStringFeature, fragmentLength * 0.51).geometry .coordinates; let lineAngle = bearing(prevPoint, nextPoint); lineAngle = -lineAngle + 90; if (lineAngle < -90 || lineAngle > 90) { lineAngle += 180; } return [ { instanceKey: `${instance.instanceKey}/${partIndex}`, id: instance.featureId, diameter: instance.properties.diameter || 0, length: instance.properties.length || fragmentLength * 1000, path, position: midPoint, angle: lineAngle, timestamps, fragmentLength, }, ]; }); }, []); const publishActiveTileSnapshot = useCallback( (targetMap: OlMap) => { const zoom = targetMap.getView().getZoom() ?? 0; const junctionSnapshot = junctionIndexRef.current?.getSnapshot( targetMap, zoom, ); const pipeSnapshot = pipeIndexRef.current?.getSnapshot(targetMap, zoom); const nextJunctionData = Array.from( junctionSnapshot?.instancesById.values() ?? [], ) .map((instances) => instances[0]) .filter(Boolean) .map((instance) => { const [x, y] = lineStringFromFlatCoordinates( instance.flatCoordinates, instance.stride, )[0]; return { id: instance.featureId, instanceKey: instance.instanceKey, position: toLonLat([x, y]), elevation: instance.properties.elevation || 0, demand: instance.properties.demand || 0, }; }) .sort((a, b) => String(a.id).localeCompare(String(b.id))); const nextPipeFragments = (pipeSnapshot?.instances ?? []) .filter((instance) => instance.geometryType.includes("Line")) .flatMap(buildPipeFragments) .sort((a, b) => a.instanceKey.localeCompare(b.instanceKey)); const labelById = new Map(); nextPipeFragments.forEach((fragment) => { const previous = labelById.get(fragment.id); if ( !previous || fragment.fragmentLength > previous.fragmentLength || (fragment.fragmentLength === previous.fragmentLength && fragment.instanceKey.localeCompare(previous.instanceKey) < 0) ) { labelById.set(fragment.id, fragment); } }); const nextPipeLabels = Array.from(labelById.values()).sort((a, b) => String(a.id).localeCompare(String(b.id)), ); setJunctionDataState(nextJunctionData); setPipeFragments(nextPipeFragments); setPipeDataState(nextPipeLabels); setElevationRange( getNumericRange(nextJunctionData.map((item) => item.elevation)), ); setDiameterRange( getNumericRange(nextPipeLabels.map((item) => item.diameter)), ); }, [buildPipeFragments], ); const operationalSources = useMemo( () => createOperationalMapSources({ mapUrl: MAP_URL, workspace: MAP_WORKSPACE, }), [MAP_URL, MAP_WORKSPACE], ); const operationalResources = useMemo( () => createOperationalMapResources({ mapUrl: MAP_URL, workspace: MAP_WORKSPACE, extent: MAP_EXTENT, persistent: true, sources: operationalSources, }), [MAP_URL, MAP_WORKSPACE, MAP_EXTENT, operationalSources], ); const { junctions: junctionSource, pipes: pipeSource } = operationalResources.sources; const { junctions: junctionsLayer, pipes: pipesLayer } = operationalResources.layers; // The map and layer instances are intentionally rebuilt only when workspace or extent changes. useEffect(() => { if (!mapRef.current) return; if (!canvasRef.current) { return; } isDisposingRef.current = false; junctionIndexRef.current = new TileFeatureIndex("junctions", junctionSource); pipeIndexRef.current = new TileFeatureIndex("pipes", pipeSource); const handleJunctionTileLoadEnd = (event: any) => { if (isDisposingRef.current) return; try { junctionIndexRef.current?.registerTile(event.tile); scheduleActiveTileSnapshot(); } catch (error) { console.error("Junction tile load error:", error); } }; const handlePipeTileLoadEnd = (event: any) => { if (isDisposingRef.current) return; try { pipeIndexRef.current?.registerTile(event.tile); scheduleActiveTileSnapshot(); } catch (error) { console.error("Pipe tile load error:", error); } }; // 监听 junctionsLayer 的 visible 变化 const handleJunctionVisibilityChange = () => { const isVisible = junctionsLayer.getVisible(); setShowJunctionTextLayer(isVisible); }; // 监听 pipesLayer 的 visible 变化 const handlePipeVisibilityChange = () => { const isVisible = pipesLayer.getVisible(); setShowPipeTextLayer(isVisible); }; // 添加事件监听器 junctionsLayer.on("change:visible", handleJunctionVisibilityChange); pipesLayer.on("change:visible", handlePipeVisibilityChange); const map = new OlMap({ target: mapRef.current, view: new View({ maxZoom: 24, projection: "EPSG:3857", }), // 图层依面、线、点、标注次序添加 layers: operationalResources.orderedLayers.slice(), controls: [], }); const scheduleActiveTileSnapshot = debounce( () => publishActiveTileSnapshot(map), 50, ); junctionSource.on("tileloadend", handleJunctionTileLoadEnd); pipeSource.on("tileloadend", handlePipeTileLoadEnd); map.getInteractions().forEach(markMapResourcePersistent); setMap(map); // 恢复上次视图;如果没有则适配 MAP_EXTENT try { const stored = localStorage.getItem(MAP_VIEW_STORAGE_KEY); if (stored) { const viewState = JSON.parse(stored); if ( viewState && Array.isArray(viewState.center) && viewState.center.length === 2 && typeof viewState.zoom === "number" && viewState.zoom >= 11 && viewState.zoom <= 24 ) { map.getView().setCenter(viewState.center); map.getView().setZoom(viewState.zoom); } else { map.getView().fit(MAP_EXTENT, { padding: [50, 50, 50, 50], duration: 1000, }); } } else { map.getView().fit(MAP_EXTENT, { padding: [50, 50, 50, 50], duration: 1000, }); } } catch (err) { console.warn("Restore map view failed", err); map.getView().fit(MAP_EXTENT, { padding: [50, 50, 50, 50], duration: 1000, }); } // 视图稳定后同步 Deck 数据并持久化,避免移动过程中重复扫描瓦片。 const handleViewChange = debounce(() => { if (isDisposingRef.current) return; const view = map.getView(); const zoom = view.getZoom() || 0; setCurrentZoom(zoom); junctionIndexRef.current?.scanLoadedTiles(); pipeIndexRef.current?.scanLoadedTiles(); scheduleActiveTileSnapshot(); try { const center = view.getCenter(); if (center) { localStorage.setItem( MAP_VIEW_STORAGE_KEY, JSON.stringify({ center, zoom }), ); } } catch (err) { console.warn("Save map view failed", err); } }, 250); map.getView().on("change", handleViewChange); // 初始化当前缩放级别并强制触发瓦片加载 const initializeTimer = window.setTimeout(() => { if (isDisposingRef.current) return; const initialZoom = map.getView().getZoom() || 11; setCurrentZoom(initialZoom); junctionIndexRef.current?.scanLoadedTiles(); pipeIndexRef.current?.scanLoadedTiles(); scheduleActiveTileSnapshot(); // 强制触发地图渲染,让瓦片加载事件触发 map.render(); }, 100); // 初始化 deck.gl const deck = new Deck({ initialViewState: { longitude: 0, latitude: 0, zoom: 1, }, canvas: canvasRef.current, controller: false, // 由 OpenLayers 控制视图 layers: [], }); const deckLayer = markMapResourcePersistent( new DeckLayer(deck, canvasRef.current, { name: "deckLayer", value: "deckLayer", }), ); deckLayerRef.current = deckLayer; setDeckLayer(deckLayer); map.addLayer(deckLayer); // 清理函数 return () => { isDisposingRef.current = true; window.clearTimeout(initializeTimer); scheduleActiveTileSnapshot.cancel(); handleViewChange.cancel(); junctionSource.un("tileloadend", handleJunctionTileLoadEnd); pipeSource.un("tileloadend", handlePipeTileLoadEnd); map.getView().un("change", handleViewChange); junctionsLayer.un("change:visible", handleJunctionVisibilityChange); pipesLayer.un("change:visible", handlePipeVisibilityChange); if (deckLayerRef.current && !deckLayerRef.current.isDisposedLayer()) { try { map.removeLayer(deckLayerRef.current); } catch { // Layer may have already been removed during teardown. } deckLayerRef.current.disposeDeck(); } deckLayerRef.current = null; setDeckLayer(undefined); // React Strict Mode re-runs effects with the same memoized layer instances. // Detach and clear them here, but leave final layer/source disposal to GC. disposeMapResources(map, { disposeLayers: false }); junctionIndexRef.current = null; pipeIndexRef.current = null; setJunctionDataState([]); setPipeDataState([]); setPipeFragments([]); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [MAP_WORKSPACE, MAP_EXTENT]); useEffect(() => { if (!isCompareMode) { isCompareDisposingRef.current = true; setCompareJunctionCalData([]); setComparePipeCalData([]); return; } if (!map || !compareMapRef.current || !compareCanvasRef.current) return; isCompareDisposingRef.current = false; const compareResources = createOperationalMapResources({ mapUrl: MAP_URL, workspace: MAP_WORKSPACE, extent: MAP_EXTENT, sources: operationalSources, }); const nextCompareMap = new OlMap({ target: compareMapRef.current, view: map.getView(), layers: compareResources.orderedLayers.slice(), controls: [], }); const handleCompareRenderComplete = () => markCompareOpenReady(); nextCompareMap.once("rendercomplete", handleCompareRenderComplete); nextCompareMap.getAllLayers().forEach((layer) => { const layerId = layer.get("value"); if (!layerId) return; const primaryLayer = map .getAllLayers() .find((currentLayer) => currentLayer.get("value") === layerId); if (primaryLayer) { layer.setVisible(primaryLayer.getVisible()); } }); setCompareMap(nextCompareMap); const compareDeck = new Deck({ initialViewState: { longitude: 0, latitude: 0, zoom: 1, }, canvas: compareCanvasRef.current, controller: false, layers: [], }); const nextCompareDeckLayer = new DeckLayer( compareDeck, compareCanvasRef.current, { name: "compareDeckLayer", value: "deckLayer", }, ); compareDeckLayerRef.current = nextCompareDeckLayer; setCompareDeckLayer(nextCompareDeckLayer); nextCompareMap.addLayer(nextCompareDeckLayer); const resizeTimerId = window.setTimeout(() => { map.updateSize(); nextCompareMap.updateSize(); }, 0); return () => { isCompareDisposingRef.current = true; window.clearTimeout(resizeTimerId); nextCompareMap.un("rendercomplete", handleCompareRenderComplete); if ( compareDeckLayerRef.current && !compareDeckLayerRef.current.isDisposedLayer() ) { try { nextCompareMap.removeLayer(compareDeckLayerRef.current); } catch { // Layer may have already been removed during teardown. } compareDeckLayerRef.current.disposeDeck(); } compareDeckLayerRef.current = null; setCompareDeckLayer(undefined); setCompareMap(undefined); disposeMapResources(nextCompareMap, { disposeSources: false }); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isCompareMode, map]); useEffect(() => { const resizeTimerId = window.setTimeout(() => { map?.updateSize(); compareMap?.updateSize(); }, 0); return () => { window.clearTimeout(resizeTimerId); }; }, [compareMap, isCompareMode, map]); useEffect(() => { setCurrentTime( getRoundedCurrentTimelineMinutes(new Date(), stepMinutes, durationMinutes), ); setSelectedDate(new Date()); setSchemeName(""); setCurrentJunctionCalData([]); setCurrentPipeCalData([]); setCompareJunctionCalData([]); setComparePipeCalData([]); setCompareMode(false); setShowJunctionTextLayer(false); setShowPipeTextLayer(false); setShowJunctionId(false); setShowPipeId(false); setShowContourLayer(false); setContours([]); setContourLayerAvailable(false); setWaterflowLayerAvailable(false); setShowWaterflowLayer(false); setForceStyleAutoApplyVersion(0); return () => { if (!map) return; cleanupTransientMapResources(map); deckLayerRef.current?.resetSessionLayers(); operationalResources.resetStyles(); }; }, [durationMinutes, pathname, map, operationalResources, stepMinutes]); // 当数据变化时,更新 deck.gl 图层 useEffect(() => { const syncDeckOverlay = ( targetDeckLayer: DeckLayer | null, targetJunctionData: any[], targetPipeData: any[], disposing: boolean, ) => { if (disposing || !targetDeckLayer || targetDeckLayer.isDisposedLayer()) { return; } const shouldShowJunctionText = (showJunctionTextLayer || showJunctionId) && currentZoom >= 15 && currentZoom <= 24 && targetJunctionData.length > 0; const shouldShowPipeText = (showPipeTextLayer || showPipeId) && currentZoom >= 15 && currentZoom <= 24 && targetPipeData.length > 0; const shouldShowContour = showContourLayer && currentZoom >= 11 && currentZoom <= 24 && targetJunctionData.length > 0; if (!shouldShowJunctionText) { targetDeckLayer.removeDeckLayer("junctionTextLayer"); } if (!shouldShowPipeText) { targetDeckLayer.removeDeckLayer("pipeTextLayer"); } if (!shouldShowContour) { targetDeckLayer.removeDeckLayer("junctionContourLayer"); } if (!shouldShowJunctionText && !shouldShowPipeText && !shouldShowContour) { return; } const junctionTextLayer = shouldShowJunctionText ? new TextLayer({ id: "junctionTextLayer", name: "节点文字", zIndex: 10, data: targetJunctionData, getPosition: (d: any) => d.position, fontFamily: "Monaco, monospace", getText: (d: any) => { let idPart = showJunctionId ? d.id : ""; let propPart = ""; if (showJunctionTextLayer && d[junctionText] !== undefined) { const value = (d[junctionText] as number).toFixed(3); propPart = `${value}`; } if (idPart && propPart) return `${idPart} - ${propPart}`; return idPart || propPart; }, getSize: 14, fontWeight: "bold", getColor: [33, 37, 41], getAngle: 0, getTextAnchor: "middle", getAlignmentBaseline: "center", getPixelOffset: [0, -10], visible: true, updateTriggers: { getText: [showJunctionId, showJunctionTextLayer, junctionText], }, extensions: [new CollisionFilterExtension()], collisionTestProps: { sizeScale: 3, }, characterSet: "auto", fontSettings: { sdf: true, fontSize: 64, buffer: 6, }, }) : null; const pipeTextLayer = shouldShowPipeText ? new TextLayer({ id: "pipeTextLayer", name: "管道文字", zIndex: 10, data: targetPipeData, getPosition: (d: any) => d.position, fontFamily: "Monaco, monospace", getText: (d: any) => { let idPart = showPipeId ? d.id : ""; let propPart = ""; if (showPipeTextLayer && d[pipeText] !== undefined) { let value; if (pipeText === "unit_headloss") { value = ( (d["unit_headloss"] / (d["length"] / 1000)) as number ).toFixed(3); } else { value = Math.abs(d[pipeText] as number).toFixed(3); } propPart = `${value}`; } if (idPart && propPart) return `${idPart} - ${propPart}`; return idPart || propPart; }, getSize: 14, fontWeight: "bold", getColor: [33, 37, 41], getAngle: (d: any) => d.angle || 0, getPixelOffset: [0, -8], getTextAnchor: "middle", getAlignmentBaseline: "bottom", visible: true, updateTriggers: { getText: [showPipeId, showPipeTextLayer, pipeText], }, extensions: [new CollisionFilterExtension()], collisionTestProps: { sizeScale: 3, }, characterSet: "auto", fontSettings: { sdf: true, fontSize: 64, buffer: 6, }, }) : null; const contourLayer = shouldShowContour ? new ContourLayer({ id: "junctionContourLayer", name: "等值线", data: targetJunctionData, aggregation: "MEAN", cellSize: 600, strokeWidth: 0, contours: contours, getPosition: (d) => d.position, getWeight: (d: any) => (d[junctionText] as number) < 0 ? 0 : (d[junctionText] as number), opacity: 1, visible: true, updateTriggers: { getWeight: [targetJunctionData, junctionText], }, }) : null; if ( junctionTextLayer && targetDeckLayer.getDeckLayerById("junctionTextLayer") ) { targetDeckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer); } else if (junctionTextLayer) { targetDeckLayer.addDeckLayer(junctionTextLayer); } if (pipeTextLayer && targetDeckLayer.getDeckLayerById("pipeTextLayer")) { targetDeckLayer.updateDeckLayer("pipeTextLayer", pipeTextLayer); } else if (pipeTextLayer) { targetDeckLayer.addDeckLayer(pipeTextLayer); } if ( contourLayer && targetDeckLayer.getDeckLayerById("junctionContourLayer") ) { targetDeckLayer.updateDeckLayer("junctionContourLayer", contourLayer); } else if (contourLayer) { targetDeckLayer.addDeckLayer(contourLayer); } }; syncDeckOverlay( deckLayerRef.current, mergedJunctionData, mergedPipeData, isDisposingRef.current, ); if (isCompareMode) { syncDeckOverlay( compareDeckLayerRef.current, mergedCompareJunctionData, mergedComparePipeData, isCompareDisposingRef.current, ); } }, [ mergedJunctionData, mergedPipeData, mergedCompareJunctionData, mergedComparePipeData, isCompareMode, junctionText, pipeText, currentZoom, showJunctionTextLayer, showPipeTextLayer, showJunctionId, showPipeId, showContourLayer, contours, ]); // 控制流动动画开关 useEffect(() => { const hasFlowData = pipeText === "flow" && currentPipeCalData.length > 0; const shouldShowWaterflow = isWaterflowLayerAvailable && showWaterflowLayer && hasFlowData && currentZoom >= 12 && currentZoom <= 24; let animationFrameId: number; const syncWaterflowLayer = ( targetDeckLayer: DeckLayer | null, targetPipeFragments: any[], disposing: boolean, ) => { if (disposing || !targetDeckLayer || targetDeckLayer.isDisposedLayer()) { return; } if (!shouldShowWaterflow || targetPipeFragments.length === 0) { targetDeckLayer.removeDeckLayer("waterflowLayer"); return; } const animationDuration = 10; const bufferTime = 2; const loopLength = animationDuration + bufferTime; const currentFrameTime = (Date.now() / 1000) % loopLength; const waterflowLayer = new TripsLayer({ id: "waterflowLayer", name: "水流", data: targetPipeFragments, getObjectId: (d: any) => d.instanceKey, getPath: (d) => d.path, getTimestamps: (d) => d.timestamps, getColor: [0, 220, 255], opacity: 0.8, visible: true, widthMinPixels: 5, jointRounded: true, trailLength: 2, currentTime: currentFrameTime, }); if (targetDeckLayer.getDeckLayerById("waterflowLayer")) { targetDeckLayer.updateDeckLayer("waterflowLayer", waterflowLayer); } else { targetDeckLayer.addDeckLayer(waterflowLayer); } }; const animate = () => { syncWaterflowLayer( deckLayerRef.current, mergedPipeFragments, isDisposingRef.current, ); if (isCompareMode) { syncWaterflowLayer( compareDeckLayerRef.current, mergedComparePipeFragments, isCompareDisposingRef.current, ); } if (shouldShowWaterflow) { animationFrameId = requestAnimationFrame(animate); } }; animate(); // 清理函数:取消动画帧 return () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); } }; }, [ currentPipeCalData, currentZoom, mergedPipeFragments, mergedComparePipeFragments, isCompareMode, pipeText, isWaterflowLayerAvailable, showWaterflowLayer, ]); return ( <>
{isCompareMode && (
方案模拟
)}
{isCompareMode && (
实时模拟
)}
{isCompareMode && (
)} {children}
); }; export default MapComponent;