1use crate::{EARTH_RADIUS_NM, Position, angle, great_circle};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct TrackError {
13 pub cross_track_nm: f64,
21 pub along_track_nm: f64,
27}
28
29impl TrackError {
30 #[must_use]
32 pub fn remaining_nm(&self, leg_length_nm: f64) -> f64 {
33 leg_length_nm - self.along_track_nm
34 }
35}
36
37#[must_use]
44pub fn track_error(from: Position, to: Position, vessel: Position) -> Option<TrackError> {
45 let leg_rad = great_circle::central_angle_rad(from, to);
46 if leg_rad < f64::EPSILON {
47 return None;
48 }
49
50 let d13 = great_circle::central_angle_rad(from, vessel);
51 let brg13 = great_circle::initial_bearing_deg(from, vessel);
52 let brg12 = great_circle::initial_bearing_deg(from, to);
53 let delta = angle::diff(brg13, brg12).to_radians();
54
55 let xt_rad = (d13.sin() * delta.sin()).asin();
56
57 let ratio = (d13.cos() / xt_rad.cos()).clamp(-1.0, 1.0);
61 let mut at_rad = ratio.acos();
62
63 if delta.abs() > std::f64::consts::FRAC_PI_2 {
67 at_rad = -at_rad;
68 }
69
70 Some(TrackError {
71 cross_track_nm: xt_rad * EARTH_RADIUS_NM,
72 along_track_nm: at_rad * EARTH_RADIUS_NM,
73 })
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 fn close(a: f64, b: f64, tol: f64) -> bool {
81 (a - b).abs() < tol
82 }
83
84 fn leg() -> (Position, Position) {
87 (Position::new(45.0, 13.0), Position::new(46.0, 13.0))
88 }
89
90 #[test]
91 fn dead_on_track_is_zero_off() {
92 let (a, b) = leg();
93 let te = track_error(a, b, Position::new(45.5, 13.0)).unwrap();
94 assert!(close(te.cross_track_nm, 0.0, 1e-9), "{te:?}");
95 assert!(close(te.along_track_nm, 30.02, 1e-2), "{te:?}");
96 }
97
98 #[test]
99 fn east_of_a_northbound_leg_is_starboard_and_positive() {
100 let (a, b) = leg();
101 let te = track_error(a, b, Position::new(45.5, 13.1)).unwrap();
102 assert!(te.cross_track_nm > 0.0, "{te:?}");
103 assert!(close(te.cross_track_nm, 4.20, 0.02), "{te:?}");
105 }
106
107 #[test]
108 fn west_of_a_northbound_leg_is_port_and_negative() {
109 let (a, b) = leg();
110 let te = track_error(a, b, Position::new(45.5, 12.9)).unwrap();
111 assert!(te.cross_track_nm < 0.0, "{te:?}");
112 }
113
114 #[test]
115 fn sign_follows_the_leg_direction_not_the_compass() {
116 let (a, b) = leg();
118 let vessel = Position::new(45.5, 13.1);
119 let fwd = track_error(a, b, vessel).unwrap();
120 let rev = track_error(b, a, vessel).unwrap();
121 assert!(close(fwd.cross_track_nm, -rev.cross_track_nm, 1e-6));
122 }
123
124 #[test]
125 fn before_the_start_the_along_track_goes_negative() {
126 let (a, b) = leg();
127 let te = track_error(a, b, Position::new(44.5, 13.0)).unwrap();
128 assert!(te.along_track_nm < 0.0, "{te:?}");
129 assert!(close(te.along_track_nm, -30.02, 1e-2), "{te:?}");
130 }
131
132 #[test]
133 fn past_the_end_it_exceeds_the_leg_length() {
134 let (a, b) = leg();
135 let leg_len = great_circle::distance_nm(a, b);
136 let te = track_error(a, b, Position::new(46.5, 13.0)).unwrap();
137 assert!(te.along_track_nm > leg_len, "{te:?}");
138 assert!(te.remaining_nm(leg_len) < 0.0, "{te:?}");
139 }
140
141 #[test]
142 fn a_zero_length_leg_has_no_side_to_be_off() {
143 let a = Position::new(45.0, 13.0);
144 assert!(track_error(a, a, Position::new(45.1, 13.1)).is_none());
145 }
146}