1use crate::error::{SpatialError, SpatialResult};
39use std::f64::consts::PI;
40
41pub const EARTH_RADIUS_M: f64 = 6_371_008.8;
43
44pub const EARTH_RADIUS_KM: f64 = EARTH_RADIUS_M / 1000.0;
46
47pub const EARTH_EQUATORIAL_RADIUS_M: f64 = 6_378_137.0;
49
50pub const EARTH_POLAR_RADIUS_M: f64 = 6_356_752.314245;
52
53pub const EARTH_FLATTENING: f64 = 1.0 / 298.257223563;
55
56pub const EARTH_ECCENTRICITY_SQ: f64 = 2.0 * EARTH_FLATTENING - EARTH_FLATTENING * EARTH_FLATTENING;
58
59#[allow(dead_code)]
78pub fn deg_to_rad(degrees: f64) -> f64 {
79 degrees * PI / 180.0
80}
81
82#[allow(dead_code)]
101pub fn rad_to_deg(radians: f64) -> f64 {
102 radians * 180.0 / PI
103}
104
105#[allow(dead_code)]
124pub fn normalize_angle(angle: f64) -> f64 {
125 let normalized = angle % (2.0 * PI);
126 if normalized < 0.0 {
127 normalized + 2.0 * PI
128 } else {
129 normalized
130 }
131}
132
133#[allow(dead_code)]
152pub fn normalize_bearing(_bearingdeg: f64) -> f64 {
153 let normalized = _bearingdeg % 360.0;
154 if normalized < 0.0 {
155 normalized + 360.0
156 } else {
157 normalized
158 }
159}
160
161#[allow(dead_code)]
187pub fn haversine_distance(point1: (f64, f64), point2: (f64, f64)) -> f64 {
188 let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
189 let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
190
191 let dlat = lat2 - lat1;
192 let dlon = lon2 - lon1;
193
194 let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
195 let c = 2.0 * a.sqrt().asin();
196
197 EARTH_RADIUS_M * c
198}
199
200#[allow(dead_code)]
223pub fn initial_bearing(point1: (f64, f64), point2: (f64, f64)) -> f64 {
224 let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
225 let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
226
227 let dlon = lon2 - lon1;
228
229 let y = dlon.sin() * lat2.cos();
230 let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * dlon.cos();
231
232 normalize_angle(y.atan2(x))
233}
234
235#[allow(dead_code)]
246pub fn final_bearing(point1: (f64, f64), point2: (f64, f64)) -> f64 {
247 let reverse_bearing = initial_bearing(point2, point1);
248 normalize_angle(reverse_bearing + PI)
249}
250
251#[allow(dead_code)]
276pub fn destination_point(start: (f64, f64), distance: f64, bearing: f64) -> (f64, f64) {
277 let (lat1, lon1) = (deg_to_rad(start.0), deg_to_rad(start.1));
278
279 let angular_distance = distance / EARTH_RADIUS_M;
280
281 let lat2 = (lat1.sin() * angular_distance.cos()
282 + lat1.cos() * angular_distance.sin() * bearing.cos())
283 .asin();
284
285 let lon2 = lon1
286 + (bearing.sin() * angular_distance.sin() * lat1.cos())
287 .atan2(angular_distance.cos() - lat1.sin() * lat2.sin());
288
289 (rad_to_deg(lat2), rad_to_deg(lon2))
290}
291
292#[allow(dead_code)]
303pub fn midpoint(point1: (f64, f64), point2: (f64, f64)) -> (f64, f64) {
304 let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
305 let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
306
307 let dlon = lon2 - lon1;
308
309 let bx = lat2.cos() * dlon.cos();
310 let by = lat2.cos() * dlon.sin();
311
312 let lat_mid = (lat1.sin() + lat2.sin()).atan2(((lat1.cos() + bx).powi(2) + by.powi(2)).sqrt());
313
314 let lon_mid = lon1 + by.atan2(lat1.cos() + bx);
315
316 (rad_to_deg(lat_mid), rad_to_deg(lon_mid))
317}
318
319#[allow(dead_code)]
331pub fn cross_track_distance(
332 point: (f64, f64),
333 path_start: (f64, f64),
334 path_end: (f64, f64),
335) -> f64 {
336 let distance_to_start = haversine_distance(path_start, point) / EARTH_RADIUS_M;
337 let bearing_to_point = initial_bearing(path_start, point);
338 let bearing_to_end = initial_bearing(path_start, path_end);
339
340 let cross_track_angular =
341 (distance_to_start.sin() * (bearing_to_point - bearing_to_end).sin()).asin();
342
343 EARTH_RADIUS_M * cross_track_angular
344}
345
346#[allow(dead_code)]
358pub fn along_track_distance(
359 point: (f64, f64),
360 path_start: (f64, f64),
361 path_end: (f64, f64),
362) -> f64 {
363 let distance_to_start = haversine_distance(path_start, point) / EARTH_RADIUS_M;
364 let cross_track_angular = cross_track_distance(point, path_start, path_end) / EARTH_RADIUS_M;
365
366 let along_track_angular = (distance_to_start.powi(2) - cross_track_angular.powi(2))
367 .sqrt()
368 .acos();
369
370 EARTH_RADIUS_M * along_track_angular
371}
372
373#[allow(dead_code)]
388pub fn spherical_polygon_area(polygon: &[(f64, f64)]) -> SpatialResult<f64> {
389 if polygon.len() < 3 {
390 return Err(SpatialError::ValueError(
391 "Polygon must have at least 3 vertices".to_string(),
392 ));
393 }
394
395 let n = polygon.len();
396 let mut sum = 0.0;
397
398 for i in 0..n {
399 let j = (i + 1) % n;
400 let (lat1, lon1) = (deg_to_rad(polygon[i].0), deg_to_rad(polygon[i].1));
401 let (lat2, lon2) = (deg_to_rad(polygon[j].0), deg_to_rad(polygon[j].1));
402
403 sum += (lon2 - lon1) * (2.0 + lat1.sin() + lat2.sin());
404 }
405
406 let area = (sum.abs() / 2.0) * EARTH_RADIUS_M * EARTH_RADIUS_M;
407 Ok(area)
408}
409
410#[allow(dead_code)]
421pub fn point_in_spherical_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
422 if polygon.len() < 3 {
423 return false;
424 }
425
426 let max_extent = polygon
428 .iter()
429 .flat_map(|(lat, lon)| [lat.abs(), lon.abs()])
430 .fold(0.0, f64::max);
431
432 if max_extent < 10.0 {
433 let (x, y) = point;
435 let mut inside = false;
436 let n = polygon.len();
437
438 for i in 0..n {
439 let j = (i + 1) % n;
440 let (xi, yi) = polygon[i];
441 let (xj, yj) = polygon[j];
442
443 if ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
444 inside = !inside;
445 }
446 }
447 return inside;
448 }
449
450 let (test_lat, test_lon) = (deg_to_rad(point.0), deg_to_rad(point.1));
452 let mut angle_sum = 0.0;
453
454 for i in 0..polygon.len() {
455 let j = (i + 1) % polygon.len();
456 let (lat1, lon1) = (deg_to_rad(polygon[i].0), deg_to_rad(polygon[i].1));
457 let (lat2, lon2) = (deg_to_rad(polygon[j].0), deg_to_rad(polygon[j].1));
458
459 let x1 = lat1.cos() * lon1.cos();
461 let y1 = lat1.cos() * lon1.sin();
462 let z1 = lat1.sin();
463
464 let x2 = lat2.cos() * lon2.cos();
465 let y2 = lat2.cos() * lon2.sin();
466 let z2 = lat2.sin();
467
468 let xt = test_lat.cos() * test_lon.cos();
469 let yt = test_lat.cos() * test_lon.sin();
470 let zt = test_lat.sin();
471
472 let v1x = x1 - xt;
474 let v1y = y1 - yt;
475 let v1z = z1 - zt;
476
477 let v2x = x2 - xt;
478 let v2y = y2 - yt;
479 let v2z = z2 - zt;
480
481 let v1_len = (v1x * v1x + v1y * v1y + v1z * v1z).sqrt();
483 let v2_len = (v2x * v2x + v2y * v2y + v2z * v2z).sqrt();
484
485 if v1_len < 1e-10 || v2_len < 1e-10 {
486 continue; }
488
489 let v1x_norm = v1x / v1_len;
490 let v1y_norm = v1y / v1_len;
491 let v1z_norm = v1z / v1_len;
492
493 let v2x_norm = v2x / v2_len;
494 let v2y_norm = v2y / v2_len;
495 let v2z_norm = v2z / v2_len;
496
497 let dot = v1x_norm * v2x_norm + v1y_norm * v2y_norm + v1z_norm * v2z_norm;
499 let dot = dot.clamp(-1.0, 1.0); let cross_x = v1y_norm * v2z_norm - v1z_norm * v2y_norm;
503 let cross_y = v1z_norm * v2x_norm - v1x_norm * v2z_norm;
504 let cross_z = v1x_norm * v2y_norm - v1y_norm * v2x_norm;
505
506 let normal_dot = cross_x * xt + cross_y * yt + cross_z * zt;
508 let angle = dot.acos();
509
510 if normal_dot < 0.0 {
511 angle_sum -= angle;
512 } else {
513 angle_sum += angle;
514 }
515 }
516
517 (angle_sum.abs() / (2.0 * PI)) > 0.5
518}
519
520#[allow(dead_code)]
536pub fn geographic_to_utm(lat: f64, lon: f64) -> SpatialResult<(f64, f64, i32, char)> {
537 if !(-80.0..=84.0).contains(&lat) {
538 return Err(SpatialError::ValueError(
539 "Latitude must be between -80° and 84° for UTM".to_string(),
540 ));
541 }
542
543 let zone_number = ((lon + 180.0) / 6.0).floor() as i32 + 1;
544 let zone_letter = utm_zone_letter(lat)?;
545
546 let lat_rad = deg_to_rad(lat);
547 let lon_rad = deg_to_rad(lon);
548 let central_meridian = deg_to_rad(((zone_number - 1) * 6 - 177) as f64);
549
550 let k0 = 0.9996; let a = EARTH_EQUATORIAL_RADIUS_M;
552 let e_sq = EARTH_ECCENTRICITY_SQ;
553
554 let n = a / (1.0 - e_sq * lat_rad.sin().powi(2)).sqrt();
555 let t = lat_rad.tan().powi(2);
556 let c = EARTH_ECCENTRICITY_SQ * lat_rad.cos().powi(2) / (1.0 - EARTH_ECCENTRICITY_SQ);
557 let a_coeff = lat_rad.cos() * (lon_rad - central_meridian);
558
559 let m = a
560 * ((1.0 - e_sq / 4.0 - 3.0 * e_sq.powi(2) / 64.0 - 5.0 * e_sq.powi(3) / 256.0) * lat_rad
561 - (3.0 * e_sq / 8.0 + 3.0 * e_sq.powi(2) / 32.0 + 45.0 * e_sq.powi(3) / 1024.0)
562 * (2.0 * lat_rad).sin()
563 + (15.0 * e_sq.powi(2) / 256.0 + 45.0 * e_sq.powi(3) / 1024.0) * (4.0 * lat_rad).sin()
564 - (35.0 * e_sq.powi(3) / 3072.0) * (6.0 * lat_rad).sin());
565
566 let easting = k0
567 * n
568 * (a_coeff
569 + (1.0 - t + c) * a_coeff.powi(3) / 6.0
570 + (5.0 - 18.0 * t + t.powi(2) + 72.0 * c - 58.0 * EARTH_ECCENTRICITY_SQ)
571 * a_coeff.powi(5)
572 / 120.0)
573 + 500000.0;
574
575 let northing = k0
576 * (m + n
577 * lat_rad.tan()
578 * (a_coeff.powi(2) / 2.0
579 + (5.0 - t + 9.0 * c + 4.0 * c.powi(2)) * a_coeff.powi(4) / 24.0
580 + (61.0 - 58.0 * t + t.powi(2) + 600.0 * c - 330.0 * EARTH_ECCENTRICITY_SQ)
581 * a_coeff.powi(6)
582 / 720.0));
583
584 let final_northing = if lat < 0.0 {
585 northing + 10000000.0
586 } else {
587 northing
588 };
589
590 Ok((easting, final_northing, zone_number, zone_letter))
591}
592
593#[allow(dead_code)]
595fn utm_zone_letter(lat: f64) -> SpatialResult<char> {
596 let letters = [
597 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
598 'W', 'X',
599 ];
600
601 if !(-80.0..=84.0).contains(&lat) {
602 return Err(SpatialError::ValueError(
603 "Latitude out of UTM range".to_string(),
604 ));
605 }
606
607 let index = ((lat + 80.0) / 8.0).floor() as usize;
608 if index < letters.len() {
609 Ok(letters[index])
610 } else {
611 Ok('X') }
613}
614
615#[allow(dead_code)]
626pub fn geographic_to_web_mercator(lat: f64, lon: f64) -> SpatialResult<(f64, f64)> {
627 if lat.abs() >= 85.051_128_779_806_59 {
628 return Err(SpatialError::ValueError(
629 "Latitude must be between -85.051° and 85.051° for Web Mercator".to_string(),
630 ));
631 }
632
633 let x = deg_to_rad(lon) * EARTH_EQUATORIAL_RADIUS_M;
634 let y = ((deg_to_rad(lat) / 2.0 + PI / 4.0).tan()).ln() * EARTH_EQUATORIAL_RADIUS_M;
635
636 Ok((x, y))
637}
638
639#[allow(dead_code)]
650pub fn web_mercator_to_geographic(x: f64, y: f64) -> (f64, f64) {
651 let lon = rad_to_deg(x / EARTH_EQUATORIAL_RADIUS_M);
652 let lat = rad_to_deg(2.0 * ((y / EARTH_EQUATORIAL_RADIUS_M).exp().atan() - PI / 4.0));
653
654 (lat, lon)
655}
656
657#[allow(dead_code)]
671pub fn vincenty_distance(point1: (f64, f64), point2: (f64, f64)) -> SpatialResult<f64> {
672 let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
673 let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
674
675 let a = EARTH_EQUATORIAL_RADIUS_M;
676 let b = EARTH_POLAR_RADIUS_M;
677 let f = EARTH_FLATTENING;
678
679 let l = lon2 - lon1;
680 let u1 = ((1.0 - f) * lat1.tan()).atan();
681 let u2 = ((1.0 - f) * lat2.tan()).atan();
682
683 let sin_u1 = u1.sin();
684 let cos_u1 = u1.cos();
685 let sin_u2 = u2.sin();
686 let cos_u2 = u2.cos();
687
688 let mut lambda = l;
689 let mut lambda_prev;
690 let mut iteration_limit = 100;
691
692 let (cos_sq_alpha, sin_sigma, cos_sigma, sigma, cos_2sigma_m) = loop {
693 iteration_limit -= 1;
694 if iteration_limit == 0 {
695 return Err(SpatialError::ComputationError(
696 "Vincenty formula failed to converge".to_string(),
697 ));
698 }
699
700 let sin_lambda = lambda.sin();
701 let cos_lambda = lambda.cos();
702
703 let sin_sigma = ((cos_u2 * sin_lambda).powi(2)
704 + (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda).powi(2))
705 .sqrt();
706
707 if sin_sigma == 0.0 {
708 return Ok(0.0); }
710
711 let cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
712 let sigma = sin_sigma.atan2(cos_sigma);
713
714 let sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma;
715 let cos_sq_alpha = 1.0 - sin_alpha.powi(2);
716
717 let cos_2sigma_m = if cos_sq_alpha == 0.0 {
718 0.0 } else {
720 cos_sigma - 2.0 * sin_u1 * sin_u2 / cos_sq_alpha
721 };
722
723 let c = f / 16.0 * cos_sq_alpha * (4.0 + f * (4.0 - 3.0 * cos_sq_alpha));
724
725 lambda_prev = lambda;
726 lambda = l
727 + (1.0 - c)
728 * f
729 * sin_alpha
730 * (sigma
731 + c * sin_sigma
732 * (cos_2sigma_m + c * cos_sigma * (-1.0 + 2.0 * cos_2sigma_m.powi(2))));
733
734 if (lambda - lambda_prev).abs() < 1e-12 {
735 break (cos_sq_alpha, sin_sigma, cos_sigma, sigma, cos_2sigma_m);
736 }
737 };
738
739 let u_sq = cos_sq_alpha * (a.powi(2) - b.powi(2)) / b.powi(2);
740 let a_coeff = 1.0 + u_sq / 16384.0 * (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
741 let b_coeff = u_sq / 1024.0 * (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
742
743 let delta_sigma = b_coeff
744 * sin_sigma
745 * (cos_2sigma_m
746 + b_coeff / 4.0
747 * (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m.powi(2))
748 - b_coeff / 6.0
749 * cos_2sigma_m
750 * (-3.0 + 4.0 * sin_sigma.powi(2))
751 * (-3.0 + 4.0 * cos_2sigma_m.powi(2))));
752
753 let distance = b * a_coeff * (sigma - delta_sigma);
754
755 Ok(distance)
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761 use approx::assert_relative_eq;
762
763 #[test]
764 fn test_degree_radian_conversion() {
765 assert_relative_eq!(deg_to_rad(0.0), 0.0, epsilon = 1e-10);
766 assert_relative_eq!(deg_to_rad(90.0), PI / 2.0, epsilon = 1e-10);
767 assert_relative_eq!(deg_to_rad(180.0), PI, epsilon = 1e-10);
768 assert_relative_eq!(deg_to_rad(360.0), 2.0 * PI, epsilon = 1e-10);
769
770 assert_relative_eq!(rad_to_deg(0.0), 0.0, epsilon = 1e-10);
771 assert_relative_eq!(rad_to_deg(PI / 2.0), 90.0, epsilon = 1e-10);
772 assert_relative_eq!(rad_to_deg(PI), 180.0, epsilon = 1e-10);
773 assert_relative_eq!(rad_to_deg(2.0 * PI), 360.0, epsilon = 1e-10);
774 }
775
776 #[test]
777 fn test_haversine_distance() {
778 let london = (51.5074, -0.1278);
780 let paris = (48.8566, 2.3522);
781 let distance = haversine_distance(london, paris);
782 assert!((distance - 344_000.0).abs() < 5_000.0); assert_relative_eq!(haversine_distance(london, london), 0.0, epsilon = 1e-6);
786
787 let north_pole = (90.0, 0.0);
789 let south_pole = (-90.0, 0.0);
790 let antipodal_distance = haversine_distance(north_pole, south_pole);
791 let expected_distance = PI * EARTH_RADIUS_M;
792 assert_relative_eq!(antipodal_distance, expected_distance, epsilon = 1000.0);
793 }
794
795 #[test]
796 fn test_initial_bearing() {
797 let london = (51.5074, -0.1278);
799 let paris = (48.8566, 2.3522);
800 let bearing = initial_bearing(london, paris);
801 let bearing_deg = rad_to_deg(bearing);
802
803 assert!(bearing_deg > 100.0 && bearing_deg < 180.0);
805
806 let start = (0.0, 0.0);
808 let north = (1.0, 0.0);
809 let north_bearing = initial_bearing(start, north);
810 assert_relative_eq!(north_bearing, 0.0, epsilon = 1e-6);
811
812 let east = (0.0, 1.0);
814 let east_bearing = initial_bearing(start, east);
815 assert_relative_eq!(east_bearing, PI / 2.0, epsilon = 1e-6);
816 }
817
818 #[test]
819 fn test_destination_point() {
820 let start = (51.5074, -0.1278); let distance = 100_000.0; let bearing = 0.0; let destination = destination_point(start, distance, bearing);
825
826 assert!(destination.0 > start.0); assert!((destination.1 - start.1).abs() < 0.1); let calculated_distance = haversine_distance(start, destination);
832 assert_relative_eq!(calculated_distance, distance, epsilon = 1000.0); }
834
835 #[test]
836 fn test_midpoint() {
837 let london = (51.5074, -0.1278);
838 let paris = (48.8566, 2.3522);
839 let mid = midpoint(london, paris);
840
841 assert!(mid.0 < london.0 && mid.0 > paris.0); assert!(mid.1 > london.1 && mid.1 < paris.1); let dist_to_london = haversine_distance(mid, london);
847 let dist_to_paris = haversine_distance(mid, paris);
848 assert_relative_eq!(dist_to_london, dist_to_paris, epsilon = 1000.0);
849 }
850
851 #[test]
852 fn test_normalize_angle() {
853 assert_relative_eq!(normalize_angle(0.0), 0.0, epsilon = 1e-10);
854 assert_relative_eq!(normalize_angle(PI), PI, epsilon = 1e-10);
855 assert_relative_eq!(normalize_angle(2.0 * PI), 0.0, epsilon = 1e-10);
856 assert_relative_eq!(normalize_angle(-PI), PI, epsilon = 1e-10);
857 assert_relative_eq!(normalize_angle(3.0 * PI), PI, epsilon = 1e-10);
858 }
859
860 #[test]
861 fn test_normalize_bearing() {
862 assert_relative_eq!(normalize_bearing(0.0), 0.0, epsilon = 1e-10);
863 assert_relative_eq!(normalize_bearing(180.0), 180.0, epsilon = 1e-10);
864 assert_relative_eq!(normalize_bearing(360.0), 0.0, epsilon = 1e-10);
865 assert_relative_eq!(normalize_bearing(-90.0), 270.0, epsilon = 1e-10);
866 assert_relative_eq!(normalize_bearing(450.0), 90.0, epsilon = 1e-10);
867 }
868
869 #[test]
870 fn test_spherical_polygon_area() {
871 let triangle = vec![
873 (0.0, 0.0), (0.0, 1.0), (1.0, 0.0), ];
877
878 let area = spherical_polygon_area(&triangle).unwrap();
879 assert!(area > 0.0);
880
881 let expected = (PI / 180.0).powi(2) * EARTH_RADIUS_M.powi(2) / 2.0;
884 assert_relative_eq!(area, expected, epsilon = expected * 0.1);
885 }
886
887 #[test]
888 fn test_geographic_to_web_mercator() {
889 let (x, y) = geographic_to_web_mercator(0.0, 0.0).unwrap();
891 assert_relative_eq!(x, 0.0, epsilon = 1e-6);
892 assert_relative_eq!(y, 0.0, epsilon = 1e-6);
893
894 let original = (45.0, -90.0);
896 let (x, y) = geographic_to_web_mercator(original.0, original.1).unwrap();
897 let back = web_mercator_to_geographic(x, y);
898 assert_relative_eq!(back.0, original.0, epsilon = 1e-6);
899 assert_relative_eq!(back.1, original.1, epsilon = 1e-6);
900
901 let result = geographic_to_web_mercator(86.0, 0.0);
903 assert!(result.is_err());
904 }
905
906 #[test]
907 fn test_geographic_to_utm() {
908 let london = (51.5074, -0.1278);
910 let (easting, northing, zone, letter) = geographic_to_utm(london.0, london.1).unwrap();
911
912 assert!(zone == 30 || zone == 31);
914 assert!(letter == 'U' || letter == 'V');
915
916 assert!(easting > 400_000.0 && easting < 700_000.0);
918 assert!(northing > 5_700_000.0 && northing < 5_800_000.0);
919
920 assert!(geographic_to_utm(85.0, 0.0).is_err()); assert!(geographic_to_utm(-85.0, 0.0).is_err()); }
924
925 #[test]
926 fn test_cross_track_distance() {
927 let start = (51.0, 0.0);
928 let end = (52.0, 1.0);
929 let point = (51.5, 0.0); let cross_track = cross_track_distance(point, start, end);
932
933 assert!(cross_track.abs() < 50_000.0); }
936
937 #[test]
938 fn test_vincenty_distance() {
939 let london = (51.5074, -0.1278);
941 let paris = (48.8566, 2.3522);
942
943 let haversine_dist = haversine_distance(london, paris);
944 let vincenty_dist = vincenty_distance(london, paris).unwrap();
945
946 let diff_percent = ((vincenty_dist - haversine_dist) / haversine_dist * 100.0).abs();
948 assert!(diff_percent < 1.0); let same_point_dist = vincenty_distance(london, london).unwrap();
952 assert_relative_eq!(same_point_dist, 0.0, epsilon = 1e-6);
953 }
954
955 #[test]
956 fn test_point_in_spherical_polygon() {
957 let square = vec![(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)];
959
960 assert!(point_in_spherical_polygon((0.0, 0.0), &square));
962
963 assert!(!point_in_spherical_polygon((2.0, 2.0), &square));
965
966 let _ = point_in_spherical_polygon((1.0, 0.0), &square);
968 }
969}