fix(map): stabilize tiled style rendering

This commit is contained in:
2026-07-17 15:12:10 +08:00
parent 59447a100c
commit 589cf45aa7
28 changed files with 3884 additions and 2461 deletions
+298 -312
View File
@@ -10,7 +10,7 @@ import React, {
useCallback,
useRef,
} from "react";
import { Map as OlMap, VectorTile } from "ol";
import { Map as OlMap } from "ol";
import View from "ol/View.js";
import "ol/ol.css";
import MapTools from "./MapTools";
@@ -31,13 +31,47 @@ import {
disposeMapResources,
markMapResourcePersistent,
} from "./mapLifecycle";
import { createOperationalMapResources } from "./operationalLayers";
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>>;
@@ -122,6 +156,57 @@ function debounce<F extends (...args: any[]) => any>(
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);
};
@@ -151,7 +236,6 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
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>();
@@ -174,14 +258,13 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
);
const [comparePipeCalData, setComparePipeCalData] = useState<any[]>([]);
const [isCompareMode, setCompareMode] = useState(false);
// junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染
// currentJunctionCalData 和 currentPipeCalData 变化时会新增并更新 junctionData 和 pipeData 的计算属性值
// junctionData 为当前层级和视口内按 ID 去重的节点数据;pipeData 为管道标签数据。
// pipeFragments 保留当前层级和视口内每个瓦片管道片段,水流动画按 instanceKey 渲染,不能按业务 ID 去重。
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 [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); // 控制管道文本图层显示
@@ -191,7 +274,6 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
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); // 控制等高线图层显示
@@ -199,67 +281,40 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
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;
if (val !== undefined && isLpsFlowProperty(junctionText)) {
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 && isLpsFlowProperty(junctionText)) {
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 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
@@ -271,7 +326,10 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
useState(0);
const toggleCompareMode = useCallback(() => {
setCompareMode((prev) => !prev);
setCompareMode((prev) => {
if (!prev) markCompareOpenStart();
return !prev;
});
}, []);
const maps = useMemo(
@@ -288,91 +346,126 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
[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;
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);
}
return false;
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,
},
];
});
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 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({
@@ -380,8 +473,9 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
workspace: MAP_WORKSPACE,
extent: MAP_EXTENT,
persistent: true,
sources: operationalSources,
}),
[MAP_URL, MAP_WORKSPACE, MAP_EXTENT],
[MAP_URL, MAP_WORKSPACE, MAP_EXTENT, operationalSources],
);
const { junctions: junctionSource, pipes: pipeSource } =
operationalResources.sources;
@@ -395,60 +489,15 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
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;
};
junctionIndexRef.current = new TileFeatureIndex("junctions", junctionSource);
pipeIndexRef.current = new TileFeatureIndex("pipes", pipeSource);
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?.();
}
}
junctionIndexRef.current?.registerTile(event.tile);
scheduleActiveTileSnapshot();
} catch (error) {
console.error("Junction tile load error:", error);
}
@@ -456,86 +505,12 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
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?.();
}
}
pipeIndexRef.current?.registerTile(event.tile);
scheduleActiveTileSnapshot();
} 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();
@@ -559,6 +534,12 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
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);
@@ -596,14 +577,18 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
duration: 1000,
});
}
// 持久化视图(中心点 + 缩放),防抖写入 localStorage
const persistView = debounce(() => {
// 视图稳定后同步 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 view = map.getView();
const center = view.getCenter();
const zoom = view.getZoom();
if (center && typeof zoom === "number") {
if (center) {
localStorage.setItem(
MAP_VIEW_STORAGE_KEY,
JSON.stringify({ center, zoom }),
@@ -613,21 +598,16 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
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 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);
@@ -656,9 +636,9 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
// 清理函数
return () => {
isDisposingRef.current = true;
clearPendingTimeouts();
debouncedUpdateDataRef.current?.cancel();
persistView.cancel();
window.clearTimeout(initializeTimer);
scheduleActiveTileSnapshot.cancel();
handleViewChange.cancel();
junctionSource.un("tileloadend", handleJunctionTileLoadEnd);
pipeSource.un("tileloadend", handlePipeTileLoadEnd);
map.getView().un("change", handleViewChange);
@@ -677,10 +657,11 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
// 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 = [];
junctionIndexRef.current = null;
pipeIndexRef.current = null;
setJunctionDataState([]);
setPipeDataState([]);
setPipeFragments([]);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [MAP_WORKSPACE, MAP_EXTENT]);
@@ -699,6 +680,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
mapUrl: MAP_URL,
workspace: MAP_WORKSPACE,
extent: MAP_EXTENT,
sources: operationalSources,
});
const nextCompareMap = new OlMap({
target: compareMapRef.current,
@@ -706,6 +688,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
layers: compareResources.orderedLayers.slice(),
controls: [],
});
const handleCompareRenderComplete = () => markCompareOpenReady();
nextCompareMap.once("rendercomplete", handleCompareRenderComplete);
nextCompareMap.getAllLayers().forEach((layer) => {
const layerId = layer.get("value");
if (!layerId) return;
@@ -748,6 +732,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
return () => {
isCompareDisposingRef.current = true;
window.clearTimeout(resizeTimerId);
nextCompareMap.un("rendercomplete", handleCompareRenderComplete);
if (
compareDeckLayerRef.current &&
!compareDeckLayerRef.current.isDisposedLayer()
@@ -762,7 +747,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
compareDeckLayerRef.current = null;
setCompareDeckLayer(undefined);
setCompareMap(undefined);
disposeMapResources(nextCompareMap);
disposeMapResources(nextCompareMap, { disposeSources: false });
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCompareMode, map]);
@@ -1015,11 +1000,11 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
// 控制流动动画开关
useEffect(() => {
flowAnimation.current = pipeText === "flow" && currentPipeCalData.length > 0;
const hasFlowData = pipeText === "flow" && currentPipeCalData.length > 0;
const shouldShowWaterflow =
isWaterflowLayerAvailable &&
showWaterflowLayer &&
flowAnimation.current &&
hasFlowData &&
currentZoom >= 12 &&
currentZoom <= 24;
@@ -1027,13 +1012,13 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const syncWaterflowLayer = (
targetDeckLayer: DeckLayer | null,
targetPipeData: any[],
targetPipeFragments: any[],
disposing: boolean,
) => {
if (disposing || !targetDeckLayer || targetDeckLayer.isDisposedLayer()) {
return;
}
if (!shouldShowWaterflow || targetPipeData.length === 0) {
if (!shouldShowWaterflow || targetPipeFragments.length === 0) {
targetDeckLayer.removeDeckLayer("waterflowLayer");
return;
}
@@ -1045,7 +1030,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const waterflowLayer = new TripsLayer({
id: "waterflowLayer",
name: "水流",
data: targetPipeData,
data: targetPipeFragments,
getObjectId: (d: any) => d.instanceKey,
getPath: (d) => d.path,
getTimestamps: (d) => d.timestamps,
getColor: [0, 220, 255],
@@ -1067,13 +1053,13 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const animate = () => {
syncWaterflowLayer(
deckLayerRef.current,
mergedPipeData,
mergedPipeFragments,
isDisposingRef.current,
);
if (isCompareMode) {
syncWaterflowLayer(
compareDeckLayerRef.current,
mergedComparePipeData,
mergedComparePipeFragments,
isCompareDisposingRef.current,
);
}
@@ -1092,8 +1078,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
}, [
currentPipeCalData,
currentZoom,
mergedPipeData,
mergedComparePipeData,
mergedPipeFragments,
mergedComparePipeFragments,
isCompareMode,
pipeText,
isWaterflowLayerAvailable,