1use std::collections::HashSet;
19
20use arrow_schema::ArrowError;
21use geo_traits::{
22 CoordTrait, Dimensions, GeometryCollectionTrait, GeometryTrait, GeometryType, LineStringTrait,
23 MultiLineStringTrait, MultiPointTrait, MultiPolygonTrait, PointTrait, PolygonTrait,
24};
25use wkb::reader::Wkb;
26
27use crate::interval::{Interval, IntervalTrait, WraparoundInterval};
28
29#[derive(Debug)]
39pub struct GeometryBounder {
40 x_left: Interval,
42 x_mid: Interval,
44 x_right: Interval,
46 y: Interval,
48 z: Interval,
50 m: Interval,
52 geometry_types: HashSet<i32>,
58 wraparound_hint: Interval,
59}
60
61impl GeometryBounder {
62 pub fn empty() -> Self {
64 Self {
65 x_left: Interval::empty(),
66 x_mid: Interval::empty(),
67 x_right: Interval::empty(),
68 y: Interval::empty(),
69 z: Interval::empty(),
70 m: Interval::empty(),
71 geometry_types: HashSet::<i32>::default(),
72 wraparound_hint: Interval::empty(),
73 }
74 }
75
76 pub fn with_wraparound_hint(self, wraparound_hint: impl Into<Interval>) -> Self {
90 Self {
91 wraparound_hint: wraparound_hint.into(),
92 ..self
93 }
94 }
95
96 pub fn x(&self) -> WraparoundInterval {
103 let out_all = Interval::empty()
104 .merge_interval(&self.x_left)
105 .merge_interval(&self.x_mid)
106 .merge_interval(&self.x_right);
107
108 if !self.x_mid.is_empty() || !self.wraparound_hint.contains_interval(&out_all) {
112 return out_all.into();
113 }
114
115 let out_width = (self.x_left.hi() - self.wraparound_hint.lo())
118 + (self.wraparound_hint.hi() - self.x_right.lo());
119 if out_all.width() < out_width {
120 return out_all.into();
121 }
122
123 WraparoundInterval::new(self.x_right.lo(), self.x_left.hi())
125 }
126
127 pub fn y(&self) -> Interval {
129 self.y
130 }
131
132 pub fn z(&self) -> Interval {
134 self.z
135 }
136
137 pub fn m(&self) -> Interval {
139 self.m
140 }
141
142 pub fn geometry_types(&self) -> Vec<i32> {
148 let mut out = self.geometry_types.iter().copied().collect::<Vec<_>>();
149 out.sort_unstable();
150 out
151 }
152
153 pub fn update_wkb(&mut self, wkb: &[u8]) -> Result<(), ArrowError> {
159 let wkb = Wkb::try_new(wkb).map_err(|e| ArrowError::ExternalError(Box::new(e)))?;
160 self.update_geometry(&wkb)?;
161 Ok(())
162 }
163
164 fn update_geometry(&mut self, geom: &impl GeometryTrait<T = f64>) -> Result<(), ArrowError> {
165 let geometry_type = geometry_type(geom)?;
166 self.geometry_types.insert(geometry_type);
167
168 visit_intervals(geom, 'x', &mut |x| self.update_x(&x))?;
169 visit_intervals(geom, 'y', &mut |y| self.y.update_interval(&y))?;
170 visit_intervals(geom, 'z', &mut |z| self.z.update_interval(&z))?;
171 visit_intervals(geom, 'm', &mut |m| self.m.update_interval(&m))?;
172
173 Ok(())
174 }
175
176 fn update_x(&mut self, x: &Interval) {
177 if x.hi() < self.wraparound_hint.mid() {
178 self.x_left.update_interval(x);
181 } else if x.lo() > self.wraparound_hint.mid() {
182 self.x_right.update_interval(x);
185 } else {
186 self.x_mid.update_interval(x);
188 }
189 }
190}
191
192fn visit_intervals(
199 geom: &impl GeometryTrait<T = f64>,
200 dimension: char,
201 func: &mut impl FnMut(Interval),
202) -> Result<(), ArrowError> {
203 let Some(n) = dimension_index(geom.dim(), dimension) else {
204 return Ok(());
205 };
206
207 match geom.as_type() {
208 GeometryType::Point(pt) => {
209 if let Some(coord) = PointTrait::coord(pt) {
210 visit_point(coord, n, func);
211 }
212 }
213 GeometryType::LineString(ls) => {
214 visit_sequence(ls.coords(), n, func);
215 }
216 GeometryType::Polygon(pl) => {
217 if let Some(exterior) = pl.exterior() {
218 visit_sequence(exterior.coords(), n, func);
219 }
220
221 for interior in pl.interiors() {
222 visit_sequence(interior.coords(), n, func);
223 }
224 }
225 GeometryType::MultiPoint(multi_pt) => {
226 visit_collection(multi_pt.points(), dimension, func)?;
227 }
228 GeometryType::MultiLineString(multi_ls) => {
229 visit_collection(multi_ls.line_strings(), dimension, func)?;
230 }
231 GeometryType::MultiPolygon(multi_pl) => {
232 visit_collection(multi_pl.polygons(), dimension, func)?;
233 }
234 GeometryType::GeometryCollection(collection) => {
235 visit_collection(collection.geometries(), dimension, func)?;
236 }
237 _ => {
238 return Err(ArrowError::InvalidArgumentError(
239 "GeometryType not supported for dimension bounds".to_string(),
240 ));
241 }
242 }
243
244 Ok(())
245}
246
247fn visit_point(coord: impl CoordTrait<T = f64>, n: usize, func: &mut impl FnMut(Interval)) {
252 let val = unsafe { coord.nth_unchecked(n) };
253 func((val, val).into());
254}
255
256fn visit_sequence(
261 coords: impl IntoIterator<Item = impl CoordTrait<T = f64>>,
262 n: usize,
263 func: &mut impl FnMut(Interval),
264) {
265 let mut interval = Interval::empty();
266 for coord in coords {
267 interval.update_value(unsafe { coord.nth_unchecked(n) });
268 }
269
270 func(interval);
271}
272
273fn visit_collection(
275 collection: impl IntoIterator<Item = impl GeometryTrait<T = f64>>,
276 target: char,
277 func: &mut impl FnMut(Interval),
278) -> Result<(), ArrowError> {
279 for geom in collection {
280 visit_intervals(&geom, target, func)?;
281 }
282
283 Ok(())
284}
285
286fn geometry_type(geom: &impl GeometryTrait<T = f64>) -> Result<i32, ArrowError> {
295 let dimension_type = match geom.dim() {
296 Dimensions::Xy => 0,
297 Dimensions::Xyz => 1000,
298 Dimensions::Xym => 2000,
299 Dimensions::Xyzm => 3000,
300 Dimensions::Unknown(_) => {
301 return Err(ArrowError::InvalidArgumentError(
302 "Unsupported dimensions".to_string(),
303 ));
304 }
305 };
306
307 let geometry_type = match geom.as_type() {
308 GeometryType::Point(_) => 1,
309 GeometryType::LineString(_) => 2,
310 GeometryType::Polygon(_) => 3,
311 GeometryType::MultiPoint(_) => 4,
312 GeometryType::MultiLineString(_) => 5,
313 GeometryType::MultiPolygon(_) => 6,
314 GeometryType::GeometryCollection(_) => 7,
315 _ => {
316 return Err(ArrowError::InvalidArgumentError(
317 "GeometryType not supported for dimension bounds".to_string(),
318 ));
319 }
320 };
321
322 Ok(dimension_type + geometry_type)
323}
324
325fn dimension_index(dim: Dimensions, target: char) -> Option<usize> {
326 match target {
327 'x' => return Some(0),
328 'y' => return Some(1),
329 _ => {}
330 }
331
332 match (dim, target) {
333 (Dimensions::Xyz, 'z') => Some(2),
334 (Dimensions::Xym, 'm') => Some(2),
335 (Dimensions::Xyzm, 'z') => Some(2),
336 (Dimensions::Xyzm, 'm') => Some(3),
337 (_, _) => None,
338 }
339}
340
341#[cfg(test)]
342mod test {
343
344 use std::str::FromStr;
345
346 use wkt::Wkt;
347
348 use super::*;
349
350 fn wkt_bounds(
351 wkt_values: impl IntoIterator<Item = impl AsRef<str>>,
352 ) -> Result<GeometryBounder, ArrowError> {
353 wkt_bounds_with_wraparound(wkt_values, Interval::empty())
354 }
355
356 fn wkt_bounds_with_wraparound(
357 wkt_values: impl IntoIterator<Item = impl AsRef<str>>,
358 wraparound: impl Into<Interval>,
359 ) -> Result<GeometryBounder, ArrowError> {
360 let mut bounder = GeometryBounder::empty().with_wraparound_hint(wraparound);
361 for wkt_value in wkt_values {
362 let wkt: Wkt = Wkt::from_str(wkt_value.as_ref())
363 .map_err(|e| ArrowError::InvalidArgumentError(e.to_string()))?;
364 bounder.update_geometry(&wkt)?;
365 }
366 Ok(bounder)
367 }
368
369 #[test]
370 fn test_wkb() {
371 let wkt: Wkt = Wkt::from_str("LINESTRING (0 1, 2 3)").unwrap();
372 let mut wkb = Vec::new();
373 wkb::writer::write_geometry(&mut wkb, &wkt, &Default::default()).unwrap();
374
375 let mut bounds = GeometryBounder::empty();
376 bounds.update_wkb(&wkb).unwrap();
377
378 assert_eq!(bounds.x(), (0, 2).into());
379 assert_eq!(bounds.y(), (1, 3).into());
380 }
381
382 #[test]
383 fn test_geometry_types() {
384 let empties = [
385 "POINT EMPTY",
386 "LINESTRING EMPTY",
387 "POLYGON EMPTY",
388 "MULTIPOINT EMPTY",
389 "MULTILINESTRING EMPTY",
390 "MULTIPOLYGON EMPTY",
391 "GEOMETRYCOLLECTION EMPTY",
392 ];
393
394 assert_eq!(
395 wkt_bounds(empties).unwrap().geometry_types(),
396 vec![1, 2, 3, 4, 5, 6, 7]
397 );
398
399 let empties_z = [
400 "POINT Z EMPTY",
401 "LINESTRING Z EMPTY",
402 "POLYGON Z EMPTY",
403 "MULTIPOINT Z EMPTY",
404 "MULTILINESTRING Z EMPTY",
405 "MULTIPOLYGON Z EMPTY",
406 "GEOMETRYCOLLECTION Z EMPTY",
407 ];
408
409 assert_eq!(
410 wkt_bounds(empties_z).unwrap().geometry_types(),
411 vec![1001, 1002, 1003, 1004, 1005, 1006, 1007]
412 );
413
414 let empties_m = [
415 "POINT M EMPTY",
416 "LINESTRING M EMPTY",
417 "POLYGON M EMPTY",
418 "MULTIPOINT M EMPTY",
419 "MULTILINESTRING M EMPTY",
420 "MULTIPOLYGON M EMPTY",
421 "GEOMETRYCOLLECTION M EMPTY",
422 ];
423
424 assert_eq!(
425 wkt_bounds(empties_m).unwrap().geometry_types(),
426 vec![2001, 2002, 2003, 2004, 2005, 2006, 2007]
427 );
428
429 let empties_zm = [
430 "POINT ZM EMPTY",
431 "LINESTRING ZM EMPTY",
432 "POLYGON ZM EMPTY",
433 "MULTIPOINT ZM EMPTY",
434 "MULTILINESTRING ZM EMPTY",
435 "MULTIPOLYGON ZM EMPTY",
436 "GEOMETRYCOLLECTION ZM EMPTY",
437 ];
438
439 assert_eq!(
440 wkt_bounds(empties_zm).unwrap().geometry_types(),
441 vec![3001, 3002, 3003, 3004, 3005, 3006, 3007]
442 );
443 }
444
445 #[test]
446 fn test_bounds_empty() {
447 let empties = [
448 "POINT EMPTY",
449 "LINESTRING EMPTY",
450 "POLYGON EMPTY",
451 "MULTIPOINT EMPTY",
452 "MULTILINESTRING EMPTY",
453 "MULTIPOLYGON EMPTY",
454 "GEOMETRYCOLLECTION EMPTY",
455 ];
456
457 let bounds = wkt_bounds(empties).unwrap();
458 assert!(bounds.x().is_empty());
459 assert!(bounds.y().is_empty());
460 assert!(bounds.z().is_empty());
461 assert!(bounds.m().is_empty());
462
463 let bounds = wkt_bounds_with_wraparound(empties, (-180, 180)).unwrap();
465 assert!(bounds.x().is_empty());
466 assert!(bounds.y().is_empty());
467 assert!(bounds.z().is_empty());
468 assert!(bounds.m().is_empty());
469 }
470
471 #[test]
472 fn test_bounds_coord() {
473 let bounds = wkt_bounds(["POINT (0 1)", "POINT (2 3)"]).unwrap();
474 assert_eq!(bounds.x(), (0, 2).into());
475 assert_eq!(bounds.y(), (1, 3).into());
476 assert!(bounds.z().is_empty());
477 assert!(bounds.m().is_empty());
478
479 let bounds = wkt_bounds(["POINT Z (0 1 2)", "POINT Z (3 4 5)"]).unwrap();
480 assert_eq!(bounds.x(), (0, 3).into());
481 assert_eq!(bounds.y(), (1, 4).into());
482 assert_eq!(bounds.z(), (2, 5).into());
483 assert!(bounds.m().is_empty());
484
485 let bounds = wkt_bounds(["POINT M (0 1 2)", "POINT M (3 4 5)"]).unwrap();
486 assert_eq!(bounds.x(), (0, 3).into());
487 assert_eq!(bounds.y(), (1, 4).into());
488 assert!(bounds.z().is_empty());
489 assert_eq!(bounds.m(), (2, 5).into());
490
491 let bounds = wkt_bounds(["POINT ZM (0 1 2 3)", "POINT ZM (4 5 6 7)"]).unwrap();
492 assert_eq!(bounds.x(), (0, 4).into());
493 assert_eq!(bounds.y(), (1, 5).into());
494 assert_eq!(bounds.z(), (2, 6).into());
495 assert_eq!(bounds.m(), (3, 7).into());
496 }
497
498 #[test]
499 fn test_bounds_sequence() {
500 let bounds = wkt_bounds(["LINESTRING (0 1, 2 3)"]).unwrap();
501 assert_eq!(bounds.x(), (0, 2).into());
502 assert_eq!(bounds.y(), (1, 3).into());
503 assert!(bounds.z().is_empty());
504 assert!(bounds.m().is_empty());
505
506 let bounds = wkt_bounds(["LINESTRING Z (0 1 2, 3 4 5)"]).unwrap();
507 assert_eq!(bounds.x(), (0, 3).into());
508 assert_eq!(bounds.y(), (1, 4).into());
509 assert_eq!(bounds.z(), (2, 5).into());
510 assert!(bounds.m().is_empty());
511
512 let bounds = wkt_bounds(["LINESTRING M (0 1 2, 3 4 5)"]).unwrap();
513 assert_eq!(bounds.x(), (0, 3).into());
514 assert_eq!(bounds.y(), (1, 4).into());
515 assert!(bounds.z().is_empty());
516 assert_eq!(bounds.m(), (2, 5).into());
517
518 let bounds = wkt_bounds(["LINESTRING ZM (0 1 2 3, 4 5 6 7)"]).unwrap();
519 assert_eq!(bounds.x(), (0, 4).into());
520 assert_eq!(bounds.y(), (1, 5).into());
521 assert_eq!(bounds.z(), (2, 6).into());
522 assert_eq!(bounds.m(), (3, 7).into());
523 }
524
525 #[test]
526 fn test_bounds_geometry_type() {
527 let bounds = wkt_bounds(["POINT (0 1)", "POINT (2 3)"]).unwrap();
528 assert_eq!(bounds.x(), (0, 2).into());
529 assert_eq!(bounds.y(), (1, 3).into());
530
531 let bounds = wkt_bounds(["LINESTRING (0 1, 2 3)"]).unwrap();
532 assert_eq!(bounds.x(), (0, 2).into());
533 assert_eq!(bounds.y(), (1, 3).into());
534
535 let bounds =
538 wkt_bounds(["POLYGON ((0 0, 0 1, 1 0, 0 0), (10 10, 10 11, 11 10, 10 10))"]).unwrap();
539 assert_eq!(bounds.x(), (0, 11).into());
540 assert_eq!(bounds.y(), (0, 11).into());
541
542 let bounds = wkt_bounds(["MULTIPOINT ((0 1), (2 3))"]).unwrap();
543 assert_eq!(bounds.x(), (0, 2).into());
544 assert_eq!(bounds.y(), (1, 3).into());
545
546 let bounds = wkt_bounds(["MULTILINESTRING ((0 1, 2 3))"]).unwrap();
547 assert_eq!(bounds.x(), (0, 2).into());
548 assert_eq!(bounds.y(), (1, 3).into());
549
550 let bounds = wkt_bounds(["MULTIPOLYGON (((0 0, 0 1, 1 0, 0 0)))"]).unwrap();
551 assert_eq!(bounds.x(), (0, 1).into());
552 assert_eq!(bounds.y(), (0, 1).into());
553
554 let bounds = wkt_bounds(["GEOMETRYCOLLECTION (POINT (0 1), POINT (2 3))"]).unwrap();
555 assert_eq!(bounds.x(), (0, 2).into());
556 assert_eq!(bounds.y(), (1, 3).into());
557 }
558
559 #[test]
560 fn test_bounds_wrap_basic() {
561 let geoms = ["POINT (-170 0)", "POINT (170 0)"];
562
563 let bounds = wkt_bounds_with_wraparound(geoms, Interval::empty()).unwrap();
565 assert_eq!(bounds.x(), (-170, 170).into());
566
567 let mut geoms_with_mid = geoms.to_vec();
570 geoms_with_mid.push("LINESTRING (-10 0, 10 0)");
571 let bounds = wkt_bounds_with_wraparound(geoms_with_mid, (-180, 180)).unwrap();
572 assert_eq!(bounds.x(), (-170, 170).into());
573
574 let bounds = wkt_bounds_with_wraparound(geoms, (-1000, 1000)).unwrap();
576 assert_eq!(bounds.x(), (-170, 170).into());
577
578 let bounds = wkt_bounds_with_wraparound(geoms, (-10, 10)).unwrap();
581 assert_eq!(bounds.x(), (-170, 170).into());
582
583 let bounds = wkt_bounds_with_wraparound(geoms, (-180, 180)).unwrap();
585 assert_eq!(bounds.x(), (170, -170).into());
586
587 let geoms = [
589 "POINT (-10 0)",
590 "POINT (-2 0)",
591 "POINT (170 0)",
592 "POINT (175 0)",
593 ];
594 let bounds = wkt_bounds_with_wraparound(geoms, (-180, 180)).unwrap();
595 assert_eq!(bounds.x(), (-10, 175).into());
596 }
597
598 #[test]
599 fn test_bounds_wrap_multipart() {
600 let fiji = "MULTIPOLYGON (
601 ((-180 -15.51, -180 -19.78, -178.61 -21.14, -178.02 -18.22, -178.57 -16.04, -180 -15.51)),
602 ((180 -15.51, 177.98 -16.25, 176.67 -17.14, 177.83 -19.31, 180 -19.78, 180 -15.51))
603 )";
604
605 let bounds = wkt_bounds_with_wraparound([fiji], (-180, 180)).unwrap();
606 assert!(bounds.x().is_wraparound());
607 assert_eq!(bounds.x(), (176.67, -178.02).into());
608 assert_eq!(bounds.y(), (-21.14, -15.51).into());
609 }
610}