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

1194 lines
40 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, VectorTile } 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 { toM3h } from "@utils/units";
import { usePathname } from "next/navigation";
import {
cleanupTransientMapResources,
disposeMapResources,
markMapResourcePersistent,
} from "./mapLifecycle";
import { createOperationalMapResources } from "./operationalLayers";
import { getRoundedCurrentTimelineMinutes } from "./Controls/timelineTime";
interface MapComponentProps {
children?: React.ReactNode;
}
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;
}
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 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 pendingTimeoutsRef = useRef<number[]>([]);
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 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染
// currentJunctionCalData 和 currentPipeCalData 变化时会新增并更新 junctionData 和 pipeData 的计算属性值
const [junctionData, setJunctionDataState] = useState<any[]>([]);
const [pipeData, setPipeDataState] = useState<any[]>([]);
const junctionDataIds = useRef(new Set<string>());
const pipeDataIds = useRef(new Set<string>());
const tileJunctionDataBuffer = useRef<any[]>([]);
const tilePipeDataBuffer = useRef<any[]>([]);
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 flowAnimation = useRef(false); // 添加动画控制标志
const [isContourLayerAvailable, setContourLayerAvailable] = useState(false); // 控制等高线图层显示
const [isWaterflowLayerAvailable, setWaterflowLayerAvailable] =
useState(false); // 控制等高线图层显示
const [showWaterflowLayer, setShowWaterflowLayer] = useState(false); // 控制等高线图层显示
const [currentZoom, setCurrentZoom] = useState(11); // 当前缩放级别
// 实时合并计算结果到基础地理数据中
const mergedJunctionData = useMemo(() => {
const nodeMap = new Map(currentJunctionCalData.map((r: any) => [r.ID, r]));
return junctionData.map((j) => {
const record = nodeMap.get(j.id);
let val = record ? record.value : undefined;
// 在这合并时将实际需水量从 LPS 转换为大写表示
if (val !== undefined && junctionText === "actualdemand") {
val = toM3h(val, "lps");
}
return record ? { ...j, [junctionText]: val } : j;
});
}, [junctionData, currentJunctionCalData, junctionText]);
const mergedPipeData = useMemo(() => {
const linkMap = new Map(currentPipeCalData.map((r: any) => [r.ID, r]));
return pipeData.map((p) => {
const record = linkMap.get(p.id);
if (!record) return p;
const isFlow = pipeText === "flow";
let val = record.value;
if (val !== undefined && isFlow) {
val = toM3h(val, "lps");
}
return {
...p,
[pipeText]: isFlow ? Math.abs(val) : val,
flowFlag: isFlow && record.value < 0 ? -1 : 1,
path: isFlow && record.value < 0 ? [...p.path].reverse() : p.path,
};
});
}, [pipeData, currentPipeCalData, pipeText]);
const mergedCompareJunctionData = useMemo(() => {
const nodeMap = new Map(compareJunctionCalData.map((r: any) => [r.ID, r]));
return junctionData.map((j) => {
const record = nodeMap.get(j.id);
let val = record ? record.value : undefined;
if (val !== undefined && junctionText === "actualdemand") {
val = toM3h(val, "lps");
}
return record ? { ...j, [junctionText]: val } : j;
});
}, [junctionData, compareJunctionCalData, junctionText]);
const mergedComparePipeData = useMemo(() => {
const linkMap = new Map(comparePipeCalData.map((r: any) => [r.ID, r]));
return pipeData.map((p) => {
const record = linkMap.get(p.id);
if (!record) return p;
const isFlow = pipeText === "flow";
let val = record.value;
if (val !== undefined && isFlow) {
val = toM3h(val, "lps");
}
return {
...p,
[pipeText]: isFlow ? Math.abs(val) : val,
flowFlag: isFlow && record.value < 0 ? -1 : 1,
path: isFlow && record.value < 0 ? [...p.path].reverse() : p.path,
};
});
}, [pipeData, 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) => !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 setJunctionData = (newData: any[]) => {
const uniqueNewData = newData.filter((item) => {
if (!item || !item.id) return false;
if (!junctionDataIds.current.has(item.id)) {
junctionDataIds.current.add(item.id);
return true;
}
return false;
});
if (uniqueNewData.length > 0) {
setJunctionDataState((prev) => prev.concat(uniqueNewData));
setElevationRange((prev) => {
const elevations = uniqueNewData
.map((d) => d.elevation)
.filter((v) => typeof v === "number");
if (elevations.length === 0) return prev;
let newMin = elevations[0];
let newMax = elevations[0];
for (let i = 1; i < elevations.length; i++) {
if (elevations[i] < newMin) newMin = elevations[i];
if (elevations[i] > newMax) newMax = elevations[i];
}
if (!prev) {
return [newMin, newMax];
}
return [Math.min(prev[0], newMin), Math.max(prev[1], newMax)];
});
}
};
const setPipeData = (newData: any[]) => {
const uniqueNewData = newData.filter((item) => {
if (!item || !item.id) return false;
if (!pipeDataIds.current.has(item.id)) {
pipeDataIds.current.add(item.id);
return true;
}
return false;
});
if (uniqueNewData.length > 0) {
setPipeDataState((prev) => prev.concat(uniqueNewData));
setDiameterRange((prev) => {
const diameters = uniqueNewData
.map((d) => d.diameter)
.filter((v) => typeof v === "number");
if (diameters.length === 0) return prev;
let newMin = diameters[0];
let newMax = diameters[0];
for (let i = 1; i < diameters.length; i++) {
if (diameters[i] < newMin) newMin = diameters[i];
if (diameters[i] > newMax) newMax = diameters[i];
}
if (!prev) {
return [newMin, newMax];
}
return [Math.min(prev[0], newMin), Math.max(prev[1], newMax)];
});
}
};
const debouncedUpdateDataRef = useRef<DebouncedFunction<() => void> | null>(
null,
);
useEffect(() => {
debouncedUpdateDataRef.current = debounce(() => {
if (tileJunctionDataBuffer.current.length > 0) {
setJunctionData(tileJunctionDataBuffer.current);
tileJunctionDataBuffer.current = [];
}
if (tilePipeDataBuffer.current.length > 0) {
setPipeData(tilePipeDataBuffer.current);
tilePipeDataBuffer.current = [];
}
}, 100);
return () => {
debouncedUpdateDataRef.current?.cancel();
debouncedUpdateDataRef.current = null;
};
}, []);
const operationalResources = useMemo(
() =>
createOperationalMapResources({
mapUrl: MAP_URL,
workspace: MAP_WORKSPACE,
extent: MAP_EXTENT,
persistent: true,
}),
[MAP_URL, MAP_WORKSPACE, MAP_EXTENT],
);
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;
const activeJunctionDataIds = junctionDataIds.current;
const activePipeDataIds = pipeDataIds.current;
const addTimeout = (callback: () => void, delay: number) => {
const timerId = window.setTimeout(() => {
pendingTimeoutsRef.current = pendingTimeoutsRef.current.filter(
(id) => id !== timerId,
);
if (isDisposingRef.current) return;
callback();
}, delay);
pendingTimeoutsRef.current.push(timerId);
return timerId;
};
const clearPendingTimeouts = () => {
pendingTimeoutsRef.current.forEach((id) => clearTimeout(id));
pendingTimeoutsRef.current = [];
};
// 缓存 junction、pipe 数据,提供给 deck.gl 提供坐标供标签显示
const handleJunctionTileLoadEnd = (event: any) => {
if (isDisposingRef.current) return;
try {
if (event.tile instanceof VectorTile) {
const renderFeatures = event.tile.getFeatures();
const data = new Map();
renderFeatures.forEach((renderFeature: any) => {
const props = renderFeature.getProperties();
const featureId = props.id;
if (featureId && !junctionDataIds.current.has(featureId)) {
const geometry = renderFeature.getGeometry();
if (geometry) {
const coordinates = geometry.getFlatCoordinates();
const coordWGS84 = toLonLat(coordinates);
data.set(featureId, {
id: featureId,
position: coordWGS84,
elevation: props.elevation || 0,
demand: props.demand || 0,
});
}
}
});
const uniqueData = Array.from(data.values());
if (uniqueData.length > 0) {
uniqueData.forEach((item) =>
tileJunctionDataBuffer.current.push(item),
);
debouncedUpdateDataRef.current?.();
}
}
} catch (error) {
console.error("Junction tile load error:", error);
}
};
const handlePipeTileLoadEnd = (event: any) => {
if (isDisposingRef.current) return;
try {
if (event.tile instanceof VectorTile) {
const renderFeatures = event.tile.getFeatures();
const data = new Map();
renderFeatures.forEach((renderFeature: any) => {
try {
const props = renderFeature.getProperties();
const featureId = props.id;
if (featureId && !pipeDataIds.current.has(featureId)) {
const geometry = renderFeature.getGeometry();
if (geometry) {
const flatCoordinates = geometry.getFlatCoordinates();
const stride = geometry.getStride(); // 获取步长,通常为 2
// 重建为 LineString GeoJSON 格式的 coordinates: [[x1, y1], [x2, y2], ...]
const lineCoords = [];
for (let i = 0; i < flatCoordinates.length; i += stride) {
lineCoords.push([
flatCoordinates[i],
flatCoordinates[i + 1],
]);
}
const lineCoordsWGS84 = lineCoords.map((coord) => {
const [lon, lat] = toLonLat(coord);
return [lon, lat];
});
// 添加验证:确保至少有 2 个坐标点
if (lineCoordsWGS84.length < 2) return; // 跳过此特征
// 计算中点
const lineStringFeature = lineString(lineCoordsWGS84);
const lineLength = length(lineStringFeature);
const midPoint = along(lineStringFeature, lineLength / 2)
.geometry.coordinates;
// 计算角度
const prevPoint = along(lineStringFeature, lineLength * 0.49)
.geometry.coordinates;
const nextPoint = along(lineStringFeature, lineLength * 0.51)
.geometry.coordinates;
let lineAngle = bearing(prevPoint, nextPoint);
lineAngle = -lineAngle + 90;
if (lineAngle < -90 || lineAngle > 90) {
lineAngle += 180;
}
// 计算时间戳(可选)
const numSegments = lineCoordsWGS84.length - 1;
const timestamps = [0];
if (numSegments > 0) {
for (let i = 1; i <= numSegments; i++) {
timestamps.push((i / numSegments) * 10);
}
}
data.set(featureId, {
id: featureId,
diameter: props.diameter || 0,
length: props.length || 0,
path: lineCoordsWGS84, // 使用重建后的坐标
position: midPoint,
angle: lineAngle,
timestamps,
});
}
}
} catch (geomError) {
console.error("Geometry calculation error:", geomError);
}
});
const uniqueData = Array.from(data.values());
if (uniqueData.length > 0) {
uniqueData.forEach((item) => tilePipeDataBuffer.current.push(item));
debouncedUpdateDataRef.current?.();
}
}
} catch (error) {
console.error("Pipe tile load error:", error);
}
};
junctionSource.on("tileloadend", handleJunctionTileLoadEnd);
pipeSource.on("tileloadend", handlePipeTileLoadEnd);
// 监听 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: [],
});
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,
});
}
// 持久化视图(中心点 + 缩放),防抖写入 localStorage
const persistView = debounce(() => {
if (isDisposingRef.current) return;
try {
const view = map.getView();
const center = view.getCenter();
const zoom = view.getZoom();
if (center && typeof zoom === "number") {
localStorage.setItem(
MAP_VIEW_STORAGE_KEY,
JSON.stringify({ center, zoom }),
);
}
} catch (err) {
console.warn("Save map view failed", err);
}
}, 250);
// 监听缩放变化并持久化,同时更新 currentZoom
const handleViewChange = () => {
addTimeout(() => {
const zoom = map.getView().getZoom() || 0;
setCurrentZoom(zoom);
persistView();
}, 250);
};
map.getView().on("change", handleViewChange);
// 初始化当前缩放级别并强制触发瓦片加载
addTimeout(() => {
const initialZoom = map.getView().getZoom() || 11;
setCurrentZoom(initialZoom);
// 强制触发地图渲染,让瓦片加载事件触发
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;
clearPendingTimeouts();
debouncedUpdateDataRef.current?.cancel();
persistView.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 });
activeJunctionDataIds.clear();
activePipeDataIds.clear();
tileJunctionDataBuffer.current = [];
tilePipeDataBuffer.current = [];
};
// 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,
});
const nextCompareMap = new OlMap({
target: compareMapRef.current,
view: map.getView(),
layers: compareResources.orderedLayers.slice(),
controls: [],
});
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);
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);
};
// 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());
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();
};
}, [pathname, map, operationalResources]);
// 当数据变化时,更新 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(() => {
flowAnimation.current = pipeText === "flow" && currentPipeCalData.length > 0;
const shouldShowWaterflow =
isWaterflowLayerAvailable &&
showWaterflowLayer &&
flowAnimation.current &&
currentZoom >= 12 &&
currentZoom <= 24;
let animationFrameId: number;
const syncWaterflowLayer = (
targetDeckLayer: DeckLayer | null,
targetPipeData: any[],
disposing: boolean,
) => {
if (disposing || !targetDeckLayer || targetDeckLayer.isDisposedLayer()) {
return;
}
if (!shouldShowWaterflow || targetPipeData.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: targetPipeData,
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,
mergedPipeData,
isDisposingRef.current,
);
if (isCompareMode) {
syncWaterflowLayer(
compareDeckLayerRef.current,
mergedComparePipeData,
isCompareDisposingRef.current,
);
}
if (shouldShowWaterflow) {
animationFrameId = requestAnimationFrame(animate);
}
};
animate();
// 清理函数:取消动画帧
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
}, [
currentPipeCalData,
currentZoom,
mergedPipeData,
mergedComparePipeData,
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;