use geometry_adapt::{Adapt, WithCs};
use geometry_algorithm::distance_with;
use geometry_cs::{Degree, Geographic, Spheroid};
use geometry_strategy::geographic::{Andoyer, Vincenty};
type GeographicPoint = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
#[inline]
fn deg(lon: f64, lat: f64) -> GeographicPoint {
WithCs::new(Adapt([lon, lat]))
}
const GDA: Spheroid = Spheroid {
equatorial_radius: 6_378_137.0,
flattening: 1.0 / 298.257_222_10,
};
const ANDOYER_EQUATORIAL_HALF: f64 = core::f64::consts::PI * 6_378_137.0;
struct Case {
name: &'static str,
lon1: f64,
lat1: f64,
lon2: f64,
lat2: f64,
expected_metres: f64,
tolerance_metres: f64,
}
const ANDOYER_CASES: &[Case] = &[
Case {
name: "polar 1deg lon 10deg lat",
lon1: 0.0,
lat1: 90.0,
lon2: 1.0,
lat2: 80.0,
expected_metres: 1_116_814.237,
tolerance_metres: 10.0,
},
Case {
name: "(4, 52) to (3, 40)",
lon1: 4.0,
lat1: 52.0,
lon2: 3.0,
lat2: 40.0,
expected_metres: 1_336_039.890,
tolerance_metres: 10.0,
},
Case {
name: "(3, 52) to (4, 40)",
lon1: 3.0,
lat1: 52.0,
lon2: 4.0,
lat2: 40.0,
expected_metres: 1_336_039.890,
tolerance_metres: 10.0,
},
Case {
name: "antipodal equator (0,0) to (180,0)",
lon1: 0.0,
lat1: 0.0,
lon2: 180.0,
lat2: 0.0,
expected_metres: ANDOYER_EQUATORIAL_HALF,
tolerance_metres: 1_000.0,
},
Case {
name: "antipodal equator (0,0) to (-180,0)",
lon1: 0.0,
lat1: 0.0,
lon2: -180.0,
lat2: 0.0,
expected_metres: ANDOYER_EQUATORIAL_HALF,
tolerance_metres: 1_000.0,
},
Case {
name: "antipodal equator (-90,0) to (90,0)",
lon1: -90.0,
lat1: 0.0,
lon2: 90.0,
lat2: 0.0,
expected_metres: ANDOYER_EQUATORIAL_HALF,
tolerance_metres: 1_000.0,
},
Case {
name: "antipodal equator (90,0) to (-90,0)",
lon1: 90.0,
lat1: 0.0,
lon2: -90.0,
lat2: 0.0,
expected_metres: ANDOYER_EQUATORIAL_HALF,
tolerance_metres: 1_000.0,
},
];
const VINCENTY_CASES: &[Case] = &[
Case {
name: "(0,0) to (0,50) N",
lon1: 0.0,
lat1: 0.0,
lon2: 0.0,
lat2: 50.0,
expected_metres: 5_540_847.042,
tolerance_metres: 0.01,
},
Case {
name: "(0,0) to (0,-50) S",
lon1: 0.0,
lat1: 0.0,
lon2: 0.0,
lat2: -50.0,
expected_metres: 5_540_847.042,
tolerance_metres: 0.01,
},
Case {
name: "(0,0) to (50,0) E",
lon1: 0.0,
lat1: 0.0,
lon2: 50.0,
lat2: 0.0,
expected_metres: 5_565_974.540,
tolerance_metres: 0.01,
},
Case {
name: "(0,0) to (-50,0) W",
lon1: 0.0,
lat1: 0.0,
lon2: -50.0,
lat2: 0.0,
expected_metres: 5_565_974.540,
tolerance_metres: 0.01,
},
Case {
name: "(0,0) to (50,50) NE",
lon1: 0.0,
lat1: 0.0,
lon2: 50.0,
lat2: 50.0,
expected_metres: 7_284_879.297,
tolerance_metres: 0.01,
},
Case {
name: "(0,89) to (1,80) sub-polar (WGS84)",
lon1: 0.0,
lat1: 89.0,
lon2: 1.0,
lat2: 80.0,
expected_metres: 1_005_153.576_9,
tolerance_metres: 1_005_153.576_9 * 0.001 / 100.0,
},
Case {
name: "(4, 52) to (3, 40) (WGS84)",
lon1: 4.0,
lat1: 52.0,
lon2: 3.0,
lat2: 40.0,
expected_metres: 1_336_039.890,
tolerance_metres: 1_336_039.890 * 0.001 / 100.0,
},
];
fn vincenty_uses_wgs84(case: &Case) -> bool {
matches!(
case.name,
"(0,89) to (1,80) sub-polar (WGS84)" | "(4, 52) to (3, 40) (WGS84)"
)
}
#[test]
fn andoyer_table() {
let strategy = Andoyer::WGS84;
for case in ANDOYER_CASES {
let p1 = deg(case.lon1, case.lat1);
let p2 = deg(case.lon2, case.lat2);
let d = distance_with(&p1, &p2, strategy);
assert!(
(d - case.expected_metres).abs() < case.tolerance_metres,
"Andoyer[{}]: got {} m, expected {} m (tol {} m)",
case.name,
d,
case.expected_metres,
case.tolerance_metres,
);
}
}
#[test]
fn vincenty_table() {
let gda = Vincenty {
spheroid: GDA,
..Vincenty::WGS84
};
let wgs = Vincenty::WGS84;
for case in VINCENTY_CASES {
let p1 = deg(case.lon1, case.lat1);
let p2 = deg(case.lon2, case.lat2);
let d = if vincenty_uses_wgs84(case) {
distance_with(&p1, &p2, wgs)
} else {
distance_with(&p1, &p2, gda)
};
assert!(
(d - case.expected_metres).abs() < case.tolerance_metres,
"Vincenty[{}]: got {} m, expected {} m (tol {} m)",
case.name,
d,
case.expected_metres,
case.tolerance_metres,
);
}
}
#[test]
fn amsterdam_paris_andoyer_vincenty_agree() {
let amsterdam = deg(4.90, 52.37);
let paris = deg(2.35, 48.86);
let d_andoyer = distance_with(&amsterdam, &paris, Andoyer::WGS84);
let d_vincenty = distance_with(&amsterdam, &paris, Vincenty::WGS84);
assert!(
(d_andoyer - d_vincenty).abs() < 5.0,
"Andoyer {} vs Vincenty {} disagree on Amsterdam->Paris (|d| {} m)",
d_andoyer,
d_vincenty,
(d_andoyer - d_vincenty).abs(),
);
}
#[test]
fn vincenty_stress_nearly_antipodal() {
let strategy = Vincenty {
spheroid: GDA,
..Vincenty::WGS84
};
let a = deg(0.0, 0.0);
let b = deg(179.5, 0.5);
let d = distance_with(&a, &b, strategy);
assert!(
d.is_finite(),
"Vincenty near-antipodal returned non-finite {d}"
);
}