perf(map): reduce tile snapshot work during zoom

This commit is contained in:
2026-07-30 12:50:33 +08:00
parent 82e75c03d0
commit 1b2a7f4fb8
5 changed files with 592 additions and 161 deletions
+174 -152
View File
@@ -18,7 +18,6 @@ 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";
@@ -39,11 +38,15 @@ import { getRoundedCurrentTimelineMinutes } from "./Controls/timelineTime";
import { useTimelineTimeConfig } from "./Controls/useTimelineTimeConfig";
import {
TileFeatureIndex,
clipLineStringPartsToExtent,
coordinatesToLonLat,
buildPipeFeatureFragments,
lineStringFromFlatCoordinates,
type PipeFeatureFragment,
type TileFeatureInstance,
} from "./tileFeatureIndex";
import {
createTileSnapshotScheduler,
type TileSnapshotScheduler,
} from "./tileSnapshotScheduler";
interface MapComponentProps {
children?: React.ReactNode;
@@ -126,36 +129,6 @@ interface DataContextType {
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]));
@@ -234,6 +207,11 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const compareCanvasRef = useRef<HTMLCanvasElement | null>(null);
const deckLayerRef = useRef<DeckLayer | null>(null);
const compareDeckLayerRef = useRef<DeckLayer | null>(null);
const tileSnapshotSchedulerRef = useRef<TileSnapshotScheduler | null>(null);
const publishedSnapshotKeyRef = useRef("");
const pipeFragmentCacheRef = useRef(
new WeakMap<TileFeatureInstance, PipeFeatureFragment[]>(),
);
const isDisposingRef = useRef(false);
const isCompareDisposingRef = useRef(false);
@@ -279,6 +257,30 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
useState(false); // 控制等高线图层显示
const [showWaterflowLayer, setShowWaterflowLayer] = useState(false); // 控制等高线图层显示
const [currentZoom, setCurrentZoom] = useState(11); // 当前缩放级别
const overlayRequirementsRef = useRef({
junctionData: false,
pipeLabels: false,
pipeFragments: false,
});
overlayRequirementsRef.current = {
junctionData:
currentZoom >= 11 &&
currentZoom <= 24 &&
(showContourLayer ||
(currentZoom >= 15 &&
(showJunctionTextLayer || showJunctionId))),
pipeLabels:
currentZoom >= 15 &&
currentZoom <= 24 &&
(showPipeTextLayer || showPipeId),
pipeFragments:
currentZoom >= 12 &&
currentZoom <= 24 &&
isWaterflowLayerAvailable &&
showWaterflowLayer &&
pipeText === "flow" &&
currentPipeCalData.length > 0,
};
// 实时合并计算结果到基础地理数据中
const mergedJunctionData = useMemo(
@@ -346,54 +348,12 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
[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 getPipeFragments = useCallback((instance: TileFeatureInstance) => {
const cached = pipeFragmentCacheRef.current.get(instance);
if (cached) return cached;
const fragments = buildPipeFeatureFragments(instance);
pipeFragmentCacheRef.current.set(instance, fragments);
return fragments;
}, []);
const publishActiveTileSnapshot = useCallback(
@@ -404,60 +364,113 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
zoom,
);
const pipeSnapshot = pipeIndexRef.current?.getSnapshot(targetMap, zoom);
if (!junctionSnapshot || !pipeSnapshot) return;
const nextJunctionData = Array.from(
junctionSnapshot?.instancesById.values() ?? [],
const requirements = overlayRequirementsRef.current;
const requirementSignature = [
requirements.junctionData ? 1 : 0,
requirements.pipeLabels ? 1 : 0,
requirements.pipeFragments ? 1 : 0,
].join("");
const snapshotKey = `${junctionSnapshot.signature}::${pipeSnapshot.signature}::${requirementSignature}`;
if (publishedSnapshotKeyRef.current === snapshotKey) return;
const junctionRepresentatives = 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)));
.filter(Boolean);
const pipeRepresentatives = Array.from(
pipeSnapshot.instancesById.values(),
)
.map((instances) => instances[0])
.filter(Boolean);
const nextPipeFragments = (pipeSnapshot?.instances ?? [])
.filter((instance) => instance.geometryType.includes("Line"))
.flatMap(buildPipeFragments)
.sort((a, b) => a.instanceKey.localeCompare(b.instanceKey));
const nextJunctionData = requirements.junctionData
? junctionRepresentatives
.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 preparedPipeFragments =
requirements.pipeLabels || requirements.pipeFragments
? pipeSnapshot.instances
.filter((instance) => instance.geometryType.includes("Line"))
.flatMap(getPipeFragments)
.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)),
);
if (requirements.pipeLabels) {
preparedPipeFragments.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 = requirements.pipeLabels
? Array.from(labelById.values()).sort((a, b) =>
String(a.id).localeCompare(String(b.id)),
)
: [];
const nextPipeFragments = requirements.pipeFragments
? preparedPipeFragments
: [];
publishedSnapshotKeyRef.current = snapshotKey;
setJunctionDataState(nextJunctionData);
setPipeFragments(nextPipeFragments);
setPipeDataState(nextPipeLabels);
setElevationRange(
getNumericRange(nextJunctionData.map((item) => item.elevation)),
getNumericRange(
junctionRepresentatives.map(
(instance) => instance.properties.elevation || 0,
),
),
);
setDiameterRange(
getNumericRange(nextPipeLabels.map((item) => item.diameter)),
getNumericRange(
pipeRepresentatives.map(
(instance) => instance.properties.diameter || 0,
),
),
);
},
[buildPipeFragments],
[getPipeFragments],
);
useEffect(() => {
tileSnapshotSchedulerRef.current?.markDirty();
}, [
currentZoom,
currentPipeCalData.length,
isWaterflowLayerAvailable,
pipeText,
showContourLayer,
showJunctionId,
showJunctionTextLayer,
showPipeId,
showPipeTextLayer,
showWaterflowLayer,
]);
const operationalSources = useMemo(
() =>
createOperationalMapSources({
@@ -497,7 +510,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
if (isDisposingRef.current) return;
try {
junctionIndexRef.current?.registerTile(event.tile);
scheduleActiveTileSnapshot();
tileSnapshotScheduler.markDirty();
} catch (error) {
console.error("Junction tile load error:", error);
}
@@ -506,7 +519,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
if (isDisposingRef.current) return;
try {
pipeIndexRef.current?.registerTile(event.tile);
scheduleActiveTileSnapshot();
tileSnapshotScheduler.markDirty();
} catch (error) {
console.error("Pipe tile load error:", error);
}
@@ -534,13 +547,40 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
layers: operationalResources.orderedLayers.slice(),
controls: [],
});
const scheduleActiveTileSnapshot = debounce(
() => publishActiveTileSnapshot(map),
50,
);
const tileSnapshotScheduler = createTileSnapshotScheduler({
publish: () => publishActiveTileSnapshot(map),
wait: 100,
});
tileSnapshotSchedulerRef.current = tileSnapshotScheduler;
junctionSource.on("tileloadend", handleJunctionTileLoadEnd);
pipeSource.on("tileloadend", handlePipeTileLoadEnd);
map.getInteractions().forEach(markMapResourcePersistent);
// 缩放或平移期间只登记瓦片,视图稳定后统一生成 Deck 数据。
const handleMoveStart = () => {
tileSnapshotScheduler.beginMove();
};
const handleMoveEnd = () => {
if (isDisposingRef.current) return;
const view = map.getView();
const zoom = view.getZoom() || 0;
setCurrentZoom(zoom);
junctionIndexRef.current?.scanLoadedTiles();
pipeIndexRef.current?.scanLoadedTiles();
tileSnapshotScheduler.endMove();
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);
}
};
map.on("movestart", handleMoveStart);
map.on("moveend", handleMoveEnd);
setMap(map);
// 恢复上次视图;如果没有则适配 MAP_EXTENT
@@ -577,29 +617,6 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
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;
@@ -607,7 +624,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
setCurrentZoom(initialZoom);
junctionIndexRef.current?.scanLoadedTiles();
pipeIndexRef.current?.scanLoadedTiles();
scheduleActiveTileSnapshot();
tileSnapshotScheduler.markDirty();
// 强制触发地图渲染,让瓦片加载事件触发
map.render();
}, 100);
@@ -637,11 +654,14 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
return () => {
isDisposingRef.current = true;
window.clearTimeout(initializeTimer);
scheduleActiveTileSnapshot.cancel();
handleViewChange.cancel();
tileSnapshotScheduler.cancel();
if (tileSnapshotSchedulerRef.current === tileSnapshotScheduler) {
tileSnapshotSchedulerRef.current = null;
}
junctionSource.un("tileloadend", handleJunctionTileLoadEnd);
pipeSource.un("tileloadend", handlePipeTileLoadEnd);
map.getView().un("change", handleViewChange);
map.un("movestart", handleMoveStart);
map.un("moveend", handleMoveEnd);
junctionsLayer.un("change:visible", handleJunctionVisibilityChange);
pipesLayer.un("change:visible", handlePipeVisibilityChange);
if (deckLayerRef.current && !deckLayerRef.current.isDisposedLayer()) {
@@ -659,6 +679,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
disposeMapResources(map, { disposeLayers: false });
junctionIndexRef.current = null;
pipeIndexRef.current = null;
publishedSnapshotKeyRef.current = "";
pipeFragmentCacheRef.current = new WeakMap();
setJunctionDataState([]);
setPipeDataState([]);
setPipeFragments([]);