use oxiproj_core::{Coord, Ellipsoid, IoUnits, Lp, Operation, ProjError, ProjResult, Xy};
const PJ_EPS_LAT: f64 = 1e-12;
#[derive(Debug)]
pub struct Pj {
pub operation: Box<dyn Operation>,
pub ellipsoid: Ellipsoid,
pub lam0: f64,
pub phi0: f64,
pub x0: f64,
pub y0: f64,
pub z0: f64,
pub k0: f64,
pub to_meter: f64,
pub fr_meter: f64,
pub vto_meter: f64,
pub vfr_meter: f64,
pub from_greenwich: f64,
pub over: bool,
pub geoc: bool,
pub is_latlong: bool,
pub left: IoUnits,
pub right: IoUnits,
pub inverted: bool,
pub bypass_prepare_finalize: bool,
}
impl Pj {
fn forward_impl(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
if !v[0].is_finite() || !v[1].is_finite() || !v[2].is_finite() {
return Ok(Coord::error());
}
let prepared = if self.left == IoUnits::Radians {
let mut lam = v[0];
let mut phi = v[1];
let z = v[2];
let t = v[3];
let tt = phi.abs() - oxiproj_core::M_HALFPI;
if tt > PJ_EPS_LAT {
return Err(ProjError::InvalidCoord);
}
if !(-10.0..=10.0).contains(&lam) {
return Err(ProjError::InvalidCoord);
}
phi = phi.clamp(-oxiproj_core::M_HALFPI, oxiproj_core::M_HALFPI);
if self.geoc && self.ellipsoid.es != 0.0 {
phi = (self.ellipsoid.one_es * phi.tan()).atan();
}
if !self.over {
lam = oxiproj_core::adjlon(lam);
}
lam = (lam - self.from_greenwich) - self.lam0;
if !self.over {
lam = oxiproj_core::adjlon(lam);
}
Coord::new(lam, phi, z, t)
} else {
c
};
let mid = self.operation.forward_4d(prepared)?;
let mv = mid.v();
let mut x = mv[0];
let mut y = mv[1];
let mut z = mv[2];
let t = mv[3];
match self.right {
IoUnits::Classic | IoUnits::Projected => {
if self.right == IoUnits::Classic {
x *= self.ellipsoid.a;
y *= self.ellipsoid.a;
}
x = self.fr_meter * (x + self.x0);
y = self.fr_meter * (y + self.y0);
z = self.vfr_meter * (z + self.z0);
}
IoUnits::Radians => {
z = self.vfr_meter * (z + self.z0);
if !self.over {
x = oxiproj_core::adjlon(x);
}
}
IoUnits::Cartesian => {
x *= self.fr_meter;
y *= self.fr_meter;
z *= self.fr_meter;
}
IoUnits::Whatever | IoUnits::Degrees => {}
}
Ok(Coord::new(x, y, z, t))
}
fn inverse_impl(&self, c: Coord) -> ProjResult<Coord> {
let v = c.v();
if !v[0].is_finite() || !v[1].is_finite() || !v[2].is_finite() {
return Err(ProjError::OutsideProjectionDomain);
}
let t = v[3];
let prepared = match self.right {
IoUnits::Whatever | IoUnits::Degrees => c,
IoUnits::Cartesian => {
let x = self.to_meter * v[0];
let y = self.to_meter * v[1];
let z = self.to_meter * v[2];
Coord::new(x, y, z, t)
}
IoUnits::Projected | IoUnits::Classic => {
let mut x = self.to_meter * v[0] - self.x0;
let mut y = self.to_meter * v[1] - self.y0;
let z = self.vto_meter * v[2] - self.z0;
if self.right == IoUnits::Classic {
x *= self.ellipsoid.ra;
y *= self.ellipsoid.ra;
}
Coord::new(x, y, z, t)
}
IoUnits::Radians => {
let z = self.vto_meter * v[2] - self.z0;
Coord::new(v[0], v[1], z, t)
}
};
let mid = self.operation.inverse_4d(prepared)?;
if self.left == IoUnits::Radians {
let mv = mid.v();
let mut lam = mv[0];
let phi = mv[1];
let z = mv[2];
let t = mv[3];
lam = lam + self.from_greenwich + self.lam0;
if !self.over {
lam = oxiproj_core::adjlon(lam);
}
let phi = if self.geoc && self.ellipsoid.es != 0.0 {
let limit = oxiproj_core::M_HALFPI - 1e-9;
if phi.abs() < limit {
(self.ellipsoid.rone_es * phi.tan()).atan()
} else {
phi
}
} else {
phi
};
Ok(Coord::new(lam, phi, z, t))
} else {
Ok(mid)
}
}
pub fn forward(&self, c: Coord) -> ProjResult<Coord> {
if self.bypass_prepare_finalize {
if self.inverted {
return self.operation.inverse_4d(c);
} else {
return self.operation.forward_4d(c);
}
}
if self.inverted {
self.inverse_impl(c)
} else {
self.forward_impl(c)
}
}
pub fn inverse(&self, c: Coord) -> ProjResult<Coord> {
if self.bypass_prepare_finalize {
if self.inverted {
return self.operation.forward_4d(c);
} else {
return self.operation.inverse_4d(c);
}
}
if self.inverted {
self.forward_impl(c)
} else {
self.inverse_impl(c)
}
}
pub fn factors(&self, coord: Coord) -> ProjResult<oxiproj_core::Factors> {
let v = coord.v();
let (phi, lam_abs) = if self.left == IoUnits::Radians {
(v[1], v[0])
} else {
(v[1].to_radians(), v[0].to_radians())
};
let lam = lam_abs - self.from_greenwich - self.lam0;
let es = self.ellipsoid.es;
let one_es = self.ellipsoid.one_es;
let op = &*self.operation;
oxiproj_core::Factors::compute(phi, lam, es, one_es, |lam_f, phi_f| {
let c = Coord::new(lam_f, phi_f, 0.0, 0.0);
let r = op.forward_4d(c)?;
let rv = r.v();
Ok((rv[0], rv[1]))
})
}
}
impl Operation for Pj {
fn forward_4d(&self, c: Coord) -> ProjResult<Coord> {
self.forward(c)
}
fn inverse_4d(&self, c: Coord) -> ProjResult<Coord> {
self.inverse(c)
}
fn forward_2d(&self, lp: Lp) -> ProjResult<Xy> {
let c = self.forward(Coord::new(lp.lam, lp.phi, 0.0, 0.0))?;
Ok(c.xy())
}
fn inverse_2d(&self, xy: Xy) -> ProjResult<Lp> {
let c = self.inverse(Coord::new(xy.x, xy.y, 0.0, 0.0))?;
Ok(c.lp())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct Id;
impl Operation for Id {
fn forward_2d(&self, lp: Lp) -> ProjResult<Xy> {
Ok(Xy::new(lp.lam, lp.phi))
}
fn inverse_2d(&self, xy: Xy) -> ProjResult<Lp> {
Ok(Lp::new(xy.x, xy.y))
}
}
fn unit_latlong() -> Pj {
Pj {
operation: Box::new(Id),
ellipsoid: Ellipsoid::named("WGS84").unwrap(),
lam0: 0.0,
phi0: 0.0,
x0: 0.0,
y0: 0.0,
z0: 0.0,
k0: 1.0,
to_meter: 1.0,
fr_meter: 1.0,
vto_meter: 1.0,
vfr_meter: 1.0,
from_greenwich: 0.0,
over: false,
geoc: false,
is_latlong: true,
left: IoUnits::Radians,
right: IoUnits::Radians,
inverted: false,
bypass_prepare_finalize: false,
}
}
#[test]
fn latlong_round_trip() {
let pj = unit_latlong();
let lam = 0.2;
let phi = 0.5;
let fwd = pj.forward(Coord::new(lam, phi, 0.0, 0.0)).unwrap();
let inv = pj.inverse(fwd).unwrap();
let r = inv.v();
assert!((r[0] - lam).abs() < 1e-12);
assert!((r[1] - phi).abs() < 1e-12);
}
#[test]
fn non_finite_forward_yields_error_coord() {
let pj = unit_latlong();
let out = pj.forward(Coord::new(f64::NAN, 0.0, 0.0, 0.0)).unwrap();
assert!(out.is_error());
}
#[test]
fn out_of_domain_latitude_rejected() {
let pj = unit_latlong();
let res = pj.forward(Coord::new(0.0, 2.0, 0.0, 0.0));
assert!(res.is_err());
}
}