use crate::errors::ProjectionError;
use crate::models::Netelement;
use geo::Point;
use rstar::{PointDistance, RTree, RTreeObject, AABB};
#[derive(Debug, Clone)]
struct NetelementIndexEntry {
index: usize,
bbox: AABB<[f64; 2]>,
segments: Vec<([f64; 2], [f64; 2])>,
}
impl RTreeObject for NetelementIndexEntry {
type Envelope = AABB<[f64; 2]>;
fn envelope(&self) -> Self::Envelope {
self.bbox
}
}
#[inline]
fn point_to_segment_dist_2(p: &[f64; 2], a: &[f64; 2], b: &[f64; 2]) -> f64 {
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let len_sq = dx * dx + dy * dy;
let (px, py) = if len_sq == 0.0 {
(a[0], a[1])
} else {
let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len_sq;
let t = t.clamp(0.0, 1.0);
(a[0] + t * dx, a[1] + t * dy)
};
let ex = p[0] - px;
let ey = p[1] - py;
ex * ex + ey * ey
}
impl PointDistance for NetelementIndexEntry {
fn distance_2(&self, point: &[f64; 2]) -> f64 {
self.segments
.iter()
.map(|(a, b)| point_to_segment_dist_2(point, a, b))
.fold(f64::MAX, f64::min)
}
}
#[derive(Clone)]
pub struct NetworkIndex {
tree: RTree<NetelementIndexEntry>,
netelements: Vec<Netelement>,
}
impl NetworkIndex {
pub fn new(netelements: Vec<Netelement>) -> Result<Self, ProjectionError> {
if netelements.is_empty() {
return Err(ProjectionError::EmptyNetwork);
}
let mut entries = Vec::new();
for (index, netelement) in netelements.iter().enumerate() {
let coords = &netelement.geometry.0;
if coords.is_empty() {
continue; }
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
for coord in coords {
min_x = min_x.min(coord.x);
max_x = max_x.max(coord.x);
min_y = min_y.min(coord.y);
max_y = max_y.max(coord.y);
}
let bbox = AABB::from_corners([min_x, min_y], [max_x, max_y]);
let segments: Vec<([f64; 2], [f64; 2])> = coords
.windows(2)
.map(|w| ([w[0].x, w[0].y], [w[1].x, w[1].y]))
.collect();
entries.push(NetelementIndexEntry {
index,
bbox,
segments,
});
}
let tree = RTree::bulk_load(entries);
Ok(Self { tree, netelements })
}
pub fn netelements(&self) -> &[Netelement] {
&self.netelements
}
}
pub fn find_nearest_netelement(
point: &Point<f64>,
index: &NetworkIndex,
) -> Result<usize, ProjectionError> {
if index.netelements.is_empty() {
return Err(ProjectionError::EmptyNetwork);
}
let query_point = [point.x(), point.y()];
let nearest_entry = index.tree.nearest_neighbor(&query_point).ok_or_else(|| {
ProjectionError::InvalidGeometry("Could not find nearest netelement".to_string())
})?;
Ok(nearest_entry.index)
}
#[cfg(test)]
mod tests {
use super::*;
use geo::{Coord, LineString};
#[test]
fn test_network_index_empty() {
let result = NetworkIndex::new(vec![]);
assert!(result.is_err());
if let Err(ProjectionError::EmptyNetwork) = result {
} else {
panic!("Expected EmptyNetwork error");
}
}
#[test]
fn test_network_index_single_netelement() {
let linestring =
LineString::from(vec![Coord { x: 4.0, y: 50.0 }, Coord { x: 5.0, y: 50.0 }]);
let netelement =
Netelement::new("NE1".to_string(), linestring, "EPSG:4326".to_string()).unwrap();
let index = NetworkIndex::new(vec![netelement]);
assert!(index.is_ok());
let index = index.unwrap();
assert_eq!(index.netelements().len(), 1);
}
#[test]
fn test_find_nearest_netelement() {
let linestring1 =
LineString::from(vec![Coord { x: 4.0, y: 50.0 }, Coord { x: 5.0, y: 50.0 }]);
let netelement1 =
Netelement::new("NE1".to_string(), linestring1, "EPSG:4326".to_string()).unwrap();
let linestring2 =
LineString::from(vec![Coord { x: 6.0, y: 51.0 }, Coord { x: 7.0, y: 51.0 }]);
let netelement2 =
Netelement::new("NE2".to_string(), linestring2, "EPSG:4326".to_string()).unwrap();
let index = NetworkIndex::new(vec![netelement1, netelement2]).unwrap();
let point1 = Point::new(4.5, 50.0);
let nearest1 = find_nearest_netelement(&point1, &index).unwrap();
assert_eq!(nearest1, 0, "Point should be nearest to first netelement");
let point2 = Point::new(6.5, 51.0);
let nearest2 = find_nearest_netelement(&point2, &index).unwrap();
assert_eq!(nearest2, 1, "Point should be nearest to second netelement");
}
#[test]
fn test_find_nearest_with_overlapping_bboxes() {
let ls_a = LineString::from(vec![Coord { x: 4.0, y: 51.0 }, Coord { x: 6.0, y: 51.0 }]);
let ne_a = Netelement::new("A".to_string(), ls_a, "EPSG:4326".to_string()).unwrap();
let ls_b = LineString::from(vec![Coord { x: 5.5, y: 50.8 }, Coord { x: 5.5, y: 51.2 }]);
let ne_b = Netelement::new("B".to_string(), ls_b, "EPSG:4326".to_string()).unwrap();
let index = NetworkIndex::new(vec![ne_a, ne_b]).unwrap();
let query = Point::new(5.0, 50.99);
let nearest = find_nearest_netelement(&query, &index).unwrap();
assert_eq!(
nearest, 0,
"Point at (5.0, 50.99) should be nearest to netelement A (idx 0), not B (idx 1)"
);
}
#[test]
fn test_point_to_segment_dist_2_midpoint() {
let p = [0.0, 1.0];
let a = [0.0, 0.0];
let b = [2.0, 0.0];
let d = point_to_segment_dist_2(&p, &a, &b);
assert!((d - 1.0).abs() < 1e-12, "expected 1.0, got {}", d);
}
#[test]
fn test_point_to_segment_dist_2_beyond_endpoint() {
let p = [3.0, 0.0];
let a = [0.0, 0.0];
let b = [2.0, 0.0];
let d = point_to_segment_dist_2(&p, &a, &b);
assert!((d - 1.0).abs() < 1e-12, "expected 1.0, got {}", d);
}
}