use crate::error::Result;
use crate::{to_degrees, to_radians};
use super::{ProjectionImpl, ProjectionParams};
pub(super) struct PolarStereographicProj {
north: bool,
lon0: f64,
fe: f64,
fn_: f64,
e: f64,
rho_coeff: f64,
}
fn t_north(e: f64, phi: f64) -> f64 {
let esin = e * phi.sin();
(std::f64::consts::FRAC_PI_4 - phi / 2.0).tan()
* ((1.0 + esin) / (1.0 - esin)).powf(e / 2.0)
}
fn m_factor(e: f64, phi: f64) -> f64 {
let esin = e * phi.sin();
phi.cos() / (1.0 - esin * esin).sqrt()
}
impl PolarStereographicProj {
pub fn new(p: &ProjectionParams, north: bool, lat_ts_deg: Option<f64>) -> Result<Self> {
let e = p.ellipsoid.e;
let a = p.ellipsoid.a;
let lon0 = to_radians(p.lon0);
let rho_coeff = match lat_ts_deg {
None => {
let k0 = p.scale;
let c = ((1.0 + e).powf(1.0 + e) * (1.0 - e).powf(1.0 - e)).sqrt();
2.0 * a * k0 / c
}
Some(lat_ts_deg) => {
let phi_ts = to_radians(lat_ts_deg.abs());
let m_ts = m_factor(e, phi_ts);
let t_ts = t_north(e, phi_ts); if t_ts.abs() < 1e-15 {
0.0
} else {
a * m_ts / t_ts
}
}
};
Ok(PolarStereographicProj {
north,
lon0,
fe: p.false_easting,
fn_: p.false_northing,
e,
rho_coeff,
})
}
}
impl ProjectionImpl for PolarStereographicProj {
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 dlon = lon - self.lon0;
let t = if self.north {
t_north(self.e, lat)
} else {
t_north(self.e, -lat) };
let rho = self.rho_coeff * t;
let e = self.fe + rho * dlon.sin();
let n = if self.north {
self.fn_ - rho * dlon.cos()
} else {
self.fn_ + rho * dlon.cos()
};
Ok((e, n))
}
fn inverse(&self, x: f64, y: f64) -> Result<(f64, f64)> {
let dx = x - self.fe;
let dy = y - self.fn_;
let rho = (dx * dx + dy * dy).sqrt();
if rho < 1e-10 {
let lat_deg = if self.north { 90.0 } else { -90.0 };
return Ok((to_degrees(self.lon0), lat_deg));
}
let t = rho / self.rho_coeff;
let lon = if self.north {
self.lon0 + dx.atan2(-dy)
} else {
self.lon0 + dx.atan2(dy)
};
let e = self.e;
let mut phi = std::f64::consts::FRAC_PI_2 - 2.0 * t.atan();
for _ in 0..20 {
let esin = e * phi.sin();
let phi_new = std::f64::consts::FRAC_PI_2
- 2.0 * (t * ((1.0 - esin) / (1.0 + esin)).powf(e / 2.0)).atan();
if (phi_new - phi).abs() < 1e-12 {
phi = phi_new;
break;
}
phi = phi_new;
}
if !self.north {
phi = -phi;
}
Ok((to_degrees(lon), to_degrees(phi)))
}
}
#[cfg(test)]
mod tests {
use crate::projections::{Projection, ProjectionKind, ProjectionParams};
use crate::ellipsoid::Ellipsoid;
const TOL_M: f64 = 1e-3; const TOL_DEG: f64 = 1e-8;
#[test]
fn ups_north_round_trip() {
let p = ProjectionParams {
kind: ProjectionKind::PolarStereographic { north: true, lat_ts: None },
lon0: 0.0,
lat0: 90.0,
false_easting: 2_000_000.0,
false_northing: 2_000_000.0,
scale: 0.994,
ellipsoid: Ellipsoid::WGS84,
..Default::default()
};
let proj = Projection::new(p).unwrap();
for &(lon, lat) in &[(0.0_f64, 85.0_f64), (90.0, 80.0), (-45.0, 75.0)] {
let (x, y) = proj.forward(lon, lat).unwrap();
let (lon2, lat2) = proj.inverse(x, y).unwrap();
assert!((lon2 - lon).abs() < TOL_DEG, "lon round-trip fail: {lon} → {lon2}");
assert!((lat2 - lat).abs() < TOL_DEG, "lat round-trip fail: {lat} → {lat2}");
}
}
#[test]
fn ups_south_round_trip() {
let p = ProjectionParams {
kind: ProjectionKind::PolarStereographic { north: false, lat_ts: None },
lon0: 0.0,
lat0: -90.0,
false_easting: 2_000_000.0,
false_northing: 2_000_000.0,
scale: 0.994,
ellipsoid: Ellipsoid::WGS84,
..Default::default()
};
let proj = Projection::new(p).unwrap();
for &(lon, lat) in &[(0.0_f64, -85.0_f64), (90.0, -80.0), (-45.0, -75.0)] {
let (x, y) = proj.forward(lon, lat).unwrap();
let (lon2, lat2) = proj.inverse(x, y).unwrap();
assert!((lon2 - lon).abs() < TOL_DEG, "lon round-trip fail: {lon} → {lon2}");
assert!((lat2 - lat).abs() < TOL_DEG, "lat round-trip fail: {lat} → {lat2}");
}
}
#[test]
fn scar_south_pole_round_trip() {
let p = ProjectionParams {
kind: ProjectionKind::PolarStereographic { north: false, lat_ts: Some(-80.0) },
lon0: -165.0,
lat0: -90.0,
false_easting: 0.0,
false_northing: 0.0,
scale: 1.0,
ellipsoid: Ellipsoid::WGS84,
..Default::default()
};
let proj = Projection::new(p).unwrap();
for &(lon, lat) in &[(-165.0_f64, -85.0_f64), (-100.0, -80.0), (-170.0, -78.0)] {
let (x, y) = proj.forward(lon, lat).unwrap();
let (lon2, lat2) = proj.inverse(x, y).unwrap();
assert!((lon2 - lon).abs() < TOL_DEG, "lon round-trip fail: {lon} → {lon2}");
assert!((lat2 - lat).abs() < TOL_DEG, "lat round-trip fail: {lat} → {lat2}");
}
}
#[test]
fn pole_maps_to_false_origin() {
let p = ProjectionParams {
kind: ProjectionKind::PolarStereographic { north: true, lat_ts: None },
lon0: 0.0,
lat0: 90.0,
false_easting: 2_000_000.0,
false_northing: 2_000_000.0,
scale: 0.994,
ellipsoid: Ellipsoid::WGS84,
..Default::default()
};
let proj = Projection::new(p).unwrap();
let (x, y) = proj.forward(0.0, 90.0).unwrap();
assert!((x - 2_000_000.0).abs() < TOL_M);
assert!((y - 2_000_000.0).abs() < TOL_M);
}
}