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, Polygon, Polygons, 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    pub(crate) fn to_sql(&self) -> Result<String, MyError> {
187        // similar to `to_geos` we need to detect when crossing dateline...
188        let x1 = self.w;
189        let y1 = self.s;
190        let x2 = self.e;
191        let y2 = self.n;
192
193        // if x_min is larger than x_max, then the box spans the antimeridian...
194        let wkt = if x1 < x2 {
195            let p = Polygon::from_xy_and_srid_unchecked(
196                vec![vec![
197                    vec![x1, y1],
198                    vec![x2, y1],
199                    vec![x2, y2],
200                    vec![x1, y2],
201                    vec![x1, y1],
202                ]],
203                self.srid,
204            );
205            p.to_wkt()
206        } else {
207            let pp = Polygons::from_xy_and_srid(
208                vec![
209                    vec![vec![
210                        vec![x1, y1],
211                        vec![180.0, y1],
212                        vec![180.0, y2],
213                        vec![x1, y2],
214                        vec![x1, y1],
215                    ]],
216                    vec![vec![
217                        vec![x2, y1],
218                        vec![x2, y2],
219                        vec![-180.0, y2],
220                        vec![-180.0, y1],
221                        vec![x2, y1],
222                    ]],
223                ],
224                self.srid,
225            );
226            pp.to_wkt()
227        };
228
229        let srid = self.srid().as_usize()?;
230        Ok(format!("ST_GeomFromText('{wkt}', {srid})"))
231    }
232
233    // Return the west bound longitude coordinate of this.
234    #[cfg(test)]
235    fn west(&self) -> f64 {
236        self.w
237    }
238
239    // Return the east bound longitude coordinate of this.
240    #[cfg(test)]
241    fn east(&self) -> f64 {
242        self.e
243    }
244
245    // Return the south bound latitude coordinate of this.
246    #[cfg(test)]
247    fn south(&self) -> f64 {
248        self.s
249    }
250
251    // Return the north bound latitude coordinate of this.
252    #[cfg(test)]
253    fn north(&self) -> f64 {
254        self.n
255    }
256
257    // Return the lowest (minimum) elevation of this.
258    #[cfg(test)]
259    fn z_min(&self) -> Option<f64> {
260        self.z_min
261    }
262
263    // Return the highest (maximum) elevation of this.
264    #[cfg(test)]
265    fn z_max(&self) -> Option<f64> {
266        self.z_max
267    }
268}
269
270impl fmt::Display for BBox {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        write!(f, "BBox (...)")
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use crate::{G, expr::E, text::cql2};
280    use geos::Geom;
281    use std::error::Error;
282
283    #[test]
284    #[tracing_test::traced_test]
285    fn test() {
286        const G1: &str = "bbox(-128.098193, -1.1, -99999.0, 180.0, 90.0, 100000.0)";
287        const G2: &str = "bbox(-128.098193,-1.1, -99999.0,180.0 , \t90.0, \n 100000.0)";
288
289        let x = cql2::geom_expression(G1);
290        assert!(x.is_ok());
291        let g = x.unwrap();
292        assert!(matches!(g, E::Spatial(G::BBox(_))));
293        let bbox1 = match g {
294            E::Spatial(G::BBox(x)) => x,
295            _ => panic!("Not a BBox"),
296        };
297        assert!(!bbox1.is_2d());
298
299        // should also succeed when coordinate sequence contains no, or other
300        // sorts of whitespaces wherever they're placed...
301        let x = cql2::geom_expression(G2);
302        assert!(x.is_ok());
303        let g = x.unwrap();
304        assert!(matches!(g, E::Spatial(G::BBox(_))));
305        let bbox2 = match g {
306            E::Spatial(G::BBox(x)) => x,
307            _ => panic!("Not a BBox"),
308        };
309        assert!(!bbox2.is_2d());
310
311        assert_eq!(bbox1.west(), bbox2.west());
312        assert_eq!(bbox1.east(), bbox2.east());
313        assert_eq!(bbox1.south(), bbox2.south());
314        assert_eq!(bbox1.north(), bbox2.north());
315        assert_eq!(bbox1.z_min(), bbox2.z_min());
316        assert_eq!(bbox1.z_max(), bbox2.z_max());
317    }
318
319    #[test]
320    #[tracing_test::traced_test]
321    fn test_to_polygon() {
322        const G1: &str = "BBOX(-180,-90,180,90)";
323        const WKT: &str = "POLYGON ((-180 -90, 180 -90, 180 90, -180 90, -180 -90))";
324        const G2: &str = "bbox(-180.0,-90.,-99999.0,180.0,90.0,100000.0)";
325
326        let x1 = cql2::geom_expression(G1);
327        assert!(x1.is_ok());
328        let g1 = x1.unwrap();
329        assert!(matches!(g1, E::Spatial(G::BBox(_))));
330        let bbox1 = match g1 {
331            E::Spatial(G::BBox(x)) => x,
332            _ => panic!("Not a BBox"),
333        };
334        assert!(bbox1.is_2d());
335        let g1 = bbox1.to_geos();
336        assert!(g1.is_ok());
337        let g1 = g1.unwrap();
338        let wkt1 = g1.to_wkt().unwrap();
339        assert_eq!(wkt1, WKT);
340
341        let x2 = cql2::geom_expression(G2);
342        assert!(x2.is_ok());
343        let g2 = x2.unwrap();
344        assert!(matches!(g2, E::Spatial(G::BBox(_))));
345        let bbox2 = match g2 {
346            E::Spatial(G::BBox(x)) => x,
347            _ => panic!("Not a BBox"),
348        };
349        assert!(!bbox2.is_2d());
350        let g2 = bbox2.to_geos();
351        assert!(g2.is_ok());
352        let g2 = g2.unwrap();
353        let wkt2 = g2.to_wkt().unwrap();
354        assert_eq!(wkt2, WKT);
355    }
356
357    #[test]
358    fn test_antimeridian() -> Result<(), Box<dyn Error>> {
359        const WKT: &str = "MULTIPOLYGON (((150 -90, 180 -90, 180 90, 150 90, 150 -90)), ((-150 -90, -150 90, -180 90, -180 -90, -150 -90)))";
360
361        let bbox = BBox::from(vec![150.0, -90.0, -150.0, 90.0]);
362        let mp = bbox.to_geos()?;
363        assert_eq!(mp.get_type()?, "MultiPolygon");
364
365        let wkt = mp.to_wkt()?;
366        assert_eq!(wkt, WKT);
367
368        let pt = Geometry::new_from_wkt("POINT(152 10)")?;
369
370        pt.within(&mp)?;
371        mp.contains(&pt)?;
372
373        Ok(())
374    }
375
376    #[test]
377    fn test_precision() -> Result<(), Box<dyn Error>> {
378        const WKT: &str = "BBOX (6.043073, 50.128052, 6.242751, 49.902226)";
379
380        let bbox_xy = vec![
381            6.043073357781111,
382            50.128051662794235,
383            6.242751092156993,
384            49.90222565367873,
385        ];
386
387        let bbox = BBox::from(bbox_xy);
388        let wkt = bbox.to_wkt_fmt(6);
389        assert_eq!(wkt, WKT);
390
391        Ok(())
392    }
393}