use oxiproj_core::{Coord, Direction, DEG_TO_RAD};
use oxiproj_engine::{create, trans};
fn resolve_proj_bin() -> Option<std::path::PathBuf> {
if let Some(v) = std::env::var_os("PROJ_BIN") {
let p = std::path::PathBuf::from(v);
return if p.is_file() { Some(p) } else { None };
}
let path_var = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join("proj");
if candidate.is_file() {
return Some(candidate);
}
let candidate_exe = dir.join("proj.exe");
if candidate_exe.is_file() {
return Some(candidate_exe);
}
}
None
}
fn assert_close(got: f64, expected: f64, tol: f64, what: &str) {
let diff = (got - expected).abs();
assert!(
diff < tol,
"{what}: got {got}, expected {expected}, abs diff {diff} >= tol {tol}",
);
}
#[test]
fn utm_zone32_forward_and_inverse() {
let pj = create("+proj=utm +zone=32 +ellps=WGS84").expect("create utm zone 32");
let rows = [
(9.0, 0.0, 500000.0000000000, 0.0000000000),
(12.0, 55.0, 691_875.632_137_542, 6_098_907.825_129_169),
];
for (lon_deg, lat_deg, expected_x, expected_y) in rows {
let lon_rad = lon_deg * DEG_TO_RAD;
let lat_rad = lat_deg * DEG_TO_RAD;
let fwd = trans(&pj, Direction::Fwd, Coord::new(lon_rad, lat_rad, 0.0, 0.0))
.expect("utm forward");
println!(
"[case 1] dx={:e} dy={:e}",
(fwd.v()[0] - expected_x).abs(),
(fwd.v()[1] - expected_y).abs()
);
assert_close(fwd.v()[0], expected_x, 1e-6, "case 1 forward x");
assert_close(fwd.v()[1], expected_y, 1e-6, "case 1 forward y");
let inv = trans(&pj, Direction::Inv, fwd).expect("utm inverse");
assert_close(inv.v()[0], lon_rad, 1e-9, "case 1 inverse lon");
assert_close(inv.v()[1], lat_rad, 1e-9, "case 1 inverse lat");
}
}
#[test]
fn merc_wgs84_forward_and_inverse() {
let pj = create("+proj=merc +ellps=WGS84").expect("create merc");
let rows = [
(0.0, 0.0, 0.0000000000, 0.0000000000),
(12.0, 55.0, 1335833.8895192828, 7_326_837.715_045_549),
];
for (lon_deg, lat_deg, expected_x, expected_y) in rows {
let lon_rad = lon_deg * DEG_TO_RAD;
let lat_rad = lat_deg * DEG_TO_RAD;
let fwd = trans(&pj, Direction::Fwd, Coord::new(lon_rad, lat_rad, 0.0, 0.0))
.expect("merc forward");
println!(
"[case 2] dx={:e} dy={:e}",
(fwd.v()[0] - expected_x).abs(),
(fwd.v()[1] - expected_y).abs()
);
assert_close(fwd.v()[0], expected_x, 1e-6, "case 2 forward x");
assert_close(fwd.v()[1], expected_y, 1e-6, "case 2 forward y");
let inv = trans(&pj, Direction::Inv, fwd).expect("merc inverse");
assert_close(inv.v()[0], lon_rad, 1e-9, "case 2 inverse lon");
assert_close(inv.v()[1], lat_rad, 1e-9, "case 2 inverse lat");
}
}
#[test]
fn etmerc_lon0_9_forward_and_inverse() {
let pj = create("+proj=etmerc +lon_0=9 +ellps=WGS84").expect("create etmerc");
let lon_deg = 9.0;
let lat_deg = 50.0;
let expected_x = 0.0000000000;
let expected_y = 5_540_847.041_684_148;
let lon_rad = lon_deg * DEG_TO_RAD;
let lat_rad = lat_deg * DEG_TO_RAD;
let fwd =
trans(&pj, Direction::Fwd, Coord::new(lon_rad, lat_rad, 0.0, 0.0)).expect("etmerc forward");
println!(
"[case 3] dx={:e} dy={:e}",
(fwd.v()[0] - expected_x).abs(),
(fwd.v()[1] - expected_y).abs()
);
assert_close(fwd.v()[0], expected_x, 1e-6, "case 3 forward x");
assert_close(fwd.v()[1], expected_y, 1e-6, "case 3 forward y");
let inv = trans(&pj, Direction::Inv, fwd).expect("etmerc inverse");
assert_close(inv.v()[0], lon_rad, 1e-9, "case 3 inverse lon");
assert_close(inv.v()[1], lat_rad, 1e-9, "case 3 inverse lat");
}
#[test]
fn merc_lat_ts_45_forward_and_inverse() {
let pj = create("+proj=merc +lat_ts=45 +ellps=WGS84").expect("create merc lat_ts");
let lon_deg = 10.0;
let lat_deg = 40.0;
let expected_x = 788468.3509397812;
let expected_y = 3427056.2478435994;
let lon_rad = lon_deg * DEG_TO_RAD;
let lat_rad = lat_deg * DEG_TO_RAD;
let fwd = trans(&pj, Direction::Fwd, Coord::new(lon_rad, lat_rad, 0.0, 0.0))
.expect("merc lat_ts forward");
println!(
"[case 4] dx={:e} dy={:e}",
(fwd.v()[0] - expected_x).abs(),
(fwd.v()[1] - expected_y).abs()
);
assert_close(fwd.v()[0], expected_x, 1e-6, "case 4 forward x");
assert_close(fwd.v()[1], expected_y, 1e-6, "case 4 forward y");
let inv = trans(&pj, Direction::Inv, fwd).expect("merc lat_ts inverse");
assert_close(inv.v()[0], lon_rad, 1e-9, "case 4 inverse lon");
assert_close(inv.v()[1], lat_rad, 1e-9, "case 4 inverse lat");
}
#[test]
fn tmerc_lat0_40_forward_and_inverse() {
let pj =
create("+proj=tmerc +lat_0=40 +k_0=0.9996 +lon_0=9 +ellps=WGS84").expect("create tmerc");
let lon_deg = 10.0;
let lat_deg = 41.0;
let expected_x = 84102.1344451089;
let expected_y = 111481.3706361904;
let lon_rad = lon_deg * DEG_TO_RAD;
let lat_rad = lat_deg * DEG_TO_RAD;
let fwd =
trans(&pj, Direction::Fwd, Coord::new(lon_rad, lat_rad, 0.0, 0.0)).expect("tmerc forward");
println!(
"[case 5] dx={:e} dy={:e}",
(fwd.v()[0] - expected_x).abs(),
(fwd.v()[1] - expected_y).abs()
);
assert_close(fwd.v()[0], expected_x, 1e-6, "case 5 forward x");
assert_close(fwd.v()[1], expected_y, 1e-6, "case 5 forward y");
let inv = trans(&pj, Direction::Inv, fwd).expect("tmerc inverse");
assert_close(inv.v()[0], lon_rad, 1e-9, "case 5 inverse lon");
assert_close(inv.v()[1], lat_rad, 1e-9, "case 5 inverse lat");
}
#[test]
fn latlong_identity_round_trip() {
let pj = create("+proj=latlong +ellps=WGS84").expect("create latlong");
let pairs = [(0.2, 0.9), (0.0, 0.0), (-0.5, 1.0)];
for (lon_rad, lat_rad) in pairs {
let fwd = trans(&pj, Direction::Fwd, Coord::new(lon_rad, lat_rad, 0.0, 0.0))
.expect("latlong forward");
assert_close(fwd.v()[0], lon_rad, 1e-12, "latlong forward lon");
assert_close(fwd.v()[1], lat_rad, 1e-12, "latlong forward lat");
let inv = trans(&pj, Direction::Inv, fwd).expect("latlong inverse");
assert_close(inv.v()[0], lon_rad, 1e-12, "latlong inverse lon");
assert_close(inv.v()[1], lat_rad, 1e-12, "latlong inverse lat");
}
}
#[test]
fn pipeline_utm_inv_round_trip() {
let pj = create(
"+proj=pipeline +step +proj=utm +zone=32 +ellps=WGS84 +step +proj=utm +zone=32 +ellps=WGS84 +inv",
)
.expect("create pipeline");
let lon_rad = 12.0 * DEG_TO_RAD;
let lat_rad = 55.0 * DEG_TO_RAD;
let out = trans(&pj, Direction::Fwd, Coord::new(lon_rad, lat_rad, 0.0, 0.0))
.expect("pipeline forward");
assert_close(out.v()[0], lon_rad, 1e-9, "pipeline round-trip lon");
assert_close(out.v()[1], lat_rad, 1e-9, "pipeline round-trip lat");
}
#[test]
#[ignore]
fn regenerate_fixtures_via_proj() {
use std::io::Write as _;
let proj_bin = match resolve_proj_bin() {
Some(p) => p,
None => {
println!(
"proj binary not found (set PROJ_BIN or add `proj` to PATH); \
skipping fixture regeneration"
);
return;
}
};
let cases: [(&[&str], &str); 5] = [
(
&["-f", "%.10f", "+proj=utm", "+zone=32", "+ellps=WGS84"],
"9 0\n12 55\n",
),
(
&["-f", "%.10f", "+proj=merc", "+ellps=WGS84"],
"0 0\n12 55\n",
),
(
&["-f", "%.10f", "+proj=etmerc", "+lon_0=9", "+ellps=WGS84"],
"9 50\n",
),
(
&["-f", "%.10f", "+proj=merc", "+lat_ts=45", "+ellps=WGS84"],
"10 40\n",
),
(
&[
"-f",
"%.10f",
"+proj=tmerc",
"+lat_0=40",
"+k_0=0.9996",
"+lon_0=9",
"+ellps=WGS84",
],
"10 41\n",
),
];
for (args, stdin_str) in cases {
let mut child = std::process::Command::new(&proj_bin)
.args(args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("spawn proj");
{
let mut stdin = child.stdin.take().expect("open proj stdin");
stdin
.write_all(stdin_str.as_bytes())
.expect("write proj stdin");
}
let output = child.wait_with_output().expect("wait for proj");
println!(
"proj {}:\n{}",
args.join(" "),
String::from_utf8_lossy(&output.stdout)
);
}
}
fn make_fake_proj_binary(tag: &str) -> (std::path::PathBuf, std::path::PathBuf) {
let dir = std::env::temp_dir().join(format!(
"oxiproj-differential-{tag}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&dir).expect("create temp dir for fake proj binary");
let fake = dir.join("proj");
std::fs::write(&fake, b"#!/bin/sh\necho fake-proj\n").expect("write fake proj binary");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&fake)
.expect("stat fake proj binary")
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&fake, perms).expect("chmod fake proj binary");
}
(dir, fake)
}
#[test]
fn resolve_proj_bin_prefers_proj_bin_env_override() {
let (dir, fake) = make_fake_proj_binary("env-override");
let saved = std::env::var_os("PROJ_BIN");
std::env::set_var("PROJ_BIN", &fake);
let resolved = resolve_proj_bin();
match saved {
Some(v) => std::env::set_var("PROJ_BIN", v),
None => std::env::remove_var("PROJ_BIN"),
}
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(resolved, Some(fake));
}
#[test]
fn resolve_proj_bin_rejects_nonexistent_env_override_rather_than_falling_back_to_path() {
let (dir, fake) = make_fake_proj_binary("path-fallback-guard");
let saved_proj_bin = std::env::var_os("PROJ_BIN");
let saved_path = std::env::var_os("PATH");
let bogus = dir.join("does-not-exist-proj-binary");
std::env::set_var("PROJ_BIN", &bogus);
std::env::set_var("PATH", &dir);
let resolved = resolve_proj_bin();
match saved_proj_bin {
Some(v) => std::env::set_var("PROJ_BIN", v),
None => std::env::remove_var("PROJ_BIN"),
}
match saved_path {
Some(v) => std::env::set_var("PATH", v),
None => std::env::remove_var("PATH"),
}
let _ = std::fs::remove_dir_all(&dir);
let _ = fake;
assert_eq!(resolved, None);
}
#[test]
fn resolve_proj_bin_falls_back_to_path_lookup_when_env_unset() {
let (dir, fake) = make_fake_proj_binary("path-lookup");
let saved_proj_bin = std::env::var_os("PROJ_BIN");
let saved_path = std::env::var_os("PATH");
std::env::remove_var("PROJ_BIN");
std::env::set_var("PATH", &dir);
let resolved = resolve_proj_bin();
match saved_proj_bin {
Some(v) => std::env::set_var("PROJ_BIN", v),
None => std::env::remove_var("PROJ_BIN"),
}
match saved_path {
Some(v) => std::env::set_var("PATH", v),
None => std::env::remove_var("PATH"),
}
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(resolved, Some(fake));
}
#[test]
fn resolve_proj_bin_returns_none_when_nothing_resolves() {
let dir = std::env::temp_dir().join(format!(
"oxiproj-differential-empty-path-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&dir).expect("create empty temp dir");
let saved_proj_bin = std::env::var_os("PROJ_BIN");
let saved_path = std::env::var_os("PATH");
std::env::remove_var("PROJ_BIN");
std::env::set_var("PATH", &dir);
let resolved = resolve_proj_bin();
match saved_proj_bin {
Some(v) => std::env::set_var("PROJ_BIN", v),
None => std::env::remove_var("PROJ_BIN"),
}
match saved_path {
Some(v) => std::env::set_var("PATH", v),
None => std::env::remove_var("PATH"),
}
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(resolved, None);
}
fn resolve_cs2cs_bin() -> Option<std::path::PathBuf> {
let exe = if cfg!(windows) { "cs2cs.exe" } else { "cs2cs" };
if let Some(v) = std::env::var_os("PROJ_BIN") {
let proj = std::path::PathBuf::from(v);
if let Some(dir) = proj.parent() {
let candidate = dir.join(exe);
if candidate.is_file() {
return Some(candidate);
}
}
}
let path_var = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join(exe);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
fn run_cs2cs(
cs2cs: &std::path::Path,
src: &str,
dst: &str,
lon_deg: f64,
lat_deg: f64,
) -> Option<(f64, f64)> {
use std::io::Write;
use std::process::{Command, Stdio};
let mut args: Vec<String> = vec!["-f".into(), "%.10f".into()];
args.extend(src.split_whitespace().map(str::to_string));
args.push("+to".into());
args.extend(dst.split_whitespace().map(str::to_string));
let mut child = Command::new(cs2cs)
.args(&args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
child
.stdin
.as_mut()?
.write_all(format!("{lon_deg} {lat_deg}\n").as_bytes())
.ok()?;
let out = child.wait_with_output().ok()?;
let text = String::from_utf8(out.stdout).ok()?;
let mut it = text.split_whitespace();
let x: f64 = it.next()?.parse().ok()?;
let y: f64 = it.next()?.parse().ok()?;
Some((x, y))
}
#[test]
fn towgs84_3param_direction_matches_system_cs2cs() {
let Some(cs2cs) = resolve_cs2cs_bin() else {
eprintln!("skipping: no cs2cs binary on PROJ_BIN dir or PATH");
return;
};
let target = "+proj=utm +zone=32 +ellps=intl +towgs84=-87,-98,-121";
let pj = create(target).expect("create towgs84 target");
for (lon_deg, lat_deg) in [(9.0, 0.0), (12.0, 47.0), (2.0, 48.5)] {
let fwd = trans(
&pj,
Direction::Fwd,
Coord::new(lon_deg * DEG_TO_RAD, lat_deg * DEG_TO_RAD, 0.0, 0.0),
)
.expect("engine forward");
let Some((rx, ry)) = run_cs2cs(
&cs2cs,
"+proj=longlat +datum=WGS84",
target,
lon_deg,
lat_deg,
) else {
eprintln!("skipping point ({lon_deg},{lat_deg}): cs2cs produced no parseable output");
continue;
};
assert_close(fwd.v()[0], rx, 1e-3, "towgs84 3-param x vs cs2cs");
assert_close(fwd.v()[1], ry, 1e-3, "towgs84 3-param y vs cs2cs");
}
}