Skip to main content

nodedb_types/
bbox.rs

1//! Axis-aligned bounding box for 2D geometries (WGS-84).
2//!
3//! Handles the anti-meridian (±180° date line) correctly: when a geometry
4//! crosses the date line, `min_lng > max_lng`. All intersection and
5//! containment logic accounts for this split case.
6
7use crate::geometry::Geometry;
8use serde::{Deserialize, Serialize};
9
10/// An axis-aligned bounding box in WGS-84 coordinates.
11///
12/// **Anti-meridian convention:** When `min_lng > max_lng`, the bbox wraps
13/// across the ±180° date line. For example, a geometry spanning 170°E to
14/// 170°W has `min_lng = 170.0, max_lng = -170.0`.
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
16pub struct BoundingBox {
17    pub min_lng: f64,
18    pub min_lat: f64,
19    pub max_lng: f64,
20    pub max_lat: f64,
21}
22
23impl BoundingBox {
24    pub fn new(min_lng: f64, min_lat: f64, max_lng: f64, max_lat: f64) -> Self {
25        Self {
26            min_lng,
27            min_lat,
28            max_lng,
29            max_lat,
30        }
31    }
32
33    pub fn from_point(lng: f64, lat: f64) -> Self {
34        Self {
35            min_lng: lng,
36            min_lat: lat,
37            max_lng: lng,
38            max_lat: lat,
39        }
40    }
41
42    pub fn crosses_antimeridian(&self) -> bool {
43        self.min_lng > self.max_lng
44    }
45
46    /// Area in approximate square degrees (for R-tree heuristics).
47    pub fn area(&self) -> f64 {
48        let lat_span = self.max_lat - self.min_lat;
49        let lng_span = if self.crosses_antimeridian() {
50            (180.0 - self.min_lng) + (self.max_lng + 180.0)
51        } else {
52            self.max_lng - self.min_lng
53        };
54        lat_span * lng_span
55    }
56
57    /// Half-perimeter (R*-tree margin heuristic).
58    pub fn margin(&self) -> f64 {
59        let lat_span = self.max_lat - self.min_lat;
60        let lng_span = if self.crosses_antimeridian() {
61            (180.0 - self.min_lng) + (self.max_lng + 180.0)
62        } else {
63            self.max_lng - self.min_lng
64        };
65        lat_span + lng_span
66    }
67
68    pub fn contains_point(&self, lng: f64, lat: f64) -> bool {
69        if lat < self.min_lat || lat > self.max_lat {
70            return false;
71        }
72        if self.crosses_antimeridian() {
73            lng >= self.min_lng || lng <= self.max_lng
74        } else {
75            lng >= self.min_lng && lng <= self.max_lng
76        }
77    }
78
79    pub fn contains_bbox(&self, other: &BoundingBox) -> bool {
80        if other.min_lat < self.min_lat || other.max_lat > self.max_lat {
81            return false;
82        }
83        match (self.crosses_antimeridian(), other.crosses_antimeridian()) {
84            (false, false) => other.min_lng >= self.min_lng && other.max_lng <= self.max_lng,
85            (true, false) => other.min_lng >= self.min_lng || other.max_lng <= self.max_lng,
86            (false, true) => false,
87            (true, true) => other.min_lng >= self.min_lng && other.max_lng <= self.max_lng,
88        }
89    }
90
91    pub fn intersects(&self, other: &BoundingBox) -> bool {
92        if self.max_lat < other.min_lat || self.min_lat > other.max_lat {
93            return false;
94        }
95        match (self.crosses_antimeridian(), other.crosses_antimeridian()) {
96            (false, false) => self.min_lng <= other.max_lng && self.max_lng >= other.min_lng,
97            (true, false) => other.max_lng >= self.min_lng || other.min_lng <= self.max_lng,
98            (false, true) => self.max_lng >= other.min_lng || self.min_lng <= other.max_lng,
99            (true, true) => true,
100        }
101    }
102
103    pub fn union(&self, other: &BoundingBox) -> BoundingBox {
104        let min_lat = self.min_lat.min(other.min_lat);
105        let max_lat = self.max_lat.max(other.max_lat);
106        let (min_lng, max_lng) =
107            union_longitude(self.min_lng, self.max_lng, other.min_lng, other.max_lng);
108        BoundingBox {
109            min_lng,
110            min_lat,
111            max_lng,
112            max_lat,
113        }
114    }
115
116    pub fn extend_point(&mut self, lng: f64, lat: f64) {
117        self.min_lat = self.min_lat.min(lat);
118        self.max_lat = self.max_lat.max(lat);
119        if self.crosses_antimeridian() {
120            let in_east = lng >= self.min_lng;
121            let in_west = lng <= self.max_lng;
122            if !in_east && !in_west {
123                if self.min_lng - lng < lng - self.max_lng {
124                    self.min_lng = lng;
125                } else {
126                    self.max_lng = lng;
127                }
128            }
129        } else {
130            self.min_lng = self.min_lng.min(lng);
131            self.max_lng = self.max_lng.max(lng);
132        }
133    }
134
135    /// Expand by meters (equirectangular approximation, <0.5% error ±70°).
136    pub fn expand_meters(&self, meters: f64) -> BoundingBox {
137        let dlat = meters / 110_540.0;
138        let center_lat = (self.min_lat + self.max_lat) / 2.0;
139        let dlng = meters / (111_320.0 * center_lat.to_radians().cos());
140        BoundingBox {
141            min_lng: self.min_lng - dlng,
142            min_lat: self.min_lat - dlat,
143            max_lng: self.max_lng + dlng,
144            max_lat: self.max_lat + dlat,
145        }
146    }
147
148    pub fn overlap_area(&self, other: &BoundingBox) -> f64 {
149        if !self.intersects(other) {
150            return 0.0;
151        }
152        let lat_overlap = self.max_lat.min(other.max_lat) - self.min_lat.max(other.min_lat);
153        if lat_overlap <= 0.0 {
154            return 0.0;
155        }
156        let lng_overlap =
157            lng_overlap_span(self.min_lng, self.max_lng, other.min_lng, other.max_lng);
158        lat_overlap * lng_overlap
159    }
160
161    pub fn enlargement(&self, other: &BoundingBox) -> f64 {
162        self.union(other).area() - self.area()
163    }
164}
165
166fn lng_overlap_span(a_min: f64, a_max: f64, b_min: f64, b_max: f64) -> f64 {
167    let a_wraps = a_min > a_max;
168    let b_wraps = b_min > b_max;
169    match (a_wraps, b_wraps) {
170        (false, false) => (a_max.min(b_max) - a_min.max(b_min)).max(0.0),
171        (true, false) => {
172            let east = (180.0_f64.min(b_max) - a_min.max(b_min)).max(0.0);
173            let west = (a_max.min(b_max) - (-180.0_f64).max(b_min)).max(0.0);
174            east + west
175        }
176        (false, true) => lng_overlap_span(b_min, b_max, a_min, a_max),
177        (true, true) => {
178            let east = (180.0 - a_min.max(b_min)).max(0.0);
179            let west = (a_max.min(b_max) + 180.0).max(0.0);
180            east + west
181        }
182    }
183}
184
185fn union_longitude(a_min: f64, a_max: f64, b_min: f64, b_max: f64) -> (f64, f64) {
186    let a_wraps = a_min > a_max;
187    let b_wraps = b_min > b_max;
188    match (a_wraps, b_wraps) {
189        (false, false) => {
190            let simple_min = a_min.min(b_min);
191            let simple_max = a_max.max(b_max);
192            let simple_span = simple_max - simple_min;
193            let gap = (b_min - a_max).max(0.0).min((a_min - b_max).max(0.0));
194            if gap <= 0.0 {
195                return (simple_min, simple_max);
196            }
197            let wrap_min = a_min.max(b_min);
198            let wrap_max = a_max.min(b_max);
199            let wrap_span = (180.0 - wrap_min) + (wrap_max + 180.0);
200            if wrap_span < simple_span {
201                (wrap_min, wrap_max)
202            } else {
203                (simple_min, simple_max)
204            }
205        }
206        (true, false) => {
207            if b_min >= a_min || b_max <= a_max {
208                (a_min, a_max)
209            } else {
210                let extend_east = a_min - b_min;
211                let extend_west = b_max - a_max;
212                if extend_east <= extend_west {
213                    (b_min, a_max)
214                } else {
215                    (a_min, b_max)
216                }
217            }
218        }
219        (false, true) => union_longitude(b_min, b_max, a_min, a_max),
220        (true, true) => (a_min.min(b_min), a_max.max(b_max)),
221    }
222}
223
224/// Compute bounding box for any Geometry.
225pub fn geometry_bbox(geom: &Geometry) -> BoundingBox {
226    match geom {
227        Geometry::Point { coordinates } => BoundingBox::from_point(coordinates[0], coordinates[1]),
228        Geometry::LineString { coordinates } => bbox_from_coords(coordinates),
229        Geometry::Polygon { coordinates } => {
230            let all: Vec<[f64; 2]> = coordinates.iter().flat_map(|r| r.iter().copied()).collect();
231            bbox_from_coords(&all)
232        }
233        Geometry::MultiPoint { coordinates } => bbox_from_coords(coordinates),
234        Geometry::MultiLineString { coordinates } => {
235            let all: Vec<[f64; 2]> = coordinates
236                .iter()
237                .flat_map(|ls| ls.iter().copied())
238                .collect();
239            bbox_from_coords(&all)
240        }
241        Geometry::MultiPolygon { coordinates } => {
242            let all: Vec<[f64; 2]> = coordinates
243                .iter()
244                .flat_map(|poly| poly.iter().flat_map(|ring| ring.iter().copied()))
245                .collect();
246            bbox_from_coords(&all)
247        }
248        Geometry::GeometryCollection { geometries } => {
249            if geometries.is_empty() {
250                return BoundingBox::new(0.0, 0.0, 0.0, 0.0);
251            }
252            let mut result = geometry_bbox(&geometries[0]);
253            for geom in &geometries[1..] {
254                result = result.union(&geometry_bbox(geom));
255            }
256            result
257        }
258    }
259}
260
261fn bbox_from_coords(coords: &[[f64; 2]]) -> BoundingBox {
262    if coords.is_empty() {
263        return BoundingBox::new(0.0, 0.0, 0.0, 0.0);
264    }
265    let mut min_lng = coords[0][0];
266    let mut min_lat = coords[0][1];
267    let mut max_lng = coords[0][0];
268    let mut max_lat = coords[0][1];
269    for c in &coords[1..] {
270        min_lng = min_lng.min(c[0]);
271        min_lat = min_lat.min(c[1]);
272        max_lng = max_lng.max(c[0]);
273        max_lat = max_lat.max(c[1]);
274    }
275    BoundingBox {
276        min_lng,
277        min_lat,
278        max_lng,
279        max_lat,
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn point_bbox() {
289        let bb = geometry_bbox(&Geometry::point(10.0, 20.0));
290        assert_eq!(bb.min_lng, 10.0);
291        assert_eq!(bb.max_lng, 10.0);
292        assert_eq!(bb.area(), 0.0);
293    }
294
295    #[test]
296    fn polygon_bbox() {
297        let poly = Geometry::polygon(vec![vec![
298            [-10.0, -5.0],
299            [10.0, -5.0],
300            [10.0, 5.0],
301            [-10.0, 5.0],
302            [-10.0, -5.0],
303        ]]);
304        let bb = geometry_bbox(&poly);
305        assert_eq!(bb.min_lng, -10.0);
306        assert_eq!(bb.max_lng, 10.0);
307    }
308
309    #[test]
310    fn contains_point_normal() {
311        let bb = BoundingBox::new(0.0, 0.0, 10.0, 10.0);
312        assert!(bb.contains_point(5.0, 5.0));
313        assert!(!bb.contains_point(11.0, 5.0));
314    }
315
316    #[test]
317    fn contains_point_antimeridian() {
318        let bb = BoundingBox::new(170.0, -10.0, -170.0, 10.0);
319        assert!(bb.crosses_antimeridian());
320        assert!(bb.contains_point(175.0, 0.0));
321        assert!(bb.contains_point(-175.0, 0.0));
322        assert!(!bb.contains_point(0.0, 0.0));
323    }
324
325    #[test]
326    fn intersects_normal() {
327        let a = BoundingBox::new(0.0, 0.0, 10.0, 10.0);
328        let b = BoundingBox::new(5.0, 5.0, 15.0, 15.0);
329        assert!(a.intersects(&b));
330        let c = BoundingBox::new(20.0, 20.0, 30.0, 30.0);
331        assert!(!a.intersects(&c));
332    }
333
334    #[test]
335    fn intersects_antimeridian() {
336        let a = BoundingBox::new(170.0, -5.0, -170.0, 5.0);
337        let b = BoundingBox::new(175.0, 0.0, -175.0, 3.0);
338        assert!(a.intersects(&b));
339        let c = BoundingBox::new(0.0, 0.0, 10.0, 10.0);
340        assert!(!a.intersects(&c));
341    }
342
343    #[test]
344    fn union_normal() {
345        let a = BoundingBox::new(0.0, 0.0, 5.0, 5.0);
346        let b = BoundingBox::new(3.0, 3.0, 10.0, 10.0);
347        let u = a.union(&b);
348        assert_eq!(u.min_lng, 0.0);
349        assert_eq!(u.max_lng, 10.0);
350    }
351
352    #[test]
353    fn overlap_area_partial() {
354        let a = BoundingBox::new(0.0, 0.0, 10.0, 10.0);
355        let b = BoundingBox::new(5.0, 5.0, 15.0, 15.0);
356        assert!((a.overlap_area(&b) - 25.0).abs() < 1e-10);
357    }
358
359    #[test]
360    fn expand_meters_basic() {
361        let bb = BoundingBox::from_point(0.0, 0.0);
362        let expanded = bb.expand_meters(1000.0);
363        assert!((expanded.min_lat - (-0.00905)).abs() < 0.001);
364    }
365
366    #[test]
367    fn geometry_collection_bbox() {
368        let gc = Geometry::GeometryCollection {
369            geometries: vec![Geometry::point(0.0, 0.0), Geometry::point(10.0, 10.0)],
370        };
371        let bb = geometry_bbox(&gc);
372        assert_eq!(bb.min_lng, 0.0);
373        assert_eq!(bb.max_lng, 10.0);
374    }
375}