Skip to main content

aerocontext_planning/
flightplan.rs

1//! Garmin FlightPlan v1 `.fpl` flight plans: import, export, and the
2//! bridge to a [`aerocontext_core::RouteBriefingRequest`].
3//!
4//! The schema is Garmin's `http://www8.garmin.com/xmlschemas/FlightPlan/v1`:
5//! a `<waypoint-table>` dictionary of `<waypoint>` entries (identifier,
6//! type, lat, lon) and an ordered `<route>` of `<route-point>`s that
7//! reference it. ForeFlight and other tools add a `<flight-data>` block
8//! (ETD, cruise altitude) and an `<aircraft>` block as siblings; those are
9//! preserved on import and re-emitted on export. Parsing is lenient —
10//! `country-code`/`comment` (strictly required by the XSD) are treated as
11//! optional, since real exporters omit them — but a route point that does
12//! not resolve to a table waypoint is a hard error: a corridor must never
13//! be built from a hole.
14
15mod garmin;
16
17pub use garmin::FplError;
18
19use aerocontext_core::{
20    FlightRules, GeoPoint, NavPoint, NavPointKind, RouteBriefingRequest, RouteWaypoint,
21};
22use chrono::{DateTime, Utc};
23
24use crate::route::{ExpandedRoute, RoutePoint};
25
26/// A Garmin waypoint type. Recognized values round-trip on export;
27/// an unknown type is coerced to `USER WAYPOINT`.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum WaypointType {
31    /// `AIRPORT`.
32    Airport,
33    /// `NDB`.
34    Ndb,
35    /// `VOR`.
36    Vor,
37    /// `INT` (enroute intersection / fix).
38    Int,
39    /// `INT-VRP` (visual reporting point).
40    IntVrp,
41    /// `USER WAYPOINT` (operator-defined lat/lon).
42    User,
43}
44
45impl WaypointType {
46    /// The schema string for this type.
47    #[must_use]
48    pub fn as_str(self) -> &'static str {
49        match self {
50            Self::Airport => "AIRPORT",
51            Self::Ndb => "NDB",
52            Self::Vor => "VOR",
53            Self::Int => "INT",
54            Self::IntVrp => "INT-VRP",
55            Self::User => "USER WAYPOINT",
56        }
57    }
58
59    /// Parse a schema string, `None` for an unknown value.
60    #[must_use]
61    pub fn parse(value: &str) -> Option<Self> {
62        match value.trim() {
63            "AIRPORT" => Some(Self::Airport),
64            "NDB" => Some(Self::Ndb),
65            "VOR" => Some(Self::Vor),
66            "INT" => Some(Self::Int),
67            "INT-VRP" => Some(Self::IntVrp),
68            "USER WAYPOINT" => Some(Self::User),
69            _ => None,
70        }
71    }
72
73    /// The closest [`NavPointKind`]; airports map to airports, navaids to
74    /// navaids, everything else to a generic waypoint.
75    #[must_use]
76    pub fn nav_kind(self) -> NavPointKind {
77        match self {
78            Self::Airport => NavPointKind::Airport,
79            Self::Vor | Self::Ndb => NavPointKind::Navaid,
80            Self::Int | Self::IntVrp | Self::User => NavPointKind::Waypoint,
81        }
82    }
83}
84
85/// One ordered waypoint of an imported plan: its identifier, type, and
86/// position (carried by the `.fpl` itself, so no snapshot is needed to
87/// build geometry).
88#[derive(Debug, Clone, PartialEq)]
89#[non_exhaustive]
90pub struct PlanWaypoint {
91    /// Identifier from the waypoint table.
92    pub identifier: String,
93    /// Garmin waypoint type.
94    pub kind: WaypointType,
95    /// WGS84 position.
96    pub position: GeoPoint,
97    /// ICAO country code when the exporter included one.
98    pub country_code: Option<String>,
99}
100
101/// An imported Garmin flight plan: ordered route plus the metadata other
102/// tools attach.
103#[derive(Debug, Clone, PartialEq, Default)]
104#[non_exhaustive]
105pub struct FlightPlan {
106    /// `<route-name>`, when present.
107    pub name: Option<String>,
108    /// `<created>` timestamp.
109    pub created: Option<DateTime<Utc>>,
110    /// Estimated time of departure (`<flight-data><etd-zulu>`).
111    pub etd: Option<DateTime<Utc>>,
112    /// Planned cruise altitude in feet (`<flight-data><altitude-ft>`).
113    pub cruise_altitude_ft: Option<i32>,
114    /// Aircraft tail number (`<aircraft><aircraft-tailnumber>`).
115    pub aircraft_tail: Option<String>,
116    /// Ordered route waypoints, departure first, destination last.
117    pub route: Vec<PlanWaypoint>,
118}
119
120impl FlightPlan {
121    /// Parse a Garmin `.fpl` document. Accepts UTF-16 (with BOM, as
122    /// ForeFlight exports) or UTF-8.
123    pub fn from_fpl_bytes(bytes: &[u8]) -> Result<Self, FplError> {
124        garmin::parse(bytes)
125    }
126
127    /// Serialize to a Garmin `.fpl` document (UTF-8), in the widely
128    /// accepted ForeFlight dialect: a `<waypoint-table>` dictionary, an
129    /// ordered `<route>`, and a `<flight-data>` block carrying ETD and
130    /// cruise altitude.
131    #[must_use]
132    pub fn to_fpl_string(&self) -> String {
133        garmin::to_xml(self)
134    }
135
136    /// The departure identifier (first route point).
137    #[must_use]
138    pub fn departure(&self) -> Option<&str> {
139        self.route.first().map(|w| w.identifier.as_str())
140    }
141
142    /// The destination identifier (last route point).
143    #[must_use]
144    pub fn destination(&self) -> Option<&str> {
145        self.route.last().map(|w| w.identifier.as_str())
146    }
147
148    /// The route as [`NavPoint`]s, for snapshot-free resolution or
149    /// display.
150    #[must_use]
151    pub fn nav_points(&self) -> Vec<NavPoint> {
152        self.route
153            .iter()
154            .map(|w| NavPoint::new(&w.identifier, w.kind.nav_kind(), w.position))
155            .collect()
156    }
157
158    /// The route as an [`ExpandedRoute`] — the `.fpl` already carries
159    /// every position, so this needs no navdata snapshot and feeds
160    /// [`Corridor::around_route`](crate::Corridor::around_route)
161    /// directly.
162    #[must_use]
163    pub fn to_expanded_route(&self) -> ExpandedRoute {
164        let points = self
165            .route
166            .iter()
167            .map(|w| RoutePoint::new(w.position).with_ident(Some(w.identifier.clone())))
168            .collect();
169        ExpandedRoute {
170            points,
171            procedures: Vec::new(),
172        }
173    }
174
175    /// Build a [`RouteBriefingRequest`] over this plan with a
176    /// `corridor_half_width_nm` corridor, carrying ETD, cruise altitude,
177    /// and flight rules so providers compose the right requests.
178    #[must_use]
179    pub fn to_route_briefing_request(
180        &self,
181        corridor_half_width_nm: f64,
182        flight_rules: FlightRules,
183    ) -> RouteBriefingRequest {
184        let waypoints = self
185            .route
186            .iter()
187            .map(|w| RouteWaypoint::new(w.position).with_ident(Some(w.identifier.clone())))
188            .collect();
189        RouteBriefingRequest::new(waypoints, corridor_half_width_nm)
190            .with_cruise_altitude_ft(self.cruise_altitude_ft)
191            .with_departure_at(self.etd)
192            .with_flight_rules(flight_rules)
193    }
194}
195
196#[cfg(test)]
197mod tests;