apsis-rs/crates/physics/apsis-geodesy/tests/geodetic.rs

151 lines
5.4 KiB
Rust

//! Tests for geodetic and Earth-centered, Earth-fixed conversions.
use apsis_geodesy::{Ecef, Geodetic, WGS84};
use apsis_math::Vec3;
/// Tolerance for position comparisons, in meters.
const POSITION_TOL: f64 = 1e-4;
/// Tolerance for latitude and longitude comparisons, in radians.
///
/// One microdegree is about 1.1e-8 radians, or roughly a centimeter on the
/// Earth's surface.
const ANGLE_TOL: f64 = 1e-11;
#[test]
fn equatorial_point_lies_on_the_semi_major_axis() {
let point = Geodetic::new(0.0, 0.0, 0.0).to_ecef(WGS84);
assert!((point.position().x - WGS84.semi_major_axis()).abs() < POSITION_TOL);
assert!(point.position().y.abs() < POSITION_TOL);
assert!(point.position().z.abs() < POSITION_TOL);
}
#[test]
fn pole_lies_on_the_semi_minor_axis() {
let point = Geodetic::new(core::f64::consts::FRAC_PI_2, 0.0, 0.0).to_ecef(WGS84);
assert!(point.position().x.abs() < POSITION_TOL);
assert!(point.position().y.abs() < POSITION_TOL);
assert!((point.position().z - WGS84.semi_minor_axis()).abs() < POSITION_TOL);
}
#[test]
fn longitude_quarter_turn_places_the_point_on_the_second_axis() {
let point = Geodetic::new(0.0, core::f64::consts::FRAC_PI_2, 0.0).to_ecef(WGS84);
assert!(point.position().x.abs() < POSITION_TOL);
assert!((point.position().y - WGS84.semi_major_axis()).abs() < POSITION_TOL);
}
#[test]
fn height_moves_the_point_along_the_ellipsoid_normal() {
let surface = Geodetic::new(0.6, 0.3, 0.0).to_ecef(WGS84);
let elevated = Geodetic::new(0.6, 0.3, 500.0).to_ecef(WGS84);
assert!((surface.position().distance(elevated.position()) - 500.0).abs() < POSITION_TOL);
// The normal at a non-polar latitude is not vertical, so the height
// changes the horizontal components as well as the third one.
assert!(elevated.position().x > surface.position().x);
}
#[test]
fn conversion_round_trips_over_a_global_grid() {
for latitude_degrees in [-89.9_f64, -60.0, -45.0, 0.0, 30.0, 45.0, 60.0, 89.9] {
for longitude_degrees in [-179.0_f64, -90.0, 0.0, 45.0, 90.0, 179.0] {
for height in [-1_000.0_f64, 0.0, 1_000.0, 35_786_000.0] {
let original = Geodetic::new(
latitude_degrees.to_radians(),
longitude_degrees.to_radians(),
height,
);
let ecef = original.to_ecef(WGS84);
let recovered = ecef
.to_geodetic(WGS84)
.expect("off-axis points have a longitude");
assert!(
(recovered.latitude - original.latitude).abs() < ANGLE_TOL,
"latitude mismatch at {latitude_degrees}, {longitude_degrees}, {height}"
);
assert!(
(recovered.longitude - original.longitude).abs() < ANGLE_TOL,
"longitude mismatch at {latitude_degrees}, {longitude_degrees}, {height}"
);
assert!(
(recovered.height - original.height).abs() < POSITION_TOL,
"height mismatch at {latitude_degrees}, {longitude_degrees}, {height}: {} vs {}",
recovered.height,
original.height
);
}
}
}
}
#[test]
fn round_trip_preserves_the_cartesian_position() {
let original = Geodetic::new(0.921, -1.234, 12_345.0).to_ecef(WGS84);
let recovered = original
.to_geodetic(WGS84)
.expect("off-axis points have a longitude")
.to_ecef(WGS84);
assert!(original.position().distance(recovered.position()) < POSITION_TOL);
}
#[test]
fn polar_axis_has_no_defined_longitude() {
let on_axis = Ecef::new(Vec3::new(0.0, 0.0, WGS84.semi_minor_axis()));
assert_eq!(on_axis.to_geodetic(WGS84), None);
// The origin is also on the axis.
assert_eq!(Ecef::new(Vec3::ZERO).to_geodetic(WGS84), None);
}
#[test]
fn non_finite_positions_are_rejected() {
let invalid = Ecef::new(Vec3::new(f64::NAN, 0.0, 6_000_000.0));
assert_eq!(invalid.to_geodetic(WGS84), None);
let infinite = Ecef::new(Vec3::new(f64::INFINITY, 0.0, 0.0));
assert_eq!(infinite.to_geodetic(WGS84), None);
}
#[test]
fn known_point_matches_an_independent_computation() {
// Latitude 37.5 degrees north, longitude 121.333333... degrees west, at
// zero height, evaluated with the defining WGS 84 parameters in
// extended precision.
let point = Geodetic::new(
37.5_f64.to_radians(),
-121.333_333_333_333_33_f64.to_radians(),
0.0,
)
.to_ecef(WGS84);
assert!(
(point.position().x + 2_634_612.181_141_605_6).abs() < 1e-6,
"x = {}",
point.position().x
);
assert!(
(point.position().y + 4_327_502.253_728_065).abs() < 1e-6,
"y = {}",
point.position().y
);
assert!(
(point.position().z - 3_861_564.098_809_606_4).abs() < 1e-6,
"z = {}",
point.position().z
);
}
#[test]
fn validity_is_reported() {
assert!(Geodetic::new(0.5, 0.5, 100.0).is_valid());
assert!(Geodetic::new(core::f64::consts::FRAC_PI_2, 0.0, 0.0).is_valid());
assert!(!Geodetic::new(2.0, 0.0, 0.0).is_valid());
assert!(!Geodetic::new(0.0, 4.0, 0.0).is_valid());
assert!(!Geodetic::new(0.0, 0.0, f64::NAN).is_valid());
}
#[test]
fn ecef_wrapper_exposes_its_vector() {
let vector = Vec3::new(1.0, 2.0, 3.0);
let ecef = Ecef::new(vector);
assert_eq!(ecef.position(), vector);
assert_eq!(ecef, Ecef(vector));
}