spatio_types/
bbox.rs

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