1use core::fmt;
28use core::str::FromStr;
29
30use crate::angle::wrap180;
31use crate::error::{ensure_finite, ensure_range, KernelError, Result};
32use crate::math;
33use crate::units::{Angle, Distance};
34
35pub const WGS84_FLATTENING: f64 = 1.0 / 298.257_223_563;
37pub const WGS84_SEMI_MAJOR_AXIS_METRES: f64 = 6_378_137.0;
39pub const WGS84_ECCENTRICITY_SQUARED: f64 = WGS84_FLATTENING * (2.0 - WGS84_FLATTENING);
41const WGS84_ECCENTRICITY: f64 = 0.081_819_190_842_621_49;
46
47fn artanh(value: f64) -> f64 {
49 0.5 * math::ln((1.0 + value) / (1.0 - value))
50}
51
52#[non_exhaustive]
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub enum NorthSouth {
59 North,
61 South,
63}
64
65impl NorthSouth {
66 #[must_use]
68 pub const fn sign(self) -> f64 {
69 match self {
70 Self::North => 1.0,
71 Self::South => -1.0,
72 }
73 }
74
75 #[must_use]
77 pub const fn letter(self) -> char {
78 match self {
79 Self::North => 'N',
80 Self::South => 'S',
81 }
82 }
83}
84
85#[non_exhaustive]
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91pub enum EastWest {
92 East,
94 West,
96}
97
98impl EastWest {
99 #[must_use]
101 pub const fn sign(self) -> f64 {
102 match self {
103 Self::East => 1.0,
104 Self::West => -1.0,
105 }
106 }
107
108 #[must_use]
110 pub const fn letter(self) -> char {
111 match self {
112 Self::East => 'E',
113 Self::West => 'W',
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
120#[cfg_attr(
121 feature = "serde",
122 derive(serde::Serialize, serde::Deserialize),
123 serde(try_from = "f64", into = "f64")
124)]
125pub struct Latitude(f64);
126
127impl Latitude {
128 pub const EQUATOR: Self = Self(0.0);
130 pub const NORTH_POLE: Self = Self(90.0);
132 pub const SOUTH_POLE: Self = Self(-90.0);
134
135 pub fn from_degrees(value: f64) -> Result<Self> {
142 ensure_range("latitude", value, -90.0, 90.0)?;
143 Ok(Self(value))
144 }
145
146 pub fn from_degrees_minutes(
152 degrees: u16,
153 minutes: f64,
154 hemisphere: NorthSouth,
155 ) -> Result<Self> {
156 ensure_finite("latitude minutes", minutes)?;
157 let magnitude = f64::from(degrees) + minutes / 60.0;
158 Self::from_degrees(magnitude * hemisphere.sign())
159 }
160
161 #[doc(hidden)]
170 #[must_use]
171 pub fn from_degrees_clamped(value: f64) -> Self {
172 Self(value.clamp(-90.0, 90.0))
173 }
174
175 #[must_use]
177 pub const fn degrees(self) -> f64 {
178 self.0
179 }
180
181 #[must_use]
183 pub fn radians(self) -> f64 {
184 math::to_radians(self.0)
185 }
186
187 #[must_use]
189 pub fn hemisphere(self) -> NorthSouth {
190 if self.0 < 0.0 {
191 NorthSouth::South
192 } else {
193 NorthSouth::North
194 }
195 }
196
197 #[must_use]
199 pub fn to_degrees_minutes(self) -> (u16, f64, NorthSouth) {
200 split_degrees_minutes(self.0, 90)
201 .map_or((0, 0.0, self.hemisphere()), |(degrees, minutes)| {
202 (degrees, minutes, self.hemisphere())
203 })
204 }
205
206 #[must_use]
208 pub fn is_polar(self) -> bool {
209 math::abs(math::abs(self.0) - 90.0) < 1e-9
210 }
211
212 pub fn meridional_parts(self) -> Result<f64> {
225 if self.is_polar() {
226 return Err(crate::KernelError::Indeterminate {
227 quantity: "meridional parts at the pole",
228 });
229 }
230 let eccentricity = WGS84_ECCENTRICITY;
234 let sine = math::sin(self.radians());
235 let correction = eccentricity * artanh(eccentricity * sine);
236 Ok(self.isometric_minutes() - math::to_degrees(correction) * 60.0)
237 }
238
239 #[doc(hidden)]
248 #[must_use]
249 pub fn from_isometric_minutes(minutes: f64) -> Self {
250 let radians = math::to_radians(minutes / 60.0);
251 let latitude = 2.0 * math::atan(math::exp(radians)) - core::f64::consts::FRAC_PI_2;
252 Self::from_degrees_clamped(math::to_degrees(latitude))
253 }
254
255 #[must_use]
260 pub fn isometric_minutes(self) -> f64 {
261 math::to_degrees(math::ln(math::tan(
262 core::f64::consts::FRAC_PI_4 + self.radians() / 2.0,
263 ))) * 60.0
264 }
265}
266
267impl fmt::Display for Latitude {
268 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 let precision = f.precision().unwrap_or(1);
271 let (degrees, minutes, hemisphere) = self.to_degrees_minutes();
272 write!(
273 f,
274 "{degrees:02}°{minutes:0>width$.precision$}'{}",
275 hemisphere.letter(),
276 width = if precision == 0 { 2 } else { precision + 3 }
277 )
278 }
279}
280
281impl FromStr for Latitude {
282 type Err = crate::KernelError;
283
284 fn from_str(input: &str) -> Result<Self> {
294 let parsed = crate::parse::sexagesimal("latitude", input)?;
295 if let Some(letter) = parsed.hemisphere {
296 if !matches!(letter, 'N' | 'S') {
297 return Err(crate::parse::parse_error("latitude", input));
298 }
299 }
300 Self::from_degrees(parsed.signed("S"))
301 }
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
309#[cfg_attr(
310 feature = "serde",
311 derive(serde::Serialize, serde::Deserialize),
312 serde(try_from = "f64", into = "f64")
313)]
314pub struct Longitude(f64);
315
316impl Longitude {
317 pub const GREENWICH: Self = Self(0.0);
319
320 pub fn from_degrees(value: f64) -> Result<Self> {
326 ensure_finite("longitude", value)?;
327 Ok(Self(wrap180(value)))
328 }
329
330 pub fn from_degrees_minutes(degrees: u16, minutes: f64, hemisphere: EastWest) -> Result<Self> {
336 ensure_finite("longitude minutes", minutes)?;
337 let magnitude = f64::from(degrees) + minutes / 60.0;
338 Self::from_degrees(magnitude * hemisphere.sign())
339 }
340
341 #[doc(hidden)]
349 #[must_use]
350 pub fn from_degrees_wrapped(value: f64) -> Self {
351 Self(wrap180(value))
352 }
353
354 #[must_use]
356 pub const fn degrees(self) -> f64 {
357 self.0
358 }
359
360 #[must_use]
362 pub fn radians(self) -> f64 {
363 math::to_radians(self.0)
364 }
365
366 #[must_use]
368 pub fn hemisphere(self) -> EastWest {
369 if self.0 < 0.0 {
370 EastWest::West
371 } else {
372 EastWest::East
373 }
374 }
375
376 #[must_use]
378 pub fn to_degrees_minutes(self) -> (u16, f64, EastWest) {
379 split_degrees_minutes(self.0, 180)
380 .map_or((0, 0.0, self.hemisphere()), |(degrees, minutes)| {
381 (degrees, minutes, self.hemisphere())
382 })
383 }
384
385 #[must_use]
388 pub fn difference_to(self, other: Self) -> Angle {
389 Angle::from_degrees_unchecked(wrap180(other.0 - self.0))
390 }
391}
392
393impl fmt::Display for Longitude {
394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396 let precision = f.precision().unwrap_or(1);
397 let (degrees, minutes, hemisphere) = self.to_degrees_minutes();
398 write!(
399 f,
400 "{degrees:03}°{minutes:0>width$.precision$}'{}",
401 hemisphere.letter(),
402 width = if precision == 0 { 2 } else { precision + 3 }
403 )
404 }
405}
406
407impl FromStr for Longitude {
408 type Err = crate::KernelError;
409
410 fn from_str(input: &str) -> Result<Self> {
417 let parsed = crate::parse::sexagesimal("longitude", input)?;
418 if let Some(letter) = parsed.hemisphere {
419 if !matches!(letter, 'E' | 'W') {
420 return Err(crate::parse::parse_error("longitude", input));
421 }
422 }
423 Self::from_degrees(parsed.signed("W"))
424 }
425}
426
427#[derive(Debug, Clone, Copy, PartialEq, Default)]
429#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
430pub struct Position {
431 latitude: Latitude,
432 longitude: Longitude,
433}
434
435impl Position {
436 pub const ORIGIN: Self = Self {
438 latitude: Latitude::EQUATOR,
439 longitude: Longitude::GREENWICH,
440 };
441
442 #[must_use]
444 pub const fn new(latitude: Latitude, longitude: Longitude) -> Self {
445 Self {
446 latitude,
447 longitude,
448 }
449 }
450
451 pub fn from_degrees(latitude: f64, longitude: f64) -> Result<Self> {
457 Ok(Self::new(
458 Latitude::from_degrees(latitude)?,
459 Longitude::from_degrees(longitude)?,
460 ))
461 }
462
463 #[must_use]
465 pub const fn latitude(self) -> Latitude {
466 self.latitude
467 }
468
469 #[must_use]
471 pub const fn longitude(self) -> Longitude {
472 self.longitude
473 }
474
475 #[must_use]
477 pub fn latitude_difference(self, other: Self) -> Angle {
478 Angle::from_degrees_unchecked(other.latitude.0 - self.latitude.0)
479 }
480
481 #[must_use]
483 pub fn longitude_difference(self, other: Self) -> Angle {
484 self.longitude.difference_to(other.longitude)
485 }
486
487 #[must_use]
492 pub fn departure(self, other: Self) -> Distance {
493 let mean_latitude = f64::midpoint(self.latitude.radians(), other.latitude.radians());
494 Distance::from_nautical_miles_unchecked(
495 self.longitude_difference(other).minutes() * math::cos(mean_latitude),
496 )
497 }
498
499 #[must_use]
502 pub fn to_geocentric_unit(self) -> GeocentricUnit {
503 let (latitude, longitude) = (self.latitude.radians(), self.longitude.radians());
504 let cos_latitude = math::cos(latitude);
505 GeocentricUnit {
506 x: cos_latitude * math::cos(longitude),
507 y: cos_latitude * math::sin(longitude),
508 z: math::sin(latitude),
509 }
510 }
511
512 #[must_use]
516 pub fn from_geocentric_unit(unit: GeocentricUnit) -> Self {
517 let horizontal = math::hypot(unit.x, unit.y);
518 Self::new(
519 Latitude::from_degrees_clamped(math::to_degrees(math::atan2(unit.z, horizontal))),
520 Longitude::from_degrees_wrapped(math::to_degrees(math::atan2(unit.y, unit.x))),
521 )
522 }
523}
524
525#[derive(Debug, Clone, Copy, PartialEq)]
552pub struct GeocentricUnit {
553 x: f64,
554 y: f64,
555 z: f64,
556}
557
558impl GeocentricUnit {
559 pub fn new(x: f64, y: f64, z: f64) -> Result<Self> {
568 ensure_finite("geocentric x", x)?;
569 ensure_finite("geocentric y", y)?;
570 ensure_finite("geocentric z", z)?;
571 Self::from_finite(x, y, z).ok_or(KernelError::Indeterminate {
572 quantity: "a direction from a vector of zero length",
573 })
574 }
575
576 #[doc(hidden)]
584 #[must_use]
585 pub fn from_finite(x: f64, y: f64, z: f64) -> Option<Self> {
586 let magnitude = math::hypot(math::hypot(x, y), z);
587 if magnitude < f64::MIN_POSITIVE {
588 return None;
589 }
590 Some(Self {
591 x: x / magnitude,
592 y: y / magnitude,
593 z: z / magnitude,
594 })
595 }
596
597 #[must_use]
599 pub const fn x(self) -> f64 {
600 self.x
601 }
602
603 #[must_use]
605 pub const fn y(self) -> f64 {
606 self.y
607 }
608
609 #[must_use]
611 pub const fn z(self) -> f64 {
612 self.z
613 }
614
615 #[must_use]
617 pub const fn components(self) -> [f64; 3] {
618 [self.x, self.y, self.z]
619 }
620
621 #[must_use]
623 pub fn dot(self, other: Self) -> f64 {
624 self.x * other.x + self.y * other.y + self.z * other.z
625 }
626
627 #[must_use]
629 pub fn antipode(self) -> Self {
630 Self {
631 x: -self.x,
632 y: -self.y,
633 z: -self.z,
634 }
635 }
636}
637
638impl FromStr for Position {
639 type Err = crate::KernelError;
640
641 fn from_str(input: &str) -> Result<Self> {
650 let (latitude, longitude) = crate::parse::split_position(input)?;
651 Ok(Self::new(latitude.parse()?, longitude.parse()?))
652 }
653}
654
655impl fmt::Display for Position {
656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
658 let precision = f.precision().unwrap_or(1);
659 write!(
660 f,
661 "{:.precision$} {:.precision$}",
662 self.latitude, self.longitude
663 )
664 }
665}
666
667fn split_degrees_minutes(value: f64, maximum: u16) -> Option<(u16, f64)> {
672 let magnitude = math::abs(value);
673 let mut degrees = u16::try_from(math::to_usize(magnitude)).ok()?;
674 let mut minutes = (magnitude - f64::from(degrees)) * 60.0;
675 if minutes >= 59.999_95 {
677 minutes = 0.0;
678 degrees = degrees.checked_add(1)?;
679 }
680 if degrees > maximum {
681 return None;
682 }
683 Some((degrees, minutes))
684}
685
686#[cfg(feature = "serde")]
687impl TryFrom<f64> for Latitude {
688 type Error = KernelError;
689
690 fn try_from(value: f64) -> Result<Self> {
692 Self::from_degrees(value)
693 }
694}
695
696#[cfg(feature = "serde")]
697impl From<Latitude> for f64 {
698 fn from(value: Latitude) -> Self {
699 value.0
700 }
701}
702
703#[cfg(feature = "serde")]
704impl TryFrom<f64> for Longitude {
705 type Error = KernelError;
706
707 fn try_from(value: f64) -> Result<Self> {
709 Self::from_degrees(value)
710 }
711}
712
713#[cfg(feature = "serde")]
714impl From<Longitude> for f64 {
715 fn from(value: Longitude) -> Self {
716 value.0
717 }
718}
719
720#[cfg(test)]
721#[allow(clippy::unwrap_used, clippy::float_cmp, clippy::indexing_slicing)]
722mod tests {
723 use super::*;
724 use alloc::format;
725
726 #[test]
727 fn latitude_validates() {
728 assert!(Latitude::from_degrees(90.0).is_ok());
729 assert!(Latitude::from_degrees(-90.0).is_ok());
730 assert!(Latitude::from_degrees(90.000_001).is_err());
731 assert!(Latitude::from_degrees(f64::NAN).is_err());
732 assert!(Latitude::from_degrees(f64::INFINITY).is_err());
733 }
734
735 #[test]
736 fn longitude_wraps_instead_of_failing() {
737 assert_eq!(Longitude::from_degrees(180.0).unwrap().degrees(), -180.0);
738 assert_eq!(Longitude::from_degrees(-180.0).unwrap().degrees(), -180.0);
739 assert_eq!(Longitude::from_degrees(190.0).unwrap().degrees(), -170.0);
740 assert_eq!(Longitude::from_degrees(-190.0).unwrap().degrees(), 170.0);
741 assert_eq!(Longitude::from_degrees(720.5).unwrap().degrees(), 0.5);
742 assert!(Longitude::from_degrees(f64::NAN).is_err());
743 }
744
745 #[test]
746 fn degrees_and_minutes_round_trip() {
747 let latitude = Latitude::from_degrees_minutes(50, 45.3, NorthSouth::North).unwrap();
748 assert!((latitude.degrees() - 50.755).abs() < 1e-12);
749 let (degrees, minutes, hemisphere) = latitude.to_degrees_minutes();
750 assert_eq!(degrees, 50);
751 assert!((minutes - 45.3).abs() < 1e-9);
752 assert_eq!(hemisphere, NorthSouth::North);
753
754 let longitude = Longitude::from_degrees_minutes(1, 17.8, EastWest::West).unwrap();
755 assert!((longitude.degrees() + 1.296_666_667).abs() < 1e-9);
756 }
757
758 #[test]
759 fn display_matches_chart_convention() {
760 let position = Position::from_degrees(50.755, -1.296_666_667).unwrap();
761 assert_eq!(format!("{position}"), "50°45.3'N 001°17.8'W");
762
763 let southern = Position::from_degrees(-33.9, 151.2).unwrap();
764 assert_eq!(format!("{southern}"), "33°54.0'S 151°12.0'E");
765
766 let boundary = Latitude::from_degrees(10.999_999_9).unwrap();
768 assert_eq!(format!("{boundary}"), "11°00.0'N");
769 }
770
771 #[test]
772 fn isometric_latitude_round_trips() {
773 for degrees in [-85.0, -45.0, -0.5, 0.0, 0.5, 10.0, 45.0, 80.0, 89.0] {
774 let latitude = Latitude::from_degrees(degrees).unwrap();
775 let back = Latitude::from_isometric_minutes(latitude.isometric_minutes());
776 assert!(
777 (back.degrees() - degrees).abs() < 1e-9,
778 "{degrees} came back as {}",
779 back.degrees()
780 );
781 }
782 assert!(Latitude::from_isometric_minutes(f64::INFINITY).is_polar());
784 assert!(Latitude::from_isometric_minutes(f64::NEG_INFINITY).is_polar());
785 }
786
787 #[test]
788 fn eccentricity_constant_is_the_square_root_it_claims_to_be() {
789 let expected = WGS84_ECCENTRICITY_SQUARED.sqrt();
790 assert!((WGS84_ECCENTRICITY - expected).abs() < 1e-15);
791 }
792
793 #[test]
794 fn positions_read_back_from_what_they_print() {
795 for (latitude, longitude) in [
796 (50.755, -1.296_666_667),
797 (-33.9, 151.2),
798 (0.0, 0.0),
799 (89.5, -179.5),
800 ] {
801 let position = Position::from_degrees(latitude, longitude).unwrap();
802 let printed = alloc::format!("{position:.4}");
803 let read: Position = printed.parse().unwrap();
804 assert!(
805 (read.latitude().degrees() - latitude).abs() < 1e-6,
806 "{printed}"
807 );
808 assert!(
809 read.longitude()
810 .difference_to(position.longitude())
811 .degrees()
812 .abs()
813 < 1e-6,
814 "{printed}"
815 );
816 }
817 }
818
819 #[test]
820 fn positions_parse_from_the_usual_forms() {
821 let expected = Position::from_degrees(50.755, -1.296_666_667).unwrap();
822 for input in [
823 "50°45.3'N 001°17.8'W",
824 "50 45.3 N 001 17.8 W",
825 "N50°45.3' W001°17.8'",
826 "50.755, -1.2966667",
827 "50.755 -1.2966667",
828 ] {
829 let parsed: Position = input.parse().unwrap();
830 assert!(
831 (parsed.latitude().degrees() - expected.latitude().degrees()).abs() < 1e-6,
832 "{input}"
833 );
834 assert!(
835 (parsed.longitude().degrees() - expected.longitude().degrees()).abs() < 1e-6,
836 "{input}"
837 );
838 }
839 }
840
841 #[test]
842 fn the_wrong_hemisphere_letter_is_refused() {
843 assert!("50°45.3'E".parse::<Latitude>().is_err());
844 assert!("001°17.8'N".parse::<Longitude>().is_err());
845 assert!("50°45.3'N".parse::<Longitude>().is_err());
846 assert!("91 00.0 N".parse::<Latitude>().is_err());
848 assert_eq!("190".parse::<Longitude>().unwrap().degrees(), -170.0);
850 }
851
852 #[test]
853 fn unreadable_input_is_an_error_not_a_panic() {
854 for input in ["", " ", "north", "50°45.3'N", "a b", "50 45.3 60.0"] {
855 assert!(input.parse::<Position>().is_err(), "{input}");
856 }
857
858 let pair: Position = "50 45.3".parse().unwrap();
861 assert_eq!(pair.latitude().degrees(), 50.0);
862 assert_eq!(pair.longitude().degrees(), 45.3);
863 for input in ["", " ", "fifty", "50 60.0", "50 45 18 12"] {
864 assert!(input.parse::<Latitude>().is_err(), "{input}");
865 assert!(input.parse::<Longitude>().is_err(), "{input}");
866 }
867 }
868
869 #[test]
870 fn hemispheres() {
871 assert_eq!(
872 Latitude::from_degrees(0.0).unwrap().hemisphere(),
873 NorthSouth::North
874 );
875 assert_eq!(
876 Latitude::from_degrees(-0.1).unwrap().hemisphere(),
877 NorthSouth::South
878 );
879 assert_eq!(
880 Longitude::from_degrees(0.0).unwrap().hemisphere(),
881 EastWest::East
882 );
883 assert_eq!(
884 Longitude::from_degrees(-0.1).unwrap().hemisphere(),
885 EastWest::West
886 );
887 assert!(Latitude::NORTH_POLE.is_polar());
888 assert!(!Latitude::from_degrees(89.0).unwrap().is_polar());
889 }
890
891 #[test]
892 fn longitude_difference_takes_the_short_way() {
893 let west = Longitude::from_degrees(-179.0).unwrap();
894 let east = Longitude::from_degrees(179.0).unwrap();
895 assert!((west.difference_to(east).degrees() + 2.0).abs() < 1e-12);
896 assert!((east.difference_to(west).degrees() - 2.0).abs() < 1e-12);
897 }
898
899 #[test]
900 fn meridional_parts_match_the_tables() {
901 for (degrees, expected) in [
904 (10.0, 599.073),
905 (30.0, 1876.862),
906 (45.0, 3013.648),
907 (60.0, 4507.404),
908 (75.0, 6948.063),
909 ] {
910 let latitude = Latitude::from_degrees(degrees).unwrap();
911 let parts = latitude.meridional_parts().unwrap();
912 assert!(
913 (parts - expected).abs() < 0.001,
914 "at {degrees}°: {parts} vs {expected}"
915 );
916 }
917
918 let latitude = Latitude::from_degrees(45.0).unwrap();
919 assert!((latitude.isometric_minutes() - 3029.9).abs() < 0.1);
921 assert!(Latitude::EQUATOR.meridional_parts().unwrap().abs() < 1e-9);
922 assert!(Latitude::NORTH_POLE.meridional_parts().is_err());
923
924 let south = Latitude::from_degrees(-45.0).unwrap();
926 assert!((south.meridional_parts().unwrap() + 3013.648).abs() < 0.001);
927 }
928
929 #[test]
930 fn geocentric_units_round_trip() {
931 for (latitude, longitude) in [
932 (0.0, 0.0),
933 (45.0, 90.0),
934 (-33.9, 151.2),
935 (89.0, -179.0),
936 (0.0, -180.0),
937 ] {
938 let position = Position::from_degrees(latitude, longitude).unwrap();
939 let unit = position.to_geocentric_unit();
940 assert!((unit.dot(unit) - 1.0).abs() < 1e-12);
942
943 let back = Position::from_geocentric_unit(unit);
944 assert!((back.latitude().degrees() - latitude).abs() < 1e-9);
945 assert!(
946 back.longitude()
947 .difference_to(position.longitude())
948 .degrees()
949 .abs()
950 < 1e-9
951 );
952
953 let opposite = Position::from_geocentric_unit(unit.antipode());
955 assert!((opposite.latitude().degrees() + latitude).abs() < 1e-9);
956 assert!((unit.dot(unit.antipode()) + 1.0).abs() < 1e-12);
957 }
958 }
959
960 #[test]
961 fn a_geocentric_unit_normalises_and_refuses_what_points_nowhere() {
962 let one = GeocentricUnit::new(1.0, 0.0, 0.0).unwrap();
964 let many = GeocentricUnit::new(1_000.0, 0.0, 0.0).unwrap();
965 assert_eq!(one, many);
966 assert!((one.x() - 1.0).abs() < 1e-12);
967 assert_eq!(one.components(), [1.0, 0.0, 0.0]);
968
969 let diagonal = GeocentricUnit::new(3.0, 4.0, 0.0).unwrap();
971 assert!((diagonal.x() - 0.6).abs() < 1e-12);
972 assert!((diagonal.y() - 0.8).abs() < 1e-12);
973
974 assert!(matches!(
975 GeocentricUnit::new(0.0, 0.0, 0.0),
976 Err(KernelError::Indeterminate { .. })
977 ));
978 assert!(matches!(
979 GeocentricUnit::new(f64::NAN, 0.0, 0.0),
980 Err(KernelError::NotFinite { .. })
981 ));
982 assert!(GeocentricUnit::new(f64::INFINITY, 0.0, 1.0).is_err());
983 }
984
985 #[test]
986 fn departure_matches_plane_sailing() {
987 let from = Position::from_degrees(60.0, 0.0).unwrap();
989 let to = Position::from_degrees(60.0, 1.0).unwrap();
990 assert!((from.departure(to).nautical_miles() - 30.0).abs() < 0.01);
991 assert!((from.latitude_difference(to).degrees()).abs() < 1e-12);
992 }
993}