fix(map): stabilize tiled style rendering
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
||||
import type VectorTileSource from "ol/source/VectorTile";
|
||||
import type { FlatStyleLike } from "ol/style/flat";
|
||||
|
||||
import {
|
||||
Box,
|
||||
@@ -31,6 +33,10 @@ import { config, NETWORK_NAME } from "@/config/config";
|
||||
import { apiFetch } from "@/lib/apiFetch";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import { useTimelineTimeConfig } from "@components/olmap/core/Controls/useTimelineTimeConfig";
|
||||
import {
|
||||
VectorTileStyleSession,
|
||||
versionedPropertyCase,
|
||||
} from "@components/olmap/core/vectorTileStyleSession";
|
||||
import { useHealthRisk } from "./HealthRiskContext";
|
||||
import {
|
||||
PredictionResult,
|
||||
@@ -54,6 +60,50 @@ const getRoundedDate = (date: Date, stepMinutes: number): Date => {
|
||||
return roundedDate;
|
||||
};
|
||||
|
||||
const buildHealthRiskStyle = (
|
||||
propertyKey: string,
|
||||
versionKey: string,
|
||||
version: number,
|
||||
): FlatStyleLike => {
|
||||
const colorCases: any[] = [];
|
||||
const widthCases: any[] = [];
|
||||
RISK_BREAKS.forEach((breakValue, index) => {
|
||||
colorCases.push(
|
||||
["<=", ["get", propertyKey], breakValue],
|
||||
RAINBOW_COLORS[index],
|
||||
);
|
||||
widthCases.push(
|
||||
["<=", ["get", propertyKey], breakValue],
|
||||
2 + (1 - index / (RISK_BREAKS.length - 1)) * 4,
|
||||
);
|
||||
});
|
||||
return {
|
||||
"stroke-color": versionedPropertyCase(
|
||||
propertyKey,
|
||||
versionKey,
|
||||
version,
|
||||
colorCases,
|
||||
"rgba(128, 128, 128, 1)",
|
||||
),
|
||||
"stroke-width": versionedPropertyCase(
|
||||
propertyKey,
|
||||
versionKey,
|
||||
version,
|
||||
widthCases,
|
||||
2,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const getSurvivalProbabilityAtYear = (
|
||||
survivalFunction: SurvivalFunction,
|
||||
index: number,
|
||||
) => {
|
||||
const values = survivalFunction.y;
|
||||
if (values.length === 0) return 1;
|
||||
return values[Math.max(0, Math.min(index, values.length - 1))];
|
||||
};
|
||||
|
||||
interface TimelineProps {
|
||||
schemeDate?: Date;
|
||||
timeRange?: { start: Date; end: Date };
|
||||
@@ -93,10 +143,10 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒
|
||||
const [isPredicting, setIsPredicting] = useState<boolean>(false);
|
||||
const [sliderPreviewYear, setSliderPreviewYear] = useState<number | null>(null);
|
||||
const { stepMinutes } = useTimelineTimeConfig();
|
||||
|
||||
// 使用 ref 存储当前的健康数据,供事件监听器读取,避免重复绑定
|
||||
const healthDataRef = useRef<Map<string, number>>(new Map());
|
||||
const healthStyleSessionRef = useRef<VectorTileStyleSession | null>(null);
|
||||
|
||||
// 计算时间轴范围 (4-73)
|
||||
const minTime = 4;
|
||||
@@ -105,9 +155,6 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 添加防抖引用
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 时间刻度数组 (4-73,每3个单位一个刻度)
|
||||
const valueMarks = Array.from({ length: 24 }, (_, i) => ({
|
||||
value: 4 + i * 3,
|
||||
@@ -129,15 +176,18 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
if (value < minTime || value > maxTime) {
|
||||
return;
|
||||
}
|
||||
// 防抖设置currentYear,避免频繁触发数据获取
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setCurrentYear(value);
|
||||
}, 500); // 500ms 防抖延迟
|
||||
setSliderPreviewYear(value);
|
||||
},
|
||||
[minTime, maxTime, setCurrentYear],
|
||||
[minTime, maxTime],
|
||||
);
|
||||
|
||||
const handleSliderChangeCommitted = useCallback(
|
||||
(_event: Event | React.SyntheticEvent, newValue: number | number[]) => {
|
||||
const value = Array.isArray(newValue) ? newValue[0] : newValue;
|
||||
setSliderPreviewYear(null);
|
||||
setCurrentYear(value);
|
||||
},
|
||||
[setCurrentYear],
|
||||
);
|
||||
|
||||
// 播放控制
|
||||
@@ -241,9 +291,6 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
};
|
||||
}, [stepMinutes]);
|
||||
|
||||
@@ -261,143 +308,50 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
) ?? null;
|
||||
}, [map]);
|
||||
|
||||
// 根据索引从 survival_function 中获取生存概率
|
||||
const getSurvivalProbabilityAtYear = useCallback(
|
||||
(survivalFunc: SurvivalFunction, index: number): number => {
|
||||
const { y } = survivalFunc;
|
||||
if (y.length === 0) return 1;
|
||||
|
||||
// 确保索引在范围内
|
||||
const safeIndex = Math.max(0, Math.min(index, y.length - 1));
|
||||
return y[safeIndex];
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 更新管道图层中的 healthRisk 属性
|
||||
const updatePipeHealthData = useCallback(
|
||||
(healthData: Map<string, number>) => {
|
||||
if (!pipeLayer) return;
|
||||
const source = pipeLayer.getSource() as any;
|
||||
if (!source) return;
|
||||
|
||||
const sourceTiles = source.sourceTiles_;
|
||||
if (!sourceTiles) return;
|
||||
|
||||
Object.values(sourceTiles).forEach((vectorTile: any) => {
|
||||
const renderFeatures = vectorTile.getFeatures();
|
||||
if (!renderFeatures || renderFeatures.length === 0) return;
|
||||
renderFeatures.forEach((renderFeature: any) => {
|
||||
const featureId = renderFeature.get("id");
|
||||
const value = healthData.get(featureId);
|
||||
if (value !== undefined) {
|
||||
renderFeature.properties_["healthRisk"] = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
[pipeLayer],
|
||||
);
|
||||
|
||||
// 监听瓦片加载,为新瓦片设置 healthRisk 属性
|
||||
// 只在 pipeLayer 变化时绑定一次,通过 ref 获取最新数据
|
||||
useEffect(() => {
|
||||
if (!pipeLayer) return;
|
||||
const source = pipeLayer.getSource() as any;
|
||||
const source = pipeLayer.getSource() as VectorTileSource | null;
|
||||
if (!source) return;
|
||||
|
||||
const listener = (event: any) => {
|
||||
const vectorTile = event.tile;
|
||||
const renderFeatures = vectorTile.getFeatures();
|
||||
if (!renderFeatures || renderFeatures.length === 0) return;
|
||||
|
||||
const healthData = healthDataRef.current;
|
||||
renderFeatures.forEach((renderFeature: any) => {
|
||||
const featureId = renderFeature.get("id");
|
||||
const value = healthData.get(featureId);
|
||||
if (value !== undefined) {
|
||||
renderFeature.properties_["healthRisk"] = value;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
source.on("tileloadend", listener);
|
||||
const defaultFlatStyle = config.MAP_DEFAULT_STYLE as FlatStyleLike;
|
||||
healthStyleSessionRef.current?.dispose();
|
||||
healthStyleSessionRef.current = new VectorTileStyleSession({
|
||||
layer: pipeLayer,
|
||||
source,
|
||||
propertyKey: "healthRisk",
|
||||
defaultStyle: defaultFlatStyle,
|
||||
buildStyle: buildHealthRiskStyle,
|
||||
map,
|
||||
buffered: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
source.un("tileloadend", listener);
|
||||
healthStyleSessionRef.current?.dispose();
|
||||
healthStyleSessionRef.current = null;
|
||||
pipeLayer.setStyle(defaultFlatStyle);
|
||||
};
|
||||
}, [pipeLayer]);
|
||||
}, [map, pipeLayer]);
|
||||
|
||||
// 应用样式到管道图层
|
||||
const applyPipeHealthStyle = useCallback(() => {
|
||||
if (!pipeLayer || predictionResults.length === 0) {
|
||||
useEffect(() => {
|
||||
const session = healthStyleSessionRef.current;
|
||||
if (!session) return;
|
||||
if (predictionResults.length === 0) {
|
||||
session.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// 为每条管道计算当前年份的生存概率
|
||||
const pipeHealthData = new Map<string, number>();
|
||||
predictionResults.forEach((result) => {
|
||||
const probability = getSurvivalProbabilityAtYear(
|
||||
result.survival_function,
|
||||
currentYear - 4, // 使用索引 (0-based)
|
||||
currentYear - 4,
|
||||
);
|
||||
pipeHealthData.set(result.link_id, probability);
|
||||
});
|
||||
session.commit(pipeHealthData);
|
||||
}, [currentYear, pipeLayer, predictionResults]);
|
||||
|
||||
// 更新 ref 数据
|
||||
healthDataRef.current = pipeHealthData;
|
||||
|
||||
// 更新图层数据
|
||||
updatePipeHealthData(pipeHealthData);
|
||||
|
||||
// 获取所有概率值用于分类
|
||||
const probabilities = Array.from(pipeHealthData.values());
|
||||
if (probabilities.length === 0) return;
|
||||
|
||||
// 使用等距分段,从0-1分为十类
|
||||
const breaks = RISK_BREAKS;
|
||||
|
||||
// 生成彩虹色(从紫色到红色,低生存概率=高风险=红色)
|
||||
const colors = RAINBOW_COLORS;
|
||||
|
||||
// 构建 WebGL 样式表达式
|
||||
const colorCases: any[] = [];
|
||||
const widthCases: any[] = [];
|
||||
|
||||
breaks.forEach((breakValue, index) => {
|
||||
const colorStr = colors[index];
|
||||
// 线宽根据健康风险调整:低生存概率(高风险)用粗线
|
||||
const width = 2 + (1 - index / (breaks.length - 1)) * 4;
|
||||
|
||||
colorCases.push(["<=", ["get", "healthRisk"], breakValue], colorStr);
|
||||
widthCases.push(["<=", ["get", "healthRisk"], breakValue], width);
|
||||
});
|
||||
// console.log(
|
||||
// `应用健康风险样式,年份: ${currentYear}, 分段: ${breaks.length}`,
|
||||
// );
|
||||
// console.log("颜色表达式:", colorCases);
|
||||
// console.log("宽度表达式:", widthCases);
|
||||
// 应用样式到图层
|
||||
pipeLayer.setStyle({
|
||||
"stroke-color": ["case", ...colorCases, "rgba(128, 128, 128, 1)"],
|
||||
"stroke-width": ["case", ...widthCases, 2],
|
||||
});
|
||||
}, [
|
||||
pipeLayer,
|
||||
predictionResults,
|
||||
currentYear,
|
||||
getSurvivalProbabilityAtYear,
|
||||
updatePipeHealthData,
|
||||
]);
|
||||
|
||||
// 监听依赖变化,更新样式
|
||||
useEffect(() => {
|
||||
if (predictionResults.length > 0 && pipeLayer) {
|
||||
applyPipeHealthStyle();
|
||||
}
|
||||
}, [applyPipeHealthStyle, pipeLayer, predictionResults.length]);
|
||||
|
||||
// 这里防止地图缩放时,瓦片重新加载引起的属性更新出错
|
||||
// 缩放期间暂停时间轴,避免视图变化与自动播放同时推进。
|
||||
useEffect(() => {
|
||||
// 监听地图缩放事件,缩放时停止播放
|
||||
if (map) {
|
||||
@@ -640,12 +594,13 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
|
||||
<Box ref={timelineRef} sx={{ px: 2, position: "relative" }}>
|
||||
<Slider
|
||||
value={currentYear}
|
||||
value={sliderPreviewYear ?? currentYear}
|
||||
min={minTime}
|
||||
max={maxTime} // 4-73的范围
|
||||
step={1} // 每1个单位一个步进
|
||||
marks={valueMarks} // 显示刻度
|
||||
onChange={handleSliderChange}
|
||||
onChangeCommitted={handleSliderChangeCommitted}
|
||||
valueLabelDisplay="auto"
|
||||
sx={{
|
||||
zIndex: 10,
|
||||
|
||||
Reference in New Issue
Block a user