Skip to main content

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}