Skip to main content

utiles_core/
bbox.rs

1//! Bounding-boxes!
2use serde::{Deserialize, Serialize};
3
4use crate::lnglat::LngLat;
5use crate::parsing::parse_bbox;
6use crate::tile::Tile;
7use crate::tile_like::TileLike;
8use crate::{Point2d, xy};
9
10/// Bounding box tuple
11#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
12pub struct BBoxTuple(f64, f64, f64, f64);
13
14/// Bounding box struct
15#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
16pub struct BBox {
17    /// west/left boundary
18    pub west: f64,
19    /// south/bottom boundary
20    pub south: f64,
21    /// east/right boundary
22    pub east: f64,
23    /// north/top boundary
24    pub north: f64,
25}
26
27/// Web Mercator Bounding box struct
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct WebBBox {
30    /// lower-left corner (west, south)/(left, bottom)
31    min: Point2d<f64>,
32
33    /// upper-right corner (east, north)/(right, top)
34    max: Point2d<f64>,
35}
36
37/// Bounding box containable enum
38pub enum BBoxContainable {
39    /// `LngLat`
40    LngLat(LngLat),
41    /// `BBox`
42    BBox(BBox),
43    /// Tile
44    Tile(Tile),
45}
46
47impl From<(f64, f64, f64, f64)> for BBox {
48    fn from(bbox: (f64, f64, f64, f64)) -> Self {
49        Self::new(bbox.0, bbox.1, bbox.2, bbox.3)
50    }
51}
52
53impl From<BBox> for (f64, f64, f64, f64) {
54    fn from(bbox: BBox) -> Self {
55        (bbox.west, bbox.south, bbox.east, bbox.north)
56    }
57}
58
59impl From<(i32, i32, i32, i32)> for BBox {
60    fn from(bbox: (i32, i32, i32, i32)) -> Self {
61        // convert to f64
62        (
63            f64::from(bbox.0),
64            f64::from(bbox.1),
65            f64::from(bbox.2),
66            f64::from(bbox.3),
67        )
68            .into()
69        // BBox {
70        //     north: bbox.0,
71        //     south: bbox.1,
72        //     east: bbox.2,
73        //     west: bbox.3,
74        // }
75    }
76}
77
78impl BBox {
79    /// Create a new `BBox`
80    #[must_use]
81    pub const fn new(west: f64, south: f64, east: f64, north: f64) -> Self {
82        Self {
83            west,
84            south,
85            east,
86            north,
87        }
88    }
89
90    /// Returns a bounding box that covers the entire world.
91    #[must_use]
92    pub const fn world_planet() -> Self {
93        Self {
94            west: -180.0,
95            south: -90.0,
96            east: 180.0,
97            north: 90.0,
98        }
99    }
100
101    /// Returns a bounding box that covers the entire web mercator world.
102    #[must_use]
103    pub const fn world_web() -> Self {
104        Self {
105            west: -180.0,
106            south: -85.051_129,
107            east: 180.0,
108            north: 85.051_129,
109        }
110    }
111
112    #[must_use]
113    pub const fn clamp_web(&self) -> Self {
114        Self {
115            west: self.west.max(-180.0),
116            south: self.south.max(-85.051_129),
117            east: self.east.min(180.0),
118            north: self.north.min(85.051_129),
119        }
120    }
121
122    #[must_use]
123    pub const fn clamp(&self, o: &Self) -> Self {
124        Self {
125            west: self.west.max(o.west),
126            south: self.south.max(o.south),
127            east: self.east.min(o.east),
128            north: self.north.min(o.north),
129        }
130    }
131
132    #[must_use]
133    pub fn geo_wrap(&self) -> Self {
134        let east = LngLat::geo_wrap_lng(self.east);
135        let west = LngLat::geo_wrap_lng(self.west);
136
137        Self {
138            west,
139            south: self.south,
140            east,
141            north: self.north,
142        }
143    }
144
145    #[must_use]
146    pub fn is_antimeridian(&self) -> bool {
147        self.west > self.east
148    }
149
150    /// Returns true if the bounding box crosses the antimeridian (the 180-degree meridian).
151    #[must_use]
152    pub fn crosses_antimeridian(&self) -> bool {
153        self.west > self.east
154    }
155
156    /// Returns the bounding box as a tuple
157    #[must_use]
158    pub const fn tuple(&self) -> (f64, f64, f64, f64) {
159        (self.west(), self.south(), self.east(), self.north())
160    }
161
162    /// Returns the top/north boundary of the bounding box
163    #[must_use]
164    pub const fn north(&self) -> f64 {
165        self.north
166    }
167
168    /// Returns the bottom/south boundary of the bounding box
169    #[must_use]
170    pub const fn south(&self) -> f64 {
171        self.south
172    }
173
174    /// Returns the right/east boundary of the bounding box
175    #[must_use]
176    pub const fn east(&self) -> f64 {
177        self.east
178    }
179
180    /// Returns the left/west boundary of the bounding box
181    #[must_use]
182    pub const fn west(&self) -> f64 {
183        self.west
184    }
185
186    /// Returns the top/north boundary of the bounding box
187    #[must_use]
188    pub const fn top(&self) -> f64 {
189        self.north
190    }
191
192    /// Returns the bottom/south boundary of the bounding box
193    #[must_use]
194    pub const fn bottom(&self) -> f64 {
195        self.south
196    }
197
198    /// Returns the right/east boundary of the bounding box
199    #[must_use]
200    pub const fn right(&self) -> f64 {
201        self.east
202    }
203
204    /// Returns the left/west boundary of the bounding box
205    #[must_use]
206    pub const fn left(&self) -> f64 {
207        self.west
208    }
209
210    /// Returns the geojson tuple/array representation of the bounding box
211    #[must_use]
212    #[inline]
213    pub fn json_arr(&self) -> String {
214        format!(
215            "[{},{},{},{}]",
216            self.west(),
217            self.south(),
218            self.east(),
219            self.north()
220        )
221    }
222
223    /// Returns the gdal-ish string representation of the bounding box
224    #[must_use]
225    #[inline]
226    pub fn projwin_str(&self) -> String {
227        format!(
228            "{} {} {} {}",
229            self.west(),
230            self.north(),
231            self.east(),
232            self.south()
233        )
234    }
235
236    /// Returns the center of the bounding box as a `LngLat`
237    #[must_use]
238    pub fn contains_lnglat(&self, lnglat: &LngLat) -> bool {
239        let lng = lnglat.lng();
240        let lat = lnglat.lat();
241        if self.crosses_antimeridian() {
242            if (lng >= self.west || lng <= self.east)
243                && lat >= self.south
244                && lat <= self.north
245            {
246                return true;
247            }
248        } else if lng >= self.west
249            && lng <= self.east
250            && lat >= self.south
251            && lat <= self.north
252        {
253            return true;
254        }
255        false
256    }
257
258    /// Returns true if the current instance contains the given `Tile`
259    #[must_use]
260    pub fn contains_tile(&self, tile: &Tile) -> bool {
261        let bbox = tile.bbox();
262        self.contains_bbox(&bbox.into())
263    }
264
265    /// Returns true if the current instance contains the given `BBox`
266    #[must_use]
267    pub fn contains_bbox(&self, other: &Self) -> bool {
268        if self.is_antimeridian() {
269            // BBox crosses the antimeridian
270            if other.is_antimeridian() {
271                // Other BBox also crosses the antimeridian
272                if self.west <= other.west && self.east >= other.east {
273                    // The current BBox contains the other BBox
274                    self.south <= other.south && self.north >= other.north
275                } else {
276                    false
277                }
278                // Other BBox does not cross the antimeridian
279            } else if self.west <= other.west || self.east >= other.east {
280                // The current BBox contains the other BBox
281                self.south <= other.south && self.north >= other.north
282            } else {
283                false
284            }
285        } else {
286            self.north >= other.north
287                && self.south <= other.south
288                && self.east >= other.east
289                && self.west <= other.west
290        }
291    }
292
293    /// Returns true if the current instance contains the given `BBoxContainable` object.
294    #[must_use]
295    pub fn contains(&self, other: &BBoxContainable) -> bool {
296        match other {
297            BBoxContainable::LngLat(lnglat) => self.contains_lnglat(lnglat),
298            BBoxContainable::BBox(bbox) => self.contains_bbox(bbox),
299            BBoxContainable::Tile(tile) => self.contains_tile(tile),
300        }
301    }
302
303    /// Returns true if the current instance is within the given bounding box.
304    #[must_use]
305    pub fn is_within(&self, other: &Self) -> bool {
306        self.north <= other.north
307            && self.south >= other.south
308            && self.east <= other.east
309            && self.west >= other.west
310    }
311
312    /// Returns true if the current instance intersects with the given bounding box.
313    #[must_use]
314    pub fn intersects(&self, other: &Self) -> bool {
315        self.north >= other.south
316            && self.south <= other.north
317            && self.east >= other.west
318            && self.west <= other.east
319    }
320
321    /// Returns a vector of bounding boxes (`BBox`) associated with the current instance.
322    ///
323    /// If the instance crosses the antimeridian (the 180-degree meridian), this function
324    /// returns two `BBox` instances:
325    /// - The first bounding box covers the area from the object's western boundary to 180 degrees east.
326    /// - The second bounding box covers the area from -180 degrees west to the object's eastern boundary.
327    ///
328    /// If the instance does not cross the antimeridian, the function returns a vector
329    /// containing a single `BBox` that represents the current instance itself.
330    ///
331    /// # Returns
332    /// `Vec<BBox>`: A vector containing one `BBox` if the instance does not cross the antimeridian,
333    /// or two `BBox`es if it does.
334    ///
335    /// # Examples
336    ///
337    /// ```
338    /// use utiles_core::BBox;
339    /// let example = BBox::new(-10.0, -10.0, 10.0, 10.0);
340    /// let bboxes = example.bboxes();
341    /// assert_eq!(bboxes.len(), 1);
342    ///
343    /// let bboxes_crosses = BBox::new(179.0, -89.0, -179.0, 89.0).bboxes();
344    /// assert_eq!(bboxes_crosses.len(), 2); // Split into two bounding boxes
345    /// ```
346    #[must_use]
347    pub fn bboxes(&self) -> Vec<Self> {
348        if self.crosses_antimeridian() {
349            vec![
350                Self {
351                    north: self.north,
352                    south: self.south,
353                    east: 180.0,
354                    west: self.west,
355                },
356                Self {
357                    north: self.north,
358                    south: self.south,
359                    east: self.east,
360                    west: -180.0,
361                },
362            ]
363        } else {
364            vec![*self]
365        }
366    }
367
368    /// Return upper left corner of bounding box as `LngLat`
369    #[must_use]
370    pub const fn ul(&self) -> LngLat {
371        LngLat::new(self.west, self.north)
372    }
373
374    /// Return upper right corner of bounding box as `LngLat`
375    #[must_use]
376    pub const fn ur(&self) -> LngLat {
377        LngLat::new(self.east, self.north)
378    }
379
380    /// Return lower right corner of bounding box as `LngLat`
381    #[must_use]
382    pub const fn lr(&self) -> LngLat {
383        LngLat::new(self.east, self.south)
384    }
385
386    /// Return lower left corner of bounding box as `LngLat`
387    #[must_use]
388    pub const fn ll(&self) -> LngLat {
389        LngLat::new(self.west, self.south)
390    }
391
392    /// Mbt metadata bounds string
393    #[must_use]
394    pub fn mbt_bounds(&self) -> String {
395        format!("{},{},{},{}", self.west, self.south, self.east, self.north)
396    }
397}
398
399/// Merge a vector of bboxes into a single bbox handling antimeridian
400#[must_use]
401pub fn geobbox_merge(bboxes: &[BBox]) -> BBox {
402    if bboxes.is_empty() {
403        return BBox::world_planet();
404    }
405    if bboxes.len() == 1 {
406        return bboxes[0];
407    }
408    // TODO: Figure this out at somepoint...
409    // let any_crosses_antimeridian = bboxes.iter().any(|bbox| bbox.crosses_antimeridian());
410    let mut west = f64::INFINITY;
411    let mut south = f64::INFINITY;
412    let mut east = f64::NEG_INFINITY;
413    let mut north = f64::NEG_INFINITY;
414    for bbox in bboxes {
415        if bbox.west < west {
416            west = bbox.west;
417        }
418        if bbox.south < south {
419            south = bbox.south;
420        }
421        if bbox.east > east {
422            east = bbox.east;
423        }
424        if bbox.north > north {
425            north = bbox.north;
426        }
427    }
428    BBox::new(west, south, east, north).geo_wrap()
429}
430
431impl From<BBox> for BBoxTuple {
432    fn from(bbox: BBox) -> Self {
433        Self(bbox.west, bbox.south, bbox.east, bbox.north)
434    }
435}
436
437impl From<BBoxTuple> for BBox {
438    fn from(tuple: BBoxTuple) -> Self {
439        Self::new(tuple.0, tuple.1, tuple.2, tuple.3)
440    }
441}
442
443impl From<&String> for BBox {
444    fn from(s: &String) -> Self {
445        // remove leading and trailing quotes
446        let s = s.trim_matches('"');
447        parse_bbox(s).unwrap_or_else(|_e| Self::world_planet())
448    }
449}
450
451impl TryFrom<&str> for BBox {
452    type Error = &'static str;
453
454    fn try_from(s: &str) -> Result<Self, Self::Error> {
455        parse_bbox(s).map_err(|_| "Failed to parse BBox")
456    }
457}
458
459impl WebBBox {
460    #[must_use]
461    pub const fn new(left: f64, bottom: f64, right: f64, top: f64) -> Self {
462        Self {
463            min: Point2d::new(left, bottom),
464            max: Point2d::new(right, top),
465        }
466    }
467
468    #[must_use]
469    #[inline]
470    pub const fn min(&self) -> Point2d<f64> {
471        self.min
472    }
473
474    #[must_use]
475    #[inline]
476    pub const fn max(&self) -> Point2d<f64> {
477        self.max
478    }
479
480    #[must_use]
481    #[inline]
482    pub const fn left(&self) -> f64 {
483        self.min.x
484    }
485
486    #[must_use]
487    #[inline]
488    pub const fn bottom(&self) -> f64 {
489        self.min.y
490    }
491
492    #[must_use]
493    #[inline]
494    pub const fn right(&self) -> f64 {
495        self.max.x
496    }
497
498    #[must_use]
499    #[inline]
500    pub const fn top(&self) -> f64 {
501        self.max.y
502    }
503
504    #[must_use]
505    #[inline]
506    pub fn width(&self) -> f64 {
507        self.max.x - self.min.x
508    }
509
510    #[must_use]
511    #[inline]
512    pub const fn west(&self) -> f64 {
513        self.min.x
514    }
515
516    #[must_use]
517    #[inline]
518    pub const fn south(&self) -> f64 {
519        self.min.y
520    }
521
522    #[must_use]
523    #[inline]
524    pub const fn east(&self) -> f64 {
525        self.max.x
526    }
527
528    #[must_use]
529    #[inline]
530    pub const fn north(&self) -> f64 {
531        self.max.y
532    }
533
534    /// Returns the geojson tuple/array representation of the bounding box
535    #[must_use]
536    #[inline]
537    pub fn json_arr(&self) -> String {
538        format!(
539            "[{},{},{},{}]",
540            self.west(),
541            self.south(),
542            self.east(),
543            self.north()
544        )
545    }
546
547    /// Returns the gdal-ish string representation of the bounding box
548    #[must_use]
549    #[inline]
550    pub fn projwin_str(&self) -> String {
551        format!(
552            "{} {} {} {}",
553            self.west(),
554            self.north(),
555            self.east(),
556            self.south()
557        )
558    }
559}
560
561impl From<BBox> for WebBBox {
562    fn from(bbox: BBox) -> Self {
563        let (west_merc, south_merc) = xy(bbox.west(), bbox.south(), None);
564        let (east_merc, north_merc) = xy(bbox.east(), bbox.north(), None);
565        Self::new(west_merc, south_merc, east_merc, north_merc)
566    }
567}
568
569impl From<Tile> for WebBBox {
570    fn from(tile: Tile) -> Self {
571        let bbox = tile.geobbox();
572        Self::new(bbox.west, bbox.south, bbox.east, bbox.north)
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    #[test]
581    fn test_merge_bboxes_non_crossing() {
582        let bboxes = vec![
583            BBox::new(-100.0, -10.0, -90.0, 10.0), // Does not cross the anti-meridian
584            BBox::new(-120.0, -5.0, -100.0, 5.0),  // Does not cross the anti-meridian
585        ];
586
587        let expected = BBox::new(-120.0, -10.0, -90.0, 10.0);
588        let result = geobbox_merge(&bboxes);
589
590        assert_eq!(result, expected);
591    }
592
593    // =========================================================================
594    // TODO - Antimeridian bbox... it's not as straight forward as I thought...
595    // =========================================================================
596    // #[test]
597    // fn test_merge_bboxes_antimeridian() {
598    //     let bboxes = vec![
599    //         BBox::new(170.0, -10.0, -170.0, 10.0), // Crosses the anti-meridian
600    //         BBox::new(160.0, -5.0, 170.0, 5.0),    // Crosses the anti-meridian
601    //     ];
602    //
603    //     let expected = BBox::new(160.0, -10.0, -170.0, 10.0);
604    //     let result = geobbox_merge(&bboxes);
605    //
606    //     assert_eq!(result, expected);
607    // }
608
609    // #[test]
610    // fn test_merge_mixed_bboxes() {
611    //     let bboxes = vec![
612    //         BBox::new(170.0, -10.0, -170.0, 10.0), // Crosses the anti-meridian
613    //         BBox::new(-100.0, -20.0, 100.0, 20.0), // Does not cross the anti-meridian
614    //     ];
615    //
616    //     let expected = BBox::new(-100.0, -20.0, -170.0, 20.0);
617    //     let result = geobbox_merge(&bboxes);
618    //
619    //     assert_eq!(result, expected);
620    // }
621}