1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4import { omit } from '@/utils/typeUtils';
14type DistributiveOmit<T, K extends PropertyKey> = T extends unknown
15 ? Omit<T, Extract<keyof T, K>>
18type SerializedAntenna =
19 | DistributiveOmit<Antenna, 'type' | 'meshScale'>
22 name: Antenna['name'];
24 efficiency: Antenna['efficiency'];
28 * Recursively removes null or undefined values from an object or array.
30export const cleanObject = <T>(obj: T): T => {
31 if (Array.isArray(obj)) {
32 return obj.map(cleanObject) as unknown as T;
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);
47 * Serializes the inner properties of a component (unwrapped).
48 * This is the exact format the C++ backend expects for granular updates.
50export const serializeComponentInner = (component: PlatformComponent) => {
52 'radarType' in component && component.radarType === 'pulsed'
56 ...(component.type !== 'transmitter' && {
57 window_skip: component.window_skip,
58 window_length: component.window_length,
62 : 'radarType' in component && component.radarType === 'cw'
66 switch (component.type) {
69 tx_id: component.txId,
70 rx_id: component.rxId,
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,
86 antenna: component.antennaId ?? 0,
87 waveform: component.waveformId ?? 0,
88 timing: component.timingId ?? 0,
89 schedule: component.schedule,
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,
104 const targetObj: Record<string, unknown> = {
106 name: component.name,
108 type: component.rcs_type,
109 value: component.rcs_value,
110 filename: component.rcs_filename,
113 if (component.rcs_model !== 'constant') {
115 type: component.rcs_model,
125 * Serializes a component wrapped in its type key (e.g., { transmitter: { ... } }).
126 * This is the format expected by the full scenario JSON array.
128export const serializeComponent = (component: PlatformComponent) => {
130 [component.type]: serializeComponentInner(component),
134export const serializePlatform = (p: Platform) => {
135 const { components, motionPath, rotation, ...rest } = p;
137 const backendComponents = components.map(serializeComponent);
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,
150 const r = omit(rotation, 'type');
151 backendRotation.rotationpath = {
152 interpolation: r.interpolation,
153 rotationwaypoints: r.waypoints.map((wp) => omit(wp, 'id')),
161 interpolation: motionPath.interpolation,
162 positionwaypoints: motionPath.waypoints.map((wp) => omit(wp, 'id')),
165 components: backendComponents,
169export const serializeWaveform = (w: Waveform) => {
170 const waveformContent =
171 w.waveformType === 'cw'
173 : { pulsed_from_file: { filename: w.filename } };
179 carrier_frequency: w.carrier_frequency,
184export const serializeTiming = (t: Timing) => {
185 const rest = omit(t, 'type');
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')),
195 if (timingObj.noise_entries?.length === 0) {
196 delete (timingObj as Partial<typeof timingObj>).noise_entries;
198 return cleanObject(timingObj);
201export const isFileBackedAntennaPendingFile = (a: Antenna): boolean =>
202 (a.pattern === 'xml' || a.pattern === 'file') &&
203 (a.filename ?? '').trim().length === 0;
205export const serializeAntenna = (a: Antenna): SerializedAntenna => {
206 if (isFileBackedAntennaPendingFile(a)) {
210 pattern: 'isotropic',
211 efficiency: a.efficiency,
215 const rest = omit(a, 'type', 'meshScale');
216 return cleanObject(rest) as SerializedAntenna;
219export const serializeGlobalParameters = (gp: GlobalParameters) => {
233 randomseed: random_seed,
234 oversample: oversample_ratio,
235 rotationangleunit: gp.rotationAngleUnit,
236 coordinatesystem: coordinateSystem,