1use crate::{great_circle, Vector3d};
28use angle_sc::{trig, Angle, Radians};
29
30pub mod intersection;
31
32pub const MIN_SIN_ANGLE: f64 = 16384.0 * f64::EPSILON;
35
36pub const MIN_SQ_NORM: f64 = MIN_SIN_ANGLE * MIN_SIN_ANGLE;
38
39pub const MIN_SQ_DISTANCE: f64 = great_circle::MIN_VALUE * great_circle::MIN_VALUE;
41
42#[must_use]
50pub fn to_point(lat: Angle, lon: Angle) -> Vector3d {
51 Vector3d::new(
52 lat.cos().0 * lon.cos().0,
53 lat.cos().0 * lon.sin().0,
54 lat.sin().0,
55 )
56}
57
58#[must_use]
64pub fn latitude(a: &Vector3d) -> Angle {
65 Angle::from_y_x(a.z, libm::hypot(a.x, a.y))
66}
67
68#[must_use]
74pub fn longitude(a: &Vector3d) -> Angle {
75 Angle::from_y_x(a.y, a.x)
76}
77
78#[must_use]
84pub fn is_unit(a: &Vector3d) -> bool {
85 const MIN_POINT_SQ_LENGTH: f64 = 1.0 - 12.0 * f64::EPSILON;
86 const MAX_POINT_SQ_LENGTH: f64 = 1.0 + 12.0 * f64::EPSILON;
87
88 (MIN_POINT_SQ_LENGTH..=MAX_POINT_SQ_LENGTH).contains(&(a.norm_squared()))
89}
90
91#[must_use]
100pub fn normalise(a: &Vector3d, min_sq_value: f64) -> Option<Vector3d> {
101 if a.norm_squared() < min_sq_value {
102 None
103 } else {
104 Some(a.normalize())
105 }
106}
107
108#[must_use]
116pub fn sq_distance(a: &Vector3d, b: &Vector3d) -> f64 {
117 (b - a).norm_squared()
118}
119
120#[must_use]
127pub fn distance(a: &Vector3d, b: &Vector3d) -> f64 {
128 (b - a).norm()
129}
130
131#[must_use]
137pub fn are_orthogonal(a: &Vector3d, b: &Vector3d) -> bool {
138 const MAX_LENGTH: f64 = 4.0 * f64::EPSILON;
139
140 (-MAX_LENGTH..=MAX_LENGTH).contains(&(a.dot(b)))
141}
142
143#[must_use]
150pub fn delta_longitude(a: &Vector3d, b: &Vector3d) -> Angle {
151 let a_lon = a.xy();
152 let b_lon = b.xy();
153 Angle::from_y_x(b_lon.perp(&a_lon), b_lon.dot(&a_lon))
154}
155
156#[must_use]
163pub fn is_west_of(a: &Vector3d, b: &Vector3d) -> bool {
164 b.xy().perp(&a.xy()) <= -f64::EPSILON
166}
167
168#[must_use]
178pub fn calculate_pole(lat: Angle, lon: Angle, azi: Angle) -> Vector3d {
179 let x = trig::UnitNegRange::clamp(
180 lon.sin().0 * azi.cos().0 - lat.sin().0 * lon.cos().0 * azi.sin().0,
181 );
182 let y = trig::UnitNegRange::clamp(
183 0.0 - lon.cos().0 * azi.cos().0 - lat.sin().0 * lon.sin().0 * azi.sin().0,
184 );
185 let z = trig::UnitNegRange(lat.cos().0 * azi.sin().0);
186
187 Vector3d::new(x.0, y.0, z.0)
188}
189
190#[must_use]
197pub fn calculate_azimuth(point: &Vector3d, pole: &Vector3d) -> Angle {
198 const MAX_LAT: f64 = 1.0 - great_circle::MIN_VALUE;
199
200 let sin_lat = point.z;
201 if MAX_LAT <= libm::fabs(sin_lat) {
203 return if sin_lat.is_sign_negative() {
205 Angle::default()
206 } else {
207 Angle::new(trig::UnitNegRange(0.0), trig::UnitNegRange(-1.0))
208 };
209 }
210
211 Angle::from_y_x(pole.z, pole.xy().perp(&point.xy()))
212}
213
214#[must_use]
225pub fn calculate_direction(lat: Angle, lon: Angle, azi: Angle) -> Vector3d {
226 let x = trig::UnitNegRange::clamp(
227 0.0 - lat.sin().0 * lon.cos().0 * azi.cos().0 - lon.sin().0 * azi.sin().0,
228 );
229 let y = trig::UnitNegRange::clamp(
230 0.0 - lat.sin().0 * lon.sin().0 * azi.cos().0 + lon.cos().0 * azi.sin().0,
231 );
232 let z = trig::UnitNegRange(lat.cos().0 * azi.cos().0);
233
234 Vector3d::new(x.0, y.0, z.0)
235}
236
237#[must_use]
244pub fn direction(a: &Vector3d, pole: &Vector3d) -> Vector3d {
245 pole.cross(a)
246}
247
248#[must_use]
256pub fn position(a: &Vector3d, dir: &Vector3d, distance: Angle) -> Vector3d {
257 distance.cos().0 * a + distance.sin().0 * dir
258}
259
260#[must_use]
269pub fn rotate(dir: &Vector3d, pole: &Vector3d, angle: Angle) -> Vector3d {
270 position(dir, pole, angle)
271}
272
273#[must_use]
282pub fn rotate_position(a: &Vector3d, pole: &Vector3d, angle: Angle, radius: Angle) -> Vector3d {
283 position(a, &rotate(&direction(a, pole), pole, angle), radius)
284}
285
286#[must_use]
294fn sin_xtd(pole: &Vector3d, point: &Vector3d) -> trig::UnitNegRange {
295 trig::UnitNegRange::clamp(pole.dot(point))
296}
297
298#[must_use]
305pub fn cross_track_distance(pole: &Vector3d, point: &Vector3d) -> Radians {
306 let sin_d = sin_xtd(pole, point);
307 if libm::fabs(sin_d.0) < f64::EPSILON {
308 Radians(0.0)
309 } else {
310 Radians(libm::asin(sin_d.0))
311 }
312}
313
314#[must_use]
322pub fn sq_cross_track_distance(pole: &Vector3d, point: &Vector3d) -> f64 {
323 let sin_d = sin_xtd(pole, point);
324 if libm::fabs(sin_d.0) < f64::EPSILON {
325 0.0
326 } else {
327 2.0 * (1.0 - trig::swap_sin_cos(sin_d).0)
328 }
329}
330
331#[must_use]
339fn calculate_point_on_plane(pole: &Vector3d, point: &Vector3d) -> Vector3d {
340 let t = sin_xtd(pole, point);
341 point - pole * t.0
342}
343
344#[must_use]
355pub fn sin_atd(a: &Vector3d, pole: &Vector3d, point: &Vector3d) -> trig::UnitNegRange {
356 trig::UnitNegRange::clamp(pole.cross(a).dot(point))
357}
358
359#[must_use]
368pub fn calculate_great_circle_atd(a: &Vector3d, pole: &Vector3d, point: &Vector3d) -> Radians {
369 let sq_atd = sq_distance(a, point);
370 if sq_atd < MIN_SQ_DISTANCE {
371 Radians(0.0)
372 } else {
373 Radians(libm::copysign(
374 great_circle::e2gc_distance(libm::sqrt(sq_atd)).0,
375 sin_atd(a, pole, point).0,
376 ))
377 }
378}
379
380#[must_use]
389pub fn along_track_distance(a: &Vector3d, pole: &Vector3d, point: &Vector3d) -> Radians {
390 let plane_point = calculate_point_on_plane(pole, point);
391 normalise(&plane_point, MIN_SQ_NORM).map_or_else(
392 || Radians(0.0), |c| calculate_great_circle_atd(a, pole, &c),
394 )
395}
396
397#[must_use]
407pub fn sq_along_track_distance(a: &Vector3d, pole: &Vector3d, point: &Vector3d) -> f64 {
408 let plane_point = calculate_point_on_plane(pole, point);
409 normalise(&plane_point, MIN_SQ_NORM).map_or_else(
410 || 0.0, |c| {
412 let sq_d = sq_distance(a, &(c));
413 if sq_d < MIN_SQ_DISTANCE {
414 0.0
415 } else {
416 sq_d
417 }
418 },
419 )
420}
421
422#[allow(clippy::similar_names)]
431#[must_use]
432pub fn calculate_atd_and_xtd(a: &Vector3d, pole: &Vector3d, p: &Vector3d) -> (Radians, Radians) {
433 let mut atd = Radians(0.0);
434 let mut xtd = Radians(0.0);
435
436 let sq_d = sq_distance(a, p);
437 if sq_d >= MIN_SQ_DISTANCE {
438 let sin_xtd = sin_xtd(pole, p).0;
440 if libm::fabs(sin_xtd) >= f64::EPSILON {
441 xtd = Radians(libm::asin(sin_xtd));
442 }
443
444 let plane_point = p - pole * sin_xtd;
446 atd = normalise(&plane_point, MIN_SQ_NORM).map_or_else(
447 || Radians(0.0), |c| calculate_great_circle_atd(a, pole, &c),
449 );
450 }
451
452 (atd, xtd)
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use crate::LatLong;
459 use angle_sc::{is_within_tolerance, Degrees, Radians};
460
461 #[test]
462 fn test_normalise() {
463 let zero = Vector3d::new(0.0, 0.0, 0.0);
464 assert!(normalise(&zero, MIN_SQ_NORM).is_none());
465
466 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
468 assert!(normalise(&g_eq, MIN_SQ_NORM).is_some());
469
470 let too_small = Vector3d::new(16383.0 * f64::EPSILON, 0.0, 0.0);
472 assert!(normalise(&too_small, MIN_SQ_NORM).is_none());
473
474 assert_eq!(
475 2.0844083160439303e-10,
476 libm::asin(MIN_SIN_ANGLE).to_degrees()
477 );
478
479 let small = Vector3d::new(MIN_SIN_ANGLE, 0.0, 0.0);
481 let result = normalise(&small, MIN_SQ_NORM);
482 assert!(result.is_some());
483
484 assert!(is_unit(&result.unwrap()));
485 assert_eq!(result.unwrap(), g_eq);
486 }
487
488 #[test]
489 fn test_point_lat_longs() {
490 let lat_lon_south = LatLong::new(Degrees(-90.0), Degrees(180.0));
492 let point_south = Vector3d::from(&lat_lon_south);
493
494 assert_eq!(Degrees(-90.0), Degrees::from(latitude(&point_south)));
495 assert_eq!(Degrees(0.0), Degrees::from(longitude(&point_south)));
496
497 let result = LatLong::from(&point_south);
498 assert_eq!(-90.0, result.lat().0);
499 assert_eq!(0.0, result.lon().0);
501
502 let lat_lon_0_0 = LatLong::new(Degrees(0.0), Degrees(0.0));
504 let point_0 = Vector3d::from(&lat_lon_0_0);
505 assert!(is_unit(&point_0));
506 assert_eq!(lat_lon_0_0, LatLong::from(&point_0));
507
508 let lat_lon_0_180 = LatLong::new(Degrees(0.0), Degrees(180.0));
510 let point_1 = Vector3d::from(&lat_lon_0_180);
511 assert!(is_unit(&point_1));
512 assert_eq!(false, is_west_of(&point_0, &point_1));
513 assert_eq!(
514 Radians(core::f64::consts::PI),
515 Radians::from(delta_longitude(&point_0, &point_1)).abs()
516 );
517
518 let lat_lon_0_m180 = LatLong::new(Degrees(0.0), Degrees(-180.0));
519 let point_2 = Vector3d::from(&lat_lon_0_m180);
520 assert!(is_unit(&point_2));
521 assert_eq!(lat_lon_0_180, LatLong::from(&point_2));
523
524 assert_eq!(false, is_west_of(&point_0, &point_2));
525 assert_eq!(
526 -core::f64::consts::PI,
527 Radians::from(delta_longitude(&point_0, &point_2)).0
528 );
529
530 let lat_lon_0_r3 = LatLong::new(Degrees(0.0), Degrees(3.0_f64.to_degrees()));
531 let point_3 = Vector3d::from(&lat_lon_0_r3);
532 assert!(is_unit(&point_3));
533 let result = LatLong::from(&point_3);
534 assert_eq!(0.0, result.lat().0);
535 assert_eq!(
536 3.0_f64,
537 Radians::from(delta_longitude(&point_3, &point_0)).0
538 );
539 assert_eq!(3.0_f64.to_degrees(), result.lon().0);
540 assert!(is_west_of(&point_0, &point_3));
541 assert_eq!(-3.0, Radians::from(delta_longitude(&point_0, &point_3)).0);
542
543 assert_eq!(false, is_west_of(&point_1, &point_3));
544 assert_eq!(
545 core::f64::consts::PI - 3.0,
546 Radians::from(delta_longitude(&point_1, &point_3)).0
547 );
548
549 let lat_lon_0_mr3 = LatLong::new(Degrees(0.0), Degrees(-3.0_f64.to_degrees()));
550 let point_4 = Vector3d::from(&lat_lon_0_mr3);
551 assert!(is_unit(&point_4));
552 assert_eq!(3.0, Radians::from(delta_longitude(&point_0, &point_4)).0);
553
554 let result = LatLong::from(&point_4);
555 assert_eq!(0.0, result.lat().0);
556 assert_eq!(-3.0_f64.to_degrees(), result.lon().0);
557 assert!(is_west_of(&point_1, &point_4));
558 assert_eq!(
559 3.0 - core::f64::consts::PI,
560 Radians::from(delta_longitude(&point_1, &point_4)).0
561 );
562 }
563
564 #[test]
565 fn test_point_distance() {
566 let lat_lon_south = LatLong::new(Degrees(-90.0), Degrees(0.0));
567 let south_pole = Vector3d::from(&lat_lon_south);
568
569 let lat_lon_north = LatLong::new(Degrees(90.0), Degrees(0.0));
570 let north_pole = Vector3d::from(&lat_lon_north);
571
572 assert_eq!(0.0, sq_distance(&south_pole, &south_pole));
573 assert_eq!(0.0, sq_distance(&north_pole, &north_pole));
574 assert_eq!(4.0, sq_distance(&south_pole, &north_pole));
575
576 assert_eq!(0.0, distance(&south_pole, &south_pole));
577 assert_eq!(0.0, distance(&north_pole, &north_pole));
578 assert_eq!(2.0, distance(&south_pole, &north_pole));
579
580 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
582
583 let idl_eq = Vector3d::new(-1.0, 0.0, 0.0);
585
586 assert_eq!(0.0, sq_distance(&g_eq, &g_eq));
587 assert_eq!(0.0, sq_distance(&idl_eq, &idl_eq));
588 assert_eq!(4.0, sq_distance(&g_eq, &idl_eq));
589
590 assert_eq!(0.0, distance(&g_eq, &g_eq));
591 assert_eq!(0.0, distance(&idl_eq, &idl_eq));
592 assert_eq!(2.0, distance(&g_eq, &idl_eq));
593 }
594
595 #[test]
596 fn test_calculate_azimuth_at_poles() {
597 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
599 let south_pole = Vector3d::new(0.0, 0.0, -1.0);
600 let result = calculate_azimuth(&south_pole, &g_eq);
601 assert_eq!(Angle::default(), result);
602
603 let north_pole = Vector3d::new(0.0, 0.0, 1.0);
604 let result = calculate_azimuth(&north_pole, &g_eq);
605 assert_eq!(Angle::default().opposite(), result);
606 }
607
608 #[test]
609 fn test_calculate_pole_azimuth_and_direction() {
610 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
612
613 let e_eq = Vector3d::new(0.0, 1.0, 0.0);
615
616 let w_eq = Vector3d::new(0.0, -1.0, 0.0);
618
619 let angle_90 = Angle::from(Degrees(90.0));
620 let pole_a = calculate_pole(
621 Angle::from(Degrees(0.0)),
622 Angle::from(Degrees(0.0)),
623 angle_90,
624 );
625 assert!(are_orthogonal(&g_eq, &pole_a));
626
627 let dir_a = calculate_direction(
628 Angle::from(Degrees(0.0)),
629 Angle::from(Degrees(0.0)),
630 angle_90,
631 );
632 assert!(are_orthogonal(&g_eq, &dir_a));
633 assert!(are_orthogonal(&pole_a, &dir_a));
634 assert_eq!(dir_a, direction(&g_eq, &pole_a));
635
636 let north_pole = Vector3d::new(0.0, 0.0, 1.0);
637 assert_eq!(north_pole, pole_a);
638
639 let result = g_eq.cross(&e_eq);
640 assert_eq!(north_pole, result);
641
642 let result = calculate_azimuth(&g_eq, &pole_a);
643 assert_eq!(angle_90, result);
644
645 let pole_b = calculate_pole(
646 Angle::from(Degrees(0.0)),
647 Angle::from(Degrees(0.0)),
648 -angle_90,
649 );
650 assert!(are_orthogonal(&g_eq, &pole_b));
651
652 let dir_b = calculate_direction(
653 Angle::from(Degrees(0.0)),
654 Angle::from(Degrees(0.0)),
655 -angle_90,
656 );
657 assert!(are_orthogonal(&g_eq, &dir_b));
658 assert!(are_orthogonal(&pole_b, &dir_b));
659 assert_eq!(dir_b, direction(&g_eq, &pole_b));
660
661 let south_pole = Vector3d::new(0.0, 0.0, -1.0);
662 assert_eq!(south_pole, pole_b);
663
664 let result = g_eq.cross(&w_eq);
665 assert_eq!(south_pole, result);
666
667 let result = calculate_azimuth(&g_eq, &pole_b);
668 assert_eq!(-angle_90, result);
669 }
670
671 #[test]
672 fn test_calculate_position() {
673 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
675
676 let e_eq = Vector3d::new(0.0, 1.0, 0.0);
678
679 let pole_0 = g_eq.cross(&e_eq);
680
681 let angle_90 = Angle::from(Degrees(90.0));
682
683 let pos_1 = position(&g_eq, &direction(&g_eq, &pole_0), angle_90);
684 assert_eq!(e_eq, pos_1);
685
686 let pos_2 = rotate_position(&g_eq, &pole_0, Angle::default(), angle_90);
687 assert_eq!(e_eq, pos_2);
688
689 let pos_3 = rotate_position(&g_eq, &pole_0, angle_90, angle_90);
690 assert_eq!(pole_0, pos_3);
691 }
692
693 #[test]
694 fn test_calculate_cross_track_distance_and_square() {
695 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
697
698 let e_eq = Vector3d::new(0.0, 1.0, 0.0);
700
701 let pole_0 = g_eq.cross(&e_eq);
702
703 let longitude = Degrees(1.0);
704
705 for lat in -89..90 {
706 let latitude = Degrees(lat as f64);
707 let latlong = LatLong::new(latitude, longitude);
708 let point = Vector3d::from(&latlong);
709
710 let expected = (lat as f64).to_radians();
711 let xtd = cross_track_distance(&pole_0, &point);
712 let tolerance = if (-83..84).contains(&lat) {
714 2.0 * f64::EPSILON
715 } else {
716 32.0 * f64::EPSILON
717 };
718 assert!(is_within_tolerance(expected, xtd.0, tolerance));
719
720 let expected = great_circle::gc2e_distance(Radians(expected));
721 let expected = expected * expected;
722 let xtd2 = sq_cross_track_distance(&pole_0, &point);
723 let tolerance = if (-83..84).contains(&lat) {
725 4.0 * f64::EPSILON
726 } else {
727 64.0 * f64::EPSILON
728 };
729 assert!(is_within_tolerance(expected, xtd2, tolerance));
730 }
731 }
732
733 #[test]
734 fn test_calculate_along_track_distance_and_square() {
735 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
737
738 let e_eq = Vector3d::new(0.0, 1.0, 0.0);
740
741 let pole_0 = g_eq.cross(&e_eq);
742
743 let latitude = Degrees(1.0);
745
746 for lon in -179..180 {
747 let longitude = Degrees(lon as f64);
748 let latlong = LatLong::new(latitude, longitude);
749 let point = Vector3d::from(&latlong);
750
751 let expected = (lon as f64).to_radians();
752 let atd = along_track_distance(&g_eq, &pole_0, &point);
753 let tolerance = if (-153..154).contains(&lon) {
755 2.0 * f64::EPSILON
756 } else {
757 32.0 * f64::EPSILON
758 };
759 assert!(is_within_tolerance(expected, atd.0, tolerance));
760
761 let (atd, xtd) = calculate_atd_and_xtd(&g_eq, &pole_0, &point);
762 assert!(is_within_tolerance(expected, atd.0, tolerance));
763 assert!(is_within_tolerance(1_f64.to_radians(), xtd.0, f64::EPSILON));
764
765 let expected = great_circle::gc2e_distance(Radians(expected));
766 let expected = expected * expected;
767 let atd2 = sq_along_track_distance(&g_eq, &pole_0, &point);
768 let tolerance = if (-86..87).contains(&lon) {
770 2.0 * f64::EPSILON
771 } else {
772 32.0 * f64::EPSILON
773 };
774 assert!(is_within_tolerance(expected, atd2, tolerance));
775 }
776 }
777
778 #[test]
779 fn test_special_cases() {
780 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
782
783 let e_eq = Vector3d::new(0.0, 1.0, 0.0);
785
786 let pole_0 = g_eq.cross(&e_eq);
787
788 assert_eq!(0.0, along_track_distance(&g_eq, &pole_0, &pole_0).0);
790 assert_eq!(0.0, sq_along_track_distance(&g_eq, &pole_0, &pole_0));
791
792 let (atd, xtd) = calculate_atd_and_xtd(&g_eq, &pole_0, &g_eq);
793 assert_eq!(0.0, atd.0);
794 assert_eq!(0.0, xtd.0);
795
796 let (atd, xtd) = calculate_atd_and_xtd(&g_eq, &pole_0, &pole_0);
797 assert_eq!(0.0, atd.0);
798 assert_eq!(core::f64::consts::FRAC_PI_2, xtd.0);
799
800 let (atd, xtd) = calculate_atd_and_xtd(&g_eq, &pole_0, &-pole_0);
801 assert_eq!(0.0, atd.0);
802 assert_eq!(-core::f64::consts::FRAC_PI_2, xtd.0);
803
804 let near_north_pole = LatLong::new(Degrees(89.99999), Degrees(0.0));
806 let p = Vector3d::from(&near_north_pole);
807 let (atd, xtd) = calculate_atd_and_xtd(&g_eq, &pole_0, &p);
808 assert_eq!(0.0, atd.0);
809 assert!(is_within_tolerance(
810 core::f64::consts::FRAC_PI_2,
811 xtd.0,
812 0.000001
813 ));
814 }
815}