use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct GeoPoint {
pub lat: f64,
pub lon: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Area {
BoundingBox {
south_west: GeoPoint,
north_east: GeoPoint,
},
PointRadius {
center: GeoPoint,
radius_nm: f64,
},
LocationRadius {
ident: String,
radius_nm: f64,
},
}
impl Area {
pub fn enclosing_bbox(&self) -> Option<(GeoPoint, GeoPoint)> {
match self {
Self::BoundingBox {
south_west,
north_east,
} => Some((*south_west, *north_east)),
Self::PointRadius { center, radius_nm } => Some(bbox_around(*center, *radius_nm)),
Self::LocationRadius { .. } => None,
}
}
}
fn bbox_around(center: GeoPoint, radius_nm: f64) -> (GeoPoint, GeoPoint) {
let lat_delta = radius_nm / 60.0;
let lon_scale = center.lat.to_radians().cos().max(1e-6);
let lon_delta = (lat_delta / lon_scale).min(180.0);
(
GeoPoint {
lat: (center.lat - lat_delta).max(-90.0),
lon: center.lon - lon_delta,
},
GeoPoint {
lat: (center.lat + lat_delta).min(90.0),
lon: center.lon + lon_delta,
},
)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ProductKind {
Metar,
Taf,
Pirep,
Sigmet,
Airmet,
GAirmet,
Cwa,
Notam,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AreaBriefingRequest {
pub area: Area,
pub products: Vec<ProductKind>,
pub lookback_hours: Option<u32>,
pub departure_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidPeriod {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
impl ValidPeriod {
pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
Self { start, end }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Product {
pub kind: ProductKind,
pub location: Option<String>,
pub issued_at: Option<DateTime<Utc>>,
pub valid: Option<ValidPeriod>,
pub raw_text: String,
}
impl Product {
pub fn new(kind: ProductKind, raw_text: impl Into<String>) -> Self {
Self {
kind,
location: None,
issued_at: None,
valid: None,
raw_text: raw_text.into(),
}
}
#[must_use]
pub fn with_location(mut self, location: Option<String>) -> Self {
self.location = location;
self
}
#[must_use]
pub fn with_issued_at(mut self, issued_at: Option<DateTime<Utc>>) -> Self {
self.issued_at = issued_at;
self
}
#[must_use]
pub fn with_valid(mut self, valid: Option<ValidPeriod>) -> Self {
self.valid = valid;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Briefing {
pub source: String,
pub generated_at: Option<DateTime<Utc>>,
pub narrative: Option<String>,
pub products: Vec<Product>,
}
impl Briefing {
pub fn new(source: impl Into<String>) -> Self {
Self {
source: source.into(),
generated_at: None,
narrative: None,
products: Vec::new(),
}
}
#[must_use]
pub fn with_generated_at(mut self, generated_at: Option<DateTime<Utc>>) -> Self {
self.generated_at = generated_at;
self
}
#[must_use]
pub fn with_narrative(mut self, narrative: Option<String>) -> Self {
self.narrative = narrative;
self
}
#[must_use]
pub fn with_products(mut self, products: Vec<Product>) -> Self {
self.products = products;
self
}
}
#[cfg(test)]
mod tests;