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    pub fn new(min_x: f64, min_y: f64, min_z: f64, max_x: f64, max_y: f64, max_z: f64) -> Self {
152        Self {
153            min_x,
154            min_y,
155            min_z,
156            max_x,
157            max_y,
158            max_z,
159        }
160    }
161
162    /// Get the center point of the bounding box.
163    pub fn center(&self) -> (f64, f64, f64) {
164        (
165            (self.min_x + self.max_x) / 2.0,
166            (self.min_y + self.max_y) / 2.0,
167            (self.min_z + self.max_z) / 2.0,
168        )
169    }
170
171    /// Get the width (x dimension) of the bounding box.
172    pub fn width(&self) -> f64 {
173        self.max_x - self.min_x
174    }
175
176    /// Get the height (y dimension) of the bounding box.
177    pub fn height(&self) -> f64 {
178        self.max_y - self.min_y
179    }
180
181    /// Get the depth (z dimension) of the bounding box.
182    pub fn depth(&self) -> f64 {
183        self.max_z - self.min_z
184    }
185
186    /// Get the volume of the bounding box.
187    pub fn volume(&self) -> f64 {
188        self.width() * self.height() * self.depth()
189    }
190
191    /// Check if a 3D point is contained within this bounding box.
192    pub fn contains_point(&self, x: f64, y: f64, z: f64) -> bool {
193        x >= self.min_x
194            && x <= self.max_x
195            && y >= self.min_y
196            && y <= self.max_y
197            && z >= self.min_z
198            && z <= self.max_z
199    }
200
201    /// Check if this bounding box intersects with another 3D bounding box.
202    pub fn intersects(&self, other: &BoundingBox3D) -> bool {
203        !(self.max_x < other.min_x
204            || self.min_x > other.max_x
205            || self.max_y < other.min_y
206            || self.min_y > other.max_y
207            || self.max_z < other.min_z
208            || self.min_z > other.max_z)
209    }
210
211    /// Expand the bounding box by a given amount in all directions.
212    pub fn expand(&self, amount: f64) -> Self {
213        Self::new(
214            self.min_x - amount,
215            self.min_y - amount,
216            self.min_z - amount,
217            self.max_x + amount,
218            self.max_y + amount,
219            self.max_z + amount,
220        )
221    }
222
223    /// Project the 3D bounding box to a 2D bounding box (discarding z).
224    pub fn to_2d(&self) -> BoundingBox2D {
225        BoundingBox2D::new(self.min_x, self.min_y, self.max_x, self.max_y)
226    }
227}
228
229/// A 2D bounding box with an associated timestamp.
230///
231/// Useful for tracking how spatial bounds change over time.
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct TemporalBoundingBox2D {
234    /// The bounding box
235    pub bbox: BoundingBox2D,
236    /// The timestamp when this bounding box was valid
237    pub timestamp: SystemTime,
238}
239
240impl TemporalBoundingBox2D {
241    /// Create a new temporal bounding box.
242    pub fn new(bbox: BoundingBox2D, timestamp: SystemTime) -> Self {
243        Self { bbox, timestamp }
244    }
245
246    /// Get a reference to the bounding box.
247    pub fn bbox(&self) -> &BoundingBox2D {
248        &self.bbox
249    }
250
251    /// Get a reference to the timestamp.
252    pub fn timestamp(&self) -> &SystemTime {
253        &self.timestamp
254    }
255}
256
257/// A 3D bounding box with an associated timestamp.
258///
259/// Useful for tracking how 3D spatial bounds change over time.
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261pub struct TemporalBoundingBox3D {
262    /// The 3D bounding box
263    pub bbox: BoundingBox3D,
264    /// The timestamp when this bounding box was valid
265    pub timestamp: SystemTime,
266}
267
268impl TemporalBoundingBox3D {
269    /// Create a new temporal 3D bounding box.
270    pub fn new(bbox: BoundingBox3D, timestamp: SystemTime) -> Self {
271        Self { bbox, timestamp }
272    }
273
274    /// Get a reference to the bounding box.
275    pub fn bbox(&self) -> &BoundingBox3D {
276        &self.bbox
277    }
278
279    /// Get a reference to the timestamp.
280    pub fn timestamp(&self) -> &SystemTime {
281        &self.timestamp
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_bbox2d_creation() {
291        let bbox = BoundingBox2D::new(-74.0, 40.7, -73.9, 40.8);
292        assert_eq!(bbox.min_x(), -74.0);
293        assert_eq!(bbox.min_y(), 40.7);
294        assert_eq!(bbox.max_x(), -73.9);
295        assert_eq!(bbox.max_y(), 40.8);
296    }
297
298    #[test]
299    fn test_bbox2d_dimensions() {
300        let bbox = BoundingBox2D::new(0.0, 0.0, 10.0, 5.0);
301        assert_eq!(bbox.width(), 10.0);
302        assert_eq!(bbox.height(), 5.0);
303    }
304
305    #[test]
306    fn test_bbox2d_center() {
307        let bbox = BoundingBox2D::new(0.0, 0.0, 10.0, 10.0);
308        let center = bbox.center();
309        assert_eq!(center.x(), 5.0);
310        assert_eq!(center.y(), 5.0);
311    }
312
313    #[test]
314    fn test_bbox2d_contains() {
315        let bbox = BoundingBox2D::new(0.0, 0.0, 10.0, 10.0);
316        assert!(bbox.contains_point(&Point::new(5.0, 5.0)));
317        assert!(bbox.contains_point(&Point::new(0.0, 0.0)));
318        assert!(bbox.contains_point(&Point::new(10.0, 10.0)));
319        assert!(!bbox.contains_point(&Point::new(-1.0, 5.0)));
320        assert!(!bbox.contains_point(&Point::new(11.0, 5.0)));
321    }
322
323    #[test]
324    fn test_bbox2d_intersects() {
325        let bbox1 = BoundingBox2D::new(0.0, 0.0, 10.0, 10.0);
326        let bbox2 = BoundingBox2D::new(5.0, 5.0, 15.0, 15.0);
327        let bbox3 = BoundingBox2D::new(20.0, 20.0, 30.0, 30.0);
328
329        assert!(bbox1.intersects(&bbox2));
330        assert!(bbox2.intersects(&bbox1));
331        assert!(!bbox1.intersects(&bbox3));
332        assert!(!bbox3.intersects(&bbox1));
333    }
334
335    #[test]
336    fn test_bbox2d_expand() {
337        let bbox = BoundingBox2D::new(0.0, 0.0, 10.0, 10.0);
338        let expanded = bbox.expand(5.0);
339        assert_eq!(expanded.min_x(), -5.0);
340        assert_eq!(expanded.min_y(), -5.0);
341        assert_eq!(expanded.max_x(), 15.0);
342        assert_eq!(expanded.max_y(), 15.0);
343    }
344
345    #[test]
346    fn test_bbox3d_creation() {
347        let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
348        assert_eq!(bbox.min_x, 0.0);
349        assert_eq!(bbox.min_y, 0.0);
350        assert_eq!(bbox.min_z, 0.0);
351        assert_eq!(bbox.max_x, 10.0);
352        assert_eq!(bbox.max_y, 10.0);
353        assert_eq!(bbox.max_z, 10.0);
354    }
355
356    #[test]
357    fn test_bbox3d_dimensions() {
358        let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 5.0, 3.0);
359        assert_eq!(bbox.width(), 10.0);
360        assert_eq!(bbox.height(), 5.0);
361        assert_eq!(bbox.depth(), 3.0);
362        assert_eq!(bbox.volume(), 150.0);
363    }
364
365    #[test]
366    fn test_bbox3d_center() {
367        let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
368        let (x, y, z) = bbox.center();
369        assert_eq!(x, 5.0);
370        assert_eq!(y, 5.0);
371        assert_eq!(z, 5.0);
372    }
373
374    #[test]
375    fn test_bbox3d_contains() {
376        let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
377        assert!(bbox.contains_point(5.0, 5.0, 5.0));
378        assert!(bbox.contains_point(0.0, 0.0, 0.0));
379        assert!(bbox.contains_point(10.0, 10.0, 10.0));
380        assert!(!bbox.contains_point(-1.0, 5.0, 5.0));
381        assert!(!bbox.contains_point(5.0, 5.0, 11.0));
382    }
383
384    #[test]
385    fn test_bbox3d_intersects() {
386        let bbox1 = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
387        let bbox2 = BoundingBox3D::new(5.0, 5.0, 5.0, 15.0, 15.0, 15.0);
388        let bbox3 = BoundingBox3D::new(20.0, 20.0, 20.0, 30.0, 30.0, 30.0);
389
390        assert!(bbox1.intersects(&bbox2));
391        assert!(bbox2.intersects(&bbox1));
392        assert!(!bbox1.intersects(&bbox3));
393        assert!(!bbox3.intersects(&bbox1));
394    }
395
396    #[test]
397    fn test_bbox3d_to_2d() {
398        let bbox3d = BoundingBox3D::new(0.0, 0.0, 5.0, 10.0, 10.0, 15.0);
399        let bbox2d = bbox3d.to_2d();
400        assert_eq!(bbox2d.min_x(), 0.0);
401        assert_eq!(bbox2d.min_y(), 0.0);
402        assert_eq!(bbox2d.max_x(), 10.0);
403        assert_eq!(bbox2d.max_y(), 10.0);
404    }
405
406    #[test]
407    fn test_temporal_bbox2d() {
408        let bbox = BoundingBox2D::new(0.0, 0.0, 10.0, 10.0);
409        let timestamp = SystemTime::now();
410        let temporal_bbox = TemporalBoundingBox2D::new(bbox.clone(), timestamp);
411
412        assert_eq!(temporal_bbox.bbox(), &bbox);
413        assert_eq!(temporal_bbox.timestamp(), &timestamp);
414    }
415
416    #[test]
417    fn test_temporal_bbox3d() {
418        let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
419        let timestamp = SystemTime::now();
420        let temporal_bbox = TemporalBoundingBox3D::new(bbox.clone(), timestamp);
421
422        assert_eq!(temporal_bbox.bbox(), &bbox);
423        assert_eq!(temporal_bbox.timestamp(), &timestamp);
424    }
425}