rustial_engine/math/ellipsoid.rs
1//! Reference ellipsoid definitions and derived geometric constants.
2//!
3//! # What is a reference ellipsoid?
4//!
5//! A reference ellipsoid is a mathematically defined surface that
6//! approximates the shape of the Earth (or another body). It is
7//! defined by two independent parameters:
8//!
9//! - **Semi-major axis `a`** (equatorial radius), in meters.
10//! - **Inverse flattening `1/f`** (the standard geodetic convention).
11//!
12//! All other geometric constants (`b`, `e2`, `e'2`) are derived from
13//! these two. See [`Ellipsoid::from_a_inv_f`] for the formulae.
14//!
15//! # Pre-built constants
16//!
17//! | Constant | Standard | Usage |
18//! |----------|----------|-------|
19//! | [`Ellipsoid::WGS84`] | NIMA TR8350.2 / EPSG:4326 | GPS, global mapping |
20//! | [`Ellipsoid::GRS80`] | IUGG 1980 / EPSG:7019 | NAD83, ETRS89 |
21//!
22//! # Consumers
23//!
24//! This type is used by [`geodesic_distance_on`](crate::geodesic_distance_on),
25//! [`geodesic_destination_on`](crate::geodesic_destination_on),
26//! [`WebMercator`](crate::WebMercator), and
27//! [`Equirectangular`](crate::Equirectangular).
28
29/// A reference ellipsoid defined by its semi-major axis and flattening.
30///
31/// All four fields are stored for fast access, but only two are
32/// independent (`a` and `f`). The derived fields `b` and `e2` are
33/// computed at construction time (in `const` context) to avoid
34/// repeated work in hot paths.
35///
36/// # Field summary
37///
38/// | Field | Formula | Description |
39/// |-------|---------|-------------|
40/// | `a` | (input) | Semi-major axis (equatorial radius), meters |
41/// | `f` | `1 / inv_f` | Flattening |
42/// | `b` | `a * (1 - f)` | Semi-minor axis (polar radius), meters |
43/// | `e2` | `2f - f^2` | First eccentricity squared |
44#[derive(Debug, Clone, Copy, PartialEq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct Ellipsoid {
47 /// Semi-major axis (equatorial radius) in meters.
48 pub a: f64,
49 /// Flattening: `f = (a - b) / a`.
50 pub f: f64,
51 /// Semi-minor axis (polar radius) in meters: `b = a * (1 - f)`.
52 pub b: f64,
53 /// First eccentricity squared: `e2 = 2f - f^2`.
54 pub e2: f64,
55}
56
57impl Ellipsoid {
58 /// Construct an ellipsoid from semi-major axis `a` (meters) and
59 /// inverse flattening `inv_f` (dimensionless).
60 ///
61 /// Inverse flattening is the standard geodetic convention because
62 /// `f` itself is a very small number (~0.003 for Earth) and
63 /// `1/f` (~298.257) is easier to read and compare across standards.
64 ///
65 /// # Derived quantities
66 ///
67 /// ```text
68 /// f = 1 / inv_f
69 /// b = a * (1 - f)
70 /// e2 = 2f - f^2
71 /// ```
72 #[inline]
73 pub const fn from_a_inv_f(a: f64, inv_f: f64) -> Self {
74 let f = 1.0 / inv_f;
75 let b = a * (1.0 - f);
76 let e2 = 2.0 * f - f * f;
77 Self { a, f, b, e2 }
78 }
79
80 /// WGS-84 ellipsoid (EPSG:4326, NIMA TR8350.2).
81 ///
82 /// The World Geodetic System 1984 is the reference frame used by
83 /// GPS and the default for all rustial coordinate operations.
84 ///
85 /// - `a = 6 378 137.0 m`
86 /// - `1/f = 298.257 223 563`
87 pub const WGS84: Self = Self::from_a_inv_f(6_378_137.0, 298.257_223_563);
88
89 /// GRS-80 ellipsoid (EPSG:7019, IUGG 1980).
90 ///
91 /// The Geodetic Reference System 1980 is used by NAD83 (North
92 /// America) and ETRS89 (Europe). It is nearly identical to WGS-84
93 /// (difference in `1/f` is ~0.0001) but is a distinct definition.
94 ///
95 /// - `a = 6 378 137.0 m`
96 /// - `1/f = 298.257 222 101`
97 pub const GRS80: Self = Self::from_a_inv_f(6_378_137.0, 298.257_222_101);
98
99 /// Second eccentricity squared: `e'2 = (a^2 - b^2) / b^2`.
100 ///
101 /// Used in Vincenty geodesic formulae and some map projections.
102 #[inline]
103 pub fn second_eccentricity_squared(&self) -> f64 {
104 (self.a * self.a - self.b * self.b) / (self.b * self.b)
105 }
106
107 /// Radius of curvature in the prime vertical (N) at geodetic
108 /// latitude `lat_rad` (radians).
109 ///
110 /// `N = a / sqrt(1 - e2 * sin^2(lat))`
111 ///
112 /// This is the distance from the surface to the polar axis along
113 /// the ellipsoid normal. Used in geographic-to-ECEF conversion
114 /// and UTM projection.
115 #[inline]
116 pub fn radius_of_curvature_n(&self, lat_rad: f64) -> f64 {
117 let sin_lat = lat_rad.sin();
118 self.a / (1.0 - self.e2 * sin_lat * sin_lat).sqrt()
119 }
120
121 /// Whether this ellipsoid is effectively a sphere (`f == 0`).
122 ///
123 /// Can be used to select simplified (spherical) code paths where
124 /// the ellipsoidal correction is negligible.
125 #[inline]
126 pub fn is_sphere(&self) -> bool {
127 self.f == 0.0
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 // -- WGS-84 reference values ------------------------------------------
136
137 #[test]
138 fn wgs84_constants() {
139 let e = Ellipsoid::WGS84;
140 assert!((e.a - 6_378_137.0).abs() < 1e-6);
141 assert!((e.b - 6_356_752.314_245_179).abs() < 1e-3);
142 assert!((e.f - 1.0 / 298.257_223_563).abs() < 1e-15);
143 assert!((e.e2 - 0.006_694_379_990_141_316).abs() < 1e-12);
144 }
145
146 #[test]
147 fn second_eccentricity() {
148 let e = Ellipsoid::WGS84;
149 let ep2 = e.second_eccentricity_squared();
150 assert!((ep2 - 0.006_739_496_742_276_434).abs() < 1e-12);
151 }
152
153 // -- GRS-80 reference values ------------------------------------------
154
155 #[test]
156 fn grs80_constants() {
157 let e = Ellipsoid::GRS80;
158 assert!((e.a - 6_378_137.0).abs() < 1e-6);
159 assert!((e.f - 1.0 / 298.257_222_101).abs() < 1e-15);
160 // GRS-80 and WGS-84 share the same `a` but differ in `f`.
161 assert!(e.f != Ellipsoid::WGS84.f);
162 // Difference is tiny (~1e-10).
163 assert!((e.f - Ellipsoid::WGS84.f).abs() < 1e-8);
164 }
165
166 // -- Custom ellipsoid -------------------------------------------------
167
168 #[test]
169 fn custom_ellipsoid_roundtrip() {
170 let e = Ellipsoid::from_a_inv_f(6_400_000.0, 300.0);
171 assert!((e.a - 6_400_000.0).abs() < 1e-6);
172 assert!((e.f - 1.0 / 300.0).abs() < 1e-15);
173 assert!((e.b - e.a * (1.0 - e.f)).abs() < 1e-6);
174 assert!((e.e2 - (2.0 * e.f - e.f * e.f)).abs() < 1e-15);
175 }
176
177 // -- Radius of curvature N --------------------------------------------
178
179 #[test]
180 fn radius_n_at_equator() {
181 let e = Ellipsoid::WGS84;
182 // At the equator (lat=0), N = a.
183 let n = e.radius_of_curvature_n(0.0);
184 assert!((n - e.a).abs() < 1e-6);
185 }
186
187 #[test]
188 fn radius_n_at_pole() {
189 let e = Ellipsoid::WGS84;
190 // At the pole (lat=90 deg), N = a / sqrt(1 - e2) = a^2 / b.
191 let n = e.radius_of_curvature_n(std::f64::consts::FRAC_PI_2);
192 let expected = e.a * e.a / e.b;
193 assert!((n - expected).abs() < 1e-3);
194 }
195
196 // -- Sphere detection -------------------------------------------------
197
198 #[test]
199 fn wgs84_is_not_sphere() {
200 assert!(!Ellipsoid::WGS84.is_sphere());
201 }
202
203 #[test]
204 fn perfect_sphere() {
205 // inv_f = infinity => f = 0 => sphere.
206 let s = Ellipsoid::from_a_inv_f(6_371_000.0, f64::INFINITY);
207 assert!(s.is_sphere());
208 assert!((s.a - s.b).abs() < 1e-6);
209 assert!(s.e2.abs() < 1e-15);
210 }
211}