oxiproj-engine 0.1.2

Proj-string parser, operation dispatch, and transformation pipelines for OxiProj.
Documentation
//! End-to-end `misc`-family differential tests exercised purely through the
//! `oxiproj-engine` public API (`create` plus forward/inverse `trans`). Each
//! case builds a globular/world projection from its PROJ string and runs a real
//! transform.
//!
//! All reference constants are HARDCODED so this test runs WITHOUT proj
//! installed (CI-safe). Forward checks compare against hardcoded PROJ reference
//! outputs within 1e-6. The van der Grinten (`vandg`) projection has an inverse,
//! so its case additionally round-trips (forward then inverse, recovering the
//! original geographic radians within 1e-9 rad). The remaining `misc`
//! projections are forward-only and their inverse is intentionally not
//! exercised.
//!
//! Each reference was regenerated with the exact proj command shown:
//! +proj=vandg +R=1
//! +proj=vandg2 +R=1
//! +proj=vandg3 +R=1
//! +proj=vandg4 +R=1
//! +proj=bacon +R=1
//! +proj=apian +R=1
//! +proj=ortel +R=1
//! +proj=nicol +R=1
//! +proj=larr +R=1
//! +proj=lask +R=1
//! +proj=august +R=1
//! +proj=gins8 +R=1

use oxiproj_core::{Coord, Direction, DEG_TO_RAD};
use oxiproj_engine::{create, trans};

/// Assert that `got` is within `tol` of `expected`, with a descriptive panic message.
fn assert_close(got: f64, expected: f64, tol: f64, what: &str) {
    let diff = (got - expected).abs();
    assert!(
        diff < tol,
        "{what}: got {got}, expected {expected}, abs diff {diff} >= tol {tol}",
    );
}

/// Create the projection from `proj`, run the forward at (`lon_deg`,`lat_deg`),
/// assert x/y against (`ex`,`ey`) within `fwd_tol`, then inverse round-trip the
/// forward result back to the original radians within 1e-9.
fn check_fwd_inv(proj: &str, lon_deg: f64, lat_deg: f64, ex: f64, ey: f64, fwd_tol: f64) {
    let lon_rad = lon_deg * DEG_TO_RAD;
    let lat_rad = lat_deg * DEG_TO_RAD;
    let pj = create(proj).unwrap_or_else(|e| panic!("create {proj}: {e:?}"));
    let fwd = trans(&pj, Direction::Fwd, Coord::new(lon_rad, lat_rad, 0.0, 0.0))
        .unwrap_or_else(|e| panic!("forward {proj}: {e:?}"));
    assert_close(fwd.v()[0], ex, fwd_tol, &format!("{proj} forward x"));
    assert_close(fwd.v()[1], ey, fwd_tol, &format!("{proj} forward y"));
    let inv = trans(&pj, Direction::Inv, fwd).unwrap_or_else(|e| panic!("inverse {proj}: {e:?}"));
    assert_close(inv.v()[0], lon_rad, 1e-9, &format!("{proj} inverse lon"));
    assert_close(inv.v()[1], lat_rad, 1e-9, &format!("{proj} inverse lat"));
}

/// Create the projection from `proj`, run the forward at (`lon_deg`,`lat_deg`),
/// and assert x/y against (`ex`,`ey`) within `fwd_tol`. For forward-only
/// projections (no inverse), so the inverse is intentionally not exercised.
fn check_fwd(proj: &str, lon_deg: f64, lat_deg: f64, ex: f64, ey: f64, fwd_tol: f64) {
    let lon_rad = lon_deg * DEG_TO_RAD;
    let lat_rad = lat_deg * DEG_TO_RAD;
    let pj = create(proj).unwrap_or_else(|e| panic!("create {proj}: {e:?}"));
    let fwd = trans(&pj, Direction::Fwd, Coord::new(lon_rad, lat_rad, 0.0, 0.0))
        .unwrap_or_else(|e| panic!("forward {proj}: {e:?}"));
    assert_close(fwd.v()[0], ex, fwd_tol, &format!("{proj} forward x"));
    assert_close(fwd.v()[1], ey, fwd_tol, &format!("{proj} forward y"));
}

#[test]
fn vandg_forward_and_inverse() {
    // van der Grinten has an inverse: forward within 1e-6, inverse round-trip within 1e-9.
    check_fwd_inv(
        "+proj=vandg +R=1",
        20.0,
        30.0,
        0.338871219566,
        0.540093568707,
        1e-6,
    );
    check_fwd_inv(
        "+proj=vandg +R=1",
        -120.0,
        -40.0,
        -1.997264619583,
        -0.807286748096,
        1e-6,
    );
}

#[test]
fn vandg2_forward() {
    check_fwd(
        "+proj=vandg2 +R=1",
        20.0,
        30.0,
        0.338667231749,
        0.545468319880,
        1e-6,
    );
    check_fwd(
        "+proj=vandg2 +R=1",
        -120.0,
        -40.0,
        -1.932092470873,
        -1.038468500057,
        1e-6,
    );
}

#[test]
fn vandg3_forward() {
    check_fwd(
        "+proj=vandg3 +R=1",
        20.0,
        30.0,
        0.338912020660,
        0.539012084453,
        1e-6,
    );
    check_fwd(
        "+proj=vandg3 +R=1",
        -120.0,
        -40.0,
        -2.013749455508,
        -0.736501042227,
        1e-6,
    );
}

#[test]
fn vandg4_forward() {
    check_fwd(
        "+proj=vandg4 +R=1",
        20.0,
        30.0,
        0.311665090508,
        0.525382626635,
        1e-6,
    );
    check_fwd(
        "+proj=vandg4 +R=1",
        -120.0,
        -40.0,
        -1.869031720677,
        -0.828680450024,
        1e-6,
    );
}

#[test]
fn bacon_forward() {
    check_fwd(
        "+proj=bacon +R=1",
        20.0,
        30.0,
        0.264952224901,
        std::f64::consts::FRAC_PI_4,
        1e-6,
    );
    check_fwd(
        "+proj=bacon +R=1",
        -120.0,
        -40.0,
        -1.745716730077,
        -1.009688416205,
        1e-6,
    );
}

#[test]
fn apian_forward() {
    check_fwd(
        "+proj=apian +R=1",
        20.0,
        30.0,
        0.311919918291,
        std::f64::consts::FRAC_PI_6,
        1e-6,
    );
    check_fwd(
        "+proj=apian +R=1",
        -120.0,
        -40.0,
        -1.937984618661,
        -0.698131700798,
        1e-6,
    );
}

#[test]
fn ortel_forward() {
    check_fwd(
        "+proj=ortel +R=1",
        20.0,
        30.0,
        0.311919918291,
        std::f64::consts::FRAC_PI_6,
        1e-6,
    );
    check_fwd(
        "+proj=ortel +R=1",
        -120.0,
        -40.0,
        -1.930728204156,
        -0.698131700798,
        1e-6,
    );
}

#[test]
fn nicol_forward() {
    check_fwd(
        "+proj=nicol +R=1",
        20.0,
        30.0,
        0.310022779015,
        0.536734059396,
        1e-6,
    );
    check_fwd(
        "+proj=nicol +R=1",
        -120.0,
        -40.0,
        -1.535924384770,
        -0.876536316796,
        1e-6,
    );
}

#[test]
fn larr_forward() {
    check_fwd(
        "+proj=larr +R=1",
        20.0,
        30.0,
        0.336954113463,
        0.542987990209,
        1e-6,
    );
    check_fwd(
        "+proj=larr +R=1",
        -120.0,
        -40.0,
        -1.963746368302,
        -0.790616231112,
        1e-6,
    );
}

#[test]
fn lask_forward() {
    check_fwd(
        "+proj=lask +R=1",
        20.0,
        30.0,
        0.327520128501,
        0.542640626452,
        1e-6,
    );
    check_fwd(
        "+proj=lask +R=1",
        -120.0,
        -40.0,
        -1.830244626164,
        -0.932593529443,
        1e-6,
    );
}

#[test]
fn august_forward() {
    check_fwd(
        "+proj=august +R=1",
        20.0,
        30.0,
        0.337740025853,
        0.550566293123,
        1e-6,
    );
    check_fwd(
        "+proj=august +R=1",
        -120.0,
        -40.0,
        -2.287820872740,
        -1.273715756799,
        1e-6,
    );
}

#[test]
fn gins8_forward() {
    check_fwd(
        "+proj=gins8 +R=1",
        20.0,
        30.0,
        0.290162539370,
        0.535561073700,
        1e-6,
    );
    check_fwd(
        "+proj=gins8 +R=1",
        -120.0,
        -40.0,
        -1.642566087164,
        -0.726486777780,
        1e-6,
    );
}