From 5cc7275186ea728c66f9d167cd530126e2cd6cd0 Mon Sep 17 00:00:00 2001 From: JIANG Date: Thu, 18 Dec 2025 16:03:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=95=B0=E6=8D=AE=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=87=BD=E6=95=B0=EF=BC=9B=E8=B0=83=E6=95=B4=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E8=BD=B4=E6=98=BE=E7=A4=BA=E8=8C=83=E5=9B=B4=E7=AC=A6?= =?UTF-8?q?=E5=90=88=E6=95=B0=E6=8D=AE=E8=BF=94=E5=9B=9E=E7=9A=84=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Timeline_health_risk_analysis.tsx | 204 +++++++++++------- 1 file changed, 127 insertions(+), 77 deletions(-) diff --git a/src/app/OlMap/Controls/Timeline_health_risk_analysis.tsx b/src/app/OlMap/Controls/Timeline_health_risk_analysis.tsx index 50d0d09..40c7367 100644 --- a/src/app/OlMap/Controls/Timeline_health_risk_analysis.tsx +++ b/src/app/OlMap/Controls/Timeline_health_risk_analysis.tsx @@ -4,7 +4,6 @@ import React, { useState, useEffect, useRef, useCallback } from "react"; import { useNotification } from "@refinedev/core"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; import { parseColor } from "@/utils/parseColor"; -import { calculateClassification } from "@/utils/breaks_classification"; import { Box, @@ -51,16 +50,16 @@ interface PredictionResult { // 彩虹色配置 const RAINBOW_COLORS = [ - "rgba(142, 68, 173, 1)", // 紫 - "rgba(63, 81, 181, 1)", // 靛青 - "rgba(33, 150, 243, 1)", // 天蓝 - "rgba(0, 188, 212, 1)", // 青色 - "rgba(0, 158, 115, 1)", // 青绿 - "rgba(76, 175, 80, 1)", // 中绿 - "rgba(199, 224, 0, 1)", // 黄绿 - "rgba(255, 215, 0, 1)", // 金黄 - "rgba(255, 127, 0, 1)", // 橙 - "rgba(255, 0, 0, 1)", // 红 + "rgba(142, 68, 173, 0.9)", // 紫 + "rgba(63, 81, 181, 0.9)", // 靛青 + "rgba(33, 150, 243, 0.9)", // 天蓝 + "rgba(0, 188, 212, 0.9)", // 青色 + "rgba(0, 158, 115, 0.9)", // 青绿 + "rgba(76, 175, 80, 0.9)", // 中绿 + "rgba(199, 224, 0, 0.9)", // 黄绿 + "rgba(255, 215, 0, 0.9)", // 金黄 + "rgba(255, 127, 0, 0.9)", // 橙 + "rgba(255, 0, 0, 0.9)", // 红 ]; interface TimelineProps { @@ -79,7 +78,7 @@ const Timeline: React.FC = ({ } const { open } = useNotification(); const [selectedDateTime, setSelectedDateTime] = useState(new Date()); - const [currentYear, setCurrentYear] = useState(1); // 时间轴当前值 (1-100) + const [currentYear, setCurrentYear] = useState(4); // 时间轴当前值 (4-73) const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(15000); // 毫秒 const [isPredicting, setIsPredicting] = useState(false); @@ -88,9 +87,12 @@ const Timeline: React.FC = ({ >([]); const [pipeLayer, setPipeLayer] = useState(null); - // 计算时间轴范围 (1-100) - const minTime = 1; - const maxTime = 100; + // 使用 ref 存储当前的健康数据,供事件监听器读取,避免重复绑定 + const healthDataRef = useRef>(new Map()); + + // 计算时间轴范围 (4-73) + const minTime = 4; + const maxTime = 73; const intervalRef = useRef(null); const timelineRef = useRef(null); @@ -98,10 +100,10 @@ const Timeline: React.FC = ({ // 添加防抖引用 const debounceRef = useRef(null); - // 时间刻度数组 (1-100,每10个单位一个刻度) - const valueMarks = Array.from({ length: 10 }, (_, i) => ({ - value: (i + 1) * 10, - label: `${(i + 1) * 10}`, + // 时间刻度数组 (4-73,每3个单位一个刻度) + const valueMarks = Array.from({ length: 24 }, (_, i) => ({ + value: 4 + i * 3, + label: `${4 + i * 3}`, })); // 播放时间间隔选项 @@ -256,34 +258,86 @@ const Timeline: React.FC = ({ clearTimeout(debounceRef.current); } }; - }, []); + }, [pipeLayer]); // 获取地图实例 const map = useMap(); - // 根据年份从 survival_function 中插值获取生存概率 + // 根据索引从 survival_function 中获取生存概率 const getSurvivalProbabilityAtYear = useCallback( - (survivalFunc: SurvivalFunction, year: number): number => { - const { x, y } = survivalFunc; - if (x.length === 0 || y.length === 0) return 1; + (survivalFunc: SurvivalFunction, index: number): number => { + const { y } = survivalFunc; + if (y.length === 0) return 1; - // 如果年份小于最小值,返回第一个概率 - if (year <= x[0]) return y[0]; - // 如果年份大于最大值,返回最后一个概率 - if (year >= x[x.length - 1]) return y[y.length - 1]; - - // 线性插值 - for (let i = 0; i < x.length - 1; i++) { - if (year >= x[i] && year <= x[i + 1]) { - const ratio = (year - x[i]) / (x[i + 1] - x[i]); - return y[i] + ratio * (y[i + 1] - y[i]); - } - } - return 1; + // 确保索引在范围内 + const safeIndex = Math.max(0, Math.min(index, y.length - 1)); + return y[safeIndex]; }, [] ); + // 更新管道图层中的 healthRisk 属性 + const updatePipeHealthData = useCallback( + (healthData: Map) => { + 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; + 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; + let i = 0; + renderFeatures.forEach((renderFeature: any) => { + const featureId = renderFeature.get("id"); + const value = healthData.get(featureId); + if (value !== undefined) { + renderFeature.properties_["healthRisk"] = value; + // 输出前10个点的信息以供调试 + if (i < 10) { + console.log( + `瓦片加载 - 设置特征 ${featureId} 的 healthRisk 为 ${value}` + ); + i++; + } + } + }); + }; + + source.on("tileloadend", listener); + + return () => { + source.un("tileloadend", listener); + }; + }, [pipeLayer]); + // 应用样式到管道图层 const applyPipeHealthStyle = useCallback(() => { if (!pipeLayer || predictionResults.length === 0) { @@ -291,38 +345,32 @@ const Timeline: React.FC = ({ } // 为每条管道计算当前年份的生存概率 - const pipeHealthData: { [key: string]: number } = {}; + const pipeHealthData = new Map(); predictionResults.forEach((result) => { const probability = getSurvivalProbabilityAtYear( result.survival_function, - currentYear + currentYear - 1 // 使用索引 (0-based) ); - pipeHealthData[result.link_id] = probability; + pipeHealthData.set(result.link_id, probability); }); - // 获取所有概率值用于分类 - const probabilities = Object.values(pipeHealthData); - if (probabilities.length === 0) return; - - // 使用优雅分段方法计算断点(10个分段) - const segments = 10; - const breaks = calculateClassification( - probabilities, - segments, - "pretty_breaks" + // 更新 ref 数据 + healthDataRef.current = pipeHealthData; + // 输出前 10 条数据以供调试 + console.log( + `更新健康数据,年份: ${currentYear}`, + Array.from(pipeHealthData.entries()).slice(0, 10) ); - // 确保包含最小值和最大值 - const minVal = Math.min(...probabilities); - const maxVal = Math.max(...probabilities); - if (!breaks.includes(minVal)) { - breaks.push(minVal); - breaks.sort((a, b) => a - b); - } - if (!breaks.includes(maxVal)) { - breaks.push(maxVal); - breaks.sort((a, b) => a - b); - } + // 更新图层数据 + updatePipeHealthData(pipeHealthData); + + // 获取所有概率值用于分类 + const probabilities = Array.from(pipeHealthData.values()); + if (probabilities.length === 0) return; + + // 使用等距分段,从0-1分为十类 + const breaks = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]; // 生成彩虹色(从紫色到红色,低生存概率=高风险=红色) const colors = RAINBOW_COLORS; @@ -361,7 +409,13 @@ const Timeline: React.FC = ({ console.log( `已应用健康风险样式,年份: ${currentYear}, 分段: ${breaks.length}` ); - }, [pipeLayer, predictionResults, currentYear, getSurvivalProbabilityAtYear]); + }, [ + pipeLayer, + predictionResults, + currentYear, + getSurvivalProbabilityAtYear, + updatePipeHealthData, + ]); // 初始化管道图层 useEffect(() => { @@ -375,24 +429,15 @@ const Timeline: React.FC = ({ if (pipesLayer) { setPipeLayer(pipesLayer); - console.log("管道图层已找到"); } }, [map]); - // 监听时间轴变化,更新样式 + // 监听依赖变化,更新样式 useEffect(() => { if (predictionResults.length > 0 && pipeLayer) { applyPipeHealthStyle(); } - }, [currentYear, predictionResults, pipeLayer, applyPipeHealthStyle]); - - // 监听预测结果变化,首次应用样式 - useEffect(() => { - if (predictionResults.length > 0 && pipeLayer) { - console.log("预测结果已更新,应用样式"); - applyPipeHealthStyle(); - } - }, [predictionResults, pipeLayer, applyPipeHealthStyle]); + }, [applyPipeHealthStyle]); // 这里防止地图缩放时,瓦片重新加载引起的属性更新出错 useEffect(() => { @@ -440,7 +485,7 @@ const Timeline: React.FC = ({ if (response.ok) { const results: PredictionResult[] = await response.json(); setPredictionResults(results); - console.log("预测结果:", results); + console.log("预测结果:", results[0], results[1], results.length); open?.({ type: "success", message: `模拟预测完成,获取到 ${results.length} 条管道数据`, @@ -613,15 +658,20 @@ const Timeline: React.FC = ({ color: "primary.main", }} > - 预测年份:{currentYear} + 预测年份: + {predictionResults.length > 0 && + predictionResults[0].survival_function.x[currentYear - 1] !== + undefined + ? predictionResults[0].survival_function.x[currentYear - 1] + : currentYear}