use crate::coord::{GeoCoord, WorldCoord};
use std::fmt;
#[inline]
fn wrap(value: f64, min: f64, max: f64) -> f64 {
let range = max - min;
if range == 0.0 {
return min;
}
((value - min) % range + range) % range + min
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GeoBounds {
sw: GeoCoord,
ne: GeoCoord,
}
impl GeoBounds {
#[inline]
pub fn new(sw: GeoCoord, ne: GeoCoord) -> Self {
Self { sw, ne }
}
#[inline]
pub fn from_coords(west: f64, south: f64, east: f64, north: f64) -> Self {
Self {
sw: GeoCoord::from_lat_lon(south, west),
ne: GeoCoord::from_lat_lon(north, east),
}
}
pub fn from_center_radius(center: GeoCoord, radius_m: f64) -> Self {
const EARTH_CIRCUMFERENCE_M: f64 = 40_075_017.0;
let lat_accuracy = 360.0 * radius_m / EARTH_CIRCUMFERENCE_M;
let lon_accuracy = lat_accuracy / (std::f64::consts::PI / 180.0 * center.lat).cos();
Self {
sw: GeoCoord::from_lat_lon(center.lat - lat_accuracy, center.lon - lon_accuracy),
ne: GeoCoord::from_lat_lon(center.lat + lat_accuracy, center.lon + lon_accuracy),
}
}
#[inline]
pub fn sw(&self) -> GeoCoord {
self.sw
}
#[inline]
pub fn ne(&self) -> GeoCoord {
self.ne
}
#[inline]
pub fn nw(&self) -> GeoCoord {
GeoCoord::from_lat_lon(self.ne.lat, self.sw.lon)
}
#[inline]
pub fn se(&self) -> GeoCoord {
GeoCoord::from_lat_lon(self.sw.lat, self.ne.lon)
}
#[inline]
pub fn west(&self) -> f64 {
self.sw.lon
}
#[inline]
pub fn south(&self) -> f64 {
self.sw.lat
}
#[inline]
pub fn east(&self) -> f64 {
self.ne.lon
}
#[inline]
pub fn north(&self) -> f64 {
self.ne.lat
}
#[inline]
pub fn center(&self) -> GeoCoord {
GeoCoord::from_lat_lon(
(self.sw.lat + self.ne.lat) / 2.0,
(self.sw.lon + self.ne.lon) / 2.0,
)
}
pub fn extend_coord(&mut self, coord: GeoCoord) {
self.sw.lat = self.sw.lat.min(coord.lat);
self.sw.lon = self.sw.lon.min(coord.lon);
self.ne.lat = self.ne.lat.max(coord.lat);
self.ne.lon = self.ne.lon.max(coord.lon);
}
pub fn extend_bounds(&mut self, other: &GeoBounds) {
self.sw.lat = self.sw.lat.min(other.sw.lat);
self.sw.lon = self.sw.lon.min(other.sw.lon);
self.ne.lat = self.ne.lat.max(other.ne.lat);
self.ne.lon = self.ne.lon.max(other.ne.lon);
}
pub fn contains_coord(&self, coord: &GeoCoord) -> bool {
let lat_ok = self.sw.lat <= coord.lat && coord.lat <= self.ne.lat;
let lon_ok = if self.sw.lon > self.ne.lon {
self.sw.lon <= coord.lon || coord.lon <= self.ne.lon
} else {
self.sw.lon <= coord.lon && coord.lon <= self.ne.lon
};
lat_ok && lon_ok
}
pub fn intersects(&self, other: &GeoBounds) -> bool {
let lat_ok = other.north() >= self.south() && other.south() <= self.north();
if !lat_ok {
return false;
}
let this_span = (self.east() - self.west()).abs();
let other_span = (other.east() - other.west()).abs();
if this_span >= 360.0 || other_span >= 360.0 {
return true;
}
let this_west = wrap(self.west(), -180.0, 180.0);
let this_east = wrap(self.east(), -180.0, 180.0);
let other_west = wrap(other.west(), -180.0, 180.0);
let other_east = wrap(other.east(), -180.0, 180.0);
let this_wraps = this_west > this_east;
let other_wraps = other_west > other_east;
if this_wraps && other_wraps {
return true;
}
if this_wraps {
return other_east >= this_west || other_west <= this_east;
}
if other_wraps {
return this_east >= other_west || this_west <= other_east;
}
other_west <= this_east && other_east >= this_west
}
pub fn adjust_antimeridian(&self) -> Self {
if self.sw.lon > self.ne.lon {
Self {
sw: self.sw,
ne: GeoCoord {
lat: self.ne.lat,
lon: self.ne.lon + 360.0,
alt: self.ne.alt,
},
}
} else {
*self
}
}
#[inline]
pub fn to_array(&self) -> [f64; 4] {
[self.sw.lon, self.sw.lat, self.ne.lon, self.ne.lat]
}
}
impl fmt::Display for GeoBounds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"GeoBounds(sw: ({:.6}, {:.6}), ne: ({:.6}, {:.6}))",
self.sw.lat, self.sw.lon, self.ne.lat, self.ne.lon
)
}
}
impl From<[f64; 4]> for GeoBounds {
#[inline]
fn from(arr: [f64; 4]) -> Self {
Self::from_coords(arr[0], arr[1], arr[2], arr[3])
}
}
impl From<GeoBounds> for [f64; 4] {
#[inline]
fn from(b: GeoBounds) -> Self {
b.to_array()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WorldBounds {
pub min: WorldCoord,
pub max: WorldCoord,
}
impl WorldBounds {
#[inline]
pub fn new(min: WorldCoord, max: WorldCoord) -> Self {
Self { min, max }
}
#[inline]
pub fn from_min_max(min: WorldCoord, max: WorldCoord) -> Self {
Self { min, max }
}
#[inline]
pub fn center(&self) -> WorldCoord {
WorldCoord::new(
(self.min.position.x + self.max.position.x) * 0.5,
(self.min.position.y + self.max.position.y) * 0.5,
(self.min.position.z + self.max.position.z) * 0.5,
)
}
#[inline]
pub fn size(&self) -> (f64, f64, f64) {
(
self.max.position.x - self.min.position.x,
self.max.position.y - self.min.position.y,
self.max.position.z - self.min.position.z,
)
}
#[inline]
pub fn contains_point(&self, point: &WorldCoord) -> bool {
point.position.x >= self.min.position.x
&& point.position.x <= self.max.position.x
&& point.position.y >= self.min.position.y
&& point.position.y <= self.max.position.y
&& point.position.z >= self.min.position.z
&& point.position.z <= self.max.position.z
}
#[inline]
pub fn intersects(&self, other: &WorldBounds) -> bool {
self.min.position.x <= other.max.position.x
&& self.max.position.x >= other.min.position.x
&& self.min.position.y <= other.max.position.y
&& self.max.position.y >= other.min.position.y
&& self.min.position.z <= other.max.position.z
&& self.max.position.z >= other.min.position.z
}
pub fn extend(&mut self, other: &WorldBounds) {
self.min = WorldCoord::new(
self.min.position.x.min(other.min.position.x),
self.min.position.y.min(other.min.position.y),
self.min.position.z.min(other.min.position.z),
);
self.max = WorldCoord::new(
self.max.position.x.max(other.max.position.x),
self.max.position.y.max(other.max.position.y),
self.max.position.z.max(other.max.position.z),
);
}
pub fn extend_point(&mut self, point: &WorldCoord) {
self.min = WorldCoord::new(
self.min.position.x.min(point.position.x),
self.min.position.y.min(point.position.y),
self.min.position.z.min(point.position.z),
);
self.max = WorldCoord::new(
self.max.position.x.max(point.position.x),
self.max.position.y.max(point.position.y),
self.max.position.z.max(point.position.z),
);
}
}
impl fmt::Display for WorldBounds {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "WorldBounds(min: {}, max: {})", self.min, self.max)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn geo_bounds_new() {
let sw = GeoCoord::from_lat_lon(40.7661, -73.9876);
let ne = GeoCoord::from_lat_lon(40.8002, -73.9397);
let b = GeoBounds::new(sw, ne);
assert_eq!(b.sw(), sw);
assert_eq!(b.ne(), ne);
}
#[test]
fn geo_bounds_from_coords() {
let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
assert!((b.west() - (-73.9876)).abs() < 1e-10);
assert!((b.south() - 40.7661).abs() < 1e-10);
assert!((b.east() - (-73.9397)).abs() < 1e-10);
assert!((b.north() - 40.8002).abs() < 1e-10);
}
#[test]
fn geo_bounds_from_array() {
let b: GeoBounds = [-73.9876, 40.7661, -73.9397, 40.8002].into();
assert!((b.west() - (-73.9876)).abs() < 1e-10);
assert!((b.north() - 40.8002).abs() < 1e-10);
}
#[test]
fn geo_bounds_to_array_roundtrip() {
let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
let arr: [f64; 4] = b.into();
assert_eq!(arr, b.to_array());
}
#[test]
fn geo_bounds_corners() {
let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
let nw = b.nw();
assert!((nw.lat - 40.8002).abs() < 1e-10);
assert!((nw.lon - (-73.9876)).abs() < 1e-10);
let se = b.se();
assert!((se.lat - 40.7661).abs() < 1e-10);
assert!((se.lon - (-73.9397)).abs() < 1e-10);
}
#[test]
fn geo_bounds_center() {
let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
let c = b.center();
assert!((c.lon - (-73.96365)).abs() < 1e-4);
assert!((c.lat - 40.78315).abs() < 1e-4);
}
#[test]
fn geo_bounds_extend_coord() {
let mut b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
b.extend_coord(GeoCoord::from_lat_lon(41.0, -74.0));
assert!((b.north() - 41.0).abs() < 1e-10);
assert!((b.west() - (-74.0)).abs() < 1e-10);
}
#[test]
fn geo_bounds_extend_bounds() {
let mut a = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
let b = GeoBounds::from_coords(-74.0, 40.5, -73.5, 41.0);
a.extend_bounds(&b);
assert!((a.west() - (-74.0)).abs() < 1e-10);
assert!((a.south() - 40.5).abs() < 1e-10);
assert!((a.east() - (-73.5)).abs() < 1e-10);
assert!((a.north() - 41.0).abs() < 1e-10);
}
#[test]
fn geo_bounds_contains_inside() {
let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
let p = GeoCoord::from_lat_lon(40.7789, -73.9567);
assert!(b.contains_coord(&p));
}
#[test]
fn geo_bounds_contains_outside() {
let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
let p = GeoCoord::from_lat_lon(41.0, -73.9567);
assert!(!b.contains_coord(&p));
}
#[test]
fn geo_bounds_contains_antimeridian() {
let b = GeoBounds::from_coords(170.0, -20.0, -170.0, -10.0);
let inside = GeoCoord::from_lat_lon(-15.0, 175.0);
assert!(b.contains_coord(&inside));
let outside = GeoCoord::from_lat_lon(-15.0, 0.0);
assert!(!b.contains_coord(&outside));
}
#[test]
fn geo_bounds_intersects_overlapping() {
let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
let b = GeoBounds::from_coords(-73.5, 40.5, -72.5, 41.5);
assert!(a.intersects(&b));
assert!(b.intersects(&a));
}
#[test]
fn geo_bounds_intersects_disjoint() {
let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
let b = GeoBounds::from_coords(10.0, 50.0, 11.0, 51.0);
assert!(!a.intersects(&b));
}
#[test]
fn geo_bounds_intersects_touching_edge() {
let a = GeoBounds::from_coords(-74.0, 40.0, -73.0, 41.0);
let b = GeoBounds::from_coords(-73.0, 41.0, -72.0, 42.0);
assert!(a.intersects(&b));
}
#[test]
fn geo_bounds_intersects_antimeridian_both_wrap() {
let a = GeoBounds::from_coords(170.0, -20.0, -170.0, -10.0);
let b = GeoBounds::from_coords(160.0, -25.0, -160.0, -5.0);
assert!(a.intersects(&b));
}
#[test]
fn geo_bounds_intersects_full_world() {
let full = GeoBounds::from_coords(-180.0, -90.0, 180.0, 90.0);
let small = GeoBounds::from_coords(10.0, 10.0, 11.0, 11.0);
assert!(full.intersects(&small));
assert!(small.intersects(&full));
}
#[test]
fn geo_bounds_from_center_radius_zero() {
let center = GeoCoord::from_lat_lon(40.7736, -73.9749);
let b = GeoBounds::from_center_radius(center, 0.0);
assert!((b.sw().lat - center.lat).abs() < 1e-10);
assert!((b.ne().lat - center.lat).abs() < 1e-10);
}
#[test]
fn geo_bounds_from_center_radius_100m() {
let center = GeoCoord::from_lat_lon(40.7736, -73.9749);
let b = GeoBounds::from_center_radius(center, 100.0);
assert!(b.sw().lat < center.lat);
assert!(b.ne().lat > center.lat);
assert!(b.sw().lon < center.lon);
assert!(b.ne().lon > center.lon);
let lat_span = b.ne().lat - b.sw().lat;
assert!((lat_span - 0.001796).abs() < 0.0001);
}
#[test]
fn geo_bounds_adjust_antimeridian_no_wrap() {
let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
let adjusted = b.adjust_antimeridian();
assert_eq!(b, adjusted);
}
#[test]
fn geo_bounds_adjust_antimeridian_wrap() {
let b = GeoBounds::from_coords(175.0, -20.0, -178.0, -15.0);
let adjusted = b.adjust_antimeridian();
assert!((adjusted.sw().lon - 175.0).abs() < 1e-10);
assert!((adjusted.ne().lon - 182.0).abs() < 1e-10);
}
#[test]
fn geo_bounds_display() {
let b = GeoBounds::from_coords(-73.9876, 40.7661, -73.9397, 40.8002);
let s = format!("{b}");
assert!(s.contains("GeoBounds"));
assert!(s.contains("sw:"));
assert!(s.contains("ne:"));
}
#[test]
fn world_bounds_new() {
let b = WorldBounds::new(
WorldCoord::new(-100.0, -200.0, 0.0),
WorldCoord::new(100.0, 200.0, 50.0),
);
assert_eq!(b.min.position.x, -100.0);
assert_eq!(b.max.position.y, 200.0);
}
#[test]
fn world_bounds_center() {
let b = WorldBounds::new(
WorldCoord::new(-100.0, -200.0, 0.0),
WorldCoord::new(100.0, 200.0, 50.0),
);
let c = b.center();
assert!((c.position.x).abs() < 1e-10);
assert!((c.position.y).abs() < 1e-10);
assert!((c.position.z - 25.0).abs() < 1e-10);
}
#[test]
fn world_bounds_size() {
let b = WorldBounds::new(
WorldCoord::new(-100.0, -200.0, 0.0),
WorldCoord::new(100.0, 200.0, 50.0),
);
let (sx, sy, sz) = b.size();
assert!((sx - 200.0).abs() < 1e-10);
assert!((sy - 400.0).abs() < 1e-10);
assert!((sz - 50.0).abs() < 1e-10);
}
#[test]
fn world_bounds_contains_point() {
let b = WorldBounds::new(
WorldCoord::new(-100.0, -200.0, 0.0),
WorldCoord::new(100.0, 200.0, 50.0),
);
assert!(b.contains_point(&WorldCoord::new(0.0, 0.0, 25.0)));
assert!(!b.contains_point(&WorldCoord::new(200.0, 0.0, 0.0)));
}
#[test]
fn world_bounds_intersects() {
let a = WorldBounds::new(
WorldCoord::new(-100.0, -100.0, 0.0),
WorldCoord::new(100.0, 100.0, 0.0),
);
let b = WorldBounds::new(
WorldCoord::new(50.0, 50.0, 0.0),
WorldCoord::new(200.0, 200.0, 0.0),
);
assert!(a.intersects(&b));
}
#[test]
fn world_bounds_disjoint() {
let a = WorldBounds::new(
WorldCoord::new(-100.0, -100.0, 0.0),
WorldCoord::new(-50.0, -50.0, 0.0),
);
let b = WorldBounds::new(
WorldCoord::new(50.0, 50.0, 0.0),
WorldCoord::new(200.0, 200.0, 0.0),
);
assert!(!a.intersects(&b));
}
#[test]
fn world_bounds_extend() {
let mut a = WorldBounds::new(
WorldCoord::new(-100.0, -100.0, 0.0),
WorldCoord::new(100.0, 100.0, 0.0),
);
let b = WorldBounds::new(
WorldCoord::new(-200.0, 50.0, -10.0),
WorldCoord::new(50.0, 300.0, 10.0),
);
a.extend(&b);
assert!((a.min.position.x - (-200.0)).abs() < 1e-10);
assert!((a.min.position.y - (-100.0)).abs() < 1e-10);
assert!((a.max.position.y - 300.0).abs() < 1e-10);
}
#[test]
fn world_bounds_extend_point() {
let mut a = WorldBounds::new(
WorldCoord::new(0.0, 0.0, 0.0),
WorldCoord::new(10.0, 10.0, 0.0),
);
a.extend_point(&WorldCoord::new(-5.0, 15.0, 3.0));
assert!((a.min.position.x - (-5.0)).abs() < 1e-10);
assert!((a.max.position.y - 15.0).abs() < 1e-10);
assert!((a.max.position.z - 3.0).abs() < 1e-10);
}
#[test]
fn world_bounds_display() {
let b = WorldBounds::new(
WorldCoord::new(1.0, 2.0, 3.0),
WorldCoord::new(4.0, 5.0, 6.0),
);
let s = format!("{b}");
assert!(s.contains("WorldBounds"));
}
}