use crate::core::{GeoBounds, Location};
use rstar::{PointDistance, RTree, RTreeObject, AABB};
#[derive(Debug, Clone)]
pub struct IndexedLocation {
pub location: Location,
pub index: usize,
}
impl IndexedLocation {
pub fn new(location: Location, index: usize) -> Self {
Self { location, index }
}
}
impl RTreeObject for IndexedLocation {
type Envelope = AABB<[f64; 2]>;
fn envelope(&self) -> Self::Envelope {
AABB::from_point([self.location.lon, self.location.lat])
}
}
impl PointDistance for IndexedLocation {
fn distance_2(&self, point: &[f64; 2]) -> f64 {
let dx = self.location.lon - point[0];
let dy = self.location.lat - point[1];
dx * dx + dy * dy
}
}
#[derive(Debug)]
pub struct SpatialIndex<T> {
tree: RTree<IndexedLocation>,
items: Vec<T>,
}
impl<T: Clone> SpatialIndex<T> {
pub fn new() -> Self {
Self {
tree: RTree::new(),
items: Vec::new(),
}
}
pub fn from_iter<I, F>(iter: I, location_fn: F) -> Self
where
I: IntoIterator<Item = T>,
F: Fn(&T) -> &Location,
{
let items: Vec<T> = iter.into_iter().collect();
let indexed: Vec<IndexedLocation> = items
.iter()
.enumerate()
.map(|(i, item)| IndexedLocation::new(location_fn(item).clone(), i))
.collect();
Self {
tree: RTree::bulk_load(indexed),
items,
}
}
pub fn insert(&mut self, item: T, location: &Location) {
let index = self.items.len();
self.items.push(item);
self.tree
.insert(IndexedLocation::new(location.clone(), index));
}
pub fn query_bbox(&self, min_lat: f64, min_lon: f64, max_lat: f64, max_lon: f64) -> Vec<&T> {
let envelope = AABB::from_corners([min_lon, min_lat], [max_lon, max_lat]);
self.tree
.locate_in_envelope(&envelope)
.map(|indexed| &self.items[indexed.index])
.collect()
}
pub fn query_bounds(&self, bounds: &GeoBounds) -> Vec<&T> {
self.query_bbox(
bounds.min_lat,
bounds.min_lon,
bounds.max_lat,
bounds.max_lon,
)
}
pub fn query_radius(&self, lat: f64, lon: f64, radius_degrees: f64) -> Vec<&T> {
let radius_sq = radius_degrees * radius_degrees;
self.tree
.locate_within_distance([lon, lat], radius_sq)
.map(|indexed| &self.items[indexed.index])
.collect()
}
pub fn query_radius_meters(&self, lat: f64, lon: f64, radius_meters: f64) -> Vec<&T> {
let degree_radius = radius_meters / 111_320.0 * 1.5;
let candidates = self.query_radius(lat, lon, degree_radius);
candidates
}
pub fn nearest(&self, lat: f64, lon: f64, k: usize) -> Vec<&T> {
self.tree
.nearest_neighbor_iter(&[lon, lat])
.take(k)
.map(|indexed| &self.items[indexed.index])
.collect()
}
pub fn nearest_one(&self, lat: f64, lon: f64) -> Option<&T> {
self.tree
.nearest_neighbor(&[lon, lat])
.map(|indexed| &self.items[indexed.index])
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn items(&self) -> &[T] {
&self.items
}
}
impl<T: Clone> Default for SpatialIndex<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Location;
#[test]
fn test_spatial_index_new() {
let index: SpatialIndex<String> = SpatialIndex::new();
assert!(index.is_empty());
}
#[test]
fn test_spatial_index_insert() {
let mut index = SpatialIndex::new();
index.insert("NYC".to_string(), &Location::new(40.7128, -74.0060));
index.insert("LA".to_string(), &Location::new(34.0522, -118.2437));
assert_eq!(index.len(), 2);
}
#[test]
fn test_spatial_index_query_bbox() {
let items = vec![
("NYC", Location::new(40.7128, -74.0060)),
("LA", Location::new(34.0522, -118.2437)),
("Chicago", Location::new(41.8781, -87.6298)),
];
let mut index: SpatialIndex<&str> = SpatialIndex::new();
for (name, loc) in &items {
index.insert(*name, loc);
}
let results = index.query_bbox(35.0, -80.0, 45.0, -70.0);
assert_eq!(results.len(), 1);
assert_eq!(*results[0], "NYC");
}
#[test]
fn test_spatial_index_nearest() {
let mut index: SpatialIndex<&str> = SpatialIndex::new();
index.insert("NYC", &Location::new(40.7128, -74.0060));
index.insert("LA", &Location::new(34.0522, -118.2437));
index.insert("Chicago", &Location::new(41.8781, -87.6298));
let nearest = index.nearest_one(39.9526, -75.1652);
assert_eq!(nearest, Some(&"NYC"));
}
#[test]
fn test_spatial_index_k_nearest() {
let mut index: SpatialIndex<&str> = SpatialIndex::new();
index.insert("NYC", &Location::new(40.7128, -74.0060));
index.insert("Boston", &Location::new(42.3601, -71.0589));
index.insert("Philadelphia", &Location::new(39.9526, -75.1652));
index.insert("LA", &Location::new(34.0522, -118.2437));
let results = index.nearest(40.7128, -74.0060, 2);
assert_eq!(results.len(), 2);
}
}