Files
aviation/api/src/metars/model.rs
2025-09-19 19:33:53 -04:00

1291 lines
44 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::airports::{Airport, UpdateAirport};
use crate::error::Error;
use crate::metars::MetarCheck;
use crate::metars::utils::parse_metar_time;
use crate::error::ApiResult;
use chrono::{DateTime, Utc};
use flate2::read::GzDecoder;
use reqwest::header::ETAG;
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres, QueryBuilder};
use std::collections::HashSet;
use std::env;
use std::fmt::Display;
use std::io::{Cursor, Read};
use std::str::FromStr;
use std::sync::OnceLock;
use regex::Regex;
use utoipa::ToSchema;
use crate::state::AppState;
static TIME_OFFSET: OnceLock<i64> = OnceLock::new();
const TABLE_NAME: &str = "metars";
const DEFAULT_REFRESH_DURATION: i64 = 3000;
fn time_offset() -> i64 {
*TIME_OFFSET.get_or_init(|| {
env::var("API_METAR_TIME_OFFSET")
.unwrap_or("1800".to_string())
.parse::<i64>()
.unwrap_or(1800)
})
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct Metar {
pub icao: String,
pub raw_text: String,
pub observation_time: DateTime<Utc>,
pub flight_category: FlightCategory,
#[serde(skip_serializing_if = "Option::is_none")]
pub report_modifier: Option<ReportModifier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub becoming_change: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_significant_change: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temporary_change: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temp_c: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dew_point_c: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub estimated_humidity: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wind_dir_degrees: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wind_speed_kt: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wind_gust_kt: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variable_wind_dir_degrees: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility_statute_mi: Option<String>,
pub runway_visual_range: Vec<RunwayVisualRange>,
#[serde(skip_serializing_if = "Option::is_none")]
pub altimeter_in_hg: Option<f64>, // inches of mercury units
#[serde(skip_serializing_if = "Option::is_none")]
pub sea_level_pressure_mb: Option<f64>,
pub remarks: Remarks,
pub weather_phenomena: Vec<String>,
pub sky_condition: Vec<SkyCondition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_temp_c: Option<f64>, // TODO
#[serde(skip_serializing_if = "Option::is_none")]
pub min_temp_c: Option<f64>, // TODO
#[serde(skip_serializing_if = "Option::is_none")]
pub density_altitude: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub enum ReportModifier {
#[serde(rename = "AUTO")]
Auto,
#[serde(rename = "COR")]
Corrected,
}
impl FromStr for ReportModifier {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"AUTO" => Ok(ReportModifier::Auto),
"COR" => Ok(ReportModifier::Corrected),
_ => Err(Error::new(400, format!("Invalid report modifier '{}'", s))),
}
}
}
impl Display for ReportModifier {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ReportModifier::Auto => write!(f, "AUTO"),
ReportModifier::Corrected => write!(f, "COR"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct RunwayVisualRange {
pub runway: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility_ft: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variable_visibility_low_ft: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variable_visibility_high_ft: Option<String>,
}
impl Default for RunwayVisualRange {
fn default() -> Self {
RunwayVisualRange {
runway: "".to_string(),
visibility_ft: None,
variable_visibility_low_ft: None,
variable_visibility_high_ft: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub enum AutomatedStationType {
#[serde(rename = "AO1")]
WithoutPrecipitationDiscriminator,
#[serde(rename = "AO2")]
WithPrecipitationDiscriminator,
}
impl FromStr for AutomatedStationType {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"AO1" => Ok(AutomatedStationType::WithoutPrecipitationDiscriminator),
"AO2" => Ok(AutomatedStationType::WithPrecipitationDiscriminator),
_ => Err(Error::new(
400,
format!("Invalid automated station type '{}'", s),
)),
}
}
}
impl Display for AutomatedStationType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AutomatedStationType::WithoutPrecipitationDiscriminator => write!(f, "AO1"),
AutomatedStationType::WithPrecipitationDiscriminator => write!(f, "AO2"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct Remarks {
#[serde(skip_serializing_if = "Option::is_none")]
pub peak_wind: Option<PeakWind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_station_type: Option<AutomatedStationType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maintenance_indicator: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rvr_missing: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub precipitation_identifier_information_not_available: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub precipitation_information_not_available: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub freezing_rain_information_not_available: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thunderstorm_information_not_available: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility_at_secondary_location_not_available: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sky_condition_at_secondary_location_not_available: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PeakWind {
pub degrees: i32,
pub speed: i32,
pub hour: Option<i32>,
pub minutes: i32,
}
impl Default for Remarks {
fn default() -> Self {
Remarks {
peak_wind: None,
auto_station_type: None,
maintenance_indicator: None,
rvr_missing: None,
precipitation_identifier_information_not_available: None,
precipitation_information_not_available: None,
freezing_rain_information_not_available: None,
thunderstorm_information_not_available: None,
visibility_at_secondary_location_not_available: None,
sky_condition_at_secondary_location_not_available: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct SkyCondition {
pub sky_cover: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cloud_base_ft_agl: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub significant_convective_clouds: Option<String>,
}
impl Default for SkyCondition {
fn default() -> Self {
SkyCondition {
sky_cover: "".to_string(),
cloud_base_ft_agl: None,
significant_convective_clouds: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub enum FlightCategory {
VFR,
MVFR,
LIFR,
IFR,
UNKN,
}
impl Default for Metar {
fn default() -> Self {
Self {
raw_text: "".to_string(),
icao: "".to_string(),
observation_time: chrono::DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc),
flight_category: FlightCategory::UNKN,
report_modifier: None,
no_significant_change: None,
temporary_change: None,
becoming_change: None,
temp_c: None,
dew_point_c: None,
wind_dir_degrees: None,
wind_speed_kt: None,
wind_gust_kt: None,
variable_wind_dir_degrees: None,
visibility_statute_mi: None,
runway_visual_range: vec![],
altimeter_in_hg: None,
sea_level_pressure_mb: None,
remarks: Remarks::default(),
weather_phenomena: vec![],
sky_condition: vec![],
max_temp_c: None,
min_temp_c: None,
estimated_humidity: None,
density_altitude: None,
}
}
}
#[derive(Serialize, Deserialize, sqlx::FromRow, Debug)]
struct MetarRow {
icao: String,
observation_time: DateTime<Utc>,
raw_text: String,
data: serde_json::Value,
}
impl MetarRow {
async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<()> {
sqlx::query(&format!(
r#"
INSERT INTO {} (
icao,
observation_time,
raw_text,
data
)
VALUES ($1, $2, $3, $4)
ON CONFLICT (icao, observation_time) DO UPDATE SET
raw_text = EXCLUDED.raw_text,
data = EXCLUDED.data
"#,
TABLE_NAME,
))
.bind(self.icao.clone())
.bind(self.observation_time.clone())
.bind(self.raw_text.clone())
.bind(self.data.clone())
.execute(pool)
.await?;
Ok(())
}
async fn insert_all(pool: &Pool<Postgres>, metars: Vec<Metar>) -> ApiResult<()> {
let chunk_size = 1000;
for chunk in metars.chunks(chunk_size) {
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(format!(
"INSERT INTO {} (icao, observation_time, raw_text, data) ",
TABLE_NAME
));
query_builder.push_values(chunk, |mut b, metar| {
let row: Self = match metar.to_row() {
Ok(row) => row,
Err(e) => {
log::warn!("Failed to serialize METAR data: {}", e);
return;
}
};
b.push_bind(row.icao)
.push_bind(row.observation_time)
.push_bind(row.raw_text)
.push_bind(row.data);
});
query_builder.push(
" ON CONFLICT (icao, observation_time) DO UPDATE SET \
raw_text = EXCLUDED.raw_text, \
data = EXCLUDED.data",
);
let query = query_builder.build();
query.execute(pool).await?;
}
Ok(())
}
}
impl Metar {
fn parse_multiple(pool: &Pool<Postgres>, metar_strings: &Vec<&str>) -> ApiResult<Vec<Self>> {
let mut metars: Vec<Self> = vec![];
for metar_string in metar_strings {
match Self::parse(pool, metar_string) {
Ok(metar) => metars.push(metar),
Err(e) => {
log::warn!("Failed to parse metar string: {}", e);
continue;
}
};
}
Ok(metars)
}
fn parse(pool: &Pool<Postgres>, metar_string: &str) -> ApiResult<Self> {
if metar_string.is_empty() {
return Err(Error::new(
404,
"Unable to parse empty METAR data".to_string(),
));
}
log::trace!("Parsing METAR data: {}", metar_string);
let mut metar: Self = Self::default();
metar.raw_text = metar_string.to_owned();
let mut metar_parts: Vec<&str> = metar_string
.trim()
.trim_matches(|c| c == '"' || c == '\'' || c == '“' || c == '”' || c == '' || c == '')
.trim()
.split_whitespace().collect();
if metar_parts.len() < 4 {
return Err(Error::new(
500,
format!(
"Unable to parse METAR data in an unexpected format: {}",
metar_string
),
));
}
// Remove METAR at the start of the text
let metar_re: Regex = Regex::new(r"(?i)^[\p{P}\s]*METAR[\p{P}\s]*$")?;
let speci_re: Regex = Regex::new(r"(?i)^[\p{P}\s]*SPECI[\p{P}\s]*$")?;
let token = metar_parts[0].trim();
if metar_re.is_match(token) {
metar_parts.remove(0);
} else if speci_re.is_match(token) {
return Err(Error::new(500, format!("Unable to parse SPECI data: {}", metar_string)));
}
// Station Identifier
metar.icao = metar_parts[0].to_string();
metar_parts.remove(0);
// Date/Time
let observation_time = metar_parts[0];
metar_parts.remove(0);
match parse_metar_time(observation_time) {
Ok(observation_time) => {
metar.observation_time = match chrono::DateTime::parse_from_rfc3339(&observation_time) {
Ok(datetime) => datetime.with_timezone(&Utc),
Err(err) => return Err(err.into()),
};
}
Err(err) => {
return Err(Error::new(
err.status,
format!(
"Unexpected observation time field '{}': {}; {}",
observation_time, metar_string, err
),
));
}
};
loop {
if metar_parts.is_empty() {
break;
}
// Report Modifiers
if !metar_parts.is_empty() && (metar_parts[0] == "AUTO" || metar_parts[0] == "COR") {
metar.report_modifier = Some(ReportModifier::from_str(metar_parts[0])?);
metar_parts.remove(0);
}
if !metar_parts.is_empty() && metar_parts[0] == "NOSIG" {
metar.no_significant_change = Some(true);
metar_parts.remove(0);
}
// Wind Direction and Speed
let wind_re = regex::Regex::new(r"^(?:[0-9]{3}|VRB)[0-9]{2}(?:KT|MPS)$")?;
let wind_gust_re = regex::Regex::new(r"^(?:[0-9]{3}|VRB)[0-9]{2}G[0-9]{2}(?:KT|MPS)$")?;
// Handle input error where there is a space between the numbers and units
let mut value: Option<String> = None;
if metar_parts.len() >= 2
&& metar_parts[0].len() == 5
&& (metar_parts[1] == "KT" || metar_parts[1] == "MPS")
{
value = Some(format!("{}{}", metar_parts[0], metar_parts[1]));
metar_parts.remove(0);
metar_parts.remove(0);
} else if metar_parts.len() >= 2
&& metar_parts[0].len() == 7
&& metar_parts[0].contains("G")
&& (metar_parts[1] == "KT" || metar_parts[1] == "MPS")
{
value = Some(format!("{}{}", metar_parts[0], metar_parts[1]));
metar_parts.remove(0);
metar_parts.remove(0);
} else if !metar_parts.is_empty() && wind_re.is_match(metar_parts[0]) {
value = Some(metar_parts[0].to_string());
metar_parts.remove(0);
} else if !metar_parts.is_empty() && wind_gust_re.is_match(metar_parts[0]) {
value = Some(metar_parts[0].to_string());
metar_parts.remove(0);
}
match value {
Some(wind) => {
if wind_re.is_match(&wind) {
let wind_dir_degrees = &wind[0..3];
metar.wind_dir_degrees = Some(wind_dir_degrees.to_string());
let mut wind_speed_kt = wind[3..5].to_string();
// Convert m/s to kt
if wind.len() == 8 {
wind_speed_kt = (wind_speed_kt.parse::<f64>()? * 1.94384).to_string();
}
metar.wind_speed_kt = Some(wind_speed_kt.parse::<f64>()?);
} else if wind_gust_re.is_match(&wind) {
let wind_dir_degrees = &wind[0..3];
metar.wind_dir_degrees = Some(wind_dir_degrees.to_string());
let mut wind_speed_kt = wind[3..5].to_string();
let mut wind_gust_kt = wind[6..8].to_string();
// Convert m/s to kt
if wind.len() == 9 {
wind_speed_kt = (wind_speed_kt.parse::<f64>()? * 1.94384).to_string();
wind_gust_kt = (wind_gust_kt.parse::<f64>()? * 1.94384).to_string();
}
metar.wind_speed_kt = Some(wind_speed_kt.parse::<f64>()?);
metar.wind_gust_kt = Some(wind_gust_kt.parse::<f64>()?);
}
}
None => {}
}
// Variable Wind Direction
let variable_wind_re = regex::Regex::new(r"^[0-9]{3}V[0-9]{3}$")?;
if !metar_parts.is_empty() && variable_wind_re.is_match(metar_parts[0]) {
metar.variable_wind_dir_degrees = Some(metar_parts[0].to_string());
metar_parts.remove(0);
}
// Visibility
let visibility_re = regex::Regex::new(r"^M?(?:[0-9]+|[0-9]+/[0-9]+)SM$")?;
let visibility_re_m = regex::Regex::new(r"^[0-9]{4}(:?N|NE|NW|S|SE|SW)?$")?;
if !metar_parts.is_empty() && visibility_re.is_match(metar_parts[0]) {
let visibility_str = &metar_parts[0][0..metar_parts[0].len() - 2];
metar_parts.remove(0);
let visibility: String = if visibility_str.contains("/") {
let visibility_parts: Vec<&str> = visibility_str.split("/").collect();
let visibility_left = visibility_parts[0];
let visibility_right = visibility_parts[1].parse::<f64>()?;
if visibility_left.starts_with("M") {
format!(
"M{}",
visibility_left[1..visibility_left.len()].parse::<f64>()? / visibility_right
)
} else if visibility_left.starts_with("P") {
format!(
"P{}",
visibility_left[1..visibility_left.len()].parse::<f64>()? / visibility_right
)
} else {
format!("{}", visibility_left.parse::<f64>()? / visibility_right)
}
} else {
visibility_str.to_string()
};
metar.visibility_statute_mi = Some(visibility);
} else if !metar_parts.is_empty()
&& metar_parts[0].parse::<f64>().is_ok()
&& metar_parts.len() > 1
&& visibility_re.is_match(metar_parts[1])
{
let visibility_whole = metar_parts[0].parse::<f64>()?;
metar_parts.remove(0);
let visibility_parts: Vec<&str> = metar_parts[0].split("/").collect();
metar_parts.remove(0);
if visibility_parts.len() == 1 {
metar.visibility_statute_mi = Some(visibility_parts[0].to_string());
} else if visibility_parts.len() == 2 {
let visibility_left = visibility_parts[0];
// Parse the right-hand of visibility, with or without an SM suffix
let visibility_right_string = match visibility_parts[1].strip_suffix("SM") {
Some(s) => s,
None => {
if visibility_parts[1]
.chars()
.all(|c| c.is_numeric() || c == '.')
{
visibility_parts[1]
} else {
log::warn!(
"Skipping unexpected visibility field '{:?}' ({})",
visibility_parts,
metar_string
);
continue;
}
}
};
let visibility_right = visibility_right_string.parse::<f64>()?;
let visibility = if visibility_left.starts_with("M") {
format!(
"M{}",
visibility_whole
+ (visibility_left[1..visibility_left.len()].parse::<f64>()? / visibility_right)
)
} else if visibility_left.starts_with("P") {
format!(
"P{}",
visibility_whole
+ (visibility_left[1..visibility_left.len()].parse::<f64>()? / visibility_right)
)
} else {
format!(
"{}",
visibility_whole + (visibility_left.parse::<f64>()? / visibility_right)
)
};
metar.visibility_statute_mi = Some(visibility);
} else if !metar_parts.is_empty() && visibility_re_m.is_match(metar_parts[0]) {
// Convert meters to statute miles
let visibility = metar_parts[0];
metar_parts.remove(0);
if &visibility[0..4] == "9999" {
metar.visibility_statute_mi = Some("P10".to_string());
} else {
let visibility = visibility[0..4].parse::<f64>()? * 0.000621371;
metar.visibility_statute_mi = Some(format!("{:.2}", visibility));
}
} else {
log::warn!(
"Skipping unexpected visibility field '{}' ({})",
metar_parts[0],
metar_string
);
}
}
// Runway Visual Range
let rvr_re = regex::Regex::new(r"^R[0-9]{1,3}(?:L|R|C)?/[PM]?[0-9]{4}FT$")?;
let variable_rvr_re =
regex::Regex::new(r"^R[0-9]{1,3}(?:L|R|C)?/[PM]?[0-9]{4}V[PM]?[0-9]{4}FT$")?;
while !metar_parts.is_empty()
&& (rvr_re.is_match(metar_parts[0]) || variable_rvr_re.is_match(metar_parts[0]))
{
let rvr_string = metar_parts[0];
metar_parts.remove(0);
let mut rvr = RunwayVisualRange::default();
let rvr_parts: Vec<&str> = rvr_string.split("/").collect();
rvr.runway = rvr_parts[0].to_string();
if rvr_re.is_match(rvr_string) {
rvr.visibility_ft = Some(rvr_parts[1].to_string());
} else {
let rvr_variable_parts: Vec<&str> = rvr_parts[1].split("V").collect();
if rvr_variable_parts.len() != 2 {
log::warn!(
"Unable to parse runway visual range in {}: {}",
rvr_string,
metar_string
);
} else {
rvr.variable_visibility_low_ft = Some(rvr_variable_parts[0].to_string());
rvr.variable_visibility_high_ft = Some(rvr_variable_parts[1].to_string());
}
}
}
// Weather Phenomena
let wx_intensity = "(?:[+-]|VC)?";
let wx_descriptor = "(?:MI|PR|BC|DR|BL|SH|TS|FZ)?";
let wx_precipitation =
"(?:DZ|RA|SN|SG|IC|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS)?";
let wx_re = regex::Regex::new(&format!(
r"^{}{}{}$",
wx_intensity, wx_descriptor, wx_precipitation
))
.unwrap();
while !metar_parts.is_empty() && wx_re.is_match(metar_parts[0]) {
metar.weather_phenomena.push(metar_parts[0].to_string());
metar_parts.remove(0);
}
metar.parse_sky_condition(&mut metar_parts);
// Temperature and Dewpoint
let temp_re = regex::Regex::new(r"^(?:M?[0-9]{2})?/(?:M?[0-9]{2})?$")?;
if !metar_parts.is_empty() && temp_re.is_match(metar_parts[0]) {
let temp_string = metar_parts[0];
metar_parts.remove(0);
let temp_parts: Vec<&str> = temp_string.split("/").collect();
let mut temp_c = "";
let mut dewpoint_c = "";
if temp_parts.len() != 2 {
if temp_string.ends_with("/") {
temp_c = temp_parts[0];
} else {
dewpoint_c = temp_parts[0];
}
} else {
temp_c = temp_parts[0];
dewpoint_c = temp_parts[1];
}
if temp_c.starts_with("M") {
metar.temp_c = Some(temp_c[1..temp_c.len()].parse::<f64>()? * -1.0);
} else if !temp_c.is_empty() {
metar.temp_c = match temp_c.parse::<f64>() {
Ok(t) => Some(t),
Err(err) => {
log::warn!("Unable to parse temperature in {}: {}", temp_c, err);
None
}
};
}
if dewpoint_c.starts_with("M") {
metar.dew_point_c = Some(dewpoint_c[1..dewpoint_c.len()].parse::<f64>()? * -1.0);
} else if !dewpoint_c.is_empty() {
metar.dew_point_c = match dewpoint_c.parse::<f64>() {
Ok(d) => Some(d),
Err(err) => {
log::warn!("Unable to parse dewpoint in {}: {}", dewpoint_c, err);
None
}
};
}
}
// Altimeter
let altim_re = regex::Regex::new(r"^A[0-9]{4}$")?;
if !metar_parts.is_empty() && altim_re.is_match(metar_parts[0]) {
let altim = metar_parts[0];
metar_parts.remove(0);
metar.altimeter_in_hg = Some(altim[1..altim.len()].parse::<f64>()? / 100.0);
}
// Pressure
let pressure_re = regex::Regex::new(r"^Q[0-9]{4}$")?;
if !metar_parts.is_empty() && pressure_re.is_match(metar_parts[0]) {
let pressure = metar_parts[0];
metar_parts.remove(0);
metar.sea_level_pressure_mb = Some(pressure[1..pressure.len()].parse::<f64>()?);
}
// Trend forecast - becoming change
if !metar_parts.is_empty() && metar_parts[0] == "BECMG" {
metar.becoming_change = Some(true);
metar_parts.remove(0);
}
// Trend forecast - temporary change
if !metar_parts.is_empty() && metar_parts[0] == "TEMPO" {
metar.temporary_change = Some(true);
metar_parts.remove(0);
}
// Trend forecast - No significant change
if !metar_parts.is_empty() && metar_parts[0] == "NOSIG" {
metar.no_significant_change = Some(true);
metar_parts.remove(0);
}
// Remarks
if !metar_parts.is_empty() && metar_parts[0] == "RMK" {
metar_parts.remove(0);
loop {
if metar_parts.is_empty() {
break;
}
let slp_re = regex::Regex::new(r"^SLP([0-9]{3})$")?;
let hourly_temp_re = regex::Regex::new(r"^T[01][0-9]{3}[01][0-9]{3}$")?;
let remark = metar_parts[0];
metar_parts.remove(0);
if remark == "AO1" || remark == "AO2" {
metar.remarks.auto_station_type = Some(AutomatedStationType::from_str(remark)?);
} else if remark == "$" {
metar.remarks.maintenance_indicator = Some(true);
} else if remark == "PK" && metar_parts.len() >= 2 && metar_parts[0] == "WND" {
metar_parts.remove(0);
let string = metar_parts[0];
metar_parts.remove(0);
let re = regex::Regex::new(
r"(?<degrees>\d{3})(?<speed>\d{2,3})/(?:(?<hour>\d{2}))?(?<minutes>\d{2})",
)
.unwrap();
if let Some(caps) = re.captures(string) {
// Get degrees, speed, minutes
let degrees: i32 = caps["degrees"].parse()?;
let speed: i32 = caps["speed"].parse()?;
let minutes: i32 = caps["minutes"].parse()?;
// Get optional hours
let hour = if let Some(hour_match) = caps.name("hour") {
Some(hour_match.as_str().parse()?)
} else {
None
};
metar.remarks.peak_wind = Some(PeakWind {
degrees,
speed,
hour,
minutes,
});
} else {
return Err(Error::new(
500,
"Input string format is invalid".to_string(),
));
}
} else if remark == "PNO" {
metar.remarks.precipitation_information_not_available = Some(true);
} else if remark == "RVRNO" {
metar.remarks.rvr_missing = Some(true);
} else if remark == "PWINO" {
metar
.remarks
.precipitation_identifier_information_not_available = Some(true);
} else if remark == "FZRANO" {
metar.remarks.freezing_rain_information_not_available = Some(true);
} else if remark == "TSNO" {
metar.remarks.thunderstorm_information_not_available = Some(true);
} else if remark == "VISNO" {
let location = metar_parts[0];
metar_parts.remove(0);
metar.remarks.visibility_at_secondary_location_not_available =
Some(location.to_string());
} else if remark == "CHINO" {
let location = metar_parts[0];
metar_parts.remove(0);
metar
.remarks
.sky_condition_at_secondary_location_not_available = Some(location.to_string());
} else if slp_re.is_match(remark) {
let slp = slp_re.captures(remark).unwrap();
let sea_level_pressure = slp[1].parse::<f64>()?;
if sea_level_pressure > 500.0 {
metar.sea_level_pressure_mb = Some((sea_level_pressure / 10.0) + 900.0);
} else {
metar.sea_level_pressure_mb = Some((sea_level_pressure / 10.0) + 1000.0);
}
} else if hourly_temp_re.is_match(remark) {
let temp_negation = &remark[1..2];
let temp = &remark[2..5];
if let Ok(t) = temp.parse::<f64>() {
if temp_negation == "0" {
metar.temp_c = Some(t / 10.0);
} else {
metar.temp_c = Some(t / 10.0 * -1.0);
}
}
let dewpoint_negation = &remark[5..6];
let dewpoint = &remark[6..9];
if let Ok(d) = dewpoint.parse::<f64>() {
if dewpoint_negation == "0" {
metar.dew_point_c = Some(d / 10.0);
} else {
metar.dew_point_c = Some(d / 10.0 * -1.0);
}
}
}
}
}
// Skip unexpected fields
if !metar_parts.is_empty() {
log::trace!(
"Skipping unexpected field: '{}' ({})",
metar_parts[0],
metar_string
);
metar_parts.remove(0);
}
}
// Flight Category
if metar.visibility_statute_mi.is_none() && metar.sky_condition.is_empty() {
metar.flight_category = FlightCategory::UNKN;
} else {
let visibility = match &metar.visibility_statute_mi {
Some(v) => {
if v.starts_with("M") || v.starts_with("P") {
v[1..v.len()].parse::<f64>()?
} else {
v.parse::<f64>()?
}
}
None => 5.0, // Assume VFR if no visibility is present
};
// Ceiling is the lowest cloud base that is BKN or OVC
let ceiling = match metar.sky_condition.first() {
Some(s) => {
if s.sky_cover == "VV" {
0.0
} else if s.sky_cover == "BKN" || s.sky_cover == "OVC" {
match s.cloud_base_ft_agl {
Some(c) => c as f64,
None => 0.0,
}
} else {
3000.0 // Assume VFR if no BKN or OVC sky condition is present
}
}
None => 3000.0, // Assume VFR if no sky condition is present
};
if visibility >= 5.0 && ceiling >= 3000.0 {
metar.flight_category = FlightCategory::VFR;
} else if visibility >= 3.0 && ceiling >= 1000.0 {
metar.flight_category = FlightCategory::MVFR;
} else if visibility >= 1.0 && ceiling >= 500.0 {
metar.flight_category = FlightCategory::IFR;
} else {
metar.flight_category = FlightCategory::LIFR;
}
}
// Calculate estimated humidity using the magnus formula
if metar.temp_c.is_some() && metar.dew_point_c.is_some() {
let temp = metar.temp_c.unwrap();
let dew_point = metar.dew_point_c.unwrap();
let a: f64 = 17.625;
let b: f64 = 243.04;
let exponent_temp = a * temp / (b + temp);
let exponent_dew = a * dew_point / (b + dew_point);
let mut estimated_humidity = 100.0 * (exponent_dew.exp() / exponent_temp.exp());
// Round to 3 decimal places
estimated_humidity = (estimated_humidity * 1000.0).round() / 1000.0;
metar.estimated_humidity = Some(estimated_humidity);
}
// Calculate estimated density
// let estimated_density = ;
// metar.density_altitude = Some(metar.density_altitude);
// Update the airport's metar observation time
let icao = metar.icao.clone();
let observation_time = metar.observation_time.clone();
let pool = pool.clone();
tokio::spawn(async move {
match Airport::update(
&pool.clone(),
&icao,
&UpdateAirport {
icao: None,
iata: None,
local: None,
name: None,
category: None,
iso_country: None,
iso_region: None,
municipality: None,
elevation_ft: None,
longitude: None,
latitude: None,
has_tower: None,
has_beacon: None,
runways: None,
communications: None,
public: None,
latest_metar_observation: Some(observation_time),
},
)
.await
{
Ok(_) => {}
Err(err) => log::error!(
"Unable to update airport {} with the latest observation time: {}",
icao,
err
),
};
});
Ok(metar)
}
fn parse_sky_condition(&mut self, metar_parts: &mut Vec<&str>) {
// Check if sky condition is CAVOK
if !metar_parts.is_empty() && metar_parts[0] == "CAVOK" {
self.sky_condition.push(SkyCondition {
sky_cover: "CLR".to_string(),
cloud_base_ft_agl: None,
significant_convective_clouds: None,
});
metar_parts.remove(0);
}
let sky_condition_re = regex::Regex::new(
r"^(?:CLR|SKC|NSC|NCD|(?:FEW|SCT|BKN|OVC|VV)([0-9/]{3})?(?:CB|TCU)?)(?:///)?$",
)
.unwrap();
while !metar_parts.is_empty() && sky_condition_re.is_match(metar_parts[0]) {
// Get the next METAR part
let mut sky_condition_string = metar_parts[0];
metar_parts.remove(0);
// Remove trailing slashes
if sky_condition_string.ends_with("///") {
sky_condition_string = &sky_condition_string[..sky_condition_string.len() - 3];
}
let mut sky_condition = SkyCondition::default();
// Handle sky cover and optionally vertical visibility
let mut vv_offset = 0;
if &sky_condition_string[0..2] == "VV" {
sky_condition.sky_cover = "VV".to_string();
vv_offset = 1;
} else {
sky_condition.sky_cover = sky_condition_string[0..3].to_string();
}
if sky_condition_string.len() > 3 - vv_offset {
if sky_condition_string.len() < 6 - vv_offset {
// Parse out the significant convective clouds
let scc = &sky_condition_string[3 - vv_offset..];
sky_condition.significant_convective_clouds = Some(scc.to_string());
} else {
// Parse out the next three digits
let cloud_base_ft_agl = &sky_condition_string[3 - vv_offset..6 - vv_offset];
sky_condition.cloud_base_ft_agl = match cloud_base_ft_agl.parse::<i32>() {
Ok(c) => Some(c * 100),
Err(err) => {
log::warn!(
"Unable to parse cloud base in {}: {}",
sky_condition_string,
err
);
None
}
};
// Parse out the significant convective clouds
let scc = &sky_condition_string[6 - vv_offset..];
sky_condition.significant_convective_clouds = Some(scc.to_string());
}
}
self.sky_condition.push(sky_condition);
}
}
pub async fn get_cached_remote_metars(
state: &AppState,
etag: Option<String>,
) -> ApiResult<(Vec<Self>, String)> {
let base_url = env::var("AVIATION_WEATHER_URL").expect("AVIATION_WEATHER_URL must be set");
let url = format!("{}/data/cache/metars.cache.csv.gz", base_url);
match state.client.get(&url, etag.clone()).await {
Ok(r) => {
let new_etag = r
.headers()
.get(ETAG)
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
let bytes = r.bytes().await?;
let mut gz = GzDecoder::new(Cursor::new(bytes));
let mut text = String::new();
gz.read_to_string(&mut text)?;
let mut output: Vec<Metar> = Vec::new();
for line in text.lines() {
// Split off the first column
let raw_text = line.splitn(2, ',').next().unwrap();
match Metar::parse(&state.pool, raw_text) {
Ok(m) => output.push(m),
Err(err) => {
log::warn!("{}", err);
}
};
}
match new_etag {
Some(etag) => Ok((output, etag)),
None => match etag {
Some(etag) => Ok((output, etag.to_string())),
None => Ok((output, String::new())),
},
}
}
Err(err) => Err(err.into()),
}
}
pub async fn get_remote_metars(state: &AppState, icaos: &Vec<String>) -> ApiResult<Vec<Self>> {
let base_url = env::var("AVIATION_WEATHER_URL").expect("AVIATION_WEATHER_URL must be set");
// Query the remote API for the missing METAR data 10 at a time
let icao_chunks = icaos
.chunks(10)
.map(|chunk| chunk.join(","))
.collect::<Vec<String>>();
let mut metars: Vec<Self> = vec![];
for icao_chunk in icao_chunks {
let url = format!(
"{}/api/data/metar?ids={}&hours=0&order=id,-obs",
base_url, icao_chunk
);
let mut m = match state.client.get(&url, None).await {
Ok(r) => match r.text().await {
Ok(r) => {
let metar_chunk = r
.trim()
.split("\n")
.filter(|m| !m.trim().is_empty())
.collect();
match Self::parse_multiple(&state.pool, &metar_chunk) {
Ok(m) => m,
Err(err) => return Err(err),
}
}
Err(err) => return Err(Error::new(500, format!("METAR parse failed: {}", err))),
},
Err(err) => return Err(err.into()),
};
metars.append(&mut m);
}
Ok(metars)
}
fn from_row(row: MetarRow) -> ApiResult<Self> {
let metar: Self = serde_json::from_value(row.data)?;
Ok(metar)
}
fn to_row(&self) -> ApiResult<MetarRow> {
let data = serde_json::to_value(self)?;
Ok(MetarRow {
icao: self.icao.to_uppercase(),
observation_time: self.observation_time,
raw_text: self.raw_text.clone(),
data,
})
}
pub async fn get_all_distinct(pool: &Pool<Postgres>, icao_list: &Vec<String>) -> ApiResult<Vec<Self>> {
if icao_list.is_empty() {
return Ok(Vec::new());
}
let metar_rows: Vec<MetarRow> = sqlx::query_as::<_, MetarRow>(&format!(
r#"
SELECT DISTINCT ON (icao) * FROM {}
WHERE icao = ANY($1)
ORDER BY icao, observation_time DESC
"#,
TABLE_NAME
))
.bind(icao_list)
.fetch_all(pool)
.await?;
let mut metars = vec![];
for metar_row in metar_rows {
metars.push(Self::from_row(metar_row)?)
}
Ok(metars)
}
pub async fn get_or_update_metars(
state: &AppState,
icaos: &Vec<String>,
) -> ApiResult<Vec<Self>> {
let metars = Self::get_all_distinct(&state.pool, &icaos).await?;
let current_time = Utc::now().timestamp();
let mut updated_metars: Vec<Self> = vec![];
let mut missing_metar_icaos: Vec<String> = vec![];
let mut found_metar_icaos: HashSet<String> = HashSet::new();
let mut requested_icaos: HashSet<String> = HashSet::from_iter(icaos.clone());
for metar in metars {
let icao = metar.icao.clone();
// Remove found icao from requested ICAOs
requested_icaos.remove(&icao);
// Handle outdated METARs
if current_time > (metar.observation_time.timestamp() + time_offset()) {
// If the METAR has previously been found, get the updated_at time, otherwise default
let refresh_seconds = match MetarCheck::get(state, &icao).await {
Some(c) => current_time - c.updated_at.timestamp(),
None => DEFAULT_REFRESH_DURATION,
};
// If the metar is outdated, add it to the refresh list
if refresh_seconds >= DEFAULT_REFRESH_DURATION {
log::trace!("{} METAR data is outdated, marked for refresh", &icao);
missing_metar_icaos.push(icao.clone());
}
// Otherwise return the outdated data (to be checked on the next cycle)
else {
log::trace!(
"{} METAR data is outdated; refreshing in {} seconds",
&icao,
DEFAULT_REFRESH_DURATION - refresh_seconds
);
updated_metars.push(metar);
}
}
// Otherwise add the valid metar to the updated list
else {
found_metar_icaos.insert(icao.clone());
let metar_check = MetarCheck::new(state, icao, true).await;
metar_check.insert(state).await?;
updated_metars.push(metar);
}
}
// Add all METARs that were not in the returned database METARs
for icao in &requested_icaos {
match MetarCheck::get(state, icao).await {
Some(c) => {
if current_time > (c.updated_at.timestamp() + DEFAULT_REFRESH_DURATION) {
missing_metar_icaos.push(icao.to_string());
}
}
None => {
missing_metar_icaos.push(icao.to_string());
}
}
}
// Retrieve missing METARs
if !missing_metar_icaos.is_empty() {
log::trace!(
"Retrieving missing METAR data for {:?}",
missing_metar_icaos
);
let mut remote_metars = Self::get_remote_metars(&state, &missing_metar_icaos)
.await
.unwrap_or_else(|err| {
log::warn!("Unable to get remote METAR data; {}", err);
vec![]
});
// Insert missing METARs
if remote_metars.len() > 0 {
for remote_metar in remote_metars.clone() {
remote_metar.insert(&state.pool).await?;
found_metar_icaos.insert(remote_metar.icao.to_string());
let mut metar_check = MetarCheck::new(state, remote_metar.icao.clone(), true).await;
metar_check.last_metar = Some(remote_metar);
metar_check.insert(state).await?;
}
updated_metars.append(&mut remote_metars);
}
// Update still missing METARs
for difference in found_metar_icaos.symmetric_difference(&requested_icaos) {
let metar_check = MetarCheck::new(state, difference.to_string(), false).await;
metar_check.insert(state).await?;
// Only add cached metar data if it's less than 4 hours old
if let Some(last_metar) = metar_check.last_metar {
let four_hours_ago = Utc::now() - chrono::Duration::hours(4);
if last_metar.observation_time < four_hours_ago {
updated_metars.push(last_metar);
}
}
}
}
Ok(updated_metars)
}
pub async fn update_metars(state: &AppState, etag: Option<String>) -> ApiResult<String> {
let (remote_metars, etag) = Self::get_cached_remote_metars(state, etag)
.await
.unwrap_or_else(|err| {
log::warn!("Unable to get cached remote METAR data; {}", err);
(vec![], String::new())
});
MetarRow::insert_all(&state.pool, remote_metars).await?;
Ok(etag)
}
pub async fn insert(&self, pool: &Pool<Postgres>) -> ApiResult<()> {
log::trace!(
"Inserting metar {} with observation time {}",
self.icao,
self.observation_time
);
let metar: MetarRow = self.to_row()?;
metar.insert(pool).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_metar_parse() {
let state = AppState::new().await.unwrap();
let mut metar_string = "METAR KABC 121755Z AUTO 21016G24KT 180V240 1SM R11/P6000FT \
-RA BR BKN015 OVC025 06/04 A2990 RMK AO2 PK WND 20032/25 WSHFT 1715 VIS 3/4V1 1/2 VIS 3/4 \
RWY11 RAB07 CIG 013V017 CIG 017 RWY11 PRESFR SLP125 P0003 60009 T00640036 10066 21012 58033 \
TSNO $"
.to_string();
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time);
metar_string = "KMIA 090053Z 33004KT 10SM FEW015 FEW024 SCT075 SCT250 25/22 A2990 RMK AO2 \
SLP126 T02500217 $"
.to_string();
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time);
metar_string =
"KMRB 082253Z 30014G23KT 10SM CLR 05/M12 A3002 RMK AO2 PK WND 30028/2157 SLP168 T00501117"
.to_string();
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time);
metar_string = "KHEF 092356Z 13009KT 10SM CLR 08/M03 A3022 RMK AO2 SLP239 6//// T00831033 \
10133 20078 53002 PNO $"
.to_string();
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time);
metar_string = "KSLK 162351Z AUTO VRB03KT 1SM -SN BR FEW007 OVC014 00/M02 A2974 RMK AO2 \
SLP090 P0001 60004 T00001017 10000 21011 53026"
.to_string();
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time);
metar_string = "KABC 121755Z AUTO 21016G24KT 180V240 1SM R11/P6000FT -RA BR BKN015 OVC025 \
SCTCB FEW123TCU 06/04 A2990 RMK AO2 PK WND 20032/25 WSHFT 1715 VIS 3/4V1 1/2 VIS 3/4 RWY11 \
RAB07 CIG 013V017 CIG 017 RWY11 PRESFR SLP125 P0003 60009 T00640036 10066 21012 58033 TSNO $"
.to_string();
let metar = Metar::parse(&state.pool, &metar_string).unwrap();
dbg!(&metar.observation_time);
dbg!(&metar.sky_condition);
}
}