1#![forbid(unsafe_code)]
21
22use enc_store::{ChartStore, Finding, Leg, Sailing, Severity, StoreError, Vessel, check_leg};
23use routes::Route;
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct Defaults {
28 pub safety_contour_m: f64,
32 pub port_xtd_nm: f64,
34 pub starboard_xtd_nm: f64,
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct LegReport {
41 pub index: usize,
44 pub to_name: Option<String>,
46 pub starts_at_nm: f64,
48 pub length_nm: f64,
50 pub findings: Vec<Finding>,
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct RouteReport {
57 pub uuid: String,
59 pub name: Option<String>,
61 pub distance_nm: f64,
63 pub legs: Vec<LegReport>,
65}
66
67impl RouteReport {
68 #[must_use]
70 pub fn worst(&self) -> Option<Severity> {
71 self.legs
72 .iter()
73 .flat_map(|leg| leg.findings.iter())
74 .map(|finding| finding.severity)
75 .min()
76 }
77
78 #[must_use]
81 pub fn is_clear(&self) -> bool {
82 !self.legs.iter().flat_map(|leg| leg.findings.iter()).any(|finding| {
83 matches!(finding.severity, Severity::Unsafe | Severity::Unsurveyed | Severity::Coarse)
84 })
85 }
86
87 #[must_use]
90 pub fn findings_along_route(&self) -> Vec<(f64, usize, &Finding)> {
91 let mut all: Vec<(f64, usize, &Finding)> = self
92 .legs
93 .iter()
94 .flat_map(|leg| {
95 leg.findings
96 .iter()
97 .map(move |finding| (leg.starts_at_nm + finding.along_track_nm, leg.index, finding))
98 })
99 .collect();
100 all.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.2.severity.cmp(&b.2.severity)));
101 all
102 }
103}
104
105pub fn check_route(chart: &ChartStore, route: &Route, defaults: Defaults) -> Result<RouteReport, StoreError> {
111 let mut legs = Vec::new();
112 let mut starts_at_nm = 0.0;
113
114 for (index, (from, to)) in route.legs().enumerate() {
115 let leg = Leg {
116 from: from.position,
117 to: to.position,
118 port_xtd_nm: defaults.port_xtd_nm,
119 starboard_xtd_nm: defaults.starboard_xtd_nm,
120 sailing: Sailing::Rhumb,
121 };
122 let vessel = Vessel { safety_contour_m: defaults.safety_contour_m };
123
124 let length_nm = leg.length_nm();
125 legs.push(LegReport {
126 index,
127 to_name: to.name.clone(),
128 starts_at_nm,
129 length_nm,
130 findings: check_leg(chart, leg, vessel)?,
131 });
132 starts_at_nm += length_nm;
133 }
134
135 Ok(RouteReport { uuid: route.uuid.clone(), name: route.name.clone(), distance_nm: starts_at_nm, legs })
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use nav_math::Position;
142 use routes::Waypoint;
143
144 fn defaults() -> Defaults {
145 Defaults { safety_contour_m: 3.0, port_xtd_nm: 0.05, starboard_xtd_nm: 0.05 }
146 }
147
148 fn route() -> Route {
149 let mut route = Route::new(
150 "94052456-65fa-48ce-a85d-41b78a9d2111",
151 vec![
152 Waypoint::at(Position::new(45.5150, 13.5680)),
153 Waypoint::at(Position::new(45.5500, 13.6200)),
154 Waypoint::at(Position::new(45.5480, 13.7300)),
155 ],
156 );
157 route.name = Some("Piran to Koper".to_owned());
158 route
159 }
160
161 #[test]
162 fn every_leg_takes_the_vessels_own_standing_tolerances() {
163 let plan = route();
164 let (from, to) = plan.legs().next().expect("a leg");
165 let leg = Leg {
166 from: from.position,
167 to: to.position,
168 port_xtd_nm: defaults().port_xtd_nm,
169 starboard_xtd_nm: defaults().starboard_xtd_nm,
170 sailing: Sailing::Rhumb,
171 };
172
173 assert_eq!(leg.port_xtd_nm, 0.05);
174 assert_eq!(leg.starboard_xtd_nm, 0.05);
175 }
176
177 fn report(findings: Vec<(usize, Severity)>) -> RouteReport {
179 let plan = route();
180 let mut legs: Vec<LegReport> = plan
181 .legs()
182 .enumerate()
183 .map(|(index, (_, to))| LegReport {
184 index,
185 to_name: to.name.clone(),
186 starts_at_nm: index as f64 * 2.0,
187 length_nm: 2.0,
188 findings: Vec::new(),
189 })
190 .collect();
191
192 for (leg_index, severity) in findings {
193 legs[leg_index].findings.push(Finding {
194 severity,
195 class: "DEPARE".to_owned(),
196 along_track_nm: 0.5,
197 until_nm: 0.5,
198 position: Position::new(45.53, 13.60),
199 reason: "test".to_owned(),
200 });
201 }
202
203 RouteReport { uuid: plan.uuid.clone(), name: plan.name.clone(), distance_nm: 4.0, legs }
204 }
205
206 #[test]
207 fn a_clean_route_is_clear() {
208 assert!(report(Vec::new()).is_clear());
209 }
210
211 #[test]
212 fn a_caution_does_not_stop_a_route_being_clear() {
213 let clean = report(vec![(1, Severity::Caution)]);
216 assert!(clean.is_clear());
217 assert_eq!(clean.worst(), Some(Severity::Caution));
218 }
219
220 #[test]
221 fn anything_the_check_could_not_answer_stops_it_being_clear() {
222 for severity in [Severity::Unsafe, Severity::Unsurveyed, Severity::Coarse] {
223 assert!(!report(vec![(0, severity)]).is_clear(), "{severity:?} should not pass");
224 }
225 }
226
227 #[test]
228 fn findings_are_offset_onto_the_whole_route() {
229 let checked = report(vec![(0, Severity::Caution), (1, Severity::Unsafe)]);
233 let along = checked.findings_along_route();
234
235 assert_eq!(along[0].0, 0.5, "first leg, half a mile in");
236 assert_eq!(along[1].0, 2.5, "second leg, two miles further on");
237 assert_eq!(along[1].1, 1, "and it knows which leg that was");
238 }
239}