1use crate::{Vector3d, great_circle};
28use angle_sc::{Angle, Radians, trig};
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, a.x.hypot(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_south_of(a: &Vector3d, b: &Vector3d) -> bool {
164 a.z < b.z
165}
166
167#[must_use]
174pub fn is_west_of(a: &Vector3d, b: &Vector3d) -> bool {
175 b.xy().perp(&a.xy()) < 0.0
176}
177
178#[must_use]
188pub fn calculate_pole(lat: Angle, lon: Angle, azi: Angle) -> Vector3d {
189 let x = trig::UnitNegRange::clamp(
190 lon.sin().0 * azi.cos().0 - lat.sin().0 * lon.cos().0 * azi.sin().0,
191 );
192 let y = trig::UnitNegRange::clamp(
193 0.0 - lon.cos().0 * azi.cos().0 - lat.sin().0 * lon.sin().0 * azi.sin().0,
194 );
195 let z = trig::UnitNegRange(lat.cos().0 * azi.sin().0);
196
197 Vector3d::new(x.0, y.0, z.0)
198}
199
200#[must_use]
207pub fn calculate_azimuth(point: &Vector3d, pole: &Vector3d) -> Angle {
208 const MAX_LAT: f64 = 1.0 - great_circle::MIN_VALUE;
209
210 let sin_lat = point.z;
211 if MAX_LAT <= sin_lat.abs() {
213 return if sin_lat.is_sign_negative() {
215 Angle::default()
216 } else {
217 Angle::new(trig::UnitNegRange(0.0), trig::UnitNegRange(-1.0))
218 };
219 }
220
221 Angle::from_y_x(pole.z, pole.xy().perp(&point.xy()))
222}
223
224#[must_use]
235pub fn calculate_direction(lat: Angle, lon: Angle, azi: Angle) -> Vector3d {
236 let x = trig::UnitNegRange::clamp(
237 0.0 - lat.sin().0 * lon.cos().0 * azi.cos().0 - lon.sin().0 * azi.sin().0,
238 );
239 let y = trig::UnitNegRange::clamp(
240 0.0 - lat.sin().0 * lon.sin().0 * azi.cos().0 + lon.cos().0 * azi.sin().0,
241 );
242 let z = trig::UnitNegRange(lat.cos().0 * azi.cos().0);
243
244 Vector3d::new(x.0, y.0, z.0)
245}
246
247#[must_use]
254pub fn direction(a: &Vector3d, pole: &Vector3d) -> Vector3d {
255 pole.cross(a)
256}
257
258#[must_use]
266pub fn position(a: &Vector3d, dir: &Vector3d, distance: Angle) -> Vector3d {
267 distance.cos().0 * a + distance.sin().0 * dir
268}
269
270#[must_use]
279pub fn rotate(dir: &Vector3d, pole: &Vector3d, angle: Angle) -> Vector3d {
280 position(dir, pole, angle)
281}
282
283#[must_use]
292pub fn rotate_position(a: &Vector3d, pole: &Vector3d, angle: Angle, radius: Angle) -> Vector3d {
293 position(a, &rotate(&direction(a, pole), pole, angle), radius)
294}
295
296#[must_use]
304fn sin_xtd(pole: &Vector3d, point: &Vector3d) -> trig::UnitNegRange {
305 trig::UnitNegRange::clamp(pole.dot(point))
306}
307
308#[must_use]
317pub fn is_right_of(pole: &Vector3d, point: &Vector3d) -> bool {
318 pole.dot(point) < 0.0
319}
320
321#[must_use]
328pub fn cross_track_distance(pole: &Vector3d, point: &Vector3d) -> Radians {
329 let sin_d = sin_xtd(pole, point);
330 if sin_d.0.abs() < f64::EPSILON {
331 Radians(0.0)
332 } else {
333 Radians(sin_d.0.asin())
334 }
335}
336
337#[must_use]
345pub fn sq_cross_track_distance(pole: &Vector3d, point: &Vector3d) -> f64 {
346 let sin_d = sin_xtd(pole, point);
347 if sin_d.0.abs() < f64::EPSILON {
348 0.0
349 } else {
350 2.0 * (1.0 - trig::swap_sin_cos(sin_d).0)
351 }
352}
353
354#[must_use]
362fn calculate_point_on_plane(pole: &Vector3d, point: &Vector3d) -> Vector3d {
363 let t = sin_xtd(pole, point);
364 point - pole * t.0
365}
366
367#[must_use]
378pub fn sin_atd(a: &Vector3d, pole: &Vector3d, point: &Vector3d) -> trig::UnitNegRange {
379 trig::UnitNegRange::clamp(pole.cross(a).dot(point))
380}
381
382#[must_use]
391pub fn calculate_great_circle_atd(a: &Vector3d, pole: &Vector3d, point: &Vector3d) -> Radians {
392 let sq_atd = sq_distance(a, point);
393 if sq_atd < MIN_SQ_DISTANCE {
394 Radians(0.0)
395 } else {
396 Radians(
397 great_circle::e2gc_distance(sq_atd.sqrt())
398 .0
399 .copysign(sin_atd(a, pole, point).0),
400 )
401 }
402}
403
404#[must_use]
413pub fn along_track_distance(a: &Vector3d, pole: &Vector3d, point: &Vector3d) -> Radians {
414 let plane_point = calculate_point_on_plane(pole, point);
415 normalise(&plane_point, MIN_SQ_NORM).map_or_else(
416 || Radians(0.0), |c| calculate_great_circle_atd(a, pole, &c),
418 )
419}
420
421#[must_use]
431pub fn sq_along_track_distance(a: &Vector3d, pole: &Vector3d, point: &Vector3d) -> f64 {
432 let plane_point = calculate_point_on_plane(pole, point);
433 normalise(&plane_point, MIN_SQ_NORM).map_or_else(
434 || 0.0, |c| {
436 let sq_d = sq_distance(a, &(c));
437 if sq_d < MIN_SQ_DISTANCE { 0.0 } else { sq_d }
438 },
439 )
440}
441
442#[allow(clippy::similar_names)]
451#[must_use]
452pub fn calculate_atd_and_xtd(a: &Vector3d, pole: &Vector3d, p: &Vector3d) -> (Radians, Radians) {
453 let mut atd = Radians(0.0);
454 let mut xtd = Radians(0.0);
455
456 let sq_d = sq_distance(a, p);
457 if sq_d >= MIN_SQ_DISTANCE {
458 let sin_xtd = sin_xtd(pole, p).0;
460 if sin_xtd.abs() >= f64::EPSILON {
461 xtd = Radians(sin_xtd.asin());
462 }
463
464 let plane_point = p - pole * sin_xtd;
466 atd = normalise(&plane_point, MIN_SQ_NORM).map_or_else(
467 || Radians(0.0), |c| calculate_great_circle_atd(a, pole, &c),
469 );
470 }
471
472 (atd, xtd)
473}
474
475#[must_use]
485pub fn normalise_centroid(centroid: &Vector3d, point: &Vector3d, pole: &Vector3d) -> Vector3d {
486 normalise(centroid, MIN_SQ_NORM).unwrap_or_else(|| {
487 position(
492 point,
493 &direction(point, pole),
494 Angle::default().quarter_turn_ccw(),
495 )
496 })
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 use crate::LatLong;
503 use angle_sc::{Degrees, Radians, is_within_tolerance};
504
505 #[test]
506 fn test_normalise() {
507 let zero = Vector3d::new(0.0, 0.0, 0.0);
508 assert!(normalise(&zero, MIN_SQ_NORM).is_none());
509
510 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
512 assert!(normalise(&g_eq, MIN_SQ_NORM).is_some());
513
514 let too_small = Vector3d::new(16383.0 * f64::EPSILON, 0.0, 0.0);
516 assert!(normalise(&too_small, MIN_SQ_NORM).is_none());
517
518 assert_eq!(2.0844083160439303e-10, MIN_SIN_ANGLE.to_degrees().asin());
519
520 let small = Vector3d::new(MIN_SIN_ANGLE, 0.0, 0.0);
522 let result = normalise(&small, MIN_SQ_NORM);
523 assert!(result.is_some());
524
525 assert!(is_unit(&result.unwrap()));
526 assert_eq!(result.unwrap(), g_eq);
527 }
528
529 #[test]
530 fn test_point_lat_longs() {
531 let lat_lon_south = LatLong::new(Degrees(-90.0), Degrees(180.0));
533 let point_south = Vector3d::from(&lat_lon_south);
534 assert!(is_unit(&point_south));
535 assert_eq!(Vector3d::new(0.0, 0.0, -1.0), point_south);
536
537 assert_eq!(Degrees(-90.0), Degrees::from(latitude(&point_south)));
538 assert_eq!(Degrees(0.0), Degrees::from(longitude(&point_south)));
539
540 let result = LatLong::from(&point_south);
541 assert_eq!(-90.0, result.lat().0);
542 assert_eq!(0.0, result.lon().0);
544
545 let lat_lon_0_0 = LatLong::new(Degrees(0.0), Degrees(0.0));
547 let point_0 = Vector3d::from(&lat_lon_0_0);
548 assert!(is_unit(&point_0));
549 assert_eq!(Vector3d::new(1.0, 0.0, 0.0), point_0);
550 assert_eq!(lat_lon_0_0, LatLong::from(&point_0));
551
552 let lat_lon_0_180 = LatLong::new(Degrees(0.0), Degrees(180.0));
554 let point_1 = Vector3d::from(&lat_lon_0_180);
555 assert!(is_unit(&point_1));
556 assert_eq!(Vector3d::new(-1.0, 0.0, 0.0), point_1);
557 assert_eq!(false, is_west_of(&point_0, &point_1));
558 assert_eq!(
559 Radians(core::f64::consts::PI),
560 Radians::from(delta_longitude(&point_0, &point_1)).abs()
561 );
562
563 let lat_lon_0_m180 = LatLong::new(Degrees(0.0), Degrees(-180.0));
564 let point_2 = Vector3d::from(&lat_lon_0_m180);
565 assert!(is_unit(&point_2));
566 assert_eq!(Vector3d::new(-1.0, 0.0, 0.0), point_2);
567 assert_eq!(lat_lon_0_180, LatLong::from(&point_2));
569
570 assert_eq!(false, is_west_of(&point_0, &point_2));
571 assert_eq!(
572 -core::f64::consts::PI,
573 Radians::from(delta_longitude(&point_0, &point_2)).0
574 );
575
576 let lat_lon_0_r3 = LatLong::new(Degrees(0.0), Degrees(3.0_f64.to_degrees()));
577 let point_3 = Vector3d::from(&lat_lon_0_r3);
578 assert!(is_unit(&point_3));
579 let result = LatLong::from(&point_3);
580 assert_eq!(0.0, result.lat().0);
581 assert_eq!(
582 3.0_f64,
583 Radians::from(delta_longitude(&point_3, &point_0)).0
584 );
585 assert_eq!(3.0_f64.to_degrees(), result.lon().0);
586 assert!(is_west_of(&point_0, &point_3));
587 assert_eq!(-3.0, Radians::from(delta_longitude(&point_0, &point_3)).0);
588
589 assert_eq!(false, is_west_of(&point_1, &point_3));
590 assert_eq!(
591 core::f64::consts::PI - 3.0,
592 Radians::from(delta_longitude(&point_1, &point_3)).0
593 );
594
595 let lat_lon_0_mr3 = LatLong::new(Degrees(0.0), Degrees(-3.0_f64.to_degrees()));
596 let point_4 = Vector3d::from(&lat_lon_0_mr3);
597 assert!(is_unit(&point_4));
598 assert_eq!(3.0, Radians::from(delta_longitude(&point_0, &point_4)).0);
599
600 let result = LatLong::from(&point_4);
601 assert_eq!(0.0, result.lat().0);
602 assert_eq!(-3.0_f64.to_degrees(), result.lon().0);
603 assert!(is_west_of(&point_1, &point_4));
604 assert_eq!(
605 3.0 - core::f64::consts::PI,
606 Radians::from(delta_longitude(&point_1, &point_4)).0
607 );
608 }
609
610 #[test]
611 fn test_point_distance() {
612 let lat_lon_south = LatLong::new(Degrees(-90.0), Degrees(0.0));
613 let south_pole = Vector3d::from(&lat_lon_south);
614
615 let lat_lon_north = LatLong::new(Degrees(90.0), Degrees(0.0));
616 let north_pole = Vector3d::from(&lat_lon_north);
617
618 assert_eq!(0.0, sq_distance(&south_pole, &south_pole));
619 assert_eq!(0.0, sq_distance(&north_pole, &north_pole));
620 assert_eq!(4.0, sq_distance(&south_pole, &north_pole));
621
622 assert_eq!(0.0, distance(&south_pole, &south_pole));
623 assert_eq!(0.0, distance(&north_pole, &north_pole));
624 assert_eq!(2.0, distance(&south_pole, &north_pole));
625
626 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
628
629 let idl_eq = Vector3d::new(-1.0, 0.0, 0.0);
631
632 assert_eq!(0.0, sq_distance(&g_eq, &g_eq));
633 assert_eq!(0.0, sq_distance(&idl_eq, &idl_eq));
634 assert_eq!(4.0, sq_distance(&g_eq, &idl_eq));
635
636 assert_eq!(0.0, distance(&g_eq, &g_eq));
637 assert_eq!(0.0, distance(&idl_eq, &idl_eq));
638 assert_eq!(2.0, distance(&g_eq, &idl_eq));
639 }
640
641 #[test]
642 fn test_calculate_azimuth_at_poles() {
643 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
645 let south_pole = Vector3d::new(0.0, 0.0, -1.0);
646 let result = calculate_azimuth(&south_pole, &g_eq);
647 assert_eq!(Angle::default(), result);
648
649 let north_pole = Vector3d::new(0.0, 0.0, 1.0);
650 let result = calculate_azimuth(&north_pole, &g_eq);
651 assert_eq!(Angle::default().opposite(), result);
652 }
653
654 #[test]
655 fn test_calculate_pole_azimuth_and_direction() {
656 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
658
659 let e_eq = Vector3d::new(0.0, 1.0, 0.0);
661
662 let w_eq = Vector3d::new(0.0, -1.0, 0.0);
664
665 let angle_90 = Angle::from(Degrees(90.0));
666 let pole_a = calculate_pole(
667 Angle::from(Degrees(0.0)),
668 Angle::from(Degrees(0.0)),
669 angle_90,
670 );
671 assert!(are_orthogonal(&g_eq, &pole_a));
672
673 let dir_a = calculate_direction(
674 Angle::from(Degrees(0.0)),
675 Angle::from(Degrees(0.0)),
676 angle_90,
677 );
678 assert!(are_orthogonal(&g_eq, &dir_a));
679 assert!(are_orthogonal(&pole_a, &dir_a));
680 assert_eq!(dir_a, direction(&g_eq, &pole_a));
681
682 let north_pole = Vector3d::new(0.0, 0.0, 1.0);
683 assert_eq!(north_pole, pole_a);
684
685 let result = g_eq.cross(&e_eq);
686 assert_eq!(north_pole, result);
687
688 let result = calculate_azimuth(&g_eq, &pole_a);
689 assert_eq!(angle_90, result);
690
691 let pole_b = calculate_pole(
692 Angle::from(Degrees(0.0)),
693 Angle::from(Degrees(0.0)),
694 -angle_90,
695 );
696 assert!(are_orthogonal(&g_eq, &pole_b));
697
698 let dir_b = calculate_direction(
699 Angle::from(Degrees(0.0)),
700 Angle::from(Degrees(0.0)),
701 -angle_90,
702 );
703 assert!(are_orthogonal(&g_eq, &dir_b));
704 assert!(are_orthogonal(&pole_b, &dir_b));
705 assert_eq!(dir_b, direction(&g_eq, &pole_b));
706
707 let south_pole = Vector3d::new(0.0, 0.0, -1.0);
708 assert_eq!(south_pole, pole_b);
709
710 let result = g_eq.cross(&w_eq);
711 assert_eq!(south_pole, result);
712
713 let result = calculate_azimuth(&g_eq, &pole_b);
714 assert_eq!(-angle_90, result);
715 }
716
717 #[test]
718 fn test_calculate_position() {
719 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
721
722 let e_eq = Vector3d::new(0.0, 1.0, 0.0);
724
725 let pole_0 = g_eq.cross(&e_eq);
726
727 let angle_90 = Angle::from(Degrees(90.0));
728
729 let pos_1 = position(&g_eq, &direction(&g_eq, &pole_0), angle_90);
730 assert_eq!(e_eq, pos_1);
731
732 let pos_2 = rotate_position(&g_eq, &pole_0, Angle::default(), angle_90);
733 assert_eq!(e_eq, pos_2);
734
735 let pos_3 = rotate_position(&g_eq, &pole_0, angle_90, angle_90);
736 assert_eq!(pole_0, pos_3);
737 }
738
739 #[test]
740 fn test_calculate_cross_track_distance_and_square() {
741 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
743
744 let e_eq = Vector3d::new(0.0, 1.0, 0.0);
746
747 let pole_0 = g_eq.cross(&e_eq);
748
749 let longitude = Degrees(1.0);
750
751 for lat in -89..90 {
752 let latitude = Degrees(lat as f64);
753 let latlong = LatLong::new(latitude, longitude);
754 let point = Vector3d::from(&latlong);
755
756 assert_eq!(lat < 0, is_south_of(&point, &g_eq));
757 assert_eq!(lat >= 0, !is_south_of(&point, &e_eq));
758 assert_eq!(lat < 0, is_right_of(&pole_0, &point));
759
760 let expected = (lat as f64).to_radians();
761 let xtd = cross_track_distance(&pole_0, &point);
762 let tolerance = if (-83..84).contains(&lat) {
764 2.0 * f64::EPSILON
765 } else {
766 32.0 * f64::EPSILON
767 };
768 assert!(is_within_tolerance(expected, xtd.0, tolerance));
769
770 let expected = great_circle::gc2e_distance(Radians(expected));
771 let expected = expected * expected;
772 let xtd2 = sq_cross_track_distance(&pole_0, &point);
773 let tolerance = if (-83..84).contains(&lat) {
775 4.0 * f64::EPSILON
776 } else {
777 64.0 * f64::EPSILON
778 };
779 assert!(is_within_tolerance(expected, xtd2, tolerance));
780 }
781 }
782
783 #[test]
784 fn test_calculate_along_track_distance_and_square() {
785 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
787
788 let e_eq = Vector3d::new(0.0, 1.0, 0.0);
790
791 let pole_0 = g_eq.cross(&e_eq);
792
793 let latitude = Degrees(1.0);
795
796 for lon in -179..180 {
797 let longitude = Degrees(lon as f64);
798 let latlong = LatLong::new(latitude, longitude);
799 let point = Vector3d::from(&latlong);
800
801 let expected = (lon as f64).to_radians();
802 let atd = along_track_distance(&g_eq, &pole_0, &point);
803 let tolerance = if (-153..154).contains(&lon) {
805 4.0 * f64::EPSILON
806 } else {
807 32.0 * f64::EPSILON
808 };
809 assert!(is_within_tolerance(expected, atd.0, tolerance));
810
811 let (atd, xtd) = calculate_atd_and_xtd(&g_eq, &pole_0, &point);
812 assert!(is_within_tolerance(expected, atd.0, tolerance));
813 assert!(is_within_tolerance(1_f64.to_radians(), xtd.0, f64::EPSILON));
814
815 let expected = great_circle::gc2e_distance(Radians(expected));
816 let expected = expected * expected;
817 let atd2 = sq_along_track_distance(&g_eq, &pole_0, &point);
818 let tolerance = if (-86..87).contains(&lon) {
820 2.0 * f64::EPSILON
821 } else {
822 32.0 * f64::EPSILON
823 };
824 assert!(is_within_tolerance(expected, atd2, tolerance));
825 }
826 }
827
828 #[test]
829 fn test_special_cases() {
830 let g_eq = Vector3d::new(1.0, 0.0, 0.0);
832
833 let e_eq = Vector3d::new(0.0, 1.0, 0.0);
835
836 let pole_0 = g_eq.cross(&e_eq);
837
838 assert_eq!(0.0, along_track_distance(&g_eq, &pole_0, &pole_0).0);
840 assert_eq!(0.0, sq_along_track_distance(&g_eq, &pole_0, &pole_0));
841
842 let (atd, xtd) = calculate_atd_and_xtd(&g_eq, &pole_0, &g_eq);
843 assert_eq!(0.0, atd.0);
844 assert_eq!(0.0, xtd.0);
845
846 let (atd, xtd) = calculate_atd_and_xtd(&g_eq, &pole_0, &pole_0);
847 assert_eq!(0.0, atd.0);
848 assert_eq!(core::f64::consts::FRAC_PI_2, xtd.0);
849
850 let (atd, xtd) = calculate_atd_and_xtd(&g_eq, &pole_0, &-pole_0);
851 assert_eq!(0.0, atd.0);
852 assert_eq!(-core::f64::consts::FRAC_PI_2, xtd.0);
853
854 let near_north_pole = LatLong::new(Degrees(89.99999), Degrees(0.0));
856 let p = Vector3d::from(&near_north_pole);
857 let (atd, xtd) = calculate_atd_and_xtd(&g_eq, &pole_0, &p);
858 assert_eq!(0.0, atd.0);
859 assert!(is_within_tolerance(
860 core::f64::consts::FRAC_PI_2,
861 xtd.0,
862 0.000001
863 ));
864 }
865
866 #[test]
867 fn test_normalise_centroid() {
868 let point_0 = Vector3d::new(0.0, 0.0, 0.0);
869 let point_1 = Vector3d::new(1.0, 0.0, 0.0);
870 let point_m1 = -point_1;
871 let pole_1 = Vector3d::new(0.0, 0.0, 1.0);
872
873 let result = normalise_centroid(&point_0, &point_1, &pole_1);
875 assert_eq!(Vector3d::new(0.0, -1.0, 0.0), result);
876
877 let result = normalise_centroid(&point_0, &point_m1, &pole_1);
879 assert_eq!(Vector3d::new(0.0, 1.0, 0.0), result);
880
881 let point_2 = point_1 + point_1;
883 let result = normalise_centroid(&point_2, &point_1, &pole_1);
884 assert_eq!(point_1, result);
885 }
886}