oxiproj-engine 0.1.2

Proj-string parser, operation dispatch, and transformation pipelines for OxiProj.
Documentation
//! Multi-step transformation pipeline.
//!
//! Ported from PROJ 9.8.0 `src/pipeline.cpp` (`pipeline_forward_4d` /
//! `pipeline_reverse_4d`).

use oxiproj_core::{Coord, Lp, Operation, ProjResult, Xy};

/// A sequence of [`crate::pj::Pj`] steps applied in order (forward) or in
/// reverse (inverse). The 4D methods are the true entry points; the 2D methods
/// wrap through them.
#[derive(Debug)]
pub struct Pipeline {
    /// The ordered steps. Each step honors its own `inverted` flag.
    pub steps: Vec<crate::pj::Pj>,
}

/// Whether an intermediate pipeline coordinate must be treated as a failed
/// transform.
///
/// PROJ's pipeline driver (`src/pipeline.cpp`) tests only the spatial `x`
/// component against `HUGE_VAL` (`if (point.xyzt.x == HUGE_VAL)`); it never
/// treats the time slot as an error marker. Mirror that here by checking only
/// the spatial triple (`x`, `y`, `z`), NOT time: a non-finite `t`
/// (`f64::INFINITY`) is the legitimate "unspecified epoch" sentinel that the
/// 2D/3D entry points and `cct` pass by default, and `Coord::is_error` — whose
/// all-slots sentinel flags *any* non-finite component — would otherwise
/// spuriously abort every pipeline run with an unspecified time.
fn spatial_is_error(point: &Coord) -> bool {
    let v = point.v();
    !v[0].is_finite() || !v[1].is_finite() || !v[2].is_finite()
}

impl Operation for Pipeline {
    fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
        // Open a fresh, isolated push/pop coordinate-stack frame for this
        // pipeline traversal, mirroring PROJ giving every `Pipeline` object its
        // own opaque `stack[4]` (`src/pipeline.cpp`). The guard lives for the
        // whole loop and is dropped on return, discarding anything a `push`
        // left behind that was never `pop`ped — so a push-without-pop stays
        // scoped to this run (its value simply remains unrestored, exactly as
        // PROJ prints) and never leaks to a later, unrelated pipeline run
        // (e.g. a subsequent `pop`-from-empty-stack case) on the same thread.
        // A nested pipeline step opens its own inner scope, keeping stacks
        // isolated per pipeline object.
        let _stack_scope = oxiproj_transformations::conversions::push_pop::enter_pipeline_scope();
        let mut point = c;
        for step in &self.steps {
            if spatial_is_error(&point) {
                return Ok(Coord::error());
            }
            if step.omit_fwd {
                continue;
            }
            point = step.forward_4d(point)?;
        }
        Ok(point)
    }

    fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
        // See `forward_4d`: isolate this traversal's push/pop stack frame.
        let _stack_scope = oxiproj_transformations::conversions::push_pop::enter_pipeline_scope();
        let mut point = c;
        for step in self.steps.iter().rev() {
            if spatial_is_error(&point) {
                return Ok(Coord::error());
            }
            if step.omit_inv {
                continue;
            }
            point = step.inverse_4d(point)?;
        }
        Ok(point)
    }

    fn forward_2d(&self, lp: Lp) -> ProjResult<Xy> {
        let c = self.forward_4d(Coord::new(lp.lam, lp.phi, 0.0, 0.0))?;
        Ok(c.xy())
    }

    fn inverse_2d(&self, xy: Xy) -> ProjResult<Lp> {
        let c = self.inverse_4d(Coord::new(xy.x, xy.y, 0.0, 0.0))?;
        Ok(c.lp())
    }
}

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

    #[test]
    fn utm_forward_then_inverse_round_trips() {
        // Two UTM zone-32 steps: the second inverted, so the pair is an
        // identity in geographic space.
        let step0 = crate::create::create("+proj=utm +zone=32 +ellps=WGS84").unwrap();
        let mut step1 = crate::create::create("+proj=utm +zone=32 +ellps=WGS84").unwrap();
        step1.inverted = true;
        let pipe = Pipeline {
            steps: vec![step0, step1],
        };
        let input = Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0);
        let out = pipe.forward_4d(input).unwrap();
        let o = out.v();
        assert!((o[0] - 12.0 * DEG_TO_RAD).abs() < 1e-9, "lam got {}", o[0]);
        assert!((o[1] - 55.0 * DEG_TO_RAD).abs() < 1e-9, "phi got {}", o[1]);
    }
}