oxiproj-engine 0.1.0

Proj-string parser, operation dispatch, and transformation pipelines for OxiProj.
Documentation
//! Transformation context: error state, algorithm-selection knobs, and grid registry.
//!
//! Ported in spirit from PROJ 9.8.0 `src/4D_api.cpp` (`PJ_CONTEXT`).

use std::collections::HashMap;

use oxiproj_core::ProjError;
use oxiproj_transformations::GridRegistry;

/// Algorithm selection for transverse Mercator, mirroring PROJ's tmerc `algo` choice.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TmercAlgo {
    /// Pick automatically. AUTO defaults to [`TmercAlgo::PoderEngsager`] for
    /// ellipsoidal tmerc here, matching PROJ's preference for the exact series.
    Auto,
    /// Evenden/Snyder approximate series (PROJ `approx`).
    EvendenSnyder,
    /// Poder/Engsager exact series (PROJ `exact`, the default for ellipsoids).
    PoderEngsager,
}

/// A lightweight transformation context.
///
/// Holds the last error encountered, the transverse-Mercator algorithm
/// preference, and an in-memory registry of raw grid file bytes indexed by name.
#[derive(Debug, Clone)]
pub struct Context {
    /// The most recent error recorded on this context, if any.
    pub last_error: Option<ProjError>,
    /// Preferred transverse-Mercator algorithm.
    pub tmerc_algo: TmercAlgo,
    /// In-memory grid data indexed by grid name.
    grid_data: HashMap<String, Vec<u8>>,
}

impl Context {
    /// Create a fresh context with no error, `TmercAlgo::Auto`, and an empty grid registry.
    pub fn new() -> Context {
        Context {
            last_error: None,
            tmerc_algo: TmercAlgo::Auto,
            grid_data: HashMap::new(),
        }
    }

    /// Register raw grid bytes under `name`.
    ///
    /// Subsequent calls to `get_grid(name)` (via [`GridRegistry`]) will return a
    /// reference to these bytes.  Calling this twice with the same `name` replaces
    /// the previous data.
    pub fn register_grid(&mut self, name: impl Into<String>, data: Vec<u8>) {
        self.grid_data.insert(name.into(), data);
    }
}

impl GridRegistry for Context {
    fn get_grid(&self, name: &str) -> Option<&[u8]> {
        self.grid_data.get(name).map(|v| v.as_slice())
    }
}

impl Default for Context {
    fn default() -> Self {
        Context::new()
    }
}

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

    #[test]
    fn new_has_no_error() {
        let ctx = Context::new();
        assert!(ctx.last_error.is_none());
        assert_eq!(ctx.tmerc_algo, TmercAlgo::Auto);
    }

    #[test]
    fn default_matches_new() {
        let a = Context::default();
        let b = Context::new();
        assert!(a.last_error.is_none() && b.last_error.is_none());
        assert_eq!(a.tmerc_algo, b.tmerc_algo);
    }

    #[test]
    fn register_and_get_grid() {
        let mut ctx = Context::new();
        ctx.register_grid("test.gsb", vec![1u8, 2, 3]);
        let data = oxiproj_transformations::GridRegistry::get_grid(&ctx, "test.gsb");
        assert_eq!(data, Some([1u8, 2, 3].as_ref()));
        assert!(oxiproj_transformations::GridRegistry::get_grid(&ctx, "missing").is_none());
    }
}