Vulcan
Aerospace Engineering Utilities Built on Janus
Loading...
Searching...
No Matches
GeodesicUtils.hpp
Go to the documentation of this file.
1// Vulcan Geodesic Utilities
2// Pure geometric computations on the WGS84 ellipsoid
3#pragma once
4
9
10#include <janus/math/Arithmetic.hpp>
11#include <janus/math/Logic.hpp>
12#include <janus/math/Trig.hpp>
13
15
16// =============================================================================
17// Distance Calculations
18// =============================================================================
19
35template <typename Scalar>
36Scalar haversine_distance(const LLA<Scalar> &lla1, const LLA<Scalar> &lla2,
37 double radius = constants::earth::R_mean) {
38 const Scalar dlat = lla2.lat - lla1.lat;
39 const Scalar dlon = lla2.lon - lla1.lon;
40
41 const Scalar sin_dlat_2 = janus::sin(dlat / 2.0);
42 const Scalar sin_dlon_2 = janus::sin(dlon / 2.0);
43
44 const Scalar a = sin_dlat_2 * sin_dlat_2 + janus::cos(lla1.lat) *
45 janus::cos(lla2.lat) *
46 sin_dlon_2 * sin_dlon_2;
47
48 const Scalar c = 2.0 * janus::atan2(janus::sqrt(a), janus::sqrt(1.0 - a));
49
50 return radius * c;
51}
52
69template <typename Scalar>
70Scalar great_circle_distance(const LLA<Scalar> &lla1, const LLA<Scalar> &lla2,
71 const EarthModel &m = EarthModel::WGS84()) {
72 const double a = m.a;
73 const double b = m.b;
74 const double f = m.f;
75
76 // Reduced latitudes
77 const Scalar U1 = janus::atan((1.0 - f) * janus::tan(lla1.lat));
78 const Scalar U2 = janus::atan((1.0 - f) * janus::tan(lla2.lat));
79
80 const Scalar sin_U1 = janus::sin(U1);
81 const Scalar cos_U1 = janus::cos(U1);
82 const Scalar sin_U2 = janus::sin(U2);
83 const Scalar cos_U2 = janus::cos(U2);
84
85 const Scalar L = lla2.lon - lla1.lon;
86
87 // Iterative solution
88 Scalar lambda = L;
89 Scalar sin_sigma, cos_sigma, sigma;
90 Scalar sin_alpha, cos2_alpha, cos_2sigma_m;
91
92 // Fixed iteration count for symbolic compatibility
93 constexpr int max_iterations = 20;
94
95 for (int i = 0; i < max_iterations; ++i) {
96 const Scalar sin_lambda = janus::sin(lambda);
97 const Scalar cos_lambda = janus::cos(lambda);
98
99 const Scalar term1 = cos_U2 * sin_lambda;
100 const Scalar term2 = cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_lambda;
101 sin_sigma = janus::sqrt(term1 * term1 + term2 * term2);
102
103 cos_sigma = sin_U1 * sin_U2 + cos_U1 * cos_U2 * cos_lambda;
104 sigma = janus::atan2(sin_sigma, cos_sigma);
105
106 // sin(α) = cos(U₁)cos(U₂)sin(λ) / sin(σ)
107 // Handle sin_sigma ≈ 0 (coincident points)
108 sin_alpha = cos_U1 * cos_U2 * sin_lambda / (sin_sigma + 1e-15);
109 cos2_alpha = 1.0 - sin_alpha * sin_alpha;
110
111 // cos(2σₘ) = cos(σ) - 2sin(U₁)sin(U₂) / cos²(α)
112 // Handle cos2_alpha ≈ 0 (equatorial line)
113 cos_2sigma_m = cos_sigma - 2.0 * sin_U1 * sin_U2 / (cos2_alpha + 1e-15);
114
115 const Scalar C =
116 f / 16.0 * cos2_alpha * (4.0 + f * (4.0 - 3.0 * cos2_alpha));
117
118 const Scalar lambda_prev = lambda;
119 lambda = L + (1.0 - C) * f * sin_alpha *
120 (sigma +
121 C * sin_sigma *
122 (cos_2sigma_m +
123 C * cos_sigma *
124 (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)));
125
126 // Note: In symbolic mode, we don't break early
127 // For numeric mode, could add convergence check here
128 (void)lambda_prev;
129 }
130
131 // Calculate distance
132 const Scalar u2 = cos2_alpha * (a * a - b * b) / (b * b);
133 const Scalar A =
134 1.0 +
135 u2 / 16384.0 * (4096.0 + u2 * (-768.0 + u2 * (320.0 - 175.0 * u2)));
136 const Scalar B =
137 u2 / 1024.0 * (256.0 + u2 * (-128.0 + u2 * (74.0 - 47.0 * u2)));
138
139 const Scalar cos2_2sigma_m = cos_2sigma_m * cos_2sigma_m;
140 const Scalar delta_sigma =
141 B * sin_sigma *
142 (cos_2sigma_m +
143 B / 4.0 *
144 (cos_sigma * (-1.0 + 2.0 * cos2_2sigma_m) -
145 B / 6.0 * cos_2sigma_m * (-3.0 + 4.0 * sin_sigma * sin_sigma) *
146 (-3.0 + 4.0 * cos2_2sigma_m)));
147
148 return b * A * (sigma - delta_sigma);
149}
150
151// =============================================================================
152// Bearing Calculations
153// =============================================================================
154
165template <typename Scalar>
166Scalar
167initial_bearing(const LLA<Scalar> &lla1, const LLA<Scalar> &lla2,
168 [[maybe_unused]] const EarthModel &m = EarthModel::WGS84()) {
169 // For spherical approximation (sufficient for bearing):
170 // θ = atan2(sin(Δλ)cos(φ₂), cos(φ₁)sin(φ₂) - sin(φ₁)cos(φ₂)cos(Δλ))
171
172 const Scalar dlon = lla2.lon - lla1.lon;
173
174 const Scalar sin_dlon = janus::sin(dlon);
175 const Scalar cos_dlon = janus::cos(dlon);
176 const Scalar sin_lat1 = janus::sin(lla1.lat);
177 const Scalar cos_lat1 = janus::cos(lla1.lat);
178 const Scalar sin_lat2 = janus::sin(lla2.lat);
179 const Scalar cos_lat2 = janus::cos(lla2.lat);
180
181 const Scalar x = sin_dlon * cos_lat2;
182 const Scalar y = cos_lat1 * sin_lat2 - sin_lat1 * cos_lat2 * cos_dlon;
183
184 Scalar bearing = janus::atan2(x, y);
185
186 // Normalize to [0, 2π) using: bearing = bearing - floor(bearing / 2π) * 2π
187 constexpr double two_pi = 2.0 * constants::angle::pi;
188 // Add 2π first to handle negative bearings, then use atan2's periodicity
189 // Since atan2 returns [-π, π], adding 2π and taking modulo gives [0, 2π)
190 bearing = bearing + Scalar(two_pi);
191 // For symbolic compatibility, use: fmod(x, y) ≈ x - floor(x/y) * y
192 // But simpler: just use conditional approach with where
193 bearing = janus::where(bearing >= Scalar(two_pi), bearing - Scalar(two_pi),
194 bearing);
195 bearing =
196 janus::where(bearing < Scalar(0.0), bearing + Scalar(two_pi), bearing);
197
198 return bearing;
199}
200
211template <typename Scalar>
212Scalar final_bearing(const LLA<Scalar> &lla1, const LLA<Scalar> &lla2,
213 const EarthModel &m = EarthModel::WGS84()) {
214 // Final bearing is the reverse of initial bearing from lla2 to lla1, + 180°
215 Scalar bearing =
216 initial_bearing(lla2, lla1, m) + Scalar(constants::angle::pi);
217
218 // Normalize to [0, 2π)
219 constexpr double two_pi = 2.0 * constants::angle::pi;
220 bearing = janus::where(bearing >= Scalar(two_pi), bearing - Scalar(two_pi),
221 bearing);
222
223 return bearing;
224}
225
226// =============================================================================
227// Direct Geodesic Problem
228// =============================================================================
229
241template <typename Scalar>
242LLA<Scalar> destination_point(const LLA<Scalar> &lla, const Scalar &bearing,
243 const Scalar &distance,
244 const EarthModel &m = EarthModel::WGS84()) {
245 const double a = m.a;
246 const double b = m.b;
247 const double f = m.f;
248
249 const Scalar sin_bearing = janus::sin(bearing);
250 const Scalar cos_bearing = janus::cos(bearing);
251
252 // Reduced latitude
253 const Scalar U1 = janus::atan((1.0 - f) * janus::tan(lla.lat));
254 const Scalar sin_U1 = janus::sin(U1);
255 const Scalar cos_U1 = janus::cos(U1);
256
257 // σ₁ = atan2(tan(U₁), cos(α₁))
258 const Scalar sigma1 = janus::atan2(janus::tan(U1), cos_bearing);
259
260 // sin(α) = cos(U₁)sin(α₁)
261 const Scalar sin_alpha = cos_U1 * sin_bearing;
262 const Scalar cos2_alpha = 1.0 - sin_alpha * sin_alpha;
263
264 const Scalar u2 = cos2_alpha * (a * a - b * b) / (b * b);
265 const Scalar A =
266 1.0 +
267 u2 / 16384.0 * (4096.0 + u2 * (-768.0 + u2 * (320.0 - 175.0 * u2)));
268 const Scalar B =
269 u2 / 1024.0 * (256.0 + u2 * (-128.0 + u2 * (74.0 - 47.0 * u2)));
270
271 // Initial guess for σ
272 Scalar sigma = distance / (b * A);
273
274 // Iterate to find σ
275 Scalar cos_2sigma_m, sin_sigma, cos_sigma;
276
277 constexpr int max_iterations = 20;
278 for (int i = 0; i < max_iterations; ++i) {
279 cos_2sigma_m = janus::cos(2.0 * sigma1 + sigma);
280 sin_sigma = janus::sin(sigma);
281 cos_sigma = janus::cos(sigma);
282
283 const Scalar cos2_2sigma_m = cos_2sigma_m * cos_2sigma_m;
284 const Scalar delta_sigma =
285 B * sin_sigma *
286 (cos_2sigma_m + B / 4.0 *
287 (cos_sigma * (-1.0 + 2.0 * cos2_2sigma_m) -
288 B / 6.0 * cos_2sigma_m *
289 (-3.0 + 4.0 * sin_sigma * sin_sigma) *
290 (-3.0 + 4.0 * cos2_2sigma_m)));
291
292 const Scalar sigma_prev = sigma;
293 sigma = distance / (b * A) + delta_sigma;
294 (void)sigma_prev;
295 }
296
297 // Compute final values
298 sin_sigma = janus::sin(sigma);
299 cos_sigma = janus::cos(sigma);
300 cos_2sigma_m = janus::cos(2.0 * sigma1 + sigma);
301
302 // Latitude - use x*x instead of pow(x, 2) for type compatibility
303 const Scalar temp = sin_U1 * sin_sigma - cos_U1 * cos_sigma * cos_bearing;
304 const Scalar lat2 = janus::atan2(
305 sin_U1 * cos_sigma + cos_U1 * sin_sigma * cos_bearing,
306 (1.0 - f) * janus::sqrt(sin_alpha * sin_alpha + temp * temp));
307
308 // Longitude
309 const Scalar lambda =
310 janus::atan2(sin_sigma * sin_bearing,
311 cos_U1 * cos_sigma - sin_U1 * sin_sigma * cos_bearing);
312
313 const Scalar C =
314 f / 16.0 * cos2_alpha * (4.0 + f * (4.0 - 3.0 * cos2_alpha));
315 const Scalar L =
316 lambda -
317 (1.0 - C) * f * sin_alpha *
318 (sigma +
319 C * sin_sigma *
320 (cos_2sigma_m +
321 C * cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)));
322
323 const Scalar lon2 = lla.lon + L;
324
325 return LLA<Scalar>(lon2, lat2, lla.alt);
326}
327
328// =============================================================================
329// Horizon & Visibility
330// =============================================================================
331
344template <typename Scalar>
345Scalar horizon_distance(const Scalar &altitude,
346 const EarthModel &m = EarthModel::WGS84()) {
347 // Use mean radius for simplicity
348 const double R = (2.0 * m.a + m.b) / 3.0;
349
350 // d = √(2Rh + h²) = √(h(2R + h))
351 return janus::sqrt(altitude * (2.0 * R + altitude));
352}
353
367template <typename Scalar>
368Scalar is_visible(const LLA<Scalar> &lla_observer,
369 const LLA<Scalar> &lla_target,
370 const EarthModel &m = EarthModel::WGS84()) {
371 // Horizon distance from each point
372 const Scalar h1 = horizon_distance(lla_observer.alt, m);
373 const Scalar h2 = horizon_distance(lla_target.alt, m);
374
375 // Ground distance between points
376 const Scalar ground_dist =
377 haversine_distance(lla_observer, lla_target, (2.0 * m.a + m.b) / 3.0);
378
379 // Visible if combined horizon distances exceed ground distance
380 return (h1 + h2) - ground_dist;
381}
382
383// =============================================================================
384// Ray-Ellipsoid Intersection
385// =============================================================================
386
388template <typename Scalar> struct RayIntersection {
389 Scalar hit;
390 Scalar t_near;
391 Scalar t_far;
392 Vec3<Scalar> point_near;
393 Vec3<Scalar> point_far;
394};
395
409template <typename Scalar>
411ray_ellipsoid_intersection(const Vec3<Scalar> &origin,
412 const Vec3<Scalar> &direction,
413 const EarthModel &m = EarthModel::WGS84()) {
414
415 const double a = m.a;
416 const double b = m.b;
417 const double a2 = a * a;
418 const double b2 = b * b;
419
420 // Normalize direction
421 const Scalar dir_norm =
422 janus::sqrt(direction(0) * direction(0) + direction(1) * direction(1) +
423 direction(2) * direction(2));
424 const Vec3<Scalar> d = direction / dir_norm;
425
426 // Ray origin components
427 const Scalar ox = origin(0);
428 const Scalar oy = origin(1);
429 const Scalar oz = origin(2);
430
431 // Ray direction components
432 const Scalar dx = d(0);
433 const Scalar dy = d(1);
434 const Scalar dz = d(2);
435
436 // Quadratic coefficients for ellipsoid intersection
437 // (x/a)² + (y/a)² + (z/b)² = 1
438 // Substitute x = ox + t*dx, etc.
439 const Scalar A = dx * dx / a2 + dy * dy / a2 + dz * dz / b2;
440 const Scalar B = 2.0 * (ox * dx / a2 + oy * dy / a2 + oz * dz / b2);
441 const Scalar C = ox * ox / a2 + oy * oy / a2 + oz * oz / b2 - 1.0;
442
443 // Discriminant
444 const Scalar discriminant = B * B - 4.0 * A * C;
445
446 // Compute intersection parameters
447 const Scalar sqrt_disc = janus::sqrt(janus::abs(discriminant) + 1e-15);
448 const Scalar t_near = (-B - sqrt_disc) / (2.0 * A);
449 const Scalar t_far = (-B + sqrt_disc) / (2.0 * A);
450
451 // Compute intersection points
452 Vec3<Scalar> point_near;
453 point_near(0) = origin(0) + t_near * d(0);
454 point_near(1) = origin(1) + t_near * d(1);
455 point_near(2) = origin(2) + t_near * d(2);
456
457 Vec3<Scalar> point_far;
458 point_far(0) = origin(0) + t_far * d(0);
459 point_far(1) = origin(1) + t_far * d(1);
460 point_far(2) = origin(2) + t_far * d(2);
461
463 result.hit = discriminant; // > 0 means hit
464 result.t_near = t_near;
465 result.t_far = t_far;
466 result.point_near = point_near;
467 result.point_far = point_far;
468
469 return result;
470}
471
472} // namespace vulcan::geodetic
constexpr double pi
Pi.
Definition Constants.hpp:153
constexpr double R_mean
Mean radius [m].
Definition Constants.hpp:22
Definition GeodesicUtils.hpp:14
Scalar final_bearing(const LLA< Scalar > &lla1, const LLA< Scalar > &lla2, const EarthModel &m=EarthModel::WGS84())
Definition GeodesicUtils.hpp:212
RayIntersection< Scalar > ray_ellipsoid_intersection(const Vec3< Scalar > &origin, const Vec3< Scalar > &direction, const EarthModel &m=EarthModel::WGS84())
Definition GeodesicUtils.hpp:411
Scalar horizon_distance(const Scalar &altitude, const EarthModel &m=EarthModel::WGS84())
Definition GeodesicUtils.hpp:345
LLA< Scalar > destination_point(const LLA< Scalar > &lla, const Scalar &bearing, const Scalar &distance, const EarthModel &m=EarthModel::WGS84())
Definition GeodesicUtils.hpp:242
Scalar initial_bearing(const LLA< Scalar > &lla1, const LLA< Scalar > &lla2, const EarthModel &m=EarthModel::WGS84())
Definition GeodesicUtils.hpp:167
Scalar haversine_distance(const LLA< Scalar > &lla1, const LLA< Scalar > &lla2, double radius=constants::earth::R_mean)
Definition GeodesicUtils.hpp:36
Scalar great_circle_distance(const LLA< Scalar > &lla1, const LLA< Scalar > &lla2, const EarthModel &m=EarthModel::WGS84())
Definition GeodesicUtils.hpp:70
Scalar is_visible(const LLA< Scalar > &lla_observer, const LLA< Scalar > &lla_target, const EarthModel &m=EarthModel::WGS84())
Definition GeodesicUtils.hpp:368
Definition EarthModel.hpp:27
static constexpr EarthModel WGS84()
WGS84 reference ellipsoid (most common for GPS/navigation).
Definition EarthModel.hpp:45
Definition Geodetic.hpp:27
Scalar lon
Longitude [rad], positive East, range [-π, π].
Definition Geodetic.hpp:28
Scalar alt
Altitude above ellipsoid [m].
Definition Geodetic.hpp:30
Scalar lat
Geodetic latitude [rad], range [-π/2, π/2].
Definition Geodetic.hpp:29
Result of ray-ellipsoid intersection.
Definition GeodesicUtils.hpp:388
Vec3< Scalar > point_far
Far intersection point (ECEF).
Definition GeodesicUtils.hpp:393
Scalar t_far
Parameter for far intersection point.
Definition GeodesicUtils.hpp:391
Scalar t_near
Parameter for near intersection point.
Definition GeodesicUtils.hpp:390
Scalar hit
Definition GeodesicUtils.hpp:389
Vec3< Scalar > point_near
Near intersection point (ECEF).
Definition GeodesicUtils.hpp:392