FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
serializers.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4import { omit } from '@/utils/typeUtils';
5import {
6 Antenna,
7 GlobalParameters,
8 Platform,
9 PlatformComponent,
10 Timing,
11 Waveform,
12} from './types';
13
14type DistributiveOmit<T, K extends PropertyKey> = T extends unknown
15 ? Omit<T, Extract<keyof T, K>>
16 : never;
17
18type SerializedAntenna =
19 | DistributiveOmit<Antenna, 'type' | 'meshScale'>
20 | {
21 id: Antenna['id'];
22 name: Antenna['name'];
23 pattern: 'isotropic';
24 efficiency: Antenna['efficiency'];
25 };
26
27/**
28 * Recursively removes null or undefined values from an object or array.
29 */
30export const cleanObject = <T>(obj: T): T => {
31 if (Array.isArray(obj)) {
32 return obj.map(cleanObject) as unknown as T;
33 }
34 if (obj !== null && typeof obj === 'object') {
35 const newObj = {} as Record<string, unknown>;
36 for (const [key, value] of Object.entries(obj)) {
37 if (value !== null && value !== undefined) {
38 newObj[key] = cleanObject(value);
39 }
40 }
41 return newObj as T;
42 }
43 return obj;
44};
45
46/**
47 * Serializes the inner properties of a component (unwrapped).
48 * This is the exact format the C++ backend expects for granular updates.
49 */
50export const serializeComponentInner = (component: PlatformComponent) => {
51 const mode =
52 'radarType' in component && component.radarType === 'pulsed'
53 ? {
54 pulsed_mode: {
55 prf: component.prf,
56 ...(component.type !== 'transmitter' && {
57 window_skip: component.window_skip,
58 window_length: component.window_length,
59 }),
60 },
61 }
62 : 'radarType' in component && component.radarType === 'cw'
63 ? { cw_mode: {} }
64 : {};
65
66 switch (component.type) {
67 case 'monostatic':
68 return {
69 tx_id: component.txId,
70 rx_id: component.rxId,
71 name: component.name,
72 ...mode,
73 antenna: component.antennaId ?? 0,
74 waveform: component.waveformId ?? 0,
75 timing: component.timingId ?? 0,
76 noise_temp: component.noiseTemperature,
77 nodirect: component.noDirectPaths,
78 nopropagationloss: component.noPropagationLoss,
79 schedule: component.schedule,
80 };
81 case 'transmitter':
82 return {
83 id: component.id,
84 name: component.name,
85 ...mode,
86 antenna: component.antennaId ?? 0,
87 waveform: component.waveformId ?? 0,
88 timing: component.timingId ?? 0,
89 schedule: component.schedule,
90 };
91 case 'receiver':
92 return {
93 id: component.id,
94 name: component.name,
95 ...mode,
96 antenna: component.antennaId ?? 0,
97 timing: component.timingId ?? 0,
98 noise_temp: component.noiseTemperature,
99 nodirect: component.noDirectPaths,
100 nopropagationloss: component.noPropagationLoss,
101 schedule: component.schedule,
102 };
103 case 'target': {
104 const targetObj: Record<string, unknown> = {
105 id: component.id,
106 name: component.name,
107 rcs: {
108 type: component.rcs_type,
109 value: component.rcs_value,
110 filename: component.rcs_filename,
111 },
112 };
113 if (component.rcs_model !== 'constant') {
114 targetObj.model = {
115 type: component.rcs_model,
116 k: component.rcs_k,
117 };
118 }
119 return targetObj;
120 }
121 }
122};
123
124/**
125 * Serializes a component wrapped in its type key (e.g., { transmitter: { ... } }).
126 * This is the format expected by the full scenario JSON array.
127 */
128export const serializeComponent = (component: PlatformComponent) => {
129 return cleanObject({
130 [component.type]: serializeComponentInner(component),
131 });
132};
133
134export const serializePlatform = (p: Platform) => {
135 const { components, motionPath, rotation, ...rest } = p;
136
137 const backendComponents = components.map(serializeComponent);
138
139 const backendRotation: Record<string, unknown> = {};
140 if (rotation.type === 'fixed') {
141 const r = omit(rotation, 'type');
142 backendRotation.fixedrotation = {
143 interpolation: 'constant',
144 startazimuth: r.startAzimuth,
145 startelevation: r.startElevation,
146 azimuthrate: r.azimuthRate,
147 elevationrate: r.elevationRate,
148 };
149 } else {
150 const r = omit(rotation, 'type');
151 backendRotation.rotationpath = {
152 interpolation: r.interpolation,
153 rotationwaypoints: r.waypoints.map((wp) => omit(wp, 'id')),
154 };
155 }
156
157 return cleanObject({
158 ...rest,
159 id: p.id,
160 motionpath: {
161 interpolation: motionPath.interpolation,
162 positionwaypoints: motionPath.waypoints.map((wp) => omit(wp, 'id')),
163 },
164 ...backendRotation,
165 components: backendComponents,
166 });
167};
168
169export const serializeWaveform = (w: Waveform) => {
170 const waveformContent =
171 w.waveformType === 'cw'
172 ? { cw: {} }
173 : { pulsed_from_file: { filename: w.filename } };
174
175 return cleanObject({
176 id: w.id,
177 name: w.name,
178 power: w.power,
179 carrier_frequency: w.carrier_frequency,
180 ...waveformContent,
181 });
182};
183
184export const serializeTiming = (t: Timing) => {
185 const rest = omit(t, 'type');
186 const timingObj = {
187 ...rest,
188 synconpulse: false,
189 freq_offset: t.freqOffset,
190 random_freq_offset_stdev: t.randomFreqOffsetStdev,
191 phase_offset: t.phaseOffset,
192 random_phase_offset_stdev: t.randomPhaseOffsetStdev,
193 noise_entries: t.noiseEntries.map((entry) => omit(entry, 'id')),
194 };
195 if (timingObj.noise_entries?.length === 0) {
196 delete (timingObj as Partial<typeof timingObj>).noise_entries;
197 }
198 return cleanObject(timingObj);
199};
200
201export const isFileBackedAntennaPendingFile = (a: Antenna): boolean =>
202 (a.pattern === 'xml' || a.pattern === 'file') &&
203 (a.filename ?? '').trim().length === 0;
204
205export const serializeAntenna = (a: Antenna): SerializedAntenna => {
206 if (isFileBackedAntennaPendingFile(a)) {
207 return cleanObject({
208 id: a.id,
209 name: a.name,
210 pattern: 'isotropic',
211 efficiency: a.efficiency,
212 });
213 }
214
215 const rest = omit(a, 'type', 'meshScale');
216 return cleanObject(rest) as SerializedAntenna;
217};
218
219export const serializeGlobalParameters = (gp: GlobalParameters) => {
220 const {
221 start,
222 end,
223 random_seed,
224 oversample_ratio,
225 coordinateSystem,
226 ...gpRest
227 } = gp;
228
229 return cleanObject({
230 ...gpRest,
231 starttime: start,
232 endtime: end,
233 randomseed: random_seed,
234 oversample: oversample_ratio,
235 rotationangleunit: gp.rotationAngleUnit,
236 coordinatesystem: coordinateSystem,
237 });
238};