aerocontext-core 0.4.0

Provider-neutral aeronautical-context model and the pluggable ContextProvider contract
Documentation
use super::*;

fn approx(a: f64, b: f64) -> bool {
    (a - b).abs() < 1e-9
}

#[test]
fn bbox_area_encloses_itself() {
    let area = Area::BoundingBox {
        south_west: GeoPoint {
            lat: 30.0,
            lon: -100.0,
        },
        north_east: GeoPoint {
            lat: 40.0,
            lon: -90.0,
        },
    };
    let (sw, ne) = area.enclosing_bbox().unwrap();
    assert!(approx(sw.lat, 30.0) && approx(sw.lon, -100.0));
    assert!(approx(ne.lat, 40.0) && approx(ne.lon, -90.0));
}

#[test]
fn point_radius_bbox_scales_longitude_by_latitude() {
    // 60 NM = 1 degree of latitude everywhere.
    let area = Area::PointRadius {
        center: GeoPoint {
            lat: 60.0,
            lon: 10.0,
        },
        radius_nm: 60.0,
    };
    let (sw, ne) = area.enclosing_bbox().unwrap();
    assert!(approx(sw.lat, 59.0) && approx(ne.lat, 61.0));
    // cos(60 deg) = 0.5, so the longitude half-width doubles.
    assert!(approx(sw.lon, 8.0) && approx(ne.lon, 12.0));
}

#[test]
fn point_radius_bbox_clamps_latitude_at_pole() {
    let area = Area::PointRadius {
        center: GeoPoint {
            lat: 89.5,
            lon: 0.0,
        },
        radius_nm: 120.0,
    };
    let (sw, ne) = area.enclosing_bbox().unwrap();
    assert!(approx(ne.lat, 90.0));
    assert!(sw.lat < 89.5);
    // Near-pole circles degrade to the full longitude band, not a panic.
    assert!(approx(ne.lon - sw.lon, 360.0));
}

#[test]
fn location_radius_needs_provider_resolution() {
    let area = Area::LocationRadius {
        ident: "KSFO".to_owned(),
        radius_nm: 50.0,
    };
    assert!(area.enclosing_bbox().is_none());
}

#[test]
fn request_round_trips_through_serde() {
    let request = AreaBriefingRequest::new(Area::PointRadius {
        center: GeoPoint {
            lat: 37.6,
            lon: -122.4,
        },
        radius_nm: 100.0,
    })
    .with_products(vec![
        ProductKind::Metar,
        ProductKind::Other("synopsis".to_owned()),
    ])
    .with_lookback_hours(Some(2))
    .with_departure_at("2026-06-05T14:30:00Z".parse().ok());
    let json = serde_json::to_string(&request).unwrap();
    let back: AreaBriefingRequest = serde_json::from_str(&json).unwrap();
    assert_eq!(back, request);
}

#[test]
fn polygon_contains_and_bbox() {
    // A square over the Bay Area.
    let square = Area::Polygon {
        vertices: vec![
            GeoPoint {
                lat: 37.0,
                lon: -123.0,
            },
            GeoPoint {
                lat: 38.5,
                lon: -123.0,
            },
            GeoPoint {
                lat: 38.5,
                lon: -121.5,
            },
            GeoPoint {
                lat: 37.0,
                lon: -121.5,
            },
        ],
    };
    let ksfo = GeoPoint {
        lat: 37.62,
        lon: -122.37,
    };
    let kden = GeoPoint {
        lat: 39.86,
        lon: -104.67,
    };
    assert_eq!(square.contains(ksfo), Some(true));
    assert_eq!(square.contains(kden), Some(false));
    let (sw, ne) = square.enclosing_bbox().unwrap();
    assert!(approx(sw.lat, 37.0) && approx(ne.lat, 38.5));
    assert!(approx(sw.lon, -123.0) && approx(ne.lon, -121.5));
}

#[test]
fn antimeridian_polygon_stays_contiguous() {
    // A ring straddling ±180 near the Aleutians.
    let ring = Area::Polygon {
        vertices: vec![
            GeoPoint {
                lat: 51.0,
                lon: 178.0,
            },
            GeoPoint {
                lat: 53.0,
                lon: 178.0,
            },
            GeoPoint {
                lat: 53.0,
                lon: -178.0,
            },
            GeoPoint {
                lat: 51.0,
                lon: -178.0,
            },
        ],
    };
    let (sw, ne) = ring.enclosing_bbox().unwrap();
    assert!(
        (ne.lon - sw.lon) < 10.0,
        "bbox spans the crossing, not the globe: {} .. {}",
        sw.lon,
        ne.lon
    );
    assert_eq!(
        ring.contains(GeoPoint {
            lat: 52.0,
            lon: 179.5,
        }),
        Some(true)
    );
    assert_eq!(
        ring.contains(GeoPoint {
            lat: 52.0,
            lon: 170.0,
        }),
        Some(false)
    );
}

#[test]
fn location_radius_contains_is_undecidable() {
    let area = Area::LocationRadius {
        ident: "KSFO".to_owned(),
        radius_nm: 25.0,
    };
    assert_eq!(area.contains(GeoPoint { lat: 0.0, lon: 0.0 }), None);
}