Skip to main content

tp_lib_core/path/
graph.rs

1//! Network topology graph representation
2//!
3//! Builds a petgraph DiGraph from netelements and netrelations to enable
4//! efficient path traversal, navigability checking, and shortest-path routing.
5
6use crate::errors::ProjectionError;
7use crate::models::{NetRelation, Netelement};
8use geo::HaversineLength;
9use petgraph::graph::{DiGraph, NodeIndex};
10use petgraph::visit::EdgeRef;
11use serde::{Deserialize, Serialize};
12use std::cmp::Ordering;
13use std::collections::{BinaryHeap, HashMap};
14
15/// Cache for shortest-path queries between netelement sides.
16///
17/// Stores `(from_ne_id, from_pos, to_ne_id, to_pos) → Option<f64>`.
18/// Lazily populated to avoid recomputing Dijkstra for repeated queries.
19pub type ShortestPathCache = HashMap<(String, u8, String, u8), Option<f64>>;
20
21/// Look up or compute the shortest-path distance between two netelement sides,
22/// caching the result for future queries.
23pub fn cached_shortest_path_distance(
24    cache: &mut ShortestPathCache,
25    graph: &DiGraph<NetelementSide, f64>,
26    node_map: &HashMap<NetelementSide, NodeIndex>,
27    from: &NetelementSide,
28    to: &NetelementSide,
29) -> Option<f64> {
30    let key = (
31        from.netelement_id.clone(),
32        from.position,
33        to.netelement_id.clone(),
34        to.position,
35    );
36
37    if let Some(&cached) = cache.get(&key) {
38        return cached;
39    }
40
41    let result = shortest_path_distance(graph, node_map, from, to);
42    cache.insert(key, result);
43    result
44}
45
46/// Represents one end of a netelement in the topology graph
47///
48/// Each netelement has two ends: position 0 (start) and position 1 (end).
49/// The graph treats each end as a separate node, allowing bidirectional
50/// navigation within and between netelements.
51///
52/// # Examples
53///
54/// ```
55/// use tp_lib_core::path::NetelementSide;
56///
57/// let start = NetelementSide {
58///     netelement_id: "NE_A".to_string(),
59///     position: 0,  // Start of segment
60/// };
61///
62/// let end = NetelementSide {
63///     netelement_id: "NE_A".to_string(),
64///     position: 1,  // End of segment
65/// };
66/// ```
67#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
68pub struct NetelementSide {
69    /// ID of the netelement
70    pub netelement_id: String,
71
72    /// Position on the netelement: 0 = start, 1 = end
73    pub position: u8,
74}
75
76impl NetelementSide {
77    /// Create a new netelement side
78    pub fn new(netelement_id: String, position: u8) -> Result<Self, ProjectionError> {
79        if position > 1 {
80            return Err(ProjectionError::InvalidGeometry(format!(
81                "NetelementSide position must be 0 or 1, got {}",
82                position
83            )));
84        }
85
86        Ok(Self {
87            netelement_id,
88            position,
89        })
90    }
91
92    /// Get the opposite end of this netelement
93    pub fn opposite(&self) -> Self {
94        Self {
95            netelement_id: self.netelement_id.clone(),
96            position: 1 - self.position,
97        }
98    }
99}
100
101/// Build a directed graph representing the railway network topology
102///
103/// Creates a petgraph DiGraph where:
104/// - **Nodes** are NetelementSide (each netelement has 2 nodes: start and end)
105/// - **Edges** represent navigability:
106///   - Internal edges: start→end and end→start within each netelement
107///   - External edges: connections between netelements via netrelations
108///
109/// # Graph Structure Example
110///
111/// For netelement "NE_A":
112/// - Node: NE_A position 0 (start)
113/// - Node: NE_A position 1 (end)
114/// - Internal edges: start→end (forward), end→start (backward)
115///
116/// For netrelation connecting NE_A(end) to NE_B(start) with forward navigability:
117/// - External edge: NE_A position 1 → NE_B position 0
118///
119/// # Parameters
120///
121/// - `netelements`: All track segments in the network
122/// - `netrelations`: Navigability connections between segments
123///
124/// # Returns
125///
126/// A DiGraph where edges represent valid navigation paths, and a mapping
127/// from NetelementSide to NodeIndex for efficient lookups.
128///
129/// # Examples
130///
131/// ```no_run
132/// use tp_lib_core::path::build_topology_graph;
133/// use tp_lib_core::models::{Netelement, NetRelation};
134///
135/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
136/// let netelements = vec![/* ... */];
137/// let netrelations = vec![/* ... */];
138///
139/// let (graph, node_map) = build_topology_graph(&netelements, &netrelations)?;
140///
141/// // Use graph for path finding algorithms
142/// # Ok(())
143/// # }
144/// ```
145#[allow(clippy::type_complexity)]
146pub fn build_topology_graph(
147    netelements: &[Netelement],
148    netrelations: &[NetRelation],
149) -> Result<
150    (
151        DiGraph<NetelementSide, f64>,
152        HashMap<NetelementSide, NodeIndex>,
153    ),
154    ProjectionError,
155> {
156    let mut graph = DiGraph::new();
157    let mut node_map: HashMap<NetelementSide, NodeIndex> = HashMap::new();
158
159    // Step 1: Create nodes for each netelement side
160    for netelement in netelements {
161        let start_side = NetelementSide::new(netelement.id.clone(), 0)?;
162        let end_side = NetelementSide::new(netelement.id.clone(), 1)?;
163
164        let start_node = graph.add_node(start_side.clone());
165        let end_node = graph.add_node(end_side.clone());
166
167        node_map.insert(start_side, start_node);
168        node_map.insert(end_side, end_node);
169    }
170
171    // Step 2: Create internal edges (bidirectional within each netelement)
172    // Weight = netelement's haversine length in meters
173    for netelement in netelements {
174        let start_side = NetelementSide::new(netelement.id.clone(), 0)?;
175        let end_side = NetelementSide::new(netelement.id.clone(), 1)?;
176
177        let start_node = node_map[&start_side];
178        let end_node = node_map[&end_side];
179
180        let length = netelement.geometry.haversine_length();
181
182        // Forward edge: start→end
183        graph.add_edge(start_node, end_node, length);
184
185        // Backward edge: end→start
186        graph.add_edge(end_node, start_node, length);
187    }
188
189    // Step 3: Create external edges from netrelations
190    // Weight = 0.0 (netelement connection crossing has negligible distance)
191    for netrelation in netrelations {
192        // Validate netrelation
193        netrelation.validate()?;
194
195        // Get nodes for connection points
196        let from_side = NetelementSide::new(
197            netrelation.from_netelement_id.clone(),
198            netrelation.position_on_a,
199        )?;
200        let to_side = NetelementSide::new(
201            netrelation.to_netelement_id.clone(),
202            netrelation.position_on_b,
203        )?;
204
205        // Check if nodes exist in graph (skip if referencing non-existent netelements)
206        if !node_map.contains_key(&from_side) || !node_map.contains_key(&to_side) {
207            continue;
208        }
209
210        let from_node = node_map[&from_side];
211        let to_node = node_map[&to_side];
212
213        // Add edges based on navigability
214        if netrelation.is_navigable_forward() {
215            graph.add_edge(from_node, to_node, 0.0);
216        }
217
218        if netrelation.is_navigable_backward() {
219            graph.add_edge(to_node, from_node, 0.0);
220        }
221    }
222
223    Ok((graph, node_map))
224}
225
226/// Compute the shortest-path distance between two netelement sides.
227///
228/// Uses a direction-aware Dijkstra that prevents consecutive external-edge
229/// (connection) traversals.  At netelement connections, multiple netelement sides
230/// may connect at the same point via zero-weight external edges.  Standard
231/// Dijkstra can "shortcut" through a connection (e.g. NE_B → connection → NE_C)
232/// without traversing the connecting netelement, which would represent a
233/// physically impossible direction reversal for a train.
234///
235/// Returns `None` if no path exists (disconnected components).
236pub fn shortest_path_distance(
237    graph: &DiGraph<NetelementSide, f64>,
238    node_map: &HashMap<NetelementSide, NodeIndex>,
239    from: &NetelementSide,
240    to: &NetelementSide,
241) -> Option<f64> {
242    let &from_idx = node_map.get(from)?;
243    let &to_idx = node_map.get(to)?;
244
245    direction_aware_dijkstra(graph, from_idx, to_idx).map(|(cost, _)| cost)
246}
247
248/// Return the sequence of graph nodes along the shortest direction-aware path.
249///
250/// Used by bridge insertion to recover intermediate netelements.
251pub fn shortest_path_route(
252    graph: &DiGraph<NetelementSide, f64>,
253    node_map: &HashMap<NetelementSide, NodeIndex>,
254    from: &NetelementSide,
255    to: &NetelementSide,
256) -> Option<Vec<NodeIndex>> {
257    let &from_idx = node_map.get(from)?;
258    let &to_idx = node_map.get(to)?;
259
260    direction_aware_dijkstra(graph, from_idx, to_idx).map(|(_, path)| path)
261}
262
263/// Direction-aware Dijkstra that prevents U-turns.
264///
265/// The state is expanded to `(NodeIndex, arrived_via_external)`.  When
266/// `arrived_via_external` is true, only internal edges (source and target
267/// belong to the same netelement) may be followed.  This forces the algorithm
268/// to traverse the connecting netelement before taking another external edge,
269/// modelling the physical constraint that a train cannot cross from one branch
270/// to another at a netelement connection without traversing the connecting segment.
271fn direction_aware_dijkstra(
272    graph: &DiGraph<NetelementSide, f64>,
273    from_idx: NodeIndex,
274    to_idx: NodeIndex,
275) -> Option<(f64, Vec<NodeIndex>)> {
276    if from_idx == to_idx {
277        return Some((0.0, vec![from_idx]));
278    }
279
280    #[derive(Clone, PartialEq)]
281    struct State {
282        cost: f64,
283        node: NodeIndex,
284        via_external: bool,
285    }
286
287    impl Eq for State {}
288
289    impl PartialOrd for State {
290        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
291            Some(self.cmp(other))
292        }
293    }
294
295    impl Ord for State {
296        fn cmp(&self, other: &Self) -> Ordering {
297            // min-heap: reversed comparison (higher cost = lower priority)
298            other
299                .cost
300                .partial_cmp(&self.cost)
301                .unwrap_or(Ordering::Equal)
302        }
303    }
304
305    type StateKey = (NodeIndex, bool);
306
307    let mut dist: HashMap<StateKey, f64> = HashMap::new();
308    let mut prev: HashMap<StateKey, StateKey> = HashMap::new();
309    let mut heap = BinaryHeap::new();
310
311    let start_key: StateKey = (from_idx, false);
312    dist.insert(start_key, 0.0);
313    heap.push(State {
314        cost: 0.0,
315        node: from_idx,
316        via_external: false,
317    });
318
319    let mut reached_target: Option<(f64, StateKey)> = None;
320
321    while let Some(State {
322        cost,
323        node,
324        via_external,
325    }) = heap.pop()
326    {
327        let key: StateKey = (node, via_external);
328
329        if let Some(&best) = dist.get(&key) {
330            if cost > best {
331                continue;
332            }
333        }
334
335        if node == to_idx {
336            reached_target = Some((cost, key));
337            break;
338        }
339
340        let source_ne = &graph[node].netelement_id;
341
342        for edge in graph.edges_directed(node, petgraph::Direction::Outgoing) {
343            let next = edge.target();
344            let w = *edge.weight();
345            let target_ne = &graph[next].netelement_id;
346            let edge_is_external = source_ne != target_ne;
347
348            // CONSTRAINT: after an external edge, only internal edges allowed.
349            if via_external && edge_is_external {
350                continue;
351            }
352
353            let new_cost = cost + w;
354            let next_key: StateKey = (next, edge_is_external);
355
356            if dist.get(&next_key).is_none_or(|&d| new_cost < d) {
357                dist.insert(next_key, new_cost);
358                prev.insert(next_key, key);
359                heap.push(State {
360                    cost: new_cost,
361                    node: next,
362                    via_external: edge_is_external,
363                });
364            }
365        }
366    }
367
368    let (cost, target_key) = reached_target?;
369
370    // Reconstruct path from predecessor map.
371    let mut path_keys = vec![target_key];
372    let mut current = target_key;
373    while let Some(&predecessor) = prev.get(&current) {
374        path_keys.push(predecessor);
375        current = predecessor;
376    }
377    path_keys.reverse();
378
379    let path: Vec<NodeIndex> = path_keys.iter().map(|(node, _)| *node).collect();
380
381    Some((cost, path))
382}
383
384/// Validate that all netrelations reference existing netelements
385///
386/// Checks that `from_netelement_id` and `to_netelement_id` in each netrelation
387/// correspond to actual netelements in the network. Returns a list of invalid
388/// netrelation IDs that reference non-existent netelements.
389///
390/// This function is used to detect data quality issues where netrelations
391/// reference netelements that don't exist (FR-006a).
392///
393/// # Parameters
394///
395/// - `netelements`: All track segments in the network
396/// - `netrelations`: Navigability connections to validate
397///
398/// # Returns
399///
400/// A vector of netrelation IDs that have invalid references. Empty if all are valid.
401///
402/// # Examples
403///
404/// ```no_run
405/// use tp_lib_core::path::validate_netrelation_references;
406/// use tp_lib_core::models::{Netelement, NetRelation};
407///
408/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
409/// let netelements = vec![/* ... */];
410/// let netrelations = vec![/* ... */];
411///
412/// let invalid = validate_netrelation_references(&netelements, &netrelations);
413///
414/// if !invalid.is_empty() {
415///     eprintln!("Warning: {} netrelations reference non-existent netelements", invalid.len());
416/// }
417/// # Ok(())
418/// # }
419/// ```
420pub fn validate_netrelation_references(
421    netelements: &[Netelement],
422    netrelations: &[NetRelation],
423) -> Vec<String> {
424    use std::collections::HashSet;
425
426    // Build set of valid netelement IDs for O(1) lookup
427    let netelement_ids: HashSet<&str> = netelements.iter().map(|ne| ne.id.as_str()).collect();
428
429    let mut invalid_netrelations = Vec::new();
430
431    for netrelation in netrelations {
432        let from_exists = netelement_ids.contains(netrelation.from_netelement_id.as_str());
433        let to_exists = netelement_ids.contains(netrelation.to_netelement_id.as_str());
434
435        if !from_exists || !to_exists {
436            invalid_netrelations.push(netrelation.id.clone());
437        }
438    }
439
440    invalid_netrelations
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use geo::{Coord, LineString};
447
448    fn create_test_netelement(id: &str) -> Netelement {
449        Netelement {
450            id: id.to_string(),
451            geometry: LineString::new(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 1.0, y: 1.0 }]),
452            crs: "EPSG:4326".to_string(),
453        }
454    }
455
456    #[test]
457    fn test_netelement_side_creation() {
458        let side = NetelementSide::new("NE_A".to_string(), 0);
459        assert!(side.is_ok());
460
461        let side = NetelementSide::new("NE_A".to_string(), 1);
462        assert!(side.is_ok());
463
464        let side = NetelementSide::new("NE_A".to_string(), 2);
465        assert!(side.is_err());
466    }
467
468    #[test]
469    fn test_netelement_side_opposite() {
470        let start = NetelementSide::new("NE_A".to_string(), 0).unwrap();
471        let end = start.opposite();
472
473        assert_eq!(end.position, 1);
474        assert_eq!(end.netelement_id, "NE_A");
475
476        let back_to_start = end.opposite();
477        assert_eq!(back_to_start.position, 0);
478    }
479
480    #[test]
481    fn test_build_graph_single_netelement() {
482        let netelements = vec![create_test_netelement("NE_A")];
483        let netrelations = vec![];
484
485        let result = build_topology_graph(&netelements, &netrelations);
486        assert!(result.is_ok());
487
488        let (graph, node_map) = result.unwrap();
489
490        // Should have 2 nodes (start and end)
491        assert_eq!(graph.node_count(), 2);
492
493        // Should have 2 internal edges (start→end, end→start)
494        assert_eq!(graph.edge_count(), 2);
495
496        // Verify nodes exist
497        let start_side = NetelementSide::new("NE_A".to_string(), 0).unwrap();
498        let end_side = NetelementSide::new("NE_A".to_string(), 1).unwrap();
499
500        assert!(node_map.contains_key(&start_side));
501        assert!(node_map.contains_key(&end_side));
502    }
503
504    #[test]
505    fn test_build_graph_with_netrelation() {
506        let netelements = vec![
507            create_test_netelement("NE_A"),
508            create_test_netelement("NE_B"),
509        ];
510
511        let netrelation = NetRelation::new(
512            "NR001".to_string(),
513            "NE_A".to_string(),
514            "NE_B".to_string(),
515            1,     // Connect end of A
516            0,     // to start of B
517            true,  // Forward navigable
518            false, // Not backward navigable
519        )
520        .unwrap();
521
522        let netrelations = vec![netrelation];
523
524        let result = build_topology_graph(&netelements, &netrelations);
525        assert!(result.is_ok());
526
527        let (graph, _node_map) = result.unwrap();
528
529        // Should have 4 nodes (2 netelements × 2 ends each)
530        assert_eq!(graph.node_count(), 4);
531
532        // Should have 4 internal edges + 1 external edge = 5 total
533        assert_eq!(graph.edge_count(), 5);
534    }
535
536    #[test]
537    fn test_build_graph_bidirectional_netrelation() {
538        let netelements = vec![
539            create_test_netelement("NE_A"),
540            create_test_netelement("NE_B"),
541        ];
542
543        let netrelation = NetRelation::new(
544            "NR001".to_string(),
545            "NE_A".to_string(),
546            "NE_B".to_string(),
547            1,    // Connect end of A
548            0,    // to start of B
549            true, // Forward navigable
550            true, // Backward navigable
551        )
552        .unwrap();
553
554        let netrelations = vec![netrelation];
555
556        let result = build_topology_graph(&netelements, &netrelations);
557        assert!(result.is_ok());
558
559        let (graph, _node_map) = result.unwrap();
560
561        // Should have 4 nodes
562        assert_eq!(graph.node_count(), 4);
563
564        // Should have 4 internal edges + 2 external edges = 6 total
565        assert_eq!(graph.edge_count(), 6);
566    }
567
568    #[test]
569    fn test_build_graph_missing_netelement_reference() {
570        let netelements = vec![create_test_netelement("NE_A")];
571
572        let netrelation = NetRelation::new(
573            "NR001".to_string(),
574            "NE_A".to_string(),
575            "NE_MISSING".to_string(), // References non-existent netelement
576            1,
577            0,
578            true,
579            false,
580        )
581        .unwrap();
582
583        let netrelations = vec![netrelation];
584
585        let result = build_topology_graph(&netelements, &netrelations);
586        assert!(result.is_ok()); // Should not fail, just skip invalid reference
587
588        let (graph, _node_map) = result.unwrap();
589
590        // Should have 2 nodes (only NE_A)
591        assert_eq!(graph.node_count(), 2);
592
593        // Should have only 2 internal edges (no external edge added)
594        assert_eq!(graph.edge_count(), 2);
595    }
596
597    #[test]
598    fn test_shortest_path_same_netelement() {
599        let netelements = vec![create_test_netelement("NE_A")];
600        let (graph, node_map) = build_topology_graph(&netelements, &[]).unwrap();
601
602        let from = NetelementSide::new("NE_A".to_string(), 0).unwrap();
603        let to = NetelementSide::new("NE_A".to_string(), 1).unwrap();
604
605        let dist = shortest_path_distance(&graph, &node_map, &from, &to);
606        assert!(dist.is_some());
607        assert!(dist.unwrap() > 0.0);
608    }
609
610    #[test]
611    fn test_shortest_path_across_netelement_connection() {
612        let netelements = vec![
613            create_test_netelement("NE_A"),
614            create_test_netelement("NE_B"),
615        ];
616
617        let netrelation = NetRelation::new(
618            "NR001".to_string(),
619            "NE_A".to_string(),
620            "NE_B".to_string(),
621            1,
622            0,
623            true,
624            false,
625        )
626        .unwrap();
627
628        let (graph, node_map) = build_topology_graph(&netelements, &[netrelation]).unwrap();
629
630        // Route: NE_A:0 → NE_A:1 → (connection 0.0) → NE_B:0 → NE_B:1
631        let from = NetelementSide::new("NE_A".to_string(), 0).unwrap();
632        let to = NetelementSide::new("NE_B".to_string(), 1).unwrap();
633
634        let dist = shortest_path_distance(&graph, &node_map, &from, &to);
635        assert!(dist.is_some());
636        // Should be length(NE_A) + 0 + length(NE_B) — both have same geometry
637        let ne_a_len = netelements[0].geometry.haversine_length();
638        let ne_b_len = netelements[1].geometry.haversine_length();
639        let expected = ne_a_len + ne_b_len;
640        assert!((dist.unwrap() - expected).abs() < 0.1);
641    }
642
643    #[test]
644    fn test_shortest_path_disconnected() {
645        let netelements = vec![
646            create_test_netelement("NE_A"),
647            create_test_netelement("NE_B"),
648        ];
649        // No netrelation → disconnected
650        let (graph, node_map) = build_topology_graph(&netelements, &[]).unwrap();
651
652        let from = NetelementSide::new("NE_A".to_string(), 0).unwrap();
653        let to = NetelementSide::new("NE_B".to_string(), 0).unwrap();
654
655        assert!(shortest_path_distance(&graph, &node_map, &from, &to).is_none());
656    }
657
658    #[test]
659    fn test_direction_aware_dijkstra_no_u_turns() {
660        // Y-shaped junction: NE_A and NE_B both connect to NE_X at position 0.
661        //
662        //   NE_A:0 ── NE_A:1 ──ext──► NE_X:0 ── NE_X:1 ──ext──► NE_B:0 ── NE_B:1
663        //                                ▲                                      │
664        //                                └────────────ext─────────────────────  ┘
665        //
666        // Without U-turn prevention the shortcut NE_A:1→NE_X:0→NE_B:1 would
667        // skip NE_X entirely (two consecutive external edges).
668        // The direction-aware Dijkstra must force traversal through NE_X.
669
670        let netelements = vec![
671            create_test_netelement("NE_A"),
672            create_test_netelement("NE_X"),
673            create_test_netelement("NE_B"),
674        ];
675
676        let netrelations = vec![
677            // NE_A:1 ↔ NE_X:0
678            NetRelation::new(
679                "NR1".to_string(),
680                "NE_A".to_string(),
681                "NE_X".to_string(),
682                1,
683                0,
684                true,
685                true,
686            )
687            .unwrap(),
688            // NE_X:1 ↔ NE_B:0
689            NetRelation::new(
690                "NR2".to_string(),
691                "NE_X".to_string(),
692                "NE_B".to_string(),
693                1,
694                0,
695                true,
696                true,
697            )
698            .unwrap(),
699            // NE_B:1 ↔ NE_X:0  (creates the U-turn shortcut)
700            NetRelation::new(
701                "NR3".to_string(),
702                "NE_B".to_string(),
703                "NE_X".to_string(),
704                1,
705                0,
706                true,
707                true,
708            )
709            .unwrap(),
710        ];
711
712        let (graph, node_map) = build_topology_graph(&netelements, &netrelations).unwrap();
713
714        let from = NetelementSide::new("NE_A".to_string(), 0).unwrap();
715        let to = NetelementSide::new("NE_B".to_string(), 0).unwrap();
716
717        let path = shortest_path_route(&graph, &node_map, &from, &to);
718        assert!(path.is_some(), "A path should exist");
719
720        let path = path.unwrap();
721
722        // Verify no U-turns: no two consecutive edges may both be external.
723        // An edge is external when its source and target belong to different netelements.
724        for window in path.windows(3) {
725            let a = &graph[window[0]];
726            let b = &graph[window[1]];
727            let c = &graph[window[2]];
728
729            let ab_external = a.netelement_id != b.netelement_id;
730            let bc_external = b.netelement_id != c.netelement_id;
731
732            assert!(
733                !(ab_external && bc_external),
734                "U-turn detected: {}:{} → {}:{} → {}:{} has consecutive external edges",
735                a.netelement_id,
736                a.position,
737                b.netelement_id,
738                b.position,
739                c.netelement_id,
740                c.position,
741            );
742        }
743    }
744}