Skip to main content

spatio_types/
bbox.rs

1use crate::geo::Point;
2use geo::Rect;
3use serde::{Deserialize, Serialize};
4use std::time::SystemTime;
5
6/// A 2D axis-aligned bounding box.
7///
8/// Represents a rectangular area defined by minimum and maximum coordinates.
9/// This is a wrapper around `geo::Rect` with additional functionality.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct BoundingBox2D {
12    /// The underlying geometric rectangle
13    pub rect: Rect,
14}
15
16impl BoundingBox2D {
17    /// Create a new bounding box from minimum and maximum coordinates.
18    ///
19    /// # Arguments
20    ///
21    /// * `min_x` - Minimum longitude/x coordinate
22    /// * `min_y` - Minimum latitude/y coordinate
23    /// * `max_x` - Maximum longitude/x coordinate
24    /// * `max_y` - Maximum latitude/y coordinate
25    ///
26    /// # Examples
27    ///
28    /// ```
29    /// use spatio_types::bbox::BoundingBox2D;
30    ///
31    /// let bbox = BoundingBox2D::new(-74.0, 40.7, -73.9, 40.8);
32    /// ```
33    pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
34        Self {
35            rect: Rect::new(
36                geo::coord! { x: min_x, y: min_y },
37                geo::coord! { x: max_x, y: max_y },
38            ),
39        }
40    }
41
42    /// Create a bounding box from a `geo::Rect`.
43    pub fn from_rect(rect: Rect) -> Self {
44        Self { rect }
45    }
46
47    /// Get the minimum x coordinate.
48    pub fn min_x(&self) -> f64 {
49        self.rect.min().x
50    }
51
52    /// Get the minimum y coordinate.
53    pub fn min_y(&self) -> f64 {
54        self.rect.min().y
55    }
56
57    /// Get the maximum x coordinate.
58    pub fn max_x(&self) -> f64 {
59        self.rect.max().x
60    }
61
62    /// Get the maximum y coordinate.
63    pub fn max_y(&self) -> f64 {
64        self.rect.max().y
65    }
66
67    /// Get the center point of the bounding box.
68    pub fn center(&self) -> Point {
69        Point::new(
70            (self.min_x() + self.max_x()) / 2.0,
71            (self.min_y() + self.max_y()) / 2.0,
72        )
73    }
74
75    /// Get the width of the bounding box.
76    pub fn width(&self) -> f64 {
77        self.max_x() - self.min_x()
78    }
79
80    /// Get the height of the bounding box.
81    pub fn height(&self) -> f64 {
82        self.max_y() - self.min_y()
83    }
84
85    /// Check if a point is contained within this bounding box.
86    pub fn contains_point(&self, point: &Point) -> bool {
87        point.x() >= self.min_x()
88            && point.x() <= self.max_x()
89            && point.y() >= self.min_y()
90            && point.y() <= self.max_y()
91    }
92
93    /// Check if this bounding box intersects with another.
94    pub fn intersects(&self, other: &BoundingBox2D) -> bool {
95        !(self.max_x() < other.min_x()
96            || self.min_x() > other.max_x()
97            || self.max_y() < other.min_y()
98            || self.min_y() > other.max_y())
99    }
100
101    /// Expand the bounding box by a given amount in all directions.
102    pub fn expand(&self, amount: f64) -> Self {
103        Self::new(
104            self.min_x() - amount,
105            self.min_y() - amount,
106            self.max_x() + amount,
107            self.max_y() + amount,
108        )
109    }
110}
111
112/// A 3D axis-aligned bounding box.
113///
114/// Represents a rectangular volume defined by minimum and maximum coordinates
115/// in three dimensions (x, y, z).
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct BoundingBox3D {
118    /// Minimum x coordinate
119    pub min_x: f64,
120    /// Minimum y coordinate
121    pub min_y: f64,
122    /// Minimum z coordinate (altitude/elevation)
123    pub min_z: f64,
124    /// Maximum x coordinate
125    pub max_x: f64,
126    /// Maximum y coordinate
127    pub max_y: f64,
128    /// Maximum z coordinate (altitude/elevation)
129    pub max_z: f64,
130}
131
132impl BoundingBox3D {
133    /// Create a new 3D bounding box from minimum and maximum coordinates.
134    ///
135    /// # Arguments
136    ///
137    /// * `min_x` - Minimum x coordinate
138    /// * `min_y` - Minimum y coordinate
139    /// * `min_z` - Minimum z coordinate (altitude/elevation)
140    /// * `max_x` - Maximum x coordinate
141    /// * `max_y` - Maximum y coordinate
142    /// * `max_z` - Maximum z coordinate (altitude/elevation)
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use spatio_types::bbox::BoundingBox3D;
148    ///
149    /// let bbox = BoundingBox3D::new(-74.0, 40.7, 0.0, -73.9, 40.8, 100.0);
150    /// ```
151    ///
152    /// Corners are normalized per axis so that `min <= max`, matching the
153    /// behaviour of [`BoundingBox2D::new`]; passing swapped bounds yields the
154    /// same (valid) box rather than negative extents.
155    #[must_use]
156    pub fn new(min_x: f64, min_y: f64, min_z: f64, max_x: f64, max_y: f64, max_z: f64) -> Self {
157        let (min_x, max_x) = if min_x <= max_x {
158            (min_x, max_x)
159        } else {
160            (max_x, min_x)
161        };
162        let (min_y, max_y) = if min_y <= max_y {
163            (min_y, max_y)
164        } else {
165            (max_y, min_y)
166        };
167        let (min_z, max_z) = if min_z <= max_z {
168            (min_z, max_z)
169        } else {
170            (max_z, min_z)
171        };
172        Self {
173            min_x,
174            min_y,
175            min_z,
176            max_x,
177            max_y,
178            max_z,
179        }
180    }
181
182    /// Get the center point of the bounding box.
183    #[inline]
184    #[must_use]
185    pub fn center(&self) -> (f64, f64, f64) {
186        (
187            (self.min_x + self.max_x) / 2.0,
188            (self.min_y + self.max_y) / 2.0,
189            (self.min_z + self.max_z) / 2.0,
190        )
191    }
192
193    /// Get the width (x dimension) of the bounding box.
194    #[inline]
195    #[must_use]
196    pub fn width(&self) -> f64 {
197        self.max_x - self.min_x
198    }
199
200    /// Get the height (y dimension) of the bounding box.
201    #[inline]
202    #[must_use]
203    pub fn height(&self) -> f64 {
204        self.max_y - self.min_y
205    }
206
207    /// Get the depth (z dimension) of the bounding box.
208    #[inline]
209    #[must_use]
210    pub fn depth(&self) -> f64 {
211        self.max_z - self.min_z
212    }
213
214    /// Get the volume of the bounding box.
215    #[inline]
216    #[must_use]
217    pub fn volume(&self) -> f64 {
218        self.width() * self.height() * self.depth()
219    }
220
221    /// Check if a 3D point is contained within this bounding box.
222    #[inline]
223    #[must_use]
224    pub fn contains_point(&self, x: f64, y: f64, z: f64) -> bool {
225        x >= self.min_x
226            && x <= self.max_x
227            && y >= self.min_y
228            && y <= self.max_y
229            && z >= self.min_z
230            && z <= self.max_z
231    }
232
233    /// Check if this bounding box intersects with another 3D bounding box.
234    #[inline]
235    #[must_use]
236    pub fn intersects(&self, other: &BoundingBox3D) -> bool {
237        !(self.max_x < other.min_x
238            || self.min_x > other.max_x
239            || self.max_y < other.min_y
240            || self.min_y > other.max_y
241            || self.max_z < other.min_z
242            || self.min_z > other.max_z)
243    }
244
245    /// Expand the bounding box by a given amount in all directions.
246    ///
247    /// A negative `amount` shrinks the box; if it exceeds half an axis' extent
248    /// the corners are re-normalized by [`BoundingBox3D::new`] so the result
249    /// stays valid (`min <= max`).
250    #[must_use]
251    pub fn expand(&self, amount: f64) -> Self {
252        Self::new(
253            self.min_x - amount,
254            self.min_y - amount,
255            self.min_z - amount,
256            self.max_x + amount,
257            self.max_y + amount,
258            self.max_z + amount,
259        )
260    }
261
262    /// Project the 3D bounding box to a 2D bounding box (discarding z).
263    #[inline]
264    #[must_use]
265    pub fn to_2d(&self) -> BoundingBox2D {
266        BoundingBox2D::new(self.min_x, self.min_y, self.max_x, self.max_y)
267    }
268}
269
270/// A 2D bounding box with an associated timestamp.
271///
272/// Useful for tracking how spatial bounds change over time.
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274pub struct TemporalBoundingBox2D {
275    /// The bounding box
276    pub bbox: BoundingBox2D,
277    /// The timestamp when this bounding box was valid
278    pub timestamp: SystemTime,
279}
280
281impl TemporalBoundingBox2D {
282    /// Create a new temporal bounding box.
283    pub fn new(bbox: BoundingBox2D, timestamp: SystemTime) -> Self {
284        Self { bbox, timestamp }
285    }
286
287    /// Get a reference to the bounding box.
288    pub fn bbox(&self) -> &BoundingBox2D {
289        &self.bbox
290    }
291
292    /// Get a reference to the timestamp.
293    pub fn timestamp(&self) -> &SystemTime {
294        &self.timestamp
295    }
296}
297
298/// A 3D bounding box with an associated timestamp.
299///
300/// Useful for tracking how 3D spatial bounds change over time.
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
302pub struct TemporalBoundingBox3D {
303    /// The 3D bounding box
304    pub bbox: BoundingBox3D,
305    /// The timestamp when this bounding box was valid
306    pub timestamp: SystemTime,
307}
308
309impl TemporalBoundingBox3D {
310    /// Create a new temporal 3D bounding box.
311    pub fn new(bbox: BoundingBox3D, timestamp: SystemTime) -> Self {
312        Self { bbox, timestamp }
313    }
314
315    /// Get a reference to the bounding box.
316    pub fn bbox(&self) -> &BoundingBox3D {
317        &self.bbox
318    }
319
320    /// Get a reference to the timestamp.
321    pub fn timestamp(&self) -> &SystemTime {
322        &self.timestamp
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_bbox3d_new_normalizes_swapped_corners() {
332        // Swapped min/max on every axis must be normalized so extents stay non-negative.
333        let bbox = BoundingBox3D::new(10.0, 20.0, 30.0, 0.0, 5.0, 15.0);
334        assert_eq!((bbox.min_x, bbox.max_x), (0.0, 10.0));
335        assert_eq!((bbox.min_y, bbox.max_y), (5.0, 20.0));
336        assert_eq!((bbox.min_z, bbox.max_z), (15.0, 30.0));
337        assert!(bbox.width() >= 0.0 && bbox.height() >= 0.0 && bbox.depth() >= 0.0);
338        assert!(bbox.contains_point(5.0, 10.0, 20.0));
339    }
340
341    #[test]
342    fn test_bbox2d_creation() {
343        let bbox = BoundingBox2D::new(-74.0, 40.7, -73.9, 40.8);
344        assert_eq!(bbox.min_x(), -74.0);
345        assert_eq!(bbox.min_y(), 40.7);
346        assert_eq!(bbox.max_x(), -73.9);
347        assert_eq!(bbox.max_y(), 40.8);
348    }
349
350    #[test]
351    fn test_bbox2d_dimensions() {
352        let bbox = BoundingBox2D::new(0.0, 0.0, 10.0, 5.0);
353        assert_eq!(bbox.width(), 10.0);
354        assert_eq!(bbox.height(), 5.0);
355    }
356
357    #[test]
358    fn test_bbox2d_center() {
359        let bbox = BoundingBox2D::new(0.0, 0.0, 10.0, 10.0);
360        let center = bbox.center();
361        assert_eq!(center.x(), 5.0);
362        assert_eq!(center.y(), 5.0);
363    }
364
365    #[test]
366    fn test_bbox2d_contains() {
367        let bbox = BoundingBox2D::new(0.0, 0.0, 10.0, 10.0);
368        assert!(bbox.contains_point(&Point::new(5.0, 5.0)));
369        assert!(bbox.contains_point(&Point::new(0.0, 0.0)));
370        assert!(bbox.contains_point(&Point::new(10.0, 10.0)));
371        assert!(!bbox.contains_point(&Point::new(-1.0, 5.0)));
372        assert!(!bbox.contains_point(&Point::new(11.0, 5.0)));
373    }
374
375    #[test]
376    fn test_bbox2d_intersects() {
377        let bbox1 = BoundingBox2D::new(0.0, 0.0, 10.0, 10.0);
378        let bbox2 = BoundingBox2D::new(5.0, 5.0, 15.0, 15.0);
379        let bbox3 = BoundingBox2D::new(20.0, 20.0, 30.0, 30.0);
380
381        assert!(bbox1.intersects(&bbox2));
382        assert!(bbox2.intersects(&bbox1));
383        assert!(!bbox1.intersects(&bbox3));
384        assert!(!bbox3.intersects(&bbox1));
385    }
386
387    #[test]
388    fn test_bbox2d_expand() {
389        let bbox = BoundingBox2D::new(0.0, 0.0, 10.0, 10.0);
390        let expanded = bbox.expand(5.0);
391        assert_eq!(expanded.min_x(), -5.0);
392        assert_eq!(expanded.min_y(), -5.0);
393        assert_eq!(expanded.max_x(), 15.0);
394        assert_eq!(expanded.max_y(), 15.0);
395    }
396
397    #[test]
398    fn test_bbox3d_creation() {
399        let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
400        assert_eq!(bbox.min_x, 0.0);
401        assert_eq!(bbox.min_y, 0.0);
402        assert_eq!(bbox.min_z, 0.0);
403        assert_eq!(bbox.max_x, 10.0);
404        assert_eq!(bbox.max_y, 10.0);
405        assert_eq!(bbox.max_z, 10.0);
406    }
407
408    #[test]
409    fn test_bbox3d_dimensions() {
410        let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 5.0, 3.0);
411        assert_eq!(bbox.width(), 10.0);
412        assert_eq!(bbox.height(), 5.0);
413        assert_eq!(bbox.depth(), 3.0);
414        assert_eq!(bbox.volume(), 150.0);
415    }
416
417    #[test]
418    fn test_bbox3d_center() {
419        let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
420        let (x, y, z) = bbox.center();
421        assert_eq!(x, 5.0);
422        assert_eq!(y, 5.0);
423        assert_eq!(z, 5.0);
424    }
425
426    #[test]
427    fn test_bbox3d_contains() {
428        let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
429        assert!(bbox.contains_point(5.0, 5.0, 5.0));
430        assert!(bbox.contains_point(0.0, 0.0, 0.0));
431        assert!(bbox.contains_point(10.0, 10.0, 10.0));
432        assert!(!bbox.contains_point(-1.0, 5.0, 5.0));
433        assert!(!bbox.contains_point(5.0, 5.0, 11.0));
434    }
435
436    #[test]
437    fn test_bbox3d_intersects() {
438        let bbox1 = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
439        let bbox2 = BoundingBox3D::new(5.0, 5.0, 5.0, 15.0, 15.0, 15.0);
440        let bbox3 = BoundingBox3D::new(20.0, 20.0, 20.0, 30.0, 30.0, 30.0);
441
442        assert!(bbox1.intersects(&bbox2));
443        assert!(bbox2.intersects(&bbox1));
444        assert!(!bbox1.intersects(&bbox3));
445        assert!(!bbox3.intersects(&bbox1));
446    }
447
448    #[test]
449    fn test_bbox3d_to_2d() {
450        let bbox3d = BoundingBox3D::new(0.0, 0.0, 5.0, 10.0, 10.0, 15.0);
451        let bbox2d = bbox3d.to_2d();
452        assert_eq!(bbox2d.min_x(), 0.0);
453        assert_eq!(bbox2d.min_y(), 0.0);
454        assert_eq!(bbox2d.max_x(), 10.0);
455        assert_eq!(bbox2d.max_y(), 10.0);
456    }
457
458    #[test]
459    fn test_temporal_bbox2d() {
460        let bbox = BoundingBox2D::new(0.0, 0.0, 10.0, 10.0);
461        let timestamp = SystemTime::now();
462        let temporal_bbox = TemporalBoundingBox2D::new(bbox.clone(), timestamp);
463
464        assert_eq!(temporal_bbox.bbox(), &bbox);
465        assert_eq!(temporal_bbox.timestamp(), &timestamp);
466    }
467
468    #[test]
469    fn test_temporal_bbox3d() {
470        let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
471        let timestamp = SystemTime::now();
472        let temporal_bbox = TemporalBoundingBox3D::new(bbox.clone(), timestamp);
473
474        assert_eq!(temporal_bbox.bbox(), &bbox);
475        assert_eq!(temporal_bbox.timestamp(), &timestamp);
476    }
477}