use crate::context::Context;
use crate::params::{ParamList, ParamView};
use crate::pj::Pj;
use oxiproj_core::{Coord, Ellipsoid, Operation, ProjError, ProjResult};
fn parse_ratio_factor(s: &str) -> ProjResult<f64> {
let s = s.trim();
let (num, consumed) = oxiproj_core::parse_leading_f64(s).ok_or(ProjError::IllegalArgValue)?;
let rest = s.get(consumed..).unwrap_or("");
let value = match rest.strip_prefix('/') {
Some(after) => {
let (denom, _) =
oxiproj_core::parse_leading_f64(after).ok_or(ProjError::IllegalArgValue)?;
if denom == 0.0 {
return Err(ProjError::IllegalArgValue);
}
num / denom
}
None => num,
};
if value <= 0.0 {
return Err(ProjError::IllegalArgValue);
}
Ok(value)
}
fn probe_axisswap_order(op: &dyn Operation) -> Option<[i8; 4]> {
const PROBE: [f64; 4] = [1.0, 2.0, 4.0, 8.0];
let out = op
.forward_4d(Coord::new(PROBE[0], PROBE[1], PROBE[2], PROBE[3]))
.ok()?;
let v = out.v();
let mut order = [0i8; 4];
let mut seen = [false; 4];
for (i, slot) in order.iter_mut().enumerate() {
let val = v[i];
if !val.is_finite() || val == 0.0 {
return None;
}
let mag = val.abs();
let axis = PROBE.iter().position(|&p| p == mag)?;
if seen[axis] {
return None;
}
seen[axis] = true;
let sign: i8 = if val < 0.0 { -1 } else { 1 };
*slot = sign * (axis as i8 + 1);
}
Some(order)
}
fn ad_forward_is_proj_consistent(
ad: &dyn oxiproj_core::ProjectGenericBox,
op: &dyn Operation,
es: f64,
one_es: f64,
) -> bool {
use oxiproj_core::autodiff::Dual1;
const TOL: f64 = 1e-6;
const LAM_REL: f64 = 0.08;
const CANDIDATE_PHI: [f64; 12] = [
0.5, -0.5, 0.7, -0.7, 0.9, -0.9, 0.3, -0.3, 1.1, -1.1, 0.2, -0.2,
];
for &phi in &CANDIDATE_PHI {
let numeric =
match oxiproj_core::Factors::compute(phi, LAM_REL, es, one_es, |lam_f, phi_f| {
let r = op.forward_4d(Coord::new(lam_f, phi_f, 0.0, 0.0))?;
let rv = r.v();
Ok((rv[0], rv[1]))
}) {
Ok(f) => f,
Err(_) => continue,
};
let lam_d = Dual1::<2>::variable(LAM_REL, 0);
let phi_d = Dual1::<2>::variable(phi, 1);
let (x, y) = match ad.project_fwd_dual2(lam_d, phi_d) {
Ok(v) => v,
Err(_) => continue,
};
let exact = oxiproj_core::FactorsExact::from_jacobian_es(
x.d[0], x.d[1], y.d[0], y.d[1], phi, 1.0, es,
);
let dh = (exact.meridian_scale - numeric.h).abs();
let dk = (exact.parallel_scale - numeric.k).abs();
return dh <= TOL * (1.0 + numeric.h.abs()) && dk <= TOL * (1.0 + numeric.k.abs());
}
false
}
fn projection_forward_ignores_es(op_ell: &dyn Operation, op_sph: &dyn Operation) -> bool {
const LAM_REL: f64 = 0.08;
const CANDIDATE_PHI: [f64; 12] = [
0.5, -0.5, 0.7, -0.7, 0.9, -0.9, 0.3, -0.3, 1.1, -1.1, 0.2, -0.2,
];
let mut compared_any = false;
for &phi in &CANDIDATE_PHI {
let probe = Coord::new(LAM_REL, phi, 0.0, 0.0);
let e = match op_ell.forward_4d(probe) {
Ok(v) => v.v(),
Err(_) => continue,
};
let s = match op_sph.forward_4d(probe) {
Ok(v) => v.v(),
Err(_) => continue,
};
if !e[0].is_finite() || !e[1].is_finite() || !s[0].is_finite() || !s[1].is_finite() {
continue;
}
compared_any = true;
let tol = 1e-9 * (1.0 + e[0].abs().max(e[1].abs()));
if (e[0] - s[0]).abs() > tol || (e[1] - s[1]).abs() > tol {
return false;
}
}
compared_any
}
fn parse_lon_wrap(params: &ParamList) -> ProjResult<Option<f64>> {
let raw = match params.get_str("lon_wrap") {
Some(s) => s,
None => return Ok(None),
};
let center = if raw.trim().is_empty() {
0.0
} else {
oxiproj_core::dmstor(raw).map_err(|_| ProjError::IllegalArgValue)?
};
if !center.is_finite() || center.abs() >= 10.0 * oxiproj_core::M_TWOPI {
return Err(ProjError::IllegalArgValue);
}
Ok(Some(center))
}
pub fn build_single_op(
name: &str,
params: &ParamList,
ellipsoid: Ellipsoid,
context: &Context,
) -> ProjResult<Pj> {
let lam0 = params.get_dms("lon_0").unwrap_or(0.0);
let phi0 = params.get_dms("lat_0").unwrap_or(0.0);
let x0 = params.get_f64("x_0").unwrap_or(0.0);
let y0 = params.get_f64("y_0").unwrap_or(0.0);
let z0 = params.get_f64("z_0").unwrap_or(0.0);
let k0 = params
.get_f64("k_0")
.or_else(|| params.get_f64("k"))
.unwrap_or(1.0);
let (to_meter, fr_meter) = match params.get_str("to_meter") {
Some(s) if !s.trim().is_empty() => {
let v = parse_ratio_factor(s)?;
(v, 1.0 / v)
}
_ => match params.get_str("units") {
Some(u) => match oxiproj_core::find_linear_unit(u) {
Some(ud) => (ud.factor, 1.0 / ud.factor),
None => (1.0, 1.0),
},
None => (1.0, 1.0),
},
};
let (vto_meter, vfr_meter) = match params.get_str("vto_meter") {
Some(s) if !s.trim().is_empty() => {
let v = parse_ratio_factor(s)?;
(v, 1.0 / v)
}
_ => match params.get_str("vunits") {
Some(u) => match oxiproj_core::find_linear_unit(u) {
Some(ud) => (ud.factor, 1.0 / ud.factor),
None => (1.0, 1.0),
},
None => (1.0, 1.0),
},
};
let over = params.get_bool("over");
let geoc = params.get_bool("geoc");
let lon_wrap_center = parse_lon_wrap(params)?;
let from_greenwich = match params.get_str("pm") {
Some(pm) => oxiproj_core::prime_meridian_offset(pm)
.or_else(|_| oxiproj_core::dmstor(pm))
.unwrap_or(0.0),
None => 0.0,
};
let view = ParamView(params);
let pp = oxiproj_projections::ProjParams {
ellipsoid: &ellipsoid,
phi0,
k0,
params: &view,
};
match oxiproj_projections::build(name, &pp) {
Ok(build) => {
let factors_es = if ellipsoid.es != 0.0 {
let sphere_only = Ellipsoid::sphere(ellipsoid.a)
.ok()
.and_then(|sph| {
let pp_sph = oxiproj_projections::ProjParams {
ellipsoid: &sph,
phi0,
k0,
params: &view,
};
oxiproj_projections::build(name, &pp_sph).ok().map(|b| {
projection_forward_ignores_es(&*build.operation, &*b.operation)
})
})
.unwrap_or(false);
if sphere_only {
0.0
} else {
ellipsoid.es
}
} else {
0.0
};
let lam0 = build.lam0_override.unwrap_or(lam0);
let k0 = build.k0_override.unwrap_or(k0);
let x0 = build.x0_override.unwrap_or(x0);
let y0 = build.y0_override.unwrap_or(y0);
let phi0 = build.phi0_override.unwrap_or(phi0);
let over = build.over_override.unwrap_or(over);
let ad_proj = build.ad_proj.filter(|ad| {
ad_forward_is_proj_consistent(
&**ad,
&*build.operation,
ellipsoid.es,
ellipsoid.one_es,
)
});
Ok(Pj {
operation: build.operation,
ellipsoid,
factors_es,
lam0,
phi0,
x0,
y0,
z0,
k0,
to_meter,
fr_meter,
vto_meter,
vfr_meter,
from_greenwich,
over,
geoc,
lon_wrap_center,
is_latlong: build.is_latlong,
left: build.left,
right: build.right,
inverted: false,
bypass_prepare_finalize: false,
omit_fwd: false,
omit_inv: false,
ad_proj,
op_name: name.to_string(),
axisswap_order: None,
})
}
Err(oxiproj_core::ProjError::InvalidOp) => {
let tp = oxiproj_transformations::TransParams {
ellipsoid: &ellipsoid,
params: &view,
registry: Some(context as &dyn oxiproj_transformations::GridRegistry),
};
let tb = oxiproj_transformations::build(name, &tp)?;
let axisswap_order = if name == "axisswap" {
probe_axisswap_order(&*tb.operation)
} else {
None
};
let cartesian_finalize = matches!(name, "cart" | "geocent");
if cartesian_finalize {
Ok(Pj {
operation: tb.operation,
ellipsoid,
factors_es: ellipsoid.es,
lam0,
phi0: 0.0,
x0: 0.0,
y0: 0.0,
z0: 0.0,
k0: 1.0,
to_meter,
fr_meter,
vto_meter,
vfr_meter,
from_greenwich,
over,
geoc: false,
lon_wrap_center,
is_latlong: false,
left: tb.left,
right: tb.right,
inverted: false,
bypass_prepare_finalize: false,
omit_fwd: false,
omit_inv: false,
ad_proj: None,
op_name: name.to_string(),
axisswap_order,
})
} else {
Ok(Pj {
operation: tb.operation,
ellipsoid,
factors_es: ellipsoid.es,
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,
lon_wrap_center,
is_latlong: false,
left: tb.left,
right: tb.right,
inverted: false,
bypass_prepare_finalize: true,
omit_fwd: false,
omit_inv: false,
ad_proj: None,
op_name: name.to_string(),
axisswap_order,
})
}
}
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::params::parse;
use oxiproj_core::{Coord, DEG_TO_RAD};
#[test]
fn merc_known_value() {
let ell = Ellipsoid::named("WGS84").unwrap();
let pj = build_single_op(
"merc",
&parse("+proj=merc +ellps=WGS84"),
ell,
&Context::new(),
)
.unwrap();
let out = pj
.forward(Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0))
.unwrap();
let o = out.v();
assert!((o[0] - 1335833.8895192828).abs() < 1e-6, "x got {}", o[0]);
assert!(
(o[1] - 7_326_837.715_045_549).abs() < 1e-6,
"y got {}",
o[1]
);
}
#[test]
fn to_meter_ratio_parses() {
assert!((parse_ratio_factor("1/0.3048").unwrap() - 3.280839895013123).abs() < 1e-12);
assert!((parse_ratio_factor("0.3048").unwrap() - 0.3048).abs() < 1e-15);
assert!(parse_ratio_factor("1/0").is_err(), "zero denominator");
assert!(parse_ratio_factor("0").is_err(), "non-positive factor");
}
#[test]
fn to_meter_ratio_scales_output() {
let ell = Ellipsoid::named("WGS84").unwrap();
let pj = build_single_op(
"merc",
&parse("+proj=merc +ellps=WGS84 +to_meter=1/0.3048"),
ell,
&Context::new(),
)
.unwrap();
let out = pj
.forward(Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0))
.unwrap();
let o = out.v();
assert!(
(o[0] - 0.3048 * 1335833.8895192828).abs() < 1e-3,
"x got {}",
o[0]
);
assert!(
(o[1] - 0.3048 * 7_326_837.715_045_549).abs() < 1e-3,
"y got {}",
o[1]
);
}
#[test]
fn utm_central_meridian_origin() {
let ell = Ellipsoid::named("WGS84").unwrap();
let pj = build_single_op(
"utm",
&parse("+proj=utm +zone=32 +ellps=WGS84"),
ell,
&Context::new(),
)
.unwrap();
let out = pj
.forward(Coord::new(9.0 * DEG_TO_RAD, 0.0, 0.0, 0.0))
.unwrap();
let o = out.v();
assert!((o[0] - 500000.0).abs() < 1e-6, "x got {}", o[0]);
assert!((o[1] - 0.0).abs() < 1e-6, "y got {}", o[1]);
}
}