Skip to main content

geometry_rtree/
bounds.rs

1//! The axis-aligned bounding box used for tree keys.
2//!
3//! Mirrors the role of `boost::geometry::model::box` inside the index
4//! (`index/detail/bounded_view.hpp`, `index/detail/rtree/node/`). The
5//! index does a lot of box arithmetic — area, enlargement, union,
6//! intersection — so the port uses a small concrete `Bounds` value
7//! rather than the generic `model::Box`, keeping the hot path free of
8//! trait dispatch.
9//!
10//! 2D, `f64` for v1.
11
12/// An axis-aligned bounding box in the plane.
13///
14/// Mirrors the minimum bounding rectangle stored at every node of a
15/// Boost rtree.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct Bounds {
18    /// Lower corner `[x_min, y_min]`.
19    pub min: [f64; 2],
20    /// Upper corner `[x_max, y_max]`.
21    pub max: [f64; 2],
22}
23
24impl Bounds {
25    /// A box from explicit corners.
26    #[must_use]
27    pub const fn new(min: [f64; 2], max: [f64; 2]) -> Self {
28        Self { min, max }
29    }
30
31    /// A degenerate box at a single point.
32    #[must_use]
33    pub const fn point(p: [f64; 2]) -> Self {
34        Self { min: p, max: p }
35    }
36
37    /// The area (2D measure) of the box.
38    #[must_use]
39    pub fn area(&self) -> f64 {
40        (self.max[0] - self.min[0]) * (self.max[1] - self.min[1])
41    }
42
43    /// Half the perimeter — Boost's "margin", used by the R\* split.
44    #[must_use]
45    pub fn half_perimeter(&self) -> f64 {
46        (self.max[0] - self.min[0]) + (self.max[1] - self.min[1])
47    }
48
49    /// The smallest box containing both `self` and `other`.
50    #[must_use]
51    pub fn union(&self, other: &Bounds) -> Bounds {
52        Bounds {
53            min: [self.min[0].min(other.min[0]), self.min[1].min(other.min[1])],
54            max: [self.max[0].max(other.max[0]), self.max[1].max(other.max[1])],
55        }
56    }
57
58    /// How much `self`'s area would grow to also contain `other` —
59    /// Boost's enlargement metric for `choose_next_node`.
60    #[must_use]
61    pub fn enlargement(&self, other: &Bounds) -> f64 {
62        self.union(other).area() - self.area()
63    }
64
65    /// Whether the two boxes share any point (closed boxes, so touching
66    /// counts).
67    #[must_use]
68    pub fn intersects(&self, other: &Bounds) -> bool {
69        self.min[0] <= other.max[0]
70            && self.max[0] >= other.min[0]
71            && self.min[1] <= other.max[1]
72            && self.max[1] >= other.min[1]
73    }
74
75    /// Whether `self` fully contains `other`.
76    #[must_use]
77    pub fn contains(&self, other: &Bounds) -> bool {
78        self.min[0] <= other.min[0]
79            && self.max[0] >= other.max[0]
80            && self.min[1] <= other.min[1]
81            && self.max[1] >= other.max[1]
82    }
83
84    /// The minimum distance from a query point to this box (0 inside).
85    /// Used by nearest-neighbour pruning.
86    #[must_use]
87    pub fn min_distance_to(&self, p: [f64; 2]) -> f64 {
88        self.comparable_min_distance_to(p).sqrt()
89    }
90
91    /// The SQUARED minimum distance from a query point to this box —
92    /// same ordering as [`min_distance_to`](Self::min_distance_to)
93    /// without the square root, for hot comparison paths. The analogue
94    /// of `boost::geometry::comparable_distance`.
95    #[must_use]
96    pub fn comparable_min_distance_to(&self, p: [f64; 2]) -> f64 {
97        let dx = clamp_gap(p[0], self.min[0], self.max[0]);
98        let dy = clamp_gap(p[1], self.min[1], self.max[1]);
99        dx * dx + dy * dy
100    }
101
102    /// The centroid, used by the STR bulk-load sort.
103    #[must_use]
104    pub fn center(&self) -> [f64; 2] {
105        [
106            (self.min[0] + self.max[0]) * 0.5,
107            (self.min[1] + self.max[1]) * 0.5,
108        ]
109    }
110}
111
112/// Distance from `v` to the interval `[lo, hi]` (0 if inside).
113fn clamp_gap(v: f64, lo: f64, hi: f64) -> f64 {
114    if v < lo {
115        lo - v
116    } else if v > hi {
117        v - hi
118    } else {
119        0.0
120    }
121}
122
123/// The union of a non-empty slice of boxes.
124///
125/// # Panics
126///
127/// Panics if `boxes` is empty — callers hold that invariant (a node
128/// always has at least one child).
129#[must_use]
130pub fn union_all(boxes: &[Bounds]) -> Bounds {
131    let mut acc = boxes[0];
132    for b in &boxes[1..] {
133        acc = acc.union(b);
134    }
135    acc
136}
137
138#[cfg(test)]
139#[allow(
140    clippy::float_cmp,
141    reason = "box arithmetic on exact integer-valued literals"
142)]
143mod tests {
144    use super::{Bounds, union_all};
145
146    #[test]
147    fn area_and_perimeter() {
148        let b = Bounds::new([0.0, 0.0], [2.0, 3.0]);
149        assert_eq!(b.area(), 6.0);
150        assert_eq!(b.half_perimeter(), 5.0);
151    }
152
153    #[test]
154    fn union_and_enlargement() {
155        let a = Bounds::new([0.0, 0.0], [1.0, 1.0]);
156        let b = Bounds::new([2.0, 2.0], [3.0, 3.0]);
157        let u = a.union(&b);
158        assert_eq!(u, Bounds::new([0.0, 0.0], [3.0, 3.0]));
159        // a has area 1; union has area 9; enlargement is 8.
160        assert_eq!(a.enlargement(&b), 8.0);
161    }
162
163    #[test]
164    fn intersects_and_contains() {
165        let a = Bounds::new([0.0, 0.0], [4.0, 4.0]);
166        let inside = Bounds::new([1.0, 1.0], [2.0, 2.0]);
167        let overlap = Bounds::new([3.0, 3.0], [5.0, 5.0]);
168        let apart = Bounds::new([9.0, 9.0], [10.0, 10.0]);
169        assert!(a.contains(&inside));
170        assert!(a.intersects(&overlap));
171        assert!(!a.contains(&overlap));
172        assert!(!a.intersects(&apart));
173    }
174
175    #[test]
176    fn min_distance() {
177        let b = Bounds::new([0.0, 0.0], [2.0, 2.0]);
178        assert_eq!(b.min_distance_to([1.0, 1.0]), 0.0); // inside
179        assert_eq!(b.min_distance_to([5.0, 1.0]), 3.0); // right of it
180        assert_eq!(b.min_distance_to([5.0, 6.0]), 5.0); // corner (3,4)→5
181        assert_eq!(b.comparable_min_distance_to([5.0, 6.0]), 25.0);
182    }
183
184    #[test]
185    fn union_all_of_many() {
186        let boxes = [
187            Bounds::point([1.0, 1.0]),
188            Bounds::point([-2.0, 3.0]),
189            Bounds::point([4.0, -1.0]),
190        ];
191        assert_eq!(union_all(&boxes), Bounds::new([-2.0, -1.0], [4.0, 3.0]));
192    }
193}