import React, { useRef, useEffect, useState, useCallback } from "react"; import vsSource from './glsl/timeline/vert.glsl'; import fsSource from './glsl/timeline/frag.glsl'; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { ChevronLeft, ChevronRight, HomeIcon, LockIcon, Pause, Play, RefreshCwIcon, UnlockIcon } from "lucide-react"; import { formatInTimeZone } from "date-fns-tz"; import { parse } from "date-fns" import { useTimeline } from "@/hooks/use-timeline"; import { Timeline as TimelineEngine, ZoomMode, TimelineConfig } from "@/lib/timeline"; import { Separator } from "@/components/ui/separator"; import { gql, useSubscription } from "@apollo/client"; const SUBSCRIPTION_QUERY = gql` subscription { statusUpdates { id message status timestamp newestDt } } ` interface Uniforms { startTimestamp: number; // Unix 时间戳开始 endTimestamp: number; // Unix 时间戳结束 currentTimestamp: number; // 当前时间戳 radius: number; d: number; timelineStartX: number; // 时间轴在屏幕上的开始X坐标 timelineEndX: number; // 时间轴在屏幕上的结束X坐标 viewportSize: [number, number]; zoomLevel: number; // 当前缩放级别 panOffset: number; // 当前平移偏移 } interface Instants { position: Float32Array; color: Float32Array; } interface VesicaDataPoint { timestamp: number; // Unix 时间戳 color?: [number, number, number, number]; // RGBA 颜色,默认为白色 } interface Props extends React.HTMLAttributes { boxSize?: [number, number]; startDate?: Date; endDate?: Date; currentDate?: Date; onDateChange?: (date: Date) => void; onPlay?: () => void; onPause?: () => void; minZoom?: number; // 最小缩放级别 maxZoom?: number; // 最大缩放级别 initialZoom?: number; // 初始缩放级别 vesicaData?: VesicaDataPoint[]; // vesica 实例数据 dateFormat?: (timestamp: number) => string; // 自定义时间格式化函数 timelineConfig?: TimelineConfig; // 时间轴配置 } export const Timeline: React.FC = React.memo(({ startDate, endDate, currentDate, onDateChange, onPlay, onPause, boxSize = [4, 8], minZoom = 0.5, maxZoom = 8, initialZoom = 1, vesicaData, dateFormat, timelineConfig, ...props }) => { const { isPlaying, togglePlay, currentDatetime, setTime, setTimelineTime, timelineDatetime } = useTimeline({}) const [lock, setLock] = useState(false) const canvasRef = useRef(null); const ticksCanvasRef = useRef(null); const timelineEngineRef = useRef(null); const [timeStep, setTimeStep] = useState(360000); const [viewportInfo, setViewportInfo] = useState({ centerTime: 0, timeRange: 0, visibleRange: [0, 0] as [number, number], currentLevel: null as any }); const { data, loading, error } = useSubscription(SUBSCRIPTION_QUERY) useEffect(() => { if (data) { if (data.statusUpdates) { if (!lock && data.statusUpdates.newestDt) { const newDt = parse(data.statusUpdates.newestDt + 'Z', 'yyyyMMddHHmmssX', new Date()) setTime(newDt) setTimelineTime(newDt) if (timelineEngineRef.current) { timelineEngineRef.current.replaceTimeMarkByTimestamp(newDt.getTime()) } } } else { } } }, [data, lock]) // 定时器效果 - 当播放时每隔指定时间执行操作 useEffect(() => { let intervalId: NodeJS.Timeout | null = null; if (isPlaying) { intervalId = setInterval(() => { // 执行时间前进操作 if (timelineEngineRef.current) { timelineEngineRef.current.playAndEnsureMarkInView(timeStep) } }, 1000); // 每秒执行一次,你可以根据需要调整这个间隔 } return () => { if (intervalId) { clearInterval(intervalId); } }; }, [isPlaying, timeStep]); useEffect(() => { if (!ticksCanvasRef.current) return; const canvas = ticksCanvasRef.current; // 计算初始时间范围 const now = Date.now(); const defaultStartTime = startDate ? startDate.getTime() : now - 24 * 60 * 60 * 1000; // 默认24小时前 const defaultEndTime = endDate ? endDate.getTime() : now + 24 * 60 * 60 * 1000; // 默认24小时后 const defaultCenterTime = currentDate ? currentDate.getTime() : now; // 合并配置 const config: TimelineConfig = { initialCenterTime: defaultCenterTime, initialTimeRange: (defaultEndTime - defaultStartTime) / 2, // 使用时间范围的一半作为初始范围 highlightWeekends: false, zoomMode: ZoomMode.MousePosition, zoomSensitivity: 0.001, colors: { background: 'transparent', // 使用透明背景,让父容器背景显示 grid: '#333333', majorTick: '#cccccc', minorTick: '#cccccc', primaryLabel: '#b4b4b4', secondaryLabel: '#b4b4b4', currentTime: '#ff0000' }, sizes: { majorTickHeight: 8, minorTickHeight: 4, labelOffset: 3, primaryFontSize: 10, secondaryFontSize: 10 }, onDateChange: async (date: Date) => { const datestr = formatInTimeZone(date, 'UTC', 'yyyyMMddHHmmss') const response = await fetch(`http://localhost:3050/api/v1/data/nearest?datetime=${datestr}&area=cn`) setTimelineTime(date) if (response.ok) { const data = await response.json() const nearestDatetime = data.nearest_data_time const nearestDate = new Date(Date.parse(nearestDatetime)) setTime(nearestDate) } else { console.error('Failed to fetch data:', response.status) } }, ...timelineConfig }; try { timelineEngineRef.current = new TimelineEngine(canvas, config); const updateViewportInfo = () => { if (timelineEngineRef.current) { const info = timelineEngineRef.current.getViewportInfo(); setViewportInfo(info); if (onDateChange) { onDateChange(new Date(info.centerTime)); } } }; const interval = setInterval(updateViewportInfo, 100); return () => { clearInterval(interval); if (timelineEngineRef.current) { timelineEngineRef.current.destroy(); timelineEngineRef.current = null; } }; } catch (error) { console.error('Failed to initialize timeline engine:', error); } }, [startDate, endDate, currentDate, initialZoom, timelineConfig, onDateChange]); // 处理画布大小变化 useEffect(() => { const handleResize = () => { if (timelineEngineRef.current && ticksCanvasRef.current) { timelineEngineRef.current.render(); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); const handleHome = useCallback(() => { if (timelineEngineRef.current) { timelineEngineRef.current.goToTime(new Date().getTime()) } }, []); // 控制按钮功能 const handlePrevious = useCallback(() => { if (timelineEngineRef.current) { timelineEngineRef.current.playBackwardAndEnsureMarkInView(timeStep) } }, [timeStep]); const handleNext = useCallback(() => { if (timelineEngineRef.current) { timelineEngineRef.current.playAndEnsureMarkInView(timeStep) } }, [timeStep]); const handleRefresh = useCallback(async () => { if (timelineEngineRef.current) { const date = new Date() const datestr = formatInTimeZone(date, 'UTC', 'yyyyMMddHHmmss') const response = await fetch(`http://localhost:3050/api/v1/data/nearest?datetime=${datestr}&area=cn`) if (response.ok) { const data = await response.json() const nearestDatetime = data.nearest_data_time const nearestDate = new Date(Date.parse(nearestDatetime)) setTime(nearestDate) timelineEngineRef.current.replaceTimeMarkByTimestamp(nearestDate.getTime()) timelineEngineRef.current.ensureMarkInView() } else { console.error('Failed to fetch data:', response.status) } } }, []); return (
); });