1mod datum;
30
31use core::fmt;
32
33use crate::error::{KernelError, Result};
34use crate::math;
35use crate::position::{Latitude, Longitude, Position};
36use crate::units::Distance;
37
38pub use datum::{Datum, Helmert};
39
40#[derive(Debug, Clone, Copy, PartialEq)]
46#[cfg_attr(
47 feature = "serde",
48 derive(serde::Serialize, serde::Deserialize),
49 serde(try_from = "StoredEllipsoid", into = "StoredEllipsoid")
50)]
51pub struct Ellipsoid {
52 semi_major_metres: f64,
53 inverse_flattening: f64,
54}
55
56impl Ellipsoid {
57 pub const WGS84: Self = Self {
59 semi_major_metres: 6_378_137.0,
60 inverse_flattening: 298.257_223_563,
61 };
62
63 pub const GRS80: Self = Self {
66 semi_major_metres: 6_378_137.0,
67 inverse_flattening: 298.257_222_101,
68 };
69
70 pub const INTERNATIONAL_1924: Self = Self {
72 semi_major_metres: 6_378_388.0,
73 inverse_flattening: 297.0,
74 };
75
76 pub const CLARKE_1866: Self = Self {
80 semi_major_metres: 6_378_206.4,
81 inverse_flattening: 294.978_698_213_898,
82 };
83
84 pub const AIRY_1830: Self = Self {
86 semi_major_metres: 6_377_563.396,
87 inverse_flattening: 299.324_964_6,
88 };
89
90 pub const KRASSOWSKY_1940: Self = Self {
92 semi_major_metres: 6_378_245.0,
93 inverse_flattening: 298.3,
94 };
95
96 pub const BESSEL_1841: Self = Self {
98 semi_major_metres: 6_377_397.155,
99 inverse_flattening: 299.152_812_8,
100 };
101
102 pub const AUSTRALIAN_NATIONAL: Self = Self {
105 semi_major_metres: 6_378_160.0,
106 inverse_flattening: 298.25,
107 };
108
109 pub fn new(semi_major_axis: Distance, inverse_flattening: f64) -> Result<Self> {
116 Self::from_raw(semi_major_axis.metres(), inverse_flattening)
117 }
118
119 fn from_raw(semi_major_metres: f64, inverse_flattening: f64) -> Result<Self> {
122 if semi_major_metres.is_nan() || semi_major_metres <= 0.0 {
123 return Err(KernelError::OutOfRange {
124 parameter: "semi-major axis",
125 value: semi_major_metres,
126 min: f64::MIN_POSITIVE,
127 max: f64::MAX,
128 });
129 }
130 if inverse_flattening.is_nan() || inverse_flattening < 1.0 {
131 return Err(KernelError::OutOfRange {
132 parameter: "inverse flattening",
133 value: inverse_flattening,
134 min: 1.0,
135 max: f64::INFINITY,
136 });
137 }
138 Ok(Self {
139 semi_major_metres,
140 inverse_flattening,
141 })
142 }
143
144 #[must_use]
146 pub fn semi_major_axis(&self) -> Distance {
147 Distance::from_metres(self.semi_major_metres).unwrap_or(Distance::ZERO)
149 }
150
151 #[must_use]
153 pub fn semi_minor_axis(&self) -> Distance {
154 Distance::from_metres(self.semi_minor_metres()).unwrap_or(Distance::ZERO)
155 }
156
157 #[must_use]
159 pub fn flattening(&self) -> f64 {
160 1.0 / self.inverse_flattening
161 }
162
163 #[must_use]
165 pub const fn inverse_flattening(&self) -> f64 {
166 self.inverse_flattening
167 }
168
169 #[must_use]
171 pub fn first_eccentricity_squared(&self) -> f64 {
172 let f = self.flattening();
173 f * (2.0 - f)
174 }
175
176 fn semi_minor_metres(&self) -> f64 {
177 self.semi_major_metres * (1.0 - self.flattening())
178 }
179
180 fn second_eccentricity_squared(&self) -> f64 {
182 let e2 = self.first_eccentricity_squared();
183 e2 / (1.0 - e2)
184 }
185
186 fn prime_vertical_radius(&self, sin_latitude: f64) -> f64 {
188 self.semi_major_metres
189 / math::sqrt(1.0 - self.first_eccentricity_squared() * sin_latitude * sin_latitude)
190 }
191}
192
193#[non_exhaustive]
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
198#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199pub enum VerticalDatum {
200 Ellipsoid,
202 MeanSeaLevel,
204 ChartDatum,
206}
207
208#[cfg(feature = "serde")]
211#[derive(serde::Serialize, serde::Deserialize)]
212struct StoredEllipsoid {
213 semi_major_metres: f64,
214 inverse_flattening: f64,
215}
216
217#[cfg(feature = "serde")]
218impl TryFrom<StoredEllipsoid> for Ellipsoid {
219 type Error = KernelError;
220
221 fn try_from(stored: StoredEllipsoid) -> Result<Self> {
222 Self::from_raw(stored.semi_major_metres, stored.inverse_flattening)
223 }
224}
225
226#[cfg(feature = "serde")]
227impl From<Ellipsoid> for StoredEllipsoid {
228 fn from(ellipsoid: Ellipsoid) -> Self {
229 Self {
230 semi_major_metres: ellipsoid.semi_major_metres,
231 inverse_flattening: ellipsoid.inverse_flattening,
232 }
233 }
234}
235
236impl VerticalDatum {
237 const fn name(self) -> &'static str {
238 match self {
239 Self::Ellipsoid => "the ellipsoid",
240 Self::MeanSeaLevel => "mean sea level",
241 Self::ChartDatum => "chart datum",
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq)]
251#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
252pub struct Height {
253 value: Distance,
254 datum: VerticalDatum,
255}
256
257impl Height {
258 #[must_use]
260 pub const fn above_ellipsoid(value: Distance) -> Self {
261 Self {
262 value,
263 datum: VerticalDatum::Ellipsoid,
264 }
265 }
266
267 #[must_use]
269 pub const fn above_mean_sea_level(value: Distance) -> Self {
270 Self {
271 value,
272 datum: VerticalDatum::MeanSeaLevel,
273 }
274 }
275
276 #[must_use]
278 pub const fn above_chart_datum(value: Distance) -> Self {
279 Self {
280 value,
281 datum: VerticalDatum::ChartDatum,
282 }
283 }
284
285 #[must_use]
287 pub const fn new(value: Distance, datum: VerticalDatum) -> Self {
288 Self { value, datum }
289 }
290
291 #[must_use]
293 pub const fn value(&self) -> Distance {
294 self.value
295 }
296
297 #[must_use]
299 pub const fn datum(&self) -> VerticalDatum {
300 self.datum
301 }
302}
303
304impl fmt::Display for Height {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307 let precision = f.precision().unwrap_or(1);
308 write!(
309 f,
310 "{:.*} m above {}",
311 precision,
312 self.value.metres(),
313 self.datum.name()
314 )
315 }
316}
317
318#[derive(Debug, Clone, Copy, PartialEq)]
320#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
321pub struct GeodeticPoint {
322 position: Position,
323 height: Height,
324}
325
326impl GeodeticPoint {
327 #[must_use]
329 pub const fn new(position: Position, height: Height) -> Self {
330 Self { position, height }
331 }
332
333 #[must_use]
335 pub const fn position(&self) -> Position {
336 self.position
337 }
338
339 #[must_use]
341 pub const fn height(&self) -> Height {
342 self.height
343 }
344}
345
346impl fmt::Display for GeodeticPoint {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 write!(f, "{}, {}", self.position, self.height)
349 }
350}
351
352#[derive(Debug, Clone, Copy, PartialEq)]
359#[cfg_attr(
360 feature = "serde",
361 derive(serde::Serialize, serde::Deserialize),
362 serde(try_from = "StoredEcefPoint", into = "StoredEcefPoint")
363)]
364pub struct EcefPoint {
365 x: f64,
366 y: f64,
367 z: f64,
368}
369
370impl EcefPoint {
371 #[must_use]
373 pub fn new(x: Distance, y: Distance, z: Distance) -> Self {
374 Self {
375 x: x.metres(),
376 y: y.metres(),
377 z: z.metres(),
378 }
379 }
380
381 #[must_use]
383 pub fn x(&self) -> Distance {
384 Distance::from_metres(self.x).unwrap_or(Distance::ZERO)
385 }
386
387 #[must_use]
389 pub fn y(&self) -> Distance {
390 Distance::from_metres(self.y).unwrap_or(Distance::ZERO)
391 }
392
393 #[must_use]
395 pub fn z(&self) -> Distance {
396 Distance::from_metres(self.z).unwrap_or(Distance::ZERO)
397 }
398
399 pub fn from_geodetic(point: GeodeticPoint, ellipsoid: &Ellipsoid) -> Result<Self> {
406 if point.height.datum != VerticalDatum::Ellipsoid {
407 return Err(KernelError::VerticalDatumMismatch {
408 required: VerticalDatum::Ellipsoid,
409 found: point.height.datum,
410 });
411 }
412 let (sin_lat, cos_lat) = sin_cos(point.position.latitude().radians());
413 let (sin_lon, cos_lon) = sin_cos(point.position.longitude().radians());
414 let n = ellipsoid.prime_vertical_radius(sin_lat);
415 let h = point.height.value.metres();
416 let e2 = ellipsoid.first_eccentricity_squared();
417 Ok(Self {
418 x: (n + h) * cos_lat * cos_lon,
419 y: (n + h) * cos_lat * sin_lon,
420 z: (n * (1.0 - e2) + h) * sin_lat,
421 })
422 }
423
424 #[allow(clippy::many_single_char_names)]
438 pub fn to_geodetic(self, ellipsoid: &Ellipsoid) -> Result<GeodeticPoint> {
439 let a = ellipsoid.semi_major_metres;
440 let b = ellipsoid.semi_minor_metres();
441 let e2 = ellipsoid.first_eccentricity_squared();
442 let ep2 = ellipsoid.second_eccentricity_squared();
443
444 let p = math::hypot(self.x, self.y);
445 let r = math::hypot(p, self.z);
446 if r < f64::MIN_POSITIVE {
447 return Err(KernelError::Indeterminate {
448 quantity: "the geodetic position of the Earth's centre",
449 });
450 }
451 let longitude = Longitude::from_degrees(math::to_degrees(math::atan2(self.y, self.x)))?;
452
453 if p < f64::MIN_POSITIVE * a {
456 let latitude = if self.z < 0.0 {
457 Latitude::SOUTH_POLE
458 } else {
459 Latitude::NORTH_POLE
460 };
461 let height = Distance::from_metres(math::abs(self.z) - b)?;
462 return Ok(GeodeticPoint::new(
463 Position::new(latitude, longitude),
464 Height::above_ellipsoid(height),
465 ));
466 }
467
468 let tan_u = (b * self.z / (a * p)) * (1.0 + ep2 * b / r);
471 let cos_u = 1.0 / math::sqrt(1.0 + tan_u * tan_u);
472 let sin_u = tan_u * cos_u;
473 let latitude_radians = math::atan2(
474 self.z + ep2 * b * sin_u * sin_u * sin_u,
475 p - e2 * a * cos_u * cos_u * cos_u,
476 );
477 let (sin_lat, cos_lat) = sin_cos(latitude_radians);
478 let n = ellipsoid.prime_vertical_radius(sin_lat);
479 let height_metres = p * cos_lat + self.z * sin_lat - a * a / n;
480
481 let latitude = Latitude::from_degrees(math::to_degrees(latitude_radians))?;
482 let height = Distance::from_metres(height_metres)?;
483 Ok(GeodeticPoint::new(
484 Position::new(latitude, longitude),
485 Height::above_ellipsoid(height),
486 ))
487 }
488
489 #[must_use]
491 pub fn chord_to(&self, other: Self) -> Distance {
492 let chord = math::hypot(
493 math::hypot(other.x - self.x, other.y - self.y),
494 other.z - self.z,
495 );
496 Distance::from_metres(chord).unwrap_or(Distance::ZERO)
497 }
498}
499
500#[cfg(feature = "serde")]
502#[derive(serde::Serialize, serde::Deserialize)]
503struct StoredEcefPoint {
504 x: f64,
505 y: f64,
506 z: f64,
507}
508
509#[cfg(feature = "serde")]
510impl TryFrom<StoredEcefPoint> for EcefPoint {
511 type Error = KernelError;
512
513 fn try_from(stored: StoredEcefPoint) -> Result<Self> {
514 Ok(Self::new(
515 Distance::from_metres(stored.x)?,
516 Distance::from_metres(stored.y)?,
517 Distance::from_metres(stored.z)?,
518 ))
519 }
520}
521
522#[cfg(feature = "serde")]
523impl From<EcefPoint> for StoredEcefPoint {
524 fn from(point: EcefPoint) -> Self {
525 Self {
526 x: point.x,
527 y: point.y,
528 z: point.z,
529 }
530 }
531}
532
533fn sin_cos(radians: f64) -> (f64, f64) {
534 (math::sin(radians), math::cos(radians))
535}
536
537#[cfg(test)]
538#[allow(clippy::unwrap_used, clippy::float_cmp)]
539mod tests {
540 use super::*;
541 use alloc::format;
542
543 fn point(latitude: f64, longitude: f64, height: f64) -> GeodeticPoint {
544 GeodeticPoint::new(
545 Position::new(
546 Latitude::from_degrees(latitude).unwrap(),
547 Longitude::from_degrees(longitude).unwrap(),
548 ),
549 Height::above_ellipsoid(Distance::from_metres(height).unwrap()),
550 )
551 }
552
553 #[test]
554 fn wgs84_derived_constants_match_the_published_ones() {
555 let e = Ellipsoid::WGS84;
556 assert!((e.semi_minor_axis().metres() - 6_356_752.314_245).abs() < 1e-6);
557 assert!((e.first_eccentricity_squared() - 6.694_379_990_14e-3).abs() < 1e-14);
558 assert!((e.second_eccentricity_squared() - 6.739_496_742_28e-3).abs() < 1e-14);
559 assert_eq!(e.inverse_flattening(), 298.257_223_563);
560 assert!((Ellipsoid::GRS80.semi_minor_axis().metres() - 6_356_752.314_140).abs() < 1e-6);
561 }
562
563 #[test]
564 fn an_ellipsoid_must_be_a_plausible_shape() {
565 assert!(Ellipsoid::new(Distance::from_metres(0.0).unwrap(), 300.0).is_err());
566 assert!(Ellipsoid::new(Distance::from_metres(6.4e6).unwrap(), 0.5).is_err());
567 assert!(Ellipsoid::new(Distance::from_metres(6.4e6).unwrap(), f64::NAN).is_err());
568 let sphere =
570 Ellipsoid::new(Distance::from_metres(6_371_000.0).unwrap(), f64::INFINITY).unwrap();
571 assert_eq!(sphere.flattening(), 0.0);
572 assert_eq!(sphere.semi_minor_axis().metres(), 6_371_000.0);
573 }
574
575 #[test]
576 fn ecef_of_reference_points_matches_the_textbook() {
577 let origin = EcefPoint::from_geodetic(point(0.0, 0.0, 0.0), &Ellipsoid::WGS84).unwrap();
579 assert!((origin.x().metres() - 6_378_137.0).abs() < 1e-6);
580 assert!(origin.y().metres().abs() < 1e-9);
581 assert!(origin.z().metres().abs() < 1e-9);
582 let pole = EcefPoint::from_geodetic(point(90.0, 0.0, 0.0), &Ellipsoid::WGS84).unwrap();
584 assert!(pole.x().metres().abs() < 1e-6);
585 assert!((pole.z().metres() - 6_356_752.314_245).abs() < 1e-6);
586 let inland =
588 EcefPoint::from_geodetic(point(34.0, -117.0, 251.0), &Ellipsoid::WGS84).unwrap();
589 assert!((inland.x().metres() - -2_403_183.467).abs() < 1e-3);
590 assert!((inland.y().metres() - -4_716_513.119).abs() < 1e-3);
591 assert!((inland.z().metres() - 3_546_586.921).abs() < 1e-3);
592 let diagonal =
594 EcefPoint::from_geodetic(point(45.0, 45.0, 1000.0), &Ellipsoid::WGS84).unwrap();
595 assert!((diagonal.x().metres() - 3_194_919.145).abs() < 1e-3);
596 assert!((diagonal.x().metres() - diagonal.y().metres()).abs() < 1e-6);
597 assert!((diagonal.z().metres() - 4_488_055.516).abs() < 1e-3);
598 }
599
600 #[test]
601 fn the_round_trip_holds_at_the_awkward_places() {
602 let places = [
603 (0.0, 0.0, 0.0),
604 (90.0, 0.0, 0.0),
605 (-90.0, 45.0, 1000.0),
606 (89.999_999, 179.999_999, -50.0),
607 (-45.0, -180.0, 20_200_000.0),
608 (50.755, -1.333, 48.0),
609 (1e-9, 1e-9, 0.0),
610 ];
611 for (latitude, longitude, height) in places {
612 let there = point(latitude, longitude, height);
613 let back = EcefPoint::from_geodetic(there, &Ellipsoid::WGS84)
614 .unwrap()
615 .to_geodetic(&Ellipsoid::WGS84)
616 .unwrap();
617 assert!(
618 (back.position().latitude().degrees() - latitude).abs() < 1e-9,
619 "latitude at {latitude} {longitude} {height}: {}",
620 back.position().latitude().degrees()
621 );
622 assert!(
623 back.position()
624 .longitude_difference(there.position())
625 .degrees()
626 .abs()
627 < 1e-9
628 || latitude.abs() == 90.0,
629 "longitude at {latitude} {longitude} {height}"
630 );
631 assert!(
632 (back.height().value().metres() - height).abs() < 1e-3,
633 "height at {latitude} {longitude} {height}: {}",
634 back.height().value().metres()
635 );
636 }
637 }
638
639 #[test]
640 fn a_sea_level_height_does_not_pretend_to_be_ellipsoidal() {
641 let msl = GeodeticPoint::new(
642 point(50.0, 0.0, 0.0).position(),
643 Height::above_mean_sea_level(Distance::from_metres(10.0).unwrap()),
644 );
645 assert_eq!(
646 EcefPoint::from_geodetic(msl, &Ellipsoid::WGS84),
647 Err(KernelError::VerticalDatumMismatch {
648 required: VerticalDatum::Ellipsoid,
649 found: VerticalDatum::MeanSeaLevel,
650 })
651 );
652 assert_eq!(format!("{}", msl.height()), "10.0 m above mean sea level");
653 }
654
655 #[test]
656 fn the_centre_of_the_earth_has_no_position() {
657 let centre = EcefPoint::new(Distance::ZERO, Distance::ZERO, Distance::ZERO);
658 assert!(matches!(
659 centre.to_geodetic(&Ellipsoid::WGS84),
660 Err(KernelError::Indeterminate { .. })
661 ));
662 }
663
664 #[test]
665 fn the_chord_is_the_straight_line_through_the_earth() {
666 let north = EcefPoint::from_geodetic(point(90.0, 0.0, 0.0), &Ellipsoid::WGS84).unwrap();
667 let south = EcefPoint::from_geodetic(point(-90.0, 0.0, 0.0), &Ellipsoid::WGS84).unwrap();
668 assert!((north.chord_to(south).metres() - 2.0 * 6_356_752.314_245).abs() < 1e-6);
669 }
670}