Files
TJWaterFrontend_Refine/src/components/olmap/core/MapComponent.tsx
T

1183 lines
39 KiB
TypeScript

"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<React.SetStateAction<number>>;
selectedDate?: Date; // 选择的日期
schemeName?: string; // 当前方案名称
setSchemeName?: React.Dispatch<React.SetStateAction<string>>;
setSelectedDate?: React.Dispatch<React.SetStateAction<Date>>;
currentJunctionCalData?: any[]; // 当前计算结果
setCurrentJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>;
currentPipeCalData?: any[]; // 当前计算结果
setCurrentPipeCalData?: React.Dispatch<React.SetStateAction<any[]>>;
compareJunctionCalData?: any[];
setCompareJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>;
comparePipeCalData?: any[];
setComparePipeCalData?: React.Dispatch<React.SetStateAction<any[]>>;
isCompareMode?: boolean;
setCompareMode?: React.Dispatch<React.SetStateAction<boolean>>;
toggleCompareMode?: () => void;
showJunctionText?: boolean; // 是否显示节点文本
showPipeText?: boolean; // 是否显示管道文本
showJunctionId?: boolean; // 是否显示节点ID
showPipeId?: boolean; // 是否显示管道ID
setShowJunctionTextLayer?: React.Dispatch<React.SetStateAction<boolean>>;
setShowPipeTextLayer?: React.Dispatch<React.SetStateAction<boolean>>;
setShowJunctionId?: React.Dispatch<React.SetStateAction<boolean>>;
setShowPipeId?: React.Dispatch<React.SetStateAction<boolean>>;
showContourLayer?: boolean;
setShowContourLayer?: React.Dispatch<React.SetStateAction<boolean>>;
isContourLayerAvailable?: boolean;
showWaterflowLayer?: boolean;
setShowWaterflowLayer?: React.Dispatch<React.SetStateAction<boolean>>;
setContourLayerAvailable?: React.Dispatch<React.SetStateAction<boolean>>;
isWaterflowLayerAvailable?: boolean;
setWaterflowLayerAvailable?: React.Dispatch<React.SetStateAction<boolean>>;
junctionText: string;
pipeText: string;
setJunctionText?: React.Dispatch<React.SetStateAction<string>>;
setPipeText?: React.Dispatch<React.SetStateAction<string>>;
setContours?: React.Dispatch<React.SetStateAction<any[]>>;
deckLayer?: DeckLayer;
compareDeckLayer?: DeckLayer;
deckLayers?: DeckLayer[];
compareMap?: OlMap;
maps?: OlMap[];
diameterRange?: [number, number];
elevationRange?: [number, number];
forceStyleAutoApplyVersion?: number;
setForceStyleAutoApplyVersion?: React.Dispatch<React.SetStateAction<number>>;
}
// 跨组件传递
const MapContext = createContext<OlMap | undefined>(undefined);
const DataContext = createContext<DataContextType | undefined>(undefined);
// 添加防抖函数
type DebouncedFunction<F extends (...args: any[]) => any> = ((
...args: Parameters<F>
) => void) & {
cancel: () => void;
};
function debounce<F extends (...args: any[]) => any>(
func: F,
waitFor: number
): DebouncedFunction<F> {
let timeout: ReturnType<typeof setTimeout> | null = null;
const debounced = (...args: Parameters<F>): 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<MapComponentProps> = ({ 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<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const compareMapRef = useRef<HTMLDivElement | null>(null);
const compareCanvasRef = useRef<HTMLCanvasElement | null>(null);
const deckLayerRef = useRef<DeckLayer | null>(null);
const compareDeckLayerRef = useRef<DeckLayer | null>(null);
const isDisposingRef = useRef(false);
const isCompareDisposingRef = useRef(false);
const [map, setMap] = useState<OlMap>();
const [deckLayer, setDeckLayer] = useState<DeckLayer>();
const [compareMap, setCompareMap] = useState<OlMap>();
const [compareDeckLayer, setCompareDeckLayer] = useState<DeckLayer>();
// currentCalData 用于存储当前计算结果
const [currentTime, setCurrentTime] = useState<number>(
getRoundedCurrentTimelineMinutes,
); // 默认选择当前时间
// const [selectedDate, setSelectedDate] = useState<Date>(new Date("2025-9-17"));
const [selectedDate, setSelectedDate] = useState<Date>(new Date()); // 默认今天
const [schemeName, setSchemeName] = useState<string>(""); // 当前方案名称
// 记录 id、对应属性的计算值
const [currentJunctionCalData, setCurrentJunctionCalData] = useState<any[]>(
[],
);
const [currentPipeCalData, setCurrentPipeCalData] = useState<any[]>([]);
const [compareJunctionCalData, setCompareJunctionCalData] = useState<any[]>(
[],
);
const [comparePipeCalData, setComparePipeCalData] = useState<any[]>([]);
const [isCompareMode, setCompareMode] = useState(false);
// junctionData 为当前层级和视口内按 ID 去重的节点数据;pipeData 为管道标签数据。
// pipeFragments 保留当前层级和视口内每个瓦片管道片段,水流动画按 instanceKey 渲染,不能按业务 ID 去重。
const [junctionData, setJunctionDataState] = useState<any[]>([]);
const [pipeData, setPipeDataState] = useState<any[]>([]);
const [pipeFragments, setPipeFragments] = useState<any[]>([]);
const junctionIndexRef = useRef<TileFeatureIndex | null>(null);
const pipeIndexRef = useRef<TileFeatureIndex | null>(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<any[]>([]);
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<string, any>();
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 (
<>
<DataContext.Provider
value={{
currentTime,
setCurrentTime,
selectedDate,
setSelectedDate,
schemeName,
setSchemeName,
currentJunctionCalData,
setCurrentJunctionCalData,
currentPipeCalData,
setCurrentPipeCalData,
compareJunctionCalData,
setCompareJunctionCalData,
comparePipeCalData,
setComparePipeCalData,
isCompareMode,
setCompareMode,
toggleCompareMode,
setShowJunctionTextLayer,
setShowPipeTextLayer,
setShowJunctionId,
setShowPipeId,
showJunctionId,
showPipeId,
showContourLayer,
setShowContourLayer,
isContourLayerAvailable,
showWaterflowLayer,
setContourLayerAvailable,
isWaterflowLayerAvailable,
setWaterflowLayerAvailable,
setShowWaterflowLayer,
setJunctionText,
setPipeText,
junctionText,
pipeText,
setContours,
deckLayer,
compareDeckLayer,
deckLayers,
compareMap,
maps,
diameterRange,
elevationRange,
forceStyleAutoApplyVersion,
setForceStyleAutoApplyVersion,
}}
>
<MapContext.Provider value={map}>
<div className="relative w-full h-full">
<div className="flex w-full h-full">
<div
className={`relative h-full ${isCompareMode ? "w-1/2" : "w-full"}`}
>
<div ref={mapRef} className="w-full h-full"></div>
<canvas
ref={canvasRef}
className="pointer-events-none absolute inset-0"
/>
{isCompareMode && (
<div className="pointer-events-none absolute right-4 top-4 rounded-md bg-black/55 px-3 py-1 text-sm font-medium text-white">
方案模拟
</div>
)}
</div>
{isCompareMode && (
<div className="relative h-full w-1/2 border-l border-white/40">
<div ref={compareMapRef} className="w-full h-full"></div>
<canvas
ref={compareCanvasRef}
className="pointer-events-none absolute inset-0"
/>
<div className="pointer-events-none absolute left-4 top-4 rounded-md bg-black/55 px-3 py-1 text-sm font-medium text-white">
实时模拟
</div>
</div>
)}
</div>
{isCompareMode && (
<div className="pointer-events-none absolute inset-y-0 left-1/2 z-10 w-px -translate-x-1/2 bg-white/85 shadow-[0_0_0_1px_rgba(15,23,42,0.18)]" />
)}
<MapTools />
{children}
</div>
</MapContext.Provider>
</DataContext.Provider>
</>
);
};
export default MapComponent;