oxiproj-transformations 0.1.2

Datum transformations and coordinate conversions for OxiProj.
Documentation
#![forbid(unsafe_code)]
//! `oxiproj-transformations` — datum transformations and coordinate conversions
//! ported from PROJ 9.8.0 `src/conversions/` and `src/transformations/`.
//!
//! Each operation implements [`oxiproj_core::Operation`] as a 4D transform,
//! mirroring the `oxiproj-projections` crate's build/param pattern. This crate
//! depends on `oxiproj-core` and `oxiproj-grids`.

pub mod batch_helmert;
pub mod conversions;
mod internal;
pub mod transformations;

use oxiproj_core::{Ellipsoid, IoUnits, Operation, ProjError, ProjResult};

// Re-export from `oxiproj-core` so that `oxiproj-grids` can implement it
// without a circular dependency. All existing users of
// `oxiproj_transformations::GridRegistry` continue to work unchanged.
pub use oxiproj_core::GridRegistry;

pub use transformations::helmert_batch;

// ---- Epoch-aware coordinate propagation (T3.1 foundation / v0.1.1) ----
pub mod epoch_transform;
pub use epoch_transform::{epoch_path, plate_for_frame, propagate_epoch};

// ---- Covariance/uncertainty propagation through transforms (T7.5 / v0.1.1) ----
pub mod covariance;
pub use covariance::{
    propagate_through_affine_2d, propagate_through_helmert_3, propagate_through_helmert_7_xy,
};

pub mod frame_chain;
pub use frame_chain::{
    apply_frame_transform, find_frame_transform, FrameTransform, FRAME_TRANSFORMS,
};

// ---- Epoch-aware coordinate transformation pipeline (T3.1 / v0.1.1) ----
pub mod epoch_pipeline;
pub use epoch_pipeline::{transform_epoch_aware, EpochAwareResult};

/// A trait the engine implements to give a transformation access to its specific params.
/// Mirrors `oxiproj_projections::ProjParamLookup`.
pub trait TransParamLookup {
    /// Look up a key whose value is an angle, returning radians (parsed via PROJ `dmstor`).
    fn get_dms(&self, key: &str) -> Option<f64>;
    /// Look up a key whose value is a plain floating-point number.
    fn get_f64(&self, key: &str) -> Option<f64>;
    /// Look up a key whose value is an integer.
    fn get_int(&self, key: &str) -> Option<i64>;
    /// Look up a key whose value is a string.
    fn get_str(&self, key: &str) -> Option<&str>;
    /// Return whether a boolean flag is set (bare `+key` or `+key=t/T/true`).
    fn get_bool(&self, key: &str) -> bool;
    /// Return whether the key is present at all.
    fn exists(&self, key: &str) -> bool;
}

/// Parameters the engine has parsed and hands to a transformation constructor.
pub struct TransParams<'a> {
    /// The ellipsoid the transformation operates on.
    pub ellipsoid: &'a Ellipsoid,
    /// Access to the transformation-specific parameters.
    pub params: &'a dyn TransParamLookup,
    /// Optional grid registry for grid-based transforms.
    pub registry: Option<&'a dyn GridRegistry>,
}

/// The constructor result: a boxed operation plus engine-facing unit metadata.
pub struct TransBuild {
    /// The boxed operation.
    pub operation: Box<dyn Operation>,
    /// Input units of the operation (left side of the pipeline).
    pub left: IoUnits,
    /// Output units of the operation (right side of the pipeline).
    pub right: IoUnits,
}

impl TransBuild {
    /// Build a `TransBuild` from an operation and its left/right units.
    pub fn new(operation: Box<dyn Operation>, left: IoUnits, right: IoUnits) -> TransBuild {
        TransBuild {
            operation,
            left,
            right,
        }
    }
}

/// Build a transformation/conversion by its PROJ name. Mirrors the projections
/// crate's `build`, chaining the per-group `build` functions.
pub fn build(name: &str, p: &TransParams) -> ProjResult<TransBuild> {
    conversions::build(name, p)
        .or_else(|| transformations::build(name, p))
        .unwrap_or(Err(ProjError::InvalidOp))
}

#[cfg(test)]
mod tests {
    use super::*;
    use oxiproj_core::Ellipsoid;

    struct NoParams;
    impl TransParamLookup for NoParams {
        fn get_dms(&self, _key: &str) -> Option<f64> {
            None
        }
        fn get_f64(&self, _key: &str) -> Option<f64> {
            None
        }
        fn get_int(&self, _key: &str) -> Option<i64> {
            None
        }
        fn get_str(&self, _key: &str) -> Option<&str> {
            None
        }
        fn get_bool(&self, _key: &str) -> bool {
            false
        }
        fn exists(&self, _key: &str) -> bool {
            false
        }
    }

    fn wgs84() -> Ellipsoid {
        Ellipsoid::named("WGS84").unwrap()
    }

    #[test]
    fn build_noop_succeeds() {
        let ell = wgs84();
        let np = NoParams;
        let pp = TransParams {
            ellipsoid: &ell,
            params: &np,
            registry: None,
        };
        assert!(build("noop", &pp).is_ok());
    }

    #[test]
    fn build_unknown_is_invalid_op() {
        let ell = wgs84();
        let np = NoParams;
        let pp = TransParams {
            ellipsoid: &ell,
            params: &np,
            registry: None,
        };
        assert_eq!(build("nonexistent", &pp).err(), Some(ProjError::InvalidOp));
    }
}