1use 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
15pub type ShortestPathCache = HashMap<(String, u8, String, u8), Option<f64>>;
20
21pub 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
68pub struct NetelementSide {
69 pub netelement_id: String,
71
72 pub position: u8,
74}
75
76impl NetelementSide {
77 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 pub fn opposite(&self) -> Self {
94 Self {
95 netelement_id: self.netelement_id.clone(),
96 position: 1 - self.position,
97 }
98 }
99}
100
101#[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 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 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 graph.add_edge(start_node, end_node, length);
184
185 graph.add_edge(end_node, start_node, length);
187 }
188
189 for netrelation in netrelations {
192 netrelation.validate()?;
194
195 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 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 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
226pub 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
248pub 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
263fn 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 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 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 let mut path_keys = vec![target_key];
372 let mut current = target_key;
373 while let Some(&predecessor) = prev.get(¤t) {
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
384pub fn validate_netrelation_references(
421 netelements: &[Netelement],
422 netrelations: &[NetRelation],
423) -> Vec<String> {
424 use std::collections::HashSet;
425
426 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 assert_eq!(graph.node_count(), 2);
492
493 assert_eq!(graph.edge_count(), 2);
495
496 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, 0, true, false, )
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 assert_eq!(graph.node_count(), 4);
531
532 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, 0, true, true, )
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 assert_eq!(graph.node_count(), 4);
563
564 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(), 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()); let (graph, _node_map) = result.unwrap();
589
590 assert_eq!(graph.node_count(), 2);
592
593 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 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 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 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 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 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 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 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 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}