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#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct Bounds {
19    /// Lower corner `[x_min, y_min]`.
20    pub min: [f64; 2],
21    /// Upper corner `[x_max, y_max]`.
22    pub max: [f64; 2],
23}
24
25impl Bounds {
26    /// A box from explicit corners.
27    #[must_use]
28    pub const fn new(min: [f64; 2], max: [f64; 2]) -> Self {
29        Self { min, max }
30    }
31
32    /// A degenerate box at a single point.
33    #[must_use]
34    pub const fn point(p: [f64; 2]) -> Self {
35        Self { min: p, max: p }
36    }
37
38    /// The area (2D measure) of the box.
39    #[must_use]
40    pub fn area(&self) -> f64 {
41        (self.max[0] - self.min[0]) * (self.max[1] - self.min[1])
42    }
43
44    /// Half the perimeter — Boost's "margin", used by the R\* split.
45    #[must_use]
46    pub fn half_perimeter(&self) -> f64 {
47        (self.max[0] - self.min[0]) + (self.max[1] - self.min[1])
48    }
49
50    /// The smallest box containing both `self` and `other`.
51    #[must_use]
52    pub fn union(&self, other: &Bounds) -> Bounds {
53        Bounds {
54            min: [self.min[0].min(other.min[0]), self.min[1].min(other.min[1])],
55            max: [self.max[0].max(other.max[0]), self.max[1].max(other.max[1])],
56        }
57    }
58
59    /// How much `self`'s area would grow to also contain `other` —
60    /// Boost's enlargement metric for `choose_next_node`.
61    #[must_use]
62    pub fn enlargement(&self, other: &Bounds) -> f64 {
63        self.union(other).area() - self.area()
64    }
65
66    /// Whether the two boxes share any point (closed boxes, so touching
67    /// counts).
68    #[must_use]
69    pub fn intersects(&self, other: &Bounds) -> bool {
70        self.min[0] <= other.max[0]
71            && self.max[0] >= other.min[0]
72            && self.min[1] <= other.max[1]
73            && self.max[1] >= other.min[1]
74    }
75
76    /// Whether `self` fully contains `other`.
77    #[must_use]
78    pub fn contains(&self, other: &Bounds) -> bool {
79        self.min[0] <= other.min[0]
80            && self.max[0] >= other.max[0]
81            && self.min[1] <= other.min[1]
82            && self.max[1] >= other.max[1]
83    }
84
85    /// Whether `self` is covered by `other`, including coincident
86    /// boundaries and degenerate boxes.
87    ///
88    /// This is Boost.Geometry's box-level `covered_by` relation.
89    #[must_use]
90    #[inline]
91    pub fn covered_by(&self, other: &Bounds) -> bool {
92        other.contains(self)
93    }
94
95    /// Whether `self` is within `other` under Boost.Geometry's box
96    /// semantics.
97    ///
98    /// Boundaries may coincide, but the geometry on the within side
99    /// must have a non-empty two-dimensional interior. This is the
100    /// distinction between `within` and [`covered_by`](Self::covered_by)
101    /// for point-like boxes.
102    #[must_use]
103    #[inline]
104    pub fn within(&self, other: &Bounds) -> bool {
105        self.min[0] < self.max[0] && self.min[1] < self.max[1] && self.covered_by(other)
106    }
107
108    /// Whether the two closed boxes have no point in common.
109    #[must_use]
110    #[inline]
111    pub fn disjoint(&self, other: &Bounds) -> bool {
112        !self.intersects(other)
113    }
114
115    /// Whether the interiors overlap in two dimensions while neither
116    /// box covers the other.
117    ///
118    /// Touching only at a boundary and containment are not overlaps.
119    #[must_use]
120    #[inline]
121    pub fn overlaps(&self, other: &Bounds) -> bool {
122        self.min[0] < other.max[0]
123            && self.max[0] > other.min[0]
124            && self.min[1] < other.max[1]
125            && self.max[1] > other.min[1]
126            && !self.contains(other)
127            && !other.contains(self)
128    }
129
130    /// The minimum distance from a query point to this box (0 inside).
131    /// Used by nearest-neighbour pruning.
132    #[must_use]
133    pub fn min_distance_to(&self, p: [f64; 2]) -> f64 {
134        geometry_coords::math::sqrt(self.comparable_min_distance_to(p))
135    }
136
137    /// The SQUARED minimum distance from a query point to this box —
138    /// same ordering as [`min_distance_to`](Self::min_distance_to)
139    /// without the square root, for hot comparison paths. The analogue
140    /// of `boost::geometry::comparable_distance`.
141    #[must_use]
142    pub fn comparable_min_distance_to(&self, p: [f64; 2]) -> f64 {
143        let dx = clamp_gap(p[0], self.min[0], self.max[0]);
144        let dy = clamp_gap(p[1], self.min[1], self.max[1]);
145        dx * dx + dy * dy
146    }
147
148    /// The centroid, used by the STR bulk-load sort.
149    #[must_use]
150    pub fn center(&self) -> [f64; 2] {
151        [
152            (self.min[0] + self.max[0]) * 0.5,
153            (self.min[1] + self.max[1]) * 0.5,
154        ]
155    }
156}
157
158/// Distance from `v` to the interval `[lo, hi]` (0 if inside).
159fn clamp_gap(v: f64, lo: f64, hi: f64) -> f64 {
160    if v < lo {
161        lo - v
162    } else if v > hi {
163        v - hi
164    } else {
165        0.0
166    }
167}
168
169/// The union of a non-empty slice of boxes.
170///
171/// # Panics
172///
173/// Panics if `boxes` is empty — callers hold that invariant (a node
174/// always has at least one child).
175#[must_use]
176pub fn union_all(boxes: &[Bounds]) -> Bounds {
177    let mut acc = boxes[0];
178    for b in &boxes[1..] {
179        acc = acc.union(b);
180    }
181    acc
182}
183
184#[cfg(test)]
185#[allow(
186    clippy::float_cmp,
187    reason = "box arithmetic on exact integer-valued literals"
188)]
189mod tests {
190    use super::{Bounds, union_all};
191
192    #[test]
193    fn area_and_perimeter() {
194        let b = Bounds::new([0.0, 0.0], [2.0, 3.0]);
195        assert_eq!(b.area(), 6.0);
196        assert_eq!(b.half_perimeter(), 5.0);
197    }
198
199    #[test]
200    fn union_and_enlargement() {
201        let a = Bounds::new([0.0, 0.0], [1.0, 1.0]);
202        let b = Bounds::new([2.0, 2.0], [3.0, 3.0]);
203        let u = a.union(&b);
204        assert_eq!(u, Bounds::new([0.0, 0.0], [3.0, 3.0]));
205        // a has area 1; union has area 9; enlargement is 8.
206        assert_eq!(a.enlargement(&b), 8.0);
207    }
208
209    #[test]
210    fn intersects_and_contains() {
211        let a = Bounds::new([0.0, 0.0], [4.0, 4.0]);
212        let inside = Bounds::new([1.0, 1.0], [2.0, 2.0]);
213        let overlap = Bounds::new([3.0, 3.0], [5.0, 5.0]);
214        let apart = Bounds::new([9.0, 9.0], [10.0, 10.0]);
215        assert!(a.contains(&inside));
216        assert!(a.intersects(&overlap));
217        assert!(!a.contains(&overlap));
218        assert!(!a.intersects(&apart));
219    }
220
221    #[test]
222    fn min_distance() {
223        let b = Bounds::new([0.0, 0.0], [2.0, 2.0]);
224        assert_eq!(b.min_distance_to([1.0, 1.0]), 0.0); // inside
225        assert_eq!(b.min_distance_to([5.0, 1.0]), 3.0); // right of it
226        assert_eq!(b.min_distance_to([5.0, 6.0]), 5.0); // corner (3,4)→5
227        assert_eq!(b.comparable_min_distance_to([5.0, 6.0]), 25.0);
228    }
229
230    #[test]
231    fn union_all_of_many() {
232        let boxes = [
233            Bounds::point([1.0, 1.0]),
234            Bounds::point([-2.0, 3.0]),
235            Bounds::point([4.0, -1.0]),
236        ];
237        assert_eq!(union_all(&boxes), Bounds::new([-2.0, -1.0], [4.0, 3.0]));
238    }
239}