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 = lat_accuracy / (std::f64::consts::PI / 180.0 * center.lat).cos();
119
120 Self {
121 sw: GeoCoord::from_lat_lon(center.lat - lat_accuracy, center.lon - lon_accuracy),
122 ne: GeoCoord::from_lat_lon(center.lat + lat_accuracy, center.lon + lon_accuracy),
123 }
124 }
125
126 #[inline]
130 pub fn sw(&self) -> GeoCoord {
131 self.sw
132 }
133
134 #[inline]
136 pub fn ne(&self) -> GeoCoord {
137 self.ne
138 }
139
140 #[inline]
142 pub fn nw(&self) -> GeoCoord {
143 GeoCoord::from_lat_lon(self.ne.lat, self.sw.lon)
144 }
145
146 #[inline]
148 pub fn se(&self) -> GeoCoord {
149 GeoCoord::from_lat_lon(self.sw.lat, self.ne.lon)
150 }
151
152 #[inline]
154 pub fn west(&self) -> f64 {
155 self.sw.lon
156 }
157
158 #[inline]
160 pub fn south(&self) -> f64 {
161 self.sw.lat
162 }
163
164 #[inline]
166 pub fn east(&self) -> f64 {
167 self.ne.lon
168 }
169
170 #[inline]
172 pub fn north(&self) -> f64 {
173 self.ne.lat
174 }
175
176 #[inline]
180 pub fn center(&self) -> GeoCoord {
181 GeoCoord::from_lat_lon(
182 (self.sw.lat + self.ne.lat) / 2.0,
183 (self.sw.lon + self.ne.lon) / 2.0,
184 )
185 }
186
187 pub fn extend_coord(&mut self, coord: GeoCoord) {
193 self.sw.lat = self.sw.lat.min(coord.lat);
194 self.sw.lon = self.sw.lon.min(coord.lon);
195 self.ne.lat = self.ne.lat.max(coord.lat);
196 self.ne.lon = self.ne.lon.max(coord.lon);
197 }
198
199 pub fn extend_bounds(&mut self, other: &GeoBounds) {
203 self.sw.lat = self.sw.lat.min(other.sw.lat);
204 self.sw.lon = self.sw.lon.min(other.sw.lon);
205 self.ne.lat = self.ne.lat.max(other.ne.lat);
206 self.ne.lon = self.ne.lon.max(other.ne.lon);
207 }
208
209 pub fn contains_coord(&self, coord: &GeoCoord) -> bool {
217 let lat_ok = self.sw.lat <= coord.lat && coord.lat <= self.ne.lat;
218
219 let lon_ok = if self.sw.lon > self.ne.lon {
220 self.sw.lon <= coord.lon || coord.lon <= self.ne.lon
223 } else {
224 self.sw.lon <= coord.lon && coord.lon <= self.ne.lon
225 };
226
227 lat_ok && lon_ok
228 }
229
230 pub fn intersects(&self, other: &GeoBounds) -> bool {
240 let lat_ok = other.north() >= self.south() && other.south() <= self.north();
242 if !lat_ok {
243 return false;
244 }
245
246 let this_span = (self.east() - self.west()).abs();
248 let other_span = (other.east() - other.west()).abs();
249 if this_span >= 360.0 || other_span >= 360.0 {
250 return true;
251 }
252
253 let this_west = wrap(self.west(), -180.0, 180.0);
255 let this_east = wrap(self.east(), -180.0, 180.0);
256 let other_west = wrap(other.west(), -180.0, 180.0);
257 let other_east = wrap(other.east(), -180.0, 180.0);
258
259 let this_wraps = this_west > this_east;
262 let other_wraps = other_west > other_east;
263
264 if this_wraps && other_wraps {
265 return true;
266 }
267
268 if this_wraps {
269 return other_east >= this_west || other_west <= this_east;
270 }
271
272 if other_wraps {
273 return this_east >= other_west || this_west <= other_east;
274 }
275
276 other_west <= this_east && other_east >= this_west
278 }
279
280 pub fn adjust_antimeridian(&self) -> Self {
291 if self.sw.lon > self.ne.lon {
292 Self {
293 sw: self.sw,
294 ne: GeoCoord {
295 lat: self.ne.lat,
296 lon: self.ne.lon + 360.0,
297 alt: self.ne.alt,
298 },
299 }
300 } else {
301 *self
302 }
303 }
304
305 #[inline]
307 pub fn to_array(&self) -> [f64; 4] {
308 [self.sw.lon, self.sw.lat, self.ne.lon, self.ne.lat]
309 }
310}
311
312impl fmt::Display for GeoBounds {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 write!(
315 f,
316 "GeoBounds(sw: ({:.6}, {:.6}), ne: ({:.6}, {:.6}))",
317 self.sw.lat, self.sw.lon, self.ne.lat, self.ne.lon
318 )
319 }
320}
321
322impl From<[f64; 4]> for GeoBounds {
325 #[inline]
327 fn from(arr: [f64; 4]) -> Self {
328 Self::from_coords(arr[0], arr[1], arr[2], arr[3])
329 }
330}
331
332impl From<GeoBounds> for [f64; 4] {
333 #[inline]
335 fn from(b: GeoBounds) -> Self {
336 b.to_array()
337 }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq)]
363#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
364pub struct WorldBounds {
365 pub min: WorldCoord,
367 pub max: WorldCoord,
369}
370
371impl WorldBounds {
372 #[inline]
374 pub fn new(min: WorldCoord, max: WorldCoord) -> Self {
375 Self { min, max }
376 }
377
378 #[inline]
380 pub fn from_min_max(min: WorldCoord, max: WorldCoord) -> Self {
381 Self { min, max }
382 }
383
384 #[inline]
386 pub fn center(&self) -> WorldCoord {
387 WorldCoord::new(
388 (self.min.position.x + self.max.position.x) * 0.5,
389 (self.min.position.y + self.max.position.y) * 0.5,
390 (self.min.position.z + self.max.position.z) * 0.5,
391 )
392 }
393
394 #[inline]
396 pub fn size(&self) -> (f64, f64, f64) {
397 (
398 self.max.position.x - self.min.position.x,
399 self.max.position.y - self.min.position.y,
400 self.max.position.z - self.min.position.z,
401 )
402 }
403
404 #[inline]
406 pub fn contains_point(&self, point: &WorldCoord) -> bool {
407 point.position.x >= self.min.position.x
408 && point.position.x <= self.max.position.x
409 && point.position.y >= self.min.position.y
410 && point.position.y <= self.max.position.y
411 && point.position.z >= self.min.position.z
412 && point.position.z <= self.max.position.z
413 }
414
415 #[inline]
417 pub fn intersects(&self, other: &WorldBounds) -> bool {
418 self.min.position.x <= other.max.position.x
419 && self.max.position.x >= other.min.position.x
420 && self.min.position.y <= other.max.position.y
421 && self.max.position.y >= other.min.position.y
422 && self.min.position.z <= other.max.position.z
423 && self.max.position.z >= other.min.position.z
424 }
425
426 pub fn extend(&mut self, other: &WorldBounds) {
428 self.min = WorldCoord::new(
429 self.min.position.x.min(other.min.position.x),
430 self.min.position.y.min(other.min.position.y),
431 self.min.position.z.min(other.min.position.z),
432 );
433 self.max = WorldCoord::new(
434 self.max.position.x.max(other.max.position.x),
435 self.max.position.y.max(other.max.position.y),
436 self.max.position.z.max(other.max.position.z),
437 );
438 }
439
440 pub fn extend_point(&mut self, point: &WorldCoord) {
442 self.min = WorldCoord::new(
443 self.min.position.x.min(point.position.x),
444 self.min.position.y.min(point.position.y),
445 self.min.position.z.min(point.position.z),
446 );
447 self.max = WorldCoord::new(
448 self.max.position.x.max(point.position.x),
449 self.max.position.y.max(point.position.y),
450 self.max.position.z.max(point.position.z),
451 );
452 }
453}
454
455impl fmt::Display for WorldBounds {
456 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457 write!(f, "WorldBounds(min: {}, max: {})", self.min, self.max)
458 }
459}
460
461#[cfg(test)]
466mod tests {
467 use super::*;
468
469 #[test]
472 fn geo_bounds_new() {
473 let sw = GeoCoord::from_lat_lon(40.7661, -73.9876);
474 let ne = GeoCoord::from_lat_lon(40.8002, -73.9397);
475 let b = GeoBounds::new(sw, ne);
476 assert_eq!(b.sw(), sw);
477 assert_eq!(b.ne(), ne);
478 }
479
480 #[test]
481 fn geo_bounds_from_coords() {
482 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
483 assert!((b.west() - (-73.9876)).abs() < 1e-10);
484 assert!((b.south() - 40.7661).abs() < 1e-10);
485 assert!((b.east() - (-73.9397)).abs() < 1e-10);
486 assert!((b.north() - 40.8002).abs() < 1e-10);
487 }
488
489 #[test]
490 fn geo_bounds_from_array() {
491 let b: GeoBounds = [-73.9876, 40.7661, -73.9397, 40.8002].into();
492 assert!((b.west() - (-73.9876)).abs() < 1e-10);
493 assert!((b.north() - 40.8002).abs() < 1e-10);
494 }
495
496 #[test]
497 fn geo_bounds_to_array_roundtrip() {
498 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
499 let arr: [f64; 4] = b.into();
500 assert_eq!(arr, b.to_array());
501 }
502
503 #[test]
506 fn geo_bounds_corners() {
507 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
508 let nw = b.nw();
509 assert!((nw.lat - 40.8002).abs() < 1e-10);
510 assert!((nw.lon - (-73.9876)).abs() < 1e-10);
511 let se = b.se();
512 assert!((se.lat - 40.7661).abs() < 1e-10);
513 assert!((se.lon - (-73.9397)).abs() < 1e-10);
514 }
515
516 #[test]
519 fn geo_bounds_center() {
520 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
521 let c = b.center();
522 assert!((c.lon - (-73.96365)).abs() < 1e-4);
523 assert!((c.lat - 40.78315).abs() < 1e-4);
524 }
525
526 #[test]
529 fn geo_bounds_extend_coord() {
530 let mut b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
531 b.extend_coord(GeoCoord::from_lat_lon(41.0, -74.0));
532 assert!((b.north() - 41.0).abs() < 1e-10);
533 assert!((b.west() - (-74.0)).abs() < 1e-10);
534 }
535
536 #[test]
537 fn geo_bounds_extend_bounds() {
538 let mut a = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
539 let b = GeoBounds::from_coords(-74.0, 40.5, -73.5, 41.0);
540 a.extend_bounds(&b);
541 assert!((a.west() - (-74.0)).abs() < 1e-10);
542 assert!((a.south() - 40.5).abs() < 1e-10);
543 assert!((a.east() - (-73.5)).abs() < 1e-10);
544 assert!((a.north() - 41.0).abs() < 1e-10);
545 }
546
547 #[test]
550 fn geo_bounds_contains_inside() {
551 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
552 let p = GeoCoord::from_lat_lon(40.7789, -73.9567);
553 assert!(b.contains_coord(&p));
554 }
555
556 #[test]
557 fn geo_bounds_contains_outside() {
558 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
559 let p = GeoCoord::from_lat_lon(41.0, -73.9567);
560 assert!(!b.contains_coord(&p));
561 }
562
563 #[test]
564 fn geo_bounds_contains_antimeridian() {
565 let b = GeoBounds::from_coords(170.0, -20.0, -170.0, -10.0);
567 let inside = GeoCoord::from_lat_lon(-15.0, 175.0);
569 assert!(b.contains_coord(&inside));
570 let outside = GeoCoord::from_lat_lon(-15.0, 0.0);
572 assert!(!b.contains_coord(&outside));
573 }
574
575 #[test]
578 fn geo_bounds_intersects_overlapping() {
579 let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
580 let b = GeoBounds::from_coords(-73.5, 40.5, -72.5, 41.5);
581 assert!(a.intersects(&b));
582 assert!(b.intersects(&a));
583 }
584
585 #[test]
586 fn geo_bounds_intersects_disjoint() {
587 let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
588 let b = GeoBounds::from_coords(10.0, 50.0, 11.0, 51.0);
589 assert!(!a.intersects(&b));
590 }
591
592 #[test]
593 fn geo_bounds_intersects_touching_edge() {
594 let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
595 let b = GeoBounds::from_coords(-73.0, 41.0, -72.0, 42.0);
596 assert!(a.intersects(&b));
597 }
598
599 #[test]
600 fn geo_bounds_intersects_antimeridian_both_wrap() {
601 let a = GeoBounds::from_coords(170.0, -20.0, -170.0, -10.0);
602 let b = GeoBounds::from_coords(160.0, -25.0, -160.0, -5.0);
603 assert!(a.intersects(&b));
604 }
605
606 #[test]
607 fn geo_bounds_intersects_full_world() {
608 let full = GeoBounds::from_coords(-180.0, -90.0, 180.0, 90.0);
609 let small = GeoBounds::from_coords(10.0, 10.0, 11.0, 11.0);
610 assert!(full.intersects(&small));
611 assert!(small.intersects(&full));
612 }
613
614 #[test]
617 fn geo_bounds_from_center_radius_zero() {
618 let center = GeoCoord::from_lat_lon(40.7736, -73.9749);
619 let b = GeoBounds::from_center_radius(center, 0.0);
620 assert!((b.sw().lat - center.lat).abs() < 1e-10);
621 assert!((b.ne().lat - center.lat).abs() < 1e-10);
622 }
623
624 #[test]
625 fn geo_bounds_from_center_radius_100m() {
626 let center = GeoCoord::from_lat_lon(40.7736, -73.9749);
627 let b = GeoBounds::from_center_radius(center, 100.0);
628 assert!(b.sw().lat < center.lat);
629 assert!(b.ne().lat > center.lat);
630 assert!(b.sw().lon < center.lon);
631 assert!(b.ne().lon > center.lon);
632 let lat_span = b.ne().lat - b.sw().lat;
634 assert!((lat_span - 0.001796).abs() < 0.0001);
635 }
636
637 #[test]
640 fn geo_bounds_adjust_antimeridian_no_wrap() {
641 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
642 let adjusted = b.adjust_antimeridian();
643 assert_eq!(b, adjusted);
644 }
645
646 #[test]
647 fn geo_bounds_adjust_antimeridian_wrap() {
648 let b = GeoBounds::from_coords(175.0, -20.0, -178.0, -15.0);
649 let adjusted = b.adjust_antimeridian();
650 assert!((adjusted.sw().lon - 175.0).abs() < 1e-10);
651 assert!((adjusted.ne().lon - 182.0).abs() < 1e-10);
652 }
653
654 #[test]
657 fn geo_bounds_display() {
658 let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
659 let s = format!("{b}");
660 assert!(s.contains("GeoBounds"));
661 assert!(s.contains("sw:"));
662 assert!(s.contains("ne:"));
663 }
664
665 #[test]
668 fn world_bounds_new() {
669 let b = WorldBounds::new(
670 WorldCoord::new(-100.0, -200.0, 0.0),
671 WorldCoord::new(100.0, 200.0, 50.0),
672 );
673 assert_eq!(b.min.position.x, -100.0);
674 assert_eq!(b.max.position.y, 200.0);
675 }
676
677 #[test]
680 fn world_bounds_center() {
681 let b = WorldBounds::new(
682 WorldCoord::new(-100.0, -200.0, 0.0),
683 WorldCoord::new(100.0, 200.0, 50.0),
684 );
685 let c = b.center();
686 assert!((c.position.x).abs() < 1e-10);
687 assert!((c.position.y).abs() < 1e-10);
688 assert!((c.position.z - 25.0).abs() < 1e-10);
689 }
690
691 #[test]
694 fn world_bounds_size() {
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 (sx, sy, sz) = b.size();
700 assert!((sx - 200.0).abs() < 1e-10);
701 assert!((sy - 400.0).abs() < 1e-10);
702 assert!((sz - 50.0).abs() < 1e-10);
703 }
704
705 #[test]
708 fn world_bounds_contains_point() {
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 assert!(b.contains_point(&WorldCoord::new(0.0, 0.0, 25.0)));
714 assert!(!b.contains_point(&WorldCoord::new(200.0, 0.0, 0.0)));
715 }
716
717 #[test]
720 fn world_bounds_intersects() {
721 let a = WorldBounds::new(
722 WorldCoord::new(-100.0, -100.0, 0.0),
723 WorldCoord::new(100.0, 100.0, 0.0),
724 );
725 let b = WorldBounds::new(
726 WorldCoord::new(50.0, 50.0, 0.0),
727 WorldCoord::new(200.0, 200.0, 0.0),
728 );
729 assert!(a.intersects(&b));
730 }
731
732 #[test]
733 fn world_bounds_disjoint() {
734 let a = WorldBounds::new(
735 WorldCoord::new(-100.0, -100.0, 0.0),
736 WorldCoord::new(-50.0, -50.0, 0.0),
737 );
738 let b = WorldBounds::new(
739 WorldCoord::new(50.0, 50.0, 0.0),
740 WorldCoord::new(200.0, 200.0, 0.0),
741 );
742 assert!(!a.intersects(&b));
743 }
744
745 #[test]
748 fn world_bounds_extend() {
749 let mut a = WorldBounds::new(
750 WorldCoord::new(-100.0, -100.0, 0.0),
751 WorldCoord::new(100.0, 100.0, 0.0),
752 );
753 let b = WorldBounds::new(
754 WorldCoord::new(-200.0, 50.0, -10.0),
755 WorldCoord::new(50.0, 300.0, 10.0),
756 );
757 a.extend(&b);
758 assert!((a.min.position.x - (-200.0)).abs() < 1e-10);
759 assert!((a.min.position.y - (-100.0)).abs() < 1e-10);
760 assert!((a.max.position.y - 300.0).abs() < 1e-10);
761 }
762
763 #[test]
764 fn world_bounds_extend_point() {
765 let mut a = WorldBounds::new(
766 WorldCoord::new(0.0, 0.0, 0.0),
767 WorldCoord::new(10.0, 10.0, 0.0),
768 );
769 a.extend_point(&WorldCoord::new(-5.0, 15.0, 3.0));
770 assert!((a.min.position.x - (-5.0)).abs() < 1e-10);
771 assert!((a.max.position.y - 15.0).abs() < 1e-10);
772 assert!((a.max.position.z - 3.0).abs() < 1e-10);
773 }
774
775 #[test]
778 fn world_bounds_display() {
779 let b = WorldBounds::new(
780 WorldCoord::new(1.0, 2.0, 3.0),
781 WorldCoord::new(4.0, 5.0, 6.0),
782 );
783 let s = format!("{b}");
784 assert!(s.contains("WorldBounds"));
785 }
786}