use crate::errors::SpartError;
use crate::geometry::{DistanceMetric, HasMinDistance, Point2D, Rectangle, span};
use crate::knn::KnnHeap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use tracing::info;
const MAX_DEPTH: usize = 32;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Quadtree<T: Clone + PartialEq> {
boundary: Rectangle,
points: Vec<Point2D<T>>,
capacity: usize,
divided: bool,
depth: usize,
count: usize,
northeast: Option<Box<Quadtree<T>>>,
northwest: Option<Box<Quadtree<T>>>,
southeast: Option<Box<Quadtree<T>>>,
southwest: Option<Box<Quadtree<T>>>,
}
impl<T: Clone + PartialEq + std::fmt::Debug> Quadtree<T> {
pub fn new(boundary: &Rectangle, capacity: usize) -> Result<Self, SpartError> {
if capacity == 0 {
return Err(SpartError::InvalidCapacity { capacity });
}
info!(
"Creating new Quadtree with boundary: {:?} and capacity: {}",
boundary, capacity
);
Ok(Self::with_depth(boundary.clone(), capacity, 0))
}
fn with_depth(boundary: Rectangle, capacity: usize, depth: usize) -> Self {
Quadtree {
boundary,
points: Vec::new(),
capacity,
divided: false,
depth,
count: 0,
northeast: None,
northwest: None,
southeast: None,
southwest: None,
}
}
fn subdivide(&mut self) {
info!("Subdividing Quadtree at boundary: {:?}", self.boundary);
let Rectangle {
x,
y,
width,
height,
} = self.boundary;
let mid_x = x + width / 2.0;
let mid_y = y + height / 2.0;
let west = span(x, mid_x);
let east = span(mid_x, x + width);
let north = span(y, mid_y);
let south = span(mid_y, y + height);
let quadrant = |qx: f64, qy: f64, qw: f64, qh: f64| {
Some(Box::new(Self::with_depth(
Rectangle {
x: qx,
y: qy,
width: qw,
height: qh,
},
self.capacity,
self.depth + 1,
)))
};
self.northwest = quadrant(x, y, west, north);
self.northeast = quadrant(mid_x, y, east, north);
self.southwest = quadrant(x, mid_y, west, south);
self.southeast = quadrant(mid_x, mid_y, east, south);
self.divided = true;
for point in std::mem::take(&mut self.points) {
let index = self.child_index(&point);
match self.child_mut(index) {
Some(child) => child.insert_within(point),
None => self.points.push(point),
}
}
}
fn child_index(&self, point: &Point2D<T>) -> usize {
let east = point.x >= self.boundary.x + self.boundary.width / 2.0;
let south = point.y >= self.boundary.y + self.boundary.height / 2.0;
usize::from(east) | (usize::from(south) << 1)
}
fn insert_within(&mut self, point: Point2D<T>) {
self.count += 1;
if !self.divided {
if self.points.len() < self.capacity || self.depth >= MAX_DEPTH {
self.points.push(point);
return;
}
self.subdivide();
}
let index = self.child_index(&point);
if self.child_mut(index).is_none() {
self.points.push(point);
return;
}
if let Some(child) = self.child_mut(index) {
child.insert_within(point);
}
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn clear(&mut self) {
self.points.clear();
self.divided = false;
self.count = 0;
self.northeast = None;
self.northwest = None;
self.southeast = None;
self.southwest = None;
}
pub fn contains(&self, point: &Point2D<T>) -> bool {
if !self.boundary.contains(point) {
return false;
}
if self.points.iter().any(|p| p == point) {
return true;
}
if !self.divided {
return false;
}
self.children()[self.child_index(point)].is_some_and(|child| child.contains(point))
}
pub fn insert(&mut self, point: Point2D<T>) -> bool {
if !self.boundary.contains(&point) {
return false;
}
self.insert_within(point);
true
}
pub fn insert_bulk(&mut self, points: &[Point2D<T>]) -> usize {
let mut inserted = 0;
for point in points {
if self.boundary.contains(point) {
self.insert_within(point.clone());
inserted += 1;
}
}
inserted
}
fn children(&self) -> [Option<&Quadtree<T>>; 4] {
[
self.northwest.as_deref(),
self.northeast.as_deref(),
self.southwest.as_deref(),
self.southeast.as_deref(),
]
}
fn child_mut(&mut self, index: usize) -> Option<&mut Quadtree<T>> {
match index {
0 => self.northwest.as_deref_mut(),
1 => self.northeast.as_deref_mut(),
2 => self.southwest.as_deref_mut(),
_ => self.southeast.as_deref_mut(),
}
}
fn min_distance_sq(&self, target: &Point2D<T>) -> f64 {
HasMinDistance::min_distance_sq(&self.boundary, target)
}
pub fn knn_search<M: DistanceMetric<Point2D<T>>>(
&self,
target: &Point2D<T>,
k: usize,
) -> Vec<&Point2D<T>> {
if k == 0 {
return Vec::new();
}
let mut heap = KnnHeap::new(k);
self.knn_search_helper::<M>(target, &mut heap);
heap.into_sorted_vec()
}
fn knn_search_helper<'a, M: DistanceMetric<Point2D<T>>>(
&'a self,
target: &Point2D<T>,
heap: &mut KnnHeap<&'a Point2D<T>>,
) {
for point in &self.points {
heap.offer(M::distance_sq(point, target), point);
}
let mut ordered: [(f64, Option<&'a Quadtree<T>>); 4] = [(f64::INFINITY, None); 4];
for (slot, child) in ordered.iter_mut().zip(self.children()) {
if let Some(child) = child {
*slot = (child.min_distance_sq(target), Some(child));
}
}
ordered.sort_by(|a, b| a.0.total_cmp(&b.0));
for (distance, child) in ordered {
let Some(child) = child else {
continue;
};
if distance > heap.worst() {
break;
}
child.knn_search_helper::<M>(target, heap);
}
}
pub fn range_search<'a, M: DistanceMetric<Point2D<T>>>(
&'a self,
center: &Point2D<T>,
radius: f64,
) -> Vec<&'a Point2D<T>> {
if radius < 0.0 {
return Vec::new();
}
let mut found = Vec::new();
self.collect_in_radius::<M>(center, radius * radius, &mut found);
found
}
fn collect_in_radius<'a, M: DistanceMetric<Point2D<T>>>(
&'a self,
center: &Point2D<T>,
radius_sq: f64,
found: &mut Vec<&'a Point2D<T>>,
) {
if self.min_distance_sq(center) > radius_sq {
return;
}
for point in &self.points {
if M::distance_sq(point, center) <= radius_sq {
found.push(point);
}
}
for child in self.children().into_iter().flatten() {
child.collect_in_radius::<M>(center, radius_sq, found);
}
}
pub fn range_search_bbox(&self, query: &Rectangle) -> Vec<&Point2D<T>> {
let mut found = Vec::new();
self.collect_in_bbox(query, &mut found);
found
}
fn collect_in_bbox<'a>(&'a self, query: &Rectangle, found: &mut Vec<&'a Point2D<T>>) {
if !self.boundary.intersects(query) {
return;
}
for point in &self.points {
if query.contains(point) {
found.push(point);
}
}
for child in self.children().into_iter().flatten() {
child.collect_in_bbox(query, found);
}
}
pub fn delete(&mut self, point: &Point2D<T>) -> bool {
if !self.boundary.contains(point) {
return false;
}
if let Some(pos) = self.points.iter().position(|p| p == point) {
self.points.remove(pos);
self.count = self.count.saturating_sub(1);
info!("Deleting point {:?} from Quadtree", point);
return true;
}
if !self.divided {
return false;
}
let index = self.child_index(point);
let deleted = self
.child_mut(index)
.is_some_and(|child| child.delete(point));
if deleted {
self.count = self.count.saturating_sub(1);
self.try_merge();
}
deleted
}
fn try_merge(&mut self) {
if !self.divided {
return;
}
let children = self.children();
if children.iter().flatten().all(|child| !child.divided) {
let total_points: usize = children.iter().flatten().map(|c| c.points.len()).sum();
if total_points + self.points.len() <= self.capacity {
let mut merged_points = Vec::with_capacity(total_points);
if let Some(child) = self.northeast.take() {
merged_points.extend(child.points);
}
if let Some(child) = self.northwest.take() {
merged_points.extend(child.points);
}
if let Some(child) = self.southeast.take() {
merged_points.extend(child.points);
}
if let Some(child) = self.southwest.take() {
merged_points.extend(child.points);
}
info!(
"Merging children into parent node at boundary {:?} with {} points",
self.boundary,
merged_points.len()
);
self.points.extend(merged_points);
self.divided = false;
}
}
}
}
crate::rtree_common::impl_bounded_spatial_index!(Quadtree, Point2D, Rectangle);
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::EuclideanDistance;
#[test]
fn test_insert_rejects_outside_boundary() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
};
let mut tree: Quadtree<&str> = Quadtree::new(&boundary, 2).unwrap();
let outside = Point2D::new(20.0, 20.0, Some("O"));
assert!(!tree.insert(outside));
}
#[test]
fn test_insert_accepts_boundary_points() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
};
let mut tree: Quadtree<&str> = Quadtree::new(&boundary, 1).unwrap();
let edge = Point2D::new(10.0, 10.0, Some("E"));
assert!(tree.insert(edge));
}
#[test]
fn test_range_search_zero_radius_returns_exact_match() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let mut tree: Quadtree<&str> = Quadtree::new(&boundary, 2).unwrap();
let target = Point2D::new(25.0, 25.0, Some("T"));
tree.insert(target.clone());
tree.insert(Point2D::new(26.0, 25.0, Some("N")));
let results = tree.range_search::<EuclideanDistance>(&target, 0.0);
assert_eq!(results.len(), 1);
assert_eq!(*results[0], target);
}
#[test]
fn test_delete_existing_point() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let mut tree: Quadtree<&str> = Quadtree::new(&boundary, 2).unwrap();
let p1 = Point2D::new(10.0, 10.0, Some("A"));
let p2 = Point2D::new(20.0, 20.0, Some("B"));
tree.insert(p1.clone());
tree.insert(p2);
assert!(tree.delete(&p1));
let results = tree.knn_search::<EuclideanDistance>(&p1, 1);
assert_ne!(*results[0], p1);
assert!(!tree.delete(&p1));
}
#[test]
fn test_empty_tree_queries() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let mut tree: Quadtree<&str> = Quadtree::new(&boundary, 2).unwrap();
let target = Point2D::new(5.0, 5.0, None::<&str>);
let knn_results = tree.knn_search::<EuclideanDistance>(&target, 5);
assert!(knn_results.is_empty());
let range_results = tree.range_search::<EuclideanDistance>(&target, 10.0);
assert!(range_results.is_empty());
assert!(!tree.delete(&target));
}
#[test]
fn test_knn_edge_cases() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let mut tree: Quadtree<&str> = Quadtree::new(&boundary, 4).unwrap();
let points = vec![
Point2D::new(10.0, 10.0, Some("A")),
Point2D::new(20.0, 20.0, Some("B")),
Point2D::new(30.0, 30.0, Some("C")),
];
let num_points = points.len();
tree.insert_bulk(&points);
let target = Point2D::new(15.0, 15.0, None::<&str>);
let knn_results = tree.knn_search::<EuclideanDistance>(&target, 0);
assert!(knn_results.is_empty());
let knn_results = tree.knn_search::<EuclideanDistance>(&target, num_points + 5);
assert_eq!(knn_results.len(), num_points);
}
#[test]
fn test_duplicates_delete_one() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let mut tree: Quadtree<&str> = Quadtree::new(&boundary, 4).unwrap();
let p1 = Point2D::new(10.0, 10.0, Some("A"));
let p2 = p1.clone();
tree.insert(p1.clone());
tree.insert(p2.clone());
let results = tree.knn_search::<EuclideanDistance>(&p1, 2);
assert_eq!(results.len(), 2);
assert!(tree.delete(&p1));
let results_after_delete = tree.knn_search::<EuclideanDistance>(&p1, 2);
assert_eq!(results_after_delete.len(), 1);
}
#[test]
fn test_range_search_includes_boundary_point() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let mut tree: Quadtree<&str> = Quadtree::new(&boundary, 4).unwrap();
let center = Point2D::new(50.0, 50.0, Some("C"));
let boundary_point = Point2D::new(60.0, 50.0, Some("B"));
tree.insert(center.clone());
tree.insert(boundary_point.clone());
let results = tree.range_search::<EuclideanDistance>(¢er, 10.0);
assert!(results.contains(&&boundary_point));
assert!(results.contains(&¢er));
}
#[test]
fn test_bulk_insert_empty_noop() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let mut tree: Quadtree<i32> = Quadtree::new(&boundary, 4).unwrap();
let empty: Vec<Point2D<i32>> = Vec::new();
tree.insert_bulk(&empty);
let target = Point2D::new(10.0, 10.0, None::<i32>);
let results = tree.knn_search::<EuclideanDistance>(&target, 1);
assert!(results.is_empty());
}
#[test]
fn test_zero_capacity_rejected() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let result = Quadtree::<i32>::new(&boundary, 0);
assert!(result.is_err());
}
#[test]
fn test_range_search_negative_radius_empty() {
let boundary = Rectangle {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let mut tree: Quadtree<&str> = Quadtree::new(&boundary, 2).unwrap();
let target = Point2D::new(10.0, 10.0, Some("T"));
tree.insert(target.clone());
let results = tree.range_search::<EuclideanDistance>(&target, -1.0);
assert!(results.is_empty());
}
}