oxiproj-core 0.1.2

Foundation types for OxiProj: coordinates, errors, ellipsoids, datums, and units.
Documentation
//! Datum definitions and 7-parameter Helmert transforms ported from PROJ `src/datums.cpp`.

#[cfg(feature = "no_std")]
use alloc::vec::Vec;

use crate::error::{ProjError, ProjResult};

/// Datum transformation type. Ported from src/proj_internal.h (`PJD_*`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(i32)]
#[non_exhaustive]
pub enum DatumType {
    /// No datum shift information available.
    Unknown = 0,
    /// Three-parameter (translation-only) Molodensky/Helmert shift.
    ThreeParam = 1,
    /// Seven-parameter (translation, rotation, scale) Helmert shift.
    SevenParam = 2,
    /// Grid-based (`nadgrids=`) datum shift.
    GridShift = 3,
    /// Already referenced to WGS84 (identity shift).
    Wgs84 = 4,
}

/// One datum definition row. Ported from src/datums.cpp.
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DatumDef {
    /// Short datum identifier, e.g. `"WGS84"`, `"NAD27"`.
    pub id: &'static str,
    /// The raw `towgs84=`/`nadgrids=` PROJ parameter string.
    pub defn: &'static str,
    /// Identifier of the associated ellipsoid, looked up via
    /// [`crate::ellipsoid::named`].
    pub ellipse_id: &'static str,
    /// Free-text description of the datum.
    pub comments: &'static str,
}

/// The PROJ named-datum table. Ported verbatim from src/datums.cpp.
pub static DATUMS: &[DatumDef] = &[
    DatumDef {
        id: "WGS84",
        defn: "towgs84=0,0,0",
        ellipse_id: "WGS84",
        comments: "",
    },
    DatumDef {
        id: "GGRS87",
        defn: "towgs84=-199.87,74.79,246.62",
        ellipse_id: "GRS80",
        comments: "Greek_Geodetic_Reference_System_1987",
    },
    DatumDef {
        id: "NAD83",
        defn: "towgs84=0,0,0",
        ellipse_id: "GRS80",
        comments: "North_American_Datum_1983",
    },
    DatumDef {
        id: "NAD27",
        defn: "nadgrids=@conus,@alaska,@ntv2_0.gsb,@ntv1_can.dat",
        ellipse_id: "clrk66",
        comments: "North_American_Datum_1927",
    },
    DatumDef {
        id: "potsdam",
        defn: "nadgrids=@BETA2007.gsb",
        ellipse_id: "bessel",
        comments: "Potsdam Rauenberg 1950 DHDN",
    },
    DatumDef {
        id: "carthage",
        defn: "towgs84=-263.0,6.0,431.0",
        ellipse_id: "clrk80ign",
        comments: "Carthage 1934 Tunisia",
    },
    DatumDef {
        id: "hermannskogel",
        defn: "towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232",
        ellipse_id: "bessel",
        comments: "Hermannskogel",
    },
    DatumDef {
        id: "ire65",
        defn: "towgs84=482.530,-130.596,564.557,-1.042,-0.214,-0.631,8.15",
        ellipse_id: "mod_airy",
        comments: "Ireland 1965",
    },
    DatumDef {
        id: "nzgd49",
        defn: "towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993",
        ellipse_id: "intl",
        comments: "New Zealand Geodetic Datum 1949",
    },
    DatumDef {
        id: "OSGB36",
        defn: "towgs84=446.448,-125.157,542.060,0.1502,0.2470,0.8421,-20.4894",
        ellipse_id: "airy",
        comments: "Airy 1830",
    },
];

/// Look up a datum by exact id. Ported from src/datums.cpp.
pub fn find_datum(id: &str) -> Option<&'static DatumDef> {
    DATUMS.iter().find(|d| d.id == id)
}

/// Parsed Helmert datum-shift parameters. Ported from src/datums.cpp.
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DatumParams {
    /// Translation along X, in meters.
    pub dx: f64,
    /// Translation along Y, in meters.
    pub dy: f64,
    /// Translation along Z, in meters.
    pub dz: f64,
    /// Rotation about X, in arc-seconds (zero for 3-parameter shifts).
    pub rx: f64,
    /// Rotation about Y, in arc-seconds (zero for 3-parameter shifts).
    pub ry: f64,
    /// Rotation about Z, in arc-seconds (zero for 3-parameter shifts).
    pub rz: f64,
    /// Scale difference, in parts-per-million (zero for 3-parameter shifts).
    pub s: f64,
    /// Which shift-parameter family these values represent.
    pub datum_type: DatumType,
}

/// Parses the `towgs84=` shift parameters from a datum definition.
///
/// Returns `Ok(None)` if the datum uses `nadgrids=` (a grid-shift datum, handled
/// elsewhere). Ported from src/datums.cpp.
pub fn datum_params(id: &str) -> ProjResult<Option<DatumParams>> {
    let def = match find_datum(id) {
        Some(d) => d,
        None => return Err(ProjError::IllegalArgValue),
    };
    let defn = def.defn;
    if let Some(rest) = defn.strip_prefix("towgs84=") {
        // parse comma-separated decimals
        let mut nums: Vec<f64> = Vec::new();
        for part in rest.split(',') {
            let part = part.trim();
            match part.parse::<f64>() {
                Ok(v) => nums.push(v),
                Err(_) => return Err(ProjError::IllegalArgValue),
            }
        }
        // Determine type
        let datum_type = if nums.len() == 7 {
            DatumType::SevenParam
        } else if nums.len() == 3 {
            // WGS84/NAD83 with all-zero 3-param are treated as Wgs84 type by PROJ.
            if (id == "WGS84" || id == "NAD83") && nums.iter().all(|&v| v == 0.0) {
                DatumType::Wgs84
            } else {
                DatumType::ThreeParam
            }
        } else {
            return Err(ProjError::IllegalArgValue);
        };
        // Extract with safe indexing (.get) — nums has length 3 or 7 here.
        let g = |i: usize| -> f64 { nums.get(i).copied().unwrap_or(0.0) };
        Ok(Some(DatumParams {
            dx: g(0),
            dy: g(1),
            dz: g(2),
            rx: g(3),
            ry: g(4),
            rz: g(5),
            s: g(6),
            datum_type,
        }))
    } else if defn.starts_with("nadgrids=") {
        Ok(None)
    } else {
        Err(ProjError::IllegalArgValue)
    }
}

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

    #[test]
    fn osgb36_ellipse_id() {
        assert_eq!(find_datum("OSGB36").map(|d| d.ellipse_id), Some("airy"));
    }

    #[test]
    fn carthage_three_param() {
        let p = datum_params("carthage").expect("ok").expect("some");
        assert_eq!(p.datum_type, DatumType::ThreeParam);
        assert_eq!(p.dx, -263.0);
        assert_eq!(p.dy, 6.0);
        assert_eq!(p.dz, 431.0);
        assert_eq!(p.s, 0.0);
    }

    #[test]
    fn osgb36_seven_param() {
        let p = datum_params("OSGB36").expect("ok").expect("some");
        assert_eq!(p.datum_type, DatumType::SevenParam);
        assert_eq!(p.s, -20.4894);
        assert_eq!(p.rx, 0.1502);
        assert_eq!(p.dx, 446.448);
    }

    #[test]
    fn nad27_is_nadgrids() {
        assert_eq!(datum_params("NAD27").expect("ok"), None);
    }

    #[test]
    fn wgs84_type() {
        let p = datum_params("WGS84").expect("ok").expect("some");
        assert_eq!(p.datum_type, DatumType::Wgs84);
        assert_eq!((p.dx, p.dy, p.dz), (0.0, 0.0, 0.0));
    }

    #[test]
    fn nad83_type() {
        assert_eq!(
            datum_params("NAD83").expect("ok").expect("some").datum_type,
            DatumType::Wgs84
        );
    }

    #[test]
    fn ggrs87_three_param() {
        assert_eq!(
            datum_params("GGRS87")
                .expect("ok")
                .expect("some")
                .datum_type,
            DatumType::ThreeParam
        );
    }

    #[test]
    fn unknown_datum_is_err() {
        assert!(datum_params("unknown_datum").is_err());
    }

    #[test]
    fn datums_len() {
        assert_eq!(DATUMS.len(), 10);
    }

    #[test]
    fn datum_type_repr() {
        assert_eq!(DatumType::Wgs84 as i32, 4);
        assert_eq!(DatumType::GridShift as i32, 3);
    }
}