oxiproj-engine 0.1.2

Proj-string parser, operation dispatch, and transformation pipelines for OxiProj.
Documentation
//! Integration tests for M17B: single-op datum-shift injection.
//!
//! These tests exercise the automatic pipeline injection that occurs when
//! `+towgs84=...` is present in a proj-string (or implied via `+datum=<name>`).
//! The engine should transparently build a cart→helmert→cart_inv→proj pipeline.

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 identifying `what` along with the actual values and absolute diff.
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}",
    );
}

#[test]
fn towgs84_zero_creates_successfully() {
    // A no-op shift (all zeros) should build successfully and be treated as a
    // plain single op (the datum-shift pipeline is NOT injected for all-zero shifts).
    let pj =
        create("+proj=tmerc +ellps=WGS84 +towgs84=0,0,0").expect("create tmerc with zero towgs84");
    let c = Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0);
    let out = trans(&pj, Direction::Fwd, c).expect("forward transform");
    let o = out.v();

    // Should match a plain tmerc with no towgs84 (zero shift = identity datum)
    let pj2 = create("+proj=tmerc +ellps=WGS84").expect("create plain tmerc");
    let out2 = trans(&pj2, Direction::Fwd, c).expect("forward transform (plain)");
    let o2 = out2.v();

    assert_close(o[0], o2[0], 1e-3, "x: zero-towgs84 vs plain tmerc");
    assert_close(o[1], o2[1], 1e-3, "y: zero-towgs84 vs plain tmerc");
}

#[test]
fn towgs84_three_param_round_trip() {
    // Non-zero 3-param shift creates a datum-shift pipeline.
    // The round-trip inverse should recover the original coordinate.
    let pj = create("+proj=latlong +ellps=GRS80 +towgs84=-199.87,74.79,246.62")
        .expect("create latlong with 3-param towgs84");
    let input = Coord::new(20.0 * DEG_TO_RAD, 37.0 * DEG_TO_RAD, 0.0, 0.0);

    let fwd = trans(&pj, Direction::Fwd, input).expect("forward datum-shifted latlong");
    let inv = trans(&pj, Direction::Inv, fwd).expect("inverse datum-shifted latlong");
    let i = inv.v();

    // Round-trip should recover original lat/lon within 1e-9 radians
    assert_close(i[0], 20.0 * DEG_TO_RAD, 1e-9, "lam round-trip");
    assert_close(i[1], 37.0 * DEG_TO_RAD, 1e-9, "phi round-trip");
}

#[test]
fn datum_ggrs87_builds_successfully() {
    // +datum=GGRS87 carries towgs84=-199.87,74.79,246.62 and ellipse_id=GRS80.
    // It should build and give finite output (datum shift applied).
    let pj = create("+proj=latlong +datum=GGRS87").expect("create latlong +datum=GGRS87");
    let input = Coord::new(20.0 * DEG_TO_RAD, 37.0 * DEG_TO_RAD, 0.0, 0.0);

    let fwd = trans(&pj, Direction::Fwd, input).expect("forward GGRS87");
    let f = fwd.v();

    // The forward output must be finite (ECEF→helmert→latlong chain worked)
    assert!(f[0].is_finite(), "lon must be finite, got {}", f[0]);
    assert!(f[1].is_finite(), "lat must be finite, got {}", f[1]);

    // GGRS87 has a substantial shift (-199.87, 74.79, 246.62 m); the output
    // should differ from the raw input (which was in GRS80/WGS84 space).
    // We just verify the round-trip is self-consistent.
    let inv = trans(&pj, Direction::Inv, fwd).expect("inverse GGRS87");
    let i = inv.v();
    assert_close(i[0], 20.0 * DEG_TO_RAD, 1e-9, "GGRS87 round-trip lam");
    assert_close(i[1], 37.0 * DEG_TO_RAD, 1e-9, "GGRS87 round-trip phi");
}

#[test]
fn towgs84_seven_param_round_trip() {
    // 7-param (Bursa-Wolf) shift: OSGB36 parameters.
    // The round-trip inverse should recover the original geographic coordinate.
    let pj = create(
        "+proj=tmerc +ellps=airy +towgs84=446.448,-125.157,542.060,0.1502,0.2470,0.8421,-20.4894",
    )
    .expect("create tmerc with 7-param towgs84 (Airy/OSGB36)");

    let input = Coord::new((-2.0_f64).to_radians(), 54.0_f64.to_radians(), 0.0, 0.0);
    let fwd = trans(&pj, Direction::Fwd, input).expect("forward 7-param");
    let inv = trans(&pj, Direction::Inv, fwd).expect("inverse 7-param");
    let i = inv.v();

    assert_close(
        i[0],
        (-2.0_f64).to_radians(),
        1e-6,
        "lam 7-param round-trip",
    );
    assert_close(i[1], 54.0_f64.to_radians(), 1e-6, "phi 7-param round-trip");
}