perf(map): reduce tile snapshot work during zoom
This commit is contained in:
@@ -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([]);
|
||||
|
||||
Reference in New Issue
Block a user