oxiproj-engine 0.1.0

Proj-string parser, operation dispatch, and transformation pipelines for OxiProj.
Documentation
//! Ellipsoid resolution from proj-string parameters.
//!
//! Ported from PROJ 9.8.0 `src/ell_set.cpp` (`pj_ell_set`).

use crate::params::ParamList;
use oxiproj_core::{Ellipsoid, ProjError, ProjResult};

/// Default semi-major axis (WGS84 / GRS80 `a`) used when no size is supplied.
const DEFAULT_A: f64 = 6378137.0;

/// Resolve an [`Ellipsoid`] from the parsed proj-string parameters.
///
/// Resolution order mirrors `pj_ell_set`:
/// 1. `+R` overrules everything (a sphere).
/// 2. A baseline from `+ellps` or `+datum` supplies `a`/`es`.
/// 3. `+a` overrides the size; shape comes from the first present of
///    `+rf`, `+f`, `+es`, `+e`, `+b` (in that order).
/// 4. Pragmatic fallbacks fill any gaps (see inline notes).
///
/// Ported from `src/ell_set.cpp`.
pub fn setup_ellipsoid(params: &ParamList) -> ProjResult<Ellipsoid> {
    // 1. +R overrules everything.
    if let Some(r) = params.get_f64("R") {
        return Ellipsoid::sphere(r);
    }

    let mut a: Option<f64> = None;
    let mut es: Option<f64> = None;
    let mut have_baseline = false;
    let mut have_shape = false;

    // 2. Baseline from +ellps, else +datum (resolved through its ellipse id).
    if let Some(name) = params.get_str("ellps") {
        let base = Ellipsoid::named(name)?;
        a = Some(base.a);
        es = Some(base.es);
        have_baseline = true;
    } else if let Some(name) = params.get_str("datum") {
        if let Some(d) = oxiproj_core::find_datum(name) {
            let base = Ellipsoid::named(d.ellipse_id)?;
            a = Some(base.a);
            es = Some(base.es);
            have_baseline = true;
            // TODO(v0.2): apply datum shift (datum_params / towgs84 / nadgrids).
        }
        // If the datum is unknown, leave the baseline unset (do not error here).
    }

    // 3a. Size override.
    if let Some(av) = params.get_f64("a") {
        a = Some(av);
    }

    // 3b. Shape override; first present wins, in this fixed order.
    if let Some(rf) = params.get_f64("rf") {
        if rf != 0.0 {
            let f = 1.0 / rf;
            es = Some(2.0 * f - f * f);
            have_shape = true;
        }
    }
    if !have_shape {
        if let Some(f) = params.get_f64("f") {
            es = Some(2.0 * f - f * f);
            have_shape = true;
        }
    }
    if !have_shape {
        if let Some(e_s) = params.get_f64("es") {
            es = Some(e_s);
            have_shape = true;
        }
    }
    if !have_shape {
        if let Some(e) = params.get_f64("e") {
            es = Some(e * e);
            have_shape = true;
        }
    }
    if !have_shape {
        if let Some(bv) = params.get_f64("b") {
            match a {
                Some(av) => {
                    let f = (av - bv) / av;
                    es = Some(2.0 * f - f * f);
                    have_shape = true;
                }
                None => return Err(ProjError::IllegalArgValue),
            }
        }
    }

    // 4. Resolve final a/es.
    //
    // Pragmatic default: a single op with no size/shape/datum and no +R is
    // WGS84. PROJ uses GRS80 for pipelines; that pipeline default is handled in
    // create.rs, while single-op tests always supply +ellps.
    if a.is_none() && !have_baseline && !have_shape {
        return Ellipsoid::named("WGS84");
    }

    // If +a was given with no shape and no baseline, treat it as a sphere.
    if a.is_some() && !have_baseline && !have_shape {
        es = Some(0.0);
    }

    let a_final = a.unwrap_or(DEFAULT_A);
    let es_final = es.unwrap_or(0.0);
    Ellipsoid::from_a_es(a_final, es_final)
}

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

    #[test]
    fn wgs84_by_ellps() {
        let e = setup_ellipsoid(&parse("+ellps=WGS84")).unwrap();
        assert_eq!(e.a, 6378137.0);
        assert!((e.es - 0.0066943799901413165).abs() < 1e-15);
    }

    #[test]
    fn sphere_by_r() {
        let e = setup_ellipsoid(&parse("+R=6371000")).unwrap();
        assert_eq!(e.es, 0.0);
        assert_eq!(e.a, 6371000.0);
    }

    #[test]
    fn a_and_rf() {
        let e = setup_ellipsoid(&parse("+a=6378137 +rf=298.257223563")).unwrap();
        let wgs84 = Ellipsoid::named("WGS84").unwrap();
        assert!((e.es - wgs84.es).abs() < 1e-12);
    }

    #[test]
    fn datum_resolves_ellipsoid() {
        let e = setup_ellipsoid(&parse("+datum=WGS84")).unwrap();
        assert_eq!(e.a, 6378137.0);
    }

    #[test]
    fn a_alone_is_sphere() {
        let e = setup_ellipsoid(&parse("+a=6378137")).unwrap();
        assert_eq!(e.es, 0.0);
    }
}