use proj_core::{AreaOfInterest, Coord, SelectionOptions, Transform};
use serde::Deserialize;
#[derive(Deserialize)]
struct ReferencePoint {
from_epsg: u32,
to_epsg: u32,
input_x: f64,
input_y: f64,
#[serde(default)]
input_z: Option<f64>,
expected_x: f64,
expected_y: f64,
#[serde(default)]
expected_z: Option<f64>,
tolerance: f64,
#[serde(default)]
tolerance_z: Option<f64>,
description: String,
}
fn load_corpus() -> Vec<ReferencePoint> {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../testdata/reference_values.json"
);
let data =
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("failed to read {path}: {e}"));
serde_json::from_str(&data).unwrap_or_else(|e| panic!("failed to parse {path}: {e}"))
}
fn check_point(r: &ReferencePoint) -> Result<(), String> {
let point = Coord::new(r.input_x, r.input_y);
let options = SelectionOptions {
area_of_interest: Some(AreaOfInterest::source_crs_point(point)),
..SelectionOptions::default()
};
let t = Transform::with_selection_options(
&format!("EPSG:{}", r.from_epsg),
&format!("EPSG:{}", r.to_epsg),
options,
)
.map_err(|e| {
format!(
"{}: unexpected transform construction failure for EPSG:{}→EPSG:{}: {e}",
r.description, r.from_epsg, r.to_epsg
)
})?;
match (r.input_z, r.expected_z) {
(Some(iz), Some(ez)) => {
let (rx, ry, rz) = t
.convert_3d((r.input_x, r.input_y, iz))
.map_err(|e| format!("{}: 3D transform failed: {e}", r.description))?;
let (dx, dy, dz) = (
(rx - r.expected_x).abs(),
(ry - r.expected_y).abs(),
(rz - ez).abs(),
);
let tol_z = r.tolerance_z.unwrap_or(r.tolerance);
if dx > r.tolerance || dy > r.tolerance || dz > tol_z {
return Err(format!(
"{}: expected ({}, {}, {}), got ({}, {}, {}), delta ({:e}, {:e}, {:e}), tol {:e}, tol_z {:e}",
r.description, r.expected_x, r.expected_y, ez, rx, ry, rz, dx, dy, dz,
r.tolerance, tol_z
));
}
}
_ => {
let (rx, ry) = t
.convert((r.input_x, r.input_y))
.map_err(|e| format!("{}: transform failed: {e}", r.description))?;
let (dx, dy) = ((rx - r.expected_x).abs(), (ry - r.expected_y).abs());
if dx > r.tolerance || dy > r.tolerance {
return Err(format!(
"{}: expected ({}, {}), got ({}, {}), delta ({:e}, {:e}), tol {:e}",
r.description, r.expected_x, r.expected_y, rx, ry, dx, dy, r.tolerance
));
}
}
}
Ok(())
}
#[test]
fn corpus_matches_c_proj() {
let corpus = load_corpus();
assert!(!corpus.is_empty(), "corpus is empty");
let mut pass = 0;
let mut failures = Vec::new();
for r in &corpus {
match check_point(r) {
Ok(()) => pass += 1,
Err(msg) => failures.push(msg),
}
}
eprintln!(
"Corpus results: {} passed, {} failed out of {} total",
pass,
failures.len(),
corpus.len()
);
if !failures.is_empty() {
panic!(
"{} of {} reference values failed:\n{}",
failures.len(),
corpus.len(),
failures.join("\n")
);
}
}
#[test]
fn corpus_has_adequate_coverage() {
let corpus = load_corpus();
let epsg_targets: Vec<u32> = corpus.iter().map(|r| r.to_epsg).collect();
assert!(epsg_targets.contains(&3857), "missing Web Mercator");
assert!(
epsg_targets.contains(&3413),
"missing Polar Stereographic North"
);
assert!(
epsg_targets.contains(&3031),
"missing Antarctic Polar Stereographic"
);
assert!(epsg_targets.contains(&3395), "missing World Mercator");
assert!(epsg_targets.contains(&2154), "missing Lambert-93");
assert!(epsg_targets.contains(&5070), "missing CONUS Albers");
assert!(epsg_targets.contains(&3035), "missing LAEA Europe");
assert!(epsg_targets.contains(&3408), "missing spherical LAEA north");
assert!(
epsg_targets.contains(&9311),
"missing spherical LAEA ellipsoid CRS"
);
assert!(epsg_targets.contains(&28992), "missing RD New");
assert!(epsg_targets.contains(&3078), "missing Hotine variant A");
assert!(epsg_targets.contains(&2056), "missing Hotine variant B");
assert!(epsg_targets.contains(&30200), "missing Cassini-Soldner");
assert!(epsg_targets.contains(&32662), "missing Plate Carree");
assert!(epsg_targets.contains(&6247), "missing Colombia Urban");
assert!(epsg_targets.contains(&24200), "missing LCC 1SP");
assert!(epsg_targets.contains(&6201), "missing LCC 2SP Michigan");
assert!(epsg_targets.contains(&9549), "missing LCC 1SP variant B");
assert!(epsg_targets.contains(&5514), "missing Krovak East North");
assert!(
epsg_targets.contains(&5516),
"missing Modified Krovak East North"
);
assert!(
epsg_targets.contains(&8353),
"missing JTSK03 Krovak East North"
);
assert!(epsg_targets.contains(&8857), "missing Equal Earth");
assert!(
epsg_targets.contains(&8859),
"missing Equal Earth Asia-Pacific"
);
assert!(
epsg_targets.contains(&5880),
"missing SIRGAS 2000 Brazil Polyconic"
);
assert!(
epsg_targets.contains(&29101),
"missing SAD69 Brazil Polyconic"
);
assert!(
epsg_targets.contains(&27701),
"missing Equi7 Africa Azimuthal Equidistant"
);
assert!(epsg_targets.contains(&3993), "missing Guam Projection");
assert!(
epsg_targets.contains(&3295),
"missing Modified Azimuthal Equidistant"
);
assert!(
epsg_targets.contains(&22780),
"missing grad-unit Levant Stereographic"
);
assert!(
epsg_targets.contains(&8441),
"missing Laborde Oblique Mercator"
);
let has_utm = epsg_targets.iter().any(|e| (32601..=32660).contains(e));
assert!(has_utm, "missing UTM north zones");
let has_utm_south = epsg_targets.iter().any(|e| (32701..=32760).contains(e));
assert!(has_utm_south, "missing UTM south zones");
let from_epsgs: Vec<u32> = corpus.iter().map(|r| r.from_epsg).collect();
assert!(from_epsgs.contains(&4267), "missing NAD27 datum shift");
assert!(from_epsgs.contains(&4277), "missing OSGB36 datum shift");
assert!(from_epsgs.contains(&4230), "missing ED50 datum shift");
assert!(from_epsgs.contains(&3857), "missing 3857→4326 inverse");
assert!(from_epsgs.contains(&3413), "missing 3413→4326 inverse");
assert!(
corpus.iter().any(|r| r.input_z.is_some()),
"missing 3D reference points"
);
assert!(
corpus.iter().any(|r| r.input_y.abs() > 89.99),
"missing near-pole points"
);
assert!(
corpus.len() >= 100,
"corpus too small: {} points",
corpus.len()
);
eprintln!("Corpus coverage: {} reference points", corpus.len());
}