Skip to main content

maps_engine_rust/
navigation.rs

1//! Turn-by-turn navigation math. Port of `navigation.ts` + `routing.ts`.
2
3use crate::geo::{bearing, destination, haversine, polyline_length, LonLat};
4
5/// A single navigation maneuver.
6#[derive(Debug, Clone)]
7pub struct Maneuver {
8    /// Index of the route point where the maneuver happens.
9    pub at_index: usize,
10    pub location: LonLat,
11    /// Turn angle in degrees: negative = left, positive = right.
12    pub turn_deg: f64,
13    pub instruction_vi: String,
14    pub instruction_en: String,
15}
16
17/// Navigation progress along a route.
18#[derive(Debug, Clone)]
19pub struct NavProgress {
20    /// Distance already covered in meters.
21    pub distance_done_m: f64,
22    /// Distance remaining in meters.
23    pub distance_left_m: f64,
24    /// Fraction completed in `[0, 1]`.
25    pub fraction: f64,
26    /// Index of the next route point ahead.
27    pub next_index: usize,
28}
29
30/// Classify a turn angle into a human instruction.
31pub fn turn_instruction(turn_deg: f64) -> (&'static str, &'static str) {
32    let a = turn_deg.abs();
33    if a < 20.0 {
34        ("Đi thẳng", "Go straight")
35    } else if a < 70.0 {
36        if turn_deg < 0.0 {
37            ("Rẽ trái", "Turn left")
38        } else {
39            ("Rẽ phải", "Turn right")
40        }
41    } else if a < 130.0 {
42        if turn_deg < 0.0 {
43            ("Rẽ trái gắt", "Sharp left")
44        } else {
45            ("Rẽ phải gắt", "Sharp right")
46        }
47    } else {
48        ("Quay đầu", "Make a U-turn")
49    }
50}
51
52/// Detect maneuvers along a route polyline.
53pub fn detect_maneuvers(route: &[LonLat]) -> Vec<Maneuver> {
54    let mut out = Vec::new();
55    if route.len() < 3 {
56        return out;
57    }
58    for i in 1..route.len() - 1 {
59        let b1 = bearing(route[i - 1], route[i]);
60        let b2 = bearing(route[i], route[i + 1]);
61        let mut turn = b2 - b1;
62        while turn > 180.0 {
63            turn -= 360.0;
64        }
65        while turn < -180.0 {
66            turn += 360.0;
67        }
68        if turn.abs() >= 20.0 {
69            let (vi, en) = turn_instruction(turn);
70            out.push(Maneuver {
71                at_index: i,
72                location: route[i],
73                turn_deg: turn,
74                instruction_vi: vi.to_string(),
75                instruction_en: en.to_string(),
76            });
77        }
78    }
79    out
80}
81
82/// Compute navigation progress given the user's current position.
83///
84/// Finds the nearest route point ahead and reports distance done/left.
85pub fn nav_progress(route: &[LonLat], user: LonLat) -> Option<NavProgress> {
86    if route.len() < 2 {
87        return None;
88    }
89    let total = polyline_length(route);
90    // Nearest point index (simple; fine for demo-scale routes).
91    let mut best = 0;
92    let mut best_d = f64::INFINITY;
93    for (i, p) in route.iter().enumerate() {
94        let d = haversine(user, *p);
95        if d < best_d {
96            best_d = d;
97            best = i;
98        }
99    }
100    let done: f64 = route
101        .windows(2)
102        .take(best)
103        .map(|w| haversine(w[0], w[1]))
104        .sum();
105    let left = (total - done).max(0.0);
106    Some(NavProgress {
107        distance_done_m: done,
108        distance_left_m: left,
109        fraction: if total > 0.0 {
110            (done / total).min(1.0)
111        } else {
112            0.0
113        },
114        next_index: (best + 1).min(route.len() - 1),
115    })
116}
117
118/// Simulate moving along the route: position after travelling `dist_m` from start.
119pub fn position_at_distance(route: &[LonLat], dist_m: f64) -> Option<LonLat> {
120    if route.is_empty() {
121        return None;
122    }
123    let mut remaining = dist_m;
124    for w in route.windows(2) {
125        let seg = haversine(w[0], w[1]);
126        if remaining <= seg {
127            let brg = bearing(w[0], w[1]);
128            return Some(destination(w[0], remaining, brg));
129        }
130        remaining -= seg;
131    }
132    Some(*route.last().unwrap())
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    fn l_shape() -> Vec<LonLat> {
140        vec![
141            LonLat::new(105.85, 21.02),
142            LonLat::new(105.86, 21.02), // east
143            LonLat::new(105.86, 21.03), // north -> left turn
144        ]
145    }
146
147    fn gentle_left() -> Vec<LonLat> {
148        vec![
149            LonLat::new(105.85, 21.02),
150            LonLat::new(105.86, 21.02), // east
151            LonLat::new(105.87, 21.03), // north-east -> ~45° left turn
152        ]
153    }
154
155    #[test]
156    fn detects_left_turn() {
157        let m = detect_maneuvers(&gentle_left());
158        assert_eq!(m.len(), 1);
159        assert!(m[0].turn_deg < 0.0);
160        assert_eq!(m[0].instruction_vi, "Rẽ trái");
161    }
162
163    #[test]
164    fn detects_sharp_left_turn() {
165        let m = detect_maneuvers(&l_shape());
166        assert_eq!(m.len(), 1);
167        assert_eq!(m[0].instruction_vi, "Rẽ trái gắt");
168    }
169
170    #[test]
171    fn straight_route_has_no_maneuvers() {
172        let r = vec![
173            LonLat::new(105.85, 21.02),
174            LonLat::new(105.86, 21.02),
175            LonLat::new(105.87, 21.02),
176        ];
177        assert!(detect_maneuvers(&r).is_empty());
178    }
179
180    #[test]
181    fn progress_at_start_and_end() {
182        let r = l_shape();
183        let p0 = nav_progress(&r, r[0]).unwrap();
184        assert!(p0.distance_done_m < 1.0);
185        assert!(p0.fraction < 0.01);
186        let p1 = nav_progress(&r, *r.last().unwrap()).unwrap();
187        assert!(p1.distance_left_m < 1.0);
188        assert!((p1.fraction - 1.0).abs() < 0.01);
189    }
190
191    #[test]
192    fn position_at_distance_mid() {
193        let r = vec![LonLat::new(105.85, 21.02), LonLat::new(105.86, 21.02)];
194        let total = polyline_length(&r);
195        let mid = position_at_distance(&r, total / 2.0).unwrap();
196        assert!((mid.lon - 105.855).abs() < 0.001);
197    }
198}