1use crate::geometry::{Geometry, Polygon};
20use rustial_math::{GeoBounds, GeoCoord};
21
22const EARTH_RADIUS: f64 = 6_378_137.0;
24
25pub fn geometry_bbox(geometry: &Geometry) -> Option<GeoBounds> {
33 let mut min_lat = f64::INFINITY;
34 let mut max_lat = f64::NEG_INFINITY;
35 let mut min_lon = f64::INFINITY;
36 let mut max_lon = f64::NEG_INFINITY;
37
38 fn visit(
39 coord: &GeoCoord,
40 min_lat: &mut f64,
41 max_lat: &mut f64,
42 min_lon: &mut f64,
43 max_lon: &mut f64,
44 ) {
45 if coord.lat < *min_lat {
46 *min_lat = coord.lat;
47 }
48 if coord.lat > *max_lat {
49 *max_lat = coord.lat;
50 }
51 if coord.lon < *min_lon {
52 *min_lon = coord.lon;
53 }
54 if coord.lon > *max_lon {
55 *max_lon = coord.lon;
56 }
57 }
58
59 fn visit_coords(
60 coords: &[GeoCoord],
61 min_lat: &mut f64,
62 max_lat: &mut f64,
63 min_lon: &mut f64,
64 max_lon: &mut f64,
65 ) {
66 for c in coords {
67 visit(c, min_lat, max_lat, min_lon, max_lon);
68 }
69 }
70
71 fn visit_geometry(
72 g: &Geometry,
73 min_lat: &mut f64,
74 max_lat: &mut f64,
75 min_lon: &mut f64,
76 max_lon: &mut f64,
77 ) {
78 match g {
79 Geometry::Point(p) => visit(&p.coord, min_lat, max_lat, min_lon, max_lon),
80 Geometry::LineString(ls) => {
81 visit_coords(&ls.coords, min_lat, max_lat, min_lon, max_lon)
82 }
83 Geometry::Polygon(p) => {
84 visit_coords(&p.exterior, min_lat, max_lat, min_lon, max_lon);
85 for hole in &p.interiors {
86 visit_coords(hole, min_lat, max_lat, min_lon, max_lon);
87 }
88 }
89 Geometry::MultiPoint(mp) => {
90 for p in &mp.points {
91 visit(&p.coord, min_lat, max_lat, min_lon, max_lon);
92 }
93 }
94 Geometry::MultiLineString(mls) => {
95 for ls in &mls.lines {
96 visit_coords(&ls.coords, min_lat, max_lat, min_lon, max_lon);
97 }
98 }
99 Geometry::MultiPolygon(mp) => {
100 for p in &mp.polygons {
101 visit_coords(&p.exterior, min_lat, max_lat, min_lon, max_lon);
102 }
103 }
104 Geometry::GeometryCollection(gc) => {
105 for g in gc {
106 visit_geometry(g, min_lat, max_lat, min_lon, max_lon);
107 }
108 }
109 }
110 }
111
112 visit_geometry(
113 geometry,
114 &mut min_lat,
115 &mut max_lat,
116 &mut min_lon,
117 &mut max_lon,
118 );
119
120 if min_lat.is_infinite() {
121 None
122 } else {
123 Some(GeoBounds::from_coords(min_lon, min_lat, max_lon, max_lat))
124 }
125}
126
127pub fn ring_area(coords: &[GeoCoord]) -> f64 {
136 let n = coords.len();
137 if n < 3 {
138 return 0.0;
139 }
140
141 let mut area = 0.0;
147 for i in 0..n {
148 let j = if i == 0 { n - 1 } else { i - 1 };
149 let k = if i == n - 1 { 0 } else { i + 1 };
150 let lon_j = coords[j].lon.to_radians();
151 let lon_k = coords[k].lon.to_radians();
152 let lat_i = coords[i].lat.to_radians();
153 area += (lon_k - lon_j) * lat_i.sin();
154 }
155 area * EARTH_RADIUS * EARTH_RADIUS * 0.5
156}
157
158pub fn polygon_area(polygon: &Polygon) -> f64 {
162 let mut area = ring_area(&polygon.exterior).abs();
163 for hole in &polygon.interiors {
164 area -= ring_area(hole).abs();
165 }
166 area.abs()
167}
168
169pub(crate) fn haversine(a: &GeoCoord, b: &GeoCoord) -> f64 {
175 let dlat = (b.lat - a.lat).to_radians();
176 let dlon = (b.lon - a.lon).to_radians();
177 let lat1 = a.lat.to_radians();
178 let lat2 = b.lat.to_radians();
179 let h = (dlat * 0.5).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon * 0.5).sin().powi(2);
180 2.0 * EARTH_RADIUS * h.sqrt().asin()
181}
182
183pub fn linestring_length(coords: &[GeoCoord]) -> f64 {
185 if coords.len() < 2 {
186 return 0.0;
187 }
188 coords.windows(2).map(|w| haversine(&w[0], &w[1])).sum()
189}
190
191pub fn geometry_centroid(geometry: &Geometry) -> Option<GeoCoord> {
205 match geometry {
206 Geometry::Point(p) => Some(p.coord),
207 Geometry::LineString(ls) => linestring_centroid(&ls.coords),
208 Geometry::Polygon(p) => ring_centroid(&p.exterior),
209 Geometry::MultiPoint(mp) => {
210 if mp.points.is_empty() {
211 return None;
212 }
213 let (sum_lat, sum_lon) = mp.points.iter().fold((0.0, 0.0), |(lat, lon), p| {
214 (lat + p.coord.lat, lon + p.coord.lon)
215 });
216 let n = mp.points.len() as f64;
217 Some(GeoCoord::from_lat_lon(sum_lat / n, sum_lon / n))
218 }
219 Geometry::MultiLineString(mls) => {
220 let mut total_len = 0.0;
221 let mut wlat = 0.0;
222 let mut wlon = 0.0;
223 for ls in &mls.lines {
224 let len = linestring_length(&ls.coords);
225 if let Some(c) = linestring_centroid(&ls.coords) {
226 wlat += c.lat * len;
227 wlon += c.lon * len;
228 total_len += len;
229 }
230 }
231 if total_len == 0.0 {
232 return None;
233 }
234 Some(GeoCoord::from_lat_lon(wlat / total_len, wlon / total_len))
235 }
236 Geometry::MultiPolygon(mp) => {
237 let mut total_area = 0.0;
238 let mut wlat = 0.0;
239 let mut wlon = 0.0;
240 for poly in &mp.polygons {
241 let area = polygon_area(poly);
242 if let Some(c) = ring_centroid(&poly.exterior) {
243 wlat += c.lat * area;
244 wlon += c.lon * area;
245 total_area += area;
246 }
247 }
248 if total_area == 0.0 {
249 return None;
250 }
251 Some(GeoCoord::from_lat_lon(wlat / total_area, wlon / total_area))
252 }
253 Geometry::GeometryCollection(gc) => {
254 let centroids: Vec<GeoCoord> = gc.iter().filter_map(geometry_centroid).collect();
255 if centroids.is_empty() {
256 return None;
257 }
258 let n = centroids.len() as f64;
259 let (slat, slon) = centroids
260 .iter()
261 .fold((0.0, 0.0), |(lat, lon), c| (lat + c.lat, lon + c.lon));
262 Some(GeoCoord::from_lat_lon(slat / n, slon / n))
263 }
264 }
265}
266
267fn linestring_centroid(coords: &[GeoCoord]) -> Option<GeoCoord> {
269 if coords.is_empty() {
270 return None;
271 }
272 if coords.len() == 1 {
273 return Some(coords[0]);
274 }
275 let mut total_len = 0.0;
276 let mut wlat = 0.0;
277 let mut wlon = 0.0;
278 for w in coords.windows(2) {
279 let len = haversine(&w[0], &w[1]);
280 let mid_lat = (w[0].lat + w[1].lat) * 0.5;
281 let mid_lon = (w[0].lon + w[1].lon) * 0.5;
282 wlat += mid_lat * len;
283 wlon += mid_lon * len;
284 total_len += len;
285 }
286 if total_len == 0.0 {
287 return Some(coords[0]);
288 }
289 Some(GeoCoord::from_lat_lon(wlat / total_len, wlon / total_len))
290}
291
292fn ring_centroid(coords: &[GeoCoord]) -> Option<GeoCoord> {
294 let n = coords.len();
295 if n < 3 {
296 return None;
297 }
298 let mut cx = 0.0;
299 let mut cy = 0.0;
300 let mut signed_area = 0.0;
301 for i in 0..n {
302 let j = (i + 1) % n;
303 let xi = coords[i].lon;
304 let yi = coords[i].lat;
305 let xj = coords[j].lon;
306 let yj = coords[j].lat;
307 let a = xi * yj - xj * yi;
308 signed_area += a;
309 cx += (xi + xj) * a;
310 cy += (yi + yj) * a;
311 }
312 if signed_area.abs() < 1e-20 {
313 return None;
314 }
315 signed_area *= 0.5;
316 cx /= 6.0 * signed_area;
317 cy /= 6.0 * signed_area;
318 Some(GeoCoord::from_lat_lon(cy, cx))
319}
320
321pub fn bearing(from: &GeoCoord, to: &GeoCoord) -> f64 {
329 let lat1 = from.lat.to_radians();
330 let lat2 = to.lat.to_radians();
331 let dlon = (to.lon - from.lon).to_radians();
332 let y = dlon.sin() * lat2.cos();
333 let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * dlon.cos();
334 (y.atan2(x).to_degrees() + 360.0) % 360.0
335}
336
337pub fn initial_bearing(from: &GeoCoord, to: &GeoCoord) -> f64 {
339 bearing(from, to)
340}
341
342pub fn interpolate_great_circle(from: &GeoCoord, to: &GeoCoord, fraction: f64) -> GeoCoord {
351 if fraction == 0.0 {
352 return *from;
353 }
354 if fraction == 1.0 {
355 return *to;
356 }
357 let lat1 = from.lat.to_radians();
358 let lon1 = from.lon.to_radians();
359 let lat2 = to.lat.to_radians();
360 let lon2 = to.lon.to_radians();
361
362 let d = {
363 let dlat = lat2 - lat1;
364 let dlon = lon2 - lon1;
365 let a = (dlat * 0.5).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon * 0.5).sin().powi(2);
366 2.0 * a.sqrt().asin()
367 };
368
369 if d.abs() < 1e-15 {
370 return *from;
371 }
372
373 let a = ((1.0 - fraction) * d).sin() / d.sin();
374 let b = (fraction * d).sin() / d.sin();
375
376 let x = a * lat1.cos() * lon1.cos() + b * lat2.cos() * lon2.cos();
377 let y = a * lat1.cos() * lon1.sin() + b * lat2.cos() * lon2.sin();
378 let z = a * lat1.sin() + b * lat2.sin();
379
380 let lat = z.atan2((x * x + y * y).sqrt()).to_degrees();
381 let lon = y.atan2(x).to_degrees();
382 GeoCoord::from_lat_lon(lat, lon)
383}
384
385impl Geometry {
390 pub fn bbox(&self) -> Option<GeoBounds> {
392 geometry_bbox(self)
393 }
394
395 pub fn centroid(&self) -> Option<GeoCoord> {
397 geometry_centroid(self)
398 }
399
400 pub fn area(&self) -> f64 {
404 match self {
405 Geometry::Polygon(p) => polygon_area(p),
406 Geometry::MultiPolygon(mp) => mp.polygons.iter().map(polygon_area).sum(),
407 _ => 0.0,
408 }
409 }
410
411 pub fn length(&self) -> f64 {
415 match self {
416 Geometry::LineString(ls) => linestring_length(&ls.coords),
417 Geometry::MultiLineString(mls) => mls
418 .lines
419 .iter()
420 .map(|ls| linestring_length(&ls.coords))
421 .sum(),
422 _ => 0.0,
423 }
424 }
425}
426
427#[cfg(test)]
432mod tests {
433 use super::*;
434 use crate::geometry::{LineString, MultiPoint, Point};
435
436 fn square_polygon() -> Polygon {
437 Polygon {
438 exterior: vec![
439 GeoCoord::from_lat_lon(0.0, 0.0),
440 GeoCoord::from_lat_lon(0.0, 1.0),
441 GeoCoord::from_lat_lon(1.0, 1.0),
442 GeoCoord::from_lat_lon(1.0, 0.0),
443 GeoCoord::from_lat_lon(0.0, 0.0),
444 ],
445 interiors: vec![],
446 }
447 }
448
449 #[test]
452 fn bbox_point() {
453 let g = Geometry::Point(Point {
454 coord: GeoCoord::from_lat_lon(10.0, 20.0),
455 });
456 let bb = g.bbox().unwrap();
457 assert_eq!(bb.south(), 10.0);
458 assert_eq!(bb.north(), 10.0);
459 assert_eq!(bb.west(), 20.0);
460 assert_eq!(bb.east(), 20.0);
461 }
462
463 #[test]
464 fn bbox_polygon() {
465 let g = Geometry::Polygon(square_polygon());
466 let bb = g.bbox().unwrap();
467 assert_eq!(bb.south(), 0.0);
468 assert_eq!(bb.north(), 1.0);
469 assert_eq!(bb.west(), 0.0);
470 assert_eq!(bb.east(), 1.0);
471 }
472
473 #[test]
474 fn bbox_empty_collection() {
475 let g = Geometry::GeometryCollection(vec![]);
476 assert!(g.bbox().is_none());
477 }
478
479 #[test]
482 fn centroid_point() {
483 let g = Geometry::Point(Point {
484 coord: GeoCoord::from_lat_lon(10.0, 20.0),
485 });
486 let c = g.centroid().unwrap();
487 assert_eq!(c.lat, 10.0);
488 assert_eq!(c.lon, 20.0);
489 }
490
491 #[test]
492 fn centroid_square_polygon() {
493 let g = Geometry::Polygon(square_polygon());
494 let c = g.centroid().unwrap();
495 assert!((c.lat - 0.5).abs() < 1e-10);
496 assert!((c.lon - 0.5).abs() < 1e-10);
497 }
498
499 #[test]
500 fn centroid_linestring() {
501 let g = Geometry::LineString(LineString {
502 coords: vec![
503 GeoCoord::from_lat_lon(0.0, 0.0),
504 GeoCoord::from_lat_lon(0.0, 2.0),
505 ],
506 });
507 let c = g.centroid().unwrap();
508 assert!((c.lat - 0.0).abs() < 1e-10);
509 assert!((c.lon - 1.0).abs() < 1e-10);
510 }
511
512 #[test]
513 fn centroid_multi_point() {
514 let g = Geometry::MultiPoint(MultiPoint {
515 points: vec![
516 Point {
517 coord: GeoCoord::from_lat_lon(0.0, 0.0),
518 },
519 Point {
520 coord: GeoCoord::from_lat_lon(2.0, 2.0),
521 },
522 ],
523 });
524 let c = g.centroid().unwrap();
525 assert!((c.lat - 1.0).abs() < 1e-10);
526 assert!((c.lon - 1.0).abs() < 1e-10);
527 }
528
529 #[test]
532 fn area_square_at_equator() {
533 let area = polygon_area(&square_polygon());
534 assert!(area > 1e10, "area should be > 1e10, got {area}");
536 assert!(area < 2e10, "area should be < 2e10, got {area}");
537 }
538
539 #[test]
540 fn area_point_is_zero() {
541 let g = Geometry::Point(Point {
542 coord: GeoCoord::from_lat_lon(0.0, 0.0),
543 });
544 assert_eq!(g.area(), 0.0);
545 }
546
547 #[test]
548 fn area_polygon_with_hole() {
549 let poly = Polygon {
550 exterior: vec![
551 GeoCoord::from_lat_lon(0.0, 0.0),
552 GeoCoord::from_lat_lon(0.0, 2.0),
553 GeoCoord::from_lat_lon(2.0, 2.0),
554 GeoCoord::from_lat_lon(2.0, 0.0),
555 GeoCoord::from_lat_lon(0.0, 0.0),
556 ],
557 interiors: vec![vec![
558 GeoCoord::from_lat_lon(0.5, 0.5),
559 GeoCoord::from_lat_lon(0.5, 1.5),
560 GeoCoord::from_lat_lon(1.5, 1.5),
561 GeoCoord::from_lat_lon(1.5, 0.5),
562 GeoCoord::from_lat_lon(0.5, 0.5),
563 ]],
564 };
565 let full = polygon_area(&Polygon {
566 exterior: poly.exterior.clone(),
567 interiors: vec![],
568 });
569 let with_hole = polygon_area(&poly);
570 assert!(
571 with_hole < full,
572 "area with hole should be less than full area"
573 );
574 let ratio = with_hole / full;
576 assert!(
577 (ratio - 0.75).abs() < 0.05,
578 "ratio should be ~0.75, got {ratio}"
579 );
580 }
581
582 #[test]
585 fn length_equator_one_degree() {
586 let g = Geometry::LineString(LineString {
587 coords: vec![
588 GeoCoord::from_lat_lon(0.0, 0.0),
589 GeoCoord::from_lat_lon(0.0, 1.0),
590 ],
591 });
592 let len = g.length();
593 assert!(len > 110_000.0);
595 assert!(len < 112_000.0);
596 }
597
598 #[test]
599 fn length_point_is_zero() {
600 let g = Geometry::Point(Point {
601 coord: GeoCoord::from_lat_lon(0.0, 0.0),
602 });
603 assert_eq!(g.length(), 0.0);
604 }
605
606 #[test]
609 fn bearing_due_north() {
610 let from = GeoCoord::from_lat_lon(0.0, 0.0);
611 let to = GeoCoord::from_lat_lon(1.0, 0.0);
612 let b = bearing(&from, &to);
613 assert!((b - 0.0).abs() < 0.01 || (b - 360.0).abs() < 0.01);
614 }
615
616 #[test]
617 fn bearing_due_east() {
618 let from = GeoCoord::from_lat_lon(0.0, 0.0);
619 let to = GeoCoord::from_lat_lon(0.0, 1.0);
620 let b = bearing(&from, &to);
621 assert!((b - 90.0).abs() < 0.01, "expected ~90°, got {b}°");
622 }
623
624 #[test]
627 fn interpolate_endpoints() {
628 let a = GeoCoord::from_lat_lon(0.0, 0.0);
629 let b = GeoCoord::from_lat_lon(10.0, 10.0);
630 let start = interpolate_great_circle(&a, &b, 0.0);
631 let end = interpolate_great_circle(&a, &b, 1.0);
632 assert_eq!(start.lat, a.lat);
633 assert_eq!(start.lon, a.lon);
634 assert_eq!(end.lat, b.lat);
635 assert_eq!(end.lon, b.lon);
636 }
637
638 #[test]
639 fn interpolate_midpoint_on_equator() {
640 let a = GeoCoord::from_lat_lon(0.0, 0.0);
641 let b = GeoCoord::from_lat_lon(0.0, 10.0);
642 let mid = interpolate_great_circle(&a, &b, 0.5);
643 assert!((mid.lat - 0.0).abs() < 0.01);
644 assert!((mid.lon - 5.0).abs() < 0.01);
645 }
646}