aerocontext-planning 0.4.2

Flight-route planning over aerocontext: Garmin .fpl import/export, route expansion, corridor generation, and the vertical cross-section
Documentation
use aerocontext_core::geo;

use super::*;

fn p(lat: f64, lon: f64) -> GeoPoint {
    GeoPoint { lat, lon }
}

/// A two-leg dogleg: north up the prime meridian, then northeast.
fn dogleg() -> Corridor {
    Corridor::around_points(&[p(0.0, 0.0), p(2.0, 0.0), p(3.5, 1.5)], 25.0).unwrap()
}

#[test]
fn construction_validates_inputs() {
    assert!(matches!(
        Corridor::around_points(&[p(0.0, 0.0)], 25.0),
        Err(CorridorError::TooFewPoints { got: 1 })
    ));
    assert!(matches!(
        Corridor::around_points(&[p(0.0, 0.0), p(1.0, 0.0)], 0.0),
        Err(CorridorError::InvalidWidth { .. })
    ));
    assert!(matches!(
        Corridor::around_points(&[p(0.0, 0.0), p(1.0, 0.0)], f64::NAN),
        Err(CorridorError::InvalidWidth { .. })
    ));
}

#[test]
fn every_centerline_point_is_contained() {
    let corridor = dogleg();
    for vertex in &corridor.centerline {
        assert!(corridor.contains(*vertex));
        assert!(corridor.distance_to_centerline_nm(*vertex) < 1e-6);
    }
}

#[test]
fn probes_abeam_each_leg_classify_by_width() {
    let corridor = dogleg();
    // Abeam the first leg (north-up): offsets are pure longitude.
    let inside = geo::destination(p(1.0, 0.0), 90.0, 0.9 * 25.0);
    let outside = geo::destination(p(1.0, 0.0), 90.0, 1.1 * 25.0);
    assert!(corridor.contains(inside));
    assert!(!corridor.contains(outside));
    // Abeam the second leg.
    let mid = p(2.75, 0.75);
    let leg_bearing = geo::initial_bearing_deg(p(2.0, 0.0), p(3.5, 1.5));
    let inside2 = geo::destination(mid, leg_bearing - 90.0, 0.9 * 25.0);
    let outside2 = geo::destination(mid, leg_bearing - 90.0, 1.1 * 25.0);
    assert!(corridor.contains(inside2));
    assert!(!corridor.contains(outside2));
}

#[test]
fn endpoint_caps_round_the_ends() {
    let corridor = dogleg();
    // Just beyond the start, within the cap radius.
    let behind_inside = geo::destination(p(0.0, 0.0), 180.0, 0.9 * 25.0);
    let behind_outside = geo::destination(p(0.0, 0.0), 180.0, 1.1 * 25.0);
    assert!(corridor.contains(behind_inside));
    assert!(!corridor.contains(behind_outside));
}

#[test]
fn polygon_ring_encloses_the_centerline_bbox() {
    let corridor = dogleg();
    assert!(
        corridor.polygon.len() >= 2 * corridor.centerline.len() + 2 * (CAP_SEGMENTS as usize - 1),
        "ring carries both sides plus caps: {}",
        corridor.polygon.len()
    );
    let area = corridor.area();
    let (sw, ne) = area.enclosing_bbox().unwrap();
    for vertex in &corridor.centerline {
        assert!(vertex.lat >= sw.lat && vertex.lat <= ne.lat);
        assert!(vertex.lon >= sw.lon && vertex.lon <= ne.lon);
    }
    // The polygon area agrees with the leg-wise predicate well inside.
    assert_eq!(area.contains(p(1.0, 0.1)), Some(true));
}

#[test]
fn hairpin_truth_stays_legwise() {
    // A 170° hairpin: the display ring may self-intersect, but the
    // leg-wise predicate must classify correctly regardless.
    let corridor = Corridor::around_points(&[p(0.0, 0.0), p(2.0, 0.2), p(0.2, 0.5)], 10.0).unwrap();
    let on_first_leg = p(1.0, 0.1);
    assert!(corridor.contains(on_first_leg));
    let far_away = p(1.0, 3.0);
    assert!(!corridor.contains(far_away));
}