use crate::error::{Error, Result};
pub fn albers_forward(
lon: f64,
lat: f64,
lon_0: f64,
lat_0: f64,
lat_1: f64,
lat_2: f64,
semi_major: f64,
) -> Result<(f64, f64)> {
if !lon.is_finite() || !lat.is_finite() {
return Err(Error::invalid_coordinate("albers: non-finite input"));
}
let (n, c, rho_0) = albers_constants(lat_0, lat_1, lat_2, semi_major)?;
let rho = albers_rho(lat, n, c, semi_major);
let theta = n * (lon - lon_0);
let x = rho * theta.sin();
let y = rho_0 - rho * theta.cos();
Ok((x, y))
}
pub fn albers_inverse(
x: f64,
y: f64,
lon_0: f64,
lat_0: f64,
lat_1: f64,
lat_2: f64,
semi_major: f64,
) -> Result<(f64, f64)> {
if !x.is_finite() || !y.is_finite() {
return Err(Error::invalid_coordinate("albers: non-finite input"));
}
let (n, c, rho_0) = albers_constants(lat_0, lat_1, lat_2, semi_major)?;
let rho_0_minus_y = rho_0 - y;
let rho = (x * x + rho_0_minus_y * rho_0_minus_y).sqrt() * if n < 0.0 { -1.0 } else { 1.0 };
let sign_n = if n >= 0.0 { 1.0 } else { -1.0 };
let theta = if rho.abs() < 1e-15 {
0.0
} else {
(x * sign_n).atan2(rho_0_minus_y * sign_n)
};
let lon = theta / n + lon_0;
let r2 = semi_major * semi_major;
let sin_lat = (c - (rho * rho * n * n) / r2) / (2.0 * n);
if sin_lat.abs() > 1.0 + 1e-10 {
return Err(Error::invalid_coordinate(
"albers inverse: sin(lat) out of range",
));
}
let lat = sin_lat.clamp(-1.0, 1.0).asin();
Ok((lon, lat))
}
fn albers_constants(
lat_0: f64,
lat_1: f64,
lat_2: f64,
semi_major: f64,
) -> Result<(f64, f64, f64)> {
let n = (lat_1.sin() + lat_2.sin()) / 2.0;
if n.abs() < 1e-15 {
return Err(Error::invalid_parameter(
"albers",
"cone constant n is near zero — check standard parallels",
));
}
let c = lat_1.cos() * lat_1.cos() + 2.0 * n * lat_1.sin();
let val = c - 2.0 * n * lat_0.sin();
if val < 0.0 {
return Err(Error::invalid_parameter(
"albers",
"negative value under sqrt for rho_0",
));
}
let rho_0 = semi_major * val.sqrt() / n;
Ok((n, c, rho_0))
}
fn albers_rho(lat: f64, n: f64, c: f64, semi_major: f64) -> f64 {
let val = c - 2.0 * n * lat.sin();
if val < 0.0 {
return 0.0;
}
semi_major * val.sqrt() / n
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
const R: f64 = 6_371_000.0;
fn us_params() -> (f64, f64, f64, f64) {
(
(-96.0_f64).to_radians(), 37.0_f64.to_radians(), 29.5_f64.to_radians(), 45.5_f64.to_radians(), )
}
#[test]
fn test_albers_at_origin() {
let (lon_0, lat_0, lat_1, lat_2) = us_params();
let (x, y) = albers_forward(lon_0, lat_0, lon_0, lat_0, lat_1, lat_2, R).expect("ok");
assert!(x.abs() < 1.0, "x at origin: {x}");
assert!(y.abs() < 1.0, "y at origin: {y}");
}
#[test]
fn test_albers_roundtrip() {
let (lon_0, lat_0, lat_1, lat_2) = us_params();
let cases = [
((-96.0_f64).to_radians(), 37.0_f64.to_radians()),
((-90.0_f64).to_radians(), 40.0_f64.to_radians()),
((-80.0_f64).to_radians(), 35.0_f64.to_radians()),
((-110.0_f64).to_radians(), 30.0_f64.to_radians()),
((-75.0_f64).to_radians(), 45.0_f64.to_radians()),
];
for (lon, lat) in cases {
let (x, y) = albers_forward(lon, lat, lon_0, lat_0, lat_1, lat_2, R).expect("fwd");
let (lon2, lat2) = albers_inverse(x, y, lon_0, lat_0, lat_1, lat_2, R).expect("inv");
assert!(
(lon - lon2).abs() < 1e-9,
"lon roundtrip: {:.6} vs {:.6}",
lon.to_degrees(),
lon2.to_degrees()
);
assert!(
(lat - lat2).abs() < 1e-9,
"lat roundtrip: {:.6} vs {:.6}",
lat.to_degrees(),
lat2.to_degrees()
);
}
}
#[test]
fn test_albers_equal_area_property() {
let (lon_0, lat_0, lat_1, lat_2) = us_params();
let delta = 0.001_f64; let lon_c = (-96.0_f64).to_radians();
let lat_a = 35.0_f64.to_radians();
let (x1, y1) = albers_forward(lon_c, lat_a, lon_0, lat_0, lat_1, lat_2, R).expect("ok");
let (x2, y2) = albers_forward(lon_c + delta, lat_a + delta, lon_0, lat_0, lat_1, lat_2, R)
.expect("ok");
let area_a = (x2 - x1).abs() * (y2 - y1).abs();
let lat_b = 42.0_f64.to_radians();
let (x3, y3) = albers_forward(lon_c, lat_b, lon_0, lat_0, lat_1, lat_2, R).expect("ok");
let (x4, y4) = albers_forward(lon_c + delta, lat_b + delta, lon_0, lat_0, lat_1, lat_2, R)
.expect("ok");
let area_b = (x4 - x3).abs() * (y4 - y3).abs();
let ratio = area_a / area_b;
assert!(
(0.8..1.2).contains(&ratio),
"area ratio should be near 1.0: {ratio}"
);
}
#[test]
fn test_albers_nonfinite() {
let (lon_0, lat_0, lat_1, lat_2) = us_params();
assert!(albers_forward(f64::NAN, 0.0, lon_0, lat_0, lat_1, lat_2, R).is_err());
assert!(albers_forward(f64::INFINITY, 0.0, lon_0, lat_0, lat_1, lat_2, R).is_err());
assert!(albers_inverse(f64::NAN, 0.0, lon_0, lat_0, lat_1, lat_2, R).is_err());
}
#[test]
fn test_albers_southern_hemisphere() {
let lon_0 = 132.0_f64.to_radians();
let lat_0 = (-27.0_f64).to_radians();
let lat_1 = (-18.0_f64).to_radians();
let lat_2 = (-36.0_f64).to_radians();
let lon = 151.0_f64.to_radians(); let lat = (-33.9_f64).to_radians();
let (x, y) = albers_forward(lon, lat, lon_0, lat_0, lat_1, lat_2, R).expect("fwd");
let (lon2, lat2) = albers_inverse(x, y, lon_0, lat_0, lat_1, lat_2, R).expect("inv");
assert!((lon - lon2).abs() < 1e-9);
assert!((lat - lat2).abs() < 1e-9);
}
#[test]
fn test_albers_constants_degenerate() {
let lat_1 = 30.0_f64.to_radians();
let lat_2 = (-30.0_f64).to_radians();
assert!(albers_constants(0.0, lat_1, lat_2, R).is_err());
}
}