zerometry/
bounding_box.rs

1use core::fmt;
2use std::{
3    io::{self, Write},
4    mem,
5    ops::RangeInclusive,
6};
7
8use geo_types::Point;
9
10use crate::{
11    COORD_SIZE_IN_BYTES, Coord, Coords, InputRelation, OutputRelation, RelationBetweenShapes,
12};
13
14pub(crate) const BOUNDING_BOX_SIZE_IN_BYTES: usize = COORD_SIZE_IN_BYTES * 2;
15
16/// Bounding box of a Zerometry.
17///
18/// The bounding box is a rectangle that contains the Zerometry.
19/// It is represented by two coordinates: the bottom-left and top-right corners.
20///
21/// The coordinates are stored in a `Coords` struct, which is a slice of `f64` values.
22/// The first coordinate is the bottom-left corner, and the second coordinate is the top-right corner.
23#[repr(transparent)]
24pub struct BoundingBox {
25    coords: Coords,
26}
27
28impl BoundingBox {
29    pub fn from_bytes(data: &[u8]) -> &Self {
30        Self::from_coords(Coords::from_bytes(data))
31    }
32
33    pub fn from_slice(data: &[f64]) -> &Self {
34        Self::from_coords(Coords::from_slice(data))
35    }
36
37    pub fn from_slice_mut(data: &mut [f64]) -> &mut Self {
38        Self::from_coords_mut(Coords::from_slice_mut(data))
39    }
40
41    pub fn from_coords(coords: &Coords) -> &Self {
42        debug_assert_eq!(
43            coords.len(),
44            2,
45            "Bounding box must have 2 coordinates but instead got {}",
46            coords.len()
47        );
48        debug_assert!(
49            coords[0].lng() <= coords[1].lng(),
50            "Bounding box must have the left side before the right side"
51        );
52        debug_assert!(
53            coords[0].lat() <= coords[1].lat(),
54            "Bounding box must have the bottom side before the top side"
55        );
56        unsafe { mem::transmute(coords) }
57    }
58
59    pub fn from_coords_mut(coords: &mut Coords) -> &mut Self {
60        debug_assert_eq!(
61            coords.len(),
62            2,
63            "Bounding box must have 2 coordinates but instead got {}",
64            coords.len()
65        );
66        debug_assert!(
67            coords[0].lng() <= coords[1].lng(),
68            "Bounding box must have the left side before the right side"
69        );
70        debug_assert!(
71            coords[0].lat() <= coords[1].lat(),
72            "Bounding box must have the bottom side before the top side"
73        );
74        unsafe { mem::transmute(coords) }
75    }
76
77    pub fn write_from_geometry(
78        writer: &mut impl Write,
79        mut points: impl Iterator<Item = Point<f64>>,
80    ) -> Result<(), io::Error> {
81        // if there is no points we ends up with an empty bouding box in 0,0 and on points in the polygon
82        let first_point = points.next().unwrap_or_default();
83        let mut top = first_point.y();
84        let mut bottom = first_point.y();
85        let mut left = first_point.x();
86        let mut right = first_point.x();
87
88        for point in points {
89            if point.y() > top {
90                top = point.y();
91            }
92            if point.y() < bottom {
93                bottom = point.y();
94            }
95            if point.x() < left {
96                left = point.x();
97            }
98            if point.x() > right {
99                right = point.x();
100            }
101        }
102
103        // 1. Write the bounding box
104        //   It's bottom left first
105        writer.write_all(&left.to_ne_bytes())?;
106        writer.write_all(&bottom.to_ne_bytes())?;
107        //   Then the top right
108        writer.write_all(&right.to_ne_bytes())?;
109        writer.write_all(&top.to_ne_bytes())?;
110        Ok(())
111    }
112
113    pub fn coords(&self) -> &Coords {
114        &self.coords
115    }
116
117    pub fn bottom_left(&self) -> &Coord {
118        &self.coords[0]
119    }
120
121    pub fn top_right(&self) -> &Coord {
122        &self.coords[1]
123    }
124
125    pub fn bottom(&self) -> f64 {
126        self.bottom_left().lat()
127    }
128    pub fn top(&self) -> f64 {
129        self.top_right().lat()
130    }
131    pub fn left(&self) -> f64 {
132        self.bottom_left().lng()
133    }
134    pub fn right(&self) -> f64 {
135        self.top_right().lng()
136    }
137
138    pub fn horizontal_range(&self) -> RangeInclusive<f64> {
139        self.left()..=self.right()
140    }
141    pub fn vertical_range(&self) -> RangeInclusive<f64> {
142        self.bottom()..=self.top()
143    }
144
145    pub fn contains_coord(&self, coord: &Coord) -> bool {
146        self.vertical_range().contains(&coord.lat())
147            && self.horizontal_range().contains(&coord.lng())
148    }
149
150    pub fn to_geo(&self) -> geo_types::Rect<f64> {
151        geo_types::Rect::new(self.bottom_left().to_geo(), self.top_right().to_geo())
152    }
153}
154
155impl fmt::Debug for BoundingBox {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        f.debug_struct("BoundingBox")
158            .field("bottom_left", &&self.coords[0])
159            .field("top_right", &&self.coords[1])
160            .finish()
161    }
162}
163
164impl RelationBetweenShapes<Coord> for BoundingBox {
165    fn relation(&self, other: &Coord, relation: InputRelation) -> OutputRelation {
166        if self.contains_coord(other) {
167            relation.to_false().make_strict_contains_if_set()
168        } else {
169            relation.to_false().make_disjoint_if_set()
170        }
171    }
172}
173
174impl RelationBetweenShapes<BoundingBox> for BoundingBox {
175    fn relation(&self, other: &BoundingBox, relation: InputRelation) -> OutputRelation {
176        let relation = relation.to_false();
177
178        let self_vertical_range = self.vertical_range();
179        let self_horizontal_range = self.horizontal_range();
180        let other_vertical_range = other.vertical_range();
181        let other_horizontal_range = other.horizontal_range();
182
183        match (
184            self_vertical_range.contains(other_vertical_range.start()),
185            self_vertical_range.contains(other_vertical_range.end()),
186            self_horizontal_range.contains(other_horizontal_range.start()),
187            self_horizontal_range.contains(other_horizontal_range.end()),
188        ) {
189            (true, true, true, true) => relation.make_strict_contains_if_set(),
190            (false, false, false, false) => {
191                match (
192                    other_vertical_range.contains(self_vertical_range.start()),
193                    other_vertical_range.contains(self_vertical_range.end()),
194                    other_horizontal_range.contains(self_horizontal_range.start()),
195                    other_horizontal_range.contains(self_horizontal_range.end()),
196                ) {
197                    (true, true, true, true) => relation.make_strict_contained_if_set(),
198                    (false, false, false, false) => relation.make_disjoint_if_set(),
199                    _ => relation.make_intersect_if_set(),
200                }
201            }
202            _ => relation.make_intersect_if_set(),
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use bytemuck::cast_slice;
210
211    use super::*;
212
213    #[test]
214    fn test_bounding_box_from_bytes() {
215        let data = [1.0, 2.0, 3.0, 4.0];
216        let bb = BoundingBox::from_bytes(cast_slice(&data));
217        insta::assert_debug_snapshot!(bb, @r"
218        BoundingBox {
219            bottom_left: Coord {
220                x: 1.0,
221                y: 2.0,
222            },
223            top_right: Coord {
224                x: 3.0,
225                y: 4.0,
226            },
227        }
228        ");
229    }
230
231    #[test]
232    #[should_panic]
233    fn test_bounding_box_from_bytes_panic_on_missing_point_bytes() {
234        let data = [1.0, 2.0];
235        BoundingBox::from_bytes(cast_slice(&data));
236    }
237
238    #[test]
239    #[should_panic]
240    fn test_bounding_box_from_bytes_panic_on_too_many_point_bytes() {
241        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
242        BoundingBox::from_bytes(cast_slice(&data));
243    }
244
245    #[test]
246    #[should_panic]
247    fn test_bounding_box_from_bytes_panic_on_too_long_bytes() {
248        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
249        BoundingBox::from_bytes(cast_slice(&data));
250    }
251
252    #[test]
253    #[should_panic]
254    fn test_bounding_box_from_bytes_panic_on_unaligned_bytes() {
255        let data = [1.0, 2.0, 3.0, 4.0, 5.0];
256        BoundingBox::from_bytes(&cast_slice(&data)[1..]);
257    }
258
259    #[test]
260    fn test_bounding_box_from_slice() {
261        let data = [1.0, 2.0, 3.0, 4.0];
262        let bb = BoundingBox::from_slice(&data);
263        insta::assert_debug_snapshot!(bb, @r"
264        BoundingBox {
265            bottom_left: Coord {
266                x: 1.0,
267                y: 2.0,
268            },
269            top_right: Coord {
270                x: 3.0,
271                y: 4.0,
272            },
273        }
274        ");
275    }
276
277    #[test]
278    #[should_panic]
279    fn test_bounding_box_from_slice_panic_on_missing_point_slice() {
280        let data = [1.0, 2.0];
281        BoundingBox::from_slice(&data);
282    }
283
284    #[test]
285    #[should_panic]
286    fn test_bounding_box_from_slice_panic_on_too_many_point_slice() {
287        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
288        BoundingBox::from_slice(&data);
289    }
290
291    #[test]
292    #[should_panic]
293    fn test_bounding_box_from_slice_panic_on_unordered_points() {
294        let data = [1.0, 4.0, 3.0, 2.0];
295        BoundingBox::from_slice(&data);
296    }
297
298    #[test]
299    fn test_bounding_box_contains_coord() {
300        let bb = BoundingBox::from_slice(&[1.0, 2.0, 3.0, 4.0]);
301        assert!(bb.contains_coord(Coord::from_slice(&[2.0, 3.0])));
302        assert!(!bb.contains_coord(Coord::from_slice(&[0.0, 0.0])));
303    }
304
305    #[test]
306    fn test_bounding_box_relation_to_coord() {
307        let bb = BoundingBox::from_slice(&[0.0, 0.0, 10.0, 10.0]);
308        assert!(bb.contains(Coord::from_slice(&[2.0, 3.0])));
309        assert!(bb.contains(Coord::from_slice(&[0.0, 0.0])));
310        assert!(bb.contains(Coord::from_slice(&[10.0, 10.0])));
311        assert!(bb.disjoint(Coord::from_slice(&[11.0, 11.0])));
312        assert!(bb.disjoint(Coord::from_slice(&[-1.0, -1.0])));
313    }
314
315    #[test]
316    fn test_bounding_box_relation_to_bounding_box() {
317        let bb = BoundingBox::from_slice(&[0.0, 0.0, 10.0, 10.0]);
318        assert!(bb.contains(BoundingBox::from_slice(&[1.0, 1.0, 3.0, 3.0])));
319        assert!(bb.intersects(BoundingBox::from_slice(&[-1.0, 0.0, 1.0, 2.0])));
320        assert!(bb.intersects(BoundingBox::from_slice(&[10.0, 0.0, 20.0, 10.0])));
321        assert!(bb.contains(BoundingBox::from_slice(&[0.0, 0.0, 10.0, 10.0])));
322        assert!(bb.contained(BoundingBox::from_slice(&[-1.0, -1.0, 11.0, 11.0])));
323        assert!(bb.disjoint(BoundingBox::from_slice(&[11.0, 11.0, 12.0, 12.0])));
324    }
325}