mgrs-convert 0.1.0

Convert WGS84 latitude-longitude to and from UTM, UPS, and MGRS grid references, round-tripping exactly at every precision from 100 km to 1 um
Documentation
//! Shared helpers for the golden-vector integration tests.

// Each test binary compiles this module separately and uses a subset of it.
#![allow(dead_code)]

use mgrs_convert::UtmUps;

/// Reads a checked-in TSV, skipping the oracle-version header line.
pub fn read_tsv(name: &str) -> Vec<Vec<String>> {
    let path = format!("{}/tests/vectors/{name}", env!("CARGO_MANIFEST_DIR"));
    let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
    text.lines()
        .filter(|l| !l.starts_with('#') && !l.is_empty())
        .map(|l| l.split('\t').map(str::to_owned).collect())
        .collect()
}

/// Parses the oracle's UTM/UPS output form: "31n 166021.443 0.000" or
/// "n 2000000.000 1333272.296". Returns (zone, north, easting, northing).
pub fn parse_utm_triplet(s: &str) -> (u8, bool, f64, f64) {
    let toks: Vec<&str> = s.split_whitespace().collect();
    assert_eq!(toks.len(), 3, "not a UTM/UPS triplet: {s}");
    let zh = toks[0];
    let north = zh.ends_with('n');
    let zone: u8 = zh[..zh.len() - 1].parse().unwrap_or(0);
    (
        zone,
        north,
        toks[1].parse().expect("easting"),
        toks[2].parse().expect("northing"),
    )
}

/// Builds a UtmUps from the oracle's input form ("18n 900000.0 4511315.422"
/// or "n 2750000.0 2000000.0").
pub fn utmups_from_input(s: &str) -> Result<UtmUps, mgrs_convert::Error> {
    let (zone, north, e, n) = parse_utm_triplet(s);
    if zone == 0 {
        UtmUps::ups(north, e, n)
    } else {
        UtmUps::utm(zone, north, e, n)
    }
}

/// Ground distance in metres between two lat/lon pairs, by a local metric that
/// never underestimates: 111,700 m per degree exceeds both the meridian degree
/// everywhere and the scaled parallel degree.
pub fn ground_distance_m(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
    let dlat = lat1 - lat2;
    let mut dlon = (lon1 - lon2).rem_euclid(360.0);
    if dlon > 180.0 {
        dlon -= 360.0;
    }
    let dlon_scaled = dlon * lat1.to_radians().cos();
    dlat.hypot(dlon_scaled) * 111_700.0
}