1#![warn(missing_docs)]
4
5use 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#[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 pub(crate) fn to_sql(&self) -> Result<String, MyError> {
187 let x1 = self.w;
189 let y1 = self.s;
190 let x2 = self.e;
191 let y2 = self.n;
192
193 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 #[cfg(test)]
235 fn west(&self) -> f64 {
236 self.w
237 }
238
239 #[cfg(test)]
241 fn east(&self) -> f64 {
242 self.e
243 }
244
245 #[cfg(test)]
247 fn south(&self) -> f64 {
248 self.s
249 }
250
251 #[cfg(test)]
253 fn north(&self) -> f64 {
254 self.n
255 }
256
257 #[cfg(test)]
259 fn z_min(&self) -> Option<f64> {
260 self.z_min
261 }
262
263 #[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 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}