1use crate::angle::TrueCourse;
41use crate::error::{ensure_range, KernelError, Result};
42use crate::event::PositionSource;
43use crate::geodesy::{Ellipsoid, GeodeticPoint, Height};
44use crate::gnss::GnssFix;
45use crate::local::{LocalFrame, Ned, Vector3};
46use crate::math;
47use crate::matrix::{Matrix, Vector};
48use crate::observation::{ObservationStatus, Observed, Quality};
49use crate::position::Position;
50use crate::snapshot::{ErrorEllipse, GroundTrack, NavigationSnapshot};
51use crate::time::{Instant, Utc};
52use crate::units::{Angle, Distance, Speed, METRES_PER_NAUTICAL_MILE};
53
54#[doc(hidden)]
59pub const STATE_DIM: usize = 6;
60
61#[non_exhaustive]
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum StateComponent {
68 North,
70 East,
72 Heading,
74 SpeedThroughWater,
76 CurrentNorth,
78 CurrentEast,
80}
81
82impl StateComponent {
83 pub const ALL: [Self; STATE_DIM] = [
85 Self::North,
86 Self::East,
87 Self::Heading,
88 Self::SpeedThroughWater,
89 Self::CurrentNorth,
90 Self::CurrentEast,
91 ];
92
93 #[doc(hidden)]
98 #[must_use]
99 pub const fn index(self) -> usize {
100 match self {
101 Self::North => 0,
102 Self::East => 1,
103 Self::Heading => 2,
104 Self::SpeedThroughWater => 3,
105 Self::CurrentNorth => 4,
106 Self::CurrentEast => 5,
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq)]
121pub struct StatePriors {
122 position: Distance,
123 heading: Angle,
124 heading_unknown: Angle,
125 speed: Speed,
126 speed_unknown: Speed,
127 current: Speed,
128}
129
130impl StatePriors {
131 #[must_use]
135 pub const fn standard() -> Self {
136 Self {
137 position: Distance::from_nautical_miles_unchecked(10.0 / METRES_PER_NAUTICAL_MILE),
138 heading: Angle::from_degrees_unchecked(3.0),
139 heading_unknown: Angle::from_degrees_unchecked(104.0),
140 speed: Speed::from_knots_unchecked(1.0),
141 speed_unknown: Speed::from_knots_unchecked(10.0),
142 current: Speed::from_knots_unchecked(1.0),
143 }
144 }
145
146 pub fn with_position(mut self, sigma: Distance) -> Result<Self> {
152 ensure_positive("position prior", sigma.metres())?;
153 self.position = sigma;
154 Ok(self)
155 }
156
157 pub fn with_heading(mut self, known: Angle, unknown: Angle) -> Result<Self> {
164 ensure_positive("heading prior", known.degrees())?;
165 ensure_positive("unknown-heading prior", unknown.degrees())?;
166 self.heading = known;
167 self.heading_unknown = unknown;
168 Ok(self)
169 }
170
171 pub fn with_speed(mut self, known: Speed, unknown: Speed) -> Result<Self> {
178 ensure_positive("speed prior", known.knots())?;
179 ensure_positive("unknown-speed prior", unknown.knots())?;
180 self.speed = known;
181 self.speed_unknown = unknown;
182 Ok(self)
183 }
184
185 pub fn with_current(mut self, sigma: Speed) -> Result<Self> {
191 ensure_positive("current prior", sigma.knots())?;
192 self.current = sigma;
193 Ok(self)
194 }
195
196 #[must_use]
198 pub const fn position(&self) -> Distance {
199 self.position
200 }
201
202 #[must_use]
204 pub const fn heading(&self) -> Angle {
205 self.heading
206 }
207
208 #[must_use]
210 pub const fn heading_unknown(&self) -> Angle {
211 self.heading_unknown
212 }
213
214 #[must_use]
216 pub const fn speed(&self) -> Speed {
217 self.speed
218 }
219
220 #[must_use]
222 pub const fn speed_unknown(&self) -> Speed {
223 self.speed_unknown
224 }
225
226 #[must_use]
228 pub const fn current(&self) -> Speed {
229 self.current
230 }
231}
232
233fn ensure_positive(parameter: &'static str, value: f64) -> Result<()> {
236 ensure_range(parameter, value, f64::MIN_POSITIVE, f64::MAX)
237}
238
239const MAX_SPEED_METRES_PER_SECOND: f64 = 51.4;
241
242const MAX_CURRENT_METRES_PER_SECOND: f64 = 10.3;
244
245#[derive(Debug, Clone, Copy, PartialEq)]
247pub struct NavigationState {
248 valid_at: Instant<Utc>,
249 frame: LocalFrame,
251 vector: Vector<STATE_DIM>,
253 covariance: Matrix<STATE_DIM, STATE_DIM>,
254}
255
256impl NavigationState {
257 pub fn initialised_from(
269 fix: &GnssFix,
270 heading: Option<Observed<TrueCourse, Angle>>,
271 ) -> Result<Self> {
272 Self::initialised_with(fix, heading, &StatePriors::standard())
273 }
274
275 pub fn initialised_with(
281 fix: &GnssFix,
282 heading: Option<Observed<TrueCourse, Angle>>,
283 priors: &StatePriors,
284 ) -> Result<Self> {
285 let anchor = GeodeticPoint::new(fix.position(), Height::above_ellipsoid(Distance::ZERO));
286 let frame = LocalFrame::at(anchor, &Ellipsoid::WGS84)?;
287
288 let (heading_radians, heading_sigma) = match (heading, fix.course_over_ground()) {
289 (Some(observed), _) => (
290 math::to_radians(observed.value().degrees()),
291 observed
292 .quality()
293 .sigma()
294 .map_or(priors.heading, |sigma| *sigma),
295 ),
296 (None, Some(course)) => (math::to_radians(course.degrees()), priors.heading),
297 (None, None) => (0.0, priors.heading_unknown),
298 };
299 let (speed, speed_sigma) = match fix.speed_over_ground() {
300 Some(speed) => (speed, priors.speed),
301 None => (Speed::ZERO, priors.speed_unknown),
302 };
303 let position_sigma = fix.horizontal_accuracy().unwrap_or(priors.position);
304
305 let vector = Vector::from_column([
306 0.0,
307 0.0,
308 heading_radians,
309 speed.metres_per_second(),
310 0.0,
311 0.0,
312 ]);
313 let square = |value: f64| value * value;
314 let covariance = Matrix::diagonal([
315 square(position_sigma.metres()),
316 square(position_sigma.metres()),
317 square(heading_sigma.radians()),
318 square(speed_sigma.metres_per_second()),
319 square(priors.current.metres_per_second()),
320 square(priors.current.metres_per_second()),
321 ]);
322 Self::from_parts(fix.taken_at(), frame, vector, covariance)
323 }
324
325 #[doc(hidden)]
338 pub fn from_parts(
339 valid_at: Instant<Utc>,
340 frame: LocalFrame,
341 vector: Vector<STATE_DIM>,
342 covariance: Matrix<STATE_DIM, STATE_DIM>,
343 ) -> Result<Self> {
344 if !vector.is_finite() {
345 return Err(KernelError::NotFinite {
346 parameter: "navigation state",
347 value: f64::NAN,
348 });
349 }
350 if !covariance.is_finite() {
351 return Err(KernelError::NotFinite {
352 parameter: "navigation state covariance",
353 value: f64::NAN,
354 });
355 }
356 if !covariance.is_covariance() {
357 return Err(KernelError::NotCovariance {
358 context: "a navigation state's covariance",
359 });
360 }
361 let element = |component: StateComponent| vector.element(component.index()).unwrap_or(0.0);
362 let speed = element(StateComponent::SpeedThroughWater);
363 if math::abs(speed) > MAX_SPEED_METRES_PER_SECOND {
364 return Err(KernelError::OutOfRange {
365 parameter: "speed through water",
366 value: speed,
367 min: -MAX_SPEED_METRES_PER_SECOND,
368 max: MAX_SPEED_METRES_PER_SECOND,
369 });
370 }
371 let current = math::hypot(
372 element(StateComponent::CurrentNorth),
373 element(StateComponent::CurrentEast),
374 );
375 if current > MAX_CURRENT_METRES_PER_SECOND {
376 return Err(KernelError::OutOfRange {
377 parameter: "current",
378 value: current,
379 min: 0.0,
380 max: MAX_CURRENT_METRES_PER_SECOND,
381 });
382 }
383 let heading = element(StateComponent::Heading);
386 let mut vector = vector;
387 vector.set(
388 StateComponent::Heading.index(),
389 0,
390 math::to_radians(crate::angle::wrap360(math::to_degrees(heading))),
391 );
392 Ok(Self {
393 valid_at,
394 frame,
395 vector,
396 covariance: covariance.symmetrised(),
397 })
398 }
399
400 #[must_use]
402 pub const fn valid_at(&self) -> Instant<Utc> {
403 self.valid_at
404 }
405
406 #[doc(hidden)]
411 #[must_use]
412 pub const fn frame(&self) -> &LocalFrame {
413 &self.frame
414 }
415
416 #[doc(hidden)]
421 #[must_use]
422 pub const fn vector(&self) -> &Vector<STATE_DIM> {
423 &self.vector
424 }
425
426 #[doc(hidden)]
431 #[must_use]
432 pub const fn covariance(&self) -> &Matrix<STATE_DIM, STATE_DIM> {
433 &self.covariance
434 }
435
436 fn component(&self, component: StateComponent) -> f64 {
438 self.vector.element(component.index()).unwrap_or(0.0)
439 }
440
441 fn variance(&self, component: StateComponent) -> f64 {
443 self.covariance
444 .get(component.index(), component.index())
445 .unwrap_or(0.0)
446 .max(0.0)
447 }
448
449 #[must_use]
451 pub fn position(&self) -> Position {
452 let (north, east) = (
453 self.component(StateComponent::North),
454 self.component(StateComponent::East),
455 );
456 if north == 0.0 && east == 0.0 {
459 return self.frame.origin().position();
460 }
461 let displacement: Vector3<Ned, Distance> = Vector3::new(
462 Distance::from_metres(north).unwrap_or(Distance::ZERO),
463 Distance::from_metres(east).unwrap_or(Distance::ZERO),
464 Distance::ZERO,
465 );
466 self.frame
469 .point_from_ned(displacement)
470 .map_or(self.frame.origin().position(), |point| point.position())
471 }
472
473 #[must_use]
475 pub fn heading(&self) -> TrueCourse {
476 TrueCourse::wrap(math::to_degrees(self.component(StateComponent::Heading)))
477 .unwrap_or(TrueCourse::NORTH)
478 }
479
480 #[must_use]
482 pub fn heading_sigma(&self) -> Angle {
483 Angle::from_radians(math::sqrt(self.variance(StateComponent::Heading)))
484 .unwrap_or(Angle::ZERO)
485 }
486
487 #[must_use]
489 pub fn speed_through_water(&self) -> Speed {
490 Speed::from_metres_per_second(self.component(StateComponent::SpeedThroughWater))
491 .unwrap_or(Speed::ZERO)
492 }
493
494 #[must_use]
496 pub fn speed_sigma(&self) -> Speed {
497 Speed::from_metres_per_second(math::sqrt(self.variance(StateComponent::SpeedThroughWater)))
498 .unwrap_or(Speed::ZERO)
499 }
500
501 #[must_use]
503 pub fn current(&self) -> Vector3<Ned, Speed> {
504 Vector3::new(
505 Speed::from_metres_per_second(self.component(StateComponent::CurrentNorth))
506 .unwrap_or(Speed::ZERO),
507 Speed::from_metres_per_second(self.component(StateComponent::CurrentEast))
508 .unwrap_or(Speed::ZERO),
509 Speed::ZERO,
510 )
511 }
512
513 #[must_use]
515 pub fn velocity_over_ground(&self) -> Vector3<Ned, Speed> {
516 let heading = self.component(StateComponent::Heading);
517 let speed = self.component(StateComponent::SpeedThroughWater);
518 let through_water: Vector3<Ned, Speed> = Vector3::new(
519 Speed::from_metres_per_second(speed * math::cos(heading)).unwrap_or(Speed::ZERO),
520 Speed::from_metres_per_second(speed * math::sin(heading)).unwrap_or(Speed::ZERO),
521 Speed::ZERO,
522 );
523 through_water + self.current()
524 }
525
526 #[must_use]
528 pub fn ground_track(&self) -> Option<GroundTrack> {
529 let velocity = self.velocity_over_ground();
530 Some(GroundTrack {
531 course_over_ground: velocity.horizontal_direction()?,
532 speed_over_ground: velocity.horizontal_magnitude(),
533 })
534 }
535
536 #[must_use]
538 pub fn horizontal_error(&self) -> ErrorEllipse {
539 let north = StateComponent::North.index();
540 let east = StateComponent::East.index();
541 ErrorEllipse::from_covariance(
542 self.covariance.get(north, north).unwrap_or(0.0),
543 self.covariance.get(north, east).unwrap_or(0.0),
544 self.covariance.get(east, east).unwrap_or(0.0),
545 )
546 .unwrap_or_else(|| ErrorEllipse::circular(Distance::ZERO))
547 }
548
549 #[must_use]
555 pub fn project(&self) -> NavigationSnapshot {
556 let ellipse = self.horizontal_error();
557 let position = Observed::new(
558 self.position(),
559 self.valid_at,
560 Quality::new(ObservationStatus::Valid).with_sigma(ellipse.equivalent_radius()),
561 );
562 let mut snapshot = NavigationSnapshot::EMPTY
563 .with_position(position, PositionSource::Estimated)
564 .with_heading(self.heading(), Some(self.heading_sigma()))
565 .with_horizontal_error(ellipse);
566 if let Some(track) = self.ground_track() {
567 snapshot = snapshot.with_ground_track(track);
568 }
569 snapshot
570 }
571}
572
573#[cfg(test)]
574#[allow(clippy::unwrap_used, clippy::float_cmp)]
575mod tests {
576 use super::*;
577 use crate::gnss::Dop;
578
579 fn fix() -> GnssFix {
580 GnssFix::builder(
581 Instant::from_unix_seconds(1_000),
582 "50°45.3'N 001°20.0'W".parse().unwrap(),
583 )
584 .course_over_ground(TrueCourse::new(90.0).unwrap())
585 .speed_over_ground(Speed::from_metres_per_second(5.0).unwrap())
586 .hdop(Dop::new(2.0).unwrap())
587 .build()
588 }
589
590 #[test]
591 fn a_state_from_a_fix_starts_at_the_fix() {
592 let state = NavigationState::initialised_from(&fix(), None).unwrap();
593 assert_eq!(state.valid_at(), Instant::from_unix_seconds(1_000));
594 assert_eq!(state.position(), fix().position());
595 assert_eq!(state.heading().degrees(), 90.0);
596 assert_eq!(state.speed_through_water().metres_per_second(), 5.0);
597 assert_eq!(state.current().magnitude().metres_per_second(), 0.0);
598 let velocity = state.velocity_over_ground();
599 assert!(velocity.north().metres_per_second().abs() < 1e-12);
600 assert!((velocity.east().metres_per_second() - 5.0).abs() < 1e-12);
601 let track = state.ground_track().unwrap();
602 assert!((track.course_over_ground.degrees() - 90.0).abs() < 1e-9);
603 assert!((state.horizontal_error().semi_major().metres() - 8.0).abs() < 1e-9);
605 assert!((state.horizontal_error().semi_minor().metres() - 8.0).abs() < 1e-9);
606 assert!((state.heading_sigma().degrees() - 3.0).abs() < 1e-9);
607 }
608
609 #[test]
610 fn a_supplied_heading_beats_the_course_over_ground() {
611 let heading = Observed::new(
612 TrueCourse::new(80.0).unwrap(),
613 Instant::from_unix_seconds(1_000),
614 Quality::new(ObservationStatus::Valid).with_sigma(Angle::from_degrees(0.5).unwrap()),
615 );
616 let state = NavigationState::initialised_from(&fix(), Some(heading)).unwrap();
617 assert_eq!(state.heading().degrees(), 80.0);
618 assert!((state.heading_sigma().degrees() - 0.5).abs() < 1e-9);
619 }
620
621 #[test]
622 fn a_bare_fix_leaves_heading_and_speed_unknown_and_says_so() {
623 let bare = GnssFix::builder(Instant::from_unix_seconds(0), fix().position()).build();
624 let state = NavigationState::initialised_from(&bare, None).unwrap();
625 assert_eq!(state.speed_through_water().metres_per_second(), 0.0);
626 assert!(state.ground_track().is_none());
627 assert!(state.heading_sigma().degrees() > 100.0);
628 assert!(state.speed_sigma().knots() > 5.0);
629 assert!((state.horizontal_error().semi_major().metres() - 10.0).abs() < 1e-9);
631 }
632
633 #[test]
634 fn priors_of_your_own_are_used_and_a_sigma_of_nothing_is_refused() {
635 let bare = GnssFix::builder(Instant::from_unix_seconds(0), fix().position()).build();
636 let priors = StatePriors::standard()
637 .with_position(Distance::from_metres(25.0).unwrap())
638 .unwrap()
639 .with_heading(
640 Angle::from_degrees(1.0).unwrap(),
641 Angle::from_degrees(90.0).unwrap(),
642 )
643 .unwrap()
644 .with_speed(
645 Speed::from_knots(0.5).unwrap(),
646 Speed::from_knots(4.0).unwrap(),
647 )
648 .unwrap()
649 .with_current(Speed::from_knots(2.0).unwrap())
650 .unwrap();
651 assert_eq!(priors.position().metres(), 25.0);
652 assert_eq!(priors.heading_unknown().degrees(), 90.0);
653 assert_eq!(priors.speed_unknown().knots(), 4.0);
654 assert_eq!(priors.current().knots(), 2.0);
655 let state = NavigationState::initialised_with(&bare, None, &priors).unwrap();
656 assert!((state.horizontal_error().semi_major().metres() - 25.0).abs() < 1e-9);
657 assert!((state.heading_sigma().degrees() - 90.0).abs() < 1e-9);
658 assert!((state.speed_sigma().knots() - 4.0).abs() < 1e-9);
659
660 let standard = StatePriors::standard();
662 assert!(standard.with_position(Distance::ZERO).is_err());
663 assert!(standard
664 .with_position(Distance::from_metres(-1.0).unwrap())
665 .is_err());
666 assert!(standard
667 .with_heading(Angle::ZERO, Angle::from_degrees(90.0).unwrap())
668 .is_err());
669 assert!(standard
670 .with_heading(Angle::from_degrees(1.0).unwrap(), Angle::ZERO)
671 .is_err());
672 assert!(standard
673 .with_speed(Speed::ZERO, Speed::from_knots(4.0).unwrap())
674 .is_err());
675 assert!(standard
676 .with_speed(Speed::from_knots(0.5).unwrap(), Speed::ZERO)
677 .is_err());
678 assert!(standard.with_current(Speed::ZERO).is_err());
679 assert!(standard
680 .with_current(Speed::from_knots_unchecked(f64::NAN))
681 .is_err());
682 assert!((standard.position().metres() - 10.0).abs() < 1e-9);
684 assert_eq!(standard.heading().degrees(), 3.0);
685 assert_eq!(standard.heading_unknown().degrees(), 104.0);
686 assert_eq!(standard.speed().knots(), 1.0);
687 assert_eq!(standard.speed_unknown().knots(), 10.0);
688 assert_eq!(standard.current().knots(), 1.0);
689 }
690
691 #[test]
692 fn the_projection_is_a_snapshot_the_display_can_use() {
693 let state = NavigationState::initialised_from(&fix(), None).unwrap();
694 let snapshot = state.project();
695 assert_eq!(snapshot.source(), Some(PositionSource::Estimated));
696 assert_eq!(*snapshot.position().unwrap().value(), fix().position());
697 assert_eq!(snapshot.heading().unwrap().degrees(), 90.0);
698 assert!(snapshot.horizontal_error().is_some());
699 assert!(snapshot.ground_track().is_some());
700 assert_eq!(snapshot.age(), None);
701 }
702
703 #[test]
704 fn the_invariants_are_checked_on_the_way_in() {
705 let good = NavigationState::initialised_from(&fix(), None).unwrap();
706 let frame = *good.frame();
707 let at = good.valid_at();
708 let mut vector = *good.vector();
709 let covariance = *good.covariance();
710
711 let mut bad = covariance;
713 bad.set(0, 1, 1e9);
714 bad.set(1, 0, 1e9);
715 assert!(NavigationState::from_parts(at, frame, vector, bad).is_err());
716 vector.set(StateComponent::SpeedThroughWater.index(), 0, 200.0);
718 assert!(NavigationState::from_parts(at, frame, vector, covariance).is_err());
719 let mut wrapped = *good.vector();
721 wrapped.set(StateComponent::Heading.index(), 0, math::to_radians(370.0));
722 let state = NavigationState::from_parts(at, frame, wrapped, covariance).unwrap();
723 assert!((state.heading().degrees() - 10.0).abs() < 1e-9);
724 let mut nan = *good.vector();
726 nan.set(0, 0, f64::NAN);
727 assert!(NavigationState::from_parts(at, frame, nan, covariance).is_err());
728 }
729}