FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
SimulationView.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 React, { useState, useEffect, useRef } from 'react';
5import {
6 Box,
7 Typography,
8 Grid,
9 Card,
10 CardContent,
11 CardActions,
12 Button,
13 CircularProgress,
14 Fade,
15 LinearProgress,
16 List,
17 ListItem,
18 ListItemText,
19} from '@mui/material';
20import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
21import MapIcon from '@mui/icons-material/Map';
22import { useScenarioStore } from '@/stores/scenarioStore';
23import { invoke } from '@tauri-apps/api/core';
24import { save } from '@tauri-apps/plugin-dialog';
25import { listen } from '@tauri-apps/api/event';
26
27interface ProgressState {
28 message: string;
29 current: number;
30 total: number;
31}
32
33export const SimulationView = React.memo(function SimulationView() {
34 const isSimulating = useScenarioStore((state) => state.isSimulating);
35 const setIsSimulating = useScenarioStore((state) => state.setIsSimulating);
36 const showError = useScenarioStore((state) => state.showError);
37 const [isGeneratingKml, setIsGeneratingKml] = useState(false);
38
39 // Use a Ref to store incoming data to avoid triggering re-renders on every event
40 const progressRef = useRef<Record<string, ProgressState>>({});
41 // Local state to trigger actual re-renders throttled by rAF
42 const [displayProgress, setDisplayProgress] = useState<
43 Record<string, ProgressState>
44 >({});
45
46 useEffect(() => {
47 let animationFrameId: number;
48
49 // The update loop synchronizes the Ref data to the State at screen refresh rate
50 const updateLoop = () => {
51 if (useScenarioStore.getState().isSimulating) {
52 setDisplayProgress({ ...progressRef.current });
53 animationFrameId = requestAnimationFrame(updateLoop);
54 }
55 };
56
57 const unlistenSimComplete = listen<void>('simulation-complete', () => {
58 console.log('Simulation completed successfully.');
59 setIsSimulating(false);
60 progressRef.current = {};
61 setDisplayProgress({});
62 cancelAnimationFrame(animationFrameId);
63 });
64
65 const unlistenSimError = listen<string>('simulation-error', (event) => {
66 const errorMessage = `Simulation failed: ${event.payload}`;
67 console.error(errorMessage);
68 showError(errorMessage);
69 setIsSimulating(false);
70 progressRef.current = {};
71 setDisplayProgress({});
72 cancelAnimationFrame(animationFrameId);
73 });
74
75 const unlistenSimProgress = listen<ProgressState>(
76 'simulation-progress',
77 (event) => {
78 const { message } = event.payload;
79 let key = message;
80
81 // Grouping logic to keep the list clean
82 if (
83 message.startsWith('Simulating') ||
84 message.startsWith('Initializing')
85 ) {
86 key = 'main';
87 } else if (
88 message.startsWith('Finalizing') ||
89 message.startsWith('Exporting') ||
90 message.startsWith('Rendering') ||
91 message.startsWith('Applying') ||
92 message.startsWith('Writing') ||
93 message.startsWith('Finished')
94 ) {
95 // Extract component name from format "Action {Name}: ..." or "Action {Name}"
96 // e.g. "Exporting Receiver1: Chunk 50" -> key "Receiver1"
97 const parts = message.split(/[:\s]+/);
98 // Simple heuristic: 2nd word is often the name in our C++ format
99 if (parts.length >= 2) {
100 key = parts[1];
101 }
102 }
103
104 progressRef.current[key] = event.payload;
105 }
106 );
107
108 const unlistenKmlComplete = listen<string>(
109 'kml-generation-complete',
110 (event) => {
111 console.log('KML generated successfully at:', event.payload);
112 setIsGeneratingKml(false);
113 }
114 );
115
116 const unlistenKmlError = listen<string>(
117 'kml-generation-error',
118 (event) => {
119 const errorMessage = `KML generation failed: ${event.payload}`;
120 console.error(errorMessage);
121 showError(errorMessage);
122 setIsGeneratingKml(false);
123 }
124 );
125
126 // Start the UI update loop if we are simulating
127 if (isSimulating) {
128 updateLoop();
129 }
130
131 return () => {
132 cancelAnimationFrame(animationFrameId);
133 Promise.all([
134 unlistenSimComplete,
135 unlistenSimError,
136 unlistenSimProgress,
137 unlistenKmlComplete,
138 unlistenKmlError,
139 ]).then((unlisteners) => {
140 unlisteners.forEach((unlisten) => unlisten());
141 });
142 };
143 }, [isSimulating, setIsSimulating, showError]);
144
145 const handleRunSimulation = async () => {
146 progressRef.current = {};
147 setDisplayProgress({});
148 setIsSimulating(true);
149 try {
150 // Ensure the C++ backend has the latest scenario from the UI
151 await useScenarioStore.getState().syncBackend();
152 await invoke('run_simulation');
153 } catch (err) {
154 const errorMessage =
155 err instanceof Error ? err.message : String(err);
156 console.error('Failed to invoke simulation:', errorMessage);
157 showError(`Failed to start simulation: ${errorMessage}`);
158 setIsSimulating(false); // Stop on invocation failure
159 }
160 };
161
162 const handleGenerateKml = async () => {
163 try {
164 const outputPath = await save({
165 title: 'Save KML File',
166 filters: [{ name: 'KML File', extensions: ['kml'] }],
167 });
168
169 if (outputPath) {
170 setIsGeneratingKml(true);
171 // Ensure the C++ backend has the latest scenario from the UI
172 await useScenarioStore.getState().syncBackend();
173 await invoke('generate_kml', { outputPath });
174 }
175 } catch (err) {
176 const errorMessage =
177 err instanceof Error ? err.message : String(err);
178 console.error('Failed to invoke KML generation:', errorMessage);
179 showError(`Failed to start KML generation: ${errorMessage}`);
180 setIsGeneratingKml(false); // Stop on invocation failure
181 }
182 };
183
184 const mainProgress = displayProgress['main'];
185 const otherProgresses = Object.entries(displayProgress)
186 .filter(([key]) => key !== 'main')
187 .sort((a, b) => a[0].localeCompare(b[0]));
188
189 return (
190 <Box sx={{ p: 4, height: '100%', overflowY: 'auto' }}>
191 <Typography variant="h4" gutterBottom>
192 Simulation Runner
193 </Typography>
194 <Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
195 Execute the configured scenario or generate a geographical
196 visualization. Ensure your scenario is fully configured before
197 proceeding.
198 </Typography>
199
200 <Grid container spacing={4} sx={{ width: '100%' }}>
201 <Grid size={{ xs: 12, md: 6 }}>
202 <Card sx={{ height: '100%' }}>
203 <CardContent>
204 <Typography variant="h5" component="div">
205 Run Full Simulation
206 </Typography>
207 <Typography sx={{ mt: 1.5 }} color="text.secondary">
208 Executes the entire simulation based on the
209 current scenario settings. This is a
210 computationally intensive process that will
211 generate output files.
212 </Typography>
213 </CardContent>
214 <CardActions sx={{ p: 2 }}>
215 <Button
216 variant="contained"
217 size="large"
218 startIcon={
219 isSimulating ? (
220 <CircularProgress
221 size={24}
222 color="inherit"
223 />
224 ) : (
225 <PlayCircleOutlineIcon />
226 )
227 }
228 disabled={isSimulating || isGeneratingKml}
229 onClick={handleRunSimulation}
230 >
231 {isSimulating ? 'Running...' : 'Run Simulation'}
232 </Button>
233 </CardActions>
234 </Card>
235 </Grid>
236 <Grid size={{ xs: 12, md: 6 }}>
237 <Card sx={{ height: '100%' }}>
238 <CardContent>
239 <Typography variant="h5" component="div">
240 Generate KML
241 </Typography>
242 <Typography sx={{ mt: 1.5 }} color="text.secondary">
243 Creates a KML file from the scenario&apos;s
244 platform motion paths and antenna pointings.
245 This allows for quick visualization in
246 applications like Google Earth without running
247 the full signal-level simulation.
248 </Typography>
249 </CardContent>
250 <CardActions sx={{ p: 2 }}>
251 <Button
252 variant="outlined"
253 size="large"
254 startIcon={
255 isGeneratingKml ? (
256 <CircularProgress
257 size={24}
258 color="inherit"
259 />
260 ) : (
261 <MapIcon />
262 )
263 }
264 disabled={isSimulating || isGeneratingKml}
265 onClick={handleGenerateKml}
266 >
267 {isGeneratingKml
268 ? 'Generating...'
269 : 'Generate KML'}
270 </Button>
271 </CardActions>
272 </Card>
273 </Grid>
274 </Grid>
275
276 <Fade in={isSimulating}>
277 <Box
278 sx={{
279 mt: 4,
280 p: 2,
281 backgroundColor: 'action.hover',
282 borderRadius: 1,
283 }}
284 >
285 {/* Main Simulation Progress */}
286 <Typography
287 variant="h6"
288 sx={{ mb: 1, textAlign: 'center' }}
289 >
290 {mainProgress
291 ? mainProgress.message
292 : 'Preparing simulation...'}
293 </Typography>
294 {mainProgress && mainProgress.total > 0 && (
295 <Box
296 sx={{
297 display: 'flex',
298 alignItems: 'center',
299 mt: 2,
300 mb: 2,
301 }}
302 >
303 <Box sx={{ width: '100%', mr: 1 }}>
304 <LinearProgress
305 variant="determinate"
306 value={
307 (mainProgress.current /
308 mainProgress.total) *
309 100
310 }
311 />
312 </Box>
313 <Box sx={{ minWidth: 40 }}>
314 <Typography
315 variant="body2"
316 color="text.secondary"
317 >{`${Math.round(
318 (mainProgress.current /
319 mainProgress.total) *
320 100
321 )}%`}</Typography>
322 </Box>
323 </Box>
324 )}
325
326 {/* Finalizer Threads List */}
327 {otherProgresses.length > 0 && (
328 <Box
329 sx={{
330 mt: 2,
331 borderTop: 1,
332 borderColor: 'divider',
333 pt: 2,
334 }}
335 >
336 <Typography
337 variant="subtitle2"
338 color="text.secondary"
339 >
340 Exporting Data:
341 </Typography>
342 <List dense>
343 {otherProgresses.map(([key, prog]) => (
344 <ListItem key={key}>
345 <ListItemText
346 primary={prog.message}
347 secondary={
348 prog.total > 0 &&
349 prog.total !== 100
350 ? 'Processing...'
351 : ''
352 }
353 />
354 {prog.total > 0 &&
355 prog.total !== 100 && (
356 /* For chunks, we often don't know the exact total ahead of time,
357 so a determinate bar might jump, but passing a large number
358 or indeterminate looks better than flickering 100% */
359 <Box
360 sx={{ width: '20%', ml: 2 }}
361 >
362 <Typography
363 variant="caption"
364 color="text.secondary"
365 >
366 Chunk {prog.current}
367 </Typography>
368 </Box>
369 )}
370 {prog.total === 100 && (
371 <Box sx={{ width: '30%', ml: 2 }}>
372 <LinearProgress
373 variant="determinate"
374 value={prog.current}
375 />
376 </Box>
377 )}
378 </ListItem>
379 ))}
380 </List>
381 </Box>
382 )}
383 </Box>
384 </Fade>
385 </Box>
386 );
387});