From 6ecd9598e789dcfdfbd6b66f7b844251f0de7a7f Mon Sep 17 00:00:00 2001 From: Ben Sherriff Date: Tue, 19 Dec 2023 12:35:47 -0500 Subject: [PATCH] Updated metar querying logic and parsing --- service/docker-compose.yml | 3 + service/migrations/000000_airports/up.sql | 3 +- service/migrations/000001_metars/up.sql | 2 +- service/src/airports/model.rs | 116 ++++++++++++++++------ service/src/airports/routes.rs | 10 +- service/src/db/schema.rs | 5 +- service/src/metars/model.rs | 105 ++++++++++++++++---- ui/src/api/airport.types.ts | 17 +++- ui/src/components/Metars/MapTiles.tsx | 2 +- 9 files changed, 198 insertions(+), 65 deletions(-) diff --git a/service/docker-compose.yml b/service/docker-compose.yml index eee96cf..95408da 100644 --- a/service/docker-compose.yml +++ b/service/docker-compose.yml @@ -22,6 +22,8 @@ services: redis: image: redis:latest container_name: weather-redis + volumes: + - redis:/data ports: - ${REDIS_PORT:-6379}:6379 networks: @@ -72,6 +74,7 @@ services: volumes: db: db_logs: + redis: minio: networks: diff --git a/service/migrations/000000_airports/up.sql b/service/migrations/000000_airports/up.sql index f298751..3b42a55 100644 --- a/service/migrations/000000_airports/up.sql +++ b/service/migrations/000000_airports/up.sql @@ -7,8 +7,7 @@ CREATE TABLE IF NOT EXISTS airports ( iso_country TEXT NOT NULL, iso_region TEXT NOT NULL, municipality TEXT NOT NULL, - iata_code TEXT NOT NULL, - local_code TEXT NOT NULL, + has_metar BOOLEAN NOT NULL DEFAULT FALSE, point GEOMETRY(POINT,4326) NOT NULL, data JSONB NOT NULL ); \ No newline at end of file diff --git a/service/migrations/000001_metars/up.sql b/service/migrations/000001_metars/up.sql index 8b0fa36..82e1964 100644 --- a/service/migrations/000001_metars/up.sql +++ b/service/migrations/000001_metars/up.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS metars ( id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, - station_id TEXT NOT NULL, + icao TEXT NOT NULL, observation_time TIMESTAMP NOT NULL, raw_text TEXT NOT NULL, data JSONB NOT NULL diff --git a/service/src/airports/model.rs b/service/src/airports/model.rs index 7262c49..facc0b9 100644 --- a/service/src/airports/model.rs +++ b/service/src/airports/model.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use std::str::FromStr; use crate::db; @@ -12,16 +13,19 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct Airport { pub icao: String, - pub category: String, + pub category: AirportCategory, pub name: String, pub elevation_ft: f32, pub iso_country: String, pub iso_region: String, pub municipality: String, - pub iata_code: String, - pub local_code: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub iata_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub local_code: Option, pub latitude: f64, pub longitude: f64, + #[serde(skip_serializing_if = "Option::is_none")] pub has_tower: Option, } @@ -29,14 +33,13 @@ impl Into for Airport { fn into(self) -> QueryAirport { return QueryAirport { icao: self.icao.clone(), - category: self.category.clone(), + category: self.category.clone().to_string(), name: self.name.clone(), elevation_ft: self.elevation_ft, iso_country: self.iso_country.clone(), iso_region: self.iso_region.clone(), municipality: self.municipality.clone(), - iata_code: self.iata_code.clone(), - local_code: self.local_code.clone(), + has_metar: false, point: Point::new(self.longitude, self.latitude, Some(4326)), data: match serde_json::to_value(&self) { Ok(d) => d, @@ -55,6 +58,57 @@ impl From for Airport { } } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum AirportCategory { + #[serde(rename = "small_airport")] + Small, + #[serde(rename = "medium_airport")] + Medium, + #[serde(rename = "large_airport")] + Large, + #[serde(rename = "heliport")] + Heliport, + #[serde(rename = "closed")] + Closed, + #[serde(rename = "seaplane_base")] + Seaplane, + #[serde(rename = "balloonport")] + Balloonport, + #[serde(rename = "unknown")] + Unknown +} + +impl FromStr for AirportCategory { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "small_airport" => Ok(AirportCategory::Small), + "medium_airport" => Ok(AirportCategory::Medium), + "large_airport" => Ok(AirportCategory::Large), + "heliport" => Ok(AirportCategory::Heliport), + "closed" => Ok(AirportCategory::Closed), + "seaplane_base" => Ok(AirportCategory::Seaplane), + "balloonport" => Ok(AirportCategory::Balloonport), + _ => Ok(AirportCategory::Unknown) + } + } +} + +impl Display for AirportCategory { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AirportCategory::Small => write!(f, "small_airport"), + AirportCategory::Medium => write!(f, "medium_airport"), + AirportCategory::Large => write!(f, "large_airport"), + AirportCategory::Heliport => write!(f, "heliport"), + AirportCategory::Closed => write!(f, "closed"), + AirportCategory::Seaplane => write!(f, "seaplane_base"), + AirportCategory::Balloonport => write!(f, "balloonport"), + AirportCategory::Unknown => write!(f, "unknown") + } + } +} + #[derive(Serialize, Deserialize, AsChangeset, Insertable, Queryable, QueryableByName)] #[diesel(table_name = airports)] pub struct QueryAirport { @@ -65,8 +119,7 @@ pub struct QueryAirport { pub iso_country: String, pub iso_region: String, pub municipality: String, - pub iata_code: String, - pub local_code: String, + pub has_metar: bool, pub point: Point, pub data: serde_json::Value } @@ -75,7 +128,7 @@ pub struct QueryAirport { pub struct QueryFilters { pub search: Option, pub bounds: Option>, - pub categories: Option>, + pub categories: Option>, pub order_field: Option, pub order_by: Option } @@ -144,33 +197,34 @@ impl QueryAirport { let mut query: String = "SELECT * FROM airports".to_string(); query = format!("{} {}", query, QueryAirport::build_filter_query(&filters)?); + query = format!("{} ORDER BY has_metar DESC", query); if let Some(order_by) = &filters.order_by { match order_by { QueryOrderBy::Asc => { if let Some(order_field) = &filters.order_field { query = match order_field { - QueryOrderField::Icao => format!("{} ORDER BY icao ASC", query), - QueryOrderField::Name => format!("{} ORDER BY name ASC", query), - QueryOrderField::Category => format!("{} ORDER BY category ASC", query), - QueryOrderField::Country => format!("{} ORDER BY iso_country ASC", query), - QueryOrderField::Region => format!("{} ORDER BY iso_region ASC", query), - QueryOrderField::Municipality => format!("{} ORDER BY municipality ASC", query), - QueryOrderField::Iata => format!("{} ORDER BY iata_code ASC", query), - QueryOrderField::Local => format!("{} ORDER BY local_code ASC", query), + QueryOrderField::Icao => format!("{}, icao ASC", query), + QueryOrderField::Name => format!("{}, name ASC", query), + QueryOrderField::Category => format!("{}, category ASC", query), + QueryOrderField::Country => format!("{}, iso_country ASC", query), + QueryOrderField::Region => format!("{}, iso_region ASC", query), + QueryOrderField::Municipality => format!("{}, municipality ASC", query), + QueryOrderField::Iata => format!("{}, iata_code ASC", query), + QueryOrderField::Local => format!("{}, local_code ASC", query), }; }; }, QueryOrderBy::Desc => { if let Some(order_field) = &filters.order_field { query = match order_field { - QueryOrderField::Icao => format!("{} ORDER BY icao DESC", query), - QueryOrderField::Name => format!("{} ORDER BY name DESC", query), - QueryOrderField::Category => format!("{} ORDER BY category DESC", query), - QueryOrderField::Country => format!("{} ORDER BY iso_country DESC", query), - QueryOrderField::Region => format!("{} ORDER BY iso_region DESC", query), - QueryOrderField::Municipality => format!("{} ORDER BY municipality DESC", query), - QueryOrderField::Iata => format!("{} ORDER BY iata_code DESC", query), - QueryOrderField::Local => format!("{} ORDER BY local_code DESC", query), + QueryOrderField::Icao => format!("{}, icao DESC", query), + QueryOrderField::Name => format!("{}, name DESC", query), + QueryOrderField::Category => format!("{}, category DESC", query), + QueryOrderField::Country => format!("{}, iso_country DESC", query), + QueryOrderField::Region => format!("{}, iso_region DESC", query), + QueryOrderField::Municipality => format!("{}, municipality DESC", query), + QueryOrderField::Iata => format!("{}, iata_code DESC", query), + QueryOrderField::Local => format!("{}, local_code DESC", query), }; }; } @@ -219,10 +273,12 @@ impl QueryAirport { } } if let Some(categories) = &filters.categories { - parts.push(format!("({})", categories.iter().map(|category| format!("category = '{}'", category)).collect::>().join(" OR "))); + parts.push(format!("({})", categories.iter().map(|category| format!("category = '{}'", category.to_string())).collect::>().join(" OR "))); } if let Some(search) = &filters.search { - let search_strs = vec!["icao", "name", "iso_country", "iso_region", "municipality", "iata_code", "local_code"]; + // Sanitize search to only allow [a-zA-Z0-9-\\s] + let search = search.chars().filter(|c| c.is_alphanumeric() || *c == '-' || *c == ' ').collect::(); + let search_strs = vec!["icao", "name", "iso_country", "iso_region", "municipality"]; parts.push(format!("({})", search_strs.iter().map(|s| format!("{} ILIKE '%{}%'", s, search)).collect::>().join(" OR "))); } @@ -233,7 +289,7 @@ impl QueryAirport { return Ok(query); } - pub fn find(icao: String) -> Result { + pub fn get(icao: &str) -> Result { let mut conn = db::connection()?; let airport = airports::table.filter(airports::icao.eq(icao)).first(&mut conn)?; Ok(airport) @@ -261,10 +317,10 @@ impl QueryAirport { Ok(inserted_airports) } - pub fn update(icao: String, airport: Self) -> Result { + pub fn update(airport: Self) -> Result { let mut conn = db::connection()?; let airport = diesel::update(airports::table) - .filter(airports::icao.eq(icao)) + .filter(airports::icao.eq(airport.icao.clone())) .set(airport) .get_result(&mut conn)?; Ok(airport) diff --git a/service/src/airports/routes.rs b/service/src/airports/routes.rs index e005cf9..9ee0b94 100644 --- a/service/src/airports/routes.rs +++ b/service/src/airports/routes.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use futures_util::stream::StreamExt as _; -use crate::{airports::{QueryAirport, QueryFilters, QueryOrderField, QueryOrderBy, Airport}, db::{Response, Metadata}, auth::{JwtAuth, verify_role}}; +use crate::{airports::{QueryAirport, QueryFilters, QueryOrderField, QueryOrderBy, Airport, AirportCategory}, db::{Response, Metadata}, auth::{JwtAuth, verify_role}}; use actix_multipart::Multipart; use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError}; use log::{error, warn}; @@ -70,7 +70,7 @@ async fn get_all(req: HttpRequest) -> HttpResponse { let mut filters = QueryFilters::default(); filters.search = params.search.clone(); filters.categories = match ¶ms.categories { - Some(c) => Some(c.split(",").map(|s| s.to_string()).collect()), + Some(c) => Some(c.split(",").map(|s| AirportCategory::from_str(s).unwrap()).collect()), None => None }; filters.bounds = match ¶ms.bounds { @@ -163,7 +163,7 @@ async fn get_all(req: HttpRequest) -> HttpResponse { #[get("/{icao}")] async fn get(icao: web::Path) -> HttpResponse { - match QueryAirport::find(icao.into_inner()) { + match QueryAirport::get(&icao.into_inner()) { Ok(a) => { let airport: Airport = a.into(); HttpResponse::Ok().json(Response { @@ -198,13 +198,13 @@ async fn create(airport: web::Json, auth: JwtAuth) -> HttpResponse { } #[put("/{icao}")] -async fn update(icao: web::Path, airport: web::Json, auth: JwtAuth) -> HttpResponse { +async fn update(_icao: web::Path, airport: web::Json, auth: JwtAuth) -> HttpResponse { let _ = match verify_role(&auth, "admin") { Ok(_) => {}, Err(err) => return ResponseError::error_response(&err) }; let query_airport: QueryAirport = airport.into_inner().into(); - match QueryAirport::update(icao.into_inner(), query_airport) { + match QueryAirport::update(query_airport) { Ok(a) => { let airport: Airport = a.into(); HttpResponse::Ok().json(airport) diff --git a/service/src/db/schema.rs b/service/src/db/schema.rs index 2e98cd8..6cf5cf6 100644 --- a/service/src/db/schema.rs +++ b/service/src/db/schema.rs @@ -9,8 +9,7 @@ diesel::table! { iso_country -> Text, iso_region -> Text, municipality -> Text, - iata_code -> Text, - local_code -> Text, + has_metar -> Bool, point -> Geometry, data -> Jsonb } @@ -19,7 +18,7 @@ diesel::table! { diesel::table! { metars (id) { id -> Integer, - station_id -> Text, + icao -> Text, observation_time -> Timestamp, raw_text -> Text, data -> Jsonb, diff --git a/service/src/metars/model.rs b/service/src/metars/model.rs index 21d5bc6..d1a19cf 100644 --- a/service/src/metars/model.rs +++ b/service/src/metars/model.rs @@ -1,3 +1,4 @@ +use crate::airports::QueryAirport; use crate::{error_handler::ServiceError, db}; use crate::db::schema::metars::{self}; use chrono::Datelike; @@ -17,7 +18,9 @@ pub struct QualityControlFlags { #[serde(skip_serializing_if = "Option::is_none")] pub maintenance_indicator_on: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub corrected: Option + pub corrected: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub no_significant_change: Option, } impl Default for QualityControlFlags { @@ -28,6 +31,7 @@ impl Default for QualityControlFlags { auto_station_with_precipication: None, maintenance_indicator_on: None, corrected: None, + no_significant_change: None, } } } @@ -36,14 +40,17 @@ impl Default for QualityControlFlags { pub struct SkyCondition { pub sky_cover: String, #[serde(skip_serializing_if = "Option::is_none")] - pub cloud_base_ft_agl: Option + pub cloud_base_ft_agl: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub significant_convective_clouds: Option } impl Default for SkyCondition { fn default() -> Self { SkyCondition { sky_cover: "".to_string(), - cloud_base_ft_agl: None + cloud_base_ft_agl: None, + significant_convective_clouds: None, } } } @@ -91,9 +98,9 @@ pub struct Metar { #[serde(skip_serializing_if = "Option::is_none")] pub wind_dir_degrees: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub wind_speed_kt: Option, + pub wind_speed_kt: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub wind_gust_kt: Option, + pub wind_gust_kt: Option, #[serde(skip_serializing_if = "Option::is_none")] pub variable_wind_dir_degrees: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -203,27 +210,39 @@ impl Metar { metar.quality_control_flags.corrected = Some(true); metar_parts.remove(0); } + if !metar_parts.is_empty() && metar_parts[0] == "NOSIG" { + metar.quality_control_flags.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$").unwrap(); - let wind_gust_re = regex::Regex::new(r"^(?:[0-9]{3}|VRB)[0-9]{2}G[0-9]{2}KT$").unwrap(); + let wind_re = regex::Regex::new(r"^(?:[0-9]{3}|VRB)[0-9]{2}(?:KT|MPS)$").unwrap(); + let wind_gust_re = regex::Regex::new(r"^(?:[0-9]{3}|VRB)[0-9]{2}G[0-9]{2}(?:KT|MPS)$").unwrap(); if !metar_parts.is_empty() && wind_re.is_match(metar_parts[0]) { let wind = metar_parts[0]; metar_parts.remove(0); let wind_dir_degrees = &wind[0..3]; - let wind_speed_kt = &wind[3..5]; metar.wind_dir_degrees = Some(wind_dir_degrees.to_string()); - metar.wind_speed_kt = Some(wind_speed_kt.parse::().unwrap()); + 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::().unwrap() * 1.94384).to_string(); + } + metar.wind_speed_kt = Some(wind_speed_kt.parse::().unwrap()); } else if !metar_parts.is_empty() && wind_gust_re.is_match(metar_parts[0]) { let wind = metar_parts[0]; metar_parts.remove(0); let wind_dir_degrees = &wind[0..3]; - let wind_speed_kt = &wind[3..5]; metar.wind_dir_degrees = Some(wind_dir_degrees.to_string()); - metar.wind_speed_kt = Some(wind_speed_kt.parse::().unwrap()); - // Gust - let wind_gust_kt = &wind[6..8]; - metar.wind_gust_kt = Some(wind_gust_kt.parse::().unwrap()); + 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::().unwrap() * 1.94384).to_string(); + wind_gust_kt = (wind_gust_kt.parse::().unwrap() * 1.94384).to_string(); + } + metar.wind_speed_kt = Some(wind_speed_kt.parse::().unwrap()); + metar.wind_gust_kt = Some(wind_gust_kt.parse::().unwrap()); } // Variable Wind Direction @@ -273,7 +292,7 @@ impl Metar { // Runway Visual Range let rvr_re = regex::Regex::new(r"^R[0-9]{1,3}(?:L|R|C)?/[PM]?[0-9]{4}FT$").unwrap(); 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$").unwrap(); - while !metar_parts.is_empty() && rvr_re.is_match(metar_parts[0]) || variable_rvr_re.is_match(metar_parts[0]) { + 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(); @@ -303,7 +322,7 @@ impl Metar { } // Sky Condition - let sky_condition_re = regex::Regex::new(r"^(?:CLR|SKC|(?:FEW|SCT|BKN|OVC|VV)([0-9]{3})?)$").unwrap(); + let sky_condition_re = regex::Regex::new(r"^(?:CLR|SKC|CAVOK|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]) { let sky_condition_string = metar_parts[0]; metar_parts.remove(0); @@ -311,7 +330,20 @@ impl Metar { let sky_cover = &sky_condition_string[0..3]; sky_condition.sky_cover = sky_cover.to_string(); if sky_condition_string.len() > 3 { - sky_condition.cloud_base_ft_agl = Some(sky_condition_string[3..sky_condition_string.len()].parse::().unwrap() * 100); + // Parse out the next three digits + let cloud_base_ft_agl = &sky_condition_string[3..6]; + sky_condition.cloud_base_ft_agl = match cloud_base_ft_agl.parse::() { + Ok(c) => Some(c * 100), + Err(err) => { + warn!("Unable to parse cloud base in {}: {}", sky_condition_string, err); + None + } + }; + if sky_condition_string.len() > 6 { + // Parse out the next two digits + let scc = &sky_condition_string[6..8]; + sky_condition.significant_convective_clouds = Some(scc.to_string()); + } } metar.sky_condition.push(sky_condition); } @@ -525,7 +557,7 @@ impl Metar { let mut insert_metars: Vec = vec![]; for metar in metars { insert_metars.push(InsertMetar { - station_id: metar.station_id.to_string(), + icao: metar.station_id.to_string(), observation_time: metar.observation_time, raw_text: metar.raw_text.to_string(), data: serde_json::to_value(metar).unwrap() @@ -552,6 +584,13 @@ impl Metar { } trace!("Retrieving missing METAR data for {:?}", missing_icaos); let missing_icaos_string: Vec = missing_icaos.iter().map(|icao| format!("{}", icao.to_string())).collect(); + let mut airports: Vec = vec![]; + missing_icaos_string.clone().iter().for_each(|icao| { + match QueryAirport::get(icao) { + Ok(a) => airports.push(a), + Err(_) => {} + } + }); let mut missing_metars = Self::get_remote_metars(missing_icaos_string.join(",")).await; if missing_metars.len() > 0 { let insert_metars = Self::to_insert(&missing_metars); @@ -559,6 +598,27 @@ impl Metar { Ok(rows) => trace!("Inserted {} metar rows", rows), Err(err) => warn!("Unable to insert metar data; {}", err) }; + // Update airports with the appropriate has_metar flag + airports.iter().for_each(|airport| { + if missing_metars.iter().any(|metar| metar.station_id == airport.icao) { + let updated = QueryAirport { + icao: airport.icao.to_string(), + category: airport.category.to_string(), + name: airport.name.to_string(), + elevation_ft: airport.elevation_ft, + iso_country: airport.iso_country.to_string(), + iso_region: airport.iso_region.to_string(), + municipality: airport.municipality.to_string(), + has_metar: true, + point: airport.point, + data: airport.data.to_owned() + }; + match QueryAirport::update(updated) { + Ok(_) => {}, + Err(err) => warn!("Unable to update airport with has_metar flag; {}", err) + } + } + }); } let mut metars: Vec = vec![]; metars.append(&mut missing_metars); @@ -570,7 +630,7 @@ impl Metar { #[derive(Serialize, Deserialize, AsChangeset, Insertable)] #[diesel(table_name = metars)] struct InsertMetar { - station_id: String, + icao: String, observation_time: chrono::NaiveDateTime, raw_text: String, data: serde_json::Value @@ -590,7 +650,7 @@ impl InsertMetar { #[diesel(table_name = metars)] struct QueryMetar { id: i32, - station_id: String, + icao: String, observation_time: chrono::NaiveDateTime, raw_text: String, data: serde_json::Value @@ -598,11 +658,12 @@ struct QueryMetar { impl QueryMetar { fn get_all(icaos: &Vec<&str>) -> Result, ServiceError> { + // Sanitize search to only allow [a-zA-Z0-9] + let icaos = icaos.iter().map(|icao| icao.chars().filter(|c| c.is_alphanumeric()).collect::()).collect::>(); let station_query: Vec = icaos.iter().map(|icao| format!("'{}'", icao.to_string())).collect(); - let mut conn = db::connection()?; let db_metars: Vec = match sql_query( - format!("SELECT DISTINCT ON (station_id) * FROM metars WHERE station_id IN ({}) ORDER BY station_id, observation_time DESC", station_query.join(",")) + format!("SELECT DISTINCT ON (icao) * FROM metars WHERE icao IN ({}) ORDER BY icao, observation_time DESC", station_query.join(",")) ).load(&mut conn) { Ok(m) => m, Err(err) => return Err(ServiceError { status: 500, message: format!("{}", err) }) diff --git a/ui/src/api/airport.types.ts b/ui/src/api/airport.types.ts index 70d6dc0..61e974f 100644 --- a/ui/src/api/airport.types.ts +++ b/ui/src/api/airport.types.ts @@ -4,7 +4,12 @@ import { Metar } from './metar.types'; export enum AirportCategory { SMALL = 'small_airport', MEDIUM = 'medium_airport', - LARGE = 'large_airport' + LARGE = 'large_airport', + HELIPORT = 'heliport', + BALLOONPORT = 'balloonport', + CLOSED = 'closed', + SEAPLANE = 'seaplane_base', + UNKNOWN = 'unknown', } export function airportCategoryToText(category: AirportCategory): string { @@ -15,6 +20,16 @@ export function airportCategoryToText(category: AirportCategory): string { return 'Medium'; case AirportCategory.LARGE: return 'Large'; + case AirportCategory.HELIPORT: + return 'Helipad'; + case AirportCategory.CLOSED: + return 'Closed'; + case AirportCategory.SEAPLANE: + return 'Seaplane Base'; + case AirportCategory.BALLOONPORT: + return 'Balloonport'; + default: + return 'Unknown'; } } diff --git a/ui/src/components/Metars/MapTiles.tsx b/ui/src/components/Metars/MapTiles.tsx index 9c6e594..540a298 100644 --- a/ui/src/components/Metars/MapTiles.tsx +++ b/ui/src/components/Metars/MapTiles.tsx @@ -53,7 +53,7 @@ export default function MapTiles() { northEast: { lat: ne.lat, lon: ne.lng }, southWest: { lat: sw.lat, lon: sw.lng } }, - categories: ['small_airport', 'medium_airport', 'large_airport'], + categories: ['large_airport'], order_field: AirportOrderField.CATEGORY, order_by: 'asc', limit: 250,