efb 0.7.0

Electronic Flight Bag library to plan and conduct a flight.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Joe Pearson
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use arinc424::fields;
use arinc424::fields::LowerUpperLimit;

use crate::measurements::Angle;
use crate::nd::*;
use crate::MagneticVariation;
use crate::VerticalDistance;
use geo::Point;

impl<'a> TryFrom<fields::Cycle<'a>> for AiracCycle {
    type Error = arinc424::Error;

    fn try_from(value: fields::Cycle) -> Result<Self, Self::Error> {
        Ok(AiracCycle::new(value.year()?, value.cycle()?))
    }
}

impl From<fields::ArspType> for AirspaceType {
    fn from(value: fields::ArspType) -> Self {
        match value {
            fields::ArspType::ClassC => Self::TMA,
            fields::ArspType::ControlArea => Self::CTA,
            fields::ArspType::TerminalControlArea => Self::TMA,
            fields::ArspType::RadarZone => Self::RadarZone,
            fields::ArspType::ClassB => Self::TMA,
            fields::ArspType::RadioMandatoryZone => Self::RMZ,
            fields::ArspType::TransponderMandatoryZone => Self::TMZ,
            fields::ArspType::ControlZone => Self::CTR,
        }
    }
}

/// Parses the ICAO classification from the ARINC 424 `arsp_class` field.
///
/// Falls back to inferring the classification from the `ArspType` when the
/// explicit field is absent (e.g. `ClassB` → `B`, `ClassC` → `C`).
pub fn parse_classification(
    arsp_type: fields::ArspType,
    arsp_class: Option<&fields::AirspaceClassification<'_>>,
) -> Option<AirspaceClassification> {
    // Try the explicit arsp_class field first
    if let Some(class) = arsp_class {
        match class.as_bytes()[0] {
            b'A' => return Some(AirspaceClassification::A),
            b'B' => return Some(AirspaceClassification::B),
            b'C' => return Some(AirspaceClassification::C),
            b'D' => return Some(AirspaceClassification::D),
            b'E' => return Some(AirspaceClassification::E),
            b'F' => return Some(AirspaceClassification::F),
            b'G' => return Some(AirspaceClassification::G),
            _ => {}
        }
    }

    // Infer from ArspType for hybrid variants
    match arsp_type {
        fields::ArspType::ClassB => Some(AirspaceClassification::B),
        fields::ArspType::ClassC => Some(AirspaceClassification::C),
        _ => None,
    }
}

impl From<fields::RestrictiveType> for AirspaceType {
    fn from(value: fields::RestrictiveType) -> Self {
        match value {
            fields::RestrictiveType::Danger => Self::Danger,
            fields::RestrictiveType::Prohibited => Self::Prohibited,
            fields::RestrictiveType::Restricted
            | fields::RestrictiveType::Alert
            | fields::RestrictiveType::Caution
            | fields::RestrictiveType::LongTermTFR
            | fields::RestrictiveType::MOA
            | fields::RestrictiveType::NationalSecurityArea
            | fields::RestrictiveType::Training
            | fields::RestrictiveType::Warning
            | fields::RestrictiveType::UnspecifiedOrUnknown => Self::Restricted,
        }
    }
}

impl<'a> TryFrom<fields::IcaoCode<'a>> for LocationIndicator {
    type Error = arinc424::Error;

    fn try_from(value: fields::IcaoCode<'a>) -> Result<Self, Self::Error> {
        LocationIndicator::try_from(value.as_str()).map_err(|_| arinc424::Error::InvalidVariant {
            field: "IcaoCode",
            bytes: value.as_bytes().to_vec(),
            expected: "valid location identifier",
        })
    }
}

/// Convert ARINC 424 latitude/longitude fields to a geo::Point.
/// geo uses (x, y) = (longitude, latitude).
pub fn lat_lon_to_point<'a>(
    lat: fields::Latitude<'a>,
    lon: fields::Longitude<'a>,
) -> Result<Point<f64>, arinc424::Error> {
    Ok(Point::new(lon.as_decimal()?, lat.as_decimal()?))
}

impl From<fields::MagVar> for MagneticVariation {
    fn from(value: fields::MagVar) -> Self {
        match value {
            fields::MagVar::East(d) => Self::East(d),
            fields::MagVar::West(d) => Self::West(d),
            fields::MagVar::OrientedToTrueNorth => Self::OrientedToTrueNorth,
        }
    }
}

impl<'a> From<fields::RegnCode<'a>> for Region {
    fn from(value: fields::RegnCode) -> Self {
        match value.as_str() {
            "ENRT" => Self::Enroute,
            // TODO: Change terminal area code.
            icao => Self::TerminalArea(
                icao.as_bytes()
                    .try_into()
                    .expect("ICAO should be 4 character"),
            ),
        }
    }
}

impl From<fields::RwyBrg> for Angle {
    fn from(rwy_brg: fields::RwyBrg) -> Self {
        match rwy_brg {
            fields::RwyBrg::MagneticNorth(degree) => Self::m(degree),
            fields::RwyBrg::TrueNorth(degree) => Self::t(degree as f32),
        }
    }
}

impl From<fields::LowerUpperLimit> for VerticalDistance {
    fn from(value: LowerUpperLimit) -> Self {
        match value {
            // TODO: Add proper limits to airspace
            LowerUpperLimit::Altitude(alt) => VerticalDistance::Altitude(alt as u16),
            LowerUpperLimit::FlightLevel(fl) => VerticalDistance::Fl(fl),
            LowerUpperLimit::NotSpecified => VerticalDistance::Unlimited,
            LowerUpperLimit::Unlimited => VerticalDistance::Unlimited,
            LowerUpperLimit::Ground => VerticalDistance::Gnd,
            LowerUpperLimit::MeanSeaLevel => VerticalDistance::Msl(0),
            LowerUpperLimit::NOTAM => VerticalDistance::Unlimited,
        }
    }
}