aerocontext_core/model.rs
1//! Provider-neutral briefing domain types.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// A geographic point in WGS84 decimal degrees.
7#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
8pub struct GeoPoint {
9 /// Latitude in degrees, north positive.
10 pub lat: f64,
11 /// Longitude in degrees, east positive.
12 pub lon: f64,
13}
14
15/// The geographic area a briefing covers.
16///
17/// Not every source supports every shape natively; adapters either convert
18/// (a point radius encloses a bounding box and vice versa) or return
19/// [`crate::ProviderError::Unsupported`].
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[non_exhaustive]
22pub enum Area {
23 /// Axis-aligned bounding box.
24 BoundingBox {
25 /// South-west corner.
26 south_west: GeoPoint,
27 /// North-east corner.
28 north_east: GeoPoint,
29 },
30 /// Circle of `radius_nm` nautical miles around a coordinate.
31 PointRadius {
32 /// Circle center.
33 center: GeoPoint,
34 /// Radius in nautical miles.
35 radius_nm: f64,
36 },
37 /// Circle of `radius_nm` nautical miles around a published location
38 /// (ICAO/FAA aerodrome, navaid or fix identifier).
39 LocationRadius {
40 /// Location identifier, e.g. `"KSFO"`.
41 ident: String,
42 /// Radius in nautical miles.
43 radius_nm: f64,
44 },
45}
46
47impl Area {
48 /// Smallest axis-aligned bounding box enclosing this area, as
49 /// `(south_west, north_east)`, when one can be computed without a
50 /// location database.
51 ///
52 /// Returns `None` for areas anchored to a published identifier; only
53 /// the provider can resolve those. The radius conversion uses the
54 /// small-area approximation (1 NM of latitude = 1/60 degree) and is
55 /// not meaningful within a radius of the poles.
56 pub fn enclosing_bbox(&self) -> Option<(GeoPoint, GeoPoint)> {
57 match self {
58 Self::BoundingBox {
59 south_west,
60 north_east,
61 } => Some((*south_west, *north_east)),
62 Self::PointRadius { center, radius_nm } => Some(bbox_around(*center, *radius_nm)),
63 Self::LocationRadius { .. } => None,
64 }
65 }
66}
67
68/// Bounding box of a circle, by the small-area equirectangular
69/// approximation.
70fn bbox_around(center: GeoPoint, radius_nm: f64) -> (GeoPoint, GeoPoint) {
71 let lat_delta = radius_nm / 60.0;
72 // Longitude degrees shrink with cos(lat); clamp so a circle near a
73 // pole degrades to "all longitudes" instead of dividing by zero.
74 let lon_scale = center.lat.to_radians().cos().max(1e-6);
75 let lon_delta = (lat_delta / lon_scale).min(180.0);
76 (
77 GeoPoint {
78 lat: (center.lat - lat_delta).max(-90.0),
79 lon: center.lon - lon_delta,
80 },
81 GeoPoint {
82 lat: (center.lat + lat_delta).min(90.0),
83 lon: center.lon + lon_delta,
84 },
85 )
86}
87
88/// The kind of an individual briefing product.
89#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[non_exhaustive]
91pub enum ProductKind {
92 /// Aerodrome routine/special weather report.
93 Metar,
94 /// Terminal aerodrome forecast.
95 Taf,
96 /// Pilot weather report.
97 Pirep,
98 /// Significant meteorological information.
99 Sigmet,
100 /// Airmen's meteorological information.
101 Airmet,
102 /// Graphical AIRMET.
103 GAirmet,
104 /// Center weather advisory.
105 Cwa,
106 /// Notice to air missions.
107 Notam,
108 /// A product this crate does not model yet, identified by the
109 /// source's own name for it.
110 Other(String),
111}
112
113/// A request for a simple area briefing.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub struct AreaBriefingRequest {
116 /// Area the briefing covers.
117 pub area: Area,
118 /// Products to include. Empty means the source's default set.
119 pub products: Vec<ProductKind>,
120 /// How many hours of history to include where a product has history
121 /// (e.g. past METARs). `None` means the source's default.
122 pub lookback_hours: Option<u32>,
123 /// Intended departure time the briefing is for. `None` means "now".
124 /// Sources that brief relative to a departure instant (Leidos) use
125 /// it; observation-oriented sources may ignore it.
126 pub departure_at: Option<DateTime<Utc>>,
127}
128
129/// A validity window, e.g. a TAF's forecast period `0418/0524`.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
131pub struct ValidPeriod {
132 /// Start of the window.
133 pub start: DateTime<Utc>,
134 /// End of the window.
135 pub end: DateTime<Utc>,
136}
137
138impl ValidPeriod {
139 /// A window spanning `start` to `end`.
140 pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
141 Self { start, end }
142 }
143}
144
145/// One discrete weather product, kept verbatim as published.
146///
147/// `#[non_exhaustive]`: construct with [`Product::new`] and the `with_*`
148/// setters, so a future source (e.g. FIS-B) can add fields without breaking
149/// existing construction sites.
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151#[non_exhaustive]
152pub struct Product {
153 /// What kind of product this is.
154 pub kind: ProductKind,
155 /// Station or location identifier when the product is tied to one.
156 pub location: Option<String>,
157 /// Issue/observation time when the source provides one.
158 pub issued_at: Option<DateTime<Utc>>,
159 /// Validity window when the product defines one (a TAF's forecast
160 /// period); `None` for instantaneous reports such as a METAR.
161 pub valid: Option<ValidPeriod>,
162 /// Verbatim product text as published by the source.
163 pub raw_text: String,
164}
165
166impl Product {
167 /// A product of `kind` carrying its verbatim `raw_text`. Optional fields
168 /// default to `None`; set them with the `with_*` builders.
169 pub fn new(kind: ProductKind, raw_text: impl Into<String>) -> Self {
170 Self {
171 kind,
172 location: None,
173 issued_at: None,
174 valid: None,
175 raw_text: raw_text.into(),
176 }
177 }
178
179 /// Set the station/location identifier.
180 #[must_use]
181 pub fn with_location(mut self, location: Option<String>) -> Self {
182 self.location = location;
183 self
184 }
185
186 /// Set the issue/observation time.
187 #[must_use]
188 pub fn with_issued_at(mut self, issued_at: Option<DateTime<Utc>>) -> Self {
189 self.issued_at = issued_at;
190 self
191 }
192
193 /// Set the validity window.
194 #[must_use]
195 pub fn with_valid(mut self, valid: Option<ValidPeriod>) -> Self {
196 self.valid = valid;
197 self
198 }
199}
200
201/// A completed weather briefing.
202///
203/// Sources differ in shape: product-oriented APIs fill [`Self::products`],
204/// document-oriented services (one rendered briefing text) fill
205/// [`Self::narrative`]. Either may be empty, both may be present.
206/// `#[non_exhaustive]`: construct with [`Briefing::new`] and the `with_*`
207/// setters.
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209#[non_exhaustive]
210pub struct Briefing {
211 /// [`crate::ContextProvider::name`] of the producing source.
212 pub source: String,
213 /// When the briefing was generated, if the source reports it.
214 pub generated_at: Option<DateTime<Utc>>,
215 /// Single-document briefing text, for document-oriented sources.
216 pub narrative: Option<String>,
217 /// Discrete products, for product-oriented sources.
218 pub products: Vec<Product>,
219}
220
221impl Briefing {
222 /// An empty briefing attributed to `source`; fill it with the `with_*`
223 /// setters.
224 pub fn new(source: impl Into<String>) -> Self {
225 Self {
226 source: source.into(),
227 generated_at: None,
228 narrative: None,
229 products: Vec::new(),
230 }
231 }
232
233 /// Set the generation time.
234 #[must_use]
235 pub fn with_generated_at(mut self, generated_at: Option<DateTime<Utc>>) -> Self {
236 self.generated_at = generated_at;
237 self
238 }
239
240 /// Set the single-document narrative.
241 #[must_use]
242 pub fn with_narrative(mut self, narrative: Option<String>) -> Self {
243 self.narrative = narrative;
244 self
245 }
246
247 /// Set the discrete products.
248 #[must_use]
249 pub fn with_products(mut self, products: Vec<Product>) -> Self {
250 self.products = products;
251 self
252 }
253}
254
255#[cfg(test)]
256mod tests;