proj-core 0.10.0

Pure-Rust coordinate transformation library with no C dependencies
Documentation
//! Operation-selection parity against C PROJ.
//!
//! `testdata/selection_parity.json` (generated by `gen-selection-parity`)
//! records, for geographic CRS pairs with several candidate EPSG operations,
//! which operation C PROJ's late-binding selection used at a probe point.
//! This test asserts proj-core's area-of-interest selection picks the same
//! EPSG operation, so ranking divergences from C PROJ surface as failures
//! instead of silent behavior drift.
//!
//! Entries can legitimately not match:
//! - the CRS pair or the expected operation is outside proj-core's registry
//!   subset (counted, not failed — that is coverage, not ranking);
//! - a documented divergence listed in `KNOWN_DIVERGENCES` with a reason.

use proj_core::{
    registry, AreaOfInterest, Coord, CoordinateOperationId, SelectionOptions, Transform,
};
use serde::Deserialize;

#[derive(Deserialize)]
struct ParityEntry {
    source_epsg: u32,
    target_epsg: u32,
    probe_lon: f64,
    probe_lat: f64,
    #[serde(default)]
    expected_operation_epsg: Option<u32>,
    expected_operation_name: String,
    #[serde(default)]
    probe_extent_name: String,
}

/// Documented selection divergences from C PROJ: (source, target, probe
/// extent, C PROJ's choice). EPSG supersession is modeled (superseded
/// variants rank below their same-pair replacements); the residual cases
/// below come from tie-break semantics the registry does not carry —
/// replacement chains without a supersession row for the specific variant,
/// C PROJ preferring a direct ballpark operation where proj-core composes a
/// more specific multi-step path, and epoch/variant preferences among
/// equal-accuracy candidates. Each entry is a candidate for future ranking
/// work; new divergences fail the test.
const KNOWN_DIVERGENCES: &[(u32, u32, &str, &str)] = &[
    (
        4143,
        4326,
        "Cote d'Ivoire (Ivory Coast) - offshore",
        "C PROJ uses EPSG:1470",
    ),
    (
        4149,
        4326,
        "Europe - Liechtenstein and Switzerland",
        "C PROJ uses EPSG:1766",
    ),
    (4152, 4326, "USA - CONUS - onshore", "C PROJ uses EPSG:1901"),
    (4156, 4258, "Slovakia", "C PROJ uses EPSG:4829"),
    (4156, 4326, "Czechia", "C PROJ uses EPSG:5239"),
    (4168, 4326, "Ghana - offshore", "C PROJ uses EPSG:1569"),
    (4181, 4258, "Luxembourg", "C PROJ uses EPSG:9938"),
    (4201, 4326, "Burkina Faso", "C PROJ uses EPSG:1100"),
    (4201, 4326, "Cameroon - onshore", "C PROJ uses EPSG:1100"),
    (4201, 4326, "Mali", "C PROJ uses EPSG:1100"),
    (4201, 4326, "Senegal - onshore", "C PROJ uses EPSG:1100"),
    (
        4208,
        4326,
        "Brazil - Campos; Espirito Santo and Santos basins",
        "C PROJ uses EPSG:5051",
    ),
    (
        4208,
        4326,
        "Brazil - Reconcavo and Jacuipe",
        "C PROJ uses EPSG:5063",
    ),
    (
        4208,
        4674,
        "Brazil - Reconcavo and Jacuipe",
        "C PROJ uses EPSG:5062",
    ),
    (4209, 4326, "Lesotho", "C PROJ uses EPSG:1113"),
    (4209, 4326, "Zambia", "C PROJ uses EPSG:1120"),
    (
        4211,
        4326,
        "Indonesia - Bali Sea west",
        "C PROJ uses EPSG:8452",
    ),
    (
        4211,
        4326,
        "Indonesia - Bali, Java and western Sumatra onshore",
        "C PROJ uses EPSG:8452",
    ),
    (4214, 4326, "China - Ordos basin", "C PROJ uses EPSG:15936"),
];

fn load_corpus() -> Vec<ParityEntry> {
    let path = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../testdata/selection_parity.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 selection_matches_c_proj() {
    let corpus = load_corpus();
    assert!(!corpus.is_empty(), "selection parity corpus is empty");

    let mut matched = 0;
    let mut outside_registry = 0;
    let mut known = 0;
    let mut failures = Vec::new();

    for entry in &corpus {
        let label = format!(
            "EPSG:{}→EPSG:{} at ({:.4}, {:.4}) [{}]",
            entry.source_epsg,
            entry.target_epsg,
            entry.probe_lon,
            entry.probe_lat,
            entry.probe_extent_name
        );

        // Ranking parity is only assessable inside our registry subset.
        if registry::lookup_epsg(entry.source_epsg).is_none()
            || registry::lookup_epsg(entry.target_epsg).is_none()
        {
            outside_registry += 1;
            continue;
        }
        let expected_in_registry = entry
            .expected_operation_epsg
            .map(|code| registry::lookup_operation(CoordinateOperationId(code)).is_some())
            .unwrap_or(false);
        if !expected_in_registry {
            outside_registry += 1;
            continue;
        }

        if KNOWN_DIVERGENCES.iter().any(|(s, t, extent, _)| {
            *s == entry.source_epsg
                && *t == entry.target_epsg
                && entry.probe_extent_name.contains(extent)
        }) {
            known += 1;
            continue;
        }

        let options = SelectionOptions {
            area_of_interest: Some(AreaOfInterest::geographic_point(Coord::new(
                entry.probe_lon,
                entry.probe_lat,
            ))),
            ..SelectionOptions::default()
        };
        let transform = match Transform::with_selection_options(
            &format!("EPSG:{}", entry.source_epsg),
            &format!("EPSG:{}", entry.target_epsg),
            options,
        ) {
            Ok(transform) => transform,
            Err(error) => {
                failures.push(format!("{label}: transform construction failed: {error}"));
                continue;
            }
        };

        let selected = transform.selected_operation();
        let selected_code = selected.id.map(|id| id.0);
        if selected_code == entry.expected_operation_epsg {
            matched += 1;
        } else {
            failures.push(format!(
                "{label}: C PROJ used EPSG:{:?} ({}), proj-core selected EPSG:{:?} ({})",
                entry.expected_operation_epsg,
                entry.expected_operation_name,
                selected_code,
                selected.name
            ));
        }
    }

    eprintln!(
        "Selection parity: {matched} matched, {} diverged, {known} known divergences, \
         {outside_registry} outside registry subset, {} total",
        failures.len(),
        corpus.len()
    );

    if !failures.is_empty() {
        panic!(
            "{} selection parity failures:\n{}",
            failures.len(),
            failures.join("\n")
        );
    }
    assert!(matched > 0, "no comparable selection parity entries");
}