Skip to main content

spatial_narrative/index/
spatial.rs

1//! Spatial indexing using R-tree for efficient geographic queries.
2//!
3//! This module provides spatial indexing capabilities using the `rstar` crate,
4//! enabling fast queries like:
5//! - Bounding box queries
6//! - Radius (distance) queries  
7//! - K-nearest neighbor searches
8//!
9//! # Example
10//!
11//! ```rust
12//! use spatial_narrative::index::SpatialIndex;
13//! use spatial_narrative::core::Location;
14//!
15//! // Build spatial index
16//! let mut index: SpatialIndex<&str> = SpatialIndex::new();
17//! index.insert("NYC", &Location::new(40.7128, -74.0060));
18//! index.insert("LA", &Location::new(34.0522, -118.2437));
19//! index.insert("Chicago", &Location::new(41.8781, -87.6298));
20//!
21//! // Query events within a bounding box
22//! let results = index.query_bbox(39.0, -120.0, 42.0, -70.0);
23//! assert!(!results.is_empty());
24//! ```
25
26use crate::core::{GeoBounds, Location};
27use rstar::{PointDistance, RTree, RTreeObject, AABB};
28
29/// A wrapper that makes Location compatible with R-tree indexing.
30#[derive(Debug, Clone)]
31pub struct IndexedLocation {
32    /// The geographic location
33    pub location: Location,
34    /// Optional associated data index
35    pub index: usize,
36}
37
38impl IndexedLocation {
39    /// Create a new indexed location.
40    pub fn new(location: Location, index: usize) -> Self {
41        Self { location, index }
42    }
43}
44
45impl RTreeObject for IndexedLocation {
46    type Envelope = AABB<[f64; 2]>;
47
48    fn envelope(&self) -> Self::Envelope {
49        AABB::from_point([self.location.lon, self.location.lat])
50    }
51}
52
53impl PointDistance for IndexedLocation {
54    fn distance_2(&self, point: &[f64; 2]) -> f64 {
55        let dx = self.location.lon - point[0];
56        let dy = self.location.lat - point[1];
57        dx * dx + dy * dy
58    }
59}
60
61/// Spatial index for efficient geographic queries.
62///
63/// Uses an R-tree data structure for O(log n) query performance.
64#[derive(Debug)]
65pub struct SpatialIndex<T> {
66    tree: RTree<IndexedLocation>,
67    items: Vec<T>,
68}
69
70impl<T: Clone> SpatialIndex<T> {
71    /// Create an empty spatial index.
72    pub fn new() -> Self {
73        Self {
74            tree: RTree::new(),
75            items: Vec::new(),
76        }
77    }
78
79    /// Create a spatial index from items with a location extractor.
80    pub fn from_iter<I, F>(iter: I, location_fn: F) -> Self
81    where
82        I: IntoIterator<Item = T>,
83        F: Fn(&T) -> &Location,
84    {
85        let items: Vec<T> = iter.into_iter().collect();
86        let indexed: Vec<IndexedLocation> = items
87            .iter()
88            .enumerate()
89            .map(|(i, item)| IndexedLocation::new(location_fn(item).clone(), i))
90            .collect();
91
92        Self {
93            tree: RTree::bulk_load(indexed),
94            items,
95        }
96    }
97
98    /// Insert an item into the index.
99    pub fn insert(&mut self, item: T, location: &Location) {
100        let index = self.items.len();
101        self.items.push(item);
102        self.tree
103            .insert(IndexedLocation::new(location.clone(), index));
104    }
105
106    /// Query items within a bounding box.
107    ///
108    /// # Arguments
109    /// * `min_lat` - Minimum latitude
110    /// * `min_lon` - Minimum longitude
111    /// * `max_lat` - Maximum latitude
112    /// * `max_lon` - Maximum longitude
113    pub fn query_bbox(&self, min_lat: f64, min_lon: f64, max_lat: f64, max_lon: f64) -> Vec<&T> {
114        let envelope = AABB::from_corners([min_lon, min_lat], [max_lon, max_lat]);
115        self.tree
116            .locate_in_envelope(&envelope)
117            .map(|indexed| &self.items[indexed.index])
118            .collect()
119    }
120
121    /// Query items within geographic bounds.
122    pub fn query_bounds(&self, bounds: &GeoBounds) -> Vec<&T> {
123        self.query_bbox(
124            bounds.min_lat,
125            bounds.min_lon,
126            bounds.max_lat,
127            bounds.max_lon,
128        )
129    }
130
131    /// Query items within a radius of a point.
132    ///
133    /// Note: This uses Euclidean distance in degrees. For accurate
134    /// great-circle distance, use `query_radius_meters`.
135    pub fn query_radius(&self, lat: f64, lon: f64, radius_degrees: f64) -> Vec<&T> {
136        let radius_sq = radius_degrees * radius_degrees;
137        self.tree
138            .locate_within_distance([lon, lat], radius_sq)
139            .map(|indexed| &self.items[indexed.index])
140            .collect()
141    }
142
143    /// Query items within a radius in meters.
144    ///
145    /// Uses the Haversine formula for accurate great-circle distance.
146    pub fn query_radius_meters(&self, lat: f64, lon: f64, radius_meters: f64) -> Vec<&T> {
147        // Convert to approximate degree radius for initial R-tree query
148        // 1 degree latitude ≈ 111,320 meters
149        let degree_radius = radius_meters / 111_320.0 * 1.5; // Add buffer
150
151        // Get candidates from R-tree
152        let candidates = self.query_radius(lat, lon, degree_radius);
153
154        // Return all candidates within the approximate radius
155        // (precise Haversine filtering would require storing locations)
156        candidates
157    }
158
159    /// Find the k nearest neighbors to a point.
160    pub fn nearest(&self, lat: f64, lon: f64, k: usize) -> Vec<&T> {
161        self.tree
162            .nearest_neighbor_iter(&[lon, lat])
163            .take(k)
164            .map(|indexed| &self.items[indexed.index])
165            .collect()
166    }
167
168    /// Find the single nearest item to a point.
169    pub fn nearest_one(&self, lat: f64, lon: f64) -> Option<&T> {
170        self.tree
171            .nearest_neighbor(&[lon, lat])
172            .map(|indexed| &self.items[indexed.index])
173    }
174
175    /// Returns the number of indexed items.
176    pub fn len(&self) -> usize {
177        self.items.len()
178    }
179
180    /// Returns true if the index is empty.
181    pub fn is_empty(&self) -> bool {
182        self.items.is_empty()
183    }
184
185    /// Get all items in the index.
186    pub fn items(&self) -> &[T] {
187        &self.items
188    }
189}
190
191impl<T: Clone> Default for SpatialIndex<T> {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::core::Location;
201
202    #[test]
203    fn test_spatial_index_new() {
204        let index: SpatialIndex<String> = SpatialIndex::new();
205        assert!(index.is_empty());
206    }
207
208    #[test]
209    fn test_spatial_index_insert() {
210        let mut index = SpatialIndex::new();
211        index.insert("NYC".to_string(), &Location::new(40.7128, -74.0060));
212        index.insert("LA".to_string(), &Location::new(34.0522, -118.2437));
213
214        assert_eq!(index.len(), 2);
215    }
216
217    #[test]
218    fn test_spatial_index_query_bbox() {
219        let items = vec![
220            ("NYC", Location::new(40.7128, -74.0060)),
221            ("LA", Location::new(34.0522, -118.2437)),
222            ("Chicago", Location::new(41.8781, -87.6298)),
223        ];
224
225        // Build index
226        let mut index: SpatialIndex<&str> = SpatialIndex::new();
227        for (name, loc) in &items {
228            index.insert(*name, loc);
229        }
230
231        // Query East Coast (should find NYC)
232        let results = index.query_bbox(35.0, -80.0, 45.0, -70.0);
233        assert_eq!(results.len(), 1);
234        assert_eq!(*results[0], "NYC");
235    }
236
237    #[test]
238    fn test_spatial_index_nearest() {
239        let mut index: SpatialIndex<&str> = SpatialIndex::new();
240        index.insert("NYC", &Location::new(40.7128, -74.0060));
241        index.insert("LA", &Location::new(34.0522, -118.2437));
242        index.insert("Chicago", &Location::new(41.8781, -87.6298));
243
244        // Find nearest to Philadelphia (should be NYC)
245        let nearest = index.nearest_one(39.9526, -75.1652);
246        assert_eq!(nearest, Some(&"NYC"));
247    }
248
249    #[test]
250    fn test_spatial_index_k_nearest() {
251        let mut index: SpatialIndex<&str> = SpatialIndex::new();
252        index.insert("NYC", &Location::new(40.7128, -74.0060));
253        index.insert("Boston", &Location::new(42.3601, -71.0589));
254        index.insert("Philadelphia", &Location::new(39.9526, -75.1652));
255        index.insert("LA", &Location::new(34.0522, -118.2437));
256
257        // Find 2 nearest to NYC
258        let results = index.nearest(40.7128, -74.0060, 2);
259        assert_eq!(results.len(), 2);
260    }
261}