1#![warn(missing_docs)]
4
5use 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#[derive(Debug, Clone, PartialEq, PartialOrd)]
17pub struct BBox {
18 w: f64, s: f64, z_min: Option<f64>, e: f64, n: f64, z_max: Option<f64>, 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 pub(crate) fn from(xy: Vec<f64>) -> Self {
74 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 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 let x1 = self.w;
104 let y1 = self.s;
105 let x2 = self.e;
106 let y2 = self.n;
107
108 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 #[cfg(any(feature = "gpkg_ds", feature = "pg_ds"))]
187 pub(crate) fn to_sql(&self) -> Result<String, MyError> {
188 let x1 = self.w;
190 let y1 = self.s;
191 let x2 = self.e;
192 let y2 = self.n;
193
194 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 #[cfg(test)]
236 fn west(&self) -> f64 {
237 self.w
238 }
239
240 #[cfg(test)]
242 fn east(&self) -> f64 {
243 self.e
244 }
245
246 #[cfg(test)]
248 fn south(&self) -> f64 {
249 self.s
250 }
251
252 #[cfg(test)]
254 fn north(&self) -> f64 {
255 self.n
256 }
257
258 #[cfg(test)]
260 fn z_min(&self) -> Option<f64> {
261 self.z_min
262 }
263
264 #[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 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}