proj-core 0.2.0

Pure-Rust coordinate transformation library with no C dependencies
Documentation
//! Corpus-driven reference value tests verified against C PROJ.
//!
//! Loads `testdata/reference_values.json` (generated by `gen-reference` using the
//! C PROJ library with bundled_proj feature) and verifies every point matches
//! proj-core's output within the specified tolerance.
//!
//! The corpus covers:
//! - All 7 projection types (Web Mercator, UTM, Polar Stereographic, LCC, Albers, Mercator, Equidistant Cylindrical)
//! - Multiple geographic test points per projection (NYC, Tokyo, London, Paris, poles, dateline, etc.)
//! - Forward and inverse transforms
//! - Cross-datum transforms (NAD27→WGS84, OSGB36→WGS84, ED50→WGS84)
//! - Roundtrip verification (forward then inverse)
//! - Edge cases (poles, antimeridian, equator, projection zone boundaries)

use proj_core::Transform;
use serde::Deserialize;

#[derive(Deserialize)]
struct ReferencePoint {
    from_epsg: u32,
    to_epsg: u32,
    input_x: f64,
    input_y: f64,
    expected_x: f64,
    expected_y: f64,
    tolerance: 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}"))
}

#[test]
fn corpus_matches_c_proj() {
    let corpus = load_corpus();
    assert!(!corpus.is_empty(), "corpus is empty");

    let mut pass = 0;
    let mut skip = 0;
    let mut failures = Vec::new();

    for r in &corpus {
        let t = match Transform::from_epsg(r.from_epsg, r.to_epsg) {
            Ok(t) => t,
            Err(_) => {
                skip += 1;
                continue;
            }
        };

        let result = match t.convert((r.input_x, r.input_y)) {
            Ok(r) => r,
            Err(e) => {
                failures.push(format!("{}: transform failed: {e}", r.description));
                continue;
            }
        };

        let (rx, ry) = result;
        let dx = (rx - r.expected_x).abs();
        let dy = (ry - r.expected_y).abs();

        if dx > r.tolerance || dy > r.tolerance {
            failures.push(format!(
                "{}: expected ({}, {}), got ({}, {}), delta ({:e}, {:e}), tol {:e}",
                r.description, r.expected_x, r.expected_y, rx, ry, dx, dy, r.tolerance
            ));
        } else {
            pass += 1;
        }
    }

    eprintln!(
        "Corpus results: {} passed, {} skipped, {} failed out of {} total",
        pass,
        skip,
        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();

    // Verify the corpus covers all projection types
    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(&32662), "missing Plate Carree");

    // Verify UTM coverage
    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");

    // Verify datum shift coverage
    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");

    // Verify inverse transforms
    assert!(from_epsgs.contains(&3857), "missing 3857→4326 inverse");
    assert!(from_epsgs.contains(&3413), "missing 3413→4326 inverse");

    // Verify corpus size is substantial
    assert!(
        corpus.len() >= 100,
        "corpus too small: {} points",
        corpus.len()
    );

    eprintln!("Corpus coverage: {} reference points", corpus.len());
}