1use geo::prelude::*;
55use geo_types::{Coord, Line, LineString, Point};
56use rstar::{RTree, AABB};
57
58#[derive(Debug, Clone)]
60pub struct Stop {
61 pub id: String,
62 pub location: Coord<f64>,
63}
64
65#[derive(Debug, Clone, PartialEq)]
67pub struct ProjectedStop {
68 pub id: String,
70 pub original_location: Coord<f64>,
72 pub projected_location: Coord<f64>,
74 pub distance_along_route: f64,
76 pub projection_error: f64,
78}
79
80#[derive(Debug, Clone, Copy)]
82pub struct ProjectionConfig {}
83
84impl Default for ProjectionConfig {
85 fn default() -> Self {
86 Self {}
87 }
88}
89
90#[derive(Debug, PartialEq)]
92pub enum ProjectionError {
93 RouteIsEmpty,
95 NoStopsProvided,
97 NoProjectionFound,
100}
101
102#[derive(Debug, Clone, Copy)]
104struct RouteSegment {
105 line: Line<f64>,
106 cumulative_distance: f64,
107}
108
109const CANDIDATE_POOL_SIZE: usize = 5;
111const MAX_SEGMENTS_TO_CHECK: usize = 1000;
113
114pub fn project_stops(
116 route_line: &LineString<f64>,
117 stops: &[Stop],
118 _config: Option<ProjectionConfig>,
119) -> Result<Vec<ProjectedStop>, ProjectionError> {
120 if route_line.0.len() < 2 {
121 return Err(ProjectionError::RouteIsEmpty);
122 }
123 if stops.is_empty() {
124 return Err(ProjectionError::NoStopsProvided);
125 }
126 if stops.len() <= 2 {
127 return project_anchored_only(route_line, stops);
128 }
129
130 let cumulative_distances: Vec<f64> = std::iter::once(0.0)
131 .chain(route_line.lines().scan(0.0, |state, line| {
132 *state += Point(line.start).euclidean_distance(&Point(line.end));
133 Some(*state)
134 }))
135 .collect();
136
137 let rtree = build_rtree(route_line, &cumulative_distances);
138 let mut projected_stops = Vec::with_capacity(stops.len());
139
140 projected_stops.push(anchor_stop(&stops[0], &route_line.0[0], 0.0));
142
143 let mut i = 1;
145 while i < stops.len() - 1 {
146 let stop_to_project = &stops[i];
147 let previous_projection = projected_stops.last().unwrap();
148
149 let mut best_candidate = find_best_candidate(
150 stop_to_project,
151 &rtree,
152 previous_projection.distance_along_route,
153 None, );
155
156 if best_candidate.is_none() && i > 0 {
158 let bad_projection = projected_stops.pop().unwrap();
159 let last_good_projection_dist =
160 projected_stops.last().map_or(0.0, |p| p.distance_along_route);
161
162 let stop_to_fix = &stops[i - 1];
163 if let Some(fixed_projection) = find_best_candidate(
164 stop_to_fix,
165 &rtree,
166 last_good_projection_dist,
167 Some(bad_projection.distance_along_route),
168 ) {
169 projected_stops.push(fixed_projection);
170 best_candidate = find_best_candidate(
171 stop_to_project,
172 &rtree,
173 projected_stops.last().unwrap().distance_along_route,
174 None,
175 );
176 } else {
177 projected_stops.push(bad_projection); }
179 }
180
181 if let Some(winner) = best_candidate {
182 projected_stops.push(winner);
183 i += 1;
184 } else {
185 return Err(ProjectionError::NoProjectionFound);
186 }
187 }
188
189 let total_route_length = *cumulative_distances.last().unwrap();
191 projected_stops.push(anchor_stop(
192 stops.last().unwrap(),
193 route_line.0.last().unwrap(),
194 total_route_length,
195 ));
196
197 Ok(projected_stops)
198}
199
200fn project_anchored_only(
202 route_line: &LineString<f64>,
203 stops: &[Stop],
204) -> Result<Vec<ProjectedStop>, ProjectionError> {
205 let mut projected_stops = Vec::new();
206 if stops.is_empty() {
207 return Ok(projected_stops);
208 }
209
210 projected_stops.push(anchor_stop(&stops[0], &route_line.0[0], 0.0));
211
212 if stops.len() > 1 {
213 let total_route_length = route_line.lines().map(|l| l.euclidean_length()).sum();
214 projected_stops.push(anchor_stop(
215 stops.last().unwrap(),
216 route_line.0.last().unwrap(),
217 total_route_length,
218 ));
219 }
220 Ok(projected_stops)
221}
222
223fn anchor_stop(stop: &Stop, location: &Coord<f64>, distance: f64) -> ProjectedStop {
225 ProjectedStop {
226 id: stop.id.clone(),
227 original_location: stop.location,
228 projected_location: *location,
229 distance_along_route: distance,
230 projection_error: Point(stop.location).euclidean_distance(&Point(*location)),
231 }
232}
233
234fn find_best_candidate(
236 stop: &Stop,
237 rtree: &RTree<RouteSegment>,
238 last_distance_along_route: f64,
239 exclude_distance: Option<f64>,
240) -> Option<ProjectedStop> {
241 let stop_point = Point::from(stop.location);
242 let search_point = [stop.location.x, stop.location.y];
243 let mut valid_candidates = Vec::new();
244 let mut segments_checked = 0;
245 let tolerance = 1e-9;
246
247 for segment in rtree.nearest_neighbor_iter(&search_point) {
248 segments_checked += 1;
249 let closest_pt = segment.line.closest_point(&stop_point);
250 let (projected_point, projected_location) = match closest_pt {
251 geo::Closest::Intersection(p) | geo::Closest::SinglePoint(p) => (p, p.into()),
252 geo::Closest::Indeterminate => continue,
253 };
254
255 let dist_on_segment = Point(segment.line.start).euclidean_distance(&projected_point);
256 let distance_along_route = segment.cumulative_distance + dist_on_segment;
257
258 if let Some(exclude_dist) = exclude_distance {
259 if (distance_along_route - exclude_dist).abs() < tolerance {
260 continue;
261 }
262 }
263
264 if distance_along_route >= last_distance_along_route {
265 let projection_error = stop_point.euclidean_distance(&projected_point);
266 valid_candidates.push(ProjectedStop {
267 id: stop.id.clone(),
268 original_location: stop.location,
269 projected_location,
270 distance_along_route,
271 projection_error,
272 });
273 }
274
275 if valid_candidates.len() >= CANDIDATE_POOL_SIZE
276 || segments_checked >= MAX_SEGMENTS_TO_CHECK
277 {
278 break;
279 }
280 }
281
282 valid_candidates
283 .into_iter()
284 .min_by(|a, b| a.projection_error.partial_cmp(&b.projection_error).unwrap())
285}
286
287fn build_rtree(route_line: &LineString<f64>, cumulative_distances: &[f64]) -> RTree<RouteSegment> {
289 let segments: Vec<RouteSegment> = route_line
290 .lines()
291 .enumerate()
292 .map(|(i, line)| RouteSegment {
293 line,
294 cumulative_distance: cumulative_distances[i],
295 })
296 .collect();
297
298 RTree::bulk_load(segments)
299}
300
301impl rstar::RTreeObject for RouteSegment {
302 type Envelope = AABB<[f64; 2]>;
303
304 fn envelope(&self) -> Self::Envelope {
305 let rect = self.line.bounding_rect();
306 AABB::from_corners([rect.min().x, rect.min().y], [rect.max().x, rect.max().y])
307 }
308}
309
310impl rstar::PointDistance for RouteSegment {
311 fn distance_2(&self, point: &[f64; 2]) -> f64 {
312 let p = Point::new(point[0], point[1]);
313 let closest_point = self.line.closest_point(&p);
314 let d2 = match closest_point {
315 geo::Closest::Intersection(p_on_line) | geo::Closest::SinglePoint(p_on_line) => {
316 p.euclidean_distance(&p_on_line)
317 }
318 geo::Closest::Indeterminate => 0.0,
319 };
320 d2 * d2
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 fn make_stop(id: &str, x: f64, y: f64) -> Stop {
329 Stop {
330 id: id.to_string(),
331 location: Coord { x, y },
332 }
333 }
334
335 #[test]
336 fn test_anchoring_with_two_stops() {
337 let route_line =
338 LineString::from(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 100.0, y: 0.0 }]);
339 let stops = vec![make_stop("1", 1.0, 1.0), make_stop("2", 99.0, -1.0)];
340 let results = project_stops(&route_line, &stops, None).unwrap();
341 assert_eq!(results[0].distance_along_route, 0.0);
342 assert_eq!(results[1].distance_along_route, 100.0);
343 }
344
345 #[test]
346 fn test_simple_linear_route_with_intermediate() {
347 let route_line =
348 LineString::from(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 100.0, y: 0.0 }]);
349 let stops = vec![
350 make_stop("start", 0.0, 1.0),
351 make_stop("mid", 50.0, -2.0),
352 make_stop("end", 100.0, 1.0),
353 ];
354 let results = project_stops(&route_line, &stops, None).unwrap();
355 assert_eq!(results.len(), 3);
356 assert!((results[1].distance_along_route - 50.0).abs() < 1e-9);
357 }
358
359 #[test]
360 fn test_critical_overlap_case() {
361 let route_line = LineString::from(vec![
362 Coord { x: 0.0, y: 0.0 }, Coord { x: 100.0, y: 0.0 }, Coord { x: 200.0, y: 0.0 },
363 Coord { x: 100.0, y: 0.0 }, Coord { x: 0.0, y: 0.0 },
364 ]);
365 let stops = vec![
366 make_stop("start", 0.0, 1.0),
367 make_stop("outbound", 99.0, 2.0),
368 make_stop("turnaround", 201.0, -1.0),
369 make_stop("inbound", 101.0, -2.0),
370 make_stop("end", 0.0, -1.0),
371 ];
372 let results = project_stops(&route_line, &stops, None).unwrap();
373 assert_eq!(results.len(), 5);
374 assert!((results[0].distance_along_route - 0.0).abs() < 1e-9);
375 assert!((results[1].distance_along_route - 99.0).abs() < 1e-9);
376 assert!((results[2].distance_along_route - 200.0).abs() < 1e-9);
377 assert!((results[3].distance_along_route - 299.0).abs() < 1e-9);
378 assert!((results[4].distance_along_route - 400.0).abs() < 1e-9);
379 }
380
381 #[test]
382 fn test_error_conditions() {
383 let empty_route = LineString::from(vec![Coord { x: 0.0, y: 0.0 }]);
384 assert_eq!(
385 project_stops(&empty_route, &[], None),
386 Err(ProjectionError::RouteIsEmpty)
387 );
388 let valid_route =
389 LineString::from(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 1.0, y: 1.0 }]);
390 assert_eq!(
391 project_stops(&valid_route, &[], None),
392 Err(ProjectionError::NoStopsProvided)
393 );
394 }
395
396 #[test]
397 fn test_backtracking_on_hairpin_turn() {
398 let route_line = LineString::from(vec![
401 Coord { x: 0.0, y: 0.0 }, Coord { x: 100.0, y: 0.0 }, Coord { x: 100.0, y: 10.0 }, Coord { x: 0.0, y: 10.0 }, Coord { x: 0.0, y: 20.0 }, ]);
407
408 let stops = vec![
409 make_stop("A", 0.0, 1.0), make_stop("B", 90.0, -1.0),
412 make_stop("C", 10.0, 11.0),
418 make_stop("D", 0.0, 19.0), ];
420
421 let results = project_stops(&route_line, &stops, None).unwrap();
422
423 assert_eq!(results.len(), 4);
424 assert!((results[0].distance_along_route - 0.0).abs() < 1e-9);
426 assert!((results[1].distance_along_route - 90.0).abs() < 1e-9);
428 assert!((results[2].distance_along_route - 200.0).abs() < 1e-9);
430 assert!((results[3].distance_along_route - 220.0).abs() < 1e-9);
432 }
433}