utiles_core/
bbox.rs

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