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#[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 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 writer.write_all(&left.to_ne_bytes())?;
106 writer.write_all(&bottom.to_ne_bytes())?;
107 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}