Skip to main content

oxigdal_algorithms/vector/network/
graph_ops.rs

1//! Graph operation types and auxiliary structures
2//!
3//! This module contains validation, topology cleaning results, time-dependent
4//! weight modeling, turn penalty management, and rich network node/edge types.
5
6use super::graph::{Edge, EdgeId, Graph, GraphType, Node, NodeId};
7use crate::error::{AlgorithmError, Result};
8use oxigdal_core::vector::Coordinate;
9use std::collections::{HashMap, HashSet, VecDeque};
10
11/// Result of graph validation containing all detected issues
12#[derive(Debug, Clone)]
13pub struct ValidationResult {
14    /// Whether the graph is valid (no critical issues)
15    pub is_valid: bool,
16    /// List of validation issues
17    pub issues: Vec<ValidationIssue>,
18}
19
20/// A single validation issue
21#[derive(Debug, Clone)]
22pub struct ValidationIssue {
23    /// Severity of the issue
24    pub severity: ValidationSeverity,
25    /// Description of the issue
26    pub description: String,
27}
28
29/// Severity level for validation issues
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ValidationSeverity {
32    /// Error: graph is structurally invalid
33    Error,
34    /// Warning: graph may have problems
35    Warning,
36    /// Info: informational note
37    Info,
38}
39
40/// Connected component information
41#[derive(Debug, Clone)]
42pub struct ConnectedComponent {
43    /// Nodes in this component
44    pub nodes: Vec<NodeId>,
45    /// Number of edges in this component
46    pub edge_count: usize,
47}
48
49/// Result of topology cleaning
50#[derive(Debug, Clone)]
51pub struct TopologyCleanResult {
52    /// Number of nodes that were snapped together
53    pub nodes_snapped: usize,
54    /// Number of self-loops removed
55    pub self_loops_removed: usize,
56    /// Number of parallel edges removed
57    pub parallel_edges_removed: usize,
58    /// Number of isolated nodes removed
59    pub isolated_nodes_removed: usize,
60    /// Number of degree-2 nodes contracted
61    pub degree2_nodes_contracted: usize,
62}
63
64/// A node in a network (with richer semantics)
65#[derive(Debug, Clone)]
66pub struct NetworkNode {
67    /// Base node
68    pub node: Node,
69    /// Node type (e.g., "intersection", "dead_end")
70    pub node_type: String,
71    /// Elevation (optional)
72    pub elevation: Option<f64>,
73}
74
75/// An edge in a network (with richer semantics)
76#[derive(Debug, Clone)]
77pub struct NetworkEdge {
78    /// Base edge
79    pub edge: Edge,
80    /// Road type (e.g., "highway", "residential")
81    pub road_type: String,
82    /// One-way restriction
83    pub one_way: bool,
84    /// Surface type (e.g., "paved", "gravel")
85    pub surface: Option<String>,
86}
87
88/// Time-dependent weight function
89///
90/// Maps a time-of-day (in seconds since midnight, 0..86400)
91/// to an edge weight multiplier.
92#[derive(Debug, Clone)]
93pub struct TimeDependentWeight {
94    /// Time intervals (start_time, multiplier) sorted by start_time
95    /// The multiplier applies from start_time until the next interval
96    pub intervals: Vec<(f64, f64)>,
97}
98
99impl TimeDependentWeight {
100    /// Create a constant (time-independent) weight
101    pub fn constant() -> Self {
102        Self {
103            intervals: vec![(0.0, 1.0)],
104        }
105    }
106
107    /// Create from interval data
108    pub fn new(mut intervals: Vec<(f64, f64)>) -> Self {
109        intervals.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
110        if intervals.is_empty() {
111            intervals.push((0.0, 1.0));
112        }
113        Self { intervals }
114    }
115
116    /// Get the weight multiplier at a given time (seconds since midnight)
117    pub fn multiplier_at(&self, time: f64) -> f64 {
118        // Normalize to 0..86400
119        let time = time % 86400.0;
120
121        // Binary search for the interval containing this time
122        let mut result = self.intervals[0].1;
123        for &(start, mult) in &self.intervals {
124            if time >= start {
125                result = mult;
126            } else {
127                break;
128            }
129        }
130        result
131    }
132
133    /// Create a typical rush-hour pattern
134    pub fn rush_hour() -> Self {
135        Self::new(vec![
136            (0.0, 0.8),     // Night: low traffic
137            (25200.0, 1.5), // 7:00 AM: morning rush
138            (32400.0, 1.0), // 9:00 AM: normal
139            (61200.0, 1.5), // 5:00 PM: evening rush
140            (68400.0, 1.0), // 7:00 PM: normal
141            (79200.0, 0.8), // 10:00 PM: low traffic
142        ])
143    }
144}
145
146/// Turn penalty specification between two edges meeting at a node
147#[derive(Debug, Clone)]
148pub struct TurnPenalty {
149    /// The node where the turn occurs
150    pub via_node: NodeId,
151    /// The incoming edge
152    pub from_edge: EdgeId,
153    /// The outgoing edge
154    pub to_edge: EdgeId,
155    /// Additional cost for making this turn
156    pub penalty: f64,
157}
158
159/// Collection of turn penalties for a graph
160#[derive(Debug, Clone, Default)]
161pub struct TurnPenalties {
162    /// Map from (via_node, from_edge, to_edge) to penalty
163    penalties: HashMap<(NodeId, EdgeId, EdgeId), f64>,
164    /// Set of prohibited turns (infinite penalty)
165    prohibited: HashSet<(NodeId, EdgeId, EdgeId)>,
166}
167
168impl TurnPenalties {
169    /// Create empty turn penalties
170    pub fn new() -> Self {
171        Self::default()
172    }
173
174    /// Add a turn penalty
175    pub fn add_penalty(
176        &mut self,
177        via_node: NodeId,
178        from_edge: EdgeId,
179        to_edge: EdgeId,
180        penalty: f64,
181    ) {
182        self.penalties
183            .insert((via_node, from_edge, to_edge), penalty);
184    }
185
186    /// Add a prohibited turn (infinite penalty)
187    pub fn add_prohibition(&mut self, via_node: NodeId, from_edge: EdgeId, to_edge: EdgeId) {
188        self.prohibited.insert((via_node, from_edge, to_edge));
189    }
190
191    /// Get the penalty for a turn, or 0.0 if no penalty
192    pub fn get_penalty(&self, via_node: NodeId, from_edge: EdgeId, to_edge: EdgeId) -> f64 {
193        if self.prohibited.contains(&(via_node, from_edge, to_edge)) {
194            return f64::INFINITY;
195        }
196        self.penalties
197            .get(&(via_node, from_edge, to_edge))
198            .copied()
199            .unwrap_or(0.0)
200    }
201
202    /// Check if a turn is prohibited
203    pub fn is_prohibited(&self, via_node: NodeId, from_edge: EdgeId, to_edge: EdgeId) -> bool {
204        self.prohibited.contains(&(via_node, from_edge, to_edge))
205    }
206
207    /// Number of penalties registered
208    pub fn len(&self) -> usize {
209        self.penalties.len() + self.prohibited.len()
210    }
211
212    /// Check if empty
213    pub fn is_empty(&self) -> bool {
214        self.penalties.is_empty() && self.prohibited.is_empty()
215    }
216}
217
218/// Haversine distance between two geographic coordinates (in meters)
219pub fn haversine_distance(a: &Coordinate, b: &Coordinate) -> f64 {
220    const EARTH_RADIUS: f64 = 6_371_000.0; // meters
221
222    let lat1 = a.y.to_radians();
223    let lat2 = b.y.to_radians();
224    let dlat = (b.y - a.y).to_radians();
225    let dlon = (b.x - a.x).to_radians();
226
227    let h = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
228
229    2.0 * EARTH_RADIUS * h.sqrt().asin()
230}
231
232// ---- Graph validation and analysis methods ----
233
234impl Graph {
235    /// Comprehensive graph validation
236    pub fn validate(&self) -> Result<()> {
237        let result = self.validate_detailed();
238        if result.is_valid {
239            Ok(())
240        } else {
241            let errors: Vec<String> = result
242                .issues
243                .iter()
244                .filter(|i| i.severity == ValidationSeverity::Error)
245                .map(|i| i.description.clone())
246                .collect();
247            Err(AlgorithmError::InvalidGeometry(errors.join("; ")))
248        }
249    }
250
251    /// Detailed graph validation returning all issues
252    pub fn validate_detailed(&self) -> ValidationResult {
253        let mut issues = Vec::new();
254
255        // Check that all edges reference valid nodes
256        for (edge_id, edge) in self.edges_iter() {
257            if !self.has_node(edge.source) {
258                issues.push(ValidationIssue {
259                    severity: ValidationSeverity::Error,
260                    description: format!(
261                        "Edge {:?} references non-existent source node {:?}",
262                        edge_id, edge.source
263                    ),
264                });
265            }
266
267            if !self.has_node(edge.target) {
268                issues.push(ValidationIssue {
269                    severity: ValidationSeverity::Error,
270                    description: format!(
271                        "Edge {:?} references non-existent target node {:?}",
272                        edge_id, edge.target
273                    ),
274                });
275            }
276
277            if edge.is_self_loop() {
278                issues.push(ValidationIssue {
279                    severity: ValidationSeverity::Warning,
280                    description: format!(
281                        "Edge {:?} is a self-loop at node {:?}",
282                        edge_id, edge.source
283                    ),
284                });
285            }
286
287            if edge.weight < 0.0 {
288                issues.push(ValidationIssue {
289                    severity: ValidationSeverity::Warning,
290                    description: format!("Edge {:?} has negative weight {}", edge_id, edge.weight),
291                });
292            }
293
294            if edge.weight.is_nan() || edge.weight.is_infinite() {
295                issues.push(ValidationIssue {
296                    severity: ValidationSeverity::Error,
297                    description: format!("Edge {:?} has invalid weight (NaN or infinite)", edge_id),
298                });
299            }
300        }
301
302        // Check for isolated nodes
303        let isolated_count = self
304            .node_ids()
305            .iter()
306            .filter(|node_id| {
307                self.outgoing_edges(**node_id).is_empty()
308                    && self.incoming_edges(**node_id).is_empty()
309            })
310            .count();
311
312        if isolated_count > 0 {
313            issues.push(ValidationIssue {
314                severity: ValidationSeverity::Info,
315                description: format!("Graph contains {} isolated node(s)", isolated_count),
316            });
317        }
318
319        // Check for parallel edges
320        let mut edge_pairs: HashMap<(NodeId, NodeId), Vec<EdgeId>> = HashMap::new();
321        for (&edge_id, edge) in self.edges_iter() {
322            let key = if self.graph_type() == GraphType::Undirected {
323                if edge.source.0 <= edge.target.0 {
324                    (edge.source, edge.target)
325                } else {
326                    (edge.target, edge.source)
327                }
328            } else {
329                (edge.source, edge.target)
330            };
331            edge_pairs.entry(key).or_default().push(edge_id);
332        }
333        for ((src, tgt), ids) in &edge_pairs {
334            if ids.len() > 1 {
335                issues.push(ValidationIssue {
336                    severity: ValidationSeverity::Warning,
337                    description: format!(
338                        "Parallel edges detected between {:?} and {:?}: {:?}",
339                        src, tgt, ids
340                    ),
341                });
342            }
343        }
344
345        let is_valid = !issues
346            .iter()
347            .any(|i| i.severity == ValidationSeverity::Error);
348
349        ValidationResult { is_valid, issues }
350    }
351
352    /// Check if the graph is connected (weakly for directed graphs)
353    pub fn is_connected(&self) -> bool {
354        if self.num_nodes() == 0 {
355            return true;
356        }
357
358        let components = self.connected_components();
359        components.len() <= 1
360    }
361
362    /// Find all connected components (weakly connected for directed graphs)
363    pub fn connected_components(&self) -> Vec<ConnectedComponent> {
364        let mut visited: HashSet<NodeId> = HashSet::new();
365        let mut components = Vec::new();
366
367        for &node_id in &self.node_ids() {
368            if visited.contains(&node_id) {
369                continue;
370            }
371
372            let mut component_nodes = Vec::new();
373            let mut queue = VecDeque::new();
374            queue.push_back(node_id);
375            visited.insert(node_id);
376
377            while let Some(current) = queue.pop_front() {
378                component_nodes.push(current);
379
380                for &edge_id in self.outgoing_edges(current) {
381                    if let Some(edge) = self.get_edge(edge_id) {
382                        let neighbor = if self.graph_type() == GraphType::Undirected {
383                            edge.other_node(current)
384                        } else {
385                            Some(edge.target)
386                        };
387                        if let Some(n) = neighbor {
388                            if !visited.contains(&n) {
389                                visited.insert(n);
390                                queue.push_back(n);
391                            }
392                        }
393                    }
394                }
395
396                if self.graph_type() == GraphType::Directed {
397                    for &edge_id in self.incoming_edges(current) {
398                        if let Some(edge) = self.get_edge(edge_id) {
399                            if !visited.contains(&edge.source) {
400                                visited.insert(edge.source);
401                                queue.push_back(edge.source);
402                            }
403                        }
404                    }
405                }
406            }
407
408            let component_node_set: HashSet<NodeId> = component_nodes.iter().copied().collect();
409            let edge_count = self
410                .edges_iter()
411                .filter(|(_, e)| {
412                    component_node_set.contains(&e.source) && component_node_set.contains(&e.target)
413                })
414                .count();
415
416            components.push(ConnectedComponent {
417                nodes: component_nodes,
418                edge_count,
419            });
420        }
421
422        components
423    }
424
425    /// Find strongly connected components using Tarjan's algorithm (directed graphs only)
426    pub fn strongly_connected_components(&self) -> Vec<Vec<NodeId>> {
427        if self.graph_type() == GraphType::Undirected {
428            return self
429                .connected_components()
430                .into_iter()
431                .map(|c| c.nodes)
432                .collect();
433        }
434
435        let mut index_counter: usize = 0;
436        let mut stack: Vec<NodeId> = Vec::new();
437        let mut on_stack: HashSet<NodeId> = HashSet::new();
438        let mut index_map: HashMap<NodeId, usize> = HashMap::new();
439        let mut lowlink: HashMap<NodeId, usize> = HashMap::new();
440        let mut sccs: Vec<Vec<NodeId>> = Vec::new();
441
442        let node_ids: Vec<NodeId> = self.node_ids();
443
444        for node in &node_ids {
445            if !index_map.contains_key(node) {
446                tarjan_dfs(
447                    self,
448                    *node,
449                    &mut index_counter,
450                    &mut stack,
451                    &mut on_stack,
452                    &mut index_map,
453                    &mut lowlink,
454                    &mut sccs,
455                );
456            }
457        }
458
459        sccs
460    }
461
462    // ---- Topology cleaning operations ----
463
464    /// Remove all isolated nodes (nodes with no edges)
465    pub fn remove_isolated_nodes(&mut self) -> Vec<NodeId> {
466        let isolated: Vec<NodeId> = self
467            .node_ids()
468            .into_iter()
469            .filter(|node_id| {
470                self.outgoing_edges(*node_id).is_empty() && self.incoming_edges(*node_id).is_empty()
471            })
472            .collect();
473
474        for node_id in &isolated {
475            self.remove_node_unchecked(*node_id);
476        }
477
478        isolated
479    }
480
481    /// Remove self-loops
482    pub fn remove_self_loops(&mut self) -> Vec<EdgeId> {
483        let self_loops: Vec<EdgeId> = self
484            .edges_iter()
485            .filter(|(_, e)| e.is_self_loop())
486            .map(|(&id, _)| id)
487            .collect();
488
489        let mut removed = Vec::new();
490        for edge_id in self_loops {
491            if self.remove_edge(edge_id).is_ok() {
492                removed.push(edge_id);
493            }
494        }
495
496        removed
497    }
498
499    /// Remove duplicate (parallel) edges, keeping the one with minimum weight
500    pub fn remove_parallel_edges(&mut self) -> Vec<EdgeId> {
501        let mut edge_groups: HashMap<(NodeId, NodeId), Vec<(EdgeId, f64)>> = HashMap::new();
502
503        for (&edge_id, edge) in self.edges_iter() {
504            let key = if self.graph_type() == GraphType::Undirected {
505                if edge.source.0 <= edge.target.0 {
506                    (edge.source, edge.target)
507                } else {
508                    (edge.target, edge.source)
509                }
510            } else {
511                (edge.source, edge.target)
512            };
513            edge_groups
514                .entry(key)
515                .or_default()
516                .push((edge_id, edge.weight));
517        }
518
519        let mut removed = Vec::new();
520        for (_key, mut group) in edge_groups {
521            if group.len() <= 1 {
522                continue;
523            }
524            group.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
525            for &(edge_id, _) in &group[1..] {
526                if self.remove_edge(edge_id).is_ok() {
527                    removed.push(edge_id);
528                }
529            }
530        }
531
532        removed
533    }
534
535    /// Contract degree-2 nodes (nodes with exactly one incoming and one outgoing edge)
536    pub fn contract_degree2_nodes(&mut self) -> usize {
537        let mut contracted = 0;
538
539        loop {
540            let candidate = self.node_ids().into_iter().find(|&node_id| {
541                let out_edges = self.outgoing_edges(node_id);
542                let in_edges = self.incoming_edges(node_id);
543
544                if self.graph_type() == GraphType::Undirected {
545                    out_edges.len() == 2
546                } else {
547                    out_edges.len() == 1
548                        && in_edges.len() == 1
549                        && self
550                            .get_edge(out_edges[0])
551                            .map(|e| e.target)
552                            .unwrap_or(node_id)
553                            != self
554                                .get_edge(in_edges[0])
555                                .map(|e| e.source)
556                                .unwrap_or(node_id)
557                }
558            });
559
560            let node_id = match candidate {
561                Some(n) => n,
562                None => break,
563            };
564
565            if self.graph_type() == GraphType::Directed {
566                let in_edges = self.incoming_edges(node_id).to_vec();
567                let out_edges = self.outgoing_edges(node_id).to_vec();
568
569                if in_edges.len() != 1 || out_edges.len() != 1 {
570                    break;
571                }
572
573                let in_edge = match self.get_edge(in_edges[0]) {
574                    Some(e) => e.clone(),
575                    None => break,
576                };
577                let out_edge = match self.get_edge(out_edges[0]) {
578                    Some(e) => e.clone(),
579                    None => break,
580                };
581
582                let new_weight = in_edge.weight + out_edge.weight;
583                let source = in_edge.source;
584                let target = out_edge.target;
585
586                let _ = self.remove_edge(in_edges[0]);
587                let _ = self.remove_edge(out_edges[0]);
588                let _ = self.remove_node(node_id);
589                let _ = self.add_edge(source, target, new_weight);
590                contracted += 1;
591            } else {
592                let adj_edges = self.outgoing_edges(node_id).to_vec();
593                if adj_edges.len() != 2 {
594                    break;
595                }
596
597                let e0 = match self.get_edge(adj_edges[0]) {
598                    Some(e) => e.clone(),
599                    None => break,
600                };
601                let e1 = match self.get_edge(adj_edges[1]) {
602                    Some(e) => e.clone(),
603                    None => break,
604                };
605
606                let n0 = e0.other_node(node_id).unwrap_or(node_id);
607                let n1 = e1.other_node(node_id).unwrap_or(node_id);
608                let new_weight = e0.weight + e1.weight;
609
610                let _ = self.remove_edge(adj_edges[0]);
611                let _ = self.remove_edge(adj_edges[1]);
612                let _ = self.remove_node(node_id);
613                let _ = self.add_edge(n0, n1, new_weight);
614                contracted += 1;
615            }
616        }
617
618        contracted
619    }
620
621    /// Snap close nodes together within a tolerance
622    pub fn snap_nodes(&mut self, tolerance: f64) -> usize {
623        let node_ids: Vec<NodeId> = self.node_ids();
624        let mut merge_map: HashMap<NodeId, NodeId> = HashMap::new();
625        let mut merged_count = 0;
626
627        for i in 0..node_ids.len() {
628            if merge_map.contains_key(&node_ids[i]) {
629                continue;
630            }
631
632            let coord_i = match self.get_node(node_ids[i]) {
633                Some(n) => n.coordinate,
634                None => continue,
635            };
636
637            for j in (i + 1)..node_ids.len() {
638                if merge_map.contains_key(&node_ids[j]) {
639                    continue;
640                }
641
642                let coord_j = match self.get_node(node_ids[j]) {
643                    Some(n) => n.coordinate,
644                    None => continue,
645                };
646
647                let dx = coord_i.x - coord_j.x;
648                let dy = coord_i.y - coord_j.y;
649                let dist = (dx * dx + dy * dy).sqrt();
650                if dist < tolerance {
651                    merge_map.insert(node_ids[j], node_ids[i]);
652                    merged_count += 1;
653                }
654            }
655        }
656
657        // Remap edges
658        let edge_ids: Vec<EdgeId> = self.edge_ids();
659        for edge_id in edge_ids {
660            let edge = match self.get_edge(edge_id) {
661                Some(e) => e.clone(),
662                None => continue,
663            };
664
665            let new_source = merge_map.get(&edge.source).copied().unwrap_or(edge.source);
666            let new_target = merge_map.get(&edge.target).copied().unwrap_or(edge.target);
667
668            if new_source != edge.source || new_target != edge.target {
669                let _ = self.remove_edge(edge_id);
670                if new_source != new_target {
671                    let _ = self.add_edge(new_source, new_target, edge.weight);
672                }
673            }
674        }
675
676        // Remove merged nodes
677        for old_id in merge_map.keys() {
678            self.remove_node_unchecked(*old_id);
679        }
680
681        merged_count
682    }
683
684    /// Clean topology by performing multiple operations
685    pub fn clean_topology(&mut self, tolerance: f64) -> TopologyCleanResult {
686        let snapped = self.snap_nodes(tolerance);
687        let self_loops = self.remove_self_loops();
688        let parallel = self.remove_parallel_edges();
689        let isolated = self.remove_isolated_nodes();
690        let contracted = self.contract_degree2_nodes();
691
692        TopologyCleanResult {
693            nodes_snapped: snapped,
694            self_loops_removed: self_loops.len(),
695            parallel_edges_removed: parallel.len(),
696            isolated_nodes_removed: isolated.len(),
697            degree2_nodes_contracted: contracted,
698        }
699    }
700}
701
702/// Tarjan DFS helper for strongly connected components
703fn tarjan_dfs(
704    graph: &Graph,
705    v: NodeId,
706    index_counter: &mut usize,
707    stack: &mut Vec<NodeId>,
708    on_stack: &mut HashSet<NodeId>,
709    index_map: &mut HashMap<NodeId, usize>,
710    lowlink: &mut HashMap<NodeId, usize>,
711    sccs: &mut Vec<Vec<NodeId>>,
712) {
713    index_map.insert(v, *index_counter);
714    lowlink.insert(v, *index_counter);
715    *index_counter += 1;
716    stack.push(v);
717    on_stack.insert(v);
718
719    for &edge_id in graph.outgoing_edges(v) {
720        if let Some(edge) = graph.get_edge(edge_id) {
721            let w = edge.target;
722            if !index_map.contains_key(&w) {
723                tarjan_dfs(
724                    graph,
725                    w,
726                    index_counter,
727                    stack,
728                    on_stack,
729                    index_map,
730                    lowlink,
731                    sccs,
732                );
733                let w_low = lowlink.get(&w).copied().unwrap_or(0);
734                let v_low = lowlink.get(&v).copied().unwrap_or(0);
735                lowlink.insert(v, v_low.min(w_low));
736            } else if on_stack.contains(&w) {
737                let w_idx = index_map.get(&w).copied().unwrap_or(0);
738                let v_low = lowlink.get(&v).copied().unwrap_or(0);
739                lowlink.insert(v, v_low.min(w_idx));
740            }
741        }
742    }
743
744    let v_low = lowlink.get(&v).copied().unwrap_or(0);
745    let v_idx = index_map.get(&v).copied().unwrap_or(0);
746    if v_low == v_idx {
747        let mut scc = Vec::new();
748        while let Some(w) = stack.pop() {
749            on_stack.remove(&w);
750            scc.push(w);
751            if w == v {
752                break;
753            }
754        }
755        sccs.push(scc);
756    }
757}
758
759#[cfg(test)]
760mod tests {
761    use super::*;
762    use crate::vector::network::{EdgeWeight, GraphBuilder, GraphType};
763    use oxigdal_core::vector::LineString;
764
765    #[test]
766    fn test_validate_graph() {
767        let mut graph = Graph::new();
768        let n1 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
769        let n2 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
770        let _ = graph.add_edge(n1, n2, 1.0);
771        assert!(graph.validate().is_ok());
772    }
773
774    #[test]
775    fn test_validate_detailed() {
776        let mut graph = Graph::new();
777        let n1 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
778        let n2 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
779        let _ = graph.add_node(Coordinate::new_2d(5.0, 5.0)); // Isolated
780        let _ = graph.add_edge(n1, n2, 1.0);
781        let _ = graph.add_edge(n1, n1, 0.5); // Self-loop
782
783        let result = graph.validate_detailed();
784        assert!(result.is_valid);
785        assert!(!result.issues.is_empty());
786    }
787
788    #[test]
789    fn test_connected_components() {
790        let mut graph = Graph::new();
791        let n1 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
792        let n2 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
793        let n3 = graph.add_node(Coordinate::new_2d(5.0, 5.0));
794        let n4 = graph.add_node(Coordinate::new_2d(6.0, 5.0));
795        let _ = graph.add_edge(n1, n2, 1.0);
796        let _ = graph.add_edge(n3, n4, 1.0);
797        let components = graph.connected_components();
798        assert_eq!(components.len(), 2);
799    }
800
801    #[test]
802    fn test_strongly_connected_components() {
803        let mut graph = Graph::with_type(GraphType::Directed);
804        let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
805        let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
806        let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
807        let _ = graph.add_edge(n0, n1, 1.0);
808        let _ = graph.add_edge(n1, n2, 1.0);
809        let _ = graph.add_edge(n2, n0, 1.0);
810        let sccs = graph.strongly_connected_components();
811        assert_eq!(sccs.len(), 1);
812        assert_eq!(sccs[0].len(), 3);
813    }
814
815    #[test]
816    fn test_remove_self_loops() {
817        let mut graph = Graph::new();
818        let n1 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
819        let n2 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
820        let _ = graph.add_edge(n1, n1, 0.5);
821        let _ = graph.add_edge(n1, n2, 1.0);
822        assert_eq!(graph.num_edges(), 2);
823        let removed = graph.remove_self_loops();
824        assert_eq!(removed.len(), 1);
825        assert_eq!(graph.num_edges(), 1);
826    }
827
828    #[test]
829    fn test_remove_isolated_nodes() {
830        let mut graph = Graph::new();
831        let n1 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
832        let n2 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
833        let _ = graph.add_node(Coordinate::new_2d(5.0, 5.0));
834        let _ = graph.add_edge(n1, n2, 1.0);
835        assert_eq!(graph.num_nodes(), 3);
836        let removed = graph.remove_isolated_nodes();
837        assert_eq!(removed.len(), 1);
838        assert_eq!(graph.num_nodes(), 2);
839    }
840
841    #[test]
842    fn test_clean_topology() {
843        let mut graph = Graph::new();
844        let n1 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
845        let n2 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
846        let _ = graph.add_node(Coordinate::new_2d(100.0, 100.0));
847        let _ = graph.add_edge(n1, n1, 0.5);
848        let _ = graph.add_edge(n1, n2, 1.0);
849        let _ = graph.add_edge(n1, n2, 2.0);
850        let result = graph.clean_topology(0.0);
851        assert_eq!(result.self_loops_removed, 1);
852        assert_eq!(result.parallel_edges_removed, 1);
853        assert_eq!(result.isolated_nodes_removed, 1);
854    }
855
856    #[test]
857    fn test_time_dependent_weight() {
858        let tdw = TimeDependentWeight::rush_hour();
859        assert!((tdw.multiplier_at(3600.0) - 0.8).abs() < 1e-10);
860        assert!((tdw.multiplier_at(28800.0) - 1.5).abs() < 1e-10);
861        assert!((tdw.multiplier_at(43200.0) - 1.0).abs() < 1e-10);
862    }
863
864    #[test]
865    fn test_turn_penalties() {
866        let mut tp = TurnPenalties::new();
867        let node = NodeId(1);
868        let from = EdgeId(0);
869        let to = EdgeId(2);
870        tp.add_penalty(node, from, to, 5.0);
871        assert!((tp.get_penalty(node, from, to) - 5.0).abs() < 1e-10);
872        assert!((tp.get_penalty(node, from, EdgeId(3)) - 0.0).abs() < 1e-10);
873    }
874
875    #[test]
876    fn test_turn_prohibition() {
877        let mut tp = TurnPenalties::new();
878        let node = NodeId(1);
879        let from = EdgeId(0);
880        let to = EdgeId(2);
881        tp.add_prohibition(node, from, to);
882        assert!(tp.is_prohibited(node, from, to));
883        assert!(tp.get_penalty(node, from, to).is_infinite());
884    }
885
886    #[test]
887    fn test_haversine_distance() {
888        let london = Coordinate::new_2d(-0.1278, 51.5074);
889        let paris = Coordinate::new_2d(2.3522, 48.8566);
890        let dist = haversine_distance(&london, &paris);
891        assert!(dist > 300_000.0 && dist < 400_000.0);
892    }
893}