oxiproj-engine 0.1.0

Proj-string parser, operation dispatch, and transformation pipelines for OxiProj.
Documentation
//! Single-operation builder: turns parsed parameters into a [`crate::pj::Pj`].
//!
//! Mirrors PROJ 9.8.0 `src/pj_init.cpp` generic parameter setup combined with
//! the per-projection dispatch in `oxiproj-projections`.

use crate::context::Context;
use crate::params::{ParamList, ParamView};
use crate::pj::Pj;
use oxiproj_core::{Ellipsoid, ProjResult};

/// Build a single (non-pipeline) operation from its name and parameters.
///
/// Parses the generic PROJ parameters (`lon_0`, `lat_0`, `x_0`, `k_0`, units,
/// `pm`, ...), then delegates the projection-specific math to
/// [`oxiproj_projections::build`] and applies any overrides it returns.
pub fn build_single_op(
    name: &str,
    params: &ParamList,
    ellipsoid: Ellipsoid,
    context: &Context,
) -> ProjResult<Pj> {
    // --- Generic parameters ---
    let lam0 = params.get_dms("lon_0").unwrap_or(0.0);
    let phi0 = params.get_dms("lat_0").unwrap_or(0.0);
    let x0 = params.get_f64("x_0").unwrap_or(0.0);
    let y0 = params.get_f64("y_0").unwrap_or(0.0);
    let z0 = params.get_f64("z_0").unwrap_or(0.0);
    let k0 = params
        .get_f64("k_0")
        .or_else(|| params.get_f64("k"))
        .unwrap_or(1.0);

    // Horizontal unit scaling.
    let (to_meter, fr_meter) = match params.get_f64("to_meter") {
        Some(v) if v != 0.0 => (v, 1.0 / v),
        _ => match params.get_str("units") {
            Some(u) => match oxiproj_core::find_linear_unit(u) {
                Some(ud) => (ud.factor, 1.0 / ud.factor),
                None => (1.0, 1.0),
            },
            None => (1.0, 1.0),
        },
    };

    // Vertical unit scaling.
    let (vto_meter, vfr_meter) = match params.get_f64("vto_meter") {
        Some(v) if v != 0.0 => (v, 1.0 / v),
        _ => match params.get_str("vunits") {
            Some(u) => match oxiproj_core::find_linear_unit(u) {
                Some(ud) => (ud.factor, 1.0 / ud.factor),
                None => (1.0, 1.0),
            },
            None => (1.0, 1.0),
        },
    };

    let over = params.get_bool("over");
    let geoc = params.get_bool("geoc");

    let from_greenwich = match params.get_str("pm") {
        Some(pm) => oxiproj_core::prime_meridian_offset(pm)
            .or_else(|_| oxiproj_core::dmstor(pm))
            .unwrap_or(0.0),
        None => 0.0,
    };

    // --- Projection-specific build ---
    let view = ParamView(params);
    let pp = oxiproj_projections::ProjParams {
        ellipsoid: &ellipsoid,
        phi0,
        k0,
        params: &view,
    };
    match oxiproj_projections::build(name, &pp) {
        Ok(build) => {
            // Apply overrides supplied by the projection.
            let lam0 = build.lam0_override.unwrap_or(lam0);
            let k0 = build.k0_override.unwrap_or(k0);
            let x0 = build.x0_override.unwrap_or(x0);
            let y0 = build.y0_override.unwrap_or(y0);
            let phi0 = build.phi0_override.unwrap_or(phi0);

            Ok(Pj {
                operation: build.operation,
                ellipsoid,
                lam0,
                phi0,
                x0,
                y0,
                z0,
                k0,
                to_meter,
                fr_meter,
                vto_meter,
                vfr_meter,
                from_greenwich,
                over,
                geoc,
                is_latlong: build.is_latlong,
                left: build.left,
                right: build.right,
                inverted: false,
                bypass_prepare_finalize: false,
            })
        }
        Err(oxiproj_core::ProjError::InvalidOp) => {
            // Not a projection: build it as a transformation/conversion.
            // Transforms are 4D operations working in cartesian/radians space;
            // they do their own coordinate-space handling in forward_4d/inverse_4d,
            // so projection-style prepare/finalize (lam0 subtraction, a-scaling,
            // false easting/northing) must NOT be applied. Mirrors PROJ driving
            // these by P->left / P->right (src/4D_api.cpp).
            let tp = oxiproj_transformations::TransParams {
                ellipsoid: &ellipsoid,
                params: &view,
                registry: Some(context as &dyn oxiproj_transformations::GridRegistry),
            };
            let tb = oxiproj_transformations::build(name, &tp)?;
            Ok(Pj {
                operation: tb.operation,
                ellipsoid,
                lam0: 0.0,
                phi0: 0.0,
                x0: 0.0,
                y0: 0.0,
                z0: 0.0,
                k0: 1.0,
                to_meter: 1.0,
                fr_meter: 1.0,
                vto_meter: 1.0,
                vfr_meter: 1.0,
                from_greenwich: 0.0,
                over: false,
                geoc: false,
                is_latlong: false,
                left: tb.left,
                right: tb.right,
                inverted: false,
                bypass_prepare_finalize: true,
            })
        }
        Err(e) => Err(e),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::params::parse;
    use oxiproj_core::{Coord, DEG_TO_RAD};

    #[test]
    fn merc_known_value() {
        let ell = Ellipsoid::named("WGS84").unwrap();
        let pj = build_single_op(
            "merc",
            &parse("+proj=merc +ellps=WGS84"),
            ell,
            &Context::new(),
        )
        .unwrap();
        let out = pj
            .forward(Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0))
            .unwrap();
        let o = out.v();
        assert!((o[0] - 1335833.8895192828).abs() < 1e-6, "x got {}", o[0]);
        assert!(
            (o[1] - 7_326_837.715_045_549).abs() < 1e-6,
            "y got {}",
            o[1]
        );
    }

    #[test]
    fn utm_central_meridian_origin() {
        let ell = Ellipsoid::named("WGS84").unwrap();
        let pj = build_single_op(
            "utm",
            &parse("+proj=utm +zone=32 +ellps=WGS84"),
            ell,
            &Context::new(),
        )
        .unwrap();
        let out = pj
            .forward(Coord::new(9.0 * DEG_TO_RAD, 0.0, 0.0, 0.0))
            .unwrap();
        let o = out.v();
        assert!((o[0] - 500000.0).abs() < 1e-6, "x got {}", o[0]);
        assert!((o[1] - 0.0).abs() < 1e-6, "y got {}", o[1]);
    }
}