use crate::context::Context;
use crate::params::{parse, ParamList};
use crate::pipeline::Pipeline;
use crate::pj::Pj;
use oxiproj_core::{Coord, Direction, Ellipsoid, IoUnits, ProjError, ProjResult};
const ELLIPSOID_KEYS: [&str; 9] = ["R", "a", "ellps", "datum", "rf", "f", "es", "e", "b"];
fn has_ellipsoid_def(pl: &ParamList) -> bool {
ELLIPSOID_KEYS.iter().any(|k| pl.exists(k))
}
fn effective_left(s: &Pj) -> IoUnits {
let u = if s.inverted { s.right } else { s.left };
match u {
IoUnits::Classic => IoUnits::Projected,
other => other,
}
}
fn effective_right(s: &Pj) -> IoUnits {
let u = if s.inverted { s.left } else { s.right };
match u {
IoUnits::Classic => IoUnits::Projected,
other => other,
}
}
fn propagate_whatever_units(steps: &mut [Pj]) {
let nsteps = steps.len();
if nsteps < 2 {
return;
}
for i in (0..nsteps - 1).rev() {
if effective_left(&steps[i]) == IoUnits::Whatever
&& effective_right(&steps[i]) == IoUnits::Whatever
{
let right_left = effective_left(&steps[i + 1]);
let right_right = effective_right(&steps[i + 1]);
if right_left != right_right || right_left != IoUnits::Whatever {
steps[i].left = right_left;
steps[i].right = right_left;
}
}
}
for i in 1..nsteps {
if effective_left(&steps[i]) == IoUnits::Whatever
&& effective_right(&steps[i]) == IoUnits::Whatever
{
let left_left = effective_left(&steps[i - 1]);
let left_right = effective_right(&steps[i - 1]);
if left_left != left_right || left_right != IoUnits::Whatever {
steps[i].left = left_right;
steps[i].right = left_right;
}
}
}
}
pub fn create(proj_string: &str) -> ProjResult<Pj> {
let ctx = Context::new();
let params = parse(proj_string);
let is_pipeline = params
.entries
.iter()
.any(|(k, v)| k == "proj" && v.as_deref() == Some("pipeline"));
if is_pipeline {
create_pipeline(¶ms, &ctx)
} else {
create_single(¶ms, &ctx)
}
}
pub fn create_with_ctx(proj_string: &str, ctx: &Context) -> ProjResult<Pj> {
let params = parse(proj_string);
let is_pipeline = params
.entries
.iter()
.any(|(k, v)| k == "proj" && v.as_deref() == Some("pipeline"));
if is_pipeline {
create_pipeline(¶ms, ctx)
} else {
create_single(¶ms, ctx)
}
}
pub fn create_projection_only(proj_string: &str) -> ProjResult<Pj> {
let ctx = Context::new();
let params = parse(proj_string);
let is_pipeline = params
.entries
.iter()
.any(|(k, v)| k == "proj" && v.as_deref() == Some("pipeline"));
if is_pipeline {
create_pipeline(¶ms, &ctx)
} else {
create_single_projection_only(¶ms, &ctx)
}
}
fn create_single_projection_only(params: &ParamList, ctx: &Context) -> ProjResult<Pj> {
let name_is_axisswap = params
.entries
.iter()
.any(|(k, v)| k == "proj" && v.as_deref() == Some("axisswap"));
let axis = if name_is_axisswap {
None
} else {
params
.get_str("axis")
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && s != "enu")
};
if axis.is_none() {
return create_single_core(params, ctx);
}
let src_ell = crate::setup::setup_ellipsoid(params)?;
let mut pipeline_str = String::from("+proj=pipeline");
if params
.entries
.iter()
.any(|(k, v)| (k == "inv" || k == "inverted") && v.is_none())
{
pipeline_str.push_str(" +inv");
}
pipeline_str.push_str(" +step ");
pipeline_str.push_str(&build_projection_step(params, &src_ell));
if let Some(ax) = &axis {
pipeline_str.push_str(" +step +proj=axisswap +axis=");
pipeline_str.push_str(ax);
}
let pipeline_params = parse(&pipeline_str);
create_pipeline(&pipeline_params, ctx)
}
fn extract_towgs84_from_params(params: &ParamList) -> Option<String> {
if let Some(s) = params.get_str("towgs84") {
if !s.is_empty() {
return Some(s.to_string());
}
}
if let Some(datum_name) = params.get_str("datum") {
if !datum_name.is_empty() {
if let Some(d) = oxiproj_core::find_datum(datum_name) {
if let Some(rest) = d.defn.strip_prefix("towgs84=") {
return Some(rest.to_string());
}
}
}
}
None
}
fn normalize_towgs84(s: &str) -> String {
let mut vals: Vec<String> = s.split(',').map(|v| v.trim().to_string()).collect();
if vals.len() > 7 {
vals.truncate(7);
} else if vals.len() > 3 {
while vals.len() < 7 {
vals.push("0".to_string());
}
} else {
while vals.len() < 3 {
vals.push("0".to_string());
}
}
vals.join(",")
}
fn towgs84_is_identity(vals: &str) -> bool {
vals.split(',')
.all(|v| matches!(v.trim(), "0" | "0.0" | "-0" | "-0.0" | ""))
}
fn is_wgs84_ellipsoid(ell: &Ellipsoid) -> bool {
(ell.a - 6378137.0).abs() < 1e-8 && (ell.es - 0.0066943799901413).abs() < 1e-15
}
fn strip_from_projection_step(key: &str) -> bool {
matches!(
key,
"towgs84" | "nadgrids" | "geoidgrids" | "axis" | "inv" | "inverted"
) || ELLIPSOID_KEYS.contains(&key)
}
fn build_projection_step(params: &ParamList, src_ell: &Ellipsoid) -> String {
let mut out: String = params
.entries
.iter()
.filter(|(k, _)| !strip_from_projection_step(k))
.map(|(k, v)| match v {
Some(val) => format!("+{}={}", k, val),
None => format!("+{}", k),
})
.collect::<Vec<_>>()
.join(" ");
out.push_str(&format!(" +a={} +es={}", src_ell.a, src_ell.es));
out
}
fn extract_nadgrids_from_params(params: &ParamList) -> Option<String> {
if let Some(s) = params.get_str("nadgrids") {
if !s.is_empty() {
return Some(s.to_string());
}
}
None
}
fn extract_geoidgrids_from_params(params: &ParamList) -> Option<String> {
if let Some(s) = params.get_str("geoidgrids") {
if !s.is_empty() {
return Some(s.to_string());
}
}
None
}
fn create_single(params: &ParamList, ctx: &Context) -> ProjResult<Pj> {
let name = params
.entries
.iter()
.find(|(k, _)| k == "proj")
.and_then(|(_, v)| v.as_deref())
.ok_or(ProjError::MissingArg)?;
let src_ell = crate::setup::setup_ellipsoid(params)?;
let mut pj = build_single_emulated(name, params, src_ell, ctx)?;
if params
.entries
.iter()
.any(|(k, v)| (k == "inv" || k == "inverted") && v.is_none())
{
pj.inverted = true;
}
Ok(pj)
}
fn op_is_projection(name: &str, params: &ParamList, ellipsoid: &Ellipsoid) -> bool {
let phi0 = params.get_dms("lat_0").unwrap_or(0.0);
let k0 = params
.get_f64("k_0")
.or_else(|| params.get_f64("k"))
.unwrap_or(1.0);
let view = crate::params::ParamView(params);
let pp = oxiproj_projections::ProjParams {
ellipsoid,
phi0,
k0,
params: &view,
};
!matches!(
oxiproj_projections::build(name, &pp),
Err(ProjError::InvalidOp)
)
}
fn build_single_emulated(
name: &str,
params: &ParamList,
src_ell: Ellipsoid,
ctx: &Context,
) -> ProjResult<Pj> {
let towgs84 = extract_towgs84_from_params(params);
let nadgrids = extract_nadgrids_from_params(params);
let geoidgrids = extract_geoidgrids_from_params(params);
let axis = if name == "axisswap" {
None
} else {
params
.get_str("axis")
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && s != "enu")
};
let needs_emulation =
towgs84.is_some() || nadgrids.is_some() || geoidgrids.is_some() || axis.is_some();
if !needs_emulation || !op_is_projection(name, params, &src_ell) {
return crate::registry::build_single_op(name, params, src_ell, ctx);
}
let mut prepare_steps: Vec<String> = Vec::new();
if let Some(grid) = &nadgrids {
prepare_steps.push(format!("+proj=hgridshift +grids={grid} +inv"));
} else if let Some(tw) = &towgs84 {
let vals = normalize_towgs84(tw);
if towgs84_is_identity(&vals) {
if !is_wgs84_ellipsoid(&src_ell) {
prepare_steps.push("+proj=cart +ellps=WGS84".to_string());
prepare_steps.push(format!(
"+proj=cart +inv +a={} +es={}",
src_ell.a, src_ell.es
));
}
} else {
prepare_steps.push("+proj=cart +ellps=WGS84".to_string());
prepare_steps.push(format!(
"+proj=helmert +towgs84={vals} +convention=position_vector +exact +inv"
));
prepare_steps.push(format!(
"+proj=cart +inv +a={} +es={}",
src_ell.a, src_ell.es
));
}
}
if let Some(grids) = &geoidgrids {
prepare_steps.push(format!("+proj=vgridshift +grids={grids}"));
}
if prepare_steps.is_empty() && axis.is_none() {
return crate::registry::build_single_op(name, params, src_ell, ctx);
}
let mut pipeline_str = String::from("+proj=pipeline");
for step in &prepare_steps {
pipeline_str.push_str(" +step ");
pipeline_str.push_str(step);
}
pipeline_str.push_str(" +step ");
pipeline_str.push_str(&build_projection_step(params, &src_ell));
if let Some(ax) = &axis {
pipeline_str.push_str(" +step +proj=axisswap +axis=");
pipeline_str.push_str(ax);
}
let pipeline_params = parse(&pipeline_str);
create_pipeline(&pipeline_params, ctx)
}
fn create_single_core(params: &ParamList, ctx: &Context) -> ProjResult<Pj> {
let name = params
.entries
.iter()
.find(|(k, _)| k == "proj")
.and_then(|(_, v)| v.as_deref())
.ok_or(ProjError::MissingArg)?;
let ellipsoid = crate::setup::setup_ellipsoid(params)?;
let mut pj = crate::registry::build_single_op(name, params, ellipsoid, ctx)?;
if params
.entries
.iter()
.any(|(k, v)| (k == "inv" || k == "inverted") && v.is_none())
{
pj.inverted = true;
}
Ok(pj)
}
fn create_pipeline(params: &ParamList, ctx: &Context) -> ProjResult<Pj> {
let mut global: Vec<(String, Option<String>)> = Vec::new();
let mut steps_entries: Vec<Vec<(String, Option<String>)>> = Vec::new();
let mut seen_pipeline = false;
let mut seen_step = false;
for (k, v) in ¶ms.entries {
if k == "step" && v.is_none() {
if !seen_pipeline {
return Err(ProjError::InvalidOp);
}
seen_step = true;
steps_entries.push(Vec::new());
continue;
}
if k == "proj" && v.as_deref() == Some("pipeline") {
if seen_pipeline {
return Err(ProjError::InvalidOp);
}
seen_pipeline = true;
continue;
}
if !seen_step {
if k == "proj" || k == "o_proj" {
return Err(ProjError::InvalidOp);
}
global.push((k.clone(), v.clone()));
} else if let Some(last) = steps_entries.last_mut() {
last.push((k.clone(), v.clone()));
}
}
if !seen_pipeline {
return Err(ProjError::InvalidOp);
}
let global_pl = ParamList { entries: global };
let global_ellipsoid = if has_ellipsoid_def(&global_pl) {
crate::setup::setup_ellipsoid(&global_pl)?
} else {
Ellipsoid::from_a_rf(6378137.0, 298.257222101)?
};
let appended_globals: Vec<(String, Option<String>)> = global_pl
.entries
.iter()
.filter(|(k, _)| {
k != "inv"
&& k != "inverted"
&& k != "step"
&& k != "proj"
&& k != "omit_fwd"
&& k != "omit_inv"
&& !ELLIPSOID_KEYS.contains(&k.as_str())
})
.cloned()
.collect();
let mut steps: Vec<Pj> = Vec::new();
for step_vec in steps_entries {
let step_pl = ParamList { entries: step_vec };
let name = step_pl.get_str("proj").ok_or(ProjError::MissingArg)?;
if name.is_empty() {
return Err(ProjError::MissingArg);
}
let inverted = step_pl
.entries
.iter()
.any(|(k, v)| k == "inv" && v.is_none());
let omit_fwd = step_pl
.entries
.iter()
.any(|(k, v)| k == "omit_fwd" && v.is_none());
let omit_inv = step_pl
.entries
.iter()
.any(|(k, v)| k == "omit_inv" && v.is_none());
let step_ellipsoid = if has_ellipsoid_def(&step_pl) {
crate::setup::setup_ellipsoid(&step_pl)?
} else {
global_ellipsoid
};
let mut augmented = step_pl.entries.clone();
augmented.extend(appended_globals.iter().cloned());
let augmented_pl = ParamList { entries: augmented };
let mut step_pj = build_single_emulated(name, &augmented_pl, step_ellipsoid, ctx)?;
step_pj.inverted = inverted;
step_pj.omit_fwd = omit_fwd;
step_pj.omit_inv = omit_inv;
steps.push(step_pj);
}
if steps.is_empty() {
return Err(ProjError::MissingArg);
}
let mut steps = crate::pipeline_opt::optimize_pipeline(steps);
if steps.is_empty() {
return Err(ProjError::MissingArg);
}
propagate_whatever_units(&mut steps);
let top_inverted = global_pl
.entries
.iter()
.any(|(k, v)| k == "inv" && v.is_none());
for pair in steps.windows(2) {
let curr_right = effective_right(&pair[0]);
let next_left = effective_left(&pair[1]);
if curr_right == IoUnits::Whatever || next_left == IoUnits::Whatever {
continue;
}
if curr_right != next_left {
return Err(ProjError::InvalidOp);
}
}
for step in &steps {
let omitted_in_fwd = if top_inverted {
step.omit_inv
} else {
step.omit_fwd
};
if omitted_in_fwd {
continue;
}
let net_inverted = top_inverted ^ step.inverted;
if net_inverted && !step.inner_has_inverse() {
return Err(ProjError::NoInverseOp);
}
}
let (left, right) = if top_inverted {
(
steps
.last()
.map(effective_right)
.unwrap_or(IoUnits::Whatever),
steps
.first()
.map(effective_left)
.unwrap_or(IoUnits::Whatever),
)
} else {
(
steps
.first()
.map(effective_left)
.unwrap_or(IoUnits::Whatever),
steps
.last()
.map(effective_right)
.unwrap_or(IoUnits::Whatever),
)
};
Ok(Pj {
operation: Box::new(Pipeline { steps }),
ellipsoid: global_ellipsoid,
factors_es: global_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: None,
is_latlong: false,
left,
right,
inverted: top_inverted,
bypass_prepare_finalize: true,
omit_fwd: false,
omit_inv: false,
ad_proj: None,
op_name: String::new(),
axisswap_order: None,
})
}
pub fn trans(pj: &Pj, dir: Direction, c: Coord) -> ProjResult<Coord> {
match dir {
Direction::Fwd => pj.forward(c),
Direction::Inv => pj.inverse(c),
Direction::Ident => Ok(c),
}
}
pub fn trans_array(pj: &Pj, dir: Direction, coords: &mut [Coord]) -> ProjResult<()> {
for c in coords.iter_mut() {
match trans(pj, dir, *c) {
Ok(r) => *c = r,
Err(_) => *c = Coord::error(),
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use oxiproj_core::DEG_TO_RAD;
#[test]
fn create_rejects_zero_semi_major_axis() {
assert!(create("+proj=longlat +a=0 +b=0 +ellps=WGS84").is_err());
assert!(create("+proj=longlat +a=0 +b=0").is_err());
}
#[test]
fn create_rejects_negative_semi_major_axis() {
assert!(create("+proj=longlat +a=-6378137 +b=-6356752").is_err());
}
#[test]
fn create_rejects_negative_flattening() {
assert!(create("+proj=merc +a=6378137 +rf=-298.257223563").is_err());
}
#[test]
fn create_pipeline_rejects_degenerate_step_ellipsoid() {
assert!(create(
"+proj=pipeline +step +proj=merc +ellps=WGS84 +step +proj=longlat +a=0 +b=0"
)
.is_err());
}
#[test]
fn merc_forward_and_round_trip() {
let pj = create("+proj=merc +ellps=WGS84").unwrap();
let input = Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0);
let fwd = trans(&pj, Direction::Fwd, input).unwrap();
let f = fwd.v();
assert!((f[0] - 1335833.8895192828).abs() < 1e-6, "x got {}", f[0]);
assert!(
(f[1] - 7_326_837.715_045_549).abs() < 1e-6,
"y got {}",
f[1]
);
let inv = trans(&pj, Direction::Inv, fwd).unwrap();
let i = inv.v();
assert!((i[0] - 12.0 * DEG_TO_RAD).abs() < 1e-9, "lam got {}", i[0]);
assert!((i[1] - 55.0 * DEG_TO_RAD).abs() < 1e-9, "phi got {}", i[1]);
}
#[test]
fn utm_known_values() {
let pj = create("+proj=utm +zone=32 +ellps=WGS84").unwrap();
let at_origin = trans(
&pj,
Direction::Fwd,
Coord::new(9.0 * DEG_TO_RAD, 0.0, 0.0, 0.0),
)
.unwrap();
let o = at_origin.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]);
let p = trans(
&pj,
Direction::Fwd,
Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
let pv = p.v();
assert!(
(pv[0] - 691_875.632_137_542).abs() < 1e-6,
"x got {}",
pv[0]
);
assert!(
(pv[1] - 6_098_907.825_129_169).abs() < 1e-6,
"y got {}",
pv[1]
);
}
#[test]
fn etmerc_central_meridian() {
let pj = create("+proj=etmerc +lon_0=9 +ellps=WGS84").unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(9.0 * DEG_TO_RAD, 50.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
let o = out.v();
assert!(o[0].abs() < 1e-6, "x got {}", o[0]);
assert!(
(o[1] - 5_540_847.041_684_148).abs() < 1e-6,
"y got {}",
o[1]
);
}
#[test]
fn pipeline_utm_round_trip() {
let pj = create(
"+proj=pipeline +step +proj=utm +zone=32 +ellps=WGS84 +step +proj=utm +zone=32 +ellps=WGS84 +inv",
)
.unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
let o = out.v();
assert!((o[0] - 12.0 * DEG_TO_RAD).abs() < 1e-9, "lam got {}", o[0]);
assert!((o[1] - 55.0 * DEG_TO_RAD).abs() < 1e-9, "phi got {}", o[1]);
}
#[test]
fn trans_array_maps_in_place() {
let pj = create("+proj=merc +ellps=WGS84").unwrap();
let mut coords = [
Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0),
Coord::new(0.0, 0.0, 0.0, 0.0),
];
trans_array(&pj, Direction::Fwd, &mut coords).unwrap();
assert!((coords[0].v()[0] - 1335833.8895192828).abs() < 1e-6);
assert!(coords[1].v()[0].abs() < 1e-6);
}
fn make_uniform_shift_ntv2(lat_sec: f32, lon_sec: f32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(b"NUM_OREC");
buf.extend_from_slice(&11i32.to_le_bytes());
buf.extend_from_slice(&[0u8; 4]);
buf.extend_from_slice(b"NUM_SREC");
buf.extend_from_slice(&11i32.to_le_bytes());
buf.extend_from_slice(&[0u8; 4]);
buf.extend_from_slice(b"NUM_FILE");
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&[0u8; 4]);
buf.extend_from_slice(b"GS_TYPE ");
buf.extend_from_slice(b"SECONDS ");
buf.extend_from_slice(&[0u8; 112]);
buf.extend_from_slice(b"SUB_NAME");
buf.extend_from_slice(b"TESTGRID");
buf.extend_from_slice(b"PARENT ");
buf.extend_from_slice(b"NONE ");
buf.extend_from_slice(b"CREATED ");
buf.extend_from_slice(b"20240101");
buf.extend_from_slice(b"UPDATED ");
buf.extend_from_slice(b"20240101");
buf.extend_from_slice(b"S_LAT ");
buf.extend_from_slice(&0.0f64.to_le_bytes());
buf.extend_from_slice(b"N_LAT ");
buf.extend_from_slice(&3600.0f64.to_le_bytes());
buf.extend_from_slice(b"E_LONG ");
buf.extend_from_slice(&0.0f64.to_le_bytes());
buf.extend_from_slice(b"W_LONG ");
buf.extend_from_slice(&3600.0f64.to_le_bytes());
buf.extend_from_slice(b"LAT_INC ");
buf.extend_from_slice(&3600.0f64.to_le_bytes());
buf.extend_from_slice(b"LONG_INC");
buf.extend_from_slice(&3600.0f64.to_le_bytes());
buf.extend_from_slice(b"GS_COUNT");
buf.extend_from_slice(&4i32.to_le_bytes());
buf.extend_from_slice(&[0u8; 4]);
for _ in 0..4 {
buf.extend_from_slice(&lat_sec.to_le_bytes()); buf.extend_from_slice(&lon_sec.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); buf.extend_from_slice(&0.0f32.to_le_bytes()); }
buf
}
fn make_zero_shift_ntv2() -> Vec<u8> {
make_uniform_shift_ntv2(0.0, 0.0)
}
#[test]
fn nadgrids_pipeline_constructed() {
let mut ctx = crate::context::Context::new();
ctx.register_grid("test.gsb", make_zero_shift_ntv2());
let pj = create_with_ctx("+proj=merc +ellps=WGS84 +nadgrids=test.gsb", &ctx).unwrap();
let plain = create("+proj=merc +ellps=WGS84").unwrap();
let coord = Coord::new(-0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 0.0, 0.0);
let shifted = trans(&pj, Direction::Fwd, coord).unwrap();
let expected = trans(&plain, Direction::Fwd, coord).unwrap();
let sv = shifted.v();
let ev = expected.v();
assert!(
(sv[0] - ev[0]).abs() < 1e-3,
"x: got {}, expected {}",
sv[0],
ev[0]
);
assert!(
(sv[1] - ev[1]).abs() < 1e-3,
"y: got {}, expected {}",
sv[1],
ev[1]
);
}
#[test]
fn pipeline_omit_fwd_skips_middle_step() {
let pj = create(
"+proj=pipeline \
+step +proj=noop \
+step +proj=axisswap +order=2,1 +omit_fwd \
+step +proj=noop",
)
.unwrap();
let input = Coord::new(1.0, 2.0, 3.0, 0.0);
let fwd = trans(&pj, Direction::Fwd, input).unwrap();
let fv = fwd.v();
assert!(
(fv[0] - 1.0).abs() < 1e-12,
"forward x should be 1.0, got {}",
fv[0]
);
assert!(
(fv[1] - 2.0).abs() < 1e-12,
"forward y should be 2.0, got {}",
fv[1]
);
let inv = trans(&pj, Direction::Inv, input).unwrap();
let iv = inv.v();
assert!(
(iv[0] - 2.0).abs() < 1e-12,
"inverse x should be 2.0 (swapped), got {}",
iv[0]
);
assert!(
(iv[1] - 1.0).abs() < 1e-12,
"inverse y should be 1.0 (swapped), got {}",
iv[1]
);
}
#[test]
fn optimizer_removes_double_omit_step() {
let pj = create(
"+proj=pipeline \
+step +proj=noop \
+step +proj=noop +omit_fwd +omit_inv \
+step +proj=noop",
)
.expect("pipeline with double-omit middle step");
let input = Coord::new(1.0, 2.0, 3.0, 0.0);
let fwd = trans(&pj, Direction::Fwd, input).expect("forward through optimized pipeline");
let fv = fwd.v();
assert!(
(fv[0] - 1.0).abs() < 1e-12,
"x should pass through unchanged, got {}",
fv[0]
);
assert!(
(fv[1] - 2.0).abs() < 1e-12,
"y should pass through unchanged, got {}",
fv[1]
);
}
#[test]
fn optimizer_cancels_axisswap_pair() {
let pj = create(
"+proj=pipeline \
+step +proj=noop \
+step +proj=axisswap +order=2,1 \
+step +proj=axisswap +order=2,1",
)
.expect("pipeline with noop + cancelling axisswap pair");
let input = Coord::new(3.0, 7.0, 0.0, 0.0);
let fwd = trans(&pj, Direction::Fwd, input).expect("forward through optimized pipeline");
let fv = fwd.v();
assert!(
(fv[0] - 3.0).abs() < 1e-12,
"x should be unchanged after axisswap cancellation, got {}",
fv[0]
);
assert!(
(fv[1] - 7.0).abs() < 1e-12,
"y should be unchanged after axisswap cancellation, got {}",
fv[1]
);
}
#[test]
fn optimizer_keeps_non_cancelling_three_cycle_pair() {
let pj = create(
"+proj=pipeline \
+step +proj=axisswap +order=2,3,1 \
+step +proj=axisswap +order=2,3,1",
)
.expect("two 3-cycle axisswap steps");
let out = trans(&pj, Direction::Fwd, Coord::new(1.0, 2.0, 3.0, 4.0))
.unwrap()
.v();
assert_eq!(
out,
[3.0, 1.0, 2.0, 4.0],
"3-cycle applied twice must be the net permutation 3,1,2, not identity"
);
}
#[test]
fn optimizer_keeps_swap_then_negate_pair() {
let pj = create(
"+proj=pipeline \
+step +proj=axisswap +order=2,1 \
+step +proj=axisswap +order=1,-2",
)
.expect("swap then negate axisswap steps");
let out = trans(&pj, Direction::Fwd, Coord::new(1.0, 2.0, 3.0, 4.0))
.unwrap()
.v();
assert_eq!(
out,
[2.0, -1.0, 3.0, 4.0],
"swap-then-negate must not be optimized to identity"
);
}
#[test]
fn optimizer_keeps_rotation_twice_pair() {
let pj = create(
"+proj=pipeline \
+step +proj=axisswap +order=-2,1 \
+step +proj=axisswap +order=-2,1",
)
.expect("two rotation axisswap steps");
let out = trans(&pj, Direction::Fwd, Coord::new(1.0, 2.0, 3.0, 4.0))
.unwrap()
.v();
assert_eq!(
out,
[-1.0, -2.0, 3.0, 4.0],
"90° rotation applied twice must be a 180° rotation, not identity"
);
}
#[test]
fn optimizer_cancels_true_inverse_axisswap_pair() {
let pj = create(
"+proj=pipeline \
+step +proj=noop \
+step +proj=axisswap +order=2,3,1 \
+step +proj=axisswap +order=2,3,1 +inv",
)
.expect("noop + axisswap and its inverse");
let out = trans(&pj, Direction::Fwd, Coord::new(1.0, 2.0, 3.0, 4.0))
.unwrap()
.v();
assert_eq!(
out,
[1.0, 2.0, 3.0, 4.0],
"a permutation and its inverse compose to the identity"
);
}
#[test]
fn optimizer_all_cancelled_returns_error() {
let result = create(
"+proj=pipeline \
+step +proj=noop +omit_fwd +omit_inv \
+step +proj=noop +omit_fwd +omit_inv",
);
assert!(
result.is_err(),
"all-cancelled pipeline must return an error"
);
}
fn assert_xy(got: Coord, ex: [f64; 2], tol: f64, label: &str) {
let g = got.v();
assert!(
(g[0] - ex[0]).abs() < tol,
"{label} x: got {}, want {}",
g[0],
ex[0]
);
assert!(
(g[1] - ex[1]).abs() < tol,
"{label} y: got {}, want {}",
g[1],
ex[1]
);
}
#[test]
fn towgs84_3param_matches_cs2cs() {
let pj = create("+proj=utm +zone=32 +ellps=intl +towgs84=-87,-98,-121").unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(9.0 * DEG_TO_RAD, 0.0, 0.0, 0.0),
)
.unwrap();
assert_xy(
out,
[500083.152337543, 120.954458759],
1e-4,
"utm intl towgs84",
);
}
#[test]
fn towgs84_7param_matches_cs2cs() {
let pj =
create("+proj=merc +ellps=intl +towgs84=59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993")
.unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(174.0 * DEG_TO_RAD, -41.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
assert_xy(
out,
[19370336.34098803, -4984632.9044438],
1e-2,
"merc intl 7-param towgs84",
);
}
#[test]
fn towgs84_datum_resolves_ellipsoid() {
let via_datum = create("+proj=merc +datum=carthage").unwrap();
let via_explicit = create("+proj=merc +ellps=clrk80ign +towgs84=-263.0,6.0,431.0").unwrap();
let p = Coord::new(10.0 * DEG_TO_RAD, 34.0 * DEG_TO_RAD, 0.0, 0.0);
let a = trans(&via_datum, Direction::Fwd, p).unwrap();
let b = trans(&via_explicit, Direction::Fwd, p).unwrap();
assert_xy(
a,
[1113152.342921798, 4004375.48761513],
1e-2,
"merc datum=carthage",
);
let (av, bv) = (a.v(), b.v());
assert!(
(av[0] - bv[0]).abs() < 1e-6 && (av[1] - bv[1]).abs() < 1e-6,
"datum vs explicit ellps mismatch: {av:?} vs {bv:?}"
);
}
#[test]
fn towgs84_4param_zero_padded_not_dropped() {
assert_eq!(normalize_towgs84("1,2,3,4"), "1,2,3,4,0,0,0");
assert_eq!(normalize_towgs84("1,2"), "1,2,0");
assert_eq!(normalize_towgs84("1,2,3,4,5,6,7,8"), "1,2,3,4,5,6,7");
let four = create("+proj=merc +ellps=intl +towgs84=1,2,3,4").unwrap();
let out = trans(
&four,
Direction::Fwd,
Coord::new(10.0 * DEG_TO_RAD, 40.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
assert_xy(
out,
[1113337.903887998, 4838632.996508759],
1e-2,
"merc intl 4-param towgs84",
);
}
#[test]
fn nadgrids_applies_hgridshift_inverse() {
let mut ctx = crate::context::Context::new();
ctx.register_grid("shift.gsb", make_uniform_shift_ntv2(5.0, -8.0));
let p = Coord::new(-0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 0.0, 0.0);
let engine = create_with_ctx("+proj=merc +ellps=WGS84 +nadgrids=shift.gsb", &ctx).unwrap();
let inv_ref = create_with_ctx(
"+proj=pipeline +step +proj=hgridshift +grids=shift.gsb +inv \
+step +proj=merc +ellps=WGS84",
&ctx,
)
.unwrap();
let fwd_ref = create_with_ctx(
"+proj=pipeline +step +proj=hgridshift +grids=shift.gsb \
+step +proj=merc +ellps=WGS84",
&ctx,
)
.unwrap();
let e = trans(&engine, Direction::Fwd, p).unwrap().v();
let i = trans(&inv_ref, Direction::Fwd, p).unwrap().v();
let f = trans(&fwd_ref, Direction::Fwd, p).unwrap().v();
assert!(
(e[0] - i[0]).abs() < 1e-9 && (e[1] - i[1]).abs() < 1e-9,
"engine must apply hgridshift +inv: engine={e:?} inv_ref={i:?}"
);
assert!(
(e[0] - f[0]).abs() > 1e-3 || (e[1] - f[1]).abs() > 1e-3,
"engine must NOT match forward hgridshift: engine={e:?} fwd_ref={f:?}"
);
}
#[test]
fn nadgrids_projection_uses_source_ellipsoid() {
let mut ctx = crate::context::Context::new();
ctx.register_grid("shift.gsb", make_uniform_shift_ntv2(5.0, -8.0));
let p = Coord::new(-0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 0.0, 0.0);
let engine = create_with_ctx("+proj=merc +ellps=intl +nadgrids=shift.gsb", &ctx).unwrap();
let reference = create_with_ctx(
"+proj=pipeline +step +proj=hgridshift +grids=shift.gsb +inv \
+step +proj=merc +ellps=intl",
&ctx,
)
.unwrap();
let e = trans(&engine, Direction::Fwd, p).unwrap().v();
let r = trans(&reference, Direction::Fwd, p).unwrap().v();
assert!(
(e[0] - r[0]).abs() < 1e-9 && (e[1] - r[1]).abs() < 1e-9,
"nadgrids projection must use intl: engine={e:?} reference={r:?}"
);
}
#[test]
fn nadgrids_takes_precedence_over_towgs84() {
let mut ctx = crate::context::Context::new();
ctx.register_grid("shift.gsb", make_uniform_shift_ntv2(5.0, -8.0));
let p = Coord::new(-0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 0.0, 0.0);
let both = create_with_ctx(
"+proj=merc +ellps=WGS84 +towgs84=100,200,300 +nadgrids=shift.gsb",
&ctx,
)
.unwrap();
let grid_only =
create_with_ctx("+proj=merc +ellps=WGS84 +nadgrids=shift.gsb", &ctx).unwrap();
let helmert_only =
create_with_ctx("+proj=merc +ellps=WGS84 +towgs84=100,200,300", &ctx).unwrap();
let b = trans(&both, Direction::Fwd, p).unwrap().v();
let g = trans(&grid_only, Direction::Fwd, p).unwrap().v();
let h = trans(&helmert_only, Direction::Fwd, p).unwrap().v();
assert!(
(b[0] - g[0]).abs() < 1e-9 && (b[1] - g[1]).abs() < 1e-9,
"nadgrids must win over towgs84: both={b:?} grid_only={g:?}"
);
assert!(
(b[0] - h[0]).abs() > 1e-3 || (b[1] - h[1]).abs() > 1e-3,
"with both present the towgs84 path must NOT be used: both={b:?} helmert_only={h:?}"
);
}
#[test]
fn pipeline_global_param_inherited_by_steps() {
let p = Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0);
let global = create("+proj=pipeline +lon_0=10 +step +proj=merc +ellps=WGS84").unwrap();
let local = create("+proj=pipeline +step +proj=merc +lon_0=10 +ellps=WGS84").unwrap();
let none = create("+proj=pipeline +step +proj=merc +ellps=WGS84").unwrap();
let g = trans(&global, Direction::Fwd, p).unwrap().v();
let l = trans(&local, Direction::Fwd, p).unwrap().v();
let n = trans(&none, Direction::Fwd, p).unwrap().v();
assert!(
(g[0] - l[0]).abs() < 1e-9 && (g[1] - l[1]).abs() < 1e-9,
"global lon_0 must equal step-local lon_0: {g:?} vs {l:?}"
);
assert!(
(g[0] - n[0]).abs() > 1.0,
"global lon_0 must change the result vs none: {g:?} vs {n:?}"
);
}
#[test]
fn axis_wsu_negates_output() {
let pj = create("+proj=merc +axis=wsu +ellps=WGS84").unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
assert_xy(
out,
[-1335833.8895192828, -7_326_837.715_045_549],
1e-6,
"merc axis=wsu",
);
}
#[test]
fn axis_neu_swaps_output() {
let pj = create("+proj=merc +axis=neu +ellps=WGS84").unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
assert_xy(
out,
[7_326_837.715_045_549, 1335833.8895192828],
1e-6,
"merc axis=neu",
);
}
#[test]
fn axis_round_trips() {
let pj = create("+proj=merc +axis=wsu +ellps=WGS84").unwrap();
let input = Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 0.0, 0.0);
let fwd = trans(&pj, Direction::Fwd, input).unwrap();
let inv = trans(&pj, Direction::Inv, fwd).unwrap().v();
assert!(
(inv[0] - 12.0 * DEG_TO_RAD).abs() < 1e-9 && (inv[1] - 55.0 * DEG_TO_RAD).abs() < 1e-9,
"axis round trip: {inv:?}"
);
}
#[test]
fn lon_wrap_wraps_output_into_center_band() {
let pj = create("+proj=longlat +lon_wrap=180 +ellps=WGS84").unwrap();
let cases = [
(-170.0, 10.0, 190.0, 10.0),
(10.0, 10.0, 10.0, 10.0),
(190.0, 10.0, 190.0, 10.0),
];
for (lam_in, phi_in, lam_want, phi_want) in cases {
let out = trans(
&pj,
Direction::Fwd,
Coord::new(lam_in * DEG_TO_RAD, phi_in * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
let v = out.v();
assert!(
(v[0] - lam_want * DEG_TO_RAD).abs() < 1e-9,
"lam_in={lam_in}: got {} deg, want {lam_want} deg",
v[0] * oxiproj_core::RAD_TO_DEG
);
assert!(
(v[1] - phi_want * DEG_TO_RAD).abs() < 1e-9,
"lam_in={lam_in}: phi got {} deg, want {phi_want} deg",
v[1] * oxiproj_core::RAD_TO_DEG
);
}
}
#[test]
fn lon_wrap_oracle_sweep() {
let pj = create("+proj=pipeline +lon_wrap=180 +step +proj=longlat +ellps=WGS84").unwrap();
let cases: [(f64, f64); 11] = [
(-170.0, 190.0),
(-180.0, 180.0),
(0.0, 0.0),
(10.0, 10.0),
(170.0, 170.0),
(179.9999, 179.9999),
(180.0, 180.0),
(180.0001, 180.0001),
(190.0, 190.0),
(359.9999, 359.9999),
(360.0, 0.0),
];
for (lam_in, lam_want) in cases {
let out = trans(
&pj,
Direction::Fwd,
Coord::new(lam_in * DEG_TO_RAD, 5.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
let got_deg = out.v()[0] * oxiproj_core::RAD_TO_DEG;
assert!(
(got_deg - lam_want).abs() < 1e-6,
"lam_in={lam_in}: got {got_deg} deg, want {lam_want} deg"
);
}
}
#[test]
fn lon_wrap_absent_leaves_longitude_unwrapped() {
let pj = create("+proj=longlat +ellps=WGS84").unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(-170.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
let v = out.v();
assert!(
(v[0] - (-170.0 * DEG_TO_RAD)).abs() < 1e-9,
"lam got {}",
v[0]
);
}
#[test]
fn lon_wrap_precedes_axisswap_in_synthesized_pipeline() {
let pj = create("+proj=longlat +lon_wrap=180 +axis=wsu +ellps=WGS84").unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(-170.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
let v = out.v();
assert!(
(v[0] - (-190.0 * DEG_TO_RAD)).abs() < 1e-9,
"x got {} deg",
v[0] * oxiproj_core::RAD_TO_DEG
);
assert!(
(v[1] - (-10.0 * DEG_TO_RAD)).abs() < 1e-9,
"y got {} deg",
v[1] * oxiproj_core::RAD_TO_DEG
);
}
#[test]
fn lon_wrap_pipeline_global_wraps_single_step_output() {
let pj = create("+proj=pipeline +lon_wrap=180 +step +proj=longlat +ellps=WGS84").unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(-170.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
let v = out.v();
assert!(
(v[0] - (190.0 * DEG_TO_RAD)).abs() < 1e-9,
"x got {} deg",
v[0] * oxiproj_core::RAD_TO_DEG
);
}
#[test]
fn lon_wrap_pipeline_global_undone_by_inverted_round_trip_step() {
let pj = create(
"+proj=pipeline +lon_wrap=180 +step +proj=longlat +ellps=WGS84 +step +proj=longlat +ellps=WGS84 +inv",
)
.unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(-170.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD, 0.0, 0.0),
)
.unwrap();
let v = out.v();
assert!(
(v[0] - (-170.0 * DEG_TO_RAD)).abs() < 1e-9,
"x got {} deg",
v[0] * oxiproj_core::RAD_TO_DEG
);
}
#[test]
fn lon_wrap_rejects_excessive_center() {
assert!(create("+proj=longlat +lon_wrap=999999 +ellps=WGS84").is_err());
}
#[test]
fn lon_wrap_accepts_bare_flag_as_zero_center() {
assert!(create("+proj=longlat +lon_wrap +ellps=WGS84").is_ok());
}
fn make_uniform_gtx(shift_m: f32) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&0.0f64.to_be_bytes()); buf.extend_from_slice(&0.0f64.to_be_bytes()); buf.extend_from_slice(&1.0f64.to_be_bytes()); buf.extend_from_slice(&1.0f64.to_be_bytes()); buf.extend_from_slice(&2i32.to_be_bytes()); buf.extend_from_slice(&2i32.to_be_bytes()); for _ in 0..4 {
buf.extend_from_slice(&shift_m.to_be_bytes());
}
buf
}
#[test]
fn geoidgrids_injects_vgridshift() {
let mut ctx = crate::context::Context::new();
ctx.register_grid("geoid.gtx", make_uniform_gtx(10.0));
let p = Coord::new(0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 5.0, 0.0);
let engine =
create_with_ctx("+proj=merc +ellps=WGS84 +geoidgrids=geoid.gtx", &ctx).unwrap();
let fwd_ref = create_with_ctx(
"+proj=pipeline +step +proj=vgridshift +grids=geoid.gtx \
+step +proj=merc +ellps=WGS84",
&ctx,
)
.unwrap();
let plain = create("+proj=merc +ellps=WGS84").unwrap();
let e = trans(&engine, Direction::Fwd, p).unwrap().v();
let r = trans(&fwd_ref, Direction::Fwd, p).unwrap().v();
let n = trans(&plain, Direction::Fwd, p).unwrap().v();
assert!(
(e[0] - r[0]).abs() < 1e-9 && (e[1] - r[1]).abs() < 1e-9 && (e[2] - r[2]).abs() < 1e-6,
"geoidgrids must equal explicit vgridshift pipeline: engine={e:?} ref={r:?}"
);
assert!(
(e[2] - (n[2] - 10.0)).abs() < 1e-6,
"geoidgrids must subtract the +10 m geoid shift from height: engine z={}, plain z={}",
e[2],
n[2]
);
assert!(
(e[0] - n[0]).abs() < 1e-6 && (e[1] - n[1]).abs() < 1e-6,
"geoidgrids must not move x/y: engine={e:?} plain={n:?}"
);
}
#[test]
fn geoidgrids_round_trips() {
let mut ctx = crate::context::Context::new();
ctx.register_grid("geoid.gtx", make_uniform_gtx(10.0));
let engine =
create_with_ctx("+proj=merc +ellps=WGS84 +geoidgrids=geoid.gtx", &ctx).unwrap();
let p = Coord::new(0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 5.0, 0.0);
let fwd = trans(&engine, Direction::Fwd, p).unwrap();
let inv = trans(&engine, Direction::Inv, fwd).unwrap().v();
assert!(
(inv[0] - 0.5 * DEG_TO_RAD).abs() < 1e-9
&& (inv[1] - 0.5 * DEG_TO_RAD).abs() < 1e-9
&& (inv[2] - 5.0).abs() < 1e-6,
"geoidgrids round trip: {inv:?}"
);
}
#[test]
fn hgridshift_resolves_grid_from_proj_data_via_public_api() {
use std::io::Write;
let ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!("oxiproj_e4_projdata_{ns}"));
std::fs::create_dir_all(&dir).expect("create temp PROJ_DATA dir");
let grid_bytes = make_uniform_shift_ntv2(3600.0, 0.0);
{
let mut f = std::fs::File::create(dir.join("e4_shift.gsb")).expect("create grid file");
f.write_all(&grid_bytes).expect("write grid bytes");
}
let saved = std::env::var_os("PROJ_DATA");
std::env::set_var("PROJ_DATA", &dir);
let p = Coord::new(-0.5 * DEG_TO_RAD, 0.5 * DEG_TO_RAD, 0.0, 0.0);
let disk_out = create("+proj=hgridshift +grids=e4_shift.gsb")
.ok()
.and_then(|pj| trans(&pj, Direction::Fwd, p).ok().map(|c| c.v()));
match saved {
Some(v) => std::env::set_var("PROJ_DATA", v),
None => std::env::remove_var("PROJ_DATA"),
}
let _ = std::fs::remove_dir_all(&dir);
let out = disk_out
.expect("hgridshift must build and transform using a grid resolved from PROJ_DATA");
assert!(
(out[0] - (-0.5 * DEG_TO_RAD)).abs() < 1e-9,
"lon should be unchanged, got {}",
out[0]
);
assert!(
(out[1] - 1.5 * DEG_TO_RAD).abs() < 1e-9,
"lat should be shifted +1 deg, got {}",
out[1]
);
let mut ctx = crate::context::Context::new();
ctx.register_grid("e4_shift.gsb", grid_bytes);
let mem = create_with_ctx("+proj=hgridshift +grids=e4_shift.gsb", &ctx)
.expect("in-memory hgridshift");
let mem_out = trans(&mem, Direction::Fwd, p)
.expect("in-memory transform")
.v();
assert!(
(out[0] - mem_out[0]).abs() < 1e-12 && (out[1] - mem_out[1]).abs() < 1e-12,
"disk-resolved result must match in-memory-registered result: disk={out:?} mem={mem_out:?}"
);
}
#[test]
fn moll_factors_use_sphere_metric() {
let pj = create("+proj=moll +ellps=WGS84").unwrap();
let f = pj.factors(Coord::new(0.0, 0.0, 0.0, 0.0)).unwrap();
assert!(
(f.s - 1.0).abs() < 1e-6,
"moll areal scale must be 1.0 (sphere metric), got {}",
f.s
);
assert!(
(f.h - 1.110_720_734_5).abs() < 1e-4,
"moll meridional scale must match PROJ 1.11072, got {}",
f.h
);
let fe = pj.factors_exact(Coord::new(0.0, 0.0, 0.0, 0.0)).unwrap();
assert!(
(fe.areal_scale - 1.0).abs() < 1e-6,
"moll exact areal scale must be 1.0, got {}",
fe.areal_scale
);
}
#[test]
fn sphere_only_projections_report_unit_areal_scale() {
for name in ["sinu", "hammer", "eck4"] {
let pj = create(&format!("+proj={name} +ellps=WGS84")).unwrap();
let f = pj
.factors(Coord::new(20.0 * DEG_TO_RAD, 40.0 * DEG_TO_RAD, 0.0, 0.0))
.unwrap();
assert!(
(f.s - 1.0).abs() < 1e-6,
"{name} areal scale must be 1.0 (sphere metric), got {}",
f.s
);
}
}
#[test]
fn ellipsoidal_projection_factors_keep_eccentricity() {
let pj = create("+proj=merc +ellps=WGS84").unwrap();
let f = pj
.factors(Coord::new(20.0 * DEG_TO_RAD, 40.0 * DEG_TO_RAD, 0.0, 0.0))
.unwrap();
assert!(
(f.h - 1.303_600_689_3).abs() < 1e-4,
"merc meridional scale must match ellipsoidal PROJ 1.30360, got {}",
f.h
);
let spherical = 1.0 / 40.0_f64.to_radians().cos();
assert!(
(f.h - spherical).abs() > 1e-3,
"merc factors must be ellipsoidal, not spherical {spherical}: got {}",
f.h
);
}
#[test]
fn geocent_produces_ecef_via_engine() {
let pj = create("+proj=geocent +ellps=WGS84").unwrap();
let out = trans(
&pj,
Direction::Fwd,
Coord::new(12.0 * DEG_TO_RAD, 55.0 * DEG_TO_RAD, 100.0, 0.0),
)
.unwrap()
.v();
assert!((out[0] - 3586525.761017917).abs() < 1e-3, "X = {}", out[0]);
assert!((out[1] - 762339.584102928).abs() < 1e-3, "Y = {}", out[1]);
assert!((out[2] - 5201465.438406702).abs() < 1e-3, "Z = {}", out[2]);
let back = trans(&pj, Direction::Inv, Coord::new(out[0], out[1], out[2], 0.0))
.unwrap()
.v();
assert!(
(back[0] - 12.0 * DEG_TO_RAD).abs() < 1e-9,
"lam = {}",
back[0]
);
assert!(
(back[1] - 55.0 * DEG_TO_RAD).abs() < 1e-9,
"phi = {}",
back[1]
);
assert!((back[2] - 100.0).abs() < 1e-3, "h = {}", back[2]);
}
}