Skip to main content

oxigdal_algorithms/vector/network/
routing.rs

1//! Advanced routing with constraints, turn restrictions, and multi-criteria optimization
2//!
3//! This module provides:
4//!
5//! - **Multi-criteria routing**: Optimize for distance, time, cost, or custom weights
6//! - **Alternative routes**: Generate multiple distinct route options
7//! - **Route optimization**: Optimize waypoint ordering (TSP approximation)
8//! - **Turn penalties**: Apply costs for turns at intersections
9//! - **Via-point routing**: Route through mandatory intermediate points
10
11use crate::error::{AlgorithmError, Result};
12use crate::vector::network::{
13    EdgeId, EdgeWeight, Graph, NodeId, ShortestPath, ShortestPathOptions, TurnPenalties,
14    astar_search, dijkstra_search, dijkstra_turn_restricted,
15};
16use oxigdal_core::vector::Coordinate;
17use std::collections::{HashMap, HashSet};
18
19/// Routing algorithm variant
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum RoutingAlgorithm {
22    /// Fastest route (minimum travel time)
23    Fastest,
24    /// Shortest route (minimum distance)
25    Shortest,
26    /// Most economical (minimum cost)
27    Economical,
28    /// Multi-criteria (weighted combination)
29    MultiCriteria,
30}
31
32/// Turn restriction type
33#[derive(Debug, Clone)]
34pub struct TurnRestriction {
35    /// Node where turn restriction applies
36    pub node: NodeId,
37    /// Incoming edge
38    pub from_edge: EdgeId,
39    /// Outgoing edge
40    pub to_edge: EdgeId,
41    /// Whether this turn is prohibited
42    pub prohibited: bool,
43}
44
45/// Multi-criteria weight specification
46#[derive(Debug, Clone)]
47pub struct RoutingCriteria {
48    /// Weight for distance (0.0 to 1.0)
49    pub distance_weight: f64,
50    /// Weight for travel time (0.0 to 1.0)
51    pub time_weight: f64,
52    /// Weight for monetary cost (0.0 to 1.0)
53    pub monetary_weight: f64,
54    /// Custom weights
55    pub custom_weights: HashMap<String, f64>,
56}
57
58impl Default for RoutingCriteria {
59    fn default() -> Self {
60        Self {
61            distance_weight: 1.0,
62            time_weight: 0.0,
63            monetary_weight: 0.0,
64            custom_weights: HashMap::new(),
65        }
66    }
67}
68
69impl RoutingCriteria {
70    /// Create criteria for shortest distance
71    pub fn shortest() -> Self {
72        Self {
73            distance_weight: 1.0,
74            time_weight: 0.0,
75            monetary_weight: 0.0,
76            custom_weights: HashMap::new(),
77        }
78    }
79
80    /// Create criteria for fastest time
81    pub fn fastest() -> Self {
82        Self {
83            distance_weight: 0.0,
84            time_weight: 1.0,
85            monetary_weight: 0.0,
86            custom_weights: HashMap::new(),
87        }
88    }
89
90    /// Create criteria for cheapest route
91    pub fn cheapest() -> Self {
92        Self {
93            distance_weight: 0.0,
94            time_weight: 0.0,
95            monetary_weight: 1.0,
96            custom_weights: HashMap::new(),
97        }
98    }
99
100    /// Create balanced criteria
101    pub fn balanced() -> Self {
102        Self {
103            distance_weight: 0.4,
104            time_weight: 0.4,
105            monetary_weight: 0.2,
106            custom_weights: HashMap::new(),
107        }
108    }
109
110    /// Convert to weight criteria map for ShortestPathOptions
111    pub fn to_weight_criteria(&self) -> HashMap<String, f64> {
112        let mut map = HashMap::new();
113        if self.distance_weight > 0.0 {
114            map.insert("distance".to_string(), self.distance_weight);
115        }
116        if self.time_weight > 0.0 {
117            map.insert("time".to_string(), self.time_weight);
118        }
119        if self.monetary_weight > 0.0 {
120            map.insert("monetary".to_string(), self.monetary_weight);
121        }
122        for (k, v) in &self.custom_weights {
123            if *v > 0.0 {
124                map.insert(k.clone(), *v);
125            }
126        }
127        map
128    }
129}
130
131/// Routing options
132#[derive(Debug, Clone)]
133pub struct RouteOptions {
134    /// Routing algorithm
135    pub algorithm: RoutingAlgorithm,
136    /// Turn restrictions
137    pub turn_restrictions: Vec<TurnRestriction>,
138    /// Avoid toll roads
139    pub avoid_tolls: bool,
140    /// Avoid highways
141    pub avoid_highways: bool,
142    /// Maximum detour factor (e.g., 1.2 = 20% longer than straight line)
143    pub max_detour_factor: Option<f64>,
144    /// Multi-criteria routing weights
145    pub criteria: RoutingCriteria,
146    /// Turn penalties
147    pub turn_penalties: Option<TurnPenalties>,
148    /// Number of alternative routes to generate
149    pub alternatives: usize,
150    /// Alternative route overlap threshold (0.0 to 1.0, lower = more distinct)
151    pub alternative_overlap_threshold: f64,
152    /// Via points (mandatory intermediate stops, in order)
153    pub via_points: Vec<NodeId>,
154}
155
156impl Default for RouteOptions {
157    fn default() -> Self {
158        Self {
159            algorithm: RoutingAlgorithm::Shortest,
160            turn_restrictions: Vec::new(),
161            avoid_tolls: false,
162            avoid_highways: false,
163            max_detour_factor: None,
164            criteria: RoutingCriteria::default(),
165            turn_penalties: None,
166            alternatives: 0,
167            alternative_overlap_threshold: 0.6,
168            via_points: Vec::new(),
169        }
170    }
171}
172
173/// A route segment
174#[derive(Debug, Clone)]
175pub struct RouteSegment {
176    /// Edge ID
177    pub edge: EdgeId,
178    /// Distance in this segment
179    pub distance: f64,
180    /// Travel time in this segment (seconds)
181    pub travel_time: f64,
182    /// Monetary cost for this segment
183    pub monetary_cost: f64,
184    /// Instructions (e.g., "Turn left on Main St")
185    pub instruction: Option<String>,
186    /// Turn angle at the end of this segment (degrees, 0=straight, positive=left, negative=right)
187    pub turn_angle: Option<f64>,
188}
189
190/// A complete route
191#[derive(Debug, Clone)]
192pub struct Route {
193    /// Sequence of segments
194    pub segments: Vec<RouteSegment>,
195    /// Total distance (meters)
196    pub total_distance: f64,
197    /// Total travel time (seconds)
198    pub total_time: f64,
199    /// Total monetary cost
200    pub total_monetary_cost: f64,
201    /// Sequence of node IDs
202    pub node_sequence: Vec<NodeId>,
203    /// Sequence of coordinates
204    pub coordinates: Vec<Coordinate>,
205    /// Whether this is the primary route
206    pub is_primary: bool,
207    /// Route quality score (lower is better for the given criteria)
208    pub quality_score: f64,
209}
210
211impl Route {
212    /// Create a new empty route
213    pub fn new() -> Self {
214        Self {
215            segments: Vec::new(),
216            total_distance: 0.0,
217            total_time: 0.0,
218            total_monetary_cost: 0.0,
219            node_sequence: Vec::new(),
220            coordinates: Vec::new(),
221            is_primary: true,
222            quality_score: 0.0,
223        }
224    }
225
226    /// Get the edge set for overlap comparison
227    fn edge_set(&self) -> HashSet<EdgeId> {
228        self.segments.iter().map(|s| s.edge).collect()
229    }
230
231    /// Calculate overlap ratio with another route
232    pub fn overlap_ratio(&self, other: &Route) -> f64 {
233        let self_edges = self.edge_set();
234        let other_edges = other.edge_set();
235
236        if self_edges.is_empty() || other_edges.is_empty() {
237            return 0.0;
238        }
239
240        let intersection = self_edges.intersection(&other_edges).count();
241        let min_size = self_edges.len().min(other_edges.len());
242
243        if min_size == 0 {
244            return 0.0;
245        }
246
247        intersection as f64 / min_size as f64
248    }
249}
250
251impl Default for Route {
252    fn default() -> Self {
253        Self::new()
254    }
255}
256
257/// Result of multi-route calculation
258#[derive(Debug, Clone)]
259pub struct RouteResult {
260    /// Primary (best) route
261    pub primary: Route,
262    /// Alternative routes (if requested)
263    pub alternatives: Vec<Route>,
264}
265
266/// Calculate a route between two points
267pub fn calculate_route(
268    graph: &Graph,
269    start: NodeId,
270    end: NodeId,
271    options: &RouteOptions,
272) -> Result<Route> {
273    // Build path options from route options
274    let path_options = build_path_options(options);
275
276    // Handle via-points routing
277    if !options.via_points.is_empty() {
278        return calculate_via_route(graph, start, end, options);
279    }
280
281    // Find shortest path based on algorithm selection
282    let shortest = if options.turn_penalties.is_some() || !options.turn_restrictions.is_empty() {
283        let mut tp = options.turn_penalties.clone().unwrap_or_default();
284        // Add turn restrictions to penalties
285        for restriction in &options.turn_restrictions {
286            if restriction.prohibited {
287                tp.add_prohibition(restriction.node, restriction.from_edge, restriction.to_edge);
288            }
289        }
290        let mut opts = path_options.clone();
291        opts.turn_penalties = Some(tp);
292        dijkstra_turn_restricted(graph, start, end, &opts)?
293    } else {
294        match options.algorithm {
295            RoutingAlgorithm::Fastest | RoutingAlgorithm::MultiCriteria => {
296                astar_search(graph, start, end, &path_options)?
297            }
298            _ => dijkstra_search(graph, start, end, &path_options)?,
299        }
300    };
301
302    if !shortest.found {
303        return Err(AlgorithmError::PathNotFound(format!(
304            "No route found from {:?} to {:?}",
305            start, end
306        )));
307    }
308
309    build_route_from_path(graph, &shortest, true)
310}
311
312/// Calculate route through via-points
313fn calculate_via_route(
314    graph: &Graph,
315    start: NodeId,
316    end: NodeId,
317    options: &RouteOptions,
318) -> Result<Route> {
319    let mut full_route = Route::new();
320    let path_options = build_path_options(options);
321
322    // Build sequence: start -> via1 -> via2 -> ... -> end
323    let mut waypoints = vec![start];
324    waypoints.extend_from_slice(&options.via_points);
325    waypoints.push(end);
326
327    for window in waypoints.windows(2) {
328        let from = window[0];
329        let to = window[1];
330
331        let shortest = dijkstra_search(graph, from, to, &path_options)?;
332
333        if !shortest.found {
334            return Err(AlgorithmError::PathNotFound(format!(
335                "No route found from {:?} to {:?} (via-point segment)",
336                from, to
337            )));
338        }
339
340        let segment_route = build_route_from_path(graph, &shortest, false)?;
341
342        // Merge into full route (avoid duplicating the connecting node)
343        if full_route.node_sequence.is_empty() {
344            full_route
345                .node_sequence
346                .extend(&segment_route.node_sequence);
347        } else {
348            // Skip first node of segment (it's the last of the previous)
349            full_route
350                .node_sequence
351                .extend(&segment_route.node_sequence[1..]);
352        }
353
354        full_route.segments.extend(segment_route.segments);
355        full_route.total_distance += segment_route.total_distance;
356        full_route.total_time += segment_route.total_time;
357        full_route.total_monetary_cost += segment_route.total_monetary_cost;
358        full_route.coordinates.extend(segment_route.coordinates);
359    }
360
361    full_route.is_primary = true;
362    full_route.quality_score = full_route.total_distance;
363
364    Ok(full_route)
365}
366
367/// Build ShortestPathOptions from RouteOptions
368fn build_path_options(options: &RouteOptions) -> ShortestPathOptions {
369    let weight_criteria = match options.algorithm {
370        RoutingAlgorithm::Fastest => Some(RoutingCriteria::fastest().to_weight_criteria()),
371        RoutingAlgorithm::Shortest => Some(RoutingCriteria::shortest().to_weight_criteria()),
372        RoutingAlgorithm::Economical => Some(RoutingCriteria::cheapest().to_weight_criteria()),
373        RoutingAlgorithm::MultiCriteria => Some(options.criteria.to_weight_criteria()),
374    };
375
376    ShortestPathOptions {
377        include_geometry: true,
378        weight_criteria,
379        turn_penalties: options.turn_penalties.clone(),
380        ..Default::default()
381    }
382}
383
384/// Build a Route from a ShortestPath result
385fn build_route_from_path(graph: &Graph, path: &ShortestPath, is_primary: bool) -> Result<Route> {
386    let mut route = Route::new();
387    let mut coordinates = Vec::new();
388
389    for (i, &edge_id) in path.edges.iter().enumerate() {
390        if let Some(edge) = graph.get_edge(edge_id) {
391            let distance = edge.multi_weight.distance;
392            let time_cost = edge.multi_weight.time;
393            let monetary = edge.multi_weight.monetary;
394
395            let travel_time = if time_cost > 0.0 {
396                time_cost
397            } else {
398                edge.travel_time()
399                    .map(|t| t * 3600.0) // hours to seconds
400                    .unwrap_or(distance / 13.89) // Default ~50 km/h
401            };
402
403            // Calculate turn angle between consecutive edges
404            let turn_angle = if i > 0 {
405                calculate_turn_angle(graph, path.edges[i - 1], edge_id)
406            } else {
407                None
408            };
409
410            let instruction = turn_angle.map(|angle| generate_turn_instruction(angle));
411
412            let segment = RouteSegment {
413                edge: edge_id,
414                distance,
415                travel_time,
416                monetary_cost: monetary,
417                instruction,
418                turn_angle,
419            };
420
421            route.segments.push(segment);
422            route.total_distance += distance;
423            route.total_time += travel_time;
424            route.total_monetary_cost += monetary;
425
426            // Add geometry if available
427            if let Some(geom) = &edge.geometry {
428                coordinates.extend(geom.coords.iter().copied());
429            }
430        }
431    }
432
433    route.node_sequence = path.nodes.clone();
434    route.coordinates = coordinates;
435    route.is_primary = is_primary;
436    route.quality_score = path.cost;
437
438    Ok(route)
439}
440
441/// Calculate the turn angle between two consecutive edges
442fn calculate_turn_angle(graph: &Graph, edge1_id: EdgeId, edge2_id: EdgeId) -> Option<f64> {
443    let edge1 = graph.get_edge(edge1_id)?;
444    let edge2 = graph.get_edge(edge2_id)?;
445
446    let src1 = graph.get_node(edge1.source)?;
447    let via = graph.get_node(edge1.target)?;
448    let dst2 = graph.get_node(edge2.target)?;
449
450    // Vector from src1 to via
451    let v1x = via.coordinate.x - src1.coordinate.x;
452    let v1y = via.coordinate.y - src1.coordinate.y;
453
454    // Vector from via to dst2
455    let v2x = dst2.coordinate.x - via.coordinate.x;
456    let v2y = dst2.coordinate.y - via.coordinate.y;
457
458    // Cross product (sin of angle)
459    let cross = v1x * v2y - v1y * v2x;
460    // Dot product (cos of angle)
461    let dot = v1x * v2x + v1y * v2y;
462
463    // Angle in degrees
464    let angle = cross.atan2(dot).to_degrees();
465
466    Some(angle)
467}
468
469/// Generate a human-readable turn instruction from angle
470fn generate_turn_instruction(angle: f64) -> String {
471    if angle.abs() < 15.0 {
472        "Continue straight".to_string()
473    } else if (15.0..60.0).contains(&angle) {
474        "Bear left".to_string()
475    } else if (60.0..120.0).contains(&angle) {
476        "Turn left".to_string()
477    } else if angle >= 120.0 {
478        "Sharp left".to_string()
479    } else if angle <= -15.0 && angle > -60.0 {
480        "Bear right".to_string()
481    } else if angle <= -60.0 && angle > -120.0 {
482        "Turn right".to_string()
483    } else {
484        "Sharp right".to_string()
485    }
486}
487
488/// Calculate route with alternative routes
489pub fn calculate_route_with_alternatives(
490    graph: &Graph,
491    start: NodeId,
492    end: NodeId,
493    options: &RouteOptions,
494) -> Result<RouteResult> {
495    // Calculate primary route
496    let primary = calculate_route(graph, start, end, options)?;
497
498    let mut alternatives = Vec::new();
499    let num_alternatives = options.alternatives;
500
501    if num_alternatives == 0 {
502        return Ok(RouteResult {
503            primary,
504            alternatives,
505        });
506    }
507
508    // Generate alternatives using penalty method
509    // For each alternative, penalize edges used in previous routes
510    let mut penalized_edges: HashMap<EdgeId, f64> = HashMap::new();
511
512    // Penalize edges from the primary route
513    for segment in &primary.segments {
514        let current = penalized_edges.entry(segment.edge).or_insert(1.0);
515        *current *= 2.0; // Double the effective cost
516    }
517
518    let path_options = build_path_options(options);
519
520    for alt_idx in 0..num_alternatives {
521        // Create a modified graph with penalized edge weights
522        let mut alt_graph = graph.clone();
523
524        for (&edge_id, &penalty_multiplier) in &penalized_edges {
525            if let Some(edge) = alt_graph.get_edge_mut(edge_id) {
526                edge.weight *= penalty_multiplier;
527                edge.multi_weight.distance *= penalty_multiplier;
528                edge.multi_weight.time *= penalty_multiplier;
529            }
530        }
531
532        let shortest = dijkstra_search(&alt_graph, start, end, &path_options)?;
533
534        if !shortest.found {
535            continue;
536        }
537
538        // Build route using original graph weights for accurate costs
539        let alt_route_result = build_alternative_route(graph, &shortest);
540        let alt_route = match alt_route_result {
541            Ok(r) => r,
542            Err(_) => continue,
543        };
544
545        // Check overlap with existing routes
546        let overlap_with_primary = alt_route.overlap_ratio(&primary);
547        let overlap_with_others = alternatives
548            .iter()
549            .map(|other: &Route| alt_route.overlap_ratio(other))
550            .fold(0.0f64, |a, b| a.max(b));
551
552        let max_overlap = overlap_with_primary.max(overlap_with_others);
553
554        if max_overlap < options.alternative_overlap_threshold {
555            // Add penalties for this route's edges too
556            for segment in &alt_route.segments {
557                let current = penalized_edges.entry(segment.edge).or_insert(1.0);
558                *current *= 1.5;
559            }
560
561            alternatives.push(alt_route);
562        } else {
563            // Increase penalties more aggressively
564            for segment in &alt_route.segments {
565                let current = penalized_edges.entry(segment.edge).or_insert(1.0);
566                *current *= 3.0;
567            }
568        }
569
570        if alternatives.len() >= num_alternatives {
571            break;
572        }
573
574        // Safety: prevent infinite attempts
575        if alt_idx > num_alternatives * 5 {
576            break;
577        }
578    }
579
580    // Sort alternatives by quality score
581    alternatives.sort_by(|a, b| {
582        a.quality_score
583            .partial_cmp(&b.quality_score)
584            .unwrap_or(std::cmp::Ordering::Equal)
585    });
586
587    Ok(RouteResult {
588        primary,
589        alternatives,
590    })
591}
592
593/// Build an alternative route with accurate costs from the original graph
594fn build_alternative_route(graph: &Graph, path: &ShortestPath) -> Result<Route> {
595    let mut route = Route::new();
596    let mut coordinates = Vec::new();
597    let mut actual_cost = 0.0;
598
599    for (i, &edge_id) in path.edges.iter().enumerate() {
600        if let Some(edge) = graph.get_edge(edge_id) {
601            let distance = edge.multi_weight.distance;
602            let time_cost = edge.multi_weight.time;
603            let monetary = edge.multi_weight.monetary;
604
605            let travel_time = if time_cost > 0.0 {
606                time_cost
607            } else {
608                edge.travel_time()
609                    .map(|t| t * 3600.0)
610                    .unwrap_or(distance / 13.89)
611            };
612
613            actual_cost += edge.weight; // Use original weight
614
615            let turn_angle = if i > 0 {
616                calculate_turn_angle(graph, path.edges[i - 1], edge_id)
617            } else {
618                None
619            };
620
621            let instruction = turn_angle.map(|angle| generate_turn_instruction(angle));
622
623            let segment = RouteSegment {
624                edge: edge_id,
625                distance,
626                travel_time,
627                monetary_cost: monetary,
628                instruction,
629                turn_angle,
630            };
631
632            route.segments.push(segment);
633            route.total_distance += distance;
634            route.total_time += travel_time;
635            route.total_monetary_cost += monetary;
636
637            if let Some(geom) = &edge.geometry {
638                coordinates.extend(geom.coords.iter().copied());
639            }
640        }
641    }
642
643    route.node_sequence = path.nodes.clone();
644    route.coordinates = coordinates;
645    route.is_primary = false;
646    route.quality_score = actual_cost;
647
648    Ok(route)
649}
650
651/// Optimize the order of waypoints to minimize total route cost
652/// Uses nearest-neighbor heuristic followed by 2-opt improvement
653///
654/// # Arguments
655///
656/// * `graph` - The network graph
657/// * `waypoints` - List of waypoints to visit (order will be optimized)
658/// * `start` - Fixed starting point
659/// * `end` - Fixed ending point (can be same as start for round-trip)
660/// * `options` - Route options
661///
662/// # Returns
663///
664/// Optimized ordering of waypoints and the total route
665pub fn optimize_waypoint_order(
666    graph: &Graph,
667    waypoints: &[NodeId],
668    start: NodeId,
669    end: Option<NodeId>,
670    options: &RouteOptions,
671) -> Result<WaypointOptimizationResult> {
672    if waypoints.is_empty() {
673        return Ok(WaypointOptimizationResult {
674            optimized_order: Vec::new(),
675            total_cost: 0.0,
676            route: Route::new(),
677        });
678    }
679
680    if waypoints.len() == 1 {
681        let mut opts = options.clone();
682        opts.via_points = waypoints.to_vec();
683        let end_node = end.unwrap_or(start);
684        let route = calculate_route(graph, start, end_node, &opts)?;
685        return Ok(WaypointOptimizationResult {
686            optimized_order: waypoints.to_vec(),
687            total_cost: route.total_distance,
688            route,
689        });
690    }
691
692    let path_options = build_path_options(options);
693
694    // Pre-compute cost matrix between all relevant nodes
695    let mut all_points = vec![start];
696    all_points.extend_from_slice(waypoints);
697    if let Some(e) = end {
698        if !all_points.contains(&e) {
699            all_points.push(e);
700        }
701    }
702
703    let mut cost_matrix: HashMap<(NodeId, NodeId), f64> = HashMap::new();
704
705    for &from in &all_points {
706        for &to in &all_points {
707            if from == to {
708                cost_matrix.insert((from, to), 0.0);
709                continue;
710            }
711
712            let result = dijkstra_search(graph, from, to, &path_options);
713            let cost = match result {
714                Ok(path) if path.found => path.cost,
715                _ => f64::MAX / 2.0, // Very high cost for unreachable pairs
716            };
717            cost_matrix.insert((from, to), cost);
718        }
719    }
720
721    // Nearest-neighbor heuristic
722    let mut order = Vec::new();
723    let mut remaining: HashSet<NodeId> = waypoints.iter().copied().collect();
724    let mut current = start;
725
726    while !remaining.is_empty() {
727        let nearest = remaining
728            .iter()
729            .min_by(|&&a, &&b| {
730                let cost_a = cost_matrix.get(&(current, a)).copied().unwrap_or(f64::MAX);
731                let cost_b = cost_matrix.get(&(current, b)).copied().unwrap_or(f64::MAX);
732                cost_a
733                    .partial_cmp(&cost_b)
734                    .unwrap_or(std::cmp::Ordering::Equal)
735            })
736            .copied();
737
738        if let Some(next) = nearest {
739            order.push(next);
740            remaining.remove(&next);
741            current = next;
742        } else {
743            break;
744        }
745    }
746
747    // 2-opt improvement
748    let improved_order = two_opt_improve(&order, start, end, &cost_matrix);
749
750    // Calculate total cost of optimized route
751    let total_cost = calculate_order_cost(&improved_order, start, end, &cost_matrix);
752
753    // Build the actual route with optimized order
754    let mut opts = options.clone();
755    opts.via_points = improved_order.clone();
756    let end_node = end.unwrap_or(start);
757    let route = calculate_route(graph, start, end_node, &opts)?;
758
759    Ok(WaypointOptimizationResult {
760        optimized_order: improved_order,
761        total_cost,
762        route,
763    })
764}
765
766/// 2-opt improvement for waypoint ordering
767fn two_opt_improve(
768    order: &[NodeId],
769    start: NodeId,
770    end: Option<NodeId>,
771    cost_matrix: &HashMap<(NodeId, NodeId), f64>,
772) -> Vec<NodeId> {
773    let mut best_order = order.to_vec();
774    let mut best_cost = calculate_order_cost(&best_order, start, end, cost_matrix);
775    let mut improved = true;
776
777    while improved {
778        improved = false;
779
780        for i in 0..best_order.len().saturating_sub(1) {
781            for j in (i + 1)..best_order.len() {
782                // Reverse the segment between i and j
783                let mut new_order = best_order.clone();
784                new_order[i..=j].reverse();
785
786                let new_cost = calculate_order_cost(&new_order, start, end, cost_matrix);
787
788                if new_cost < best_cost - 1e-10 {
789                    best_order = new_order;
790                    best_cost = new_cost;
791                    improved = true;
792                }
793            }
794        }
795    }
796
797    best_order
798}
799
800/// Calculate total cost for a given waypoint order
801fn calculate_order_cost(
802    order: &[NodeId],
803    start: NodeId,
804    end: Option<NodeId>,
805    cost_matrix: &HashMap<(NodeId, NodeId), f64>,
806) -> f64 {
807    let mut total = 0.0;
808    let mut current = start;
809
810    for &next in order {
811        total += cost_matrix
812            .get(&(current, next))
813            .copied()
814            .unwrap_or(f64::MAX / 2.0);
815        current = next;
816    }
817
818    if let Some(end_node) = end {
819        total += cost_matrix
820            .get(&(current, end_node))
821            .copied()
822            .unwrap_or(f64::MAX / 2.0);
823    }
824
825    total
826}
827
828/// Result of waypoint order optimization
829#[derive(Debug, Clone)]
830pub struct WaypointOptimizationResult {
831    /// Optimized ordering of waypoints
832    pub optimized_order: Vec<NodeId>,
833    /// Total route cost
834    pub total_cost: f64,
835    /// The complete route following the optimized order
836    pub route: Route,
837}
838
839/// Calculate multiple routes in batch
840pub fn calculate_routes_batch(
841    graph: &Graph,
842    pairs: &[(NodeId, NodeId)],
843    options: &RouteOptions,
844) -> Result<Vec<Route>> {
845    pairs
846        .iter()
847        .map(|(start, end)| calculate_route(graph, *start, *end, options))
848        .collect()
849}
850
851/// Origin-destination matrix computation
852///
853/// Computes shortest path costs between all origin-destination pairs
854pub fn od_matrix(
855    graph: &Graph,
856    origins: &[NodeId],
857    destinations: &[NodeId],
858    options: &RouteOptions,
859) -> Result<Vec<Vec<f64>>> {
860    let path_options = build_path_options(options);
861
862    let mut matrix = Vec::with_capacity(origins.len());
863
864    for &origin in origins {
865        let mut row = Vec::with_capacity(destinations.len());
866
867        // Use single-source Dijkstra for efficiency
868        let sssp = crate::vector::network::dijkstra_single_source(graph, origin, &path_options)?;
869
870        for &dest in destinations {
871            let cost = sssp.get(&dest).map(|(c, _)| *c).unwrap_or(f64::INFINITY);
872            row.push(cost);
873        }
874
875        matrix.push(row);
876    }
877
878    Ok(matrix)
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884
885    fn create_routing_graph() -> Graph {
886        let mut graph = Graph::new();
887
888        // Simple road network:
889        //  n0 --1-- n1 --1-- n2
890        //   |               / |
891        //   2             1   1
892        //   |           /     |
893        //  n3 --3-- n4 --1-- n5
894        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
895        let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
896        let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
897        let n3 = graph.add_node(Coordinate::new_2d(0.0, 1.0));
898        let n4 = graph.add_node(Coordinate::new_2d(1.0, 1.0));
899        let n5 = graph.add_node(Coordinate::new_2d(2.0, 1.0));
900
901        let _ = graph.add_edge(n0, n1, 1.0);
902        let _ = graph.add_edge(n1, n2, 1.0);
903        let _ = graph.add_edge(n0, n3, 2.0);
904        let _ = graph.add_edge(n3, n4, 3.0);
905        let _ = graph.add_edge(n4, n2, 1.0);
906        let _ = graph.add_edge(n4, n5, 1.0);
907        let _ = graph.add_edge(n2, n5, 1.0);
908
909        graph
910    }
911
912    #[test]
913    fn test_route_creation() {
914        let route = Route::new();
915        assert_eq!(route.segments.len(), 0);
916        assert_eq!(route.total_distance, 0.0);
917    }
918
919    #[test]
920    fn test_route_options() {
921        let options = RouteOptions::default();
922        assert_eq!(options.algorithm, RoutingAlgorithm::Shortest);
923        assert!(!options.avoid_tolls);
924    }
925
926    #[test]
927    fn test_calculate_route() {
928        let graph = create_routing_graph();
929        let nodes = graph.sorted_node_ids();
930
931        let route = calculate_route(&graph, nodes[0], nodes[5], &RouteOptions::default());
932        assert!(route.is_ok());
933
934        let r = route.expect("route");
935        assert!(!r.segments.is_empty());
936        assert!(r.total_distance > 0.0);
937    }
938
939    #[test]
940    fn test_routing_criteria() {
941        let fastest = RoutingCriteria::fastest();
942        let criteria_map = fastest.to_weight_criteria();
943        assert!(criteria_map.contains_key("time"));
944        assert!(!criteria_map.contains_key("distance"));
945    }
946
947    #[test]
948    fn test_via_point_routing() {
949        let graph = create_routing_graph();
950        let nodes = graph.sorted_node_ids();
951
952        let mut options = RouteOptions::default();
953        options.via_points = vec![nodes[1]]; // Route through node 1
954
955        let route = calculate_route(&graph, nodes[0], nodes[5], &options);
956        assert!(route.is_ok());
957
958        let r = route.expect("route");
959        assert!(r.node_sequence.contains(&nodes[1]));
960    }
961
962    #[test]
963    fn test_route_overlap() {
964        let mut route1 = Route::new();
965        route1.segments.push(RouteSegment {
966            edge: EdgeId(0),
967            distance: 1.0,
968            travel_time: 1.0,
969            monetary_cost: 0.0,
970            instruction: None,
971            turn_angle: None,
972        });
973        route1.segments.push(RouteSegment {
974            edge: EdgeId(1),
975            distance: 1.0,
976            travel_time: 1.0,
977            monetary_cost: 0.0,
978            instruction: None,
979            turn_angle: None,
980        });
981
982        let mut route2 = Route::new();
983        route2.segments.push(RouteSegment {
984            edge: EdgeId(0),
985            distance: 1.0,
986            travel_time: 1.0,
987            monetary_cost: 0.0,
988            instruction: None,
989            turn_angle: None,
990        });
991        route2.segments.push(RouteSegment {
992            edge: EdgeId(2),
993            distance: 1.0,
994            travel_time: 1.0,
995            monetary_cost: 0.0,
996            instruction: None,
997            turn_angle: None,
998        });
999
1000        let overlap = route1.overlap_ratio(&route2);
1001        assert!((overlap - 0.5).abs() < 1e-10);
1002    }
1003
1004    #[test]
1005    fn test_turn_instruction() {
1006        assert_eq!(generate_turn_instruction(0.0), "Continue straight");
1007        assert_eq!(generate_turn_instruction(90.0), "Turn left");
1008        assert_eq!(generate_turn_instruction(-90.0), "Turn right");
1009        assert_eq!(generate_turn_instruction(30.0), "Bear left");
1010        assert_eq!(generate_turn_instruction(-30.0), "Bear right");
1011        assert_eq!(generate_turn_instruction(150.0), "Sharp left");
1012        assert_eq!(generate_turn_instruction(-150.0), "Sharp right");
1013    }
1014
1015    #[test]
1016    fn test_batch_routes() {
1017        let graph = create_routing_graph();
1018        let nodes = graph.sorted_node_ids();
1019
1020        let pairs = vec![(nodes[0], nodes[5]), (nodes[0], nodes[2])];
1021        let result = calculate_routes_batch(&graph, &pairs, &RouteOptions::default());
1022        assert!(result.is_ok());
1023
1024        let routes = result.expect("batch routes");
1025        assert_eq!(routes.len(), 2);
1026    }
1027
1028    #[test]
1029    fn test_od_matrix() {
1030        let graph = create_routing_graph();
1031        let nodes = graph.sorted_node_ids();
1032
1033        let origins = vec![nodes[0], nodes[3]];
1034        let destinations = vec![nodes[2], nodes[5]];
1035
1036        let result = od_matrix(&graph, &origins, &destinations, &RouteOptions::default());
1037        assert!(result.is_ok());
1038
1039        let matrix = result.expect("od matrix");
1040        assert_eq!(matrix.len(), 2);
1041        assert_eq!(matrix[0].len(), 2);
1042        // From n0 to n2 should be 2.0 (n0->n1->n2)
1043        assert!((matrix[0][0] - 2.0).abs() < 1e-10);
1044    }
1045
1046    #[test]
1047    fn test_waypoint_optimization() {
1048        let mut graph = Graph::new();
1049
1050        // Create a line graph: n0 -- n1 -- n2 -- n3 -- n4
1051        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1052        let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1053        let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
1054        let n3 = graph.add_node(Coordinate::new_2d(3.0, 0.0));
1055        let n4 = graph.add_node(Coordinate::new_2d(4.0, 0.0));
1056
1057        let _ = graph.add_edge(n0, n1, 1.0);
1058        let _ = graph.add_edge(n1, n2, 1.0);
1059        let _ = graph.add_edge(n2, n3, 1.0);
1060        let _ = graph.add_edge(n3, n4, 1.0);
1061        // Also add reverse edges for flexibility
1062        let _ = graph.add_edge(n1, n0, 1.0);
1063        let _ = graph.add_edge(n2, n1, 1.0);
1064        let _ = graph.add_edge(n3, n2, 1.0);
1065        let _ = graph.add_edge(n4, n3, 1.0);
1066
1067        // Optimize visiting n3, n1 from n0 to n4
1068        // Natural order would be n0 -> n1 -> n3 -> n4 (cost 4)
1069        // Reverse n3, n1 would be n0 -> n3 -> n1 -> n4 (cost 3+2+3=8, worse)
1070        let result =
1071            optimize_waypoint_order(&graph, &[n3, n1], n0, Some(n4), &RouteOptions::default());
1072        assert!(result.is_ok());
1073
1074        let opt = result.expect("optimization");
1075        // Should optimize to [n1, n3] order
1076        assert_eq!(opt.optimized_order, vec![n1, n3]);
1077    }
1078}