use crate::errors::ProjectionError;
use crate::models::{NetRelation, Netelement};
use geo::HaversineLength;
use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::visit::EdgeRef;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};
pub type ShortestPathCache = HashMap<(String, u8, String, u8), Option<f64>>;
pub fn cached_shortest_path_distance(
cache: &mut ShortestPathCache,
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
from: &NetelementSide,
to: &NetelementSide,
) -> Option<f64> {
let key = (
from.netelement_id.clone(),
from.position,
to.netelement_id.clone(),
to.position,
);
if let Some(&cached) = cache.get(&key) {
return cached;
}
let result = shortest_path_distance(graph, node_map, from, to);
cache.insert(key, result);
result
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NetelementSide {
pub netelement_id: String,
pub position: u8,
}
impl NetelementSide {
pub fn new(netelement_id: String, position: u8) -> Result<Self, ProjectionError> {
if position > 1 {
return Err(ProjectionError::InvalidGeometry(format!(
"NetelementSide position must be 0 or 1, got {}",
position
)));
}
Ok(Self {
netelement_id,
position,
})
}
pub fn opposite(&self) -> Self {
Self {
netelement_id: self.netelement_id.clone(),
position: 1 - self.position,
}
}
}
#[allow(clippy::type_complexity)]
pub fn build_topology_graph(
netelements: &[Netelement],
netrelations: &[NetRelation],
) -> Result<
(
DiGraph<NetelementSide, f64>,
HashMap<NetelementSide, NodeIndex>,
),
ProjectionError,
> {
let mut graph = DiGraph::new();
let mut node_map: HashMap<NetelementSide, NodeIndex> = HashMap::new();
for netelement in netelements {
let start_side = NetelementSide::new(netelement.id.clone(), 0)?;
let end_side = NetelementSide::new(netelement.id.clone(), 1)?;
let start_node = graph.add_node(start_side.clone());
let end_node = graph.add_node(end_side.clone());
node_map.insert(start_side, start_node);
node_map.insert(end_side, end_node);
}
for netelement in netelements {
let start_side = NetelementSide::new(netelement.id.clone(), 0)?;
let end_side = NetelementSide::new(netelement.id.clone(), 1)?;
let start_node = node_map[&start_side];
let end_node = node_map[&end_side];
let length = netelement.geometry.haversine_length();
graph.add_edge(start_node, end_node, length);
graph.add_edge(end_node, start_node, length);
}
for netrelation in netrelations {
netrelation.validate()?;
let from_side = NetelementSide::new(
netrelation.from_netelement_id.clone(),
netrelation.position_on_a,
)?;
let to_side = NetelementSide::new(
netrelation.to_netelement_id.clone(),
netrelation.position_on_b,
)?;
if !node_map.contains_key(&from_side) || !node_map.contains_key(&to_side) {
continue;
}
let from_node = node_map[&from_side];
let to_node = node_map[&to_side];
if netrelation.is_navigable_forward() {
graph.add_edge(from_node, to_node, 0.0);
}
if netrelation.is_navigable_backward() {
graph.add_edge(to_node, from_node, 0.0);
}
}
Ok((graph, node_map))
}
pub fn shortest_path_distance(
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
from: &NetelementSide,
to: &NetelementSide,
) -> Option<f64> {
let &from_idx = node_map.get(from)?;
let &to_idx = node_map.get(to)?;
direction_aware_dijkstra(graph, from_idx, to_idx).map(|(cost, _)| cost)
}
pub fn shortest_path_route(
graph: &DiGraph<NetelementSide, f64>,
node_map: &HashMap<NetelementSide, NodeIndex>,
from: &NetelementSide,
to: &NetelementSide,
) -> Option<Vec<NodeIndex>> {
let &from_idx = node_map.get(from)?;
let &to_idx = node_map.get(to)?;
direction_aware_dijkstra(graph, from_idx, to_idx).map(|(_, path)| path)
}
fn direction_aware_dijkstra(
graph: &DiGraph<NetelementSide, f64>,
from_idx: NodeIndex,
to_idx: NodeIndex,
) -> Option<(f64, Vec<NodeIndex>)> {
if from_idx == to_idx {
return Some((0.0, vec![from_idx]));
}
#[derive(Clone, PartialEq)]
struct State {
cost: f64,
node: NodeIndex,
via_external: bool,
}
impl Eq for State {}
impl PartialOrd for State {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for State {
fn cmp(&self, other: &Self) -> Ordering {
other
.cost
.partial_cmp(&self.cost)
.unwrap_or(Ordering::Equal)
}
}
type StateKey = (NodeIndex, bool);
let mut dist: HashMap<StateKey, f64> = HashMap::new();
let mut prev: HashMap<StateKey, StateKey> = HashMap::new();
let mut heap = BinaryHeap::new();
let start_key: StateKey = (from_idx, false);
dist.insert(start_key, 0.0);
heap.push(State {
cost: 0.0,
node: from_idx,
via_external: false,
});
let mut reached_target: Option<(f64, StateKey)> = None;
while let Some(State {
cost,
node,
via_external,
}) = heap.pop()
{
let key: StateKey = (node, via_external);
if let Some(&best) = dist.get(&key) {
if cost > best {
continue;
}
}
if node == to_idx {
reached_target = Some((cost, key));
break;
}
let source_ne = &graph[node].netelement_id;
for edge in graph.edges_directed(node, petgraph::Direction::Outgoing) {
let next = edge.target();
let w = *edge.weight();
let target_ne = &graph[next].netelement_id;
let edge_is_external = source_ne != target_ne;
if via_external && edge_is_external {
continue;
}
let new_cost = cost + w;
let next_key: StateKey = (next, edge_is_external);
if dist.get(&next_key).is_none_or(|&d| new_cost < d) {
dist.insert(next_key, new_cost);
prev.insert(next_key, key);
heap.push(State {
cost: new_cost,
node: next,
via_external: edge_is_external,
});
}
}
}
let (cost, target_key) = reached_target?;
let mut path_keys = vec![target_key];
let mut current = target_key;
while let Some(&predecessor) = prev.get(¤t) {
path_keys.push(predecessor);
current = predecessor;
}
path_keys.reverse();
let path: Vec<NodeIndex> = path_keys.iter().map(|(node, _)| *node).collect();
Some((cost, path))
}
pub fn validate_netrelation_references(
netelements: &[Netelement],
netrelations: &[NetRelation],
) -> Vec<String> {
use std::collections::HashSet;
let netelement_ids: HashSet<&str> = netelements.iter().map(|ne| ne.id.as_str()).collect();
let mut invalid_netrelations = Vec::new();
for netrelation in netrelations {
let from_exists = netelement_ids.contains(netrelation.from_netelement_id.as_str());
let to_exists = netelement_ids.contains(netrelation.to_netelement_id.as_str());
if !from_exists || !to_exists {
invalid_netrelations.push(netrelation.id.clone());
}
}
invalid_netrelations
}
#[cfg(test)]
mod tests {
use super::*;
use geo::{Coord, LineString};
fn create_test_netelement(id: &str) -> Netelement {
Netelement {
id: id.to_string(),
geometry: LineString::new(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 1.0, y: 1.0 }]),
crs: "EPSG:4326".to_string(),
}
}
#[test]
fn test_netelement_side_creation() {
let side = NetelementSide::new("NE_A".to_string(), 0);
assert!(side.is_ok());
let side = NetelementSide::new("NE_A".to_string(), 1);
assert!(side.is_ok());
let side = NetelementSide::new("NE_A".to_string(), 2);
assert!(side.is_err());
}
#[test]
fn test_netelement_side_opposite() {
let start = NetelementSide::new("NE_A".to_string(), 0).unwrap();
let end = start.opposite();
assert_eq!(end.position, 1);
assert_eq!(end.netelement_id, "NE_A");
let back_to_start = end.opposite();
assert_eq!(back_to_start.position, 0);
}
#[test]
fn test_build_graph_single_netelement() {
let netelements = vec![create_test_netelement("NE_A")];
let netrelations = vec![];
let result = build_topology_graph(&netelements, &netrelations);
assert!(result.is_ok());
let (graph, node_map) = result.unwrap();
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 2);
let start_side = NetelementSide::new("NE_A".to_string(), 0).unwrap();
let end_side = NetelementSide::new("NE_A".to_string(), 1).unwrap();
assert!(node_map.contains_key(&start_side));
assert!(node_map.contains_key(&end_side));
}
#[test]
fn test_build_graph_with_netrelation() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
];
let netrelation = NetRelation::new(
"NR001".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1, 0, true, false, )
.unwrap();
let netrelations = vec![netrelation];
let result = build_topology_graph(&netelements, &netrelations);
assert!(result.is_ok());
let (graph, _node_map) = result.unwrap();
assert_eq!(graph.node_count(), 4);
assert_eq!(graph.edge_count(), 5);
}
#[test]
fn test_build_graph_bidirectional_netrelation() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
];
let netrelation = NetRelation::new(
"NR001".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1, 0, true, true, )
.unwrap();
let netrelations = vec![netrelation];
let result = build_topology_graph(&netelements, &netrelations);
assert!(result.is_ok());
let (graph, _node_map) = result.unwrap();
assert_eq!(graph.node_count(), 4);
assert_eq!(graph.edge_count(), 6);
}
#[test]
fn test_build_graph_missing_netelement_reference() {
let netelements = vec![create_test_netelement("NE_A")];
let netrelation = NetRelation::new(
"NR001".to_string(),
"NE_A".to_string(),
"NE_MISSING".to_string(), 1,
0,
true,
false,
)
.unwrap();
let netrelations = vec![netrelation];
let result = build_topology_graph(&netelements, &netrelations);
assert!(result.is_ok());
let (graph, _node_map) = result.unwrap();
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 2);
}
#[test]
fn test_shortest_path_same_netelement() {
let netelements = vec![create_test_netelement("NE_A")];
let (graph, node_map) = build_topology_graph(&netelements, &[]).unwrap();
let from = NetelementSide::new("NE_A".to_string(), 0).unwrap();
let to = NetelementSide::new("NE_A".to_string(), 1).unwrap();
let dist = shortest_path_distance(&graph, &node_map, &from, &to);
assert!(dist.is_some());
assert!(dist.unwrap() > 0.0);
}
#[test]
fn test_shortest_path_across_netelement_connection() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
];
let netrelation = NetRelation::new(
"NR001".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1,
0,
true,
false,
)
.unwrap();
let (graph, node_map) = build_topology_graph(&netelements, &[netrelation]).unwrap();
let from = NetelementSide::new("NE_A".to_string(), 0).unwrap();
let to = NetelementSide::new("NE_B".to_string(), 1).unwrap();
let dist = shortest_path_distance(&graph, &node_map, &from, &to);
assert!(dist.is_some());
let ne_a_len = netelements[0].geometry.haversine_length();
let ne_b_len = netelements[1].geometry.haversine_length();
let expected = ne_a_len + ne_b_len;
assert!((dist.unwrap() - expected).abs() < 0.1);
}
#[test]
fn test_shortest_path_disconnected() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
];
let (graph, node_map) = build_topology_graph(&netelements, &[]).unwrap();
let from = NetelementSide::new("NE_A".to_string(), 0).unwrap();
let to = NetelementSide::new("NE_B".to_string(), 0).unwrap();
assert!(shortest_path_distance(&graph, &node_map, &from, &to).is_none());
}
#[test]
fn test_direction_aware_dijkstra_no_u_turns() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_X"),
create_test_netelement("NE_B"),
];
let netrelations = vec![
NetRelation::new(
"NR1".to_string(),
"NE_A".to_string(),
"NE_X".to_string(),
1,
0,
true,
true,
)
.unwrap(),
NetRelation::new(
"NR2".to_string(),
"NE_X".to_string(),
"NE_B".to_string(),
1,
0,
true,
true,
)
.unwrap(),
NetRelation::new(
"NR3".to_string(),
"NE_B".to_string(),
"NE_X".to_string(),
1,
0,
true,
true,
)
.unwrap(),
];
let (graph, node_map) = build_topology_graph(&netelements, &netrelations).unwrap();
let from = NetelementSide::new("NE_A".to_string(), 0).unwrap();
let to = NetelementSide::new("NE_B".to_string(), 0).unwrap();
let path = shortest_path_route(&graph, &node_map, &from, &to);
assert!(path.is_some(), "A path should exist");
let path = path.unwrap();
for window in path.windows(3) {
let a = &graph[window[0]];
let b = &graph[window[1]];
let c = &graph[window[2]];
let ab_external = a.netelement_id != b.netelement_id;
let bc_external = b.netelement_id != c.netelement_id;
assert!(
!(ab_external && bc_external),
"U-turn detected: {}:{} → {}:{} → {}:{} has consecutive external edges",
a.netelement_id,
a.position,
b.netelement_id,
b.position,
c.netelement_id,
c.position,
);
}
}
}