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 { *min_lat = coord.lat; }
46 if coord.lat > *max_lat { *max_lat = coord.lat; }
47 if coord.lon < *min_lon { *min_lon = coord.lon; }
48 if coord.lon > *max_lon { *max_lon = coord.lon; }
49 }
50
51 fn visit_coords(
52 coords: &[GeoCoord],
53 min_lat: &mut f64,
54 max_lat: &mut f64,
55 min_lon: &mut f64,
56 max_lon: &mut f64,
57 ) {
58 for c in coords {
59 visit(c, min_lat, max_lat, min_lon, max_lon);
60 }
61 }
62
63 fn visit_geometry(
64 g: &Geometry,
65 min_lat: &mut f64,
66 max_lat: &mut f64,
67 min_lon: &mut f64,
68 max_lon: &mut f64,
69 ) {
70 match g {
71 Geometry::Point(p) => visit(&p.coord, min_lat, max_lat, min_lon, max_lon),
72 Geometry::LineString(ls) => visit_coords(&ls.coords, min_lat, max_lat, min_lon, max_lon),
73 Geometry::Polygon(p) => {
74 visit_coords(&p.exterior, min_lat, max_lat, min_lon, max_lon);
75 for hole in &p.interiors {
76 visit_coords(hole, min_lat, max_lat, min_lon, max_lon);
77 }
78 }
79 Geometry::MultiPoint(mp) => {
80 for p in &mp.points {
81 visit(&p.coord, min_lat, max_lat, min_lon, max_lon);
82 }
83 }
84 Geometry::MultiLineString(mls) => {
85 for ls in &mls.lines {
86 visit_coords(&ls.coords, min_lat, max_lat, min_lon, max_lon);
87 }
88 }
89 Geometry::MultiPolygon(mp) => {
90 for p in &mp.polygons {
91 visit_coords(&p.exterior, min_lat, max_lat, min_lon, max_lon);
92 }
93 }
94 Geometry::GeometryCollection(gc) => {
95 for g in gc {
96 visit_geometry(g, min_lat, max_lat, min_lon, max_lon);
97 }
98 }
99 }
100 }
101
102 visit_geometry(geometry, &mut min_lat, &mut max_lat, &mut min_lon, &mut max_lon);
103
104 if min_lat.is_infinite() {
105 None
106 } else {
107 Some(GeoBounds::from_coords(min_lon, min_lat, max_lon, max_lat))
108 }
109}
110
111pub fn ring_area(coords: &[GeoCoord]) -> f64 {
120 let n = coords.len();
121 if n < 3 {
122 return 0.0;
123 }
124
125 let mut area = 0.0;
131 for i in 0..n {
132 let j = if i == 0 { n - 1 } else { i - 1 };
133 let k = if i == n - 1 { 0 } else { i + 1 };
134 let lon_j = coords[j].lon.to_radians();
135 let lon_k = coords[k].lon.to_radians();
136 let lat_i = coords[i].lat.to_radians();
137 area += (lon_k - lon_j) * lat_i.sin();
138 }
139 area * EARTH_RADIUS * EARTH_RADIUS * 0.5
140}
141
142pub fn polygon_area(polygon: &Polygon) -> f64 {
146 let mut area = ring_area(&polygon.exterior).abs();
147 for hole in &polygon.interiors {
148 area -= ring_area(hole).abs();
149 }
150 area.abs()
151}
152
153pub(crate) fn haversine(a: &GeoCoord, b: &GeoCoord) -> f64 {
159 let dlat = (b.lat - a.lat).to_radians();
160 let dlon = (b.lon - a.lon).to_radians();
161 let lat1 = a.lat.to_radians();
162 let lat2 = b.lat.to_radians();
163 let h = (dlat * 0.5).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon * 0.5).sin().powi(2);
164 2.0 * EARTH_RADIUS * h.sqrt().asin()
165}
166
167pub fn linestring_length(coords: &[GeoCoord]) -> f64 {
169 if coords.len() < 2 {
170 return 0.0;
171 }
172 coords.windows(2).map(|w| haversine(&w[0], &w[1])).sum()
173}
174
175pub fn geometry_centroid(geometry: &Geometry) -> Option<GeoCoord> {
189 match geometry {
190 Geometry::Point(p) => Some(p.coord),
191 Geometry::LineString(ls) => linestring_centroid(&ls.coords),
192 Geometry::Polygon(p) => ring_centroid(&p.exterior),
193 Geometry::MultiPoint(mp) => {
194 if mp.points.is_empty() {
195 return None;
196 }
197 let (sum_lat, sum_lon) = mp.points.iter().fold((0.0, 0.0), |(lat, lon), p| {
198 (lat + p.coord.lat, lon + p.coord.lon)
199 });
200 let n = mp.points.len() as f64;
201 Some(GeoCoord::from_lat_lon(sum_lat / n, sum_lon / n))
202 }
203 Geometry::MultiLineString(mls) => {
204 let mut total_len = 0.0;
205 let mut wlat = 0.0;
206 let mut wlon = 0.0;
207 for ls in &mls.lines {
208 let len = linestring_length(&ls.coords);
209 if let Some(c) = linestring_centroid(&ls.coords) {
210 wlat += c.lat * len;
211 wlon += c.lon * len;
212 total_len += len;
213 }
214 }
215 if total_len == 0.0 {
216 return None;
217 }
218 Some(GeoCoord::from_lat_lon(wlat / total_len, wlon / total_len))
219 }
220 Geometry::MultiPolygon(mp) => {
221 let mut total_area = 0.0;
222 let mut wlat = 0.0;
223 let mut wlon = 0.0;
224 for poly in &mp.polygons {
225 let area = polygon_area(poly);
226 if let Some(c) = ring_centroid(&poly.exterior) {
227 wlat += c.lat * area;
228 wlon += c.lon * area;
229 total_area += area;
230 }
231 }
232 if total_area == 0.0 {
233 return None;
234 }
235 Some(GeoCoord::from_lat_lon(wlat / total_area, wlon / total_area))
236 }
237 Geometry::GeometryCollection(gc) => {
238 let centroids: Vec<GeoCoord> = gc.iter().filter_map(geometry_centroid).collect();
239 if centroids.is_empty() {
240 return None;
241 }
242 let n = centroids.len() as f64;
243 let (slat, slon) = centroids.iter().fold((0.0, 0.0), |(lat, lon), c| {
244 (lat + c.lat, lon + c.lon)
245 });
246 Some(GeoCoord::from_lat_lon(slat / n, slon / n))
247 }
248 }
249}
250
251fn linestring_centroid(coords: &[GeoCoord]) -> Option<GeoCoord> {
253 if coords.is_empty() {
254 return None;
255 }
256 if coords.len() == 1 {
257 return Some(coords[0]);
258 }
259 let mut total_len = 0.0;
260 let mut wlat = 0.0;
261 let mut wlon = 0.0;
262 for w in coords.windows(2) {
263 let len = haversine(&w[0], &w[1]);
264 let mid_lat = (w[0].lat + w[1].lat) * 0.5;
265 let mid_lon = (w[0].lon + w[1].lon) * 0.5;
266 wlat += mid_lat * len;
267 wlon += mid_lon * len;
268 total_len += len;
269 }
270 if total_len == 0.0 {
271 return Some(coords[0]);
272 }
273 Some(GeoCoord::from_lat_lon(wlat / total_len, wlon / total_len))
274}
275
276fn ring_centroid(coords: &[GeoCoord]) -> Option<GeoCoord> {
278 let n = coords.len();
279 if n < 3 {
280 return None;
281 }
282 let mut cx = 0.0;
283 let mut cy = 0.0;
284 let mut signed_area = 0.0;
285 for i in 0..n {
286 let j = (i + 1) % n;
287 let xi = coords[i].lon;
288 let yi = coords[i].lat;
289 let xj = coords[j].lon;
290 let yj = coords[j].lat;
291 let a = xi * yj - xj * yi;
292 signed_area += a;
293 cx += (xi + xj) * a;
294 cy += (yi + yj) * a;
295 }
296 if signed_area.abs() < 1e-20 {
297 return None;
298 }
299 signed_area *= 0.5;
300 cx /= 6.0 * signed_area;
301 cy /= 6.0 * signed_area;
302 Some(GeoCoord::from_lat_lon(cy, cx))
303}
304
305pub fn bearing(from: &GeoCoord, to: &GeoCoord) -> f64 {
313 let lat1 = from.lat.to_radians();
314 let lat2 = to.lat.to_radians();
315 let dlon = (to.lon - from.lon).to_radians();
316 let y = dlon.sin() * lat2.cos();
317 let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * dlon.cos();
318 (y.atan2(x).to_degrees() + 360.0) % 360.0
319}
320
321pub fn initial_bearing(from: &GeoCoord, to: &GeoCoord) -> f64 {
323 bearing(from, to)
324}
325
326pub fn interpolate_great_circle(from: &GeoCoord, to: &GeoCoord, fraction: f64) -> GeoCoord {
335 if fraction == 0.0 {
336 return *from;
337 }
338 if fraction == 1.0 {
339 return *to;
340 }
341 let lat1 = from.lat.to_radians();
342 let lon1 = from.lon.to_radians();
343 let lat2 = to.lat.to_radians();
344 let lon2 = to.lon.to_radians();
345
346 let d = {
347 let dlat = lat2 - lat1;
348 let dlon = lon2 - lon1;
349 let a = (dlat * 0.5).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon * 0.5).sin().powi(2);
350 2.0 * a.sqrt().asin()
351 };
352
353 if d.abs() < 1e-15 {
354 return *from;
355 }
356
357 let a = ((1.0 - fraction) * d).sin() / d.sin();
358 let b = (fraction * d).sin() / d.sin();
359
360 let x = a * lat1.cos() * lon1.cos() + b * lat2.cos() * lon2.cos();
361 let y = a * lat1.cos() * lon1.sin() + b * lat2.cos() * lon2.sin();
362 let z = a * lat1.sin() + b * lat2.sin();
363
364 let lat = z.atan2((x * x + y * y).sqrt()).to_degrees();
365 let lon = y.atan2(x).to_degrees();
366 GeoCoord::from_lat_lon(lat, lon)
367}
368
369impl Geometry {
374 pub fn bbox(&self) -> Option<GeoBounds> {
376 geometry_bbox(self)
377 }
378
379 pub fn centroid(&self) -> Option<GeoCoord> {
381 geometry_centroid(self)
382 }
383
384 pub fn area(&self) -> f64 {
388 match self {
389 Geometry::Polygon(p) => polygon_area(p),
390 Geometry::MultiPolygon(mp) => mp.polygons.iter().map(polygon_area).sum(),
391 _ => 0.0,
392 }
393 }
394
395 pub fn length(&self) -> f64 {
399 match self {
400 Geometry::LineString(ls) => linestring_length(&ls.coords),
401 Geometry::MultiLineString(mls) => {
402 mls.lines.iter().map(|ls| linestring_length(&ls.coords)).sum()
403 }
404 _ => 0.0,
405 }
406 }
407}
408
409#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::geometry::{LineString, MultiPoint, Point};
417
418 fn square_polygon() -> Polygon {
419 Polygon {
420 exterior: vec![
421 GeoCoord::from_lat_lon(0.0, 0.0),
422 GeoCoord::from_lat_lon(0.0, 1.0),
423 GeoCoord::from_lat_lon(1.0, 1.0),
424 GeoCoord::from_lat_lon(1.0, 0.0),
425 GeoCoord::from_lat_lon(0.0, 0.0),
426 ],
427 interiors: vec![],
428 }
429 }
430
431 #[test]
434 fn bbox_point() {
435 let g = Geometry::Point(Point {
436 coord: GeoCoord::from_lat_lon(10.0, 20.0),
437 });
438 let bb = g.bbox().unwrap();
439 assert_eq!(bb.south(), 10.0);
440 assert_eq!(bb.north(), 10.0);
441 assert_eq!(bb.west(), 20.0);
442 assert_eq!(bb.east(), 20.0);
443 }
444
445 #[test]
446 fn bbox_polygon() {
447 let g = Geometry::Polygon(square_polygon());
448 let bb = g.bbox().unwrap();
449 assert_eq!(bb.south(), 0.0);
450 assert_eq!(bb.north(), 1.0);
451 assert_eq!(bb.west(), 0.0);
452 assert_eq!(bb.east(), 1.0);
453 }
454
455 #[test]
456 fn bbox_empty_collection() {
457 let g = Geometry::GeometryCollection(vec![]);
458 assert!(g.bbox().is_none());
459 }
460
461 #[test]
464 fn centroid_point() {
465 let g = Geometry::Point(Point {
466 coord: GeoCoord::from_lat_lon(10.0, 20.0),
467 });
468 let c = g.centroid().unwrap();
469 assert_eq!(c.lat, 10.0);
470 assert_eq!(c.lon, 20.0);
471 }
472
473 #[test]
474 fn centroid_square_polygon() {
475 let g = Geometry::Polygon(square_polygon());
476 let c = g.centroid().unwrap();
477 assert!((c.lat - 0.5).abs() < 1e-10);
478 assert!((c.lon - 0.5).abs() < 1e-10);
479 }
480
481 #[test]
482 fn centroid_linestring() {
483 let g = Geometry::LineString(LineString {
484 coords: vec![
485 GeoCoord::from_lat_lon(0.0, 0.0),
486 GeoCoord::from_lat_lon(0.0, 2.0),
487 ],
488 });
489 let c = g.centroid().unwrap();
490 assert!((c.lat - 0.0).abs() < 1e-10);
491 assert!((c.lon - 1.0).abs() < 1e-10);
492 }
493
494 #[test]
495 fn centroid_multi_point() {
496 let g = Geometry::MultiPoint(MultiPoint {
497 points: vec![
498 Point { coord: GeoCoord::from_lat_lon(0.0, 0.0) },
499 Point { coord: GeoCoord::from_lat_lon(2.0, 2.0) },
500 ],
501 });
502 let c = g.centroid().unwrap();
503 assert!((c.lat - 1.0).abs() < 1e-10);
504 assert!((c.lon - 1.0).abs() < 1e-10);
505 }
506
507 #[test]
510 fn area_square_at_equator() {
511 let area = polygon_area(&square_polygon());
512 assert!(area > 1e10, "area should be > 1e10, got {area}");
514 assert!(area < 2e10, "area should be < 2e10, got {area}");
515 }
516
517 #[test]
518 fn area_point_is_zero() {
519 let g = Geometry::Point(Point {
520 coord: GeoCoord::from_lat_lon(0.0, 0.0),
521 });
522 assert_eq!(g.area(), 0.0);
523 }
524
525 #[test]
526 fn area_polygon_with_hole() {
527 let poly = Polygon {
528 exterior: vec![
529 GeoCoord::from_lat_lon(0.0, 0.0),
530 GeoCoord::from_lat_lon(0.0, 2.0),
531 GeoCoord::from_lat_lon(2.0, 2.0),
532 GeoCoord::from_lat_lon(2.0, 0.0),
533 GeoCoord::from_lat_lon(0.0, 0.0),
534 ],
535 interiors: vec![vec![
536 GeoCoord::from_lat_lon(0.5, 0.5),
537 GeoCoord::from_lat_lon(0.5, 1.5),
538 GeoCoord::from_lat_lon(1.5, 1.5),
539 GeoCoord::from_lat_lon(1.5, 0.5),
540 GeoCoord::from_lat_lon(0.5, 0.5),
541 ]],
542 };
543 let full = polygon_area(&Polygon {
544 exterior: poly.exterior.clone(),
545 interiors: vec![],
546 });
547 let with_hole = polygon_area(&poly);
548 assert!(with_hole < full, "area with hole should be less than full area");
549 let ratio = with_hole / full;
551 assert!((ratio - 0.75).abs() < 0.05, "ratio should be ~0.75, got {ratio}");
552 }
553
554 #[test]
557 fn length_equator_one_degree() {
558 let g = Geometry::LineString(LineString {
559 coords: vec![
560 GeoCoord::from_lat_lon(0.0, 0.0),
561 GeoCoord::from_lat_lon(0.0, 1.0),
562 ],
563 });
564 let len = g.length();
565 assert!(len > 110_000.0);
567 assert!(len < 112_000.0);
568 }
569
570 #[test]
571 fn length_point_is_zero() {
572 let g = Geometry::Point(Point {
573 coord: GeoCoord::from_lat_lon(0.0, 0.0),
574 });
575 assert_eq!(g.length(), 0.0);
576 }
577
578 #[test]
581 fn bearing_due_north() {
582 let from = GeoCoord::from_lat_lon(0.0, 0.0);
583 let to = GeoCoord::from_lat_lon(1.0, 0.0);
584 let b = bearing(&from, &to);
585 assert!((b - 0.0).abs() < 0.01 || (b - 360.0).abs() < 0.01);
586 }
587
588 #[test]
589 fn bearing_due_east() {
590 let from = GeoCoord::from_lat_lon(0.0, 0.0);
591 let to = GeoCoord::from_lat_lon(0.0, 1.0);
592 let b = bearing(&from, &to);
593 assert!((b - 90.0).abs() < 0.01, "expected ~90°, got {b}°");
594 }
595
596 #[test]
599 fn interpolate_endpoints() {
600 let a = GeoCoord::from_lat_lon(0.0, 0.0);
601 let b = GeoCoord::from_lat_lon(10.0, 10.0);
602 let start = interpolate_great_circle(&a, &b, 0.0);
603 let end = interpolate_great_circle(&a, &b, 1.0);
604 assert_eq!(start.lat, a.lat);
605 assert_eq!(start.lon, a.lon);
606 assert_eq!(end.lat, b.lat);
607 assert_eq!(end.lon, b.lon);
608 }
609
610 #[test]
611 fn interpolate_midpoint_on_equator() {
612 let a = GeoCoord::from_lat_lon(0.0, 0.0);
613 let b = GeoCoord::from_lat_lon(0.0, 10.0);
614 let mid = interpolate_great_circle(&a, &b, 0.5);
615 assert!((mid.lat - 0.0).abs() < 0.01);
616 assert!((mid.lon - 5.0).abs() < 0.01);
617 }
618}