tp_lib_core/projection/
spatial.rs

1//! Spatial indexing for efficient nearest-netelement queries
2
3use crate::models::Netelement;
4use crate::errors::ProjectionError;
5use geo::Point;
6use rstar::{RTree, RTreeObject, AABB};
7
8/// Wrapper for netelement entries in the R-tree with bounding box
9#[derive(Debug, Clone)]
10struct NetelementIndexEntry {
11    /// Index of the netelement in the original Vec<Netelement>
12    index: usize,
13    /// Bounding box of the netelement geometry (min/max lon/lat)
14    bbox: AABB<[f64; 2]>,
15}
16
17impl RTreeObject for NetelementIndexEntry {
18    type Envelope = AABB<[f64; 2]>;
19    
20    fn envelope(&self) -> Self::Envelope {
21        self.bbox
22    }
23}
24
25/// Spatial index for netelements using R-tree
26pub struct NetworkIndex {
27    tree: RTree<NetelementIndexEntry>,
28    netelements: Vec<Netelement>,
29}
30
31impl NetworkIndex {
32    /// Build spatial index from netelements
33    pub fn new(netelements: Vec<Netelement>) -> Result<Self, ProjectionError> {
34        if netelements.is_empty() {
35            return Err(ProjectionError::EmptyNetwork);
36        }
37        
38        // Build R-tree entries with bounding boxes
39        let mut entries = Vec::new();
40        for (index, netelement) in netelements.iter().enumerate() {
41            let coords = &netelement.geometry.0;
42            if coords.is_empty() {
43                continue; // Skip empty geometries
44            }
45            
46            // Calculate bounding box
47            let mut min_x = f64::MAX;
48            let mut max_x = f64::MIN;
49            let mut min_y = f64::MAX;
50            let mut max_y = f64::MIN;
51            
52            for coord in coords {
53                min_x = min_x.min(coord.x);
54                max_x = max_x.max(coord.x);
55                min_y = min_y.min(coord.y);
56                max_y = max_y.max(coord.y);
57            }
58            
59            let bbox = AABB::from_corners([min_x, min_y], [max_x, max_y]);
60            entries.push(NetelementIndexEntry { index, bbox });
61        }
62        
63        let tree = RTree::bulk_load(entries);
64        
65        Ok(Self {
66            tree,
67            netelements,
68        })
69    }
70    
71    /// Get a reference to the netelements
72    pub fn netelements(&self) -> &[Netelement] {
73        &self.netelements
74    }
75}
76
77/// Find the nearest netelement to a given point using R-tree spatial index
78pub fn find_nearest_netelement(
79    point: &Point<f64>,
80    index: &NetworkIndex,
81) -> Result<usize, ProjectionError> {
82    if index.netelements.is_empty() {
83        return Err(ProjectionError::EmptyNetwork);
84    }
85    
86    // Use R-tree to find candidates near the point
87    // We use locate_in_envelope_intersecting to find entries whose bbox contains or is near the point
88    let query_point = [point.x(), point.y()];
89    
90    // Find the nearest entry by checking distance to all candidates
91    // For MVP, we'll use a simple linear search over candidates near the point
92    let mut best_index = 0;
93    let mut best_distance = f64::MAX;
94    
95    // Get entries that intersect with a small envelope around the point
96    let epsilon = 0.01; // ~1km search radius
97    let search_envelope = AABB::from_corners(
98        [query_point[0] - epsilon, query_point[1] - epsilon],
99        [query_point[0] + epsilon, query_point[1] + epsilon],
100    );
101    
102    // Check all entries in the search envelope
103    for entry in index.tree.locate_in_envelope_intersecting(&search_envelope) {
104        // Calculate center of bounding box as proxy for distance
105        let bbox_center_x = (entry.bbox.lower()[0] + entry.bbox.upper()[0]) / 2.0;
106        let bbox_center_y = (entry.bbox.lower()[1] + entry.bbox.upper()[1]) / 2.0;
107        
108        let dx = bbox_center_x - query_point[0];
109        let dy = bbox_center_y - query_point[1];
110        let distance = (dx * dx + dy * dy).sqrt();
111        
112        if distance < best_distance {
113            best_distance = distance;
114            best_index = entry.index;
115        }
116    }
117    
118    // If no entries found in envelope, fall back to first netelement
119    if best_distance == f64::MAX {
120        best_index = 0;
121    }
122    
123    Ok(best_index)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use geo::{LineString, Coord};
130
131    #[test]
132    fn test_network_index_empty() {
133        let result = NetworkIndex::new(vec![]);
134        assert!(result.is_err());
135        if let Err(ProjectionError::EmptyNetwork) = result {
136            // Expected
137        } else {
138            panic!("Expected EmptyNetwork error");
139        }
140    }
141
142    #[test]
143    fn test_network_index_single_netelement() {
144        let linestring = LineString::from(vec![
145            Coord { x: 4.0, y: 50.0 },
146            Coord { x: 5.0, y: 50.0 },
147        ]);
148        let netelement = Netelement::new(
149            "NE1".to_string(),
150            linestring,
151            "EPSG:4326".to_string(),
152        ).unwrap();
153        
154        let index = NetworkIndex::new(vec![netelement]);
155        assert!(index.is_ok());
156        let index = index.unwrap();
157        assert_eq!(index.netelements().len(), 1);
158    }
159
160    #[test]
161    fn test_find_nearest_netelement() {
162        // Create two netelements
163        let linestring1 = LineString::from(vec![
164            Coord { x: 4.0, y: 50.0 },
165            Coord { x: 5.0, y: 50.0 },
166        ]);
167        let netelement1 = Netelement::new(
168            "NE1".to_string(),
169            linestring1,
170            "EPSG:4326".to_string(),
171        ).unwrap();
172        
173        let linestring2 = LineString::from(vec![
174            Coord { x: 6.0, y: 51.0 },
175            Coord { x: 7.0, y: 51.0 },
176        ]);
177        let netelement2 = Netelement::new(
178            "NE2".to_string(),
179            linestring2,
180            "EPSG:4326".to_string(),
181        ).unwrap();
182        
183        let index = NetworkIndex::new(vec![netelement1, netelement2]).unwrap();
184        
185        // Point closer to first netelement
186        let point1 = Point::new(4.5, 50.0);
187        let nearest1 = find_nearest_netelement(&point1, &index).unwrap();
188        assert_eq!(nearest1, 0, "Point should be nearest to first netelement");
189        
190        // Point closer to second netelement
191        let point2 = Point::new(6.5, 51.0);
192        let nearest2 = find_nearest_netelement(&point2, &index).unwrap();
193        assert_eq!(nearest2, 1, "Point should be nearest to second netelement");
194    }
195}