fix(map): stabilize tiled style rendering
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user