FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
PlatformComponentInspector.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 {
5 Box,
6 Button,
7 Checkbox,
8 FormControl,
9 FormControlLabel,
10 IconButton,
11 InputLabel,
12 MenuItem,
13 Select,
14 TextField,
15 Typography,
16} from '@mui/material';
17import DeleteIcon from '@mui/icons-material/Delete';
18import {
19 useScenarioStore,
20 PlatformComponent,
21 TargetComponent,
22 MonostaticComponent,
23 TransmitterComponent,
24 ReceiverComponent,
25 SchedulePeriod,
26} from '@/stores/scenarioStore';
27import { NumberField, FileInput, Section } from './InspectorControls';
28
29interface PlatformComponentInspectorProps {
30 component: PlatformComponent;
31 platformId: string;
32 index: number;
33}
34
35export function PlatformComponentInspector({
36 component,
37 platformId,
38 index,
39}: PlatformComponentInspectorProps) {
40 const { updateItem, waveforms, timings, antennas, setPlatformRcsModel } =
41 useScenarioStore.getState();
42
43 // Updates are targeted using the array index in the path string
44 const handleChange = (path: string, value: unknown) =>
45 updateItem(platformId, `components.${index}.${path}`, value);
46
47 const renderSchedule = (
48 c: MonostaticComponent | TransmitterComponent | ReceiverComponent
49 ) => {
50 const schedule = c.schedule || [];
51
52 const handleAddPeriod = () => {
53 handleChange('schedule', [...schedule, { start: 0, end: 0 }]);
54 };
55
56 const handleRemovePeriod = (idx: number) => {
57 const newSchedule = [...schedule];
58 newSchedule.splice(idx, 1);
59 handleChange('schedule', newSchedule);
60 };
61
62 const handlePeriodChange = (
63 idx: number,
64 field: keyof SchedulePeriod,
65 val: number | null
66 ) => {
67 const newSchedule = [...schedule];
68 newSchedule[idx] = { ...newSchedule[idx], [field]: val ?? 0 };
69 handleChange('schedule', newSchedule);
70 };
71
72 return (
73 <Section title="Operating Schedule">
74 {schedule.length === 0 && (
75 <Typography variant="body2" color="text.secondary">
76 No specific schedule defined (always active).
77 </Typography>
78 )}
79 {schedule.map((period, i) => (
80 <Box
81 key={i}
82 sx={{
83 display: 'flex',
84 alignItems: 'center',
85 gap: 1,
86 p: 1,
87 border: 1,
88 borderColor: 'divider',
89 borderRadius: 1,
90 }}
91 >
92 <NumberField
93 label="Start (s)"
94 value={period.start}
95 onChange={(v) => handlePeriodChange(i, 'start', v)}
96 />
97 <NumberField
98 label="End (s)"
99 value={period.end}
100 onChange={(v) => handlePeriodChange(i, 'end', v)}
101 />
102 <IconButton
103 size="small"
104 onClick={() => handleRemovePeriod(i)}
105 color="error"
106 >
107 <DeleteIcon fontSize="small" />
108 </IconButton>
109 </Box>
110 ))}
111 <Button
112 onClick={handleAddPeriod}
113 size="small"
114 variant="outlined"
115 sx={{ mt: 1 }}
116 >
117 Add Schedule Period
118 </Button>
119 </Section>
120 );
121 };
122
123 const renderCommonRadarFields = (
124 c: MonostaticComponent | TransmitterComponent | ReceiverComponent
125 ) => (
126 <>
127 <TextField
128 label="Component Name"
129 size="small"
130 fullWidth
131 value={c.name}
132 onChange={(e) => handleChange('name', e.target.value)}
133 sx={{ mb: 2 }}
134 />
135 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
136 <InputLabel>Radar Mode</InputLabel>
137 <Select
138 label="Radar Mode"
139 value={c.radarType}
140 onChange={(e) => handleChange('radarType', e.target.value)}
141 >
142 <MenuItem value="pulsed">Pulsed</MenuItem>
143 <MenuItem value="cw">CW</MenuItem>
144 </Select>
145 </FormControl>
146
147 {'waveformId' in c && (
148 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
149 <InputLabel>Waveform</InputLabel>
150 <Select
151 label="Waveform"
152 value={c.waveformId ?? ''}
153 onChange={(e) =>
154 handleChange('waveformId', e.target.value)
155 }
156 >
157 <MenuItem value="">
158 <em>None</em>
159 </MenuItem>
160 {waveforms.map((w) => (
161 <MenuItem key={w.id} value={w.id}>
162 {w.name}
163 </MenuItem>
164 ))}
165 </Select>
166 </FormControl>
167 )}
168
169 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
170 <InputLabel>Antenna</InputLabel>
171 <Select
172 label="Antenna"
173 value={c.antennaId ?? ''}
174 onChange={(e) => handleChange('antennaId', e.target.value)}
175 >
176 <MenuItem value="">
177 <em>None</em>
178 </MenuItem>
179 {antennas.map((a) => (
180 <MenuItem key={a.id} value={a.id}>
181 {a.name}
182 </MenuItem>
183 ))}
184 </Select>
185 </FormControl>
186 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
187 <InputLabel>Timing Source</InputLabel>
188 <Select
189 label="Timing Source"
190 value={c.timingId ?? ''}
191 onChange={(e) => handleChange('timingId', e.target.value)}
192 >
193 <MenuItem value="">
194 <em>None</em>
195 </MenuItem>
196 {timings.map((t) => (
197 <MenuItem key={t.id} value={t.id}>
198 {t.name}
199 </MenuItem>
200 ))}
201 </Select>
202 </FormControl>
203 </>
204 );
205
206 const renderReceiverFields = (
207 c: MonostaticComponent | ReceiverComponent
208 ) => (
209 <>
210 {c.radarType === 'pulsed' && (
211 <>
212 <NumberField
213 label="Window Skip (s)"
214 value={c.window_skip}
215 onChange={(v) => handleChange('window_skip', v)}
216 />
217 <NumberField
218 label="Window Length (s)"
219 value={c.window_length}
220 onChange={(v) => handleChange('window_length', v)}
221 />
222 </>
223 )}
224 <NumberField
225 label="Noise Temperature (K)"
226 value={c.noiseTemperature}
227 onChange={(v) => handleChange('noiseTemperature', v)}
228 />
229 <FormControlLabel
230 control={
231 <Checkbox
232 checked={c.noDirectPaths}
233 onChange={(e) =>
234 handleChange('noDirectPaths', e.target.checked)
235 }
236 />
237 }
238 label="Ignore Direct Paths"
239 />
240 <FormControlLabel
241 control={
242 <Checkbox
243 checked={c.noPropagationLoss}
244 onChange={(e) =>
245 handleChange('noPropagationLoss', e.target.checked)
246 }
247 />
248 }
249 label="Ignore Propagation Loss"
250 />
251 </>
252 );
253
254 switch (component.type) {
255 case 'monostatic':
256 return (
257 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
258 {renderCommonRadarFields(component)}
259 {component.radarType === 'pulsed' && (
260 <NumberField
261 label="PRF (Hz)"
262 value={component.prf}
263 onChange={(v) => handleChange('prf', v)}
264 />
265 )}
266 {renderReceiverFields(component)}
267 {renderSchedule(component)}
268 </Box>
269 );
270 case 'transmitter':
271 return (
272 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
273 {renderCommonRadarFields(component)}
274 {component.radarType === 'pulsed' && (
275 <NumberField
276 label="PRF (Hz)"
277 value={component.prf}
278 onChange={(v) => handleChange('prf', v)}
279 />
280 )}
281 {renderSchedule(component)}
282 </Box>
283 );
284 case 'receiver':
285 return (
286 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
287 {renderCommonRadarFields(component)}
288 {component.radarType === 'pulsed' && (
289 <NumberField
290 label="PRF (Hz)"
291 value={component.prf}
292 onChange={(v) => handleChange('prf', v)}
293 />
294 )}
295 {renderReceiverFields(component)}
296 {renderSchedule(component)}
297 </Box>
298 );
299 case 'target':
300 return (
301 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
302 <TextField
303 label="Component Name"
304 size="small"
305 fullWidth
306 value={component.name}
307 onChange={(e) => handleChange('name', e.target.value)}
308 />
309 <TextField
310 label="Component Name"
311 size="small"
312 fullWidth
313 value={component.name}
314 onChange={(e) => handleChange('name', e.target.value)}
315 />
316 <FormControl fullWidth size="small">
317 <InputLabel>RCS Type</InputLabel>
318 <Select
319 label="RCS Type"
320 value={component.rcs_type}
321 onChange={(e) =>
322 handleChange('rcs_type', e.target.value)
323 }
324 >
325 <MenuItem value="isotropic">Isotropic</MenuItem>
326 <MenuItem value="file">File</MenuItem>
327 </Select>
328 </FormControl>
329 {component.rcs_type === 'isotropic' && (
330 <NumberField
331 label="RCS Value (m^2)"
332 value={component.rcs_value ?? 0}
333 onChange={(v) => handleChange('rcs_value', v)}
334 />
335 )}
336 {component.rcs_type === 'file' && (
337 <FileInput
338 label="RCS File"
339 value={component.rcs_filename}
340 onChange={(v) => handleChange('rcs_filename', v)}
341 filters={[{ name: 'RCS Data', extensions: ['*'] }]}
342 />
343 )}
344
345 <FormControl fullWidth size="small">
346 <InputLabel>RCS Model</InputLabel>
347 <Select
348 label="RCS Model"
349 value={component.rcs_model}
350 onChange={(e) =>
351 setPlatformRcsModel(
352 platformId,
353 component.id,
354 e.target
355 .value as TargetComponent['rcs_model']
356 )
357 }
358 >
359 <MenuItem value="constant">Constant</MenuItem>
360 <MenuItem value="chisquare">Chi-Square</MenuItem>
361 <MenuItem value="gamma">Gamma</MenuItem>
362 </Select>
363 </FormControl>
364 {(component.rcs_model === 'chisquare' ||
365 component.rcs_model === 'gamma') && (
366 <NumberField
367 label="K Value"
368 value={component.rcs_k ?? 0}
369 onChange={(v) => handleChange('rcs_k', v)}
370 />
371 )}
372 </Box>
373 );
374 default:
375 return (
376 <Typography color="text.secondary">
377 Unknown component type.
378 </Typography>
379 );
380 }
381}