Skip to main content

ndslive_math/
bounding_box.rs

1// SPDX-License-Identifier: BSD-3-Clause
2//! Axis-aligned bounding boxes in NDS coordinate space.
3//!
4//! Faithful port of `python/src/ndslive/math/bounding_box.py`.
5
6use crate::tileid::PackedTileId;
7use crate::wgs84::Wgs84;
8
9/// Axis-aligned bounding box in NDS coordinates (32-bit integers).
10///
11/// - `min_x` / `max_x`: longitude (32-bit signed)
12/// - `min_y` / `max_y`: latitude (31-bit signed)
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct NdsBoundingBox {
15    /// SW corner longitude (NDS coords).
16    pub min_x: i32,
17    /// SW corner latitude (NDS coords).
18    pub min_y: i32,
19    /// NE corner longitude (NDS coords).
20    pub max_x: i32,
21    /// NE corner latitude (NDS coords).
22    pub max_y: i32,
23}
24
25impl NdsBoundingBox {
26    /// Construct a bounding box from explicit corner coordinates.
27    pub fn new(min_x: i32, min_y: i32, max_x: i32, max_y: i32) -> Self {
28        NdsBoundingBox {
29            min_x,
30            min_y,
31            max_x,
32            max_y,
33        }
34    }
35
36    /// Check whether this bounding box intersects (overlaps) `other`.
37    ///
38    /// Boundaries are inclusive: touching edges count as intersecting, matching
39    /// the Python reference.
40    pub fn intersects(&self, other: &NdsBoundingBox) -> bool {
41        !(self.max_x < other.min_x
42            || self.min_x > other.max_x
43            || self.max_y < other.min_y
44            || self.min_y > other.max_y)
45    }
46
47    /// Check whether this bounding box fully contains `other`.
48    pub fn contains(&self, other: &NdsBoundingBox) -> bool {
49        self.min_x <= other.min_x
50            && self.max_x >= other.max_x
51            && self.min_y <= other.min_y
52            && self.max_y >= other.max_y
53    }
54
55    /// Create a bounding box covering a tile's area.
56    ///
57    /// Uses the tile's (exclusive) NE corner directly, matching the Python
58    /// `from_tile`.
59    ///
60    /// Note: NDS coordinates are 32-bit; for very large tiles (e.g. a level-0
61    /// tile whose exclusive NE corner is `2^31`) the corner does not fit in a
62    /// signed `i32` and wraps. The Python reference uses unbounded integers, so
63    /// this is the one place the fixed-width Rust port can differ at the extreme
64    /// boundary. All other corners (level >= 1, or any tile not touching the
65    /// antimeridian / pole) are representable exactly.
66    pub fn from_tile(tile: &PackedTileId) -> Self {
67        let (sw_x, sw_y) = tile.south_west_corner();
68        let (ne_x, ne_y) = tile.north_east_corner();
69        NdsBoundingBox {
70            min_x: sw_x as i32,
71            min_y: sw_y as i32,
72            max_x: ne_x as i32,
73            max_y: ne_y as i32,
74        }
75    }
76
77    /// Create a bounding box from WGS84 corner coordinates.
78    ///
79    /// `sw` is the south-west (min lon/lat) corner, `ne` the north-east corner.
80    pub fn from_wgs84_corners(sw: &Wgs84, ne: &Wgs84) -> Self {
81        let (min_x, min_y) = sw.to_nds_coordinates();
82        let (max_x, max_y) = ne.to_nds_coordinates();
83        NdsBoundingBox {
84            min_x,
85            min_y,
86            max_x,
87            max_y,
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn identical_boxes_intersect_and_contain() {
98        let a = NdsBoundingBox::new(0, 0, 100, 100);
99        let b = NdsBoundingBox::new(0, 0, 100, 100);
100        assert!(a.intersects(&b));
101        assert!(a.contains(&b));
102    }
103
104    #[test]
105    fn disjoint_boxes() {
106        let a = NdsBoundingBox::new(0, 0, 100, 100);
107        let b = NdsBoundingBox::new(200, 200, 300, 300);
108        assert!(!a.intersects(&b));
109        assert!(!a.contains(&b));
110    }
111
112    #[test]
113    fn partial_overlap_no_containment() {
114        let a = NdsBoundingBox::new(0, 0, 100, 100);
115        let b = NdsBoundingBox::new(50, 50, 150, 150);
116        assert!(a.intersects(&b));
117        assert!(!a.contains(&b));
118    }
119
120    #[test]
121    fn inner_box_contained() {
122        let a = NdsBoundingBox::new(0, 0, 100, 100);
123        let b = NdsBoundingBox::new(10, 10, 20, 20);
124        assert!(a.intersects(&b));
125        assert!(a.contains(&b));
126    }
127
128    #[test]
129    fn from_tile_matches_corners() {
130        let t = PackedTileId::from_i64(131072).unwrap();
131        let bb = NdsBoundingBox::from_tile(&t);
132        assert_eq!(bb.min_x, 0);
133        assert_eq!(bb.min_y, 0);
134    }
135}