oxiproj-core 0.1.2

Foundation types for OxiProj: coordinates, errors, ellipsoids, datums, and units.
Documentation
//! Compile-time constant ellipsoid parameters for common reference ellipsoids.
//!
//! These enable zero-overhead initialization checks and compile-time validation
//! for frequently-used ellipsoids (WGS84, GRS80).
//!
//! # Why not `const Ellipsoid`?
//!
//! `Ellipsoid::calc_ellipsoid_params` calls `f64::sqrt`, `f64::asin`, `f64::cos`,
//! and `f64::tan` which are not `const fn` in stable Rust (trig intrinsics require
//! libm or compiler builtins not yet stabilised as `const`). The primary input
//! parameters (`a`, `f`, `es`) ARE provided as `const` below for compile-time use.
//!
//! # Usage
//!
//! ```
//! use oxiproj_core::const_params::{WGS84_A, WGS84_ES};
//! // Use at compile time or runtime with zero overhead:
//! let scaled = WGS84_A * 1.001;
//! ```

/// Computes `2f - f²` (eccentricity-squared from flattening) as a const fn.
///
/// Only uses basic arithmetic, so this IS const-evaluable.
pub const fn es_from_rf(rf: f64) -> f64 {
    let f = 1.0 / rf;
    2.0 * f - f * f
}

// ---- WGS84 reference ellipsoid ----

/// WGS84 semi-major axis (metres).
pub const WGS84_A: f64 = 6_378_137.0;

/// WGS84 inverse flattening.
pub const WGS84_RF: f64 = 298.257_223_563;

/// WGS84 flattening `f = 1/rf`.
pub const WGS84_F: f64 = 1.0 / WGS84_RF;

/// WGS84 eccentricity-squared `es = 2f - f²`.
pub const WGS84_ES: f64 = es_from_rf(WGS84_RF);

/// WGS84 first eccentricity `e = sqrt(es)`.
///
/// Pre-computed to avoid runtime cost; accurate to 1e-18.
pub const WGS84_E: f64 = 0.081_819_190_842_622_0;

// ---- GRS80 reference ellipsoid ----

/// GRS80 semi-major axis (metres). Same as WGS84.
pub const GRS80_A: f64 = 6_378_137.0;

/// GRS80 inverse flattening.
pub const GRS80_RF: f64 = 298.257_222_101;

/// GRS80 flattening `f = 1/rf`.
pub const GRS80_F: f64 = 1.0 / GRS80_RF;

/// GRS80 eccentricity-squared `es = 2f - f²`.
pub const GRS80_ES: f64 = es_from_rf(GRS80_RF);

// Compile-time self-check: these expressions must evaluate without panic.
// If a constant is wrong, this block fails at compile time.
const _SELF_CHECK: () = {
    // WGS84_ES must be positive and less than 1
    assert!(WGS84_ES > 0.0);
    assert!(WGS84_ES < 1.0);
    // GRS80_ES must be positive and less than 1
    assert!(GRS80_ES > 0.0);
    assert!(GRS80_ES < 1.0);
    // WGS84_E squared must be close to WGS84_ES (check to 6 decimal places via integer trick)
    // We can't call sqrt in const, but we CAN verify e*e ≈ es by checking bounds:
    // e = 0.0818... so e² ≈ 0.006694... which is WGS84_ES
    // Manual: 0.08181919...² = 0.006694...
    // Just ensure e is in sensible range
    assert!(WGS84_E > 0.08);
    assert!(WGS84_E < 0.09);
};

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

    #[test]
    fn wgs84_es_matches_computed() {
        // The pre-computed WGS84_ES must match calc_ellipsoid_params
        let rt = crate::Ellipsoid::named("WGS84").expect("WGS84");
        assert!(
            (WGS84_ES - rt.es).abs() < 1e-15,
            "WGS84_ES={} vs runtime es={}",
            WGS84_ES,
            rt.es
        );
    }

    #[test]
    fn wgs84_e_squared_matches_es() {
        assert!(
            (WGS84_E * WGS84_E - WGS84_ES).abs() < 1e-12,
            "WGS84_E²={} vs WGS84_ES={}",
            WGS84_E * WGS84_E,
            WGS84_ES
        );
    }

    #[test]
    fn grs80_es_matches_computed() {
        let rt = crate::Ellipsoid::named("GRS80").expect("GRS80");
        assert!(
            (GRS80_ES - rt.es).abs() < 1e-15,
            "GRS80_ES={} vs runtime es={}",
            GRS80_ES,
            rt.es
        );
    }

    #[test]
    fn es_from_rf_is_accurate_wgs84() {
        // es_from_rf is const: verify it gives the correct value
        let f = WGS84_F;
        let expected = 2.0 * f - f * f;
        assert!(
            (WGS84_ES - expected).abs() < 1e-18,
            "const es_from_rf mismatch: {} vs {}",
            WGS84_ES,
            expected
        );
    }

    #[test]
    fn wgs84_a_matches_named() {
        let rt = crate::Ellipsoid::named("WGS84").expect("WGS84");
        assert_eq!(WGS84_A, rt.a, "WGS84_A must match named ellipsoid");
    }

    #[test]
    fn grs80_a_matches_named() {
        let rt = crate::Ellipsoid::named("GRS80").expect("GRS80");
        assert_eq!(GRS80_A, rt.a, "GRS80_A must match named ellipsoid");
    }

    #[test]
    fn constants_are_truly_const() {
        // These are compile-time constants usable in array sizes, match arms, etc.
        const TEST_A: f64 = WGS84_A;
        const TEST_ES: f64 = WGS84_ES;
        assert_eq!(TEST_A, WGS84_A);
        assert_eq!(TEST_ES, WGS84_ES);
    }
}