use std::f64::consts::FRAC_PI_2;
use crate::ellipsoid::Ellipsoid;
use crate::error::{Error, Result};
use crate::projection::{
ensure_finite_lon_lat, ensure_finite_xy, normalize_longitude, validate_angle,
validate_latitude_param, validate_lon_lat, validate_offset, validate_projected, validate_scale,
};
#[derive(Clone)]
pub(crate) struct PolarStereographic {
lon0: f64,
is_north: bool,
false_easting: f64,
false_northing: f64,
e: f64,
two_a_k0: f64,
t_polar: f64,
}
impl PolarStereographic {
pub(crate) fn new(
ellipsoid: Ellipsoid,
lon0: f64,
lat_ts: f64,
k0_input: f64,
false_easting: f64,
false_northing: f64,
) -> Result<Self> {
validate_angle("central meridian", lon0)?;
validate_latitude_param("latitude of true scale", lat_ts)?;
validate_scale("scale factor", k0_input)?;
validate_offset("false easting", false_easting)?;
validate_offset("false northing", false_northing)?;
let is_north = lat_ts >= 0.0;
let e = ellipsoid.e();
let e2 = ellipsoid.e2();
let a = ellipsoid.semi_major_axis();
let k0 = if (lat_ts.abs() - FRAC_PI_2).abs() < 1e-10 {
k0_input
} else {
let abs_lat_ts = lat_ts.abs();
let sin_ts = abs_lat_ts.sin();
let cos_ts = abs_lat_ts.cos();
let e_sin_ts = e * sin_ts;
let m_ts = cos_ts / (1.0 - e2 * sin_ts * sin_ts).sqrt();
let t_ts = ((FRAC_PI_2 - abs_lat_ts) / 2.0).tan()
/ ((1.0 - e_sin_ts) / (1.0 + e_sin_ts)).powf(e / 2.0);
let t_90 = compute_t_polar(e);
m_ts / (2.0 * t_ts / t_90)
};
if !k0.is_finite() || k0 <= 0.0 {
return Err(Error::InvalidDefinition(
"Polar Stereographic scale factor must resolve to a finite positive number".into(),
));
}
let two_a_k0 = 2.0 * a * k0;
Ok(Self {
lon0,
is_north,
false_easting,
false_northing,
e,
two_a_k0,
t_polar: compute_t_polar(e),
})
}
pub(crate) fn new_variant_c(
ellipsoid: Ellipsoid,
lon0: f64,
lat_ts: f64,
easting_false_origin: f64,
northing_false_origin: f64,
) -> Result<Self> {
validate_latitude_param("latitude of standard parallel", lat_ts)?;
validate_offset("northing at false origin", northing_false_origin)?;
if (lat_ts.abs() - FRAC_PI_2).abs() < 1e-10 {
return Err(Error::InvalidDefinition(
"Polar Stereographic variant C requires a standard parallel off the pole".into(),
));
}
let e2 = ellipsoid.e2();
let sin_ts = lat_ts.sin();
let rho_f =
ellipsoid.semi_major_axis() * lat_ts.cos() / (1.0 - e2 * sin_ts * sin_ts).sqrt();
let effective_false_northing = if lat_ts >= 0.0 {
northing_false_origin + rho_f
} else {
northing_false_origin - rho_f
};
Self::new(
ellipsoid,
lon0,
lat_ts,
1.0,
easting_false_origin,
effective_false_northing,
)
}
}
fn compute_t_polar(e: f64) -> f64 {
((1.0 - e) / (1.0 + e)).powf(e / 2.0)
}
fn compute_t(lat: f64, e: f64) -> f64 {
let sin_lat = lat.sin();
let e_sin = e * sin_lat;
((FRAC_PI_2 - lat) / 2.0).tan() / ((1.0 - e_sin) / (1.0 + e_sin)).powf(e / 2.0)
}
impl super::ProjectionImpl for PolarStereographic {
fn forward(&self, lon: f64, lat: f64) -> Result<(f64, f64)> {
validate_lon_lat(lon, lat)?;
let e = self.e;
let t_polar = self.t_polar;
let (effective_lat, sign) = if self.is_north {
(lat, 1.0)
} else {
(-lat, -1.0)
};
let t = compute_t(effective_lat, e);
let rho = self.two_a_k0 * t / t_polar;
let d_lon = normalize_longitude(lon - self.lon0);
let x = self.false_easting + rho * d_lon.sin();
let y = self.false_northing - sign * rho * d_lon.cos();
ensure_finite_xy("Polar Stereographic", x, y)
}
fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64)> {
validate_projected(x, y)?;
let e = self.e;
let t_polar = self.t_polar;
let dx = x - self.false_easting;
let dy = y - self.false_northing;
let (dy_eff, sign) = if self.is_north {
(-dy, 1.0)
} else {
(dy, -1.0)
};
let rho = (dx * dx + dy_eff * dy_eff).sqrt();
if rho < 1e-10 {
let lat = if self.is_north { FRAC_PI_2 } else { -FRAC_PI_2 };
return Ok((normalize_longitude(self.lon0), lat));
}
let t = rho * t_polar / self.two_a_k0;
let lat_unsigned =
super::latitude_from_conformal_t("Polar Stereographic inverse latitude", t, e)?;
let lat = sign * lat_unsigned;
let lon = self.lon0 + dx.atan2(dy_eff);
ensure_finite_lon_lat("Polar Stereographic", lon, lat)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ellipsoid;
use crate::projection::ProjectionImpl;
fn epsg_3413() -> PolarStereographic {
PolarStereographic::new(
ellipsoid::WGS84,
(-45.0_f64).to_radians(),
70.0_f64.to_radians(),
1.0,
0.0,
0.0,
)
.unwrap()
}
fn epsg_3031() -> PolarStereographic {
PolarStereographic::new(
ellipsoid::WGS84,
0.0_f64.to_radians(),
(-71.0_f64).to_radians(),
1.0,
0.0,
0.0,
)
.unwrap()
}
#[test]
fn north_pole_at_origin() {
let proj = epsg_3413();
let (x, y) = proj
.forward((-45.0_f64).to_radians(), 90.0_f64.to_radians())
.unwrap();
assert!(x.abs() < 0.01, "x = {x}");
assert!(y.abs() < 0.01, "y = {y}");
}
#[test]
fn variant_c_matches_epsg_worked_example() {
let proj = PolarStereographic::new_variant_c(
ellipsoid::INTL1924,
140.0_f64.to_radians(),
(-67.0_f64).to_radians(),
300_000.0,
200_000.0,
)
.unwrap();
let lat: f64 = -(66.0 + 36.0 / 60.0 + 18.820 / 3600.0);
let lon: f64 = 140.0 + 4.0 / 60.0 + 17.040 / 3600.0;
let (x, y) = proj.forward(lon.to_radians(), lat.to_radians()).unwrap();
assert!((x - 303_169.52).abs() < 1e-2, "x = {x}");
assert!((y - 244_055.72).abs() < 1e-2, "y = {y}");
let (lon2, lat2) = proj.inverse(303_169.522, 244_055.721).unwrap();
assert!(
(lon2.to_degrees() - lon).abs() < 1e-7,
"{}",
lon2.to_degrees()
);
assert!(
(lat2.to_degrees() - lat).abs() < 1e-7,
"{}",
lat2.to_degrees()
);
}
#[test]
fn variant_c_rejects_polar_standard_parallel() {
assert!(PolarStereographic::new_variant_c(
ellipsoid::WGS84,
0.0,
(-90.0_f64).to_radians(),
0.0,
0.0
)
.is_err());
}
#[test]
fn roundtrip_north() {
let proj = epsg_3413();
let lon = (-45.0_f64).to_radians();
let lat = 75.0_f64.to_radians();
let (x, y) = proj.forward(lon, lat).unwrap();
let (lon_back, lat_back) = proj.inverse(x, y).unwrap();
assert!((lon_back - lon).abs() < 1e-8, "lon: {lon_back} vs {lon}");
assert!((lat_back - lat).abs() < 1e-8, "lat: {lat_back} vs {lat}");
}
#[test]
fn forward_wraps_longitude_delta() {
let proj = PolarStereographic::new(
ellipsoid::WGS84,
179.0_f64.to_radians(),
70.0_f64.to_radians(),
1.0,
0.0,
0.0,
)
.unwrap();
let lat = 80.0_f64.to_radians();
let wrapped = proj.forward((-181.0_f64).to_radians(), lat).unwrap();
let canonical = proj.forward(179.0_f64.to_radians(), lat).unwrap();
assert!((wrapped.0 - canonical.0).abs() < 1e-8);
assert!((wrapped.1 - canonical.1).abs() < 1e-8);
}
#[test]
fn roundtrip_south() {
let proj = epsg_3031();
let lon = 45.0_f64.to_radians();
let lat = (-75.0_f64).to_radians();
let (x, y) = proj.forward(lon, lat).unwrap();
let (lon_back, lat_back) = proj.inverse(x, y).unwrap();
assert!((lon_back - lon).abs() < 1e-8, "lon: {lon_back} vs {lon}");
assert!((lat_back - lat).abs() < 1e-8, "lat: {lat_back} vs {lat}");
}
#[test]
fn known_value_3413() {
let proj = epsg_3413();
let lon = 0.0_f64.to_radians();
let lat = 75.0_f64.to_radians();
let (x, y) = proj.forward(lon, lat).unwrap();
let (lon_back, lat_back) = proj.inverse(x, y).unwrap();
assert!(
(lon_back - lon).abs() < 1e-8,
"lon roundtrip: {lon_back} vs {lon}"
);
assert!(
(lat_back - lat).abs() < 1e-8,
"lat roundtrip: {lat_back} vs {lat}"
);
}
#[test]
fn wrong_hemisphere_extends_continuously() {
let proj = PolarStereographic::new(
ellipsoid::WGS84,
(-45.0_f64).to_radians(),
70.0_f64.to_radians(),
1.0,
0.0,
0.0,
)
.unwrap();
let lon = (-45.0_f64).to_radians();
let lat = (-30.0_f64).to_radians();
let (x_south, y_south) = proj.forward(lon, lat).unwrap();
let (_, y_north) = proj.forward(lon, -lat).unwrap();
assert!(
y_south < y_north && y_south < -2.0e7,
"southern point must not mirror: y_south={y_south}, y_north={y_north}"
);
let (lon2, lat2) = proj.inverse(x_south, y_south).unwrap();
assert!((lon2 - lon).abs() < 1e-12, "lon: {lon2}");
assert!((lat2 - lat).abs() < 1e-12, "lat: {lat2}");
let eps = 1e-9;
let (_, y_plus) = proj.forward(lon, eps).unwrap();
let (_, y_minus) = proj.forward(lon, -eps).unwrap();
assert!(
(y_plus - y_minus).abs() < 1.0,
"equator crossing must be continuous: {y_plus} vs {y_minus}"
);
}
}