1use super::coordinates::EARTH_RADIUS_M;
9use crate::error::{SpatialError, SpatialResult};
10use std::f64::consts::PI;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct BoundingBox {
29 pub min_lat: f64,
31 pub max_lat: f64,
33 pub min_lon: f64,
35 pub max_lon: f64,
37}
38
39impl BoundingBox {
40 pub fn new(min_lat: f64, max_lat: f64, min_lon: f64, max_lon: f64) -> SpatialResult<Self> {
56 if min_lat > max_lat {
57 return Err(SpatialError::ValueError(format!(
58 "min_lat ({min_lat}) > max_lat ({max_lat})"
59 )));
60 }
61 if min_lon > max_lon {
62 return Err(SpatialError::ValueError(format!(
63 "min_lon ({min_lon}) > max_lon ({max_lon})"
64 )));
65 }
66 if !(-90.0..=90.0).contains(&min_lat) || !(-90.0..=90.0).contains(&max_lat) {
67 return Err(SpatialError::ValueError(
68 "Latitude values must be in range [-90, 90]".to_string(),
69 ));
70 }
71 if !(-180.0..=180.0).contains(&min_lon) || !(-180.0..=180.0).contains(&max_lon) {
72 return Err(SpatialError::ValueError(
73 "Longitude values must be in range [-180, 180]".to_string(),
74 ));
75 }
76 Ok(BoundingBox {
77 min_lat,
78 max_lat,
79 min_lon,
80 max_lon,
81 })
82 }
83
84 pub fn from_points(points: &[(f64, f64)]) -> SpatialResult<Self> {
94 if points.is_empty() {
95 return Err(SpatialError::ValueError(
96 "Cannot create BoundingBox from empty point set".to_string(),
97 ));
98 }
99 let mut min_lat = f64::INFINITY;
100 let mut max_lat = f64::NEG_INFINITY;
101 let mut min_lon = f64::INFINITY;
102 let mut max_lon = f64::NEG_INFINITY;
103
104 for &(lat, lon) in points {
105 if lat < min_lat {
106 min_lat = lat;
107 }
108 if lat > max_lat {
109 max_lat = lat;
110 }
111 if lon < min_lon {
112 min_lon = lon;
113 }
114 if lon > max_lon {
115 max_lon = lon;
116 }
117 }
118 Ok(BoundingBox {
119 min_lat,
120 max_lat,
121 min_lon,
122 max_lon,
123 })
124 }
125
126 pub fn contains(&self, lat: f64, lon: f64) -> bool {
133 lat >= self.min_lat && lat <= self.max_lat && lon >= self.min_lon && lon <= self.max_lon
134 }
135
136 pub fn intersects(&self, other: &BoundingBox) -> bool {
138 self.min_lat <= other.max_lat
139 && self.max_lat >= other.min_lat
140 && self.min_lon <= other.max_lon
141 && self.max_lon >= other.min_lon
142 }
143
144 pub fn expand(&self, margin_deg: f64) -> SpatialResult<BoundingBox> {
156 if margin_deg < 0.0 {
157 return Err(SpatialError::ValueError(format!(
158 "margin_deg ({margin_deg}) must be non-negative"
159 )));
160 }
161 Ok(BoundingBox {
162 min_lat: (self.min_lat - margin_deg).max(-90.0),
163 max_lat: (self.max_lat + margin_deg).min(90.0),
164 min_lon: (self.min_lon - margin_deg).max(-180.0),
165 max_lon: (self.max_lon + margin_deg).min(180.0),
166 })
167 }
168
169 pub fn center(&self) -> (f64, f64) {
171 (
172 (self.min_lat + self.max_lat) / 2.0,
173 (self.min_lon + self.max_lon) / 2.0,
174 )
175 }
176
177 pub fn width_deg(&self) -> f64 {
179 self.max_lon - self.min_lon
180 }
181
182 pub fn height_deg(&self) -> f64 {
184 self.max_lat - self.min_lat
185 }
186
187 pub fn area_m2(&self) -> f64 {
191 let lat_center = (self.min_lat + self.max_lat) / 2.0;
192 let m_per_deg_lat = PI / 180.0 * EARTH_RADIUS_M;
193 let m_per_deg_lon = PI / 180.0 * EARTH_RADIUS_M * lat_center.to_radians().cos();
194 self.height_deg() * m_per_deg_lat * self.width_deg() * m_per_deg_lon
195 }
196}
197
198pub fn polygon_area(coords: &[(f64, f64)]) -> SpatialResult<f64> {
231 if coords.len() < 3 {
232 return Err(SpatialError::ValueError(
233 "Polygon must have at least 3 vertices".to_string(),
234 ));
235 }
236
237 let centroid_lat = coords.iter().map(|(lat, _)| lat).sum::<f64>() / coords.len() as f64;
239 let m_per_deg_lat = PI / 180.0 * EARTH_RADIUS_M;
240 let m_per_deg_lon = PI / 180.0 * EARTH_RADIUS_M * centroid_lat.to_radians().cos();
241
242 let n = coords.len();
244 let mut area2 = 0.0_f64;
245 for i in 0..n {
246 let j = (i + 1) % n;
247 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;
250 let yj = coords[j].0 * m_per_deg_lat;
251 area2 += xi * yj - xj * yi;
252 }
253
254 Ok(area2.abs() / 2.0)
255}
256
257pub fn point_in_polygon(lat: f64, lon: f64, polygon: &[(f64, f64)]) -> bool {
287 let n = polygon.len();
288 if n < 3 {
289 return false;
290 }
291
292 for &(vlat, vlon) in polygon {
294 if (vlat - lat).abs() < f64::EPSILON && (vlon - lon).abs() < f64::EPSILON {
295 return true;
296 }
297 }
298
299 let mut inside = false;
300 let mut j = n - 1;
301
302 for i in 0..n {
303 let (yi, xi) = polygon[i];
304 let (yj, xj) = polygon[j];
305
306 if is_on_segment(lat, lon, yi, xi, yj, xj) {
308 return true;
309 }
310
311 if ((yi > lat) != (yj > lat)) && (lon < (xj - xi) * (lat - yi) / (yj - yi) + xi) {
313 inside = !inside;
314 }
315 j = i;
316 }
317
318 inside
319}
320
321fn is_on_segment(py: f64, px: f64, y0: f64, x0: f64, y1: f64, x1: f64) -> bool {
323 let cross = (x1 - x0) * (py - y0) - (y1 - y0) * (px - x0);
325 if cross.abs() > 1e-10 {
326 return false;
327 }
328 let min_x = x0.min(x1);
330 let max_x = x0.max(x1);
331 let min_y = y0.min(y1);
332 let max_y = y0.max(y1);
333 px >= min_x && px <= max_x && py >= min_y && py <= max_y
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
343 fn test_bbox_contains() {
344 let bbox = BoundingBox::new(48.0, 52.0, -1.0, 3.0).expect("bbox");
345 assert!(bbox.contains(50.0, 1.0));
346 assert!(bbox.contains(48.0, -1.0)); assert!(!bbox.contains(53.0, 1.0));
348 assert!(!bbox.contains(50.0, 4.0));
349 }
350
351 #[test]
352 fn test_bbox_intersects() {
353 let a = BoundingBox::new(0.0, 2.0, 0.0, 2.0).expect("a");
354 let b = BoundingBox::new(1.0, 3.0, 1.0, 3.0).expect("b");
355 let c = BoundingBox::new(3.0, 5.0, 3.0, 5.0).expect("c");
356 assert!(a.intersects(&b));
357 assert!(b.intersects(&a));
358 assert!(!a.intersects(&c));
359 }
360
361 #[test]
362 fn test_bbox_expand() {
363 let bbox = BoundingBox::new(10.0, 20.0, 10.0, 20.0).expect("bbox");
364 let expanded = bbox.expand(5.0).expect("expand");
365 assert!((expanded.min_lat - 5.0).abs() < 1e-10);
366 assert!((expanded.max_lat - 25.0).abs() < 1e-10);
367 assert!((expanded.min_lon - 5.0).abs() < 1e-10);
368 assert!((expanded.max_lon - 25.0).abs() < 1e-10);
369 }
370
371 #[test]
372 fn test_bbox_expand_clamped() {
373 let bbox = BoundingBox::new(-89.0, 89.0, -179.0, 179.0).expect("bbox");
374 let expanded = bbox.expand(5.0).expect("expand");
375 assert!(expanded.min_lat >= -90.0);
376 assert!(expanded.max_lat <= 90.0);
377 assert!(expanded.min_lon >= -180.0);
378 assert!(expanded.max_lon <= 180.0);
379 }
380
381 #[test]
382 fn test_bbox_from_points() {
383 let points = [(1.0, 2.0), (3.0, 4.0), (2.0, 1.0)];
384 let bbox = BoundingBox::from_points(&points).expect("from_points");
385 assert!((bbox.min_lat - 1.0).abs() < 1e-10);
386 assert!((bbox.max_lat - 3.0).abs() < 1e-10);
387 assert!((bbox.min_lon - 1.0).abs() < 1e-10);
388 assert!((bbox.max_lon - 4.0).abs() < 1e-10);
389 }
390
391 #[test]
392 fn test_bbox_center() {
393 let bbox = BoundingBox::new(0.0, 10.0, 0.0, 20.0).expect("bbox");
394 let (lat, lon) = bbox.center();
395 assert!((lat - 5.0).abs() < 1e-10);
396 assert!((lon - 10.0).abs() < 1e-10);
397 }
398
399 #[test]
400 fn test_bbox_invalid() {
401 assert!(BoundingBox::new(10.0, 5.0, 0.0, 10.0).is_err());
402 assert!(BoundingBox::new(0.0, 10.0, 10.0, 5.0).is_err());
403 assert!(BoundingBox::new(-91.0, 10.0, 0.0, 10.0).is_err());
404 assert!(BoundingBox::new(0.0, 10.0, 0.0, 181.0).is_err());
405 }
406
407 #[test]
410 fn test_polygon_area_unit_degree_square() {
411 let square = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)];
413 let area = polygon_area(&square).expect("area");
414 let expected = 12_321_000_000.0;
416 assert!(
417 (area - expected).abs() / expected < 0.01,
418 "area={area}, expected≈{expected}"
419 );
420 }
421
422 #[test]
423 fn test_polygon_area_winding_independent() {
424 let cw = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
426 let ccw = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)];
427 let a1 = polygon_area(&cw).expect("area cw");
428 let a2 = polygon_area(&ccw).expect("area ccw");
429 assert!((a1 - a2).abs() < 1.0, "areas differ: {a1} vs {a2}");
430 }
431
432 #[test]
433 fn test_polygon_area_too_few_points() {
434 assert!(polygon_area(&[(0.0, 0.0), (1.0, 0.0)]).is_err());
435 }
436
437 #[test]
440 fn test_point_in_polygon_inside() {
441 let square = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
442 assert!(point_in_polygon(5.0, 5.0, &square));
443 }
444
445 #[test]
446 fn test_point_in_polygon_outside() {
447 let square = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
448 assert!(!point_in_polygon(11.0, 5.0, &square));
449 assert!(!point_in_polygon(5.0, 11.0, &square));
450 }
451
452 #[test]
453 fn test_point_in_polygon_corner() {
454 let square = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
455 assert!(point_in_polygon(0.0, 0.0, &square));
456 assert!(point_in_polygon(10.0, 10.0, &square));
457 }
458
459 #[test]
460 fn test_point_in_polygon_nonconvex() {
461 let poly = [
463 (0.0, 0.0),
464 (0.0, 6.0),
465 (3.0, 6.0),
466 (3.0, 3.0),
467 (6.0, 3.0),
468 (6.0, 0.0),
469 ];
470 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)); }
474}