wbprojection 0.1.0

Whitebox Projections is a map projection library for Rust, inspired by PROJ
Documentation
//! Albers Equal-Area Conic projection.

use crate::error::{ProjectionError, Result};
use crate::{to_degrees, to_radians};
use super::{ProjectionImpl, ProjectionParams};

pub(super) struct AlbersProj {
    lon0: f64,
    a: f64,
    e2: f64,
    n: f64,
    c: f64,
    rho0: f64,
    fe: f64,
    fn_: f64,
}

fn alpha(e2: f64, lat: f64) -> f64 {
    let e = e2.sqrt();
    let sin_lat = lat.sin();
    let esin = e * sin_lat;
    (1.0 - e2) * (
        sin_lat / (1.0 - e2 * sin_lat * sin_lat)
        - (1.0 / (2.0 * e)) * ((1.0 - esin) / (1.0 + esin)).ln()
    )
}

fn m(e2: f64, lat: f64) -> f64 {
    let sin_lat = lat.sin();
    lat.cos() / (1.0 - e2 * sin_lat * sin_lat).sqrt()
}

impl AlbersProj {
    pub fn new(p: &ProjectionParams, lat1_deg: f64, lat2_deg: f64) -> Result<Self> {
        let lat1 = to_radians(lat1_deg);
        let lat2 = to_radians(lat2_deg);
        let lat0 = to_radians(p.lat0);
        let a = p.ellipsoid.a;
        let e2 = p.ellipsoid.e2;

        let m1 = m(e2, lat1);
        let m2 = m(e2, lat2);
        let a1 = alpha(e2, lat1);
        let a2 = alpha(e2, lat2);
        let a0 = alpha(e2, lat0);

        let n = (m1 * m1 - m2 * m2) / (a2 - a1);
        let c = m1 * m1 + n * a1;
        let rho0 = a * (c - n * a0).sqrt() / n;

        Ok(AlbersProj {
            lon0: to_radians(p.lon0),
            a,
            e2,
            n,
            c,
            rho0,
            fe: p.false_easting,
            fn_: p.false_northing,
        })
    }
}

impl ProjectionImpl for AlbersProj {
    fn forward(&self, lon_deg: f64, lat_deg: f64) -> Result<(f64, f64)> {
        let lat = to_radians(lat_deg);
        let lon = to_radians(lon_deg);

        let a_val = alpha(self.e2, lat);
        let rho = self.a * (self.c - self.n * a_val).sqrt() / self.n;
        let theta = self.n * (lon - self.lon0);

        let x = rho * theta.sin() + self.fe;
        let y = self.rho0 - rho * theta.cos() + self.fn_;
        Ok((x, y))
    }

    fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64)> {
        let mut x = x - self.fe;
        let mut y = self.rho0 - (y - self.fn_);

        let mut rho = (x * x + y * y).sqrt();
        if rho != 0.0 && self.n < 0.0 {
            rho = -rho;
            x = -x;
            y = -y;
        }

        let theta = x.atan2(y);
        let lon = theta / self.n + self.lon0;

        let q = (self.c - (rho * self.n / self.a).powi(2)) / self.n;
        let e2 = self.e2;
        let e = e2.sqrt();

        let phi = if e < 1e-15 {
            (q / 2.0).clamp(-1.0, 1.0).asin()
        } else {
            let mut phi = (q / 2.0).clamp(-1.0, 1.0).asin();
            let mut converged = false;
            for _ in 0..25 {
                let sin_phi = phi.sin();
                let cos_phi = phi.cos();
                if cos_phi.abs() < 1e-15 {
                    break;
                }
                let esin = e * sin_phi;
                let one_minus = 1.0 - e2 * sin_phi * sin_phi;
                let phi_new = phi + one_minus * one_minus / (2.0 * cos_phi) * (
                    q / (1.0 - e2)
                    - sin_phi / one_minus
                    + (1.0 / (2.0 * e)) * ((1.0 - esin) / (1.0 + esin)).ln()
                );
                if (phi_new - phi).abs() < 1e-12 {
                    phi = phi_new;
                    converged = true;
                    break;
                }
                phi = phi_new;
            }
            if !converged {
                return Err(ProjectionError::ConvergenceFailure { iterations: 25 });
            }
            phi
        };

        Ok((to_degrees(lon), to_degrees(phi)))
    }
}