FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
Timeline.tsx
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
3
4import { useEffect, useMemo, useRef } from 'react';
5import { Box, Typography, Slider, IconButton } from '@mui/material';
6import PlayArrowIcon from '@mui/icons-material/PlayArrow';
7import PauseIcon from '@mui/icons-material/Pause';
8import FastRewindIcon from '@mui/icons-material/FastRewind';
9import FastForwardIcon from '@mui/icons-material/FastForward';
10import { useScenarioStore } from '@/stores/scenarioStore';
11
12export default function Timeline() {
13 const {
14 globalParameters,
15 isPlaying,
16 currentTime,
17 togglePlayPause,
18 setCurrentTime,
19 targetPlaybackDuration,
20 } = useScenarioStore();
21
22 const animationFrameRef = useRef(0);
23 const lastTimeRef = useRef(0);
24
25 // Calculate dynamic values based on simulation duration and user settings.
26 const { simulationDuration, speedFactor, sliderStep, timeStep } =
27 useMemo(() => {
28 const duration = Math.max(
29 0,
30 globalParameters.end - globalParameters.start
31 );
32
33 let realPlaybackDuration: number;
34
35 if (targetPlaybackDuration !== null && targetPlaybackDuration > 0) {
36 realPlaybackDuration = targetPlaybackDuration;
37 } else {
38 if (duration > 0 && duration < 5) {
39 realPlaybackDuration = 5;
40 } else {
41 realPlaybackDuration = duration;
42 }
43 }
44
45 const factor =
46 realPlaybackDuration > 0 ? duration / realPlaybackDuration : 0;
47 const sStep = duration > 0 ? duration / 1000 : 0.01;
48 const tStep = duration > 0 ? duration / 100 : 0.01;
49
50 return {
51 simulationDuration: duration,
52 speedFactor: factor,
53 sliderStep: sStep,
54 timeStep: tStep,
55 };
56 }, [
57 globalParameters.start,
58 globalParameters.end,
59 targetPlaybackDuration,
60 ]);
61
62 // Effect to stop playback when the end is reached.
63 useEffect(() => {
64 if (isPlaying && currentTime >= globalParameters.end) {
65 togglePlayPause();
66 }
67 }, [isPlaying, currentTime, globalParameters.end, togglePlayPause]);
68
69 useEffect(() => {
70 const animate = (now: number) => {
71 const deltaTime = (now - lastTimeRef.current) / 1000;
72 lastTimeRef.current = now;
73
74 // Let the store handle clamping. The effect above will stop playback.
75 setCurrentTime((prevTime) => prevTime + deltaTime * speedFactor);
76
77 animationFrameRef.current = requestAnimationFrame(animate);
78 };
79
80 if (isPlaying && simulationDuration > 0) {
81 lastTimeRef.current = performance.now();
82 animationFrameRef.current = requestAnimationFrame(animate);
83 } else {
84 cancelAnimationFrame(animationFrameRef.current);
85 }
86
87 return () => cancelAnimationFrame(animationFrameRef.current);
88 }, [
89 isPlaying,
90 setCurrentTime,
91 simulationDuration,
92 speedFactor,
93 globalParameters.end,
94 ]);
95
96 const handleSliderChange = (_event: Event, newValue: number | number[]) => {
97 setCurrentTime(newValue as number);
98 };
99
100 return (
101 <Box
102 sx={{
103 display: 'flex',
104 alignItems: 'center',
105 gap: 2,
106 height: '100%',
107 px: 2,
108 overflow: 'hidden',
109 }}
110 >
111 <Box sx={{ flexShrink: 0 }}>
112 <IconButton
113 size="small"
114 onClick={() => setCurrentTime((t) => t - timeStep)}
115 >
116 <FastRewindIcon />
117 </IconButton>
118 <IconButton onClick={togglePlayPause}>
119 {isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
120 </IconButton>
121 <IconButton
122 size="small"
123 onClick={() => setCurrentTime((t) => t + timeStep)}
124 >
125 <FastForwardIcon />
126 </IconButton>
127 </Box>
128 <Typography
129 variant="caption"
130 sx={{ flexShrink: 0, width: '4em', textAlign: 'center' }}
131 >
132 {currentTime.toFixed(2)}s
133 </Typography>
134 <Slider
135 value={currentTime}
136 onChange={handleSliderChange}
137 step={sliderStep}
138 min={globalParameters.start}
139 max={globalParameters.end}
140 sx={{
141 mx: 1,
142 flexGrow: 1,
143 minWidth: 50,
144 // Disable transitions to ensure precise, immediate tracking
145 '& .MuiSlider-thumb, & .MuiSlider-track': {
146 transition: 'none',
147 },
148 }}
149 valueLabelDisplay="auto"
150 valueLabelFormat={(value) => `${value.toFixed(2)}s`}
151 />
152 <Typography
153 variant="caption"
154 sx={{ flexShrink: 0, width: '4em', textAlign: 'right' }}
155 >
156 {globalParameters.end.toFixed(2)}s
157 </Typography>
158 </Box>
159 );
160}