1use crate::coord::{GeoCoord, WorldCoord};
31use std::fmt;
32
33#[inline]
37fn wrap(value: f64, min: f64, max: f64) -> f64 {
38 let range = max - min;
39 if range == 0.0 {
40 return min;
41 }
42 ((value - min) % range + range) % range + min
43}
44
45#[derive(Debug, Clone, Copy, PartialEq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct GeoBounds {
76 sw: GeoCoord,
78 ne: GeoCoord,
80}
81
82impl GeoBounds {
83 #[inline]
89 pub fn new(sw: GeoCoord, ne: GeoCoord) -> Self {
90 Self { sw, ne }
91 }
92
93 #[inline]
97 pub fn from_coords(west: f64, south: f64, east: f64, north: f64) -> Self {
98 Self {
99 sw: GeoCoord::from_lat_lon(south, west),
100 ne: GeoCoord::from_lat_lon(north, east),
101 }
102 }
103
104 pub fn from_center_radius(center: GeoCoord, radius_m: f64) -> Self {
116 const EARTH_CIRCUMFERENCE_M: f64 = 40_075_017.0;
117 let lat_accuracy = 360.0 * radius_m / EARTH_CIRCUMFERENCE_M;
118 let lon_accuracy =
119 lat_accuracy / (std::f64::consts::PI / 180.0 * center.lat).cos();
120
121 Self {
122 sw: GeoCoord::from_lat_lon(
123 center.lat - lat_accuracy,
124 center.lon - lon_accuracy,
125 ),
126 ne: GeoCoord::from_lat_lon(
127 center.lat + lat_accuracy,
128 center.lon + lon_accuracy,
129 ),
130 }
131 }
132
133 #[inline]
137 pub fn sw(&self) -> GeoCoord {
138 self.sw
139 }
140
141 #[inline]
143 pub fn ne(&self) -> GeoCoord {
144 self.ne
145 }
146
147 #[inline]
149 pub fn nw(&self) -> GeoCoord {
150 GeoCoord::from_lat_lon(self.ne.lat, self.sw.lon)
151 }
152
153 #[inline]
155 pub fn se(&self) -> GeoCoord {
156 GeoCoord::from_lat_lon(self.sw.lat, self.ne.lon)
157 }
158
159 #[inline]
161 pub fn west(&self) -> f64 {
162 self.sw.lon
163 }
164
165 #[inline]
167 pub fn south(&self) -> f64 {
168 self.sw.lat
169 }
170
171 #[inline]
173 pub fn east(&self) -> f64 {
174 self.ne.lon
175 }
176
177 #[inline]
179 pub fn north(&self) -> f64 {
180 self.ne.lat
181 }
182
183 #[inline]
187 pub fn center(&self) -> GeoCoord {
188 GeoCoord::from_lat_lon(
189 (self.sw.lat + self.ne.lat) / 2.0,
190 (self.sw.lon + self.ne.lon) / 2.0,
191 )
192 }
193
194 pub fn extend_coord(&mut self, coord: GeoCoord) {
200 self.sw.lat = self.sw.lat.min(coord.lat);
201 self.sw.lon = self.sw.lon.min(coord.lon);
202 self.ne.lat = self.ne.lat.max(coord.lat);
203 self.ne.lon = self.ne.lon.max(coord.lon);
204 }
205
206 pub fn extend_bounds(&mut self, other: &GeoBounds) {
210 self.sw.lat = self.sw.lat.min(other.sw.lat);
211 self.sw.lon = self.sw.lon.min(other.sw.lon);
212 self.ne.lat = self.ne.lat.max(other.ne.lat);
213 self.ne.lon = self.ne.lon.max(other.ne.lon);
214 }
215
216 pub fn contains_coord(&self, coord: &GeoCoord) -> bool {
224 let lat_ok = self.sw.lat <= coord.lat && coord.lat <= self.ne.lat;
225
226 let lon_ok = if self.sw.lon > self.ne.lon {
227 self.sw.lon <= coord.lon || coord.lon <= self.ne.lon
230 } else {
231 self.sw.lon <= coord.lon && coord.lon <= self.ne.lon
232 };
233
234 lat_ok && lon_ok
235 }
236
237 pub fn intersects(&self, other: &GeoBounds) -> bool {
247 let lat_ok =
249 other.north() >= self.south() && other.south() <= self.north();
250 if !lat_ok {
251 return false;
252 }
253
254 let this_span = (self.east() - self.west()).abs();
256 let other_span = (other.east() - other.west()).abs();
257 if this_span >= 360.0 || other_span >= 360.0 {
258 return true;
259 }
260
261 let this_west = wrap(self.west(), -180.0, 180.0);
263 let this_east = wrap(self.east(), -180.0, 180.0);
264 let other_west = wrap(other.west(), -180.0, 180.0);
265 let other_east = wrap(other.east(), -180.0, 180.0);
266
267 let this_wraps = this_west > this_east;
270 let other_wraps = other_west > other_east;
271
272 if this_wraps && other_wraps {
273 return true;
274 }
275
276 if this_wraps {
277 return other_east >= this_west || other_west <= this_east;
278 }
279
280 if other_wraps {
281 return this_east >= other_west || this_west <= other_east;
282 }
283
284 other_west <= this_east && other_east >= this_west
286 }
287
288 pub fn adjust_antimeridian(&self) -> Self {
299 if self.sw.lon > self.ne.lon {
300 Self {
301 sw: self.sw,
302 ne: GeoCoord {
303 lat: self.ne.lat,
304 lon: self.ne.lon + 360.0,
305 alt: self.ne.alt,
306 },
307 }
308 } else {
309 *self
310 }
311 }
312
313 #[inline]
315 pub fn to_array(&self) -> [f64; 4] {
316 [self.sw.lon, self.sw.lat, self.ne.lon, self.ne.lat]
317 }
318}
319
320impl fmt::Display for GeoBounds {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 write!(
323 f,
324 "GeoBounds(sw: ({:.6}, {:.6}), ne: ({:.6}, {:.6}))",
325 self.sw.lat, self.sw.lon, self.ne.lat, self.ne.lon
326 )
327 }
328}
329
330impl From<[f64; 4]> for GeoBounds {
333 #[inline]
335 fn from(arr: [f64; 4]) -> Self {
336 Self::from_coords(arr[0], arr[1], arr[2], arr[3])
337 }
338}
339
340impl From<GeoBounds> for [f64; 4] {
341 #[inline]
343 fn from(b: GeoBounds) -> Self {
344 b.to_array()
345 }
346}
347
348#[derive(Debug, Clone, Copy, PartialEq)]
371#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
372pub struct WorldBounds {
373 pub min: WorldCoord,
375 pub max: WorldCoord,
377}
378
379impl WorldBounds {
380 #[inline]
382 pub fn new(min: WorldCoord, max: WorldCoord) -> Self {
383 Self { min, max }
384 }
385
386 #[inline]
388 pub fn from_min_max(min: WorldCoord, max: WorldCoord) -> Self {
389 Self { min, max }
390 }
391
392 #[inline]
394 pub fn center(&self) -> WorldCoord {
395 WorldCoord::new(
396 (self.min.position.x + self.max.position.x) * 0.5,
397 (self.min.position.y + self.max.position.y) * 0.5,
398 (self.min.position.z + self.max.position.z) * 0.5,
399 )
400 }
401
402 #[inline]
404 pub fn size(&self) -> (f64, f64, f64) {
405 (
406 self.max.position.x - self.min.position.x,
407 self.max.position.y - self.min.position.y,
408 self.max.position.z - self.min.position.z,
409 )
410 }
411
412 #[inline]
414 pub fn contains_point(&self, point: &WorldCoord) -> bool {
415 point.position.x >= self.min.position.x
416 && point.position.x <= self.max.position.x
417 && point.position.y >= self.min.position.y
418 && point.position.y <= self.max.position.y
419 && point.position.z >= self.min.position.z
420 && point.position.z <= self.max.position.z
421 }
422
423 #[inline]
425 pub fn intersects(&self, other: &WorldBounds) -> bool {
426 self.min.position.x <= other.max.position.x
427 && self.max.position.x >= other.min.position.x
428 && self.min.position.y <= other.max.position.y
429 && self.max.position.y >= other.min.position.y
430 && self.min.position.z <= other.max.position.z
431 && self.max.position.z >= other.min.position.z
432 }
433
434 pub fn extend(&mut self, other: &WorldBounds) {
436 self.min = WorldCoord::new(
437 self.min.position.x.min(other.min.position.x),
438 self.min.position.y.min(other.min.position.y),
439 self.min.position.z.min(other.min.position.z),
440 );
441 self.max = WorldCoord::new(
442 self.max.position.x.max(other.max.position.x),
443 self.max.position.y.max(other.max.position.y),
444 self.max.position.z.max(other.max.position.z),
445 );
446 }
447
448 pub fn extend_point(&mut self, point: &WorldCoord) {
450 self.min = WorldCoord::new(
451 self.min.position.x.min(point.position.x),
452 self.min.position.y.min(point.position.y),
453 self.min.position.z.min(point.position.z),
454 );
455 self.max = WorldCoord::new(
456 self.max.position.x.max(point.position.x),
457 self.max.position.y.max(point.position.y),
458 self.max.position.z.max(point.position.z),
459 );
460 }
461}
462
463impl fmt::Display for WorldBounds {
464 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465 write!(
466 f,
467 "WorldBounds(min: {}, max: {})",
468 self.min, self.max
469 )
470 }
471}
472
473#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
484 fn geo_bounds_new() {
485 let sw = GeoCoord::from_lat_lon(40.7661, -73.9876);
486 let ne = GeoCoord::from_lat_lon(40.8002, -73.9397);
487 let b = GeoBounds::new(sw, ne);
488 assert_eq!(b.sw(), sw);
489 assert_eq!(b.ne(), ne);
490 }
491
492 #[test]
493 fn geo_bounds_from_coords() {
494 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
495 assert!((b.west() - (-73.9876)).abs() < 1e-10);
496 assert!((b.south() - 40.7661).abs() < 1e-10);
497 assert!((b.east() - (-73.9397)).abs() < 1e-10);
498 assert!((b.north() - 40.8002).abs() < 1e-10);
499 }
500
501 #[test]
502 fn geo_bounds_from_array() {
503 let b: GeoBounds = [-73.9876, 40.7661, -73.9397, 40.8002].into();
504 assert!((b.west() - (-73.9876)).abs() < 1e-10);
505 assert!((b.north() - 40.8002).abs() < 1e-10);
506 }
507
508 #[test]
509 fn geo_bounds_to_array_roundtrip() {
510 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
511 let arr: [f64; 4] = b.into();
512 assert_eq!(arr, b.to_array());
513 }
514
515 #[test]
518 fn geo_bounds_corners() {
519 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
520 let nw = b.nw();
521 assert!((nw.lat - 40.8002).abs() < 1e-10);
522 assert!((nw.lon - (-73.9876)).abs() < 1e-10);
523 let se = b.se();
524 assert!((se.lat - 40.7661).abs() < 1e-10);
525 assert!((se.lon - (-73.9397)).abs() < 1e-10);
526 }
527
528 #[test]
531 fn geo_bounds_center() {
532 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
533 let c = b.center();
534 assert!((c.lon - (-73.96365)).abs() < 1e-4);
535 assert!((c.lat - 40.78315).abs() < 1e-4);
536 }
537
538 #[test]
541 fn geo_bounds_extend_coord() {
542 let mut b =
543 GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
544 b.extend_coord(GeoCoord::from_lat_lon(41.0, -74.0));
545 assert!((b.north() - 41.0).abs() < 1e-10);
546 assert!((b.west() - (-74.0)).abs() < 1e-10);
547 }
548
549 #[test]
550 fn geo_bounds_extend_bounds() {
551 let mut a =
552 GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
553 let b = GeoBounds::from_coords(-74.0, 40.5, -73.5, 41.0);
554 a.extend_bounds(&b);
555 assert!((a.west() - (-74.0)).abs() < 1e-10);
556 assert!((a.south() - 40.5).abs() < 1e-10);
557 assert!((a.east() - (-73.5)).abs() < 1e-10);
558 assert!((a.north() - 41.0).abs() < 1e-10);
559 }
560
561 #[test]
564 fn geo_bounds_contains_inside() {
565 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
566 let p = GeoCoord::from_lat_lon(40.7789, -73.9567);
567 assert!(b.contains_coord(&p));
568 }
569
570 #[test]
571 fn geo_bounds_contains_outside() {
572 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
573 let p = GeoCoord::from_lat_lon(41.0, -73.9567);
574 assert!(!b.contains_coord(&p));
575 }
576
577 #[test]
578 fn geo_bounds_contains_antimeridian() {
579 let b = GeoBounds::from_coords(170.0, -20.0, -170.0, -10.0);
581 let inside = GeoCoord::from_lat_lon(-15.0, 175.0);
583 assert!(b.contains_coord(&inside));
584 let outside = GeoCoord::from_lat_lon(-15.0, 0.0);
586 assert!(!b.contains_coord(&outside));
587 }
588
589 #[test]
592 fn geo_bounds_intersects_overlapping() {
593 let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
594 let b = GeoBounds::from_coords(-73.5, 40.5, -72.5, 41.5);
595 assert!(a.intersects(&b));
596 assert!(b.intersects(&a));
597 }
598
599 #[test]
600 fn geo_bounds_intersects_disjoint() {
601 let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
602 let b = GeoBounds::from_coords(10.0, 50.0, 11.0, 51.0);
603 assert!(!a.intersects(&b));
604 }
605
606 #[test]
607 fn geo_bounds_intersects_touching_edge() {
608 let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
609 let b = GeoBounds::from_coords(-73.0, 41.0, -72.0, 42.0);
610 assert!(a.intersects(&b));
611 }
612
613 #[test]
614 fn geo_bounds_intersects_antimeridian_both_wrap() {
615 let a = GeoBounds::from_coords(170.0, -20.0, -170.0, -10.0);
616 let b = GeoBounds::from_coords(160.0, -25.0, -160.0, -5.0);
617 assert!(a.intersects(&b));
618 }
619
620 #[test]
621 fn geo_bounds_intersects_full_world() {
622 let full = GeoBounds::from_coords(-180.0, -90.0, 180.0, 90.0);
623 let small = GeoBounds::from_coords(10.0, 10.0, 11.0, 11.0);
624 assert!(full.intersects(&small));
625 assert!(small.intersects(&full));
626 }
627
628 #[test]
631 fn geo_bounds_from_center_radius_zero() {
632 let center = GeoCoord::from_lat_lon(40.7736, -73.9749);
633 let b = GeoBounds::from_center_radius(center, 0.0);
634 assert!((b.sw().lat - center.lat).abs() < 1e-10);
635 assert!((b.ne().lat - center.lat).abs() < 1e-10);
636 }
637
638 #[test]
639 fn geo_bounds_from_center_radius_100m() {
640 let center = GeoCoord::from_lat_lon(40.7736, -73.9749);
641 let b = GeoBounds::from_center_radius(center, 100.0);
642 assert!(b.sw().lat < center.lat);
643 assert!(b.ne().lat > center.lat);
644 assert!(b.sw().lon < center.lon);
645 assert!(b.ne().lon > center.lon);
646 let lat_span = b.ne().lat - b.sw().lat;
648 assert!((lat_span - 0.001796).abs() < 0.0001);
649 }
650
651 #[test]
654 fn geo_bounds_adjust_antimeridian_no_wrap() {
655 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
656 let adjusted = b.adjust_antimeridian();
657 assert_eq!(b, adjusted);
658 }
659
660 #[test]
661 fn geo_bounds_adjust_antimeridian_wrap() {
662 let b = GeoBounds::from_coords(175.0, -20.0, -178.0, -15.0);
663 let adjusted = b.adjust_antimeridian();
664 assert!((adjusted.sw().lon - 175.0).abs() < 1e-10);
665 assert!((adjusted.ne().lon - 182.0).abs() < 1e-10);
666 }
667
668 #[test]
671 fn geo_bounds_display() {
672 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
673 let s = format!("{b}");
674 assert!(s.contains("GeoBounds"));
675 assert!(s.contains("sw:"));
676 assert!(s.contains("ne:"));
677 }
678
679 #[test]
682 fn world_bounds_new() {
683 let b = WorldBounds::new(
684 WorldCoord::new(-100.0, -200.0, 0.0),
685 WorldCoord::new(100.0, 200.0, 50.0),
686 );
687 assert_eq!(b.min.position.x, -100.0);
688 assert_eq!(b.max.position.y, 200.0);
689 }
690
691 #[test]
694 fn world_bounds_center() {
695 let b = WorldBounds::new(
696 WorldCoord::new(-100.0, -200.0, 0.0),
697 WorldCoord::new(100.0, 200.0, 50.0),
698 );
699 let c = b.center();
700 assert!((c.position.x).abs() < 1e-10);
701 assert!((c.position.y).abs() < 1e-10);
702 assert!((c.position.z - 25.0).abs() < 1e-10);
703 }
704
705 #[test]
708 fn world_bounds_size() {
709 let b = WorldBounds::new(
710 WorldCoord::new(-100.0, -200.0, 0.0),
711 WorldCoord::new(100.0, 200.0, 50.0),
712 );
713 let (sx, sy, sz) = b.size();
714 assert!((sx - 200.0).abs() < 1e-10);
715 assert!((sy - 400.0).abs() < 1e-10);
716 assert!((sz - 50.0).abs() < 1e-10);
717 }
718
719 #[test]
722 fn world_bounds_contains_point() {
723 let b = WorldBounds::new(
724 WorldCoord::new(-100.0, -200.0, 0.0),
725 WorldCoord::new(100.0, 200.0, 50.0),
726 );
727 assert!(b.contains_point(&WorldCoord::new(0.0, 0.0, 25.0)));
728 assert!(!b.contains_point(&WorldCoord::new(200.0, 0.0, 0.0)));
729 }
730
731 #[test]
734 fn world_bounds_intersects() {
735 let a = WorldBounds::new(
736 WorldCoord::new(-100.0, -100.0, 0.0),
737 WorldCoord::new(100.0, 100.0, 0.0),
738 );
739 let b = WorldBounds::new(
740 WorldCoord::new(50.0, 50.0, 0.0),
741 WorldCoord::new(200.0, 200.0, 0.0),
742 );
743 assert!(a.intersects(&b));
744 }
745
746 #[test]
747 fn world_bounds_disjoint() {
748 let a = WorldBounds::new(
749 WorldCoord::new(-100.0, -100.0, 0.0),
750 WorldCoord::new(-50.0, -50.0, 0.0),
751 );
752 let b = WorldBounds::new(
753 WorldCoord::new(50.0, 50.0, 0.0),
754 WorldCoord::new(200.0, 200.0, 0.0),
755 );
756 assert!(!a.intersects(&b));
757 }
758
759 #[test]
762 fn world_bounds_extend() {
763 let mut a = WorldBounds::new(
764 WorldCoord::new(-100.0, -100.0, 0.0),
765 WorldCoord::new(100.0, 100.0, 0.0),
766 );
767 let b = WorldBounds::new(
768 WorldCoord::new(-200.0, 50.0, -10.0),
769 WorldCoord::new(50.0, 300.0, 10.0),
770 );
771 a.extend(&b);
772 assert!((a.min.position.x - (-200.0)).abs() < 1e-10);
773 assert!((a.min.position.y - (-100.0)).abs() < 1e-10);
774 assert!((a.max.position.y - 300.0).abs() < 1e-10);
775 }
776
777 #[test]
778 fn world_bounds_extend_point() {
779 let mut a = WorldBounds::new(
780 WorldCoord::new(0.0, 0.0, 0.0),
781 WorldCoord::new(10.0, 10.0, 0.0),
782 );
783 a.extend_point(&WorldCoord::new(-5.0, 15.0, 3.0));
784 assert!((a.min.position.x - (-5.0)).abs() < 1e-10);
785 assert!((a.max.position.y - 15.0).abs() < 1e-10);
786 assert!((a.max.position.z - 3.0).abs() < 1e-10);
787 }
788
789 #[test]
792 fn world_bounds_display() {
793 let b = WorldBounds::new(
794 WorldCoord::new(1.0, 2.0, 3.0),
795 WorldCoord::new(4.0, 5.0, 6.0),
796 );
797 let s = format!("{b}");
798 assert!(s.contains("WorldBounds"));
799 }
800}