"use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; import { useNotification } from "@refinedev/core"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; import { Box, Button, Slider, Typography, Paper, MenuItem, Select, FormControl, InputLabel, IconButton, Stack, Tooltip, } from "@mui/material"; import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import "dayjs/locale/zh-cn"; // 引入中文包 import dayjs from "dayjs"; import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material"; import { TbArrowBackUp, TbArrowForwardUp } from "react-icons/tb"; import { FiSkipBack, FiSkipForward } from "react-icons/fi"; import { useData } from "../../../app/OlMap/MapComponent"; import { config, NETWORK_NAME } from "@/config/config"; import { useMap } from "../../../app/OlMap/MapComponent"; import { useHealthRisk } from "./HealthRiskContext"; import { PredictionResult, SurvivalFunction, RAINBOW_COLORS, RISK_BREAKS, } from "./types"; // 辅助函数:将日期向下取整到最近的15分钟 const getRoundedDate = (date: Date): Date => { const minutes = date.getHours() * 60 + date.getMinutes(); const roundedMinutes = Math.floor(minutes / 15) * 15; const roundedDate = new Date(date); roundedDate.setHours( Math.floor(roundedMinutes / 60), roundedMinutes % 60, 0, 0, ); return roundedDate; }; interface TimelineProps { schemeDate?: Date; timeRange?: { start: Date; end: Date }; disableDateSelection?: boolean; schemeName?: string; } const Timeline: React.FC = ({ disableDateSelection = false, }) => { const data = useData(); if (!data) { return
Loading...
; // 或其他占位符 } const { open } = useNotification(); const { predictionResults, setPredictionResults, currentYear, setCurrentYear, } = useHealthRisk(); const [selectedDateTime, setSelectedDateTime] = useState(new Date()); const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(5000); // 毫秒 const [isPredicting, setIsPredicting] = useState(false); const [pipeLayer, setPipeLayer] = useState(null); // 使用 ref 存储当前的健康数据,供事件监听器读取,避免重复绑定 const healthDataRef = useRef>(new Map()); // 计算时间轴范围 (4-73) const minTime = 4; const maxTime = 73; const intervalRef = useRef(null); const timelineRef = useRef(null); // 添加防抖引用 const debounceRef = useRef(null); // 时间刻度数组 (4-73,每3个单位一个刻度) const valueMarks = Array.from({ length: 24 }, (_, i) => ({ value: 4 + i * 3, label: `${4 + i * 3}`, })); // 播放时间间隔选项 const intervalOptions = [ { value: 2000, label: "2秒" }, { value: 5000, label: "5秒" }, { value: 10000, label: "10秒" }, ]; // 处理时间轴滑动 const handleSliderChange = useCallback( (event: Event, newValue: number | number[]) => { const value = Array.isArray(newValue) ? newValue[0] : newValue; // 如果有时间范围限制,只允许在范围内拖动 if (value < minTime || value > maxTime) { return; } // 防抖设置currentYear,避免频繁触发数据获取 if (debounceRef.current) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { setCurrentYear(value); }, 500); // 500ms 防抖延迟 }, [minTime, maxTime], ); // 播放控制 const handlePlay = useCallback(() => { if (!isPlaying) { setIsPlaying(true); intervalRef.current = setInterval(() => { setCurrentYear((prev: number) => { let next = prev + 1; if (next > maxTime) next = minTime; return next; }); }, playInterval); } }, [isPlaying, playInterval]); const handlePause = useCallback(() => { setIsPlaying(false); if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); const handleStop = useCallback(() => { setIsPlaying(false); if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); // 步进控制 const handleDayStepBackward = useCallback(() => { setSelectedDateTime((prev: Date) => { const newDate = new Date(prev); newDate.setDate(newDate.getDate() - 1); return newDate; }); }, []); const handleDayStepForward = useCallback(() => { setSelectedDateTime((prev: Date) => { const newDate = new Date(prev); newDate.setDate(newDate.getDate() + 1); return newDate; }); }, []); const handleStepBackward = useCallback(() => { setCurrentYear((prev: number) => { let next = prev - 1; if (next < minTime) next += maxTime - minTime + 1; return next; }); }, [minTime, maxTime]); const handleStepForward = useCallback(() => { setCurrentYear((prev: number) => { let next = prev + 1; if (next > maxTime) next = minTime; return next; }); }, [minTime, maxTime]); // 日期时间选择处理 const handleDateTimeChange = useCallback((newDate: Date | null) => { if (newDate) { setSelectedDateTime(getRoundedDate(newDate)); } }, []); // 播放间隔改变处理 const handleIntervalChange = useCallback( (event: any) => { const newInterval = event.target.value; setPlayInterval(newInterval); // 如果正在播放,重新启动定时器 if (isPlaying && intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setCurrentYear((prev: number) => { let next = prev + 1; if (next > maxTime) next = minTime; return next; }); }, newInterval); } }, [isPlaying], ); // 组件加载时设置初始时间为当前时间的最近15分钟 useEffect(() => { setSelectedDateTime(getRoundedDate(new Date())); return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } if (debounceRef.current) { clearTimeout(debounceRef.current); } }; }, [pipeLayer]); // 获取地图实例 const map = useMap(); // 根据索引从 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) => { 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; 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); return () => { source.un("tileloadend", listener); }; }, [pipeLayer]); // 应用样式到管道图层 const applyPipeHealthStyle = useCallback(() => { if (!pipeLayer || predictionResults.length === 0) { return; } // 为每条管道计算当前年份的生存概率 const pipeHealthData = new Map(); predictionResults.forEach((result) => { const probability = getSurvivalProbabilityAtYear( result.survival_function, currentYear - 4, // 使用索引 (0-based) ); pipeHealthData.set(result.link_id, probability); }); // 更新 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 (!map) return; const layers = map.getLayers().getArray(); const pipesLayer = layers.find( (layer) => layer instanceof WebGLVectorTileLayer && layer.get("value") === "pipes", ) as WebGLVectorTileLayer | undefined; if (pipesLayer) { setPipeLayer(pipesLayer); } }, [map]); // 监听依赖变化,更新样式 useEffect(() => { if (predictionResults.length > 0 && pipeLayer) { applyPipeHealthStyle(); } }, [applyPipeHealthStyle]); // 这里防止地图缩放时,瓦片重新加载引起的属性更新出错 useEffect(() => { // 监听地图缩放事件,缩放时停止播放 if (map) { const onZoom = () => { handlePause(); }; map.getView().on("change:resolution", onZoom); // 清理事件监听 return () => { map.getView().un("change:resolution", onZoom); }; } }, [map, handlePause]); const handleSimulatePrediction = async () => { if (!NETWORK_NAME) { open?.({ type: "error", message: "管网名称缺失,无法进行模拟预测。", }); return; } // 提前提取日期和时间值,避免异步操作期间被拖动改变 const calculationDateTime = selectedDateTime; // 从日期时间选择器获取完整的日期时间,并转换为UTC时间 const query_time = calculationDateTime.toISOString().slice(0, 19) + "Z"; setIsPredicting(true); // 显示处理中的通知 open?.({ type: "progress", message: "正在模拟预测,请稍候...", undoableTimeout: 3, }); try { const response = await fetch( `${config.BACKEND_URL}/api/v1/composite/pipeline-health-prediction?query_time=${query_time}&network_name=${NETWORK_NAME}`, ); if (response.ok) { const results: PredictionResult[] = await response.json(); setPredictionResults(results); open?.({ type: "success", message: `模拟预测完成,获取到 ${results.length} 条管道数据`, }); } else { // 读取后端 HTTPException 返回的 detail 信息 const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.detail || "模拟预测失败"; open?.({ type: "error", message: errorMessage, }); } } catch (error: any) { console.error("Simulation prediction failed:", error); open?.({ type: "error", message: `模拟预测时发生错误: ${error.message || "未知错误"}`, }); } finally { setIsPredicting(false); } }; return (
{/* 控制按钮栏 */} {/* 日期时间选择器 */} value && handleDateTimeChange(value.toDate()) } format="YYYY-MM-DD HH:mm" views={["year", "month", "day", "hours", "minutes"]} minutesStep={15} sx={{ width: 200 }} slotProps={{ textField: { size: "small", }, }} maxDateTime={dayjs(new Date())} // 禁止选取未来的日期时间 disabled={disableDateSelection} ampm={false} /> {/* 播放控制按钮 */} {/* 播放间隔选择 */} 播放间隔 {isPlaying ? : } {/* 功能按钮 */} {/* 当前时间显示 */} 预测年份:{currentYear}
); }; export default Timeline;