Skip to main content

oxigdal_algorithms/vector/network/
shortest_path.rs

1//! Shortest path algorithms for network analysis
2//!
3//! This module implements various shortest path finding algorithms optimized
4//! for geospatial networks, including:
5//!
6//! - **Dijkstra**: Classic single-source shortest path
7//! - **A***: Heuristic-guided search for faster point-to-point queries
8//! - **Bidirectional Dijkstra**: Searches from both ends simultaneously
9//! - **Turn-restricted routing**: Edge-based state expansion with turn penalties
10//! - **Time-dependent routing**: Edge costs vary by departure time
11//! - **Floyd-Warshall**: All-pairs shortest paths for small graphs
12
13use crate::error::{AlgorithmError, Result};
14use crate::vector::network::{EdgeId, Graph, NodeId, TimeDependentWeight, TurnPenalties};
15use std::cmp::Ordering;
16use std::collections::{BinaryHeap, HashMap};
17
18/// Type alias for edge-based state in turn-restricted routing
19/// Maps (NodeId, arriving EdgeId) to (predecessor NodeId, predecessor arriving EdgeId, used EdgeId)
20type EdgePredecessorMap = HashMap<(NodeId, Option<EdgeId>), (NodeId, Option<EdgeId>, EdgeId)>;
21
22/// Algorithm to use for pathfinding
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum PathFindingAlgorithm {
25    /// Dijkstra's algorithm (guarantees shortest path)
26    Dijkstra,
27    /// A* algorithm (heuristic-guided, faster but needs good heuristic)
28    AStar,
29    /// Bidirectional search (searches from both ends)
30    Bidirectional,
31    /// Dijkstra with turn restrictions (edge-based expansion)
32    DijkstraTurnRestricted,
33    /// A* with turn restrictions
34    AStarTurnRestricted,
35    /// Time-dependent Dijkstra
36    TimeDependentDijkstra,
37}
38
39/// Options for shortest path computation
40#[derive(Debug, Clone)]
41pub struct ShortestPathOptions {
42    /// Algorithm to use
43    pub algorithm: PathFindingAlgorithm,
44    /// Maximum path length (in weight units)
45    pub max_length: Option<f64>,
46    /// Whether to return the full path geometry
47    pub include_geometry: bool,
48    /// Heuristic weight for A* (1.0 = optimal, >1.0 = faster but suboptimal)
49    pub heuristic_weight: f64,
50    /// Turn penalties (optional)
51    pub turn_penalties: Option<TurnPenalties>,
52    /// Time-dependent weights per edge (optional)
53    pub time_dependent_weights: Option<HashMap<EdgeId, TimeDependentWeight>>,
54    /// Departure time (seconds since midnight) for time-dependent routing
55    pub departure_time: f64,
56    /// Weight criteria for multi-criteria routing (maps weight name to coefficient)
57    pub weight_criteria: Option<HashMap<String, f64>>,
58}
59
60impl Default for ShortestPathOptions {
61    fn default() -> Self {
62        Self {
63            algorithm: PathFindingAlgorithm::Dijkstra,
64            max_length: None,
65            include_geometry: true,
66            heuristic_weight: 1.0,
67            turn_penalties: None,
68            time_dependent_weights: None,
69            departure_time: 0.0,
70            weight_criteria: None,
71        }
72    }
73}
74
75/// Result of a shortest path computation
76#[derive(Debug, Clone)]
77pub struct ShortestPath {
78    /// Sequence of nodes in the path
79    pub nodes: Vec<NodeId>,
80    /// Sequence of edges in the path
81    pub edges: Vec<EdgeId>,
82    /// Total cost of the path
83    pub cost: f64,
84    /// Whether a path was found
85    pub found: bool,
86    /// Number of nodes visited during search
87    pub nodes_visited: usize,
88}
89
90impl ShortestPath {
91    /// Create a new empty path (not found)
92    pub fn not_found() -> Self {
93        Self {
94            nodes: Vec::new(),
95            edges: Vec::new(),
96            cost: f64::INFINITY,
97            found: false,
98            nodes_visited: 0,
99        }
100    }
101
102    /// Create a path from node and edge sequences
103    pub fn new(nodes: Vec<NodeId>, edges: Vec<EdgeId>, cost: f64) -> Self {
104        Self {
105            nodes,
106            edges,
107            cost,
108            found: true,
109            nodes_visited: 0,
110        }
111    }
112
113    /// Set the number of nodes visited
114    pub fn with_visited(mut self, count: usize) -> Self {
115        self.nodes_visited = count;
116        self
117    }
118}
119
120/// State in the priority queue for Dijkstra's algorithm
121#[derive(Debug, Clone)]
122struct DijkstraState {
123    cost: f64,
124    node: NodeId,
125}
126
127impl PartialEq for DijkstraState {
128    fn eq(&self, other: &Self) -> bool {
129        self.cost == other.cost && self.node == other.node
130    }
131}
132
133impl Eq for DijkstraState {}
134
135impl PartialOrd for DijkstraState {
136    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
137        Some(self.cmp(other))
138    }
139}
140
141impl Ord for DijkstraState {
142    fn cmp(&self, other: &Self) -> Ordering {
143        // Reverse ordering for min-heap
144        other
145            .cost
146            .partial_cmp(&self.cost)
147            .unwrap_or(Ordering::Equal)
148    }
149}
150
151/// Edge-based state for turn-restricted pathfinding
152#[derive(Debug, Clone)]
153struct EdgeState {
154    cost: f64,
155    node: NodeId,
156    /// The edge used to arrive at this node (None for start node)
157    arriving_edge: Option<EdgeId>,
158}
159
160impl PartialEq for EdgeState {
161    fn eq(&self, other: &Self) -> bool {
162        self.cost == other.cost && self.node == other.node
163    }
164}
165
166impl Eq for EdgeState {}
167
168impl PartialOrd for EdgeState {
169    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
170        Some(self.cmp(other))
171    }
172}
173
174impl Ord for EdgeState {
175    fn cmp(&self, other: &Self) -> Ordering {
176        other
177            .cost
178            .partial_cmp(&self.cost)
179            .unwrap_or(Ordering::Equal)
180    }
181}
182
183/// Find shortest path using Dijkstra's algorithm
184///
185/// # Arguments
186///
187/// * `graph` - The network graph
188/// * `start` - Starting node
189/// * `end` - Target node
190/// * `options` - Path finding options
191///
192/// # Returns
193///
194/// The shortest path from start to end
195pub fn dijkstra_search(
196    graph: &Graph,
197    start: NodeId,
198    end: NodeId,
199    options: &ShortestPathOptions,
200) -> Result<ShortestPath> {
201    if graph.get_node(start).is_none() {
202        return Err(AlgorithmError::InvalidGeometry(format!(
203            "Start node {:?} not found",
204            start
205        )));
206    }
207
208    if graph.get_node(end).is_none() {
209        return Err(AlgorithmError::InvalidGeometry(format!(
210            "End node {:?} not found",
211            end
212        )));
213    }
214
215    let mut distances: HashMap<NodeId, f64> = HashMap::new();
216    let mut predecessors: HashMap<NodeId, (NodeId, EdgeId)> = HashMap::new();
217    let mut heap = BinaryHeap::new();
218    let mut visited_count: usize = 0;
219
220    distances.insert(start, 0.0);
221    heap.push(DijkstraState {
222        cost: 0.0,
223        node: start,
224    });
225
226    while let Some(DijkstraState { cost, node }) = heap.pop() {
227        visited_count += 1;
228
229        // Skip if we've already found a better path
230        if let Some(&best_cost) = distances.get(&node) {
231            if cost > best_cost {
232                continue;
233            }
234        }
235
236        // Check max length constraint
237        if let Some(max_len) = options.max_length {
238            if cost > max_len {
239                continue;
240            }
241        }
242
243        // Found target
244        if node == end {
245            let path = reconstruct_path(start, end, &predecessors, cost);
246            return Ok(path.with_visited(visited_count));
247        }
248
249        // Explore neighbors
250        for &edge_id in graph.outgoing_edges(node) {
251            if let Some(edge) = graph.get_edge(edge_id) {
252                let next_node = edge.target;
253                let edge_cost = get_edge_cost(edge, options);
254                let next_cost = cost + edge_cost;
255
256                let is_better = distances
257                    .get(&next_node)
258                    .is_none_or(|&current| next_cost < current);
259
260                if is_better {
261                    distances.insert(next_node, next_cost);
262                    predecessors.insert(next_node, (node, edge_id));
263                    heap.push(DijkstraState {
264                        cost: next_cost,
265                        node: next_node,
266                    });
267                }
268            }
269        }
270    }
271
272    Ok(ShortestPath::not_found())
273}
274
275/// Find shortest path using Dijkstra with turn restrictions
276///
277/// Uses edge-based state expansion to support turn penalties and restrictions.
278pub fn dijkstra_turn_restricted(
279    graph: &Graph,
280    start: NodeId,
281    end: NodeId,
282    options: &ShortestPathOptions,
283) -> Result<ShortestPath> {
284    if graph.get_node(start).is_none() {
285        return Err(AlgorithmError::InvalidGeometry(format!(
286            "Start node {:?} not found",
287            start
288        )));
289    }
290    if graph.get_node(end).is_none() {
291        return Err(AlgorithmError::InvalidGeometry(format!(
292            "End node {:?} not found",
293            end
294        )));
295    }
296
297    let turn_penalties = options.turn_penalties.as_ref().cloned().unwrap_or_default();
298
299    // State: (node, arriving_edge) -> best cost
300    let mut distances: HashMap<(NodeId, Option<EdgeId>), f64> = HashMap::new();
301    let mut predecessors: EdgePredecessorMap = HashMap::new();
302    let mut heap = BinaryHeap::new();
303    let mut visited_count: usize = 0;
304
305    distances.insert((start, None), 0.0);
306    heap.push(EdgeState {
307        cost: 0.0,
308        node: start,
309        arriving_edge: None,
310    });
311
312    let mut best_end_cost = f64::INFINITY;
313    let mut best_end_arriving: Option<EdgeId> = None;
314
315    while let Some(EdgeState {
316        cost,
317        node,
318        arriving_edge,
319    }) = heap.pop()
320    {
321        visited_count += 1;
322
323        if node == end && cost < best_end_cost {
324            best_end_cost = cost;
325            best_end_arriving = arriving_edge;
326        }
327
328        if cost > best_end_cost {
329            break; // All remaining states are worse
330        }
331
332        let state_key = (node, arriving_edge);
333        if let Some(&best) = distances.get(&state_key) {
334            if cost > best {
335                continue;
336            }
337        }
338
339        if let Some(max_len) = options.max_length {
340            if cost > max_len {
341                continue;
342            }
343        }
344
345        for &edge_id in graph.outgoing_edges(node) {
346            if let Some(edge) = graph.get_edge(edge_id) {
347                let next_node = edge.target;
348
349                // Check turn restriction
350                let turn_cost = if let Some(from_edge) = arriving_edge {
351                    let penalty = turn_penalties.get_penalty(node, from_edge, edge_id);
352                    if penalty.is_infinite() {
353                        continue; // Turn is prohibited
354                    }
355                    penalty
356                } else {
357                    0.0
358                };
359
360                let edge_cost = get_edge_cost(edge, options);
361                let next_cost = cost + edge_cost + turn_cost;
362
363                let next_key = (next_node, Some(edge_id));
364                let is_better = distances
365                    .get(&next_key)
366                    .is_none_or(|&current| next_cost < current);
367
368                if is_better {
369                    distances.insert(next_key, next_cost);
370                    predecessors.insert(next_key, (node, arriving_edge, edge_id));
371                    heap.push(EdgeState {
372                        cost: next_cost,
373                        node: next_node,
374                        arriving_edge: Some(edge_id),
375                    });
376                }
377            }
378        }
379    }
380
381    if best_end_cost < f64::INFINITY {
382        // Reconstruct path from edge-based predecessors
383        let path = reconstruct_edge_based_path(
384            start,
385            end,
386            best_end_arriving,
387            &predecessors,
388            best_end_cost,
389        );
390        Ok(path.with_visited(visited_count))
391    } else {
392        Ok(ShortestPath::not_found())
393    }
394}
395
396/// Reconstruct path from edge-based predecessors
397fn reconstruct_edge_based_path(
398    start: NodeId,
399    end: NodeId,
400    end_arriving: Option<EdgeId>,
401    predecessors: &EdgePredecessorMap,
402    cost: f64,
403) -> ShortestPath {
404    let mut nodes = Vec::new();
405    let mut edges = Vec::new();
406
407    let mut current_node = end;
408    let mut current_arriving = end_arriving;
409
410    nodes.push(current_node);
411
412    while current_node != start || current_arriving.is_some() {
413        let key = (current_node, current_arriving);
414        if let Some(&(prev_node, prev_arriving, edge_id)) = predecessors.get(&key) {
415            edges.push(edge_id);
416            nodes.push(prev_node);
417            current_node = prev_node;
418            current_arriving = prev_arriving;
419        } else {
420            if current_node == start {
421                break;
422            }
423            return ShortestPath::not_found();
424        }
425    }
426
427    nodes.reverse();
428    edges.reverse();
429
430    ShortestPath::new(nodes, edges, cost)
431}
432
433/// Get the effective cost of traversing an edge given the options
434fn get_edge_cost(edge: &crate::vector::network::Edge, options: &ShortestPathOptions) -> f64 {
435    if let Some(ref criteria) = options.weight_criteria {
436        edge.multi_weight.weighted_cost(criteria)
437    } else {
438        edge.weight
439    }
440}
441
442/// Reconstruct path from predecessors map
443fn reconstruct_path(
444    start: NodeId,
445    end: NodeId,
446    predecessors: &HashMap<NodeId, (NodeId, EdgeId)>,
447    cost: f64,
448) -> ShortestPath {
449    let mut nodes = Vec::new();
450    let mut edges = Vec::new();
451    let mut current = end;
452
453    nodes.push(current);
454
455    while current != start {
456        if let Some(&(prev_node, edge_id)) = predecessors.get(&current) {
457            edges.push(edge_id);
458            nodes.push(prev_node);
459            current = prev_node;
460        } else {
461            // Path reconstruction failed
462            return ShortestPath::not_found();
463        }
464    }
465
466    nodes.reverse();
467    edges.reverse();
468
469    ShortestPath::new(nodes, edges, cost)
470}
471
472/// Find shortest path using A* algorithm
473///
474/// Uses Euclidean distance as heuristic.
475pub fn astar_search(
476    graph: &Graph,
477    start: NodeId,
478    end: NodeId,
479    options: &ShortestPathOptions,
480) -> Result<ShortestPath> {
481    if graph.get_node(start).is_none() {
482        return Err(AlgorithmError::InvalidGeometry(format!(
483            "Start node {:?} not found",
484            start
485        )));
486    }
487
488    let end_node = graph
489        .get_node(end)
490        .ok_or_else(|| AlgorithmError::InvalidGeometry(format!("End node {:?} not found", end)))?;
491
492    let end_coord = &end_node.coordinate;
493
494    let mut g_scores: HashMap<NodeId, f64> = HashMap::new();
495    let mut predecessors: HashMap<NodeId, (NodeId, EdgeId)> = HashMap::new();
496    let mut heap = BinaryHeap::new();
497    let mut visited_count: usize = 0;
498
499    g_scores.insert(start, 0.0);
500    let h_start = heuristic(graph, start, end_coord) * options.heuristic_weight;
501
502    heap.push(DijkstraState {
503        cost: h_start,
504        node: start,
505    });
506
507    while let Some(DijkstraState { cost: _f, node }) = heap.pop() {
508        visited_count += 1;
509
510        let current_g = g_scores.get(&node).copied().unwrap_or(f64::INFINITY);
511
512        // Check max length constraint
513        if let Some(max_len) = options.max_length {
514            if current_g > max_len {
515                continue;
516            }
517        }
518
519        if node == end {
520            let cost = g_scores.get(&end).copied().unwrap_or(f64::INFINITY);
521            let path = reconstruct_path(start, end, &predecessors, cost);
522            return Ok(path.with_visited(visited_count));
523        }
524
525        for &edge_id in graph.outgoing_edges(node) {
526            if let Some(edge) = graph.get_edge(edge_id) {
527                let next_node = edge.target;
528                let edge_cost = get_edge_cost(edge, options);
529                let tentative_g = current_g + edge_cost;
530
531                let is_better = g_scores
532                    .get(&next_node)
533                    .is_none_or(|&current| tentative_g < current);
534
535                if is_better {
536                    g_scores.insert(next_node, tentative_g);
537                    predecessors.insert(next_node, (node, edge_id));
538
539                    let h = heuristic(graph, next_node, end_coord) * options.heuristic_weight;
540                    let f = tentative_g + h;
541
542                    heap.push(DijkstraState {
543                        cost: f,
544                        node: next_node,
545                    });
546                }
547            }
548        }
549    }
550
551    Ok(ShortestPath::not_found())
552}
553
554/// Find shortest path using A* with turn restrictions
555pub fn astar_turn_restricted(
556    graph: &Graph,
557    start: NodeId,
558    end: NodeId,
559    options: &ShortestPathOptions,
560) -> Result<ShortestPath> {
561    if graph.get_node(start).is_none() {
562        return Err(AlgorithmError::InvalidGeometry(format!(
563            "Start node {:?} not found",
564            start
565        )));
566    }
567
568    let end_node = graph
569        .get_node(end)
570        .ok_or_else(|| AlgorithmError::InvalidGeometry(format!("End node {:?} not found", end)))?;
571    let end_coord = end_node.coordinate;
572
573    let turn_penalties = options.turn_penalties.as_ref().cloned().unwrap_or_default();
574
575    // State: (node, arriving_edge) -> best g-score
576    let mut g_scores: HashMap<(NodeId, Option<EdgeId>), f64> = HashMap::new();
577    let mut predecessors: EdgePredecessorMap = HashMap::new();
578    let mut heap = BinaryHeap::new();
579    let mut visited_count: usize = 0;
580
581    g_scores.insert((start, None), 0.0);
582    let h_start = heuristic(graph, start, &end_coord) * options.heuristic_weight;
583    heap.push(EdgeState {
584        cost: h_start,
585        node: start,
586        arriving_edge: None,
587    });
588
589    let mut best_end_cost = f64::INFINITY;
590    let mut best_end_arriving: Option<EdgeId> = None;
591
592    while let Some(EdgeState {
593        cost: _f,
594        node,
595        arriving_edge,
596    }) = heap.pop()
597    {
598        visited_count += 1;
599
600        let state_key = (node, arriving_edge);
601        let current_g = g_scores.get(&state_key).copied().unwrap_or(f64::INFINITY);
602
603        if node == end && current_g < best_end_cost {
604            best_end_cost = current_g;
605            best_end_arriving = arriving_edge;
606        }
607
608        if current_g > best_end_cost {
609            break;
610        }
611
612        if let Some(max_len) = options.max_length {
613            if current_g > max_len {
614                continue;
615            }
616        }
617
618        for &edge_id in graph.outgoing_edges(node) {
619            if let Some(edge) = graph.get_edge(edge_id) {
620                let next_node = edge.target;
621
622                let turn_cost = if let Some(from_edge) = arriving_edge {
623                    let penalty = turn_penalties.get_penalty(node, from_edge, edge_id);
624                    if penalty.is_infinite() {
625                        continue;
626                    }
627                    penalty
628                } else {
629                    0.0
630                };
631
632                let edge_cost = get_edge_cost(edge, options);
633                let tentative_g = current_g + edge_cost + turn_cost;
634
635                let next_key = (next_node, Some(edge_id));
636                let is_better = g_scores
637                    .get(&next_key)
638                    .is_none_or(|&current| tentative_g < current);
639
640                if is_better {
641                    g_scores.insert(next_key, tentative_g);
642                    predecessors.insert(next_key, (node, arriving_edge, edge_id));
643
644                    let h = heuristic(graph, next_node, &end_coord) * options.heuristic_weight;
645                    let f = tentative_g + h;
646
647                    heap.push(EdgeState {
648                        cost: f,
649                        node: next_node,
650                        arriving_edge: Some(edge_id),
651                    });
652                }
653            }
654        }
655    }
656
657    if best_end_cost < f64::INFINITY {
658        let path = reconstruct_edge_based_path(
659            start,
660            end,
661            best_end_arriving,
662            &predecessors,
663            best_end_cost,
664        );
665        Ok(path.with_visited(visited_count))
666    } else {
667        Ok(ShortestPath::not_found())
668    }
669}
670
671/// Heuristic function for A* (Euclidean distance)
672fn heuristic(graph: &Graph, node: NodeId, target_coord: &oxigdal_core::vector::Coordinate) -> f64 {
673    if let Some(current_node) = graph.get_node(node) {
674        let dx = current_node.coordinate.x - target_coord.x;
675        let dy = current_node.coordinate.y - target_coord.y;
676        (dx * dx + dy * dy).sqrt()
677    } else {
678        0.0
679    }
680}
681
682/// Find shortest path using bidirectional Dijkstra
683///
684/// Searches from both start and end simultaneously, meeting in the middle.
685/// Typically 2x faster than unidirectional Dijkstra for point-to-point queries.
686pub fn bidirectional_search(
687    graph: &Graph,
688    start: NodeId,
689    end: NodeId,
690    options: &ShortestPathOptions,
691) -> Result<ShortestPath> {
692    if graph.get_node(start).is_none() {
693        return Err(AlgorithmError::InvalidGeometry(format!(
694            "Start node {:?} not found",
695            start
696        )));
697    }
698
699    if graph.get_node(end).is_none() {
700        return Err(AlgorithmError::InvalidGeometry(format!(
701            "End node {:?} not found",
702            end
703        )));
704    }
705
706    // Special case: start == end
707    if start == end {
708        return Ok(ShortestPath::new(vec![start], vec![], 0.0));
709    }
710
711    // Forward search from start
712    let mut forward_distances: HashMap<NodeId, f64> = HashMap::new();
713    let mut forward_predecessors: HashMap<NodeId, (NodeId, EdgeId)> = HashMap::new();
714    let mut forward_heap = BinaryHeap::new();
715    let mut forward_settled: HashMap<NodeId, f64> = HashMap::new();
716
717    // Backward search from end
718    let mut backward_distances: HashMap<NodeId, f64> = HashMap::new();
719    let mut backward_predecessors: HashMap<NodeId, (NodeId, EdgeId)> = HashMap::new();
720    let mut backward_heap = BinaryHeap::new();
721    let mut backward_settled: HashMap<NodeId, f64> = HashMap::new();
722
723    let mut visited_count: usize = 0;
724
725    forward_distances.insert(start, 0.0);
726    forward_heap.push(DijkstraState {
727        cost: 0.0,
728        node: start,
729    });
730
731    backward_distances.insert(end, 0.0);
732    backward_heap.push(DijkstraState {
733        cost: 0.0,
734        node: end,
735    });
736
737    let mut best_cost = f64::INFINITY;
738    let mut meeting_node: Option<NodeId> = None;
739
740    // Alternating forward/backward expansion
741    loop {
742        let forward_min = forward_heap.peek().map(|s| s.cost).unwrap_or(f64::INFINITY);
743        let backward_min = backward_heap
744            .peek()
745            .map(|s| s.cost)
746            .unwrap_or(f64::INFINITY);
747
748        // Stopping criterion: when the sum of minimum costs exceeds best_cost
749        if forward_min + backward_min >= best_cost {
750            break;
751        }
752
753        if forward_heap.is_empty() && backward_heap.is_empty() {
754            break;
755        }
756
757        // Forward step
758        if forward_min <= backward_min {
759            if let Some(DijkstraState { cost, node }) = forward_heap.pop() {
760                visited_count += 1;
761
762                if let Some(&best) = forward_distances.get(&node) {
763                    if cost > best {
764                        continue;
765                    }
766                }
767
768                forward_settled.insert(node, cost);
769
770                // Check if this node was reached by backward search
771                if let Some(&backward_cost) = backward_settled.get(&node) {
772                    let total = cost + backward_cost;
773                    if total < best_cost {
774                        best_cost = total;
775                        meeting_node = Some(node);
776                    }
777                }
778
779                if let Some(max_len) = options.max_length {
780                    if cost > max_len {
781                        continue;
782                    }
783                }
784
785                for &edge_id in graph.outgoing_edges(node) {
786                    if let Some(edge) = graph.get_edge(edge_id) {
787                        let next_node = edge.target;
788                        let edge_cost = get_edge_cost(edge, options);
789                        let next_cost = cost + edge_cost;
790
791                        let is_better = forward_distances
792                            .get(&next_node)
793                            .is_none_or(|&current| next_cost < current);
794
795                        if is_better {
796                            forward_distances.insert(next_node, next_cost);
797                            forward_predecessors.insert(next_node, (node, edge_id));
798                            forward_heap.push(DijkstraState {
799                                cost: next_cost,
800                                node: next_node,
801                            });
802
803                            // Check meeting
804                            if let Some(&bw_cost) = backward_settled.get(&next_node) {
805                                let total = next_cost + bw_cost;
806                                if total < best_cost {
807                                    best_cost = total;
808                                    meeting_node = Some(next_node);
809                                }
810                            }
811                        }
812                    }
813                }
814            }
815        } else {
816            // Backward step
817            if let Some(DijkstraState { cost, node }) = backward_heap.pop() {
818                visited_count += 1;
819
820                if let Some(&best) = backward_distances.get(&node) {
821                    if cost > best {
822                        continue;
823                    }
824                }
825
826                backward_settled.insert(node, cost);
827
828                // Check meeting
829                if let Some(&forward_cost) = forward_settled.get(&node) {
830                    let total = forward_cost + cost;
831                    if total < best_cost {
832                        best_cost = total;
833                        meeting_node = Some(node);
834                    }
835                }
836
837                if let Some(max_len) = options.max_length {
838                    if cost > max_len {
839                        continue;
840                    }
841                }
842
843                for &edge_id in graph.incoming_edges(node) {
844                    if let Some(edge) = graph.get_edge(edge_id) {
845                        let prev_node = edge.source;
846                        let edge_cost = get_edge_cost(edge, options);
847                        let prev_cost = cost + edge_cost;
848
849                        let is_better = backward_distances
850                            .get(&prev_node)
851                            .is_none_or(|&current| prev_cost < current);
852
853                        if is_better {
854                            backward_distances.insert(prev_node, prev_cost);
855                            backward_predecessors.insert(prev_node, (node, edge_id));
856                            backward_heap.push(DijkstraState {
857                                cost: prev_cost,
858                                node: prev_node,
859                            });
860
861                            // Check meeting
862                            if let Some(&fw_cost) = forward_settled.get(&prev_node) {
863                                let total = fw_cost + prev_cost;
864                                if total < best_cost {
865                                    best_cost = total;
866                                    meeting_node = Some(prev_node);
867                                }
868                            }
869                        }
870                    }
871                }
872            }
873        }
874    }
875
876    if let Some(meeting) = meeting_node {
877        let path = reconstruct_bidirectional_path(
878            start,
879            end,
880            meeting,
881            &forward_predecessors,
882            &backward_predecessors,
883            best_cost,
884        );
885        Ok(path.with_visited(visited_count))
886    } else {
887        Ok(ShortestPath::not_found())
888    }
889}
890
891/// Reconstruct path from bidirectional search
892fn reconstruct_bidirectional_path(
893    start: NodeId,
894    end: NodeId,
895    meeting: NodeId,
896    forward_preds: &HashMap<NodeId, (NodeId, EdgeId)>,
897    backward_preds: &HashMap<NodeId, (NodeId, EdgeId)>,
898    cost: f64,
899) -> ShortestPath {
900    let mut nodes = Vec::new();
901    let mut edges = Vec::new();
902
903    // Forward path (start to meeting)
904    let mut current = meeting;
905    let mut forward_nodes = vec![current];
906    let mut forward_edges = Vec::new();
907
908    while current != start {
909        if let Some(&(prev_node, edge_id)) = forward_preds.get(&current) {
910            forward_edges.push(edge_id);
911            forward_nodes.push(prev_node);
912            current = prev_node;
913        } else {
914            break;
915        }
916    }
917
918    forward_nodes.reverse();
919    forward_edges.reverse();
920
921    nodes.extend(forward_nodes);
922    edges.extend(forward_edges);
923
924    // Backward path (meeting to end)
925    current = meeting;
926    while current != end {
927        if let Some(&(next_node, edge_id)) = backward_preds.get(&current) {
928            edges.push(edge_id);
929            nodes.push(next_node);
930            current = next_node;
931        } else {
932            break;
933        }
934    }
935
936    ShortestPath::new(nodes, edges, cost)
937}
938
939/// Time-dependent Dijkstra search
940///
941/// Edge costs vary based on departure time. This models real-world scenarios
942/// like rush-hour traffic, time-varying tolls, etc.
943///
944/// FIFO property must hold: departing later means arriving later on any edge.
945pub fn time_dependent_dijkstra(
946    graph: &Graph,
947    start: NodeId,
948    end: NodeId,
949    options: &ShortestPathOptions,
950) -> Result<ShortestPath> {
951    if graph.get_node(start).is_none() {
952        return Err(AlgorithmError::InvalidGeometry(format!(
953            "Start node {:?} not found",
954            start
955        )));
956    }
957    if graph.get_node(end).is_none() {
958        return Err(AlgorithmError::InvalidGeometry(format!(
959            "End node {:?} not found",
960            end
961        )));
962    }
963
964    let td_weights = options
965        .time_dependent_weights
966        .as_ref()
967        .cloned()
968        .unwrap_or_default();
969
970    let departure_time = options.departure_time;
971
972    // arrival_time[node] = earliest arrival time
973    let mut arrival_times: HashMap<NodeId, f64> = HashMap::new();
974    let mut predecessors: HashMap<NodeId, (NodeId, EdgeId)> = HashMap::new();
975    let mut heap = BinaryHeap::new();
976    let mut visited_count: usize = 0;
977
978    arrival_times.insert(start, departure_time);
979    heap.push(DijkstraState {
980        cost: departure_time,
981        node: start,
982    });
983
984    while let Some(DijkstraState {
985        cost: current_time,
986        node,
987    }) = heap.pop()
988    {
989        visited_count += 1;
990
991        if let Some(&best_time) = arrival_times.get(&node) {
992            if current_time > best_time {
993                continue;
994            }
995        }
996
997        if let Some(max_len) = options.max_length {
998            if current_time - departure_time > max_len {
999                continue;
1000            }
1001        }
1002
1003        if node == end {
1004            let total_cost = current_time - departure_time;
1005            let path = reconstruct_path(start, end, &predecessors, total_cost);
1006            return Ok(path.with_visited(visited_count));
1007        }
1008
1009        for &edge_id in graph.outgoing_edges(node) {
1010            if let Some(edge) = graph.get_edge(edge_id) {
1011                let next_node = edge.target;
1012
1013                // Get time-dependent multiplier
1014                let multiplier = td_weights
1015                    .get(&edge_id)
1016                    .map(|tdw| tdw.multiplier_at(current_time))
1017                    .unwrap_or(1.0);
1018
1019                let travel_cost = edge.weight * multiplier;
1020                let arrival_time = current_time + travel_cost;
1021
1022                let is_better = arrival_times
1023                    .get(&next_node)
1024                    .is_none_or(|&current| arrival_time < current);
1025
1026                if is_better {
1027                    arrival_times.insert(next_node, arrival_time);
1028                    predecessors.insert(next_node, (node, edge_id));
1029                    heap.push(DijkstraState {
1030                        cost: arrival_time,
1031                        node: next_node,
1032                    });
1033                }
1034            }
1035        }
1036    }
1037
1038    Ok(ShortestPath::not_found())
1039}
1040
1041/// All-pairs shortest paths result
1042#[derive(Debug, Clone)]
1043pub struct AllPairsResult {
1044    /// Distance matrix: dist\[i\]\[j\] = shortest distance from node i to node j
1045    pub distances: HashMap<NodeId, HashMap<NodeId, f64>>,
1046    /// Next-hop matrix for path reconstruction: next\[i\]\[j\] = next node on shortest path from i to j
1047    pub next_hop: HashMap<NodeId, HashMap<NodeId, Option<NodeId>>>,
1048    /// Node ordering for matrix access
1049    pub node_order: Vec<NodeId>,
1050}
1051
1052impl AllPairsResult {
1053    /// Get the shortest distance between two nodes
1054    pub fn distance(&self, from: NodeId, to: NodeId) -> f64 {
1055        self.distances
1056            .get(&from)
1057            .and_then(|row| row.get(&to))
1058            .copied()
1059            .unwrap_or(f64::INFINITY)
1060    }
1061
1062    /// Reconstruct the shortest path between two nodes
1063    pub fn path(&self, from: NodeId, to: NodeId) -> Option<Vec<NodeId>> {
1064        if self.distance(from, to).is_infinite() {
1065            return None;
1066        }
1067
1068        let mut path = vec![from];
1069        let mut current = from;
1070
1071        while current != to {
1072            let next = self
1073                .next_hop
1074                .get(&current)
1075                .and_then(|row| row.get(&to))
1076                .copied()
1077                .flatten();
1078
1079            match next {
1080                Some(n) => {
1081                    if path.contains(&n) {
1082                        return None; // Cycle detected
1083                    }
1084                    path.push(n);
1085                    current = n;
1086                }
1087                None => return None,
1088            }
1089        }
1090
1091        Some(path)
1092    }
1093}
1094
1095/// Floyd-Warshall all-pairs shortest paths
1096///
1097/// Computes shortest paths between all pairs of nodes.
1098/// Time complexity: O(V^3), so only suitable for small to medium graphs.
1099///
1100/// # Arguments
1101///
1102/// * `graph` - The network graph
1103/// * `max_nodes` - Maximum number of nodes (returns error if exceeded)
1104///
1105/// # Returns
1106///
1107/// All-pairs shortest path result with distance matrix and path reconstruction
1108pub fn floyd_warshall(graph: &Graph, max_nodes: usize) -> Result<AllPairsResult> {
1109    let n = graph.num_nodes();
1110    if n > max_nodes {
1111        return Err(AlgorithmError::InvalidParameter {
1112            parameter: "max_nodes",
1113            message: format!(
1114                "Graph has {} nodes, exceeds limit of {} for Floyd-Warshall",
1115                n, max_nodes
1116            ),
1117        });
1118    }
1119
1120    let node_order: Vec<NodeId> = graph.sorted_node_ids();
1121    let node_index: HashMap<NodeId, usize> = node_order
1122        .iter()
1123        .enumerate()
1124        .map(|(i, &id)| (id, i))
1125        .collect();
1126
1127    // Initialize distance and next-hop matrices
1128    let mut dist: Vec<Vec<f64>> = vec![vec![f64::INFINITY; n]; n];
1129    let mut next: Vec<Vec<Option<usize>>> = vec![vec![None; n]; n];
1130
1131    // Self distances are 0
1132    for i in 0..n {
1133        dist[i][i] = 0.0;
1134        next[i][i] = Some(i);
1135    }
1136
1137    // Initialize from edges
1138    for edge in graph.edges_iter().map(|(_, e)| e) {
1139        let src_idx = node_index.get(&edge.source).copied();
1140        let tgt_idx = node_index.get(&edge.target).copied();
1141
1142        if let (Some(i), Some(j)) = (src_idx, tgt_idx) {
1143            if edge.weight < dist[i][j] {
1144                dist[i][j] = edge.weight;
1145                next[i][j] = Some(j);
1146            }
1147        }
1148    }
1149
1150    // Floyd-Warshall main loop
1151    for k in 0..n {
1152        for i in 0..n {
1153            if dist[i][k] == f64::INFINITY {
1154                continue;
1155            }
1156            for j in 0..n {
1157                if dist[k][j] == f64::INFINITY {
1158                    continue;
1159                }
1160                let new_dist = dist[i][k] + dist[k][j];
1161                if new_dist < dist[i][j] {
1162                    dist[i][j] = new_dist;
1163                    next[i][j] = next[i][k];
1164                }
1165            }
1166        }
1167    }
1168
1169    // Convert to HashMap-based result
1170    let mut distances: HashMap<NodeId, HashMap<NodeId, f64>> = HashMap::new();
1171    let mut next_hop: HashMap<NodeId, HashMap<NodeId, Option<NodeId>>> = HashMap::new();
1172
1173    for i in 0..n {
1174        let from_id = node_order[i];
1175        let mut dist_row = HashMap::new();
1176        let mut next_row = HashMap::new();
1177
1178        for j in 0..n {
1179            let to_id = node_order[j];
1180            dist_row.insert(to_id, dist[i][j]);
1181            next_row.insert(to_id, next[i][j].map(|idx| node_order[idx]));
1182        }
1183
1184        distances.insert(from_id, dist_row);
1185        next_hop.insert(from_id, next_row);
1186    }
1187
1188    Ok(AllPairsResult {
1189        distances,
1190        next_hop,
1191        node_order,
1192    })
1193}
1194
1195/// Single-source shortest paths (Dijkstra from one source to all reachable nodes)
1196pub fn dijkstra_single_source(
1197    graph: &Graph,
1198    source: NodeId,
1199    options: &ShortestPathOptions,
1200) -> Result<HashMap<NodeId, (f64, Vec<NodeId>)>> {
1201    if graph.get_node(source).is_none() {
1202        return Err(AlgorithmError::InvalidGeometry(format!(
1203            "Source node {:?} not found",
1204            source
1205        )));
1206    }
1207
1208    let mut distances: HashMap<NodeId, f64> = HashMap::new();
1209    let mut predecessors: HashMap<NodeId, NodeId> = HashMap::new();
1210    let mut heap = BinaryHeap::new();
1211
1212    distances.insert(source, 0.0);
1213    heap.push(DijkstraState {
1214        cost: 0.0,
1215        node: source,
1216    });
1217
1218    while let Some(DijkstraState { cost, node }) = heap.pop() {
1219        if let Some(&best) = distances.get(&node) {
1220            if cost > best {
1221                continue;
1222            }
1223        }
1224
1225        if let Some(max_len) = options.max_length {
1226            if cost > max_len {
1227                continue;
1228            }
1229        }
1230
1231        for &edge_id in graph.outgoing_edges(node) {
1232            if let Some(edge) = graph.get_edge(edge_id) {
1233                let next_node = edge.target;
1234                let edge_cost = get_edge_cost(edge, options);
1235                let next_cost = cost + edge_cost;
1236
1237                let is_better = distances
1238                    .get(&next_node)
1239                    .is_none_or(|&current| next_cost < current);
1240
1241                if is_better {
1242                    distances.insert(next_node, next_cost);
1243                    predecessors.insert(next_node, node);
1244                    heap.push(DijkstraState {
1245                        cost: next_cost,
1246                        node: next_node,
1247                    });
1248                }
1249            }
1250        }
1251    }
1252
1253    // Build result with paths
1254    let mut result = HashMap::new();
1255    for (&node, &dist) in &distances {
1256        let mut path = vec![node];
1257        let mut current = node;
1258        while current != source {
1259            if let Some(&prev) = predecessors.get(&current) {
1260                path.push(prev);
1261                current = prev;
1262            } else {
1263                break;
1264            }
1265        }
1266        path.reverse();
1267        result.insert(node, (dist, path));
1268    }
1269
1270    Ok(result)
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275    use super::*;
1276    use crate::vector::network::TurnPenalties;
1277    use oxigdal_core::vector::Coordinate;
1278
1279    fn create_test_graph() -> Graph {
1280        let mut graph = Graph::new();
1281
1282        // Create a simple graph:
1283        //   0 -- 1 -- 2
1284        //   |         |
1285        //   3 ------- 4
1286        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1287        let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1288        let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
1289        let n3 = graph.add_node(Coordinate::new_2d(0.0, 1.0));
1290        let n4 = graph.add_node(Coordinate::new_2d(2.0, 1.0));
1291
1292        let _ = graph.add_edge(n0, n1, 1.0);
1293        let _ = graph.add_edge(n1, n2, 1.0);
1294        let _ = graph.add_edge(n0, n3, 1.0);
1295        let _ = graph.add_edge(n3, n4, 3.0);
1296        let _ = graph.add_edge(n2, n4, 1.0);
1297
1298        graph
1299    }
1300
1301    #[test]
1302    fn test_dijkstra_simple_path() {
1303        let graph = create_test_graph();
1304        let nodes = graph.sorted_node_ids();
1305        let start = nodes[0];
1306        let end = nodes[2];
1307
1308        let path = dijkstra_search(&graph, start, end, &ShortestPathOptions::default());
1309        assert!(path.is_ok());
1310
1311        let shortest = path.expect("Failed to find path");
1312        assert!(shortest.found);
1313        assert_eq!(shortest.cost, 2.0); // 0 -> 1 -> 2
1314    }
1315
1316    #[test]
1317    fn test_dijkstra_no_path() {
1318        let mut graph = Graph::new();
1319        let n1 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1320        let n2 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1321        // No edge between n1 and n2
1322
1323        let path = dijkstra_search(&graph, n1, n2, &ShortestPathOptions::default());
1324        assert!(path.is_ok());
1325
1326        let shortest = path.expect("Failed to search");
1327        assert!(!shortest.found);
1328    }
1329
1330    #[test]
1331    fn test_astar_search() {
1332        let graph = create_test_graph();
1333        let nodes = graph.sorted_node_ids();
1334        let start = nodes[0];
1335        let end = nodes[4];
1336
1337        let path = astar_search(&graph, start, end, &ShortestPathOptions::default());
1338        assert!(path.is_ok());
1339
1340        let shortest = path.expect("Failed to find path");
1341        assert!(shortest.found);
1342    }
1343
1344    #[test]
1345    fn test_bidirectional_search() {
1346        let graph = create_test_graph();
1347        let nodes = graph.sorted_node_ids();
1348        let start = nodes[0];
1349        let end = nodes[4];
1350
1351        let path = bidirectional_search(&graph, start, end, &ShortestPathOptions::default());
1352        assert!(path.is_ok());
1353
1354        let shortest = path.expect("Failed to find path");
1355        assert!(shortest.found);
1356    }
1357
1358    #[test]
1359    fn test_max_length_constraint() {
1360        let graph = create_test_graph();
1361        let nodes = graph.sorted_node_ids();
1362        let start = nodes[0];
1363        let end = nodes[4];
1364
1365        let options = ShortestPathOptions {
1366            max_length: Some(2.0), // Too short to reach end
1367            ..Default::default()
1368        };
1369
1370        let path = dijkstra_search(&graph, start, end, &options);
1371        assert!(path.is_ok());
1372
1373        let shortest = path.expect("Failed to search");
1374        assert!(!shortest.found);
1375    }
1376
1377    #[test]
1378    fn test_turn_restricted_dijkstra() {
1379        let mut graph = Graph::new();
1380
1381        // Create a graph where turn restriction forces a longer path
1382        //  n0 --e0--> n1 --e1--> n2
1383        //              |
1384        //             e2
1385        //              v
1386        //             n3
1387
1388        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1389        let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1390        let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
1391        let n3 = graph.add_node(Coordinate::new_2d(1.0, 1.0));
1392
1393        let e0 = graph.add_edge(n0, n1, 1.0).expect("edge");
1394        let e1 = graph.add_edge(n1, n2, 1.0).expect("edge");
1395        let e2 = graph.add_edge(n1, n3, 1.0).expect("edge");
1396        let _ = graph.add_edge(n3, n2, 2.0).expect("edge");
1397
1398        // Without turn restriction: n0 -> n1 -> n2 (cost 2)
1399        let path = dijkstra_search(&graph, n0, n2, &ShortestPathOptions::default());
1400        assert!(path.is_ok());
1401        let shortest = path.expect("path");
1402        assert_eq!(shortest.cost, 2.0);
1403
1404        // Prohibit turn from e0 to e1 at n1 (no left turn)
1405        let mut tp = TurnPenalties::new();
1406        tp.add_prohibition(n1, e0, e1);
1407
1408        let options = ShortestPathOptions {
1409            turn_penalties: Some(tp),
1410            ..Default::default()
1411        };
1412
1413        // With turn restriction: n0 -> n1 -> n3 -> n2 (cost 4)
1414        let path = dijkstra_turn_restricted(&graph, n0, n2, &options);
1415        assert!(path.is_ok());
1416        let shortest = path.expect("path");
1417        assert!(shortest.found);
1418        assert_eq!(shortest.cost, 4.0);
1419    }
1420
1421    #[test]
1422    fn test_turn_penalty() {
1423        let mut graph = Graph::new();
1424
1425        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1426        let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1427        let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
1428
1429        let e0 = graph.add_edge(n0, n1, 1.0).expect("edge");
1430        let e1 = graph.add_edge(n1, n2, 1.0).expect("edge");
1431
1432        let mut tp = TurnPenalties::new();
1433        tp.add_penalty(n1, e0, e1, 5.0);
1434
1435        let options = ShortestPathOptions {
1436            turn_penalties: Some(tp),
1437            ..Default::default()
1438        };
1439
1440        let path = dijkstra_turn_restricted(&graph, n0, n2, &options);
1441        assert!(path.is_ok());
1442        let shortest = path.expect("path");
1443        assert!(shortest.found);
1444        // Cost: edge(0->1) + turn_penalty + edge(1->2) = 1 + 5 + 1 = 7
1445        assert!((shortest.cost - 7.0).abs() < 1e-10);
1446    }
1447
1448    #[test]
1449    fn test_time_dependent_dijkstra() {
1450        let mut graph = Graph::new();
1451        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1452        let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1453
1454        let e0 = graph.add_edge(n0, n1, 100.0).expect("edge"); // base cost 100
1455
1456        // Rush hour: multiplier 2.0 during 7-9 AM
1457        let mut td_weights = HashMap::new();
1458        td_weights.insert(
1459            e0,
1460            TimeDependentWeight::new(vec![
1461                (0.0, 1.0),
1462                (25200.0, 2.0), // 7:00 AM
1463                (32400.0, 1.0), // 9:00 AM
1464            ]),
1465        );
1466
1467        // Departure at 8:00 AM (28800s) -> rush hour multiplier 2.0
1468        let options = ShortestPathOptions {
1469            time_dependent_weights: Some(td_weights.clone()),
1470            departure_time: 28800.0,
1471            ..Default::default()
1472        };
1473
1474        let path = time_dependent_dijkstra(&graph, n0, n1, &options);
1475        assert!(path.is_ok());
1476        let shortest = path.expect("path");
1477        assert!(shortest.found);
1478        assert!((shortest.cost - 200.0).abs() < 1e-10); // 100 * 2.0
1479
1480        // Departure at noon -> normal multiplier 1.0
1481        let options2 = ShortestPathOptions {
1482            time_dependent_weights: Some(td_weights),
1483            departure_time: 43200.0,
1484            ..Default::default()
1485        };
1486
1487        let path2 = time_dependent_dijkstra(&graph, n0, n1, &options2);
1488        assert!(path2.is_ok());
1489        let shortest2 = path2.expect("path");
1490        assert!((shortest2.cost - 100.0).abs() < 1e-10); // 100 * 1.0
1491    }
1492
1493    #[test]
1494    fn test_floyd_warshall() {
1495        let mut graph = Graph::new();
1496        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1497        let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1498        let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
1499
1500        let _ = graph.add_edge(n0, n1, 1.0);
1501        let _ = graph.add_edge(n1, n2, 2.0);
1502        let _ = graph.add_edge(n0, n2, 5.0);
1503
1504        let result = floyd_warshall(&graph, 100);
1505        assert!(result.is_ok());
1506
1507        let apsp = result.expect("Floyd-Warshall failed");
1508        assert!((apsp.distance(n0, n2) - 3.0).abs() < 1e-10); // via n1: 1+2=3 < 5
1509        assert!((apsp.distance(n0, n1) - 1.0).abs() < 1e-10);
1510    }
1511
1512    #[test]
1513    fn test_floyd_warshall_path_reconstruction() {
1514        let mut graph = Graph::new();
1515        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1516        let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1517        let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
1518
1519        let _ = graph.add_edge(n0, n1, 1.0);
1520        let _ = graph.add_edge(n1, n2, 2.0);
1521        let _ = graph.add_edge(n0, n2, 5.0);
1522
1523        let apsp = floyd_warshall(&graph, 100).expect("Floyd-Warshall failed");
1524
1525        let path = apsp.path(n0, n2);
1526        assert!(path.is_some());
1527        let path = path.expect("path");
1528        assert_eq!(path, vec![n0, n1, n2]);
1529    }
1530
1531    #[test]
1532    fn test_floyd_warshall_too_large() {
1533        let mut graph = Graph::new();
1534        for i in 0..20 {
1535            graph.add_node(Coordinate::new_2d(i as f64, 0.0));
1536        }
1537
1538        let result = floyd_warshall(&graph, 10);
1539        assert!(result.is_err());
1540    }
1541
1542    #[test]
1543    fn test_single_source_dijkstra() {
1544        let graph = create_test_graph();
1545        let nodes = graph.sorted_node_ids();
1546        let start = nodes[0];
1547
1548        let result = dijkstra_single_source(&graph, start, &ShortestPathOptions::default());
1549        assert!(result.is_ok());
1550
1551        let distances = result.expect("Failed single-source Dijkstra");
1552        // Start node has distance 0
1553        assert!((distances.get(&start).expect("start").0 - 0.0).abs() < 1e-10);
1554    }
1555
1556    #[test]
1557    fn test_astar_turn_restricted() {
1558        let mut graph = Graph::new();
1559        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1560        let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1561        let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
1562        let n3 = graph.add_node(Coordinate::new_2d(1.0, 1.0));
1563
1564        let e0 = graph.add_edge(n0, n1, 1.0).expect("edge");
1565        let e1 = graph.add_edge(n1, n2, 1.0).expect("edge");
1566        let _ = graph.add_edge(n1, n3, 1.0).expect("edge");
1567        let _ = graph.add_edge(n3, n2, 2.0).expect("edge");
1568
1569        // Prohibit e0 -> e1 at n1
1570        let mut tp = TurnPenalties::new();
1571        tp.add_prohibition(n1, e0, e1);
1572
1573        let options = ShortestPathOptions {
1574            turn_penalties: Some(tp),
1575            ..Default::default()
1576        };
1577
1578        let path = astar_turn_restricted(&graph, n0, n2, &options);
1579        assert!(path.is_ok());
1580        let shortest = path.expect("path");
1581        assert!(shortest.found);
1582        // Must go n0->n1->n3->n2 = 1+1+2 = 4
1583        assert!((shortest.cost - 4.0).abs() < 1e-10);
1584    }
1585
1586    #[test]
1587    fn test_nodes_visited_count() {
1588        let graph = create_test_graph();
1589        let nodes = graph.sorted_node_ids();
1590        let start = nodes[0];
1591        let end = nodes[4];
1592
1593        let path = dijkstra_search(&graph, start, end, &ShortestPathOptions::default());
1594        let shortest = path.expect("path");
1595        assert!(shortest.nodes_visited > 0);
1596    }
1597
1598    #[test]
1599    fn test_bidirectional_same_node() {
1600        let mut graph = Graph::new();
1601        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1602
1603        let path = bidirectional_search(&graph, n0, n0, &ShortestPathOptions::default());
1604        assert!(path.is_ok());
1605        let shortest = path.expect("path");
1606        assert!(shortest.found);
1607        assert_eq!(shortest.cost, 0.0);
1608    }
1609}