openair 0.6.0

Library for reading and writing airspace files in OpenAir format.
Documentation
use openair::{Airspace, AirspaceType, Altitude, Class, Coord, Geometry, LegacyClassConflict};

fn airspace(class: Class, airspace_type: Option<AirspaceType>) -> Airspace {
    Airspace {
        name: None,
        class,
        type_: airspace_type,
        lower_bound: Altitude::Gnd,
        upper_bound: Altitude::FlightLevel(100),
        geom: Geometry::Circle {
            centerpoint: Coord {
                lat: 47.0,
                lng: 8.0,
            },
            radius: 5.0,
        },
        frequency: None,
        call_sign: None,
        transponder_code: None,
        activation_times: None,
    }
}

#[test]
fn normalizes_legacy_classes() {
    for (legacy_class, expected_type) in [
        ("CTR", AirspaceType::ControlZone),
        ("R", AirspaceType::RestrictedArea),
        ("Q", AirspaceType::DangerArea),
        ("P", AirspaceType::ProhibitedArea),
        ("GP", AirspaceType::OverflightRestriction),
        ("W", AirspaceType::GlidingSector),
        ("RMZ", AirspaceType::RadioMandatoryZone),
        ("TMZ", AirspaceType::TransponderMandatoryZone),
    ] {
        let mut airspace = airspace(Class::Unknown(legacy_class.into()), None);

        airspace.normalize_legacy_class().unwrap();

        assert_eq!(airspace.class, Class::Unclassified);
        assert_eq!(airspace.type_, Some(expected_type));
    }
}

#[test]
fn accepts_matching_existing_type() {
    let mut airspace = airspace(
        Class::Unknown("R".into()),
        Some(AirspaceType::RestrictedArea),
    );

    airspace.normalize_legacy_class().unwrap();

    assert_eq!(airspace.class, Class::Unclassified);
    assert_eq!(airspace.type_, Some(AirspaceType::RestrictedArea));
}

#[test]
fn rejects_conflicting_existing_type_without_mutation() {
    let mut airspace = airspace(
        Class::Unknown("R".into()),
        Some(AirspaceType::RadioMandatoryZone),
    );

    let error = airspace.normalize_legacy_class().unwrap_err();

    assert_eq!(
        error,
        LegacyClassConflict {
            legacy_class: "R".into(),
            resolved_type: AirspaceType::RestrictedArea,
            existing_type: AirspaceType::RadioMandatoryZone,
        }
    );
    assert_eq!(
        error.to_string(),
        "Legacy class R resolves to AY R but airspace already has AY RMZ"
    );
    assert_eq!(airspace.class, Class::Unknown("R".into()));
    assert_eq!(airspace.type_, Some(AirspaceType::RadioMandatoryZone));
}

#[test]
fn leaves_other_classes_unchanged() {
    for class in [Class::D, Class::Unknown("FUTURE".into())] {
        let mut airspace = airspace(class.clone(), None);

        airspace.normalize_legacy_class().unwrap();

        assert_eq!(airspace.class, class);
        assert_eq!(airspace.type_, None);
    }
}

#[test]
fn normalization_is_idempotent() {
    let mut airspace = airspace(Class::Unknown("W".into()), None);

    airspace.normalize_legacy_class().unwrap();
    airspace.normalize_legacy_class().unwrap();

    assert_eq!(airspace.class, Class::Unclassified);
    assert_eq!(airspace.type_, Some(AirspaceType::GlidingSector));
}