Skip to main content

geospatial_types/
bounding_box.rs

1//! Axis-aligned rectangular bounding box record.
2
3use crate::coordinate::GeographicCoordinate;
4use serde::{Deserialize, Serialize};
5
6/// Rectangular area between a south-west and a north-east corner.
7///
8/// Boxes never span the antimeridian in this model; integrators needing a
9/// crossing region encode it as two adjacent boxes.
10#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub struct BoundingBox {
12    /// South-west corner (minimum latitude and longitude).
13    pub south_west: GeographicCoordinate,
14    /// North-east corner (maximum latitude and longitude).
15    pub north_east: GeographicCoordinate,
16}
17
18impl BoundingBox {
19    /// Builds a bounding box, rejecting corners that are inverted or outside
20    /// canonical coordinate ranges.
21    pub fn from_corners(
22        south_west: GeographicCoordinate,
23        north_east: GeographicCoordinate,
24    ) -> Result<Self, crate::SDKError> {
25        let _ = GeographicCoordinate::from_degrees(south_west.latitude, south_west.longitude)?;
26        let _ = GeographicCoordinate::from_degrees(north_east.latitude, north_east.longitude)?;
27        if south_west.latitude > north_east.latitude || south_west.longitude > north_east.longitude
28        {
29            return Err(crate::SDKError::InvalidCoordinateInput(
30                "bounding box corners are inverted".to_string(),
31            ));
32        }
33        Ok(Self {
34            south_west,
35            north_east,
36        })
37    }
38
39    /// Reports whether the coordinate lies within the closed box.
40    pub fn contains_coordinate(&self, coordinate: &GeographicCoordinate) -> bool {
41        coordinate.latitude >= self.south_west.latitude
42            && coordinate.latitude <= self.north_east.latitude
43            && coordinate.longitude >= self.south_west.longitude
44            && coordinate.longitude <= self.north_east.longitude
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::BoundingBox;
51    use crate::coordinate::GeographicCoordinate;
52
53    fn coordinate(latitude: f64, longitude: f64) -> GeographicCoordinate {
54        GeographicCoordinate::from_degrees(latitude, longitude).unwrap()
55    }
56
57    #[test]
58    fn contains_points_inside_and_on_edges() {
59        let bounding_box =
60            BoundingBox::from_corners(coordinate(45.0, 5.0), coordinate(49.0, 11.0)).unwrap();
61        assert!(bounding_box.contains_coordinate(&coordinate(47.0, 8.0)));
62        assert!(bounding_box.contains_coordinate(&coordinate(45.0, 5.0)));
63        assert!(bounding_box.contains_coordinate(&coordinate(49.0, 11.0)));
64        assert!(!bounding_box.contains_coordinate(&coordinate(44.9, 8.0)));
65        assert!(!bounding_box.contains_coordinate(&coordinate(47.0, 11.1)));
66    }
67
68    #[test]
69    fn rejects_inverted_corners() {
70        assert!(BoundingBox::from_corners(coordinate(49.0, 11.0), coordinate(45.0, 5.0)).is_err());
71    }
72}