1use core::fmt;
34use core::marker::PhantomData;
35
36use crate::error::{ensure_finite, ensure_range, KernelError, Result};
37use crate::math;
38
39pub const MAX_VARIATION_DEG: f64 = 180.0;
41
42pub const MAX_DEVIATION_DEG: f64 = 180.0;
44
45#[must_use]
50pub fn wrap360(degrees: f64) -> f64 {
51 let remainder = degrees % 360.0;
52 if remainder < 0.0 {
53 let shifted = remainder + 360.0;
54 if shifted >= 360.0 {
57 0.0
58 } else {
59 shifted
60 }
61 } else {
62 remainder + 0.0
64 }
65}
66
67#[must_use]
69pub fn wrap180(degrees: f64) -> f64 {
70 let wrapped = wrap360(degrees);
71 if wrapped >= 180.0 {
72 wrapped - 360.0
73 } else {
74 wrapped
75 }
76}
77
78mod sealed {
79 pub trait Sealed {}
80}
81
82pub trait Frame: sealed::Sealed + Copy + Clone + fmt::Debug + 'static {
86 const NAME: &'static str;
88 const SUFFIX: char;
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95pub struct True;
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100pub struct Magnetic;
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105pub struct Compass;
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct Gyro;
113
114impl sealed::Sealed for True {}
115impl sealed::Sealed for Magnetic {}
116impl sealed::Sealed for Compass {}
117impl sealed::Sealed for Gyro {}
118
119impl Frame for True {
120 const NAME: &'static str = "true";
121 const SUFFIX: char = 'T';
122}
123
124impl Frame for Magnetic {
125 const NAME: &'static str = "magnetic";
126 const SUFFIX: char = 'M';
127}
128
129impl Frame for Compass {
130 const NAME: &'static str = "compass";
131 const SUFFIX: char = 'C';
132}
133
134impl Frame for Gyro {
135 const NAME: &'static str = "gyro";
136 const SUFFIX: char = 'G';
137}
138
139#[derive(Clone, Copy, PartialEq, PartialOrd, Default)]
144pub struct Direction<F: Frame> {
145 degrees: f64,
146 frame: PhantomData<F>,
147}
148
149pub type TrueCourse = Direction<True>;
151pub type TrueBearing = Direction<True>;
153pub type MagneticCourse = Direction<Magnetic>;
155pub type MagneticBearing = Direction<Magnetic>;
157pub type CompassCourse = Direction<Compass>;
159pub type CompassBearing = Direction<Compass>;
161pub type GyroCourse = Direction<Gyro>;
163pub type GyroBearing = Direction<Gyro>;
165
166impl<F: Frame> Direction<F> {
167 pub const NORTH: Self = Self::from_wrapped(0.0);
169 pub const EAST: Self = Self::from_wrapped(90.0);
171 pub const SOUTH: Self = Self::from_wrapped(180.0);
173 pub const WEST: Self = Self::from_wrapped(270.0);
175
176 const fn from_wrapped(degrees: f64) -> Self {
178 Self {
179 degrees,
180 frame: PhantomData,
181 }
182 }
183
184 #[doc(hidden)]
193 #[must_use]
194 pub fn from_degrees_wrapped(degrees: f64) -> Self {
195 Self::from_wrapped(wrap360(degrees))
196 }
197
198 pub fn new(degrees: f64) -> Result<Self> {
208 ensure_range("course", degrees, 0.0, 360.0)?;
209 Ok(Self::from_wrapped(wrap360(degrees)))
210 }
211
212 pub fn wrap(degrees: f64) -> Result<Self> {
218 ensure_finite("course", degrees)?;
219 Ok(Self::from_wrapped(wrap360(degrees)))
220 }
221
222 #[must_use]
224 pub const fn degrees(self) -> f64 {
225 self.degrees
226 }
227
228 #[must_use]
230 pub fn radians(self) -> f64 {
231 math::to_radians(self.degrees)
232 }
233
234 #[must_use]
236 pub fn reciprocal(self) -> Self {
237 Self::from_wrapped(wrap360(self.degrees + 180.0))
238 }
239
240 #[must_use]
246 pub fn components(self, magnitude: f64) -> (f64, f64) {
247 let radians = self.radians();
248 (
249 magnitude * math::cos(radians),
250 magnitude * math::sin(radians),
251 )
252 }
253
254 pub fn offset(self, delta: f64) -> Result<Self> {
260 ensure_finite("delta", delta)?;
261 Ok(Self::from_wrapped(wrap360(self.degrees + delta)))
262 }
263
264 #[must_use]
267 pub fn signed_difference(self, other: Self) -> f64 {
268 wrap180(other.degrees - self.degrees)
269 }
270
271 #[must_use]
273 pub fn angular_distance(self, other: Self) -> f64 {
274 math::abs(self.signed_difference(other))
275 }
276
277 #[doc(hidden)]
286 #[must_use]
287 pub const fn relabel<G: Frame>(self) -> Direction<G> {
288 Direction::from_wrapped(self.degrees)
289 }
290}
291
292impl<F: Frame> fmt::Display for Direction<F> {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295 let precision = f.precision().unwrap_or(1);
296 write!(
297 f,
298 "{:0>width$.precision$}°{}",
299 self.degrees,
300 F::SUFFIX,
301 width = if precision == 0 { 3 } else { precision + 4 },
302 )
303 }
304}
305
306impl<F: Frame> fmt::Debug for Direction<F> {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 write!(f, "{}({}°)", F::NAME, self.degrees)
309 }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
316#[cfg_attr(
317 feature = "serde",
318 derive(serde::Serialize, serde::Deserialize),
319 serde(try_from = "f64", into = "f64")
320)]
321pub struct Variation(f64);
322
323impl Variation {
324 pub const ZERO: Self = Self(0.0);
326
327 pub fn new(degrees: f64) -> Result<Self> {
334 ensure_range("variation", degrees, -MAX_VARIATION_DEG, MAX_VARIATION_DEG)?;
335 Ok(Self(degrees))
336 }
337
338 #[must_use]
340 pub const fn degrees(self) -> f64 {
341 self.0
342 }
343}
344
345impl fmt::Display for Variation {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 let precision = f.precision().unwrap_or(1);
348 let hemisphere = if self.0 < 0.0 { 'W' } else { 'E' };
349 write!(f, "{:.precision$}°{hemisphere}", math::abs(self.0))
350 }
351}
352
353#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
357#[cfg_attr(
358 feature = "serde",
359 derive(serde::Serialize, serde::Deserialize),
360 serde(try_from = "f64", into = "f64")
361)]
362pub struct Deviation(f64);
363
364impl Deviation {
365 pub const ZERO: Self = Self(0.0);
367
368 pub fn new(degrees: f64) -> Result<Self> {
375 ensure_range("deviation", degrees, -MAX_DEVIATION_DEG, MAX_DEVIATION_DEG)?;
376 Ok(Self(degrees))
377 }
378
379 #[must_use]
381 pub const fn degrees(self) -> f64 {
382 self.0
383 }
384}
385
386impl fmt::Display for Deviation {
387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388 let precision = f.precision().unwrap_or(1);
389 let hemisphere = if self.0 < 0.0 { 'W' } else { 'E' };
390 write!(f, "{:.precision$}°{hemisphere}", math::abs(self.0))
391 }
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
413#[non_exhaustive]
414#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
415pub enum CardinalPoint {
416 #[default]
418 N,
419 NE,
421 E,
423 SE,
425 S,
427 SW,
429 W,
431 NW,
433}
434
435impl CardinalPoint {
436 pub const ALL: [Self; 8] = [
438 Self::N,
439 Self::NE,
440 Self::E,
441 Self::SE,
442 Self::S,
443 Self::SW,
444 Self::W,
445 Self::NW,
446 ];
447
448 #[must_use]
453 pub const fn whole_degrees(self) -> i32 {
454 match self {
455 Self::N => 0,
456 Self::NE => 45,
457 Self::E => 90,
458 Self::SE => 135,
459 Self::S => 180,
460 Self::SW => 225,
461 Self::W => 270,
462 Self::NW => 315,
463 }
464 }
465
466 #[must_use]
468 pub fn degrees(self) -> f64 {
469 f64::from(self.whole_degrees())
470 }
471
472 #[must_use]
474 pub const fn abbreviation(self) -> &'static str {
475 match self {
476 Self::N => "N",
477 Self::NE => "NE",
478 Self::E => "E",
479 Self::SE => "SE",
480 Self::S => "S",
481 Self::SW => "SW",
482 Self::W => "W",
483 Self::NW => "NW",
484 }
485 }
486}
487
488impl fmt::Display for CardinalPoint {
489 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490 f.write_str(self.abbreviation())
491 }
492}
493
494impl core::str::FromStr for CardinalPoint {
495 type Err = KernelError;
496
497 fn from_str(text: &str) -> Result<Self> {
503 let trimmed = text.trim();
504 Self::ALL
505 .into_iter()
506 .find(|point| point.abbreviation().eq_ignore_ascii_case(trimmed))
507 .ok_or_else(|| KernelError::UnknownCardinalDirection {
508 direction: trimmed.into(),
509 })
510 }
511}
512
513impl<F: Frame> From<CardinalPoint> for Direction<F> {
514 fn from(point: CardinalPoint) -> Self {
516 Self::from_wrapped(point.degrees())
517 }
518}
519
520#[non_exhaustive]
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
525#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
526pub enum Side {
527 Ahead,
529 Starboard,
531 Astern,
533 Port,
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
539#[cfg_attr(
540 feature = "serde",
541 derive(serde::Serialize, serde::Deserialize),
542 serde(try_from = "f64", into = "f64")
543)]
544pub struct RelativeBearing(f64);
545
546impl RelativeBearing {
547 pub const AHEAD: Self = Self(0.0);
549 pub const ABEAM_STARBOARD: Self = Self(90.0);
551 pub const ASTERN: Self = Self(180.0);
553 pub const ABEAM_PORT: Self = Self(270.0);
555
556 pub fn new(degrees: f64) -> Result<Self> {
563 ensure_range("relative bearing", degrees, 0.0, 360.0)?;
564 Ok(Self(wrap360(degrees)))
565 }
566
567 pub fn wrap(degrees: f64) -> Result<Self> {
573 ensure_finite("relative bearing", degrees)?;
574 Ok(Self(wrap360(degrees)))
575 }
576
577 #[doc(hidden)]
585 #[must_use]
586 pub fn from_degrees_wrapped(degrees: f64) -> Self {
587 Self(wrap360(degrees))
588 }
589
590 #[must_use]
592 pub const fn degrees(self) -> f64 {
593 self.0
594 }
595
596 #[must_use]
598 pub fn signed_degrees(self) -> f64 {
599 wrap180(self.0)
600 }
601
602 #[must_use]
604 pub fn side(self) -> Side {
605 const TOLERANCE: f64 = 1e-9;
606 let signed = self.signed_degrees();
607 if math::abs(signed) < TOLERANCE {
608 Side::Ahead
609 } else if math::abs(math::abs(signed) - 180.0) < TOLERANCE {
610 Side::Astern
611 } else if signed > 0.0 {
612 Side::Starboard
613 } else {
614 Side::Port
615 }
616 }
617}
618
619impl fmt::Display for RelativeBearing {
620 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
622 let precision = f.precision().unwrap_or(1);
623 let magnitude = math::abs(self.signed_degrees());
624 match self.side() {
625 Side::Ahead => write!(f, "dead ahead"),
626 Side::Astern => write!(f, "dead astern"),
627 Side::Starboard => write!(f, "{magnitude:.precision$}° green"),
628 Side::Port => write!(f, "{magnitude:.precision$}° red"),
629 }
630 }
631}
632
633#[cfg(feature = "serde")]
634impl TryFrom<f64> for Variation {
635 type Error = KernelError;
636
637 fn try_from(value: f64) -> Result<Self> {
639 Self::new(value)
640 }
641}
642
643#[cfg(feature = "serde")]
644impl From<Variation> for f64 {
645 fn from(value: Variation) -> Self {
646 value.0
647 }
648}
649
650#[cfg(feature = "serde")]
651impl TryFrom<f64> for Deviation {
652 type Error = KernelError;
653
654 fn try_from(value: f64) -> Result<Self> {
656 Self::new(value)
657 }
658}
659
660#[cfg(feature = "serde")]
661impl From<Deviation> for f64 {
662 fn from(value: Deviation) -> Self {
663 value.0
664 }
665}
666
667#[cfg(feature = "serde")]
668impl TryFrom<f64> for RelativeBearing {
669 type Error = KernelError;
670
671 fn try_from(value: f64) -> Result<Self> {
673 Self::new(value)
674 }
675}
676
677#[cfg(feature = "serde")]
678impl From<RelativeBearing> for f64 {
679 fn from(value: RelativeBearing) -> Self {
680 value.0
681 }
682}
683
684#[cfg(feature = "serde")]
685impl<F: Frame> serde::Serialize for Direction<F> {
686 fn serialize<S: serde::Serializer>(
688 &self,
689 serializer: S,
690 ) -> core::result::Result<S::Ok, S::Error> {
691 serializer.serialize_f64(self.degrees)
692 }
693}
694
695#[cfg(feature = "serde")]
696impl<'de, F: Frame> serde::Deserialize<'de> for Direction<F> {
697 fn deserialize<D: serde::Deserializer<'de>>(
700 deserializer: D,
701 ) -> core::result::Result<Self, D::Error> {
702 let degrees = f64::deserialize(deserializer)?;
703 Self::new(degrees).map_err(serde::de::Error::custom)
704 }
705}
706
707#[cfg(test)]
708#[allow(clippy::unwrap_used, clippy::float_cmp, clippy::indexing_slicing)]
709mod tests {
710 use super::*;
711 use alloc::format;
712
713 #[test]
714 fn wrap360_is_correct_below_minus_360() {
715 assert_eq!(wrap360(-400.0), 320.0);
717 assert_eq!(wrap360(-720.0), 0.0);
718 assert_eq!(wrap360(-0.0), 0.0);
719 assert_eq!(wrap360(0.0), 0.0);
720 assert_eq!(wrap360(360.0), 0.0);
721 assert_eq!(wrap360(725.0), 5.0);
722 assert!(wrap360(-1e15).is_finite());
723 }
724
725 #[test]
726 fn wrap360_keeps_tiny_negatives_off_the_far_end() {
727 for value in [-1e-16, -1e-18, -f64::MIN_POSITIVE, -1e-14] {
729 let wrapped = wrap360(value);
730 assert!(
731 (0.0..360.0).contains(&wrapped),
732 "{value} wrapped to {wrapped}"
733 );
734 }
735 assert_eq!(wrap360(-1e-16), 0.0);
736 assert!(wrap360(-1e-10) < 360.0);
738 assert!(wrap360(-1e-10) > 359.999);
739 }
740
741 #[test]
742 fn wrap360_never_leaves_the_interval() {
743 let mut value = -2000.0;
744 while value < 2000.0 {
745 let wrapped = wrap360(value);
746 assert!((0.0..360.0).contains(&wrapped), "{value} -> {wrapped}");
747 value += 0.37;
748 }
749 }
750
751 #[test]
752 fn wrap180_is_symmetric() {
753 assert_eq!(wrap180(0.0), 0.0);
754 assert_eq!(wrap180(90.0), 90.0);
755 assert_eq!(wrap180(180.0), -180.0);
756 assert_eq!(wrap180(190.0), -170.0);
757 assert_eq!(wrap180(-190.0), 170.0);
758 }
759
760 #[test]
761 fn direction_rejects_bad_input() {
762 assert!(TrueCourse::new(f64::NAN).is_err());
763 assert!(TrueCourse::new(f64::INFINITY).is_err());
764 assert!(TrueCourse::new(-0.1).is_err());
765 assert!(TrueCourse::new(400.0).is_err());
766 assert!(TrueCourse::wrap(f64::NAN).is_err());
767 }
768
769 #[test]
770 fn direction_normalises() {
771 assert_eq!(TrueCourse::new(360.0).unwrap().degrees(), 0.0);
772 assert_eq!(TrueCourse::wrap(-10.0).unwrap().degrees(), 350.0);
773 assert_eq!(TrueCourse::wrap(730.0).unwrap().degrees(), 10.0);
774 }
775
776 #[test]
777 fn reciprocal_round_trips() {
778 for degrees in [0.0, 45.0, 179.0, 180.0, 359.9] {
779 let direction = TrueCourse::new(degrees).unwrap();
780 assert!((direction.reciprocal().reciprocal().degrees() - degrees).abs() < 1e-12);
781 }
782 }
783
784 #[test]
785 fn signed_difference_takes_the_short_way() {
786 let a = TrueCourse::new(350.0).unwrap();
787 let b = TrueCourse::new(10.0).unwrap();
788 assert!((a.signed_difference(b) - 20.0).abs() < 1e-12);
789 assert!((b.signed_difference(a) + 20.0).abs() < 1e-12);
790 assert!((a.angular_distance(b) - 20.0).abs() < 1e-12);
791 }
792
793 #[test]
794 fn variation_and_deviation_validate() {
795 assert!(Variation::new(-181.0).is_err());
796 assert!(Variation::new(f64::NAN).is_err());
797 assert!(Variation::new(180.0).is_ok());
798 assert!(Deviation::new(f64::INFINITY).is_err());
799 assert_eq!(Deviation::ZERO.degrees(), 0.0);
800 }
801
802 #[test]
803 fn relative_bearing_sides() {
804 assert_eq!(RelativeBearing::new(0.0).unwrap().side(), Side::Ahead);
805 assert_eq!(RelativeBearing::new(90.0).unwrap().side(), Side::Starboard);
806 assert_eq!(RelativeBearing::new(180.0).unwrap().side(), Side::Astern);
807 assert_eq!(RelativeBearing::new(270.0).unwrap().side(), Side::Port);
808 assert!((RelativeBearing::new(270.0).unwrap().signed_degrees() + 90.0).abs() < 1e-12);
809 }
810
811 #[test]
812 fn display_is_chart_style() {
813 assert_eq!(format!("{}", TrueCourse::new(45.0).unwrap()), "045.0°T");
814 assert_eq!(
815 format!("{}", MagneticCourse::new(357.89).unwrap()),
816 "357.9°M"
817 );
818 assert_eq!(format!("{}", Variation::new(-2.7).unwrap()), "2.7°W");
819 assert_eq!(format!("{}", Deviation::new(1.5).unwrap()), "1.5°E");
820 assert_eq!(
821 format!("{}", RelativeBearing::new(300.0).unwrap()),
822 "60.0° red"
823 );
824 }
825
826 #[test]
827 fn cardinal_points_are_multiples_of_forty_five_degrees() {
828 for (index, point) in CardinalPoint::ALL.into_iter().enumerate() {
829 assert_eq!(point.whole_degrees(), i32::try_from(index).unwrap() * 45);
830 assert_eq!(point.degrees(), f64::from(point.whole_degrees()));
831 }
832 }
833
834 #[test]
835 fn cardinal_points_parse_regardless_of_case_and_padding() {
836 for point in CardinalPoint::ALL {
837 let name = point.abbreviation();
838 assert_eq!(name.parse::<CardinalPoint>().unwrap(), point);
839 assert_eq!(name.to_lowercase().parse::<CardinalPoint>().unwrap(), point);
840 assert_eq!(
841 format!(" {name} ").parse::<CardinalPoint>().unwrap(),
842 point
843 );
844 assert_eq!(format!("{point}"), name);
845 }
846 }
847
848 #[test]
849 fn an_unknown_point_names_itself_in_the_error() {
850 assert!(matches!(
851 "NNE".parse::<CardinalPoint>(),
852 Err(KernelError::UnknownCardinalDirection { direction }) if direction == "NNE"
853 ));
854 assert!("north".parse::<CardinalPoint>().is_err());
855 assert!("".parse::<CardinalPoint>().is_err());
856 }
857
858 #[test]
859 fn a_cardinal_point_becomes_a_direction_in_any_frame() {
860 assert_eq!(CompassCourse::from(CardinalPoint::SW).degrees(), 225.0);
861 assert_eq!(TrueCourse::from(CardinalPoint::N).degrees(), 0.0);
862 assert_eq!(
863 MagneticCourse::from(CardinalPoint::NW),
864 Direction::from(CardinalPoint::NW)
865 );
866 }
867}