wbprojection 0.1.0

Whitebox Projections is a map projection library for Rust, inspired by PROJ
Documentation
//! Van der Grinten I projection.

use super::{ProjectionImpl, ProjectionParams};
use crate::error::{ProjectionError, Result};
use crate::{to_degrees, to_radians};
use std::f64::consts::{FRAC_PI_2, PI};

pub(super) struct VanDerGrintenProj {
    lon0: f64,
    a: f64,
    fe: f64,
    fn_: f64,
}

impl VanDerGrintenProj {
    pub fn new(p: &ProjectionParams) -> Result<Self> {
        Ok(VanDerGrintenProj {
            lon0: to_radians(p.lon0),
            a: p.ellipsoid.a,
            fe: p.false_easting,
            fn_: p.false_northing,
        })
    }

    fn wrap_lon(mut lon: f64) -> f64 {
        while lon > PI {
            lon -= 2.0 * PI;
        }
        while lon < -PI {
            lon += 2.0 * PI;
        }
        lon
    }

    fn normalized_forward(lon_rel: f64, lat: f64) -> Result<(f64, f64)> {
        let eps = 1e-14;
        if !(-FRAC_PI_2..=FRAC_PI_2).contains(&lat) {
            return Err(ProjectionError::out_of_bounds(
                "latitude outside valid range [-90, 90]",
            ));
        }

        if lat.abs() < eps {
            return Ok((lon_rel, 0.0));
        }

        let abs_lat = lat.abs();
        let abs_lon = lon_rel.abs();

        let theta = ((2.0 * abs_lat / PI).clamp(0.0, 1.0)).asin();
        let sin_t = theta.sin();

        if abs_lon < eps || (abs_lat - FRAC_PI_2).abs() < eps {
            let y = lat.signum() * PI * (0.5 * theta).tan();
            return Ok((0.0, y));
        }

        if sin_t.abs() < eps {
            return Ok((lon_rel, 0.0));
        }

        let cos_t = theta.cos();
        let a_term = 0.5 * ((PI / abs_lon) - (abs_lon / PI)).abs();
        let g = cos_t / (sin_t + cos_t - 1.0);
        let p = g * (2.0 / sin_t - 1.0);
        let q = a_term * a_term + g;

        let p2 = p * p;
        let a2 = a_term * a_term;
        let den = p2 + a2;

        let rad_x = a2 * (g - p2) * (g - p2) - (p2 + a2) * (g * g - p2);
        let sqrt_x = rad_x.max(0.0).sqrt();
        let x = lon_rel.signum() * PI * (a_term * (g - p2) + sqrt_x) / den;

        let rad_y = (a2 + 1.0) * den - q * q;
        let sqrt_y = rad_y.max(0.0).sqrt();
        let y = lat.signum() * PI * (p * q - a_term * sqrt_y) / den;

        Ok((x, y))
    }
}

impl ProjectionImpl for VanDerGrintenProj {
    fn forward(&self, lon_deg: f64, lat_deg: f64) -> Result<(f64, f64)> {
        let lon = to_radians(lon_deg);
        let lat = to_radians(lat_deg);
        let lon_rel = Self::wrap_lon(lon - self.lon0);
        let (x_norm, y_norm) = Self::normalized_forward(lon_rel, lat)?;
        Ok((self.a * x_norm + self.fe, self.a * y_norm + self.fn_))
    }

    fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64)> {
        let x_target = (x - self.fe) / self.a;
        let y_target = (y - self.fn_) / self.a;

        let rho = (x_target * x_target + y_target * y_target).sqrt();
        if rho > PI + 1e-10 {
            return Err(ProjectionError::out_of_bounds(
                "point outside Van der Grinten valid world disk",
            ));
        }

        let mut lat = y_target.clamp(-FRAC_PI_2, FRAC_PI_2);
        let mut lon_rel = x_target.clamp(-PI, PI);

        for _ in 0..60 {
            let (fx, fy) = Self::normalized_forward(lon_rel, lat)?;
            let r1 = fx - x_target;
            let r2 = fy - y_target;

            if r1.abs() < 1e-13 && r2.abs() < 1e-13 {
                let lon = Self::wrap_lon(self.lon0 + lon_rel);
                return Ok((to_degrees(lon), to_degrees(lat)));
            }

            let h = 1e-7;
            let lat_h = (lat + h).clamp(-FRAC_PI_2, FRAC_PI_2);
            let lon_h = Self::wrap_lon(lon_rel + h);

            let (fx_lon_h, fy_lon_h) = Self::normalized_forward(lon_h, lat)?;
            let (fx_lat_h, fy_lat_h) = Self::normalized_forward(lon_rel, lat_h)?;

            let j11 = (fx_lon_h - fx) / h;
            let j12 = (fx_lat_h - fx) / h;
            let j21 = (fy_lon_h - fy) / h;
            let j22 = (fy_lat_h - fy) / h;

            let det = j11 * j22 - j12 * j21;
            if det.abs() < 1e-16 {
                break;
            }

            let d_lon = (-r1 * j22 + r2 * j12) / det;
            let d_lat = (-j11 * r2 + j21 * r1) / det;

            lon_rel = Self::wrap_lon(lon_rel + d_lon.clamp(-0.5, 0.5));
            lat = (lat + d_lat.clamp(-0.5, 0.5)).clamp(-FRAC_PI_2, FRAC_PI_2);
        }

        Err(ProjectionError::ConvergenceFailure { iterations: 60 })
    }
}