Skip to main content

ogc_cql2/geom/
bbox.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4
5//! Bounding box geometry.
6//!
7
8use crate::{
9    CRS, EPSG_4326, GTrait, MyError, geom::ensure_precision, srid::SRID,
10};
11use core::fmt;
12use geos::{CoordSeq, Geometry};
13use tracing::{error, warn};
14
15/// 2D or 3D bounding box.
16#[derive(Debug, Clone, PartialEq, PartialOrd)]
17pub struct BBox {
18    w: f64,             // west bound longitude
19    s: f64,             // south bound latitude
20    z_min: Option<f64>, // minimum elevation
21    e: f64,             // east bound longitude
22    n: f64,             // north bound latitude
23    z_max: Option<f64>, // maximum elevation
24
25    srid: SRID,
26}
27
28impl GTrait for BBox {
29    fn is_2d(&self) -> bool {
30        self.z_min.is_none()
31    }
32
33    fn to_wkt_fmt(&self, precision: usize) -> String {
34        if let Some(z_min) = self.z_min {
35            format!(
36                "BBOX ({:.6$}, {:.6$}, {:.6$}, {:.6$}, {:.6$}, {:.6$})",
37                self.w,
38                self.s,
39                z_min,
40                self.e,
41                self.n,
42                self.z_max.unwrap(),
43                precision
44            )
45        } else {
46            format!(
47                "BBOX ({:.4$}, {:.4$}, {:.4$}, {:.4$})",
48                self.w, self.s, self.e, self.n, precision
49            )
50        }
51    }
52
53    fn check_coordinates(&self, crs: &CRS) -> Result<(), MyError> {
54        crs.check_point([self.w, self.s].as_ref())?;
55        crs.check_point([self.e, self.n].as_ref())?;
56        Ok(())
57    }
58
59    fn type_(&self) -> &str {
60        "BBox"
61    }
62
63    fn srid(&self) -> SRID {
64        self.srid
65    }
66}
67
68impl BBox {
69    /// (from [1]) If the vertical axis is included, the third and the sixth
70    /// number are the bottom and the top of the 3-dimensional bounding box.
71    ///
72    /// [1]: https://docs.ogc.org/is/21-065r2/21-065r2.html#basic-spatial-data-types
73    pub(crate) fn from(xy: Vec<f64>) -> Self {
74        // bounding boxes SRID is always EPSG:4326...
75        let srid = EPSG_4326;
76        if xy.len() == 4 {
77            BBox {
78                w: ensure_precision(&xy[0]),
79                s: ensure_precision(&xy[1]),
80                z_min: None,
81                e: ensure_precision(&xy[2]),
82                n: ensure_precision(&xy[3]),
83                z_max: None,
84                srid,
85            }
86        } else {
87            // panics if not 6-element long...
88            BBox {
89                w: ensure_precision(&xy[0]),
90                s: ensure_precision(&xy[1]),
91                z_min: Some(ensure_precision(&xy[2])),
92                e: ensure_precision(&xy[3]),
93                n: ensure_precision(&xy[4]),
94                z_max: Some(ensure_precision(&xy[5])),
95                srid,
96            }
97        }
98    }
99
100    pub(crate) fn to_geos(&self) -> Result<Geometry, MyError> {
101        // convert this to one 2D polygon, or in the case of a box that spans the
102        // antimeridian, a 2D multi-polygon.
103        let x1 = self.w;
104        let y1 = self.s;
105        let x2 = self.e;
106        let y2 = self.n;
107
108        // if x_min is larger than x_max, then the box spans the antimeridian...
109        if x1 < x2 {
110            let cs =
111                CoordSeq::new_from_vec(&[&[x1, y1], &[x2, y1], &[x2, y2], &[x1, y2], &[x1, y1]])
112                    .map_err(|x| {
113                        error!("Failed creating BBOX outer ring coordinates: {x}");
114                        MyError::Geos(x)
115                    })?;
116
117            let outer = Geometry::create_linear_ring(cs).map_err(|x| {
118                error!("Failed creating BBOX outer ring: {x}");
119                MyError::Geos(x)
120            })?;
121
122            Geometry::create_polygon(outer, vec![]).map_err(|x| {
123                error!("Failed creating BBOX polygon: {x}");
124                MyError::Geos(x)
125            })
126        } else {
127            let cs1 = CoordSeq::new_from_vec(&[
128                &[x1, y1],
129                &[180.0, y1],
130                &[180.0, y2],
131                &[x1, y2],
132                &[x1, y1],
133            ])
134            .map_err(|x| {
135                error!("Failed creating BBOX 1st outer ring coordinates: {x}");
136                MyError::Geos(x)
137            })?;
138
139            let cs2 = CoordSeq::new_from_vec(&[
140                &[x2, y1],
141                &[x2, y2],
142                &[-180.0, y2],
143                &[-180.0, y1],
144                &[x2, y1],
145            ])
146            .map_err(|x| {
147                error!("Failed creating BBOX 2nd outer ring coordinates: {x}");
148                MyError::Geos(x)
149            })?;
150
151            let outer1 = Geometry::create_linear_ring(cs1).map_err(|x| {
152                error!("Failed creating BBOX 1st outer ring: {x}");
153                MyError::Geos(x)
154            })?;
155
156            let outer2 = Geometry::create_linear_ring(cs2).map_err(|x| {
157                error!("Failed creating BBOX 2nd outer ring: {x}");
158                MyError::Geos(x)
159            })?;
160
161            let p1 = Geometry::create_polygon(outer1, vec![]).map_err(|x| {
162                error!("Failed creating BBOX 1st polygon: {x}");
163                MyError::Geos(x)
164            })?;
165            let p2 = Geometry::create_polygon(outer2, vec![]).map_err(|x| {
166                error!("Failed creating BBOX 1st polygon: {x}");
167                MyError::Geos(x)
168            })?;
169
170            Geometry::create_multipolygon(vec![p1, p2]).map_err(|x| {
171                error!("Failed creating BBOX multi-polygon: {x}");
172                MyError::Geos(x)
173            })
174        }
175    }
176
177    pub(crate) fn set_srid_unchecked(&mut self, srid: &SRID) {
178        if self.srid != *srid {
179            warn!("Replacing current SRID ({}) w/ {srid}", self.srid);
180            self.srid = srid.to_owned();
181        }
182    }
183
184    // some services do not handle this type of geometry but can deal w/ it if
185    // it's portrayed as a [Multi]Polygon instead.
186    #[cfg(any(feature = "gpkg_ds", feature = "pg_ds"))]
187    pub(crate) fn to_sql(&self) -> Result<String, MyError> {
188        // similar to `to_geos` we need to detect when crossing dateline...
189        let x1 = self.w;
190        let y1 = self.s;
191        let x2 = self.e;
192        let y2 = self.n;
193
194        // if x_min is larger than x_max, then the box spans the antimeridian...
195        let wkt = if x1 < x2 {
196            let p = super::Polygon::from_xy_and_srid_unchecked(
197                vec![vec![
198                    vec![x1, y1],
199                    vec![x2, y1],
200                    vec![x2, y2],
201                    vec![x1, y2],
202                    vec![x1, y1],
203                ]],
204                self.srid,
205            );
206            p.to_wkt()
207        } else {
208            let pp = super::Polygons::from_xy_and_srid(
209                vec![
210                    vec![vec![
211                        vec![x1, y1],
212                        vec![180.0, y1],
213                        vec![180.0, y2],
214                        vec![x1, y2],
215                        vec![x1, y1],
216                    ]],
217                    vec![vec![
218                        vec![x2, y1],
219                        vec![x2, y2],
220                        vec![-180.0, y2],
221                        vec![-180.0, y1],
222                        vec![x2, y1],
223                    ]],
224                ],
225                self.srid,
226            );
227            pp.to_wkt()
228        };
229
230        let srid = self.srid().into_inner();
231        Ok(format!("ST_GeomFromText('{wkt}', {srid})"))
232    }
233
234    // Return the west bound longitude coordinate of this.
235    #[cfg(test)]
236    fn west(&self) -> f64 {
237        self.w
238    }
239
240    // Return the east bound longitude coordinate of this.
241    #[cfg(test)]
242    fn east(&self) -> f64 {
243        self.e
244    }
245
246    // Return the south bound latitude coordinate of this.
247    #[cfg(test)]
248    fn south(&self) -> f64 {
249        self.s
250    }
251
252    // Return the north bound latitude coordinate of this.
253    #[cfg(test)]
254    fn north(&self) -> f64 {
255        self.n
256    }
257
258    // Return the lowest (minimum) elevation of this.
259    #[cfg(test)]
260    fn z_min(&self) -> Option<f64> {
261        self.z_min
262    }
263
264    // Return the highest (maximum) elevation of this.
265    #[cfg(test)]
266    fn z_max(&self) -> Option<f64> {
267        self.z_max
268    }
269}
270
271impl fmt::Display for BBox {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        write!(f, "BBox (...)")
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::{G, expr::E, text::cql2};
281    use geos::Geom;
282    use std::error::Error;
283
284    #[test]
285    #[tracing_test::traced_test]
286    fn test() {
287        const G1: &str = "bbox(-128.098193, -1.1, -99999.0, 180.0, 90.0, 100000.0)";
288        const G2: &str = "bbox(-128.098193,-1.1, -99999.0,180.0 , \t90.0, \n 100000.0)";
289
290        let x = cql2::geom_expression(G1);
291        assert!(x.is_ok());
292        let g = x.unwrap();
293        assert!(matches!(g, E::Spatial(G::BBox(_))));
294        let bbox1 = match g {
295            E::Spatial(G::BBox(x)) => x,
296            _ => panic!("Not a BBox"),
297        };
298        assert!(!bbox1.is_2d());
299
300        // should also succeed when coordinate sequence contains no, or other
301        // sorts of whitespaces wherever they're placed...
302        let x = cql2::geom_expression(G2);
303        assert!(x.is_ok());
304        let g = x.unwrap();
305        assert!(matches!(g, E::Spatial(G::BBox(_))));
306        let bbox2 = match g {
307            E::Spatial(G::BBox(x)) => x,
308            _ => panic!("Not a BBox"),
309        };
310        assert!(!bbox2.is_2d());
311
312        assert_eq!(bbox1.west(), bbox2.west());
313        assert_eq!(bbox1.east(), bbox2.east());
314        assert_eq!(bbox1.south(), bbox2.south());
315        assert_eq!(bbox1.north(), bbox2.north());
316        assert_eq!(bbox1.z_min(), bbox2.z_min());
317        assert_eq!(bbox1.z_max(), bbox2.z_max());
318    }
319
320    #[test]
321    #[tracing_test::traced_test]
322    fn test_to_polygon() {
323        const G1: &str = "BBOX(-180,-90,180,90)";
324        const WKT: &str = "POLYGON ((-180 -90, 180 -90, 180 90, -180 90, -180 -90))";
325        const G2: &str = "bbox(-180.0,-90.,-99999.0,180.0,90.0,100000.0)";
326
327        let x1 = cql2::geom_expression(G1);
328        assert!(x1.is_ok());
329        let g1 = x1.unwrap();
330        assert!(matches!(g1, E::Spatial(G::BBox(_))));
331        let bbox1 = match g1 {
332            E::Spatial(G::BBox(x)) => x,
333            _ => panic!("Not a BBox"),
334        };
335        assert!(bbox1.is_2d());
336        let g1 = bbox1.to_geos();
337        assert!(g1.is_ok());
338        let g1 = g1.unwrap();
339        let wkt1 = g1.to_wkt().unwrap();
340        assert_eq!(wkt1, WKT);
341
342        let x2 = cql2::geom_expression(G2);
343        assert!(x2.is_ok());
344        let g2 = x2.unwrap();
345        assert!(matches!(g2, E::Spatial(G::BBox(_))));
346        let bbox2 = match g2 {
347            E::Spatial(G::BBox(x)) => x,
348            _ => panic!("Not a BBox"),
349        };
350        assert!(!bbox2.is_2d());
351        let g2 = bbox2.to_geos();
352        assert!(g2.is_ok());
353        let g2 = g2.unwrap();
354        let wkt2 = g2.to_wkt().unwrap();
355        assert_eq!(wkt2, WKT);
356    }
357
358    #[test]
359    fn test_antimeridian() -> Result<(), Box<dyn Error>> {
360        const WKT: &str = "MULTIPOLYGON (((150 -90, 180 -90, 180 90, 150 90, 150 -90)), ((-150 -90, -150 90, -180 90, -180 -90, -150 -90)))";
361
362        let bbox = BBox::from(vec![150.0, -90.0, -150.0, 90.0]);
363        let mp = bbox.to_geos()?;
364        assert_eq!(mp.get_type()?, "MultiPolygon");
365
366        let wkt = mp.to_wkt()?;
367        assert_eq!(wkt, WKT);
368
369        let pt = Geometry::new_from_wkt("POINT(152 10)")?;
370
371        pt.within(&mp)?;
372        mp.contains(&pt)?;
373
374        Ok(())
375    }
376
377    #[test]
378    fn test_precision() -> Result<(), Box<dyn Error>> {
379        const WKT: &str = "BBOX (6.043073, 50.128052, 6.242751, 49.902226)";
380
381        let bbox_xy = vec![
382            6.043073357781111,
383            50.128051662794235,
384            6.242751092156993,
385            49.90222565367873,
386        ];
387
388        let bbox = BBox::from(bbox_xy);
389        let wkt = bbox.to_wkt_fmt(6);
390        assert_eq!(wkt, WKT);
391
392        Ok(())
393    }
394}