use std::f64::consts::PI;
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use super::geo_distance::{
distance_satisfies, great_circle_distance_m, DistanceOp, DISTANCE_EQUALITY_RESOLUTION_M,
EARTH_RADIUS_M,
};
const REFERENCE_AGREEMENT_M: f64 = 1e-6;
const ALL_OPS: [DistanceOp; 6] = [
DistanceOp::Eq,
DistanceOp::NotEq,
DistanceOp::Gt,
DistanceOp::Gte,
DistanceOp::Lt,
DistanceOp::Lte,
];
type Pair = (f64, f64, f64, f64);
const NEAR_ANTIPODAL: Pair = (
36.308_407_363_430_36,
54.900_479_056_369_05,
-36.308_406_445_675_69,
-125.099_520_729_386_2,
);
const NEAR_ANTIPODAL_REFERENCE_M: f64 = 20_015_086.692_180_898;
const NAN_UNDER_HAVERSINE: Pair = (2.5, -120.0, -2.5, 60.0);
const PARIS_LONDON: Pair = (48.8566, 2.3522, 51.5074, -0.1278);
const PARIS_LONDON_REFERENCE_M: f64 = 343_556.060_341_041_65;
const NYC_LA: Pair = (40.7128, -74.0060, 34.0522, -118.2437);
const NYC_LA_REFERENCE_M: f64 = 3_935_746.254_609_723;
const ACROSS_ANTIMERIDIAN: Pair = (0.0, 179.9, 0.0, -179.9);
const ACROSS_ANTIMERIDIAN_REFERENCE_M: f64 = 22_238.985_328_910_483;
const HALF_CIRCUMFERENCE_M: f64 = PI * EARTH_RADIUS_M;
fn distance(pair: Pair) -> f64 {
let (lat1, lng1, lat2, lng2) = pair;
great_circle_distance_m(lat1, lng1, lat2, lng2).expect("test: valid coordinates")
}
fn assert_reproduces(pair: Pair, reference_m: f64) {
let computed = distance(pair);
assert!(
(computed - reference_m).abs() < REFERENCE_AGREEMENT_M,
"{pair:?}: computed {computed} m, reference {reference_m} m"
);
}
#[test]
fn near_antipodal_pair_reproduces_its_reference() {
assert_reproduces(NEAR_ANTIPODAL, NEAR_ANTIPODAL_REFERENCE_M);
}
#[test]
fn pair_that_haversine_turned_into_nan_is_half_the_circumference() {
assert_reproduces(NAN_UNDER_HAVERSINE, HALF_CIRCUMFERENCE_M);
}
#[test]
fn exact_antipodes_on_the_equator_are_half_the_circumference() {
assert_reproduces((0.0, 0.0, 0.0, 180.0), HALF_CIRCUMFERENCE_M);
}
#[test]
fn pole_to_pole_is_half_the_circumference() {
assert_reproduces((90.0, 0.0, -90.0, 0.0), HALF_CIRCUMFERENCE_M);
}
#[test]
fn longitude_does_not_move_a_pole() {
assert_reproduces((90.0, 0.0, 90.0, 123.0), 0.0);
}
#[test]
fn coincident_points_are_exactly_zero_apart() {
let (lat, lng, _, _) = PARIS_LONDON;
assert_eq!(distance((lat, lng, lat, lng)).to_bits(), 0.0_f64.to_bits());
}
#[test]
fn distance_across_the_antimeridian_takes_the_short_way() {
assert_reproduces(ACROSS_ANTIMERIDIAN, ACROSS_ANTIMERIDIAN_REFERENCE_M);
}
#[test]
fn city_pairs_reproduce_their_references() {
assert_reproduces(PARIS_LONDON, PARIS_LONDON_REFERENCE_M);
assert_reproduces(NYC_LA, NYC_LA_REFERENCE_M);
}
#[test]
fn distance_is_symmetric() {
let (lat1, lng1, lat2, lng2) = PARIS_LONDON;
assert_eq!(
distance((lat1, lng1, lat2, lng2)).to_bits(),
distance((lat2, lng2, lat1, lng1)).to_bits()
);
}
#[test]
fn a_coordinate_off_the_globe_has_no_distance() {
let (lat1, lng1, lat2, lng2) = PARIS_LONDON;
for (lat, lng) in [
(f64::NAN, lng1),
(lat1, f64::NAN),
(90.5, lng1),
(lat1, -180.5),
] {
assert_eq!(
great_circle_distance_m(lat, lng, lat2, lng2),
None,
"({lat}, {lng})"
);
assert_eq!(
great_circle_distance_m(lat2, lng2, lat, lng),
None,
"({lat}, {lng})"
);
}
}
#[test]
fn equality_is_to_the_millimetre() {
let dist = distance(PARIS_LONDON);
let within = dist + 0.000_5;
let beyond = dist + 0.002;
assert!(distance_satisfies(Some(dist), DistanceOp::Eq, within));
assert!(!distance_satisfies(Some(dist), DistanceOp::NotEq, within));
assert!(!distance_satisfies(Some(dist), DistanceOp::Eq, beyond));
assert!(distance_satisfies(Some(dist), DistanceOp::NotEq, beyond));
}
#[test]
fn ordering_operators_compare_exactly() {
let dist = distance(PARIS_LONDON);
let just_above = dist + DISTANCE_EQUALITY_RESOLUTION_M / 2.0;
assert!(distance_satisfies(Some(dist), DistanceOp::Lt, just_above));
assert!(!distance_satisfies(Some(dist), DistanceOp::Gte, just_above));
}
#[test]
fn no_distance_matches_no_operator() {
for op in ALL_OPS {
assert!(!distance_satisfies(None, op, 0.0), "{op:?}");
}
}
#[test]
fn nan_threshold_matches_no_operator() {
let dist = distance(PARIS_LONDON);
for op in ALL_OPS {
assert!(!distance_satisfies(Some(dist), op, f64::NAN), "{op:?}");
}
}
fn chord_distance_m(lat1: f64, lng1: f64, lat2: f64, lng2: f64) -> f64 {
let unit = |lat: f64, lng: f64| {
let (sin_lat, cos_lat) = lat.to_radians().sin_cos();
let (sin_lng, cos_lng) = lng.to_radians().sin_cos();
[cos_lat * cos_lng, cos_lat * sin_lng, sin_lat]
};
let (a, b) = (unit(lat1, lng1), unit(lat2, lng2));
let norm = |combine: fn(f64, f64) -> f64| {
a.iter()
.zip(b)
.map(|(x, y)| combine(*x, y).powi(2))
.sum::<f64>()
.sqrt()
};
let direct = norm(|x, y| x - y);
let opposite = norm(|x, y| x + y);
if direct <= opposite {
EARTH_RADIUS_M * 2.0 * (direct / 2.0).asin()
} else {
EARTH_RADIUS_M * (PI - 2.0 * (opposite / 2.0).asin())
}
}
fn random_point(rng: &mut StdRng) -> (f64, f64) {
let lat = rng
.random_range(-1.0_f64..=1.0)
.asin()
.to_degrees()
.clamp(-90.0, 90.0);
let lng = rng.random_range(-180.0..=180.0);
(lat, lng)
}
fn near_antipode_of(rng: &mut StdRng, lat: f64, lng: f64) -> (f64, f64) {
const ANTIPODE_BAND_DEG: f64 = 1e-3;
let band = -ANTIPODE_BAND_DEG..=ANTIPODE_BAND_DEG;
let antipode_lat = (-lat + rng.random_range(band.clone())).clamp(-90.0, 90.0);
let mut antipode_lng = lng + 180.0 + rng.random_range(band);
if antipode_lng > 180.0 {
antipode_lng -= 360.0;
}
(antipode_lat, antipode_lng)
}
#[test]
#[ignore = "measurement: run with --ignored to re-measure the formula"]
fn sweep_agrees_with_the_chord_formulation() {
const SEED: u64 = 2299;
const PAIRS_PER_REGION: usize = 1_000_000;
let mut rng = StdRng::seed_from_u64(SEED);
for near_antipode in [false, true] {
let mut worst_gap_m = 0.0_f64;
let mut worst_pair = (0.0, 0.0, 0.0, 0.0);
for _ in 0..PAIRS_PER_REGION {
let (lat1, lng1) = random_point(&mut rng);
let (lat2, lng2) = if near_antipode {
near_antipode_of(&mut rng, lat1, lng1)
} else {
random_point(&mut rng)
};
let pair = (lat1, lng1, lat2, lng2);
let gap_m = (distance(pair) - chord_distance_m(lat1, lng1, lat2, lng2)).abs();
if gap_m > worst_gap_m {
worst_gap_m = gap_m;
worst_pair = pair;
}
}
println!(
"near_antipode={near_antipode}: {PAIRS_PER_REGION} pairs (seed {SEED}), \
worst gap {worst_gap_m:e} m at {worst_pair:?}"
);
assert!(
worst_gap_m < REFERENCE_AGREEMENT_M,
"worst gap {worst_gap_m} m at {worst_pair:?}"
);
}
}