Skip to main content

oxiproj_core/
operation.rs

1//! Core operation contract for OxiProj, ported from PROJ.
2//!
3//! PROJ models every coordinate operation as a C `struct PJ` (see
4//! `src/proj_internal.h`) that carries a set of forward/inverse function
5//! pointers — `fwd`, `inv`, `fwd3d`, `inv3d`, `fwd4d`, `inv4d` — together with
6//! the input/output unit enumeration `enum pj_io_units`. The dispatch logic in
7//! `src/fwd.cpp` (`pj_fwd`, `pj_fwd3d`, `pj_fwd4d`) and `src/inv.cpp`
8//! (`pj_inv`, `pj_inv3d`, `pj_inv4d`) implements an *arity cascade*: a higher
9//! dimensional call falls back to the lower dimensional function pointer when
10//! the higher one is absent, passing the extra coordinate slots through
11//! unchanged.
12//!
13//! This module replaces the raw function pointers with the safe, object-safe
14//! [`Operation`] trait. Its default method bodies reproduce PROJ's arity
15//! cascade (`forward_4d` → `forward_3d` → `forward_2d`, and the inverse
16//! mirror), and [`IoUnits`] mirrors `enum pj_io_units`.
17
18use crate::coord::{Coord, Lp, Lpz, Xy, Xyz};
19use crate::error::{ProjError, ProjResult};
20
21/// Input/output unit kind of an operation.
22///
23/// Ported from src/proj_internal.h (enum pj_io_units). The discriminants match
24/// PROJ's enumeration values so the two can be compared directly.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27#[repr(i32)]
28pub enum IoUnits {
29    /// Doesn't matter (or depends on pipeline neighbours)
30    Whatever = 0,
31    /// Scaled meters (right), projected system
32    Classic = 1,
33    /// Meters, projected system
34    Projected = 2,
35    /// Meters, 3D cartesian system
36    Cartesian = 3,
37    /// Radians
38    Radians = 4,
39    /// Degrees
40    Degrees = 5,
41}
42
43/// The core operation contract: a single coordinate operation that can be
44/// applied forward and (optionally) inverse, at 2D, 3D, or 4D arity.
45///
46/// This is the OxiProj equivalent of the forward/inverse function pointers on
47/// PROJ's `struct PJ` (`fwd`, `inv`, `fwd3d`, `inv3d`, `fwd4d`, `inv4d`), as
48/// declared in src/proj_internal.h and dispatched by src/fwd.cpp / src/inv.cpp.
49///
50/// The trait requires `Debug + Send + Sync` so that a boxed operation,
51/// `Box<dyn Operation>`, is itself `Debug + Send + Sync`: the transformation
52/// engine stores operations as boxed trait objects and must be able to print
53/// and share them across threads.
54///
55/// The trait is object-safe: every method takes `&self` and none is generic,
56/// so `Box<dyn Operation>` (and `&dyn Operation`) are valid types.
57pub trait Operation: core::fmt::Debug + Send + Sync {
58    /// Apply the 2D forward operation, mapping geodetic [`Lp`] to projected [`Xy`].
59    ///
60    /// The default returns `Err(ProjError::InvalidOp)`, meaning "this operation
61    /// does not provide a 2D forward".
62    ///
63    /// Rationale for choosing [`ProjError::InvalidOp`] rather than
64    /// [`ProjError::NoInverseOp`]: `NoInverseOp` (PROJ's
65    /// `PROJ_ERR_OTHER_NO_INVERSE_OP`) is semantically about *inverse*
66    /// operations, so it is the wrong signal for a missing *forward* direction.
67    /// `InvalidOp` (PROJ's `PROJ_ERR_INVALID_OP`) is the correct "no operation
68    /// of this kind is set up" indication, matching how PROJ's `pj_fwd` treats a
69    /// null forward function pointer as a setup/operation error rather than an
70    /// inverse error.
71    fn forward_2d(&self, lp: Lp) -> ProjResult<Xy> {
72        let _ = lp;
73        Err(ProjError::InvalidOp)
74    }
75
76    /// Apply the 2D inverse operation, mapping projected [`Xy`] back to geodetic [`Lp`].
77    ///
78    /// The default returns `Err(ProjError::NoInverseOp)`, meaning no inverse is
79    /// available for this operation.
80    fn inverse_2d(&self, xy: Xy) -> ProjResult<Lp> {
81        let _ = xy;
82        Err(ProjError::NoInverseOp)
83    }
84
85    /// Apply the 3D forward operation, mapping geodetic [`Lpz`] to Cartesian [`Xyz`].
86    ///
87    /// The default delegates to [`forward_2d`](Operation::forward_2d) on the
88    /// horizontal components and passes the vertical `z` slot through
89    /// unchanged, mirroring PROJ's `pj_fwd3d` cascade in src/fwd.cpp.
90    fn forward_3d(&self, lpz: Lpz) -> ProjResult<Xyz> {
91        let xy = self.forward_2d(Lp::new(lpz.lam, lpz.phi))?;
92        Ok(Xyz::new(xy.x, xy.y, lpz.z))
93    }
94
95    /// Apply the 3D inverse operation, mapping Cartesian [`Xyz`] back to geodetic [`Lpz`].
96    ///
97    /// The default delegates to [`inverse_2d`](Operation::inverse_2d) on the
98    /// horizontal components and passes the vertical `z` slot through
99    /// unchanged, mirroring PROJ's `pj_inv3d` cascade in src/inv.cpp.
100    fn inverse_3d(&self, xyz: Xyz) -> ProjResult<Lpz> {
101        let lp = self.inverse_2d(Xy::new(xyz.x, xyz.y))?;
102        Ok(Lpz::new(lp.lam, lp.phi, xyz.z))
103    }
104
105    /// Apply the 4D forward operation on a full [`Coord`] (with time).
106    ///
107    /// The default delegates to [`forward_3d`](Operation::forward_3d) on the
108    /// spatial components and passes the time `t` slot through unchanged,
109    /// mirroring PROJ's `pj_fwd4d` cascade in src/fwd.cpp.
110    fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
111        let lpz = c.lpz();
112        let t = c.v()[3];
113        let xyz = self.forward_3d(lpz)?;
114        Ok(Coord::new(xyz.x, xyz.y, xyz.z, t))
115    }
116
117    /// Apply the 4D inverse operation on a full [`Coord`] (with time).
118    ///
119    /// The default delegates to [`inverse_3d`](Operation::inverse_3d) on the
120    /// spatial components and passes the time `t` slot through unchanged,
121    /// mirroring PROJ's `pj_inv4d` cascade in src/inv.cpp.
122    fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
123        let xyz = c.xyz();
124        let t = c.v()[3];
125        let lpz = self.inverse_3d(xyz)?;
126        Ok(Coord::new(lpz.lam, lpz.phi, lpz.z, t))
127    }
128
129    /// Report whether an inverse operation is available (default `true`).
130    fn has_inverse(&self) -> bool {
131        true
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    #[cfg(feature = "no_std")]
139    use alloc::boxed::Box;
140
141    fn approx(a: f64, b: f64) -> bool {
142        (a - b).abs() < 1e-12
143    }
144
145    #[derive(Debug)]
146    struct Shift;
147
148    impl Operation for Shift {
149        fn forward_2d(&self, lp: Lp) -> ProjResult<Xy> {
150            Ok(Xy::new(lp.lam + 1.0, lp.phi + 2.0))
151        }
152        fn inverse_2d(&self, xy: Xy) -> ProjResult<Lp> {
153            Ok(Lp::new(xy.x - 1.0, xy.y - 2.0))
154        }
155    }
156
157    #[test]
158    fn shift_forward_3d_passes_z_through() -> ProjResult<()> {
159        let op = Shift;
160        let result = op.forward_3d(Lpz::new(0.1, 0.2, 99.0))?;
161        assert!(approx(result.z, 99.0));
162        assert!(approx(result.x, 1.1));
163        assert!(approx(result.y, 2.2));
164        Ok(())
165    }
166
167    #[test]
168    fn shift_forward_4d_passes_z_and_t_through() -> ProjResult<()> {
169        let op = Shift;
170        let result = op.forward_4d(Coord::new(0.1, 0.2, 99.0, 2020.0))?;
171        assert!(approx(result.v()[2], 99.0));
172        assert!(approx(result.v()[3], 2020.0));
173        assert!(approx(result.v()[0], 1.1));
174        assert!(approx(result.v()[1], 2.2));
175        Ok(())
176    }
177
178    #[test]
179    fn shift_inverse_4d_round_trips() -> ProjResult<()> {
180        let op = Shift;
181        let c = Coord::new(0.1, 0.2, 99.0, 2020.0);
182        let f = op.forward_4d(c)?;
183        let r = op.inverse_4d(f)?;
184        assert!(approx(r.v()[0], 0.1));
185        assert!(approx(r.v()[1], 0.2));
186        assert!(approx(r.v()[2], 99.0));
187        assert!(approx(r.v()[3], 2020.0));
188        Ok(())
189    }
190
191    #[derive(Debug)]
192    struct Identity4d;
193
194    impl Operation for Identity4d {
195        fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
196            Ok(c)
197        }
198        fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
199            Ok(c)
200        }
201        fn has_inverse(&self) -> bool {
202            true
203        }
204    }
205
206    #[test]
207    fn identity4d_forward_works_directly() -> ProjResult<()> {
208        let op = Identity4d;
209        let result = op.forward_4d(Coord::new(5.0, 6.0, 7.0, 8.0))?;
210        assert!(approx(result.v()[0], 5.0));
211        assert!(approx(result.v()[1], 6.0));
212        assert!(approx(result.v()[2], 7.0));
213        assert!(approx(result.v()[3], 8.0));
214        Ok(())
215    }
216
217    #[test]
218    fn identity4d_forward_2d_returns_default_invalid_op() {
219        let op = Identity4d;
220        assert!(op.forward_2d(Lp::new(0.0, 0.0)) == Err(ProjError::InvalidOp));
221    }
222
223    #[test]
224    fn identity4d_has_inverse_true() {
225        let op = Identity4d;
226        assert!(op.has_inverse());
227    }
228
229    #[derive(Debug)]
230    struct NoInverse;
231
232    impl Operation for NoInverse {
233        fn forward_2d(&self, lp: Lp) -> ProjResult<Xy> {
234            Ok(Xy::new(lp.lam, lp.phi))
235        }
236        fn has_inverse(&self) -> bool {
237            false
238        }
239    }
240
241    #[test]
242    fn no_inverse_reports_false() {
243        assert!(!NoInverse.has_inverse());
244        assert!(NoInverse.inverse_2d(Xy::new(0.0, 0.0)) == Err(ProjError::NoInverseOp));
245    }
246
247    #[test]
248    fn io_units_discriminants() {
249        assert!(IoUnits::Whatever as i32 == 0);
250        assert!(IoUnits::Classic as i32 == 1);
251        assert!(IoUnits::Projected as i32 == 2);
252        assert!(IoUnits::Cartesian as i32 == 3);
253        assert!(IoUnits::Radians as i32 == 4);
254        assert!(IoUnits::Degrees as i32 == 5);
255    }
256
257    #[test]
258    fn operation_is_object_safe() -> ProjResult<()> {
259        let b: Box<dyn Operation> = Box::new(Shift);
260        let xy = b.forward_2d(Lp::new(0.0, 0.0))?;
261        assert!(approx(xy.x, 1.0));
262        assert!(approx(xy.y, 2.0));
263        Ok(())
264    }
265}