Skip to main content

navcore_routes/
plan.rs

1//! What a route is, before any question of where it lives.
2//!
3//! A route is a named, ordered list of waypoints, nothing more. Signal
4//! K's own Route resource carries none of IEC
5//! 61174 (RTZ)'s per-leg richness -- no per-leg cross-track corridor,
6//! planned speed, safety-contour override or rhumb/great-circle choice,
7//! only `name`/`description`/`distance`/a line -- and no client needs
8//! that richness either, recomputing a whole route from one global
9//! vessel-settings struct rather than per leg. See [`crate::signalk`]'s
10//! own doc for the rest of that reasoning.
11
12use nav_math::Position;
13
14/// A route, in the order its waypoints are sailed.
15#[derive(Debug, Clone, PartialEq)]
16pub struct Route {
17    /// Signal K's UUID, and the identity everywhere else too -- the same
18    /// choice `waypoints::Waypoint::uuid`'s own doc explains, for the
19    /// same reason: one id, whatever server holds it.
20    pub uuid: String,
21    /// What the mariner calls it.
22    pub name: Option<String>,
23    /// Anything they wrote about it.
24    pub description: Option<String>,
25    /// In the order they are sailed.
26    pub waypoints: Vec<Waypoint>,
27}
28
29impl Route {
30    /// A fresh, unnamed route through these waypoints. The caller
31    /// supplies the id: minting one needs randomness, and this layer
32    /// deliberately has no source of it, the same reasoning
33    /// `waypoints::Waypoint::at`'s own doc gives.
34    #[must_use]
35    pub fn new(uuid: impl Into<String>, waypoints: Vec<Waypoint>) -> Self {
36        Self { uuid: uuid.into(), name: None, description: None, waypoints }
37    }
38
39    /// The legs, in order: each is the pair of waypoints it runs
40    /// between. Empty for a route of fewer than two waypoints, which is
41    /// a position rather than a plan.
42    pub fn legs(&self) -> impl Iterator<Item = (&Waypoint, &Waypoint)> {
43        self.waypoints.windows(2).map(|pair| (&pair[0], &pair[1]))
44    }
45
46    /// Total distance in nautical miles, each leg a rhumb line -- the
47    /// same convention `nav_math::rhumb` and this whole codebase's own
48    /// charts already draw a route leg as.
49    #[must_use]
50    pub fn distance_nm(&self) -> f64 {
51        self.legs().map(|(from, to)| nav_math::rhumb::distance_nm(from.position, to.position)).sum()
52    }
53}
54
55/// One waypoint of a route -- a position and, optionally, what the
56/// mariner calls it, the same pair Signal K's own `coordinatesMeta`
57/// carries per point.
58#[derive(Debug, Clone, PartialEq)]
59pub struct Waypoint {
60    /// Where it is.
61    pub position: Position,
62    /// What the mariner calls it, if anything.
63    pub name: Option<String>,
64}
65
66impl Waypoint {
67    /// A plain, unnamed waypoint at a position.
68    #[must_use]
69    pub const fn at(position: Position) -> Self {
70        Self { position, name: None }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    fn route() -> Route {
79        Route::new(
80            "94052456-65fa-48ce-a85d-41b78a9d2111",
81            vec![
82                Waypoint::at(Position::new(45.5150, 13.5680)),
83                Waypoint::at(Position::new(45.5500, 13.6200)),
84                Waypoint::at(Position::new(45.5480, 13.7300)),
85            ],
86        )
87    }
88
89    #[test]
90    fn a_new_route_has_no_name_or_description() {
91        let route = route();
92        assert_eq!(route.name, None);
93        assert_eq!(route.description, None);
94        assert_eq!(route.waypoints.len(), 3);
95    }
96
97    #[test]
98    fn a_single_waypoint_has_no_distance() {
99        let lone = Route::new("id", vec![Waypoint::at(Position::new(45.0, 13.0))]);
100        assert_eq!(lone.distance_nm(), 0.0);
101        assert_eq!(lone.legs().count(), 0);
102    }
103
104    #[test]
105    fn a_route_of_no_waypoints_has_no_distance_either() {
106        // Not just the single-waypoint case's neighbour: a route just
107        // cleared, or loaded from a malformed empty `LineString`, is
108        // still a valid (if useless) `Route` to hold -- nothing here
109        // should panic or divide by zero.
110        let empty = Route::new("id", Vec::new());
111        assert_eq!(empty.distance_nm(), 0.0);
112        assert_eq!(empty.legs().count(), 0);
113    }
114
115    #[test]
116    fn a_route_of_three_waypoints_has_two_legs() {
117        assert_eq!(route().legs().count(), 2);
118    }
119
120    #[test]
121    fn distance_sums_every_leg() {
122        // Loose bound: the three points here span roughly ten nautical
123        // miles end to end, and the point of this test is that both legs
124        // were added, not the exact rhumb-line arithmetic nav_math's own
125        // tests already cover.
126        let nm = route().distance_nm();
127        assert!(nm > 1.0, "{nm}");
128    }
129
130    #[test]
131    fn at_makes_a_plain_unnamed_waypoint() {
132        let waypoint = Waypoint::at(Position::new(1.0, 2.0));
133        assert_eq!(waypoint.name, None);
134        assert_eq!(waypoint.position, Position::new(1.0, 2.0));
135    }
136}