Parsing, querying, inserting airports

This commit is contained in:
2025-04-09 20:38:01 -04:00
parent 240ed741f9
commit 4aa3190783
12 changed files with 453 additions and 104221 deletions

5
api/Cargo.lock generated
View File

@@ -377,6 +377,7 @@ dependencies = [
"chrono", "chrono",
"dotenv", "dotenv",
"env_logger", "env_logger",
"futures",
"futures-util", "futures-util",
"geo-types", "geo-types",
"log", "log",
@@ -3403,9 +3404,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.15.1" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
"serde", "serde",

View File

@@ -22,7 +22,7 @@ reqwest = "0.12.15"
serde = {version = "1.0.219", features = ["derive"]} serde = {version = "1.0.219", features = ["derive"]}
serde_json = "1.0.140" serde_json = "1.0.140"
tokio = { version = "1.44.2", features = ["macros", "rt", "time"] } tokio = { version = "1.44.2", features = ["macros", "rt", "time"] }
uuid = { version = "1.10.0", features = ["serde", "v4"] } uuid = { version = "1.16.0", features = ["serde", "v4"] }
log = "0.4.27" log = "0.4.27"
argon2 = "0.5.3" argon2 = "0.5.3"
redis = { version = "0.29.5", features = ["tokio-comp", "connection-manager", "r2d2", "json"] } redis = { version = "0.29.5", features = ["tokio-comp", "connection-manager", "r2d2", "json"] }
@@ -33,3 +33,4 @@ rand = "0.9.0"
rand_chacha = "0.9.0" rand_chacha = "0.9.0"
geo-types = "0.7.15" geo-types = "0.7.15"
byteorder = "1.5.0" byteorder = "1.5.0"
futures = "0.3.31"

View File

@@ -17,6 +17,22 @@ CREATE TABLE IF NOT EXISTS airports (
public BOOLEAN DEFAULT false public BOOLEAN DEFAULT false
); );
CREATE TABLE IF NOT EXISTS runways (
id UUID PRIMARY KEY NOT NULL,
icao TEXT NOT NULL,
runway_id TEXT NOT NULL,
length_ft REAL NOT NULL,
width_ft REAL NOT NULL,
surface TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS frequencies (
id UUID PRIMARY KEY NOT NULL,
icao TEXT NOT NULL,
frequency_id TEXT NOT NULL,
frequency_mhz REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS metars ( CREATE TABLE IF NOT EXISTS metars (
icao TEXT NOT NULL, icao TEXT NOT NULL,
observation_time TIMESTAMPTZ NOT NULL, observation_time TIMESTAMPTZ NOT NULL,

View File

@@ -1,11 +1,13 @@
use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
use actix_web::web::Json; use actix_web::web::Json;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Execute, Postgres, QueryBuilder}; use sqlx::{Execute, Postgres, QueryBuilder};
use crate::airports::model::airport_category::AirportCategory; use crate::airports::model::airport_category::AirportCategory;
use crate::airports::{Frequency, Runway, UpdateFrequency, UpdateRunway}; use crate::airports::{Frequency, FrequencyRow, Runway, RunwayRow, UpdateFrequency, UpdateRunway};
use crate::db; use crate::db;
use crate::error::ApiResult; use crate::error::{ApiResult, Error};
use crate::metars::Metar;
const TABLE_NAME: &str = "airports"; const TABLE_NAME: &str = "airports";
@@ -31,6 +33,8 @@ pub struct Airport {
pub runways: Vec<Runway>, pub runways: Vec<Runway>,
pub frequencies: Vec<Frequency>, pub frequencies: Vec<Frequency>,
pub public: bool, pub public: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_metar: Option<Metar>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -45,6 +49,7 @@ pub struct AirportQuery {
pub iso_countries: Option<String>, pub iso_countries: Option<String>,
pub iso_regions: Option<String>, pub iso_regions: Option<String>,
pub municipalities: Option<String>, pub municipalities: Option<String>,
pub metars: Option<bool>,
} }
impl Default for AirportQuery { impl Default for AirportQuery {
@@ -60,6 +65,7 @@ impl Default for AirportQuery {
iso_countries: None, iso_countries: None,
iso_regions: None, iso_regions: None,
municipalities: None, municipalities: None,
metars: None,
} }
} }
} }
@@ -125,7 +131,7 @@ impl Into<AirportRow> for Airport {
impl From<AirportRow> for Airport { impl From<AirportRow> for Airport {
fn from(airport: AirportRow) -> Self { fn from(airport: AirportRow) -> Self {
Airport { Self {
icao: airport.icao.clone(), icao: airport.icao.clone(),
iata: airport.iata.clone(), iata: airport.iata.clone(),
local: airport.local.clone(), local: airport.local.clone(),
@@ -148,6 +154,7 @@ impl From<AirportRow> for Airport {
runways: vec![], runways: vec![],
frequencies: vec![], frequencies: vec![],
public: airport.public, public: airport.public,
latest_metar: None,
} }
} }
} }
@@ -156,24 +163,53 @@ impl Airport {
pub async fn select(icao: &str) -> Option<Self> { pub async fn select(icao: &str) -> Option<Self> {
let pool = db::pool(); let pool = db::pool();
let airport: Option<AirportRow> = sqlx::query_as(&format!( let airport_fut = async {
r#" sqlx::query_as(&format!("SELECT * FROM {} WHERE icao = $1", TABLE_NAME))
SELECT * FROM {} WHERE icao = $1
"#,
TABLE_NAME
))
.bind(icao) .bind(icao)
.fetch_optional(pool) .fetch_optional(pool)
.await .await
.unwrap_or_else(|err| { };
log::error!("Unable to find airport '{}'", icao);
None
});
match airport { let runways_fut = Runway::select_all(icao);
Some(a) => Some(a.into()), let frequencies_fut = Frequency::select_all(icao);
None => None,
let (airport_result, runways_result, frequencies_result) =
tokio::join!(airport_fut, runways_fut, frequencies_fut);
let airport_row: Option<AirportRow> = match airport_result {
Ok(opt) => opt,
Err(err) => {
log::error!("Unable to find airport '{}': {}", icao, err);
return None;
} }
};
let runways: Vec<Runway> = match runways_result {
Ok(r) => r,
Err(err) => {
log::error!("Error retrieving runways for airport '{}': {}", icao, err);
vec![]
}
};
let frequencies: Vec<Frequency> = match frequencies_result {
Ok(f) => f,
Err(err) => {
log::error!(
"Error retrieving frequencies for airport '{}': {}",
icao,
err
);
vec![]
}
};
airport_row.map(|row| {
let mut airport: Airport = row.into();
airport.runways = runways;
airport.frequencies = frequencies;
airport
})
} }
pub async fn select_all(query: &AirportQuery) -> ApiResult<Vec<Self>> { pub async fn select_all(query: &AirportQuery) -> ApiResult<Vec<Self>> {
@@ -183,31 +219,34 @@ impl Airport {
builder.push(TABLE_NAME); builder.push(TABLE_NAME);
let mut has_where = false; let mut has_where = false;
macro_rules! push_condition { Self::push_condition_array(&mut builder, &mut has_where, "icao", &query.icaos);
($field:expr, $value:expr) => { Self::push_condition_array(&mut builder, &mut has_where, "iata", &query.iatas);
if let Some(ref val) = $value { Self::push_condition_array(
if !has_where { &mut builder,
builder.push(" WHERE "); &mut has_where,
has_where = true; "iso_country",
} else { &query.iso_countries,
builder.push(" AND "); );
} Self::push_condition_array(
builder.push($field).push(" = ").push_bind(val); &mut builder,
} &mut has_where,
}; "iso_region",
} &query.iso_regions,
);
// push_condition!("icao", query.icaos); Self::push_condition_array(
// push_condition!("iata", query.iata); &mut builder,
// push_condition!("iso_country", query.iso_country); &mut has_where,
// push_condition!("iso_region", query.iso_region); "municipality",
// push_condition!("municipality", query.municipality); &query.municipalities,
);
Self::push_condition_array(&mut builder, &mut has_where, "local", &query.locals);
Self::push_condition_array(&mut builder, &mut has_where, "name", &query.names);
Self::push_condition_array(&mut builder, &mut has_where, "category", &query.categories);
// Apply pagination. // Apply pagination.
if let Some(limit) = query.limit { if let Some(limit) = query.limit {
builder.push(" LIMIT ").push_bind(limit as i64); builder.push(" LIMIT ").push_bind(limit as i64);
let offset = if let Some(page) = query.page { let offset = if let Some(page) = query.page {
// Calculate offset (page is 1-based).
(page.saturating_sub(1) * limit) as i64 (page.saturating_sub(1) * limit) as i64
} else { } else {
0 0
@@ -215,9 +254,22 @@ impl Airport {
builder.push(" OFFSET ").push_bind(offset); builder.push(" OFFSET ").push_bind(offset);
} }
let query = builder.build_query_as(); let airport_query = builder.build_query_as::<AirportRow>();
let airport_rows: Vec<AirportRow> = query.fetch_all(pool).await?; let airport_rows: Vec<AirportRow> = airport_query.fetch_all(pool).await?;
Ok(airport_rows.into_iter().map(From::from).collect()) let mut airports: Vec<Airport> = airport_rows.into_iter().map(From::from).collect();
// Bulk update airports with runways and frequencies
if !airports.is_empty() {
let icaos: Vec<String> = airports.iter().map(|a| a.icao.clone()).collect();
let mut runway_map = Runway::select_all_map(icaos.clone()).await?;
let mut frequency_map = Frequency::select_all_map(icaos).await?;
for airport in airports.iter_mut() {
airport.runways = runway_map.remove(&airport.icao).unwrap_or_default();
airport.frequencies = frequency_map.remove(&airport.icao).unwrap_or_default();
}
}
Ok(airports)
} }
pub async fn count(query: &AirportQuery) -> i64 { pub async fn count(query: &AirportQuery) -> i64 {
@@ -227,49 +279,48 @@ impl Airport {
builder.push(TABLE_NAME); builder.push(TABLE_NAME);
let mut has_where = false; let mut has_where = false;
macro_rules! push_condition_array { Self::push_condition_array(&mut builder, &mut has_where, "icao", &query.icaos);
($column:expr, $field:expr) => { Self::push_condition_array(&mut builder, &mut has_where, "iata", &query.iatas);
if let Some(ref value_str) = $field { Self::push_condition_array(
// split on commas, trim whitespace, and drop empties &mut builder,
let values: Vec<&str> = value_str &mut has_where,
.split(',') "iso_country",
.map(|s| s.trim()) &query.iso_countries,
.filter(|s| !s.is_empty()) );
.collect(); Self::push_condition_array(
if !values.is_empty() { &mut builder,
if !has_where { &mut has_where,
builder.push(" WHERE "); "iso_region",
has_where = true; &query.iso_regions,
} else { );
builder.push(" AND "); Self::push_condition_array(
} &mut builder,
dbg!(&values); &mut has_where,
builder.push($column); "municipality",
builder.push(" = ANY("); &query.municipalities,
builder.push_bind(values); );
builder.push(")"); Self::push_condition_array(&mut builder, &mut has_where, "local", &query.locals);
} Self::push_condition_array(&mut builder, &mut has_where, "name", &query.names);
} Self::push_condition_array(&mut builder, &mut has_where, "category", &query.categories);
};
}
push_condition_array!("icao", query.icaos);
push_condition_array!("iata", query.iatas);
push_condition_array!("iso_country", query.iso_countries);
push_condition_array!("iso_region", query.iso_regions);
push_condition_array!("municipality", query.municipalities);
push_condition_array!("local", query.locals);
push_condition_array!("name", query.names);
push_condition_array!("category", query.categories);
let sql_query = builder.build_query_scalar(); let sql_query = builder.build_query_scalar();
dbg!(&sql_query.sql());
sql_query.fetch_one(pool).await.unwrap_or_else(|_| 0) sql_query.fetch_one(pool).await.unwrap_or_else(|_| 0)
} }
pub async fn insert(&self) -> ApiResult<Self> { pub async fn insert(&self) -> ApiResult<Self> {
let pool = db::pool(); let pool = db::pool();
let mut all_runway_rows: Vec<RunwayRow> = Vec::new();
let mut all_frequency_rows: Vec<FrequencyRow> = Vec::new();
for runway in &self.runways {
all_runway_rows.push(Runway::into(runway, &self.icao));
}
for frequency in &self.frequencies {
all_frequency_rows.push(Frequency::into(frequency, &self.icao));
}
Runway::insert_all(&all_runway_rows).await?;
Frequency::insert_all(&all_frequency_rows).await?;
let airport: AirportRow = sqlx::query_as(&format!( let airport: AirportRow = sqlx::query_as(&format!(
r#" r#"
INSERT INTO {} ( INSERT INTO {} (
@@ -306,12 +357,25 @@ impl Airport {
pub async fn insert_all(airports: Vec<Self>) -> ApiResult<()> { pub async fn insert_all(airports: Vec<Self>) -> ApiResult<()> {
let pool = db::pool(); let pool = db::pool();
let airport_rows: Vec<AirportRow> = airports.into_iter().map(Into::into).collect();
// Define the maximum size of a single insertion batch.
let chunk_size = 1000; let chunk_size = 1000;
let mut all_runway_rows: Vec<RunwayRow> = Vec::new();
let mut all_frequency_rows: Vec<FrequencyRow> = Vec::new();
let airport_rows: Vec<AirportRow> = airports
.into_iter()
.map(|airport| {
for runway in &airport.runways {
all_runway_rows.push(Runway::into(runway, &airport.icao));
}
for frequency in &airport.frequencies {
all_frequency_rows.push(Frequency::into(frequency, &airport.icao));
}
airport.into()
})
.collect();
Runway::insert_all(&all_runway_rows).await?;
Frequency::insert_all(&all_frequency_rows).await?;
for chunk in airport_rows.chunks(chunk_size) { for chunk in airport_rows.chunks(chunk_size) {
// Build a dynamic query for batch insertion.
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new( let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
"INSERT INTO airports (icao, iata, local, name, category, \ "INSERT INTO airports (icao, iata, local, name, category, \
iso_country, iso_region, municipality, elevation_ft, \ iso_country, iso_region, municipality, elevation_ft, \
@@ -376,4 +440,32 @@ impl Airport {
Ok(()) Ok(())
} }
fn push_condition_array<'a>(
builder: &mut QueryBuilder<'a, Postgres>,
has_where: &mut bool,
column: &str,
field: &'a Option<String>,
) {
if let Some(ref value_str) = field {
// Split on commas, trim whitespace, and drop empties.
let values: Vec<&str> = value_str
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
if !values.is_empty() {
if !*has_where {
builder.push(" WHERE ");
*has_where = true;
} else {
builder.push(" AND ");
}
builder.push(column);
builder.push(" = ANY(");
builder.push_bind(values);
builder.push(")");
}
}
}
} }

View File

@@ -1,15 +1,115 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Postgres, QueryBuilder};
use uuid::Uuid;
use crate::db;
use crate::error::ApiResult;
const TABLE_NAME: &str = "frequencies";
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Frequency { pub struct Frequency {
pub id: String, #[serde(rename = "id")]
pub frequency_id: String,
pub frequency_mhz: f32,
}
#[derive(Debug, Deserialize, sqlx::FromRow)]
pub struct FrequencyRow {
pub id: Uuid,
pub icao: String,
pub frequency_id: String,
pub frequency_mhz: f32, pub frequency_mhz: f32,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct UpdateFrequency { pub struct UpdateFrequency {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>, pub icao: Option<String>,
#[serde(rename = "id", skip_serializing_if = "Option::is_none")]
pub frequency_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub frequency_mhz: Option<f32>, pub frequency_mhz: Option<f32>,
} }
impl From<FrequencyRow> for Frequency {
fn from(frequency: FrequencyRow) -> Self {
Self {
frequency_id: frequency.frequency_id.clone(),
frequency_mhz: frequency.frequency_mhz,
}
}
}
impl Frequency {
pub fn into(frequency: &Frequency, icao: &str) -> FrequencyRow {
FrequencyRow {
id: Uuid::new_v4(),
icao: icao.to_string(),
frequency_id: frequency.frequency_id.clone(),
frequency_mhz: frequency.frequency_mhz.clone(),
}
}
pub async fn select_all_map(icaos: Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> {
let pool = db::pool();
let frequency_rows: Vec<FrequencyRow> = sqlx::query_as(&format!(
r#"SELECT * FROM {} WHERE icao = ANY($1)"#,
TABLE_NAME
))
.bind(&icaos)
.fetch_all(pool)
.await?;
let mut frequency_map: HashMap<String, Vec<Self>> = HashMap::new();
for frequency_row in frequency_rows {
let icao = frequency_row.icao.clone();
let frequency = frequency_row.into();
frequency_map
.entry(icao.to_string())
.or_default()
.push(frequency);
}
Ok(frequency_map)
}
pub async fn select_all(icao: &str) -> ApiResult<Vec<Self>> {
let pool = db::pool();
let frequency_row: Vec<FrequencyRow> = sqlx::query_as(&format!(
r#"
SELECT * FROM {} WHERE icao = $1
"#,
TABLE_NAME
))
.bind(icao)
.fetch_all(pool)
.await?;
Ok(frequency_row.into_iter().map(From::from).collect())
}
pub async fn insert_all(frequencies: &Vec<FrequencyRow>) -> ApiResult<()> {
let pool = db::pool();
let chunk_size = 1000;
for chunk in frequencies.chunks(chunk_size) {
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(&format!(
"INSERT INTO {} (id, icao, frequency_id, frequency_mhz) ",
TABLE_NAME
));
query_builder.push_values(chunk, |mut b, row| {
b.push_bind(&row.id)
.push_bind(&row.icao)
.push_bind(&row.frequency_id)
.push_bind(&row.frequency_mhz);
});
let query = query_builder.build();
query.execute(pool).await?;
}
Ok(())
}
}

View File

@@ -1,8 +1,26 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Postgres, QueryBuilder};
use uuid::Uuid;
use crate::db;
use crate::error::ApiResult;
const TABLE_NAME: &str = "runways";
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Runway { pub struct Runway {
pub id: String, #[serde(rename = "id")]
pub runway_id: String,
pub length_ft: f32,
pub width_ft: f32,
pub surface: String,
}
#[derive(Debug, Deserialize, sqlx::FromRow)]
pub struct RunwayRow {
pub id: Uuid,
pub icao: String,
pub runway_id: String,
pub length_ft: f32, pub length_ft: f32,
pub width_ft: f32, pub width_ft: f32,
pub surface: String, pub surface: String,
@@ -11,7 +29,9 @@ pub struct Runway {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct UpdateRunway { pub struct UpdateRunway {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>, pub icao: Option<String>,
#[serde(rename = "id", skip_serializing_if = "Option::is_none")]
pub frequency_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub length_ft: Option<f32>, pub length_ft: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -19,3 +39,88 @@ pub struct UpdateRunway {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub surface: Option<String>, pub surface: Option<String>,
} }
impl From<RunwayRow> for Runway {
fn from(runway: RunwayRow) -> Self {
Self {
runway_id: runway.runway_id.clone(),
length_ft: runway.length_ft.clone(),
width_ft: runway.width_ft.clone(),
surface: runway.surface.clone(),
}
}
}
impl Runway {
pub fn into(runway: &Runway, icao: &str) -> RunwayRow {
RunwayRow {
id: Uuid::new_v4(),
icao: icao.to_string(),
runway_id: runway.runway_id.clone(),
length_ft: runway.length_ft.clone(),
width_ft: runway.width_ft.clone(),
surface: runway.surface.clone(),
}
}
pub async fn select_all_map(icaos: Vec<String>) -> ApiResult<HashMap<String, Vec<Self>>> {
let pool = db::pool();
let runway_rows: Vec<RunwayRow> = sqlx::query_as(&format!(
r#"SELECT * FROM {} WHERE icao = ANY($1)"#,
TABLE_NAME
))
.bind(&icaos)
.fetch_all(pool)
.await?;
let mut runway_map: HashMap<String, Vec<Self>> = HashMap::new();
for runway_row in runway_rows {
let icao = runway_row.icao.clone();
let runway = runway_row.into();
runway_map.entry(icao.to_string()).or_default().push(runway);
}
Ok(runway_map)
}
pub async fn select_all(icao: &str) -> ApiResult<Vec<Self>> {
let pool = db::pool();
let runway_rows: Vec<RunwayRow> = sqlx::query_as(&format!(
r#"
SELECT * FROM {} WHERE icao = $1
"#,
TABLE_NAME
))
.bind(icao)
.fetch_all(pool)
.await?;
Ok(runway_rows.into_iter().map(From::from).collect())
}
pub async fn insert_all(runways: &Vec<RunwayRow>) -> ApiResult<()> {
let pool = db::pool();
let chunk_size = 1000;
for chunk in runways.chunks(chunk_size) {
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(&format!(
"INSERT INTO {} (id, icao, runway_id, length_ft, width_ft, surface) ",
TABLE_NAME
));
query_builder.push_values(chunk, |mut b, row| {
b.push_bind(&row.id)
.push_bind(&row.icao)
.push_bind(&row.runway_id)
.push_bind(&row.length_ft)
.push_bind(&row.width_ft)
.push_bind(&row.surface);
});
let query = query_builder.build();
query.execute(pool).await?;
}
Ok(())
}
}

View File

@@ -58,7 +58,10 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
async fn get_airports(req: HttpRequest) -> HttpResponse { async fn get_airports(req: HttpRequest) -> HttpResponse {
let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) { let mut query = match web::Query::<AirportQuery>::from_query(req.query_string()) {
Ok(q) => q.into_inner(), Ok(q) => q.into_inner(),
Err(_) => AirportQuery::default(), Err(err) => {
log::error!("{}", err);
AirportQuery::default()
}
}; };
let total = Airport::count(&query).await; let total = Airport::count(&query).await;

View File

@@ -42,6 +42,10 @@ pub struct Metar {
pub min_t_c: Option<f64>, pub min_t_c: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub precip_in: Option<f64>, pub precip_in: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub humidity: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub density_altitude: Option<f64>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@@ -184,6 +188,8 @@ impl Default for Metar {
max_t_c: None, max_t_c: None,
min_t_c: None, min_t_c: None,
precip_in: None, precip_in: None,
humidity: None,
density_altitude: None,
} }
} }
} }
@@ -632,18 +638,19 @@ impl Metar {
let remark = metar_parts[0]; let remark = metar_parts[0];
metar_parts.remove(0); metar_parts.remove(0);
if remark == "AO1" { if remark == "AO1" {
metar metar.remarks.auto_station_without_precipication = Some(true);
.remarks
.auto_station_without_precipication = Some(true);
} else if remark == "AO2" { } else if remark == "AO2" {
metar.remarks.auto_station_with_precipication = Some(true); metar.remarks.auto_station_with_precipication = Some(true);
} else if remark == "$" { } else if remark == "$" {
metar.remarks.maintenance_indicator_on = Some(true); metar.remarks.maintenance_indicator_on = Some(true);
} else if remark == "PK" && metar_parts.len() >= 2 && metar_parts[0] == "WND"{ } else if remark == "PK" && metar_parts.len() >= 2 && metar_parts[0] == "WND" {
metar_parts.remove(0); metar_parts.remove(0);
let string = metar_parts[0]; let string = metar_parts[0];
metar_parts.remove(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(); 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) { if let Some(caps) = re.captures(string) {
// Get degrees, speed, minutes // Get degrees, speed, minutes
let degrees: i32 = caps["degrees"].parse()?; let degrees: i32 = caps["degrees"].parse()?;
@@ -660,15 +667,16 @@ impl Metar {
degrees, degrees,
speed, speed,
hour, hour,
minutes minutes,
}); });
} else { } else {
return Err(Error::new(500, "Input string format is invalid".to_string())); return Err(Error::new(
500,
"Input string format is invalid".to_string(),
));
} }
} else if remark == "PNO" { } else if remark == "PNO" {
metar metar.remarks.precipication_information_not_available = Some(true);
.remarks
.precipication_information_not_available = Some(true);
} else if remark == "RVRNO" { } else if remark == "RVRNO" {
metar.remarks.rvr_missing = Some(true); metar.remarks.rvr_missing = Some(true);
} else if remark == "PWINO" { } else if remark == "PWINO" {
@@ -676,19 +684,14 @@ impl Metar {
.remarks .remarks
.precipication_identifier_information_not_available = Some(true); .precipication_identifier_information_not_available = Some(true);
} else if remark == "FZRANO" { } else if remark == "FZRANO" {
metar metar.remarks.freezing_rain_information_not_available = Some(true);
.remarks
.freezing_rain_information_not_available = Some(true);
} else if remark == "TSNO" { } else if remark == "TSNO" {
metar metar.remarks.thunderstorm_information_not_available = Some(true);
.remarks
.thunderstorm_information_not_available = Some(true);
} else if remark == "VISNO" { } else if remark == "VISNO" {
let location = metar_parts[0]; let location = metar_parts[0];
metar_parts.remove(0); metar_parts.remove(0);
metar metar.remarks.visibility_at_secondary_location_not_available =
.remarks Some(location.to_string());
.visibility_at_secondary_location_not_available = Some(location.to_string());
} else if remark == "CHINO" { } else if remark == "CHINO" {
let location = metar_parts[0]; let location = metar_parts[0];
metar_parts.remove(0); metar_parts.remove(0);
@@ -713,7 +716,7 @@ impl Metar {
metar.temp_c = Some(t / 10.0 * -1.0); metar.temp_c = Some(t / 10.0 * -1.0);
} }
} }
let dewpoint_negation = &remark[6..7]; let dewpoint_negation = &remark[5..6];
let dewpoint = &remark[6..9]; let dewpoint = &remark[6..9];
if let Ok(d) = dewpoint.parse::<f64>() { if let Ok(d) = dewpoint.parse::<f64>() {
if dewpoint_negation == "0" { if dewpoint_negation == "0" {
@@ -778,6 +781,16 @@ impl Metar {
} }
} }
// Calculate estimated humidity
if metar.temp_c.is_some() && metar.dewpoint_c.is_some() {
let estimated_humidity = 100.0 - ((metar.temp_c.unwrap() - metar.dewpoint_c.unwrap()) * 5.0);
metar.humidity = Some(estimated_humidity);
}
// Calculate estimated density
// let estimated_density = ;
// metar.density_altitude = Some(metar.density_altitude);
Ok(metar) Ok(metar)
} }
@@ -957,10 +970,16 @@ mod tests {
RMK AO2 PK WND 20032/25 WSHFT 1715 VIS 3/4V1 1/2 VIS 3/4 RWY11 RAB07 CIG 013V017 CIG 017 RWY11 PRESFR 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(); SLP125 P0003 60009 T00640036 10066 21012 58033 TSNO $".to_string();
let metar = Metar::parse(&metar_string).unwrap(); let metar = Metar::parse(&metar_string).unwrap();
dbg!(&metar); // dbg!(&metar);
metar_string = "KMRB 082253Z 30014G23KT 10SM CLR 05/M12 A3002 RMK AO2 PK WND 30028/2157 SLP168 T00501117".to_string(); metar_string = "KMIA 090053Z 33004KT 10SM FEW015 FEW024 SCT075 SCT250 25/22 A2990 RMK AO2 SLP126 T02500217 $".to_string();
let metar = Metar::parse(&metar_string).unwrap(); let metar = Metar::parse(&metar_string).unwrap();
dbg!(&metar); dbg!(&metar);
metar_string =
"KMRB 082253Z 30014G23KT 10SM CLR 05/M12 A3002 RMK AO2 PK WND 30028/2157 SLP168 T00501117"
.to_string();
let metar = Metar::parse(&metar_string).unwrap();
// dbg!(&metar);
} }
} }

View File

@@ -5,7 +5,7 @@ meta {
} }
get { get {
url: {{BASE_URL}}/airports/TEST url: {{BASE_URL}}/airports/KHEF
body: none body: none
auth: none auth: none
} }

View File

@@ -5,7 +5,7 @@ meta {
} }
get { get {
url: {{BASE_URL}}/airports?page=1&limit=1000 url: {{BASE_URL}}/airports?page=1&limit=1000&icaos=KHEF&metarss=true
body: none body: none
auth: none auth: none
} }
@@ -13,4 +13,6 @@ get {
params:query { params:query {
page: 1 page: 1
limit: 1000 limit: 1000
icaos: KHEF
metarss: true
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long