use super::coordinates::EARTH_RADIUS_M;
use crate::error::{SpatialError, SpatialResult};
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoundingBox {
pub min_lat: f64,
pub max_lat: f64,
pub min_lon: f64,
pub max_lon: f64,
}
impl BoundingBox {
pub fn new(min_lat: f64, max_lat: f64, min_lon: f64, max_lon: f64) -> SpatialResult<Self> {
if min_lat > max_lat {
return Err(SpatialError::ValueError(format!(
"min_lat ({min_lat}) > max_lat ({max_lat})"
)));
}
if min_lon > max_lon {
return Err(SpatialError::ValueError(format!(
"min_lon ({min_lon}) > max_lon ({max_lon})"
)));
}
if !(-90.0..=90.0).contains(&min_lat) || !(-90.0..=90.0).contains(&max_lat) {
return Err(SpatialError::ValueError(
"Latitude values must be in range [-90, 90]".to_string(),
));
}
if !(-180.0..=180.0).contains(&min_lon) || !(-180.0..=180.0).contains(&max_lon) {
return Err(SpatialError::ValueError(
"Longitude values must be in range [-180, 180]".to_string(),
));
}
Ok(BoundingBox {
min_lat,
max_lat,
min_lon,
max_lon,
})
}
pub fn from_points(points: &[(f64, f64)]) -> SpatialResult<Self> {
if points.is_empty() {
return Err(SpatialError::ValueError(
"Cannot create BoundingBox from empty point set".to_string(),
));
}
let mut min_lat = f64::INFINITY;
let mut max_lat = f64::NEG_INFINITY;
let mut min_lon = f64::INFINITY;
let mut max_lon = f64::NEG_INFINITY;
for &(lat, lon) in points {
if lat < min_lat {
min_lat = lat;
}
if lat > max_lat {
max_lat = lat;
}
if lon < min_lon {
min_lon = lon;
}
if lon > max_lon {
max_lon = lon;
}
}
Ok(BoundingBox {
min_lat,
max_lat,
min_lon,
max_lon,
})
}
pub fn contains(&self, lat: f64, lon: f64) -> bool {
lat >= self.min_lat && lat <= self.max_lat && lon >= self.min_lon && lon <= self.max_lon
}
pub fn intersects(&self, other: &BoundingBox) -> bool {
self.min_lat <= other.max_lat
&& self.max_lat >= other.min_lat
&& self.min_lon <= other.max_lon
&& self.max_lon >= other.min_lon
}
pub fn expand(&self, margin_deg: f64) -> SpatialResult<BoundingBox> {
if margin_deg < 0.0 {
return Err(SpatialError::ValueError(format!(
"margin_deg ({margin_deg}) must be non-negative"
)));
}
Ok(BoundingBox {
min_lat: (self.min_lat - margin_deg).max(-90.0),
max_lat: (self.max_lat + margin_deg).min(90.0),
min_lon: (self.min_lon - margin_deg).max(-180.0),
max_lon: (self.max_lon + margin_deg).min(180.0),
})
}
pub fn center(&self) -> (f64, f64) {
(
(self.min_lat + self.max_lat) / 2.0,
(self.min_lon + self.max_lon) / 2.0,
)
}
pub fn width_deg(&self) -> f64 {
self.max_lon - self.min_lon
}
pub fn height_deg(&self) -> f64 {
self.max_lat - self.min_lat
}
pub fn area_m2(&self) -> f64 {
let lat_center = (self.min_lat + self.max_lat) / 2.0;
let m_per_deg_lat = PI / 180.0 * EARTH_RADIUS_M;
let m_per_deg_lon = PI / 180.0 * EARTH_RADIUS_M * lat_center.to_radians().cos();
self.height_deg() * m_per_deg_lat * self.width_deg() * m_per_deg_lon
}
}
pub fn polygon_area(coords: &[(f64, f64)]) -> SpatialResult<f64> {
if coords.len() < 3 {
return Err(SpatialError::ValueError(
"Polygon must have at least 3 vertices".to_string(),
));
}
let centroid_lat = coords.iter().map(|(lat, _)| lat).sum::<f64>() / coords.len() as f64;
let m_per_deg_lat = PI / 180.0 * EARTH_RADIUS_M;
let m_per_deg_lon = PI / 180.0 * EARTH_RADIUS_M * centroid_lat.to_radians().cos();
let n = coords.len();
let mut area2 = 0.0_f64;
for i in 0..n {
let j = (i + 1) % n;
let xi = coords[i].1 * m_per_deg_lon; let yi = coords[i].0 * m_per_deg_lat; let xj = coords[j].1 * m_per_deg_lon;
let yj = coords[j].0 * m_per_deg_lat;
area2 += xi * yj - xj * yi;
}
Ok(area2.abs() / 2.0)
}
pub fn point_in_polygon(lat: f64, lon: f64, polygon: &[(f64, f64)]) -> bool {
let n = polygon.len();
if n < 3 {
return false;
}
for &(vlat, vlon) in polygon {
if (vlat - lat).abs() < f64::EPSILON && (vlon - lon).abs() < f64::EPSILON {
return true;
}
}
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let (yi, xi) = polygon[i];
let (yj, xj) = polygon[j];
if is_on_segment(lat, lon, yi, xi, yj, xj) {
return true;
}
if ((yi > lat) != (yj > lat)) && (lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) {
inside = !inside;
}
j = i;
}
inside
}
fn is_on_segment(py: f64, px: f64, y0: f64, x0: f64, y1: f64, x1: f64) -> bool {
let cross = (x1 - x0) * (py - y0) - (y1 - y0) * (px - x0);
if cross.abs() > 1e-10 {
return false;
}
let min_x = x0.min(x1);
let max_x = x0.max(x1);
let min_y = y0.min(y1);
let max_y = y0.max(y1);
px >= min_x && px <= max_x && py >= min_y && py <= max_y
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bbox_contains() {
let bbox = BoundingBox::new(48.0, 52.0, -1.0, 3.0).expect("bbox");
assert!(bbox.contains(50.0, 1.0));
assert!(bbox.contains(48.0, -1.0)); assert!(!bbox.contains(53.0, 1.0));
assert!(!bbox.contains(50.0, 4.0));
}
#[test]
fn test_bbox_intersects() {
let a = BoundingBox::new(0.0, 2.0, 0.0, 2.0).expect("a");
let b = BoundingBox::new(1.0, 3.0, 1.0, 3.0).expect("b");
let c = BoundingBox::new(3.0, 5.0, 3.0, 5.0).expect("c");
assert!(a.intersects(&b));
assert!(b.intersects(&a));
assert!(!a.intersects(&c));
}
#[test]
fn test_bbox_expand() {
let bbox = BoundingBox::new(10.0, 20.0, 10.0, 20.0).expect("bbox");
let expanded = bbox.expand(5.0).expect("expand");
assert!((expanded.min_lat - 5.0).abs() < 1e-10);
assert!((expanded.max_lat - 25.0).abs() < 1e-10);
assert!((expanded.min_lon - 5.0).abs() < 1e-10);
assert!((expanded.max_lon - 25.0).abs() < 1e-10);
}
#[test]
fn test_bbox_expand_clamped() {
let bbox = BoundingBox::new(-89.0, 89.0, -179.0, 179.0).expect("bbox");
let expanded = bbox.expand(5.0).expect("expand");
assert!(expanded.min_lat >= -90.0);
assert!(expanded.max_lat <= 90.0);
assert!(expanded.min_lon >= -180.0);
assert!(expanded.max_lon <= 180.0);
}
#[test]
fn test_bbox_from_points() {
let points = [(1.0, 2.0), (3.0, 4.0), (2.0, 1.0)];
let bbox = BoundingBox::from_points(&points).expect("from_points");
assert!((bbox.min_lat - 1.0).abs() < 1e-10);
assert!((bbox.max_lat - 3.0).abs() < 1e-10);
assert!((bbox.min_lon - 1.0).abs() < 1e-10);
assert!((bbox.max_lon - 4.0).abs() < 1e-10);
}
#[test]
fn test_bbox_center() {
let bbox = BoundingBox::new(0.0, 10.0, 0.0, 20.0).expect("bbox");
let (lat, lon) = bbox.center();
assert!((lat - 5.0).abs() < 1e-10);
assert!((lon - 10.0).abs() < 1e-10);
}
#[test]
fn test_bbox_invalid() {
assert!(BoundingBox::new(10.0, 5.0, 0.0, 10.0).is_err());
assert!(BoundingBox::new(0.0, 10.0, 10.0, 5.0).is_err());
assert!(BoundingBox::new(-91.0, 10.0, 0.0, 10.0).is_err());
assert!(BoundingBox::new(0.0, 10.0, 0.0, 181.0).is_err());
}
#[test]
fn test_polygon_area_unit_degree_square() {
let square = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)];
let area = polygon_area(&square).expect("area");
let expected = 12_321_000_000.0;
assert!(
(area - expected).abs() / expected < 0.01,
"area={area}, expected≈{expected}"
);
}
#[test]
fn test_polygon_area_winding_independent() {
let cw = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
let ccw = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)];
let a1 = polygon_area(&cw).expect("area cw");
let a2 = polygon_area(&ccw).expect("area ccw");
assert!((a1 - a2).abs() < 1.0, "areas differ: {a1} vs {a2}");
}
#[test]
fn test_polygon_area_too_few_points() {
assert!(polygon_area(&[(0.0, 0.0), (1.0, 0.0)]).is_err());
}
#[test]
fn test_point_in_polygon_inside() {
let square = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
assert!(point_in_polygon(5.0, 5.0, &square));
}
#[test]
fn test_point_in_polygon_outside() {
let square = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
assert!(!point_in_polygon(11.0, 5.0, &square));
assert!(!point_in_polygon(5.0, 11.0, &square));
}
#[test]
fn test_point_in_polygon_corner() {
let square = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
assert!(point_in_polygon(0.0, 0.0, &square));
assert!(point_in_polygon(10.0, 10.0, &square));
}
#[test]
fn test_point_in_polygon_nonconvex() {
let poly = [
(0.0, 0.0),
(0.0, 6.0),
(3.0, 6.0),
(3.0, 3.0),
(6.0, 3.0),
(6.0, 0.0),
];
assert!(point_in_polygon(1.0, 1.0, &poly)); assert!(point_in_polygon(1.0, 5.0, &poly)); assert!(!point_in_polygon(4.0, 5.0, &poly)); }
}