FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
kml_generator.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2023-present FERS Contributors (see AUTHORS.md).
4//
5// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
6
7/**
8 * @file kml_generator.cpp
9 * @brief Source file for KML file generation from FERS simulation scenarios.
10 */
11
13
14#include <GeographicLib/Geocentric.hpp>
15#include <GeographicLib/Geodesic.hpp>
16#include <GeographicLib/LocalCartesian.hpp>
17#include <GeographicLib/UTMUPS.hpp>
18#include <algorithm>
19#include <cmath>
20#include <fstream>
21#include <functional>
22#include <iomanip>
23#include <map>
24#include <memory>
25#include <optional>
26#include <ranges>
27#include <sstream>
28#include <string>
29#include <vector>
30
32#include "core/config.h"
33#include "core/logging.h"
34#include "core/parameters.h"
35#include "core/world.h"
36#include "math/coord.h"
37#include "math/path.h"
38#include "radar/platform.h"
39#include "radar/radar_obj.h"
40#include "radar/receiver.h"
41#include "radar/target.h"
42#include "radar/transmitter.h"
43#include "signal/radar_signal.h"
44
45
46namespace
47{
48 using namespace std;
49 using ConverterFunc = std::function<void(const math::Vec3&, double&, double&, double&)>;
50
51 // --- Constants ---
52 constexpr int TRACK_NUM_DIVISIONS = 100;
53
54 constexpr int ISOTROPIC_PATTERN_POINTS = 100;
55
56 constexpr double ISOTROPIC_PATTERN_RADIUS_KM = 20.0;
57
58 constexpr double DIRECTIONAL_ANTENNA_ARROW_LENGTH_M = 20000.0;
59
60 // --- Geodetic and Coordinate Helpers ---
61 double sincAntennaGain(const double theta, const double alpha, const double beta, const double gamma)
62 {
63 if (theta == 0.0)
64 {
65 return alpha;
66 }
67 const double gain = alpha * std::pow(std::sin(beta * theta) / (beta * theta), gamma);
68 return gain;
69 }
70
71 double find3DbDropAngle(const double alpha, const double beta, const double gamma)
72 {
73 constexpr int num_points = 1000;
74 std::vector<double> theta(num_points);
75 std::vector<double> gain(num_points);
76 for (int i = 0; i < num_points; ++i)
77 {
78 theta[i] = -PI + 2.0 * PI * i / (num_points - 1);
79 gain[i] = sincAntennaGain(theta[i], alpha, beta, gamma);
80 }
81 const double max_gain = *std::max_element(gain.begin() + num_points / 2, gain.end());
82 const double max_gain_db = 10.0 * std::log10(max_gain);
83 const double target_gain_db = max_gain_db - 3.0;
84 double target_gain = std::pow(10.0, target_gain_db / 10.0);
85 const int idx = std::distance(
86 gain.begin() + num_points / 2,
87 std::min_element(gain.begin() + num_points / 2, gain.end(), [target_gain](const double a, const double b)
88 { return std::abs(a - target_gain) < std::abs(b - target_gain); }));
89 const double angle_3db_drop = theta[idx + num_points / 2];
90 return angle_3db_drop * 180.0 / PI;
91 }
92
93 /**
94 * @brief Calculates the half-power (-3dB) beamwidth angle for a Gaussian antenna.
95 * @param gaussianAnt Pointer to the Gaussian antenna object.
96 * @return The 3dB drop half-angle in degrees.
97 */
98 double findGaussian3DbDropAngle(const antenna::Gaussian* gaussianAnt)
99 {
100 // 3dB drop is when gain is 0.5. For gaussian, G = exp(-theta^2 * scale)
101 // 0.5 = exp(-theta^2 * scale) => ln(0.5) = -theta^2 * scale
102 // theta = sqrt(-ln(0.5) / scale) = sqrt(ln(2) / scale)
103 // We use the azimuth scale for the visualization in the horizontal plane.
104 if (gaussianAnt->getAzimuthScale() <= 0.0)
105 {
107 "Gaussian antenna '{}' has a non-positive azimuth scale ({}). 3dB beamwidth is undefined. KML will "
108 "only show boresight.",
109 gaussianAnt->getName(), gaussianAnt->getAzimuthScale());
110 return 0.0;
111 }
112 const double half_angle_rad = std::sqrt(std::log(2.0) / gaussianAnt->getAzimuthScale());
113 return half_angle_rad * 180.0 / PI;
114 }
115
116 /**
117 * @brief Calculates the half-power (-3dB) beamwidth angle for a Parabolic antenna.
118 * @param parabolicAnt Pointer to the Parabolic antenna object.
119 * @param wavelength The operating wavelength in meters.
120 * @return The 3dB drop half-angle in degrees.
121 */
122 double findParabolic3DbDropAngle(const antenna::Parabolic* parabolicAnt, const double wavelength)
123 {
124 // For a parabolic antenna, the gain pattern is related to (2*J1(x)/x)^2,
125 // where J1 is the Bessel function of the first kind of order one.
126 // The 3dB point occurs approximately when x = 1.6.
127 // x = PI * diameter * sin(theta) / wavelength
128 // sin(theta) = 1.6 * wavelength / (PI * diameter)
129 if (parabolicAnt->getDiameter() <= 0.0)
130 {
132 "Parabolic antenna '{}' has a non-positive diameter ({}). This is physically impossible. KML will only "
133 "show boresight.",
134 parabolicAnt->getName(), parabolicAnt->getDiameter());
135 return 0.0;
136 }
137 // TODO: magic numbers
138 const double arg = 1.6 * wavelength / (PI * parabolicAnt->getDiameter());
139 // For physically realizable antennas, arg should be <= 1.
140 if (arg > 1.0)
141 {
143 "Parabolic antenna '{}': The operating wavelength ({:.4f}m) is very large compared to its diameter "
144 "({:.4f}m), resulting in a nearly omnidirectional pattern. KML visualization will cap the 3dB "
145 "half-angle at 90 degrees.",
146 parabolicAnt->getName(), wavelength, parabolicAnt->getDiameter());
147 return 90.0; // Extremely wide beam, cap at 90 degrees.
148 }
149 const double half_angle_rad = std::asin(arg);
150 return half_angle_rad * 180.0 / PI;
151 }
152
153 /**
154 * @brief Calculates the half-power (-3dB) beamwidth angle for a SquareHorn antenna.
155 * @param squarehornAnt Pointer to the SquareHorn antenna object.
156 * @param wavelength The operating wavelength in meters.
157 * @return The 3dB drop half-angle in degrees.
158 */
159 double findSquareHorn3DbDropAngle(const antenna::SquareHorn* squarehornAnt, const double wavelength)
160 {
161 // The gain pattern for a square horn is related to sinc(x)^2.
162 // The 3dB point occurs when sinc(x) = sqrt(0.5), which is approx. x = 1.39155.
163 // x = PI * dimension * sin(theta) / wavelength
164 // sin(theta) = 1.39155 * wavelength / (PI * dimension)
165 if (squarehornAnt->getDimension() <= 0.0)
166 {
168 "SquareHorn antenna '{}' has a non-positive dimension ({}). This is physically impossible. KML will "
169 "only show boresight.",
170 squarehornAnt->getName(), squarehornAnt->getDimension());
171 return 0.0;
172 }
173 // TODO: magic numbers
174 const double arg = 1.39155 * wavelength / (PI * squarehornAnt->getDimension());
175 if (arg > 1.0)
176 {
178 "SquareHorn antenna '{}': The operating wavelength ({:.4f}m) is very large compared to its dimension "
179 "({:.4f}m), resulting in a nearly omnidirectional pattern. KML visualization will cap the 3dB "
180 "half-angle at 90 degrees.",
181 squarehornAnt->getName(), wavelength, squarehornAnt->getDimension());
182 return 90.0; // Extremely wide beam, cap at 90 degrees.
183 }
184 const double half_angle_rad = std::asin(arg);
185 return half_angle_rad * 180.0 / PI;
186 }
187
188 std::string formatCoordinates(const double lon, const double lat, const double alt)
189 {
190 std::stringstream ss;
191 ss << std::fixed << std::setprecision(6) << lon << "," << lat << "," << alt;
192 return ss.str();
193 }
194
195 void calculateDestinationCoordinate(const double startLatitude, const double startLongitude, const double angle,
196 const double distance, double& destLatitude, double& destLongitude)
197 {
198 const GeographicLib::Geodesic& geod = GeographicLib::Geodesic::WGS84();
199 geod.Direct(startLatitude, startLongitude, angle, distance, destLatitude, destLongitude);
200 }
201
202 std::vector<std::pair<double, double>> generateCircleCoordinates(const double lat, const double lon,
203 const double radius_km)
204 {
205 std::vector<std::pair<double, double>> circle_coordinates;
206 for (int i = 0; i < ISOTROPIC_PATTERN_POINTS; i++)
207 {
208 const double bearing = i * 360.0 / ISOTROPIC_PATTERN_POINTS;
209 double new_lat, new_lon;
210 calculateDestinationCoordinate(lat, lon, bearing, radius_km * 1000.0, new_lat, new_lon);
211 circle_coordinates.emplace_back(new_lat, new_lon);
212 }
213 return circle_coordinates;
214 }
215
216 // --- KML Generation Helpers ---
217 void writeKmlHeaderAndStyles(std::ofstream& kmlFile)
218 {
219 kmlFile << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
220 kmlFile << "<kml xmlns=\"http://www.opengis.net/kml/2.2\" xmlns:gx=\"http://www.google.com/kml/ext/2.2\">\n";
221 kmlFile << "<Document>\n";
222 kmlFile << " <name>";
223 if (params::params.simulation_name.empty())
224 {
225 kmlFile << "FERS Simulation Visualization";
226 }
227 else
228 {
230 }
231 kmlFile << "</name>\n";
232 kmlFile << " <Style "
233 "id=\"receiver\"><IconStyle><Icon><href>https://cdn-icons-png.flaticon.com/512/645/645436.png</"
234 "href></Icon></IconStyle></Style>\n";
235 kmlFile << " <Style "
236 "id=\"transmitter\"><IconStyle><Icon><href>https://cdn-icons-png.flaticon.com/128/224/224666.png</"
237 "href></Icon></IconStyle></Style>\n";
238 kmlFile << " <Style "
239 "id=\"target\"><IconStyle><Icon><href>https://upload.wikimedia.org/wikipedia/commons/thumb/a/ad/"
240 "Target_red_dot1.svg/1200px-Target_red_dot1.svg.png</href></Icon></IconStyle><LineStyle><width>2</"
241 "width></LineStyle></Style>\n";
242 kmlFile << " <Style "
243 "id=\"translucentPolygon\"><LineStyle><color>ff0000ff</color><width>2</width></"
244 "LineStyle><PolyStyle><color>00ffffff</color></PolyStyle></Style>\n";
245 kmlFile << " <Style "
246 "id=\"arrowStyle\"><IconStyle><Icon><href>http://maps.google.com/mapfiles/kml/shapes/arrow.png</"
247 "href></Icon><scale>0.5</scale></IconStyle></Style>\n";
248 kmlFile << " <Style id=\"lineStyle\"><LineStyle><color>ff0000ff</color><width>2</width></LineStyle></Style>\n";
249 kmlFile
250 << " <Style id=\"lineStyleBlue\"><LineStyle><color>ffff0000</color><width>2</width></LineStyle></Style>\n";
251 }
252
253 void writePoint(std::ofstream& kmlFile, const std::string& indent, const std::string& name,
254 const std::string& styleUrl, const std::string& coordinates, const double objectAltitude,
255 const double referenceAltitude)
256 {
257 kmlFile << indent << "<Placemark>\n";
258 kmlFile << indent << " <name>" << name << "</name>\n";
259 kmlFile << indent << " <styleUrl>" << styleUrl << "</styleUrl>\n";
260 kmlFile << indent << " <Point>\n";
261 kmlFile << indent << " <coordinates>" << coordinates << "</coordinates>\n";
262 kmlFile << indent << " <altitudeMode>absolute</altitudeMode>\n";
263 if (objectAltitude > referenceAltitude)
264 {
265 kmlFile << indent << " <extrude>1</extrude>\n";
266 }
267 kmlFile << indent << " </Point>\n";
268 kmlFile << indent << "</Placemark>\n";
269 }
270
271 void writeAntennaBeamLine(std::ofstream& kmlFile, const std::string& indent, const std::string& name,
272 const std::string& style, const std::string& startCoords, const std::string& endCoords)
273 {
274 kmlFile << indent << "<Placemark>\n";
275 kmlFile << indent << " <name>" << name << "</name>\n";
276 kmlFile << indent << " <styleUrl>" << style << "</styleUrl>\n";
277 kmlFile << indent << " <LineString>\n";
278 kmlFile << indent << " <altitudeMode>absolute</altitudeMode>\n";
279 kmlFile << indent << " <tessellate>1</tessellate>\n";
280 kmlFile << indent << " <coordinates>" << startCoords << " " << endCoords << "</coordinates>\n";
281 kmlFile << indent << " </LineString>\n";
282 kmlFile << indent << "</Placemark>\n";
283 }
284
285 // --- Platform Processing Logic ---
286 std::string getPlacemarkStyleForPlatform(const std::vector<const radar::Object*>& objects)
287 {
288 bool has_receiver = false;
289 bool has_transmitter = false;
290 for (const auto* obj : objects)
291 {
292 if (dynamic_cast<const radar::Receiver*>(obj))
293 {
294 has_receiver = true;
295 }
296 if (dynamic_cast<const radar::Transmitter*>(obj))
297 {
298 has_transmitter = true;
299 }
300 }
301
302 if (has_receiver)
303 {
304 return "#receiver";
305 }
306 if (has_transmitter)
307 {
308 return "#transmitter";
309 }
310 return "#target";
311 }
312
313 const radar::Radar* getPrimaryRadar(const std::vector<const radar::Object*>& objects)
314 {
315 for (const auto* obj : objects)
316 {
317 if (const auto r = dynamic_cast<const radar::Radar*>(obj))
318 {
319 return r;
320 }
321 }
322 return nullptr;
323 }
324
325 void generateIsotropicAntennaKml(std::ofstream& kmlFile, const math::Vec3& position, const ConverterFunc& converter,
326 const std::string& indent)
327 {
328 double lat, lon, alt_abs;
329 converter(position, lat, lon, alt_abs);
330
331 const auto circle_coordinates = generateCircleCoordinates(lat, lon, ISOTROPIC_PATTERN_RADIUS_KM);
332
333 kmlFile << indent << "<Placemark>\n";
334 kmlFile << indent << " <name>Isotropic pattern range</name>\n";
335 kmlFile << indent << " <styleUrl>#translucentPolygon</styleUrl>\n";
336 kmlFile << indent << " <Polygon>\n";
337 kmlFile << indent << " <extrude>1</extrude>\n";
338 kmlFile << indent << " <altitudeMode>absolute</altitudeMode>\n";
339 kmlFile << indent << " <outerBoundaryIs><LinearRing><coordinates>\n";
340 for (const auto& [pt_lat, pt_lon] : circle_coordinates)
341 {
342 kmlFile << indent << " " << formatCoordinates(pt_lon, pt_lat, alt_abs) << "\n";
343 }
344 kmlFile << indent << " "
345 << formatCoordinates(circle_coordinates[0].second, circle_coordinates[0].first, alt_abs) << "\n";
346 kmlFile << indent << " </coordinates></LinearRing></outerBoundaryIs>\n";
347 kmlFile << indent << " </Polygon>\n";
348 kmlFile << indent << "</Placemark>\n";
349 }
350
351 void generateDirectionalAntennaKml(std::ofstream& kmlFile, const radar::Platform* platform,
352 const ConverterFunc& converter, const std::optional<double>& angle3DbDropDeg,
353 const std::string& indent)
354 {
355 const auto& first_wp_pos = platform->getMotionPath()->getCoords().front().pos;
356 double start_lat, start_lon, start_alt;
357 converter(first_wp_pos, start_lat, start_lon, start_alt);
358 const std::string start_coords_str = formatCoordinates(start_lon, start_lat, start_alt);
359
360 const math::SVec3 initial_rotation = platform->getRotationPath()->getPosition(params::startTime());
361
362 // The parser now handles the conversion from compass heading to the internal
363 // FERS format (radians, CCW from East). The KML generator needs to convert
364 // this back to a standard KML heading (degrees, CW from North).
365 const double fers_azimuth_deg = initial_rotation.azimuth * 180.0 / PI;
366 double start_azimuth_deg_kml = 90.0 - fers_azimuth_deg;
367 // Normalize to [0, 360)
368 start_azimuth_deg_kml = std::fmod(start_azimuth_deg_kml, 360.0);
369 if (start_azimuth_deg_kml < 0.0)
370 {
371 start_azimuth_deg_kml += 360.0;
372 }
373
374 // Project the arrow length onto the horizontal plane for the geodetic calculation
375 // and calculate the change in altitude separately.
376 const double horizontal_distance = DIRECTIONAL_ANTENNA_ARROW_LENGTH_M * std::cos(initial_rotation.elevation);
377 const double delta_altitude = DIRECTIONAL_ANTENNA_ARROW_LENGTH_M * std::sin(initial_rotation.elevation);
378 const double end_alt = start_alt + delta_altitude;
379
380 // TODO: Antenna beam visualization is static, showing only the orientation at the simulation's start time.
381 // This does not represent dynamic scanning defined by a platform's <rotationpath>. The KML should
382 // ideally visualize the scan volume or animate the beam's movement using a <gx:Track> to match FERS's
383 // capabilities.
384 // Main beam
385 double dest_lat, dest_lon;
386 calculateDestinationCoordinate(start_lat, start_lon, start_azimuth_deg_kml, horizontal_distance, dest_lat,
387 dest_lon);
388 const std::string end_coords_str = formatCoordinates(dest_lon, dest_lat, end_alt);
389 writeAntennaBeamLine(kmlFile, indent, "Antenna Boresight", "#lineStyle", start_coords_str, end_coords_str);
390
391 // 3dB beamwidth lines, if angle is provided and is greater than a small epsilon
392 if (angle3DbDropDeg.has_value() && *angle3DbDropDeg > EPSILON)
393 {
394 double side1_lat, side1_lon;
395 calculateDestinationCoordinate(start_lat, start_lon, start_azimuth_deg_kml - *angle3DbDropDeg,
396 horizontal_distance, side1_lat, side1_lon);
397 const std::string side1_coords_str = formatCoordinates(side1_lon, side1_lat, end_alt);
398 writeAntennaBeamLine(kmlFile, indent, "Antenna 3dB Beamwidth", "#lineStyleBlue", start_coords_str,
399 side1_coords_str);
400
401 double side2_lat, side2_lon;
402 calculateDestinationCoordinate(start_lat, start_lon, start_azimuth_deg_kml + *angle3DbDropDeg,
403 horizontal_distance, side2_lat, side2_lon);
404 const std::string side2_coords_str = formatCoordinates(side2_lon, side2_lat, end_alt);
405 writeAntennaBeamLine(kmlFile, indent, "Antenna 3dB Beamwidth", "#lineStyleBlue", start_coords_str,
406 side2_coords_str);
407 }
408
409 // Arrow placemark
410 const double arrow_heading = std::fmod(start_azimuth_deg_kml + 180.0, 360.0);
411 kmlFile << indent << "<Placemark>\n";
412 kmlFile << indent << " <name>Antenna Arrow</name>\n";
413 kmlFile << indent << " <styleUrl>#arrowStyle</styleUrl>\n";
414 kmlFile << indent << " <Point><coordinates>" << end_coords_str
415 << "</coordinates><altitudeMode>absolute</altitudeMode></Point>\n";
416 kmlFile << indent << " <Style>\n";
417 kmlFile << indent << " <IconStyle><heading>" << arrow_heading << "</heading></IconStyle>\n";
418 kmlFile << indent << " </Style>\n";
419 kmlFile << indent << "</Placemark>\n";
420 }
421
422 void generateAntennaKml(std::ofstream& kmlFile, const radar::Platform* platform, const radar::Radar* radar,
423 const ConverterFunc& converter, const std::string& indent)
424 {
425 const antenna::Antenna* ant = radar->getAntenna();
426 if (!ant || platform->getMotionPath()->getCoords().empty())
427 {
428 return;
429 }
430
431 if (dynamic_cast<const antenna::Isotropic*>(ant))
432 {
433 const math::Vec3 initial_pos = platform->getMotionPath()->getCoords().front().pos;
434 generateIsotropicAntennaKml(kmlFile, initial_pos, converter, indent);
435 }
436 else // Handle all directional antennas
437 {
438 std::optional<double> angle_3db_drop_deg;
439
440 // Attempt to find wavelength for wavelength-dependent patterns
441 std::optional<double> wavelength;
442 if (const auto* tx = dynamic_cast<const radar::Transmitter*>(radar))
443 {
444 if (tx->getSignal())
445 {
446 wavelength = params::c() / tx->getSignal()->getCarrier();
447 }
448 }
449 else if (const auto* rx = dynamic_cast<const radar::Receiver*>(radar))
450 {
451 if (const auto* attached_tx = dynamic_cast<const radar::Transmitter*>(rx->getAttached()))
452 {
453 if (attached_tx->getSignal())
454 {
455 wavelength = params::c() / attached_tx->getSignal()->getCarrier();
456 }
457 }
458 }
459
460 // Calculate 3dB drop angle based on antenna type
461 if (const auto* sinc_ant = dynamic_cast<const antenna::Sinc*>(ant))
462 {
463 angle_3db_drop_deg = find3DbDropAngle(sinc_ant->getAlpha(), sinc_ant->getBeta(), sinc_ant->getGamma());
464 }
465 else if (const auto* gaussian_ant = dynamic_cast<const antenna::Gaussian*>(ant))
466 {
467 angle_3db_drop_deg = findGaussian3DbDropAngle(gaussian_ant);
468 }
469 else if (const auto* parabolic_ant = dynamic_cast<const antenna::Parabolic*>(ant))
470 {
471 if (wavelength)
472 {
473 angle_3db_drop_deg = findParabolic3DbDropAngle(parabolic_ant, *wavelength);
474 }
475 }
476 else if (const auto* squarehorn_ant = dynamic_cast<const antenna::SquareHorn*>(ant))
477 {
478 if (wavelength)
479 {
480 angle_3db_drop_deg = findSquareHorn3DbDropAngle(squarehorn_ant, *wavelength);
481 }
482 }
483 else if (dynamic_cast<const antenna::XmlAntenna*>(ant) || dynamic_cast<const antenna::H5Antenna*>(ant))
484 {
485 // For XmlAntenna and H5Antenna, angle_3db_drop_deg remains nullopt,
486 // resulting in only the boresight arrow being drawn. This is an intentional
487 // symbolic representation. Alert the user about this.
489 "KML visualization for antenna '{}' ('{}') is symbolic. "
490 "Only the boresight direction is shown, as a 3dB beamwidth is not calculated from file-based "
491 "patterns.",
492 ant->getName(), dynamic_cast<const antenna::XmlAntenna*>(ant) ? "xml" : "file");
493 }
494
495 generateDirectionalAntennaKml(kmlFile, platform, converter, angle_3db_drop_deg, indent);
496 }
497 }
498
499 void generateDynamicPathKml(std::ofstream& kmlFile, const radar::Platform* platform, const std::string& styleUrl,
500 const double refAlt, const ConverterFunc& converter, const std::string& indent)
501 {
502 const math::Path* path = platform->getMotionPath();
503 const auto& waypoints = path->getCoords();
504
505 double first_alt_abs;
506 {
507 double lat, lon;
508 converter(waypoints.front().pos, lat, lon, first_alt_abs);
509 }
510
511 kmlFile << indent << "<Placemark>\n";
512 kmlFile << indent << " <name>" << platform->getName() << " Path</name>\n";
513 kmlFile << indent << " <styleUrl>" << styleUrl << "</styleUrl>\n";
514 kmlFile << indent << " <gx:Track>\n";
515 kmlFile << indent << " <altitudeMode>absolute</altitudeMode>\n";
516 if (first_alt_abs > refAlt)
517 {
518 kmlFile << indent << " <extrude>1</extrude>\n";
519 }
520
521 // The sampling time range is now based on the platform's specific motion path duration,
522 // ensuring accurate track resolution for objects with short lifespans.
523 const double start_time = waypoints.front().t;
524 const double end_time = waypoints.back().t;
525
526 // Handle single-point paths or paths with zero duration by emitting a single coordinate.
527 if (const double time_diff = end_time - start_time; time_diff <= 0.0)
528 {
529 const math::Vec3 p_pos = path->getPosition(start_time);
530 double p_lon, p_lat, p_alt_abs;
531 converter(p_pos, p_lat, p_lon, p_alt_abs);
532 kmlFile << indent << " <when>" << start_time << "</when>\n";
533 kmlFile << indent << " <gx:coord>" << p_lon << " " << p_lat << " " << p_alt_abs << "</gx:coord>\n";
534 }
535 else
536 {
537 const double time_step = time_diff / TRACK_NUM_DIVISIONS;
538 for (int i = 0; i <= TRACK_NUM_DIVISIONS; ++i)
539 {
540 const double current_time = start_time + i * time_step;
541 const math::Vec3 p_pos = path->getPosition(current_time);
542 double p_lon, p_lat, p_alt_abs;
543 converter(p_pos, p_lat, p_lon, p_alt_abs);
544 kmlFile << indent << " <when>" << current_time << "</when>\n";
545 kmlFile << indent << " <gx:coord>" << p_lon << " " << p_lat << " " << p_alt_abs << "</gx:coord>\n";
546 }
547 }
548
549 kmlFile << indent << " </gx:Track>\n";
550 kmlFile << indent << "</Placemark>\n";
551 }
552
553 void generateTrackEndpointsKml(std::ofstream& kmlFile, const radar::Platform* platform, const double refAlt,
554 const ConverterFunc& converter, const std::string& indent)
555 {
556 const math::Path* path = platform->getMotionPath();
557 if (path->getCoords().size() <= 1)
558 {
559 return;
560 }
561
562 const auto& [start_wp_pos, start_wp_t] = path->getCoords().front();
563 const auto& [end_wp_pos, end_wp_t] = path->getCoords().back();
564
565 double start_lat, start_lon, start_alt_abs;
566 converter(start_wp_pos, start_lat, start_lon, start_alt_abs);
567 const std::string start_coordinates = formatCoordinates(start_lon, start_lat, start_alt_abs);
568
569 double end_lat, end_lon, end_alt_abs;
570 converter(end_wp_pos, end_lat, end_lon, end_alt_abs);
571 const std::string end_coordinates = formatCoordinates(end_lon, end_lat, end_alt_abs);
572
573 writePoint(kmlFile, indent, "Start: " + platform->getName(), "#target", start_coordinates, start_alt_abs,
574 refAlt);
575 writePoint(kmlFile, indent, "End: " + platform->getName(), "#target", end_coordinates, end_alt_abs, refAlt);
576 }
577
578 void generateStaticPlacemarkKml(std::ofstream& kmlFile, const radar::Platform* platform,
579 const std::string& styleUrl, const double refAlt, const ConverterFunc& converter,
580 const std::string& indent)
581 {
582 const auto& [first_wp_pos, first_wp_t] = platform->getMotionPath()->getCoords().front();
583 double lat, lon, alt_abs;
584 converter(first_wp_pos, lat, lon, alt_abs);
585 const std::string coordinates = formatCoordinates(lon, lat, alt_abs);
586
587 kmlFile << indent << "<Placemark>\n";
588 kmlFile << indent << " <name>" << platform->getName() << "</name>\n";
589 kmlFile << indent << " <styleUrl>" << styleUrl << "</styleUrl>\n";
590 kmlFile << indent << " <LookAt>\n";
591 kmlFile << indent << " <longitude>" << lon << "</longitude>\n";
592 kmlFile << indent << " <latitude>" << lat << "</latitude>\n";
593 kmlFile << indent << " <altitude>" << alt_abs << "</altitude>\n";
594 kmlFile << indent << " <heading>-148.41</heading><tilt>40.55</tilt><range>500.65</range>\n";
595 kmlFile << indent << " </LookAt>\n";
596 kmlFile << indent << " <Point>\n";
597 kmlFile << indent << " <coordinates>" << coordinates << "</coordinates>\n";
598 kmlFile << indent << " <altitudeMode>absolute</altitudeMode>\n";
599 if (alt_abs > refAlt)
600 {
601 kmlFile << indent << " <extrude>1</extrude>\n";
602 }
603 kmlFile << indent << " </Point>\n";
604 kmlFile << indent << "</Placemark>\n";
605 }
606
607 void generatePlatformPathKml(std::ofstream& kmlFile, const radar::Platform* platform, const std::string& style,
608 const double refAlt, const ConverterFunc& converter, const std::string& indent)
609 {
610 const auto path_type = platform->getMotionPath()->getType();
611 const bool is_dynamic =
613
614 if (is_dynamic)
615 {
616 generateDynamicPathKml(kmlFile, platform, style, refAlt, converter, indent);
617 generateTrackEndpointsKml(kmlFile, platform, refAlt, converter, indent);
618 }
619 else
620 {
621 generateStaticPlacemarkKml(kmlFile, platform, style, refAlt, converter, indent);
622 }
623 }
624
625 void processPlatform(const radar::Platform* platform, const std::vector<const radar::Object*>& objects,
626 std::ofstream& kmlFile, const ConverterFunc& converter, const double referenceAltitude,
627 const std::string& indent)
628 {
629 if (platform->getMotionPath()->getCoords().empty())
630 {
631 return;
632 }
633
634 kmlFile << indent << "<Folder>\n";
635 kmlFile << indent << " <name>" << platform->getName() << "</name>\n";
636
637 const std::string inner_indent = indent + " ";
638 const auto placemark_style = getPlacemarkStyleForPlatform(objects);
639
640 if (const auto* radar_obj = getPrimaryRadar(objects))
641 {
642 generateAntennaKml(kmlFile, platform, radar_obj, converter, inner_indent);
643 }
644
645 generatePlatformPathKml(kmlFile, platform, placemark_style, referenceAltitude, converter, inner_indent);
646
647 kmlFile << indent << "</Folder>\n";
648 }
649
650}
651
652namespace serial
653{
654 bool KmlGenerator::generateKml(const core::World& world, const std::string& outputKmlPath)
655 {
656 try
657 {
658 // Setup coordinate conversion based on global parameters
659 ConverterFunc converter;
660 double reference_latitude, reference_longitude, reference_altitude;
661
662 switch (params::coordinateFrame())
663 {
665 {
666 reference_latitude = params::originLatitude();
667 reference_longitude = params::originLongitude();
668 reference_altitude = params::originAltitude();
669 auto proj = std::make_shared<GeographicLib::LocalCartesian>(reference_latitude, reference_longitude,
670 reference_altitude);
671 converter = [proj](const math::Vec3& pos, double& lat, double& lon, double& alt)
672 { proj->Reverse(pos.x, pos.y, pos.z, lat, lon, alt); };
673 break;
674 }
676 {
677 const int zone = params::utmZone();
678 const bool northp = params::utmNorthHemisphere();
679 converter = [zone, northp](const math::Vec3& pos, double& lat, double& lon, double& alt)
680 {
681 double gamma, k;
682 GeographicLib::UTMUPS::Reverse(zone, northp, pos.x, pos.y, lat, lon, gamma, k);
683 alt = pos.z; // Altitude is given directly in the z-coordinate
684 };
685 break;
686 }
688 {
689 const auto& earth = GeographicLib::Geocentric::WGS84();
690 converter = [&earth](const math::Vec3& pos, double& lat, double& lon, double& alt)
691 { earth.Reverse(pos.x, pos.y, pos.z, lat, lon, alt); };
692 break;
693 }
694 }
695
696 map<const radar::Platform*, vector<const radar::Object*>> platform_to_objects;
697 const auto group_objects = [&](const auto& objectCollection)
698 {
699 for (const auto& obj_ptr : objectCollection)
700 {
701 platform_to_objects[obj_ptr->getPlatform()].push_back(obj_ptr.get());
702 }
703 };
704
705 group_objects(world.getReceivers());
706 group_objects(world.getTransmitters());
707 group_objects(world.getTargets());
708
710 {
711 bool ref_set = false;
712 for (const auto& platform : platform_to_objects | views::keys)
713 {
714 if (!platform->getMotionPath()->getCoords().empty())
715 {
716 const math::Vec3& first_pos = platform->getMotionPath()->getCoords().front().pos;
717 converter(first_pos, reference_latitude, reference_longitude, reference_altitude);
718 ref_set = true;
719 break;
720 }
721 }
722 if (!ref_set) // Fallback if no platforms or no waypoints
723 {
724 reference_latitude = params::originLatitude(); // UCT default
725 reference_longitude = params::originLongitude();
726 reference_altitude = params::originAltitude();
727 }
728 }
729
730 std::ofstream kml_file(outputKmlPath.c_str());
731 if (!kml_file.is_open())
732 {
733 LOG(logging::Level::ERROR, "Error opening output KML file {}", outputKmlPath);
734 return false;
735 }
736
737 writeKmlHeaderAndStyles(kml_file);
738
739 kml_file << " <Folder>\n";
740 kml_file << " <name>Reference Coordinate</name>\n";
741 kml_file << " <description>Placemarks for various elements in the FERSXML file. All Placemarks are "
742 "situated relative to this reference point.</description>\n";
743 kml_file << " <LookAt>\n";
744 kml_file << " <longitude>" << reference_longitude << "</longitude>\n";
745 kml_file << " <latitude>" << reference_latitude << "</latitude>\n";
746 kml_file << " <altitude>" << reference_altitude << "</altitude>\n";
747 kml_file << " <heading>-148.41</heading><tilt>40.55</tilt><range>10000</range>\n";
748 kml_file << " </LookAt>\n";
749
750 const std::string platform_indent = " ";
751 for (const auto& [platform, objects] : platform_to_objects)
752 {
753 processPlatform(platform, objects, kml_file, converter, reference_altitude, platform_indent);
754 }
755
756 kml_file << " </Folder>\n";
757 kml_file << "</Document>\n";
758 kml_file << "</kml>\n";
759 kml_file.close();
760 }
761 catch (const std::exception& e)
762 {
763 LOG(logging::Level::ERROR, "Error generating KML file: {}", e.what());
764 return false;
765 }
766 catch (...)
767 {
768 LOG(logging::Level::ERROR, "Unknown error occurred while generating KML file.");
769 return false;
770 }
771 return true;
772 }
773}
Header file defining various types of antennas and their gain patterns.
Abstract base class representing an antenna.
std::string getName() const noexcept
Retrieves the name of the antenna.
Represents a Gaussian-shaped antenna gain pattern.
RealType getAzimuthScale() const noexcept
Gets the azimuth scale factor.
Represents an antenna whose gain pattern is loaded from a HDF5 file.
Represents an isotropic antenna with uniform gain in all directions.
Represents a parabolic reflector antenna.
RealType getDiameter() const noexcept
Gets the diameter of the parabolic reflector.
Represents a sinc function-based antenna gain pattern.
Represents a square horn antenna.
RealType getDimension() const noexcept
Gets the dimension of the square horn.
Represents an antenna whose gain pattern is defined by an XML file.
The World class manages the simulator environment.
Definition world.h:38
const std::vector< std::unique_ptr< radar::Target > > & getTargets() const noexcept
Retrieves the list of radar targets.
Definition world.h:143
const std::vector< std::unique_ptr< radar::Transmitter > > & getTransmitters() const noexcept
Retrieves the list of radar transmitters.
Definition world.h:163
const std::vector< std::unique_ptr< radar::Receiver > > & getReceivers() const noexcept
Retrieves the list of radar receivers.
Definition world.h:153
Represents a path with coordinates and allows for various interpolation methods.
Definition path.h:30
Vec3 getPosition(RealType t) const
Retrieves the position at a given time along the path.
Definition path.cpp:35
const std::vector< Coord > & getCoords() const noexcept
Gets the list of coordinates in the path.
Definition path.h:83
InterpType getType() const noexcept
Retrieves the current interpolation type of the path.
Definition path.h:76
SVec3 getPosition(RealType t) const
Gets the rotational position at a given time.
A class representing a vector in spherical coordinates.
RealType elevation
The elevation angle of the vector.
RealType azimuth
The azimuth angle of the vector.
A class representing a vector in rectangular coordinates.
RealType x
The x component of the vector.
RealType z
The z component of the vector.
RealType y
The y component of the vector.
Represents a simulation platform with motion and rotation paths.
Definition platform.h:31
math::Path * getMotionPath() const noexcept
Gets the motion path of the platform.
Definition platform.h:59
const std::string & getName() const noexcept
Gets the name of the platform.
Definition platform.h:89
math::RotationPath * getRotationPath() const noexcept
Gets the rotation path of the platform.
Definition platform.h:66
Represents a radar system on a platform.
Definition radar_obj.h:46
Manages radar signal reception and response processing.
Definition receiver.h:36
Represents a radar transmitter system.
Definition transmitter.h:32
static bool generateKml(const core::World &world, const std::string &outputKmlPath)
Generates a KML file from a pre-built simulation world.
Global configuration file for the project.
constexpr RealType EPSILON
Machine epsilon for real numbers.
Definition config.h:51
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
Coordinate and rotation structure operations.
KML file generator for geographical visualization of FERS scenarios.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
@ WARNING
Warning level for potentially harmful situations.
@ INFO
Info level for informational messages.
@ ERROR
Error level for error events.
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:91
double originLongitude() noexcept
Definition parameters.h:238
int utmZone() noexcept
Definition parameters.h:274
@ UTM
Universal Transverse Mercator.
@ ENU
East-North-Up local tangent plane (default)
@ ECEF
Earth-Centered, Earth-Fixed.
CoordinateFrame coordinateFrame() noexcept
Definition parameters.h:272
double originLatitude() noexcept
Definition parameters.h:236
bool utmNorthHemisphere() noexcept
Definition parameters.h:276
Parameters params
Definition parameters.h:73
RealType c() noexcept
Get the speed of light.
Definition parameters.h:79
double originAltitude() noexcept
Definition parameters.h:240
Defines the Parameters struct and provides methods for managing simulation parameters.
Provides the definition and functionality of the Path class for handling coordinate-based paths with ...
Defines the Platform class used in radar simulation.
Defines the Radar class and associated functionality.
Classes for handling radar waveforms and signals.
Radar Receiver class for managing signal reception and response handling.
std::string simulation_name
The name of the simulation, from the XML.
Definition parameters.h:62
Defines classes for radar targets and their Radar Cross-Section (RCS) models.
Header file for the Transmitter class in the radar namespace.
Header file for the World class in the simulator.