use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::metar::MetarObservation;
#[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,
},
Polygon {
vertices: Vec<GeoPoint>,
},
}
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,
Self::Polygon { vertices } => polygon_bbox(vertices),
}
}
pub fn contains(&self, point: GeoPoint) -> Option<bool> {
match self {
Self::BoundingBox {
south_west,
north_east,
} => Some(
point.lat >= south_west.lat
&& point.lat <= north_east.lat
&& point.lon >= south_west.lon
&& point.lon <= north_east.lon,
),
Self::PointRadius { center, radius_nm } => {
Some(crate::geo::distance_nm(*center, point) <= *radius_nm)
}
Self::LocationRadius { .. } => None,
Self::Polygon { vertices } => Some(polygon_contains(vertices, point)),
}
}
}
fn polygon_bbox(vertices: &[GeoPoint]) -> Option<(GeoPoint, GeoPoint)> {
let first = vertices.first()?;
let mut min = *first;
let mut max = *first;
let mut reference = first.lon;
for vertex in vertices {
let lon = unwrap_lon(vertex.lon, reference);
reference = lon;
min.lat = min.lat.min(vertex.lat);
min.lon = min.lon.min(lon);
max.lat = max.lat.max(vertex.lat);
max.lon = max.lon.max(lon);
}
Some((min, max))
}
fn polygon_contains(vertices: &[GeoPoint], point: GeoPoint) -> bool {
if vertices.len() < 3 {
return false;
}
let mut inside = false;
let mut j = vertices.len() - 1;
for i in 0..vertices.len() {
let (vi, vj) = (vertices[i], vertices[j]);
let lon_i = unwrap_lon(vi.lon, point.lon);
let lon_j = unwrap_lon(vj.lon, point.lon);
if (vi.lat > point.lat) != (vj.lat > point.lat) {
let denominator = vj.lat - vi.lat;
if denominator.abs() > f64::EPSILON {
let crossing = lon_i + (point.lat - vi.lat) / denominator * (lon_j - lon_i);
if point.lon < crossing {
inside = !inside;
}
}
}
j = i;
}
inside
}
fn unwrap_lon(lon: f64, reference: f64) -> f64 {
if !lon.is_finite() || !reference.is_finite() {
return lon;
}
let mut unwrapped = lon;
while unwrapped - reference > 180.0 {
unwrapped -= 360.0;
}
while unwrapped - reference < -180.0 {
unwrapped += 360.0;
}
unwrapped
}
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)]
#[non_exhaustive]
pub struct AreaBriefingRequest {
pub area: Area,
pub products: Vec<ProductKind>,
pub lookback_hours: Option<u32>,
pub departure_at: Option<DateTime<Utc>>,
}
impl AreaBriefingRequest {
pub fn new(area: Area) -> Self {
Self {
area,
products: Vec::new(),
lookback_hours: None,
departure_at: None,
}
}
#[must_use]
pub fn with_products(mut self, products: Vec<ProductKind>) -> Self {
self.products = products;
self
}
#[must_use]
pub fn with_lookback_hours(mut self, hours: Option<u32>) -> Self {
self.lookback_hours = hours;
self
}
#[must_use]
pub fn with_departure_at(mut self, at: Option<DateTime<Utc>>) -> Self {
self.departure_at = at;
self
}
}
#[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,
pub observation: Option<MetarObservation>,
}
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(),
observation: None,
}
}
#[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
}
#[must_use]
pub fn with_observation(mut self, observation: Option<MetarObservation>) -> Self {
self.observation = observation;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Briefing {
pub source: String,
pub generated_at: Option<DateTime<Utc>>,
pub departure_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,
departure_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_departure_at(mut self, at: Option<DateTime<Utc>>) -> Self {
self.departure_at = 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;