maps_engine_rust/
navigation.rs1use crate::geo::{bearing, destination, haversine, polyline_length, LonLat};
4
5#[derive(Debug, Clone)]
7pub struct Maneuver {
8 pub at_index: usize,
10 pub location: LonLat,
11 pub turn_deg: f64,
13 pub instruction_vi: String,
14 pub instruction_en: String,
15}
16
17#[derive(Debug, Clone)]
19pub struct NavProgress {
20 pub distance_done_m: f64,
22 pub distance_left_m: f64,
24 pub fraction: f64,
26 pub next_index: usize,
28}
29
30pub 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
52pub 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
82pub 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 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
118pub 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), LonLat::new(105.86, 21.03), ]
145 }
146
147 fn gentle_left() -> Vec<LonLat> {
148 vec![
149 LonLat::new(105.85, 21.02),
150 LonLat::new(105.86, 21.02), LonLat::new(105.87, 21.03), ]
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}