openair 0.5.0

Library for reading and writing airspace files in OpenAir format.
Documentation
use std::io::Cursor;

use indoc::indoc;
use insta::assert_debug_snapshot;
use openair::*;

#[test]
fn test_switzerland_fixture() {
    let airspace = include_bytes!("../example_data/Switzerland.txt");
    let mut cursor = Cursor::new(airspace);

    let spaces = parse(&mut cursor).collect::<Result<Vec<_>, _>>().unwrap();
    assert_debug_snapshot!(spaces.first().unwrap());

    let names = spaces.iter().map(|a| a.to_string()).collect::<Vec<_>>();
    assert_debug_snapshot!(names);
}

#[test]
fn test_germany_fixture() {
    let airspace = include_bytes!("../example_data/Germany.txt");
    let mut cursor = Cursor::new(airspace);

    let spaces = parse(&mut cursor).collect::<Result<Vec<_>, _>>().unwrap();
    assert_debug_snapshot!(spaces.first().unwrap());

    let names = spaces.iter().map(|a| a.to_string()).collect::<Vec<_>>();
    assert_debug_snapshot!(names);
}

#[test]
fn test_germany_border_fixture() {
    let airspace = include_bytes!("../example_data/Germany_Border.txt");
    let mut cursor = Cursor::new(airspace);

    let spaces = parse(&mut cursor).collect::<Result<Vec<_>, _>>().unwrap();
    assert_debug_snapshot!(spaces.first().unwrap());

    let names = spaces.iter().map(|a| a.to_string()).collect::<Vec<_>>();
    assert_debug_snapshot!(names);
}

#[test]
fn test_france_fixture() {
    let airspace = include_bytes!("../example_data/France.txt");
    let mut cursor = Cursor::new(airspace);

    let spaces = parse(&mut cursor).collect::<Result<Vec<_>, _>>().unwrap();
    assert_debug_snapshot!(spaces.first().unwrap());
    assert_debug_snapshot!(spaces.last().unwrap());

    let names = spaces.iter().map(|a| a.to_string()).collect::<Vec<_>>();
    assert_debug_snapshot!(names);
}

/// Parse an airspace as generated by flyland.
#[test]
fn flyland_buochs() {
    let mut airspace = indoc! {"
        AC D
        AN BUOCHS Be CTR 119.625
        AL GND
        AH 12959 ft
        DP 46:57:13 N 008:27:52 E
        DP 46:57:46 N 008:30:41 E
        DP 46:57:55 N 008:28:40 E
        DP 46:58:28 N 008:27:56 E
        DP 46:57:13 N 008:27:52 E
        * n-Points: 5
    "}
    .as_bytes();
    let mut spaces = parse(&mut airspace).collect::<Result<Vec<_>, _>>().unwrap();
    assert_eq!(spaces.len(), 1);
    let space: Airspace = spaces.pop().unwrap();
    assert_eq!(space.name.as_deref(), Some("BUOCHS Be CTR 119.625"));
    assert_eq!(space.lower_bound, Altitude::Gnd);
    assert_eq!(space.upper_bound, Altitude::FeetAmsl(12959));
    if let Geometry::Polygon { segments } = space.geom {
        assert_eq!(segments.len(), 5);
    } else {
        panic!("Unexpected enum variant");
    }
}

/// The order of bounds should not matter.
#[test]
fn inverted_bounds() {
    let mut a1 = indoc! {"
        AC D
        AN SOMESPACE
        AL GND
        AH 12959 ft
        DP 46:57:13 N 008:27:52 E
        DP 46:57:46 N 008:30:41 E
        *
    "}
    .as_bytes();
    let mut a2 = indoc! {"
        AC D
        AN SOMESPACE
        AH 12959 ft
        AL GND
        DP 46:57:13 N 008:27:52 E
        DP 46:57:46 N 008:30:41 E
        *
    "}
    .as_bytes();
    let spaces1 = parse(&mut a1).collect::<Result<Vec<_>, _>>().unwrap();
    let spaces2 = parse(&mut a2).collect::<Result<Vec<_>, _>>().unwrap();
    let space1 = spaces1.last().unwrap();
    let space2 = spaces2.last().unwrap();
    assert_eq!(space1, space2);
}

/// Variables can be defined multiple times.
#[test]
fn multi_variable() {
    let mut a = indoc! {"
        AC D
        AN SOMESPACE
        AL GND
        AH FL100
        V X=52:00:00N 013:00:00E
        V D=+
        DA 2,0,30
        V X=52:00:00N 013:00:00E
        V D=-
        DA 4,60,30
        *
    "}
    .as_bytes();
    let spaces = parse(&mut a).collect::<Result<Vec<_>, _>>().unwrap();
    let airspace = spaces.last().unwrap();
    assert_eq!(
        airspace.geom,
        Geometry::Polygon {
            segments: vec![
                PolygonSegment::ArcSegment(ArcSegment {
                    centerpoint: Coord {
                        lat: 52.0,
                        lng: 13.0
                    },
                    radius: 2.0,
                    angle_start: 0.0,
                    angle_end: 30.0,
                    direction: Direction::Cw,
                }),
                PolygonSegment::ArcSegment(ArcSegment {
                    centerpoint: Coord {
                        lat: 52.0,
                        lng: 13.0
                    },
                    radius: 4.0,
                    angle_start: 60.0,
                    angle_end: 30.0,
                    direction: Direction::Ccw,
                }),
            ],
        }
    );
}

/// Test AY/AF/AG records.
#[test]
fn extension_records() {
    let mut a = indoc! {"
        AC D
        AN SOMESPACE
        AL GND
        AH 100 ft AGL
        AY AWY
        AF 132.350
        AG Dutch Mil
        AX 1234
        A* custom extension
        V X=52:00:00 N 013:00:00 E
        DC 5
    "}
    .as_bytes();
    let spaces = parse(&mut a).collect::<Result<Vec<_>, _>>().unwrap();
    let airspace = spaces.last().unwrap();
    assert_eq!(airspace.type_, Some("AWY".to_string()));
    assert_eq!(airspace.frequency, Some("132.350".to_string()));
    assert_eq!(airspace.call_sign, Some("Dutch Mil".to_string()));
    assert_eq!(airspace.transponder_code, Some(1234));
}

/// AN is optional per the OpenAir spec. Airspaces without AN should parse
/// successfully with `name` set to `None`.
#[test]
fn missing_name() {
    let mut airspace = indoc! {"
        AC D
        AL GND
        AH 5000 ft
        DP 50:00:00 N 010:00:00 E
        DP 50:00:00 N 010:01:00 E
        DP 50:01:00 N 010:01:00 E
        DP 50:01:00 N 010:00:00 E
    "}
    .as_bytes();
    let spaces = parse(&mut airspace).collect::<Result<Vec<_>, _>>().unwrap();
    assert_eq!(spaces.len(), 1);
    let space = &spaces[0];
    assert_eq!(space.name, None);
    assert_eq!(space.class, Class::D);
    assert_eq!(space.lower_bound, Altitude::Gnd);
    assert_eq!(space.upper_bound, Altitude::FeetAmsl(5000));
}

/// Test that AN (Airspace Name) can act as a separator when it appears before AC.
/// Some files use AN before AC, making AN the delimiter between airspaces.
#[test]
fn an_record_as_separator() {
    let mut airspace_data = indoc! {"
        AN FIRST AIRSPACE
        AC D
        AL GND
        AH 5000 ft
        DP 50:00:00 N 010:00:00 E
        DP 50:00:00 N 010:01:00 E
        DP 50:01:00 N 010:01:00 E
        DP 50:01:00 N 010:00:00 E

        AN SECOND AIRSPACE
        AC R
        * Random comment
        AL 1000 ft
        AH FL100
        DP 51:00:00 N 011:00:00 E
        DP 51:00:00 N 011:01:00 E
        DP 51:01:00 N 011:01:00 E
        DP 51:01:00 N 011:00:00 E
    "}
    .as_bytes();

    let spaces = parse(&mut airspace_data)
        .collect::<Result<Vec<_>, _>>()
        .unwrap();

    // Should parse as two separate airspaces
    assert_eq!(
        spaces.len(),
        2,
        "Expected 2 airspaces, got {}",
        spaces.len()
    );

    // Check first airspace
    let first = &spaces[0];
    assert_eq!(first.name.as_deref(), Some("FIRST AIRSPACE"));
    assert_eq!(first.class, Class::D);
    assert_eq!(first.lower_bound, Altitude::Gnd);
    assert_eq!(first.upper_bound, Altitude::FeetAmsl(5000));

    // Check second airspace
    let second = &spaces[1];
    assert_eq!(second.name.as_deref(), Some("SECOND AIRSPACE"));
    assert_eq!(second.class, Class::Restricted);
    assert_eq!(second.lower_bound, Altitude::FeetAmsl(1000));
    assert_eq!(second.upper_bound, Altitude::FlightLevel(100));
}