Skip to main content

rustial_engine/math/
bounds.rs

1//! Geographic and projected bounding boxes.
2//!
3//! # Overview
4//!
5//! This module defines two bounding-box types used throughout the rustial
6//! workspace:
7//!
8//! - [`GeoBounds`] -- a geographic bounding box defined by its southwest and
9//!   northeast corners in WGS-84 degrees.  This is the Rust equivalent of
10//!   MapLibre GL JS's `LngLatBounds`.
11//! - [`WorldBounds`] -- an axis-aligned bounding box in projected world space
12//!   (meters, typically EPSG:3857 / Web Mercator).
13//!
14//! # Coordinate conventions
15//!
16//! | Type | SW corner | NE corner |
17//! |------|-----------|-----------|
18//! | `GeoBounds` | min lon, min lat | max lon, max lat |
19//! | `WorldBounds` | min x, min y, min z | max x, max y, max z |
20//!
21//! # Antimeridian handling
22//!
23//! [`GeoBounds`] supports bounding boxes that cross the 180th meridian.
24//! When `west > east` (i.e. `sw.lon > ne.lon`), the box is understood to
25//! wrap around the antimeridian.  The [`contains_coord`](GeoBounds::contains_coord),
26//! [`intersects`](GeoBounds::intersects), and
27//! [`adjust_antimeridian`](GeoBounds::adjust_antimeridian) methods handle
28//! this correctly.
29
30use crate::coord::{GeoCoord, WorldCoord};
31use std::fmt;
32
33/// Wrap a value into the range `[min, max)`.
34///
35/// Equivalent to MapLibre's `wrap(value, min, max)` utility.
36#[inline]
37fn wrap(value: f64, min: f64, max: f64) -> f64 {
38    let range = max - min;
39    if range == 0.0 {
40        return min;
41    }
42    ((value - min) % range + range) % range + min
43}
44
45// ---------------------------------------------------------------------------
46// GeoBounds
47// ---------------------------------------------------------------------------
48
49/// A geographic bounding box defined by its southwest and northeast corners
50/// in WGS-84 degrees.
51///
52/// This is the Rust equivalent of MapLibre GL JS's `LngLatBounds` and
53/// Mapbox GL JS's `LngLatBounds`.
54///
55/// # Construction
56///
57/// | Method | Description |
58/// |--------|-------------|
59/// | [`new`](Self::new) | From explicit sw/ne corners |
60/// | [`from_coords`](Self::from_coords) | From west, south, east, north |
61/// | [`from_center_radius`](Self::from_center_radius) | Expand a center point by a radius in meters |
62/// | `From<[f64; 4]>` | `[west, south, east, north]` |
63///
64/// # Antimeridian
65///
66/// When `sw.lon > ne.lon`, the bounds wrap across the 180th meridian.
67/// Use [`adjust_antimeridian`](Self::adjust_antimeridian) to unwrap
68/// into a contiguous longitude range (ne.lon may exceed 180).
69///
70/// # Display
71///
72/// Formats as `"GeoBounds(sw: (lat, lon), ne: (lat, lon))"`.
73#[derive(Debug, Clone, Copy, PartialEq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct GeoBounds {
76    /// Southwest corner (minimum latitude, minimum longitude).
77    sw: GeoCoord,
78    /// Northeast corner (maximum latitude, maximum longitude).
79    ne: GeoCoord,
80}
81
82impl GeoBounds {
83    /// Create a geographic bounding box from explicit southwest and
84    /// northeast corners.
85    ///
86    /// The caller should ensure `sw.lat <= ne.lat`.  Longitude may
87    /// have `sw.lon > ne.lon` to represent antimeridian-crossing bounds.
88    #[inline]
89    pub fn new(sw: GeoCoord, ne: GeoCoord) -> Self {
90        Self { sw, ne }
91    }
92
93    /// Create a geographic bounding box from edge values.
94    ///
95    /// Equivalent to MapLibre's `new LngLatBounds([west, south, east, north])`.
96    #[inline]
97    pub fn from_coords(west: f64, south: f64, east: f64, north: f64) -> Self {
98        Self {
99            sw: GeoCoord::from_lat_lon(south, west),
100            ne: GeoCoord::from_lat_lon(north, east),
101        }
102    }
103
104    /// Create a bounding box by expanding a center point by a radius
105    /// in meters.
106    ///
107    /// Uses the same approximation as MapLibre's `LngLatBounds.fromLngLat`:
108    /// latitude accuracy is computed from the Earth's equatorial
109    /// circumference, and longitude accuracy is scaled by `cos(lat)`.
110    ///
111    /// # Arguments
112    ///
113    /// * `center` - The center geographic coordinate.
114    /// * `radius_m` - Distance in meters to expand in all directions.
115    pub fn from_center_radius(center: GeoCoord, radius_m: f64) -> Self {
116        const EARTH_CIRCUMFERENCE_M: f64 = 40_075_017.0;
117        let lat_accuracy = 360.0 * radius_m / EARTH_CIRCUMFERENCE_M;
118        let lon_accuracy = lat_accuracy / (std::f64::consts::PI / 180.0 * center.lat).cos();
119
120        Self {
121            sw: GeoCoord::from_lat_lon(center.lat - lat_accuracy, center.lon - lon_accuracy),
122            ne: GeoCoord::from_lat_lon(center.lat + lat_accuracy, center.lon + lon_accuracy),
123        }
124    }
125
126    // -- Accessors --------------------------------------------------------
127
128    /// Southwest corner.
129    #[inline]
130    pub fn sw(&self) -> GeoCoord {
131        self.sw
132    }
133
134    /// Northeast corner.
135    #[inline]
136    pub fn ne(&self) -> GeoCoord {
137        self.ne
138    }
139
140    /// Northwest corner.
141    #[inline]
142    pub fn nw(&self) -> GeoCoord {
143        GeoCoord::from_lat_lon(self.ne.lat, self.sw.lon)
144    }
145
146    /// Southeast corner.
147    #[inline]
148    pub fn se(&self) -> GeoCoord {
149        GeoCoord::from_lat_lon(self.sw.lat, self.ne.lon)
150    }
151
152    /// West edge (longitude).
153    #[inline]
154    pub fn west(&self) -> f64 {
155        self.sw.lon
156    }
157
158    /// South edge (latitude).
159    #[inline]
160    pub fn south(&self) -> f64 {
161        self.sw.lat
162    }
163
164    /// East edge (longitude).
165    #[inline]
166    pub fn east(&self) -> f64 {
167        self.ne.lon
168    }
169
170    /// North edge (latitude).
171    #[inline]
172    pub fn north(&self) -> f64 {
173        self.ne.lat
174    }
175
176    /// Geographic center of the bounding box.
177    ///
178    /// Equivalent to MapLibre's `LngLatBounds.getCenter()`.
179    #[inline]
180    pub fn center(&self) -> GeoCoord {
181        GeoCoord::from_lat_lon(
182            (self.sw.lat + self.ne.lat) / 2.0,
183            (self.sw.lon + self.ne.lon) / 2.0,
184        )
185    }
186
187    // -- Mutation / extension ---------------------------------------------
188
189    /// Extend the bounds to include a single geographic coordinate.
190    ///
191    /// Equivalent to MapLibre's `LngLatBounds.extend(LngLat)`.
192    pub fn extend_coord(&mut self, coord: GeoCoord) {
193        self.sw.lat = self.sw.lat.min(coord.lat);
194        self.sw.lon = self.sw.lon.min(coord.lon);
195        self.ne.lat = self.ne.lat.max(coord.lat);
196        self.ne.lon = self.ne.lon.max(coord.lon);
197    }
198
199    /// Extend the bounds to include another bounding box.
200    ///
201    /// Equivalent to MapLibre's `LngLatBounds.extend(LngLatBounds)`.
202    pub fn extend_bounds(&mut self, other: &GeoBounds) {
203        self.sw.lat = self.sw.lat.min(other.sw.lat);
204        self.sw.lon = self.sw.lon.min(other.sw.lon);
205        self.ne.lat = self.ne.lat.max(other.ne.lat);
206        self.ne.lon = self.ne.lon.max(other.ne.lon);
207    }
208
209    // -- Queries ----------------------------------------------------------
210
211    /// Check if a geographic coordinate is within the bounding box.
212    ///
213    /// Handles antimeridian-crossing bounds (where `west > east`).
214    ///
215    /// Equivalent to MapLibre's `LngLatBounds.contains()`.
216    pub fn contains_coord(&self, coord: &GeoCoord) -> bool {
217        let lat_ok = self.sw.lat <= coord.lat && coord.lat <= self.ne.lat;
218
219        let lon_ok = if self.sw.lon > self.ne.lon {
220            // Antimeridian-crossing: longitude is inside when it is NOT
221            // in the gap between ne.lon and sw.lon.
222            self.sw.lon <= coord.lon || coord.lon <= self.ne.lon
223        } else {
224            self.sw.lon <= coord.lon && coord.lon <= self.ne.lon
225        };
226
227        lat_ok && lon_ok
228    }
229
230    /// Check if this bounding box intersects another.
231    ///
232    /// Returns `true` if the bounding boxes share any area, including
233    /// cases where they only touch along an edge or at a corner.
234    ///
235    /// Properly handles cases where either or both bounding boxes cross
236    /// the antimeridian.
237    ///
238    /// Equivalent to MapLibre's `LngLatBounds.intersects()`.
239    pub fn intersects(&self, other: &GeoBounds) -> bool {
240        // Latitude check (simple range overlap).
241        let lat_ok = other.north() >= self.south() && other.south() <= self.north();
242        if !lat_ok {
243            return false;
244        }
245
246        // Check if either bound covers the full world (span >= 360 deg).
247        let this_span = (self.east() - self.west()).abs();
248        let other_span = (other.east() - other.west()).abs();
249        if this_span >= 360.0 || other_span >= 360.0 {
250            return true;
251        }
252
253        // Normalise longitudes to [-180, 180].
254        let this_west = wrap(self.west(), -180.0, 180.0);
255        let this_east = wrap(self.east(), -180.0, 180.0);
256        let other_west = wrap(other.west(), -180.0, 180.0);
257        let other_east = wrap(other.east(), -180.0, 180.0);
258
259        // Strict inequality: equal values indicate zero-width (point),
260        // not wrapping.
261        let this_wraps = this_west > this_east;
262        let other_wraps = other_west > other_east;
263
264        if this_wraps && other_wraps {
265            return true;
266        }
267
268        if this_wraps {
269            return other_east >= this_west || other_west <= this_east;
270        }
271
272        if other_wraps {
273            return this_east >= other_west || this_west <= other_east;
274        }
275
276        // Neither wraps: standard interval overlap.
277        other_west <= this_east && other_east >= this_west
278    }
279
280    // -- Antimeridian adjustment ------------------------------------------
281
282    /// Adjust bounds that cross the antimeridian so that
283    /// `ne.lon >= sw.lon`.
284    ///
285    /// When `sw.lon > ne.lon` (crossing the 180th meridian), this adds
286    /// 360 to `ne.lon` so the bounds form a contiguous longitude range.
287    /// The resulting `ne.lon` may exceed 180.
288    ///
289    /// Equivalent to MapLibre's `LngLatBounds.adjustAntiMeridian()`.
290    pub fn adjust_antimeridian(&self) -> Self {
291        if self.sw.lon > self.ne.lon {
292            Self {
293                sw: self.sw,
294                ne: GeoCoord {
295                    lat: self.ne.lat,
296                    lon: self.ne.lon + 360.0,
297                    alt: self.ne.alt,
298                },
299            }
300        } else {
301            *self
302        }
303    }
304
305    /// Return the bounding box as `[west, south, east, north]`.
306    #[inline]
307    pub fn to_array(&self) -> [f64; 4] {
308        [self.sw.lon, self.sw.lat, self.ne.lon, self.ne.lat]
309    }
310}
311
312impl fmt::Display for GeoBounds {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        write!(
315            f,
316            "GeoBounds(sw: ({:.6}, {:.6}), ne: ({:.6}, {:.6}))",
317            self.sw.lat, self.sw.lon, self.ne.lat, self.ne.lon
318        )
319    }
320}
321
322// -- From conversions for GeoBounds ----------------------------------------
323
324impl From<[f64; 4]> for GeoBounds {
325    /// Create from `[west, south, east, north]`.
326    #[inline]
327    fn from(arr: [f64; 4]) -> Self {
328        Self::from_coords(arr[0], arr[1], arr[2], arr[3])
329    }
330}
331
332impl From<GeoBounds> for [f64; 4] {
333    /// Convert to `[west, south, east, north]`.
334    #[inline]
335    fn from(b: GeoBounds) -> Self {
336        b.to_array()
337    }
338}
339
340// ---------------------------------------------------------------------------
341// WorldBounds
342// ---------------------------------------------------------------------------
343
344/// An axis-aligned bounding box in projected world space (meters).
345///
346/// The coordinate system matches [`WorldCoord`]: right-handed Z-up,
347/// X east, Y north, Z up.
348///
349/// `min` contains the component-wise minimum and `max` the component-wise
350/// maximum.  For 2D tile operations `min.z` and `max.z` are typically 0.
351///
352/// # Construction
353///
354/// | Method | Description |
355/// |--------|-------------|
356/// | [`new`](Self::new) | From explicit min/max corners |
357/// | [`from_min_max`](Self::from_min_max) | Alias for `new` |
358///
359/// # Display
360///
361/// Formats as `"WorldBounds(min: ..., max: ...)"`.
362#[derive(Debug, Clone, Copy, PartialEq)]
363#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
364pub struct WorldBounds {
365    /// Component-wise minimum corner (south-west-bottom).
366    pub min: WorldCoord,
367    /// Component-wise maximum corner (north-east-top).
368    pub max: WorldCoord,
369}
370
371impl WorldBounds {
372    /// Create a world-space bounding box from explicit min/max corners.
373    #[inline]
374    pub fn new(min: WorldCoord, max: WorldCoord) -> Self {
375        Self { min, max }
376    }
377
378    /// Alias for [`new`](Self::new).
379    #[inline]
380    pub fn from_min_max(min: WorldCoord, max: WorldCoord) -> Self {
381        Self { min, max }
382    }
383
384    /// Center point of the bounding box.
385    #[inline]
386    pub fn center(&self) -> WorldCoord {
387        WorldCoord::new(
388            (self.min.position.x + self.max.position.x) * 0.5,
389            (self.min.position.y + self.max.position.y) * 0.5,
390            (self.min.position.z + self.max.position.z) * 0.5,
391        )
392    }
393
394    /// Size of the bounding box along each axis (meters).
395    #[inline]
396    pub fn size(&self) -> (f64, f64, f64) {
397        (
398            self.max.position.x - self.min.position.x,
399            self.max.position.y - self.min.position.y,
400            self.max.position.z - self.min.position.z,
401        )
402    }
403
404    /// Whether a world-space point is inside the bounding box (inclusive).
405    #[inline]
406    pub fn contains_point(&self, point: &WorldCoord) -> bool {
407        point.position.x >= self.min.position.x
408            && point.position.x <= self.max.position.x
409            && point.position.y >= self.min.position.y
410            && point.position.y <= self.max.position.y
411            && point.position.z >= self.min.position.z
412            && point.position.z <= self.max.position.z
413    }
414
415    /// Whether two world-space bounding boxes overlap (inclusive).
416    #[inline]
417    pub fn intersects(&self, other: &WorldBounds) -> bool {
418        self.min.position.x <= other.max.position.x
419            && self.max.position.x >= other.min.position.x
420            && self.min.position.y <= other.max.position.y
421            && self.max.position.y >= other.min.position.y
422            && self.min.position.z <= other.max.position.z
423            && self.max.position.z >= other.min.position.z
424    }
425
426    /// Extend the bounds to include another bounding box.
427    pub fn extend(&mut self, other: &WorldBounds) {
428        self.min = WorldCoord::new(
429            self.min.position.x.min(other.min.position.x),
430            self.min.position.y.min(other.min.position.y),
431            self.min.position.z.min(other.min.position.z),
432        );
433        self.max = WorldCoord::new(
434            self.max.position.x.max(other.max.position.x),
435            self.max.position.y.max(other.max.position.y),
436            self.max.position.z.max(other.max.position.z),
437        );
438    }
439
440    /// Extend the bounds to include a single world-space point.
441    pub fn extend_point(&mut self, point: &WorldCoord) {
442        self.min = WorldCoord::new(
443            self.min.position.x.min(point.position.x),
444            self.min.position.y.min(point.position.y),
445            self.min.position.z.min(point.position.z),
446        );
447        self.max = WorldCoord::new(
448            self.max.position.x.max(point.position.x),
449            self.max.position.y.max(point.position.y),
450            self.max.position.z.max(point.position.z),
451        );
452    }
453}
454
455impl fmt::Display for WorldBounds {
456    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457        write!(f, "WorldBounds(min: {}, max: {})", self.min, self.max)
458    }
459}
460
461// ---------------------------------------------------------------------------
462// Tests
463// ---------------------------------------------------------------------------
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    // -- GeoBounds construction -------------------------------------------
470
471    #[test]
472    fn geo_bounds_new() {
473        let sw = GeoCoord::from_lat_lon(40.7661, -73.9876);
474        let ne = GeoCoord::from_lat_lon(40.8002, -73.9397);
475        let b = GeoBounds::new(sw, ne);
476        assert_eq!(b.sw(), sw);
477        assert_eq!(b.ne(), ne);
478    }
479
480    #[test]
481    fn geo_bounds_from_coords() {
482        let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
483        assert!((b.west() - (-73.9876)).abs() < 1e-10);
484        assert!((b.south() - 40.7661).abs() < 1e-10);
485        assert!((b.east() - (-73.9397)).abs() < 1e-10);
486        assert!((b.north() - 40.8002).abs() < 1e-10);
487    }
488
489    #[test]
490    fn geo_bounds_from_array() {
491        let b: GeoBounds = [-73.9876, 40.7661, -73.9397, 40.8002].into();
492        assert!((b.west() - (-73.9876)).abs() < 1e-10);
493        assert!((b.north() - 40.8002).abs() < 1e-10);
494    }
495
496    #[test]
497    fn geo_bounds_to_array_roundtrip() {
498        let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
499        let arr: [f64; 4] = b.into();
500        assert_eq!(arr, b.to_array());
501    }
502
503    // -- GeoBounds corner/edge accessors ----------------------------------
504
505    #[test]
506    fn geo_bounds_corners() {
507        let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
508        let nw = b.nw();
509        assert!((nw.lat - 40.8002).abs() < 1e-10);
510        assert!((nw.lon - (-73.9876)).abs() < 1e-10);
511        let se = b.se();
512        assert!((se.lat - 40.7661).abs() < 1e-10);
513        assert!((se.lon - (-73.9397)).abs() < 1e-10);
514    }
515
516    // -- GeoBounds center -------------------------------------------------
517
518    #[test]
519    fn geo_bounds_center() {
520        let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
521        let c = b.center();
522        assert!((c.lon - (-73.96365)).abs() < 1e-4);
523        assert!((c.lat - 40.78315).abs() < 1e-4);
524    }
525
526    // -- GeoBounds extend -------------------------------------------------
527
528    #[test]
529    fn geo_bounds_extend_coord() {
530        let mut b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
531        b.extend_coord(GeoCoord::from_lat_lon(41.0, -74.0));
532        assert!((b.north() - 41.0).abs() < 1e-10);
533        assert!((b.west() - (-74.0)).abs() < 1e-10);
534    }
535
536    #[test]
537    fn geo_bounds_extend_bounds() {
538        let mut a = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
539        let b = GeoBounds::from_coords(-74.0, 40.5, -73.5, 41.0);
540        a.extend_bounds(&b);
541        assert!((a.west() - (-74.0)).abs() < 1e-10);
542        assert!((a.south() - 40.5).abs() < 1e-10);
543        assert!((a.east() - (-73.5)).abs() < 1e-10);
544        assert!((a.north() - 41.0).abs() < 1e-10);
545    }
546
547    // -- GeoBounds contains -----------------------------------------------
548
549    #[test]
550    fn geo_bounds_contains_inside() {
551        let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
552        let p = GeoCoord::from_lat_lon(40.7789, -73.9567);
553        assert!(b.contains_coord(&p));
554    }
555
556    #[test]
557    fn geo_bounds_contains_outside() {
558        let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
559        let p = GeoCoord::from_lat_lon(41.0, -73.9567);
560        assert!(!b.contains_coord(&p));
561    }
562
563    #[test]
564    fn geo_bounds_contains_antimeridian() {
565        // Bounds crossing the antimeridian: from 170 E to 170 W (= -170)
566        let b = GeoBounds::from_coords(170.0, -20.0, -170.0, -10.0);
567        // A point at 175 E should be inside.
568        let inside = GeoCoord::from_lat_lon(-15.0, 175.0);
569        assert!(b.contains_coord(&inside));
570        // A point at 0 should be outside.
571        let outside = GeoCoord::from_lat_lon(-15.0, 0.0);
572        assert!(!b.contains_coord(&outside));
573    }
574
575    // -- GeoBounds intersects ---------------------------------------------
576
577    #[test]
578    fn geo_bounds_intersects_overlapping() {
579        let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
580        let b = GeoBounds::from_coords(-73.5, 40.5, -72.5, 41.5);
581        assert!(a.intersects(&b));
582        assert!(b.intersects(&a));
583    }
584
585    #[test]
586    fn geo_bounds_intersects_disjoint() {
587        let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
588        let b = GeoBounds::from_coords(10.0, 50.0, 11.0, 51.0);
589        assert!(!a.intersects(&b));
590    }
591
592    #[test]
593    fn geo_bounds_intersects_touching_edge() {
594        let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
595        let b = GeoBounds::from_coords(-73.0, 41.0, -72.0, 42.0);
596        assert!(a.intersects(&b));
597    }
598
599    #[test]
600    fn geo_bounds_intersects_antimeridian_both_wrap() {
601        let a = GeoBounds::from_coords(170.0, -20.0, -170.0, -10.0);
602        let b = GeoBounds::from_coords(160.0, -25.0, -160.0, -5.0);
603        assert!(a.intersects(&b));
604    }
605
606    #[test]
607    fn geo_bounds_intersects_full_world() {
608        let full = GeoBounds::from_coords(-180.0, -90.0, 180.0, 90.0);
609        let small = GeoBounds::from_coords(10.0, 10.0, 11.0, 11.0);
610        assert!(full.intersects(&small));
611        assert!(small.intersects(&full));
612    }
613
614    // -- GeoBounds from_center_radius -------------------------------------
615
616    #[test]
617    fn geo_bounds_from_center_radius_zero() {
618        let center = GeoCoord::from_lat_lon(40.7736, -73.9749);
619        let b = GeoBounds::from_center_radius(center, 0.0);
620        assert!((b.sw().lat - center.lat).abs() < 1e-10);
621        assert!((b.ne().lat - center.lat).abs() < 1e-10);
622    }
623
624    #[test]
625    fn geo_bounds_from_center_radius_100m() {
626        let center = GeoCoord::from_lat_lon(40.7736, -73.9749);
627        let b = GeoBounds::from_center_radius(center, 100.0);
628        assert!(b.sw().lat < center.lat);
629        assert!(b.ne().lat > center.lat);
630        assert!(b.sw().lon < center.lon);
631        assert!(b.ne().lon > center.lon);
632        // Lat span: 2 * 360 * 100 / 40075017 ~ 0.001796 deg.
633        let lat_span = b.ne().lat - b.sw().lat;
634        assert!((lat_span - 0.001796).abs() < 0.0001);
635    }
636
637    // -- GeoBounds adjust_antimeridian ------------------------------------
638
639    #[test]
640    fn geo_bounds_adjust_antimeridian_no_wrap() {
641        let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
642        let adjusted = b.adjust_antimeridian();
643        assert_eq!(b, adjusted);
644    }
645
646    #[test]
647    fn geo_bounds_adjust_antimeridian_wrap() {
648        let b = GeoBounds::from_coords(175.0, -20.0, -178.0, -15.0);
649        let adjusted = b.adjust_antimeridian();
650        assert!((adjusted.sw().lon - 175.0).abs() < 1e-10);
651        assert!((adjusted.ne().lon - 182.0).abs() < 1e-10);
652    }
653
654    // -- GeoBounds Display ------------------------------------------------
655
656    #[test]
657    fn geo_bounds_display() {
658        let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
659        let s = format!("{b}");
660        assert!(s.contains("GeoBounds"));
661        assert!(s.contains("sw:"));
662        assert!(s.contains("ne:"));
663    }
664
665    // -- WorldBounds construction -----------------------------------------
666
667    #[test]
668    fn world_bounds_new() {
669        let b = WorldBounds::new(
670            WorldCoord::new(-100.0, -200.0, 0.0),
671            WorldCoord::new(100.0, 200.0, 50.0),
672        );
673        assert_eq!(b.min.position.x, -100.0);
674        assert_eq!(b.max.position.y, 200.0);
675    }
676
677    // -- WorldBounds center -----------------------------------------------
678
679    #[test]
680    fn world_bounds_center() {
681        let b = WorldBounds::new(
682            WorldCoord::new(-100.0, -200.0, 0.0),
683            WorldCoord::new(100.0, 200.0, 50.0),
684        );
685        let c = b.center();
686        assert!((c.position.x).abs() < 1e-10);
687        assert!((c.position.y).abs() < 1e-10);
688        assert!((c.position.z - 25.0).abs() < 1e-10);
689    }
690
691    // -- WorldBounds size -------------------------------------------------
692
693    #[test]
694    fn world_bounds_size() {
695        let b = WorldBounds::new(
696            WorldCoord::new(-100.0, -200.0, 0.0),
697            WorldCoord::new(100.0, 200.0, 50.0),
698        );
699        let (sx, sy, sz) = b.size();
700        assert!((sx - 200.0).abs() < 1e-10);
701        assert!((sy - 400.0).abs() < 1e-10);
702        assert!((sz - 50.0).abs() < 1e-10);
703    }
704
705    // -- WorldBounds contains_point ---------------------------------------
706
707    #[test]
708    fn world_bounds_contains_point() {
709        let b = WorldBounds::new(
710            WorldCoord::new(-100.0, -200.0, 0.0),
711            WorldCoord::new(100.0, 200.0, 50.0),
712        );
713        assert!(b.contains_point(&WorldCoord::new(0.0, 0.0, 25.0)));
714        assert!(!b.contains_point(&WorldCoord::new(200.0, 0.0, 0.0)));
715    }
716
717    // -- WorldBounds intersects -------------------------------------------
718
719    #[test]
720    fn world_bounds_intersects() {
721        let a = WorldBounds::new(
722            WorldCoord::new(-100.0, -100.0, 0.0),
723            WorldCoord::new(100.0, 100.0, 0.0),
724        );
725        let b = WorldBounds::new(
726            WorldCoord::new(50.0, 50.0, 0.0),
727            WorldCoord::new(200.0, 200.0, 0.0),
728        );
729        assert!(a.intersects(&b));
730    }
731
732    #[test]
733    fn world_bounds_disjoint() {
734        let a = WorldBounds::new(
735            WorldCoord::new(-100.0, -100.0, 0.0),
736            WorldCoord::new(-50.0, -50.0, 0.0),
737        );
738        let b = WorldBounds::new(
739            WorldCoord::new(50.0, 50.0, 0.0),
740            WorldCoord::new(200.0, 200.0, 0.0),
741        );
742        assert!(!a.intersects(&b));
743    }
744
745    // -- WorldBounds extend -----------------------------------------------
746
747    #[test]
748    fn world_bounds_extend() {
749        let mut a = WorldBounds::new(
750            WorldCoord::new(-100.0, -100.0, 0.0),
751            WorldCoord::new(100.0, 100.0, 0.0),
752        );
753        let b = WorldBounds::new(
754            WorldCoord::new(-200.0, 50.0, -10.0),
755            WorldCoord::new(50.0, 300.0, 10.0),
756        );
757        a.extend(&b);
758        assert!((a.min.position.x - (-200.0)).abs() < 1e-10);
759        assert!((a.min.position.y - (-100.0)).abs() < 1e-10);
760        assert!((a.max.position.y - 300.0).abs() < 1e-10);
761    }
762
763    #[test]
764    fn world_bounds_extend_point() {
765        let mut a = WorldBounds::new(
766            WorldCoord::new(0.0, 0.0, 0.0),
767            WorldCoord::new(10.0, 10.0, 0.0),
768        );
769        a.extend_point(&WorldCoord::new(-5.0, 15.0, 3.0));
770        assert!((a.min.position.x - (-5.0)).abs() < 1e-10);
771        assert!((a.max.position.y - 15.0).abs() < 1e-10);
772        assert!((a.max.position.z - 3.0).abs() < 1e-10);
773    }
774
775    // -- WorldBounds Display ----------------------------------------------
776
777    #[test]
778    fn world_bounds_display() {
779        let b = WorldBounds::new(
780            WorldCoord::new(1.0, 2.0, 3.0),
781            WorldCoord::new(4.0, 5.0, 6.0),
782        );
783        let s = format!("{b}");
784        assert!(s.contains("WorldBounds"));
785    }
786}