1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import React, { useState, useEffect, useRef } from 'react';
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';
27interface ProgressState {
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);
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>
47 let animationFrameId: number;
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);
57 const unlistenSimComplete = listen<void>('simulation-complete', () => {
58 console.log('Simulation completed successfully.');
59 setIsSimulating(false);
60 progressRef.current = {};
61 setDisplayProgress({});
62 cancelAnimationFrame(animationFrameId);
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);
75 const unlistenSimProgress = listen<ProgressState>(
76 'simulation-progress',
78 const { message } = event.payload;
81 // Grouping logic to keep the list clean
83 message.startsWith('Simulating') ||
84 message.startsWith('Initializing')
88 message.startsWith('Finalizing') ||
89 message.startsWith('Exporting') ||
90 message.startsWith('Rendering') ||
91 message.startsWith('Applying') ||
92 message.startsWith('Writing') ||
93 message.startsWith('Finished')
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) {
104 progressRef.current[key] = event.payload;
108 const unlistenKmlComplete = listen<string>(
109 'kml-generation-complete',
111 console.log('KML generated successfully at:', event.payload);
112 setIsGeneratingKml(false);
116 const unlistenKmlError = listen<string>(
117 'kml-generation-error',
119 const errorMessage = `KML generation failed: ${event.payload}`;
120 console.error(errorMessage);
121 showError(errorMessage);
122 setIsGeneratingKml(false);
126 // Start the UI update loop if we are simulating
132 cancelAnimationFrame(animationFrameId);
139 ]).then((unlisteners) => {
140 unlisteners.forEach((unlisten) => unlisten());
143 }, [isSimulating, setIsSimulating, showError]);
145 const handleRunSimulation = async () => {
146 progressRef.current = {};
147 setDisplayProgress({});
148 setIsSimulating(true);
150 // Ensure the C++ backend has the latest scenario from the UI
151 await useScenarioStore.getState().syncBackend();
152 await invoke('run_simulation');
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
162 const handleGenerateKml = async () => {
164 const outputPath = await save({
165 title: 'Save KML File',
166 filters: [{ name: 'KML File', extensions: ['kml'] }],
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 });
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
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]));
190 <Box sx={{ p: 4, height: '100%', overflowY: 'auto' }}>
191 <Typography variant="h4" gutterBottom>
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
200 <Grid container spacing={4} sx={{ width: '100%' }}>
201 <Grid size={{ xs: 12, md: 6 }}>
202 <Card sx={{ height: '100%' }}>
204 <Typography variant="h5" component="div">
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.
214 <CardActions sx={{ p: 2 }}>
225 <PlayCircleOutlineIcon />
228 disabled={isSimulating || isGeneratingKml}
229 onClick={handleRunSimulation}
231 {isSimulating ? 'Running...' : 'Run Simulation'}
236 <Grid size={{ xs: 12, md: 6 }}>
237 <Card sx={{ height: '100%' }}>
239 <Typography variant="h5" component="div">
242 <Typography sx={{ mt: 1.5 }} color="text.secondary">
243 Creates a KML file from the scenario'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.
250 <CardActions sx={{ p: 2 }}>
264 disabled={isSimulating || isGeneratingKml}
265 onClick={handleGenerateKml}
276 <Fade in={isSimulating}>
281 backgroundColor: 'action.hover',
285 {/* Main Simulation Progress */}
288 sx={{ mb: 1, textAlign: 'center' }}
291 ? mainProgress.message
292 : 'Preparing simulation...'}
294 {mainProgress && mainProgress.total > 0 && (
298 alignItems: 'center',
303 <Box sx={{ width: '100%', mr: 1 }}>
305 variant="determinate"
307 (mainProgress.current /
308 mainProgress.total) *
313 <Box sx={{ minWidth: 40 }}>
316 color="text.secondary"
318 (mainProgress.current /
319 mainProgress.total) *
326 {/* Finalizer Threads List */}
327 {otherProgresses.length > 0 && (
332 borderColor: 'divider',
338 color="text.secondary"
343 {otherProgresses.map(([key, prog]) => (
346 primary={prog.message}
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% */
360 sx={{ width: '20%', ml: 2 }}
364 color="text.secondary"
370 {prog.total === 100 && (
371 <Box sx={{ width: '30%', ml: 2 }}>
373 variant="determinate"