use crate::error::{Error, Result};
pub fn polyconic_forward(
lon: f64,
lat: f64,
lon_0: f64,
lat_0: f64,
semi_major: f64,
) -> Result<(f64, f64)> {
if !lon.is_finite() || !lat.is_finite() {
return Err(Error::invalid_coordinate("polyconic: non-finite input"));
}
let dlon = lon - lon_0;
if lat.abs() < 1e-12 {
let x = semi_major * dlon;
let y = -semi_major * lat_0;
return Ok((x, y));
}
let cot_lat = lat.cos() / lat.sin();
let e_val = dlon * lat.sin();
let x = semi_major * cot_lat * e_val.sin();
let y = semi_major * (lat - lat_0 + cot_lat * (1.0 - e_val.cos()));
Ok((x, y))
}
pub fn polyconic_inverse(
x: f64,
y: f64,
lon_0: f64,
lat_0: f64,
semi_major: f64,
) -> Result<(f64, f64)> {
if !x.is_finite() || !y.is_finite() {
return Err(Error::invalid_coordinate("polyconic: non-finite input"));
}
let xr = x / semi_major;
let yr = y / semi_major;
let m0 = lat_0;
let b = yr + m0;
if b.abs() < 1e-12 {
let lat = 0.0;
let lon = xr + lon_0;
return Ok((lon, lat));
}
let mut lat = b;
const MAX_ITER: usize = 20;
const TOL: f64 = 1e-12;
for _ in 0..MAX_ITER {
if lat.abs() < 1e-14 {
lat = 0.0;
break;
}
let sin_lat = lat.sin();
let cos_lat = lat.cos();
let tan_lat = sin_lat / cos_lat;
let cot_lat = cos_lat / sin_lat;
let sin_e = xr * tan_lat;
if sin_e.abs() > 1.0 + 1e-10 {
break;
}
let sin_e_clamped = sin_e.clamp(-1.0, 1.0);
let e_val = sin_e_clamped.asin();
let cos_e = e_val.cos();
let f = lat - lat_0 + cot_lat * (1.0 - cos_e) - yr;
let sec2 = 1.0 / (cos_lat * cos_lat);
let de_dphi = xr * sec2 / (1.0 - sin_e_clamped * sin_e_clamped).sqrt().max(1e-15);
let csc2 = 1.0 / (sin_lat * sin_lat);
let fp = 1.0 - csc2 * (1.0 - cos_e) + cot_lat * sin_e_clamped * de_dphi;
if fp.abs() < 1e-15 {
break;
}
let delta = f / fp;
lat -= delta;
if delta.abs() < TOL {
break;
}
}
let lon = if lat.abs() < 1e-12 {
xr + lon_0
} else {
let sin_lat = lat.sin();
let cos_lat = lat.cos();
if cos_lat.abs() < 1e-15 {
lon_0
} else {
let sin_e = (xr * sin_lat / cos_lat).clamp(-1.0, 1.0);
let e_val = sin_e.asin();
e_val / sin_lat + lon_0
}
};
Ok((lon, lat))
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
const R: f64 = 6_371_000.0;
#[test]
fn test_polyconic_at_origin() {
let lon_0 = (-96.0_f64).to_radians();
let lat_0 = 37.0_f64.to_radians();
let (x, y) = polyconic_forward(lon_0, lat_0, lon_0, lat_0, R).expect("ok");
assert!(x.abs() < 1.0, "x at origin: {x}");
assert!(y.abs() < 1.0, "y at origin: {y}");
}
#[test]
fn test_polyconic_on_central_meridian() {
let lon_0 = 0.0;
let lat_0 = 0.0;
let lat = 45.0_f64.to_radians();
let (x, y) = polyconic_forward(lon_0, lat, lon_0, lat_0, R).expect("ok");
assert!(x.abs() < 1.0, "x on central meridian: {x}");
let expected_y = R * lat;
assert!(
(y - expected_y).abs() < 1.0,
"y on meridian: {y} vs expected {expected_y}"
);
}
#[test]
fn test_polyconic_at_equator() {
let lon_0 = 0.0;
let lat_0 = 0.0;
let lon = 30.0_f64.to_radians();
let (x, y) = polyconic_forward(lon, 0.0, lon_0, lat_0, R).expect("ok");
let expected_x = R * lon;
assert!((x - expected_x).abs() < 1.0, "x at equator: {x}");
assert!(y.abs() < 1.0, "y at equator: {y}");
}
#[test]
fn test_polyconic_roundtrip() {
let lon_0 = (-96.0_f64).to_radians();
let lat_0 = 37.0_f64.to_radians();
let cases = [
((-96.0_f64).to_radians(), 37.0_f64.to_radians()),
((-94.0_f64).to_radians(), 38.0_f64.to_radians()),
((-98.0_f64).to_radians(), 36.0_f64.to_radians()),
((-96.0_f64).to_radians(), 42.0_f64.to_radians()),
((-90.0_f64).to_radians(), 37.0_f64.to_radians()),
];
for (lon, lat) in cases {
let (x, y) = polyconic_forward(lon, lat, lon_0, lat_0, R).expect("fwd");
let (lon2, lat2) = polyconic_inverse(x, y, lon_0, lat_0, R).expect("inv");
assert!(
(lon - lon2).abs() < 1e-6,
"lon roundtrip: {:.5}° vs {:.5}°",
lon.to_degrees(),
lon2.to_degrees()
);
assert!(
(lat - lat2).abs() < 1e-6,
"lat roundtrip: {:.5}° vs {:.5}°",
lat.to_degrees(),
lat2.to_degrees()
);
}
}
#[test]
fn test_polyconic_nonfinite() {
assert!(polyconic_forward(f64::NAN, 0.0, 0.0, 0.0, R).is_err());
assert!(polyconic_forward(f64::INFINITY, 0.0, 0.0, 0.0, R).is_err());
assert!(polyconic_inverse(f64::NAN, 0.0, 0.0, 0.0, R).is_err());
}
#[test]
fn test_polyconic_equator_roundtrip() {
let lon_0 = 0.0;
let lat_0 = 0.0;
let lon = 10.0_f64.to_radians();
let lat = 0.0;
let (x, y) = polyconic_forward(lon, lat, lon_0, lat_0, R).expect("fwd");
let (lon2, lat2) = polyconic_inverse(x, y, lon_0, lat_0, R).expect("inv");
assert!((lon - lon2).abs() < 1e-9, "equator lon: {lon} vs {lon2}");
assert!((lat - lat2).abs() < 1e-9, "equator lat: {lat} vs {lat2}");
}
}