use oxiproj_core::{Coord, Direction, DEG_TO_RAD};
use oxiproj_engine::{create, parse, trans, Pj};
use proptest::prelude::*;
use proptest::test_runner::TestCaseError;
fn arb_plain_text() -> impl Strategy<Value = String> {
"\\PC{0,200}"
}
fn arb_token() -> impl Strategy<Value = String> {
let key = prop_oneof![
3 => "[a-zA-Z0-9_]{0,10}".prop_map(|s| s),
1 => Just("proj".to_string()),
1 => Just("ellps".to_string()),
1 => Just("datum".to_string()),
1 => Just("towgs84".to_string()),
1 => Just("pipeline".to_string()),
1 => Just("step".to_string()),
1 => Just("inv".to_string()),
1 => Just("axis".to_string()),
1 => Just("order".to_string()),
1 => Just("zone".to_string()),
1 => Just("lat_0".to_string()),
1 => Just("lon_0".to_string()),
1 => Just("nadgrids".to_string()),
1 => Just("units".to_string()),
];
let value = prop_oneof![
3 => "[a-zA-Z0-9_.,+-]{0,12}".prop_map(|s| s),
1 => Just("merc".to_string()),
1 => Just("utm".to_string()),
1 => Just("WGS84".to_string()),
1 => Just("pipeline".to_string()),
1 => (-400i64..400).prop_map(|n| n.to_string()),
];
(key, proptest::option::of(value)).prop_map(|(k, v)| match v {
Some(v) if !v.is_empty() => format!("+{k}={v}"),
_ => format!("+{k}"),
})
}
fn arb_proj_string() -> impl Strategy<Value = String> {
prop::collection::vec(arb_token(), 0..8).prop_map(|toks| toks.join(" "))
}
fn arb_any_proj_input() -> impl Strategy<Value = String> {
prop_oneof![3 => arb_proj_string(), 1 => arb_plain_text()]
}
proptest! {
#![proptest_config(ProptestConfig { cases: 96, .. ProptestConfig::default() })]
#[test]
fn parse_never_panics(s in arb_any_proj_input()) {
let _ = parse(&s);
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 256, .. ProptestConfig::default() })]
#[test]
fn create_never_panics(s in arb_any_proj_input()) {
let _ = create(&s);
}
}
fn assert_roundtrip(pj: &Pj, lon_deg: f64, lat_deg: f64, tol: f64) -> Result<(), TestCaseError> {
let lon_rad = lon_deg * DEG_TO_RAD;
let lat_rad = lat_deg * DEG_TO_RAD;
let input = Coord::new(lon_rad, lat_rad, 0.0, 0.0);
let fwd = trans(pj, Direction::Fwd, input).map_err(|e| {
TestCaseError::fail(format!("forward failed at ({lon_deg}, {lat_deg}): {e:?}"))
})?;
let back = trans(pj, Direction::Inv, fwd).map_err(|e| {
TestCaseError::fail(format!("inverse failed at ({lon_deg}, {lat_deg}): {e:?}"))
})?;
let dlon = (back.v()[0] - lon_rad).abs();
let dlat = (back.v()[1] - lat_rad).abs();
prop_assert!(
dlon < tol,
"longitude round-trip diff {dlon} >= {tol} at ({lon_deg}, {lat_deg})"
);
prop_assert!(
dlat < tol,
"latitude round-trip diff {dlat} >= {tol} at ({lon_deg}, {lat_deg})"
);
Ok(())
}
const ROUNDTRIP_TOL: f64 = 1e-7;
proptest! {
#![proptest_config(ProptestConfig { cases: 64, .. ProptestConfig::default() })]
#[test]
fn merc_roundtrip(lon_deg in -179.0f64..179.0, lat_deg in -80.0f64..80.0) {
let pj = create("+proj=merc +ellps=WGS84").expect("merc must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn tmerc_roundtrip(lon_deg in 3.0f64..15.0, lat_deg in -80.0f64..80.0) {
let pj = create("+proj=tmerc +lon_0=9 +ellps=WGS84").expect("tmerc must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn etmerc_roundtrip(lon_deg in -41.0f64..59.0, lat_deg in -80.0f64..80.0) {
let pj = create("+proj=etmerc +lon_0=9 +ellps=WGS84").expect("etmerc must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn utm_roundtrip(lon_deg in 3.0f64..15.0, lat_deg in -80.0f64..80.0) {
let pj = create("+proj=utm +zone=32 +ellps=WGS84").expect("utm must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn lcc_roundtrip(lon_deg in -125.0f64..-65.0, lat_deg in 20.0f64..60.0) {
let pj = create("+proj=lcc +lat_1=33 +lat_2=45 +lat_0=39 +lon_0=-96 +ellps=WGS84")
.expect("lcc must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn aea_roundtrip(lon_deg in -125.0f64..-65.0, lat_deg in 15.0f64..55.0) {
let pj = create("+proj=aea +lat_1=29.5 +lat_2=45.5 +lat_0=23 +lon_0=-96 +ellps=WGS84")
.expect("aea must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn stere_polar_roundtrip(lon_deg in -179.0f64..179.0, lat_deg in 10.0f64..89.0) {
let pj = create("+proj=stere +lat_0=90 +lon_0=0 +ellps=WGS84").expect("stere must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn laea_roundtrip(lon_deg in -50.0f64..70.0, lat_deg in 5.0f64..80.0) {
let pj = create("+proj=laea +lat_0=45 +lon_0=10 +ellps=WGS84").expect("laea must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn eqc_roundtrip(lon_deg in -179.0f64..179.0, lat_deg in -85.0f64..85.0) {
let pj = create("+proj=eqc +ellps=WGS84").expect("eqc must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn cea_roundtrip(lon_deg in -179.0f64..179.0, lat_deg in -85.0f64..85.0) {
let pj = create("+proj=cea +ellps=WGS84").expect("cea must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn moll_roundtrip(lon_deg in -179.0f64..179.0, lat_deg in -85.0f64..85.0) {
let pj = create("+proj=moll +ellps=WGS84").expect("moll must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
#[test]
fn sinu_roundtrip(lon_deg in -179.0f64..179.0, lat_deg in -85.0f64..85.0) {
let pj = create("+proj=sinu +ellps=WGS84").expect("sinu must construct");
assert_roundtrip(&pj, lon_deg, lat_deg, ROUNDTRIP_TOL)?;
}
}