solverforge_maps/routing/
network.rs

1//! Road network graph core types and routing.
2
3use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5
6use super::algo::{astar, kosaraju_scc};
7use super::coord::Coord;
8use super::error::RoutingError;
9use super::geo::{coord_key, haversine_distance};
10use super::graph::{DiGraph, EdgeIdx, NodeIdx};
11use super::spatial::{KdTree, Point2D, Segment, SegmentIndex};
12
13#[derive(Debug, Clone)]
14pub struct NodeData {
15    pub lat: f64,
16    pub lng: f64,
17}
18
19impl NodeData {
20    #[inline]
21    pub fn coord(&self) -> Coord {
22        Coord::new(self.lat, self.lng)
23    }
24}
25
26#[derive(Debug, Clone)]
27pub struct EdgeData {
28    pub travel_time_s: f64,
29    pub distance_m: f64,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct RouteResult {
34    pub duration_seconds: i64,
35    pub distance_meters: f64,
36    pub geometry: Vec<Coord>,
37}
38
39impl RouteResult {
40    /// Simplify the route geometry using Douglas-Peucker algorithm.
41    ///
42    /// # Arguments
43    /// * `tolerance_m` - Tolerance in meters; points within this distance of
44    ///   the simplified line are removed.
45    pub fn simplify(mut self, tolerance_m: f64) -> Self {
46        if self.geometry.len() <= 2 {
47            return self;
48        }
49
50        self.geometry = douglas_peucker(&self.geometry, tolerance_m);
51        self
52    }
53}
54
55#[derive(Debug, Clone, Copy, Default)]
56pub struct SnappedCoord {
57    pub original: Coord,
58    pub snapped: Coord,
59    pub snap_distance_m: f64,
60    pub(crate) node_index: NodeIdx,
61}
62
63/// Data stored in the point spatial index.
64#[derive(Debug, Clone, Copy)]
65struct SpatialPoint {
66    node_index: NodeIdx,
67}
68
69/// Data stored in the segment spatial index.
70#[derive(Debug, Clone, Copy)]
71struct SpatialSegment {
72    from_node: NodeIdx,
73    to_node: NodeIdx,
74    edge_index: EdgeIdx,
75}
76
77#[derive(Debug, Clone, Copy)]
78pub struct EdgeSnappedLocation {
79    pub original: Coord,
80    pub snapped: Coord,
81    pub snap_distance_m: f64,
82    pub(crate) edge_index: EdgeIdx,
83    pub(crate) position: f64,
84    pub(crate) from_node: NodeIdx,
85    pub(crate) to_node: NodeIdx,
86}
87
88pub struct RoadNetwork {
89    pub(super) graph: DiGraph<NodeData, EdgeData>,
90    pub(super) coord_to_node: HashMap<(i64, i64), NodeIdx>,
91    spatial_index: Option<KdTree<SpatialPoint>>,
92    edge_spatial_index: Option<SegmentIndex<SpatialSegment>>,
93}
94
95impl RoadNetwork {
96    pub fn new() -> Self {
97        Self {
98            graph: DiGraph::new(),
99            coord_to_node: HashMap::new(),
100            spatial_index: None,
101            edge_spatial_index: None,
102        }
103    }
104
105    pub(super) fn get_or_create_node(&mut self, lat: f64, lng: f64) -> NodeIdx {
106        let key = coord_key(lat, lng);
107        if let Some(&idx) = self.coord_to_node.get(&key) {
108            idx
109        } else {
110            let idx = self.graph.add_node(NodeData { lat, lng });
111            self.coord_to_node.insert(key, idx);
112            idx
113        }
114    }
115
116    pub(super) fn add_node_at(&mut self, lat: f64, lng: f64) -> NodeIdx {
117        let idx = self.graph.add_node(NodeData { lat, lng });
118        let key = coord_key(lat, lng);
119        self.coord_to_node.insert(key, idx);
120        idx
121    }
122
123    pub(super) fn add_edge(&mut self, from: NodeIdx, to: NodeIdx, data: EdgeData) {
124        self.graph.add_edge(from, to, data);
125    }
126
127    pub(super) fn add_edge_by_index(
128        &mut self,
129        from: usize,
130        to: usize,
131        travel_time_s: f64,
132        distance_m: f64,
133    ) {
134        let from_idx = NodeIdx::new(from);
135        let to_idx = NodeIdx::new(to);
136        self.graph.add_edge(
137            from_idx,
138            to_idx,
139            EdgeData {
140                travel_time_s,
141                distance_m,
142            },
143        );
144    }
145
146    pub(super) fn build_spatial_index(&mut self) {
147        // Build point index for nodes
148        let points: Vec<(Point2D, SpatialPoint)> = self
149            .graph
150            .node_indices()
151            .filter_map(|node_idx| {
152                let node = self.graph.node_weight(node_idx)?;
153                Some((
154                    Point2D::new(node.lat, node.lng),
155                    SpatialPoint {
156                        node_index: node_idx,
157                    },
158                ))
159            })
160            .collect();
161
162        self.spatial_index = Some(KdTree::from_items(points));
163
164        // Build segment index for edges
165        let segments: Vec<(Segment, SpatialSegment)> = self
166            .graph
167            .edge_indices()
168            .filter_map(|edge_idx| {
169                let (from_node, to_node) = self.graph.edge_endpoints(edge_idx)?;
170                let from_data = self.graph.node_weight(from_node)?;
171                let to_data = self.graph.node_weight(to_node)?;
172                Some((
173                    Segment::new(
174                        Point2D::new(from_data.lat, from_data.lng),
175                        Point2D::new(to_data.lat, to_data.lng),
176                    ),
177                    SpatialSegment {
178                        from_node,
179                        to_node,
180                        edge_index: edge_idx,
181                    },
182                ))
183            })
184            .collect();
185
186        self.edge_spatial_index = Some(SegmentIndex::bulk_load(segments));
187    }
188
189    /// Iterate over all nodes as (lat, lng) pairs.
190    pub fn nodes_iter(&self) -> impl Iterator<Item = (f64, f64)> + '_ {
191        self.graph
192            .node_indices()
193            .filter_map(|idx| self.graph.node_weight(idx).map(|n| (n.lat, n.lng)))
194    }
195
196    /// Iterate over all edges as (from_idx, to_idx, travel_time_s, distance_m) tuples.
197    pub fn edges_iter(&self) -> impl Iterator<Item = (usize, usize, f64, f64)> + '_ {
198        self.graph.edge_indices().filter_map(|idx| {
199            let (from, to) = self.graph.edge_endpoints(idx)?;
200            let weight = self.graph.edge_weight(idx)?;
201            Some((
202                from.index(),
203                to.index(),
204                weight.travel_time_s,
205                weight.distance_m,
206            ))
207        })
208    }
209
210    /// Snap a coordinate to the nearest node in the road network.
211    ///
212    /// Returns `None` if the network is empty.
213    pub fn snap_to_road(&self, coord: Coord) -> Option<NodeIdx> {
214        self.spatial_index
215            .as_ref()?
216            .nearest_neighbor(&Point2D::new(coord.lat, coord.lng))
217            .map(|p| p.node_index)
218    }
219
220    /// Snap a coordinate to the road network with detailed information.
221    ///
222    /// Returns a `SnappedCoord` containing both original and snapped coordinates,
223    /// the snap distance, and an internal node index for routing.
224    pub fn snap_to_road_detailed(&self, coord: Coord) -> Result<SnappedCoord, RoutingError> {
225        let tree = self
226            .spatial_index
227            .as_ref()
228            .ok_or(RoutingError::SnapFailed {
229                coord,
230                nearest_distance_m: None,
231            })?;
232
233        let query = Point2D::new(coord.lat, coord.lng);
234        let (nearest, _dist_sq) =
235            tree.nearest_neighbor_with_distance(&query)
236                .ok_or(RoutingError::SnapFailed {
237                    coord,
238                    nearest_distance_m: None,
239                })?;
240
241        // Get the actual coordinates from the graph node
242        let node_data =
243            self.graph
244                .node_weight(nearest.node_index)
245                .ok_or(RoutingError::SnapFailed {
246                    coord,
247                    nearest_distance_m: None,
248                })?;
249
250        let snapped = Coord::new(node_data.lat, node_data.lng);
251        let snap_distance_m = haversine_distance(coord, snapped);
252
253        Ok(SnappedCoord {
254            original: coord,
255            snapped,
256            snap_distance_m,
257            node_index: nearest.node_index,
258        })
259    }
260
261    /// Snap a coordinate to the nearest road segment (edge-based snapping).
262    ///
263    /// This is production-grade snapping that projects the coordinate onto the
264    /// nearest road segment rather than snapping to the nearest intersection.
265    pub fn snap_to_edge(&self, coord: Coord) -> Result<EdgeSnappedLocation, RoutingError> {
266        let index = self
267            .edge_spatial_index
268            .as_ref()
269            .ok_or(RoutingError::SnapFailed {
270                coord,
271                nearest_distance_m: None,
272            })?;
273
274        let query = Point2D::new(coord.lat, coord.lng);
275        let (segment, seg_data, proj_point, _dist_sq) =
276            index
277                .nearest_segment(&query)
278                .ok_or(RoutingError::SnapFailed {
279                    coord,
280                    nearest_distance_m: None,
281                })?;
282
283        // Compute position along segment
284        let (_, position) = segment.project_point(&query);
285        let snapped = Coord::new(proj_point.x, proj_point.y);
286        let snap_distance_m = haversine_distance(coord, snapped);
287
288        Ok(EdgeSnappedLocation {
289            original: coord,
290            snapped,
291            snap_distance_m,
292            edge_index: seg_data.edge_index,
293            position,
294            from_node: seg_data.from_node,
295            to_node: seg_data.to_node,
296        })
297    }
298
299    /// Find a route between two coordinates.
300    ///
301    /// This method snaps the coordinates to the nearest road network nodes
302    /// and then finds the shortest path by travel time.
303    pub fn route(&self, from: Coord, to: Coord) -> Result<RouteResult, RoutingError> {
304        let start_snap = self.snap_to_road_detailed(from)?;
305        let end_snap = self.snap_to_road_detailed(to)?;
306
307        self.route_snapped(&start_snap, &end_snap)
308    }
309
310    /// Find a route between two edge-snapped locations.
311    ///
312    /// This handles the case where start and end are on road segments,
313    /// not necessarily at intersections.
314    pub fn route_edge_snapped(
315        &self,
316        from: &EdgeSnappedLocation,
317        to: &EdgeSnappedLocation,
318    ) -> Result<RouteResult, RoutingError> {
319        let from_edge = self
320            .graph
321            .edge_weight(from.edge_index)
322            .ok_or(RoutingError::NoPath {
323                from: from.original,
324                to: to.original,
325            })?;
326        let to_edge = self
327            .graph
328            .edge_weight(to.edge_index)
329            .ok_or(RoutingError::NoPath {
330                from: from.original,
331                to: to.original,
332            })?;
333
334        // If both snapped to the same edge, check if direct travel is possible
335        if from.edge_index == to.edge_index {
336            let segment_time = from_edge.travel_time_s;
337            let segment_dist = from_edge.distance_m;
338            let travel_fraction = (to.position - from.position).abs();
339            return Ok(RouteResult {
340                duration_seconds: (segment_time * travel_fraction).round() as i64,
341                distance_meters: segment_dist * travel_fraction,
342                geometry: vec![from.snapped, to.snapped],
343            });
344        }
345
346        // Cost from snap point to from_node (going backward on edge)
347        let cost_to_from_node = from_edge.travel_time_s * from.position;
348        // Cost from snap point to to_node (going forward on edge)
349        let cost_to_to_node = from_edge.travel_time_s * (1.0 - from.position);
350
351        // Cost from to_edge's from_node to snap point
352        let cost_from_dest_from = to_edge.travel_time_s * to.position;
353        // Cost from to_edge's to_node to snap point
354        let cost_from_dest_to = to_edge.travel_time_s * (1.0 - to.position);
355
356        // Try routing from from_node to both destination edge endpoints
357        let mut best_result: Option<(f64, Vec<NodeIdx>, NodeIdx, f64)> = None;
358
359        for &(start_node, start_cost) in &[
360            (from.from_node, cost_to_from_node),
361            (from.to_node, cost_to_to_node),
362        ] {
363            for &(end_node, end_cost) in &[
364                (to.from_node, cost_from_dest_from),
365                (to.to_node, cost_from_dest_to),
366            ] {
367                if start_node == end_node {
368                    let total_cost = start_cost + end_cost;
369                    if best_result.is_none() || total_cost < best_result.as_ref().unwrap().0 {
370                        best_result = Some((total_cost, vec![start_node], end_node, end_cost));
371                    }
372                    continue;
373                }
374
375                let result = astar(
376                    &self.graph,
377                    start_node,
378                    |n| n == end_node,
379                    |e| e.travel_time_s,
380                    |_| 0.0,
381                );
382
383                if let Some((path_cost, path)) = result {
384                    let total_cost = start_cost + path_cost + end_cost;
385                    if best_result.is_none() || total_cost < best_result.as_ref().unwrap().0 {
386                        best_result = Some((total_cost, path, end_node, end_cost));
387                    }
388                }
389            }
390        }
391
392        match best_result {
393            Some((total_cost, path, _, _)) => {
394                let mut geometry = vec![from.snapped];
395                for &idx in &path {
396                    if let Some(node) = self.graph.node_weight(idx) {
397                        geometry.push(node.coord());
398                    }
399                }
400                geometry.push(to.snapped);
401
402                let mut distance = 0.0;
403                distance += from_edge.distance_m * from.position.min(1.0 - from.position);
404                for window in path.windows(2) {
405                    if let Some(edge) = self.graph.find_edge(window[0], window[1]) {
406                        if let Some(weight) = self.graph.edge_weight(edge) {
407                            distance += weight.distance_m;
408                        }
409                    }
410                }
411
412                distance += to_edge.distance_m * to.position.min(1.0 - to.position);
413
414                Ok(RouteResult {
415                    duration_seconds: total_cost.round() as i64,
416                    distance_meters: distance,
417                    geometry,
418                })
419            }
420            None => Err(RoutingError::NoPath {
421                from: from.original,
422                to: to.original,
423            }),
424        }
425    }
426
427    pub fn route_snapped(
428        &self,
429        from: &SnappedCoord,
430        to: &SnappedCoord,
431    ) -> Result<RouteResult, RoutingError> {
432        if from.node_index == to.node_index {
433            return Ok(RouteResult {
434                duration_seconds: 0,
435                distance_meters: 0.0,
436                geometry: vec![from.original, to.original],
437            });
438        }
439
440        let result = astar(
441            &self.graph,
442            from.node_index,
443            |n| n == to.node_index,
444            |e| e.travel_time_s,
445            |_| 0.0,
446        );
447
448        match result {
449            Some((cost, path)) => {
450                let geometry: Vec<Coord> = path
451                    .iter()
452                    .filter_map(|&idx| self.graph.node_weight(idx).map(|n| n.coord()))
453                    .collect();
454
455                let mut distance = 0.0;
456                for window in path.windows(2) {
457                    if let Some(edge) = self.graph.find_edge(window[0], window[1]) {
458                        if let Some(weight) = self.graph.edge_weight(edge) {
459                            distance += weight.distance_m;
460                        }
461                    }
462                }
463
464                Ok(RouteResult {
465                    duration_seconds: cost.round() as i64,
466                    distance_meters: distance,
467                    geometry,
468                })
469            }
470            None => Err(RoutingError::NoPath {
471                from: from.original,
472                to: to.original,
473            }),
474        }
475    }
476
477    pub fn route_with(
478        &self,
479        from: Coord,
480        to: Coord,
481        objective: Objective,
482    ) -> Result<RouteResult, RoutingError> {
483        let start_snap = self.snap_to_road_detailed(from)?;
484        let end_snap = self.snap_to_road_detailed(to)?;
485
486        if start_snap.node_index == end_snap.node_index {
487            return Ok(RouteResult {
488                duration_seconds: 0,
489                distance_meters: 0.0,
490                geometry: vec![from, to],
491            });
492        }
493
494        let result = match objective {
495            Objective::Time => astar(
496                &self.graph,
497                start_snap.node_index,
498                |n| n == end_snap.node_index,
499                |e| e.travel_time_s,
500                |_| 0.0,
501            ),
502            Objective::Distance => astar(
503                &self.graph,
504                start_snap.node_index,
505                |n| n == end_snap.node_index,
506                |e| e.distance_m,
507                |_| 0.0,
508            ),
509        };
510
511        match result {
512            Some((_, path)) => {
513                let geometry: Vec<Coord> = path
514                    .iter()
515                    .filter_map(|&idx| self.graph.node_weight(idx).map(|n| n.coord()))
516                    .collect();
517
518                let mut distance = 0.0;
519                let mut time = 0.0;
520                for window in path.windows(2) {
521                    if let Some(edge) = self.graph.find_edge(window[0], window[1]) {
522                        if let Some(weight) = self.graph.edge_weight(edge) {
523                            distance += weight.distance_m;
524                            time += weight.travel_time_s;
525                        }
526                    }
527                }
528
529                Ok(RouteResult {
530                    duration_seconds: time.round() as i64,
531                    distance_meters: distance,
532                    geometry,
533                })
534            }
535            None => Err(RoutingError::NoPath { from, to }),
536        }
537    }
538
539    pub fn node_count(&self) -> usize {
540        self.graph.node_count()
541    }
542
543    pub fn edge_count(&self) -> usize {
544        self.graph.edge_count()
545    }
546
547    pub fn strongly_connected_components(&self) -> usize {
548        kosaraju_scc(&self.graph).len()
549    }
550
551    pub fn largest_component_fraction(&self) -> f64 {
552        let sccs = kosaraju_scc(&self.graph);
553        if sccs.is_empty() {
554            return 0.0;
555        }
556        let largest = sccs.iter().map(|c| c.len()).max().unwrap_or(0);
557        let total = self.graph.node_count();
558        if total == 0 {
559            0.0
560        } else {
561            largest as f64 / total as f64
562        }
563    }
564
565    pub fn is_strongly_connected(&self) -> bool {
566        self.strongly_connected_components() == 1
567    }
568
569    /// Filter the network to keep only the largest strongly connected component.
570    pub fn filter_to_largest_scc(&mut self) {
571        let sccs = kosaraju_scc(&self.graph);
572        if sccs.len() <= 1 {
573            return; // Already connected or empty
574        }
575
576        // Find the largest SCC
577        let largest_scc: HashSet<NodeIdx> = sccs
578            .into_iter()
579            .max_by_key(|scc| scc.len())
580            .unwrap_or_default()
581            .into_iter()
582            .collect();
583
584        // Retain only nodes in the largest SCC
585        self.graph.retain_nodes(|n, _| largest_scc.contains(&n));
586
587        self.rebuild_coord_to_node();
588        self.build_spatial_index();
589    }
590
591    fn rebuild_coord_to_node(&mut self) {
592        self.coord_to_node.clear();
593        for idx in self.graph.node_indices() {
594            if let Some(node) = self.graph.node_weight(idx) {
595                let key = coord_key(node.lat, node.lng);
596                self.coord_to_node.insert(key, idx);
597            }
598        }
599    }
600}
601
602impl Default for RoadNetwork {
603    fn default() -> Self {
604        Self::new()
605    }
606}
607
608impl RoadNetwork {
609    /// Build a network from raw node/edge data (for testing).
610    ///
611    /// # Arguments
612    /// * `nodes` - Slice of (lat, lng) tuples for each node
613    /// * `edges` - Slice of (from_idx, to_idx, time_s, dist_m) tuples
614    ///
615    /// # Example
616    /// ```
617    /// use solverforge_maps::RoadNetwork;
618    ///
619    /// let nodes = &[(40.0, -75.0), (39.8, -75.2)];
620    /// let edges = &[(0, 1, 60.0, 1000.0), (1, 0, 60.0, 1000.0)];
621    /// let network = RoadNetwork::from_test_data(nodes, edges);
622    ///
623    /// assert_eq!(network.node_count(), 2);
624    /// assert_eq!(network.edge_count(), 2);
625    /// ```
626    pub fn from_test_data(nodes: &[(f64, f64)], edges: &[(usize, usize, f64, f64)]) -> Self {
627        let mut network = Self::new();
628
629        // Add all nodes
630        for &(lat, lng) in nodes {
631            network.add_node_at(lat, lng);
632        }
633
634        // Add all edges
635        for &(from, to, time_s, dist_m) in edges {
636            network.add_edge_by_index(from, to, time_s, dist_m);
637        }
638
639        // Build spatial index for snapping
640        network.build_spatial_index();
641
642        network
643    }
644}
645
646#[derive(Debug, Clone, Copy, PartialEq, Eq)]
647pub enum Objective {
648    Time,
649    Distance,
650}
651
652/// Douglas-Peucker line simplification algorithm.
653fn douglas_peucker(points: &[Coord], tolerance_m: f64) -> Vec<Coord> {
654    if points.len() <= 2 {
655        return points.to_vec();
656    }
657
658    let first = points[0];
659    let last = points[points.len() - 1];
660    let mut max_dist = 0.0;
661    let mut max_idx = 0;
662
663    for (i, point) in points.iter().enumerate().skip(1).take(points.len() - 2) {
664        let dist = perpendicular_distance(*point, first, last);
665        if dist > max_dist {
666            max_dist = dist;
667            max_idx = i;
668        }
669    }
670
671    if max_dist > tolerance_m {
672        let mut left = douglas_peucker(&points[..=max_idx], tolerance_m);
673        let right = douglas_peucker(&points[max_idx..], tolerance_m);
674
675        left.pop();
676        left.extend(right);
677        left
678    } else {
679        vec![first, last]
680    }
681}
682
683fn perpendicular_distance(point: Coord, line_start: Coord, line_end: Coord) -> f64 {
684    let dx = line_end.lng - line_start.lng;
685    let dy = line_end.lat - line_start.lat;
686
687    let line_length_sq = dx * dx + dy * dy;
688    if line_length_sq < f64::EPSILON {
689        return haversine_distance(point, line_start);
690    }
691
692    let t =
693        ((point.lng - line_start.lng) * dx + (point.lat - line_start.lat) * dy) / line_length_sq;
694    let t = t.clamp(0.0, 1.0);
695
696    let projected = Coord::new(line_start.lat + t * dy, line_start.lng + t * dx);
697
698    haversine_distance(point, projected)
699}