use crate::error::{Error, Result};
pub fn equidistant_conic_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("eqdc: non-finite input"));
}
let (n, g, rho_0) = eqdc_constants(lat_0, lat_1, lat_2, semi_major)?;
let rho = semi_major * (g - lat);
let theta = n * (lon - lon_0);
let x = rho * theta.sin();
let y = rho_0 - rho * theta.cos();
Ok((x, y))
}
pub fn equidistant_conic_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("eqdc: non-finite input"));
}
let (n, g, rho_0) = eqdc_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 lat = g - rho / semi_major;
let theta = if rho.abs() < 1e-15 {
0.0
} else {
(x / rho).atan2(rho_0_minus_y / rho)
};
let lon = theta / n + lon_0;
Ok((lon, lat))
}
fn eqdc_constants(lat_0: f64, lat_1: f64, lat_2: f64, semi_major: f64) -> Result<(f64, f64, f64)> {
let n = if (lat_2 - lat_1).abs() < 1e-12 {
lat_1.sin()
} else {
(lat_1.cos() - lat_2.cos()) / (lat_2 - lat_1)
};
if n.abs() < 1e-15 {
return Err(Error::invalid_parameter(
"eqdc",
"cone constant n is zero — check standard parallels",
));
}
let g = lat_1.cos() / n + lat_1;
let rho_0 = semi_major * (g - lat_0);
Ok((n, g, rho_0))
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
const R: f64 = 6_371_000.0;
#[test]
fn test_eqdc_roundtrip() {
let lat_1 = 29.5_f64.to_radians();
let lat_2 = 45.5_f64.to_radians();
let lat_0 = 37.0_f64.to_radians();
let lon_0 = (-96.0_f64).to_radians();
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()),
];
for (lon, lat) in cases {
let (x, y) = equidistant_conic_forward(lon, lat, lon_0, lat_0, lat_1, lat_2, R)
.expect("forward ok");
let (lon2, lat2) =
equidistant_conic_inverse(x, y, lon_0, lat_0, lat_1, lat_2, R).expect("inverse ok");
assert!(
(lon - lon2).abs() < 1e-9,
"lon roundtrip: {lon:.5} vs {lon2:.5}"
);
assert!(
(lat - lat2).abs() < 1e-9,
"lat roundtrip: {lat:.5} vs {lat2:.5}"
);
}
}
#[test]
fn test_eqdc_at_origin() {
let lat_1 = 29.5_f64.to_radians();
let lat_2 = 45.5_f64.to_radians();
let lat_0 = 37.0_f64.to_radians();
let lon_0 = (-96.0_f64).to_radians();
let (x, y) =
equidistant_conic_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}");
}
}