Skip to main content

oxiproj_engine/
pipeline.rs

1//! Multi-step transformation pipeline.
2//!
3//! Ported from PROJ 9.8.0 `src/pipeline.cpp` (`pipeline_forward_4d` /
4//! `pipeline_reverse_4d`).
5
6use oxiproj_core::{Coord, Lp, Operation, ProjResult, Xy};
7
8/// A sequence of [`crate::pj::Pj`] steps applied in order (forward) or in
9/// reverse (inverse). The 4D methods are the true entry points; the 2D methods
10/// wrap through them.
11#[derive(Debug)]
12pub struct Pipeline {
13    /// The ordered steps. Each step honors its own `inverted` flag.
14    pub steps: Vec<crate::pj::Pj>,
15}
16
17/// Whether an intermediate pipeline coordinate must be treated as a failed
18/// transform.
19///
20/// PROJ's pipeline driver (`src/pipeline.cpp`) tests only the spatial `x`
21/// component against `HUGE_VAL` (`if (point.xyzt.x == HUGE_VAL)`); it never
22/// treats the time slot as an error marker. Mirror that here by checking only
23/// the spatial triple (`x`, `y`, `z`), NOT time: a non-finite `t`
24/// (`f64::INFINITY`) is the legitimate "unspecified epoch" sentinel that the
25/// 2D/3D entry points and `cct` pass by default, and `Coord::is_error` — whose
26/// all-slots sentinel flags *any* non-finite component — would otherwise
27/// spuriously abort every pipeline run with an unspecified time.
28fn spatial_is_error(point: &Coord) -> bool {
29    let v = point.v();
30    !v[0].is_finite() || !v[1].is_finite() || !v[2].is_finite()
31}
32
33impl Operation for Pipeline {
34    fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
35        // Open a fresh, isolated push/pop coordinate-stack frame for this
36        // pipeline traversal, mirroring PROJ giving every `Pipeline` object its
37        // own opaque `stack[4]` (`src/pipeline.cpp`). The guard lives for the
38        // whole loop and is dropped on return, discarding anything a `push`
39        // left behind that was never `pop`ped — so a push-without-pop stays
40        // scoped to this run (its value simply remains unrestored, exactly as
41        // PROJ prints) and never leaks to a later, unrelated pipeline run
42        // (e.g. a subsequent `pop`-from-empty-stack case) on the same thread.
43        // A nested pipeline step opens its own inner scope, keeping stacks
44        // isolated per pipeline object.
45        let _stack_scope = oxiproj_transformations::conversions::push_pop::enter_pipeline_scope();
46        let mut point = c;
47        for step in &self.steps {
48            if spatial_is_error(&point) {
49                return Ok(Coord::error());
50            }
51            if step.omit_fwd {
52                continue;
53            }
54            point = step.forward_4d(point)?;
55        }
56        Ok(point)
57    }
58
59    fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
60        // See `forward_4d`: isolate this traversal's push/pop stack frame.
61        let _stack_scope = oxiproj_transformations::conversions::push_pop::enter_pipeline_scope();
62        let mut point = c;
63        for step in self.steps.iter().rev() {
64            if spatial_is_error(&point) {
65                return Ok(Coord::error());
66            }
67            if step.omit_inv {
68                continue;
69            }
70            point = step.inverse_4d(point)?;
71        }
72        Ok(point)
73    }
74
75    fn forward_2d(&self, lp: Lp) -> ProjResult<Xy> {
76        let c = self.forward_4d(Coord::new(lp.lam, lp.phi, 0.0, 0.0))?;
77        Ok(c.xy())
78    }
79
80    fn inverse_2d(&self, xy: Xy) -> ProjResult<Lp> {
81        let c = self.inverse_4d(Coord::new(xy.x, xy.y, 0.0, 0.0))?;
82        Ok(c.lp())
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use oxiproj_core::DEG_TO_RAD;
90
91    #[test]
92    fn utm_forward_then_inverse_round_trips() {
93        // Two UTM zone-32 steps: the second inverted, so the pair is an
94        // identity in geographic space.
95        let step0 = crate::create::create("+proj=utm +zone=32 +ellps=WGS84").unwrap();
96        let mut step1 = crate::create::create("+proj=utm +zone=32 +ellps=WGS84").unwrap();
97        step1.inverted = true;
98        let pipe = Pipeline {
99            steps: vec![step0, step1],
100        };
101        let input = Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0);
102        let out = pipe.forward_4d(input).unwrap();
103        let o = out.v();
104        assert!((o[0] - 12.0 * DEG_TO_RAD).abs() < 1e-9, "lam got {}", o[0]);
105        assert!((o[1] - 55.0 * DEG_TO_RAD).abs() < 1e-9, "phi got {}", o[1]);
106    }
107}