diff --git a/README.md b/README.md index c88f2c4..a216a74 100755 --- a/README.md +++ b/README.md @@ -8,4 +8,13 @@ 1. Copy `.env.TEMPLATE` to `.env` 2. Generate JWT RS256 (RSA Signature with SHA-256) Private/Public keys with `make generate` 3. Build the service and ui images with `make build` -4. Run the application with `make up` \ No newline at end of file +4. Run the application with `make up` + +## Decoding METARS +The following resources were used to help decode METARS. +- [Metar Decode Key PDF](https://www.weather.gov/media/wrh/mesowest/metar_decode_key.pdf) +- [Metar Decode (NPS EDU)](https://met.nps.edu/~bcreasey/mr3222/files/helpful/DecodeMETAR-TAF.html) +- [Weather Phenomena](http://www.moratech.com/aviation/metar-class/metar-pg9-ww.html) + +## OpenMapTiles +[Generate Vector Tiles](https://openmaptiles.org/docs/generate/generate-openmaptiles/) \ No newline at end of file diff --git a/service/airport-codes.json b/service/airport-codes.json index 9796163..132574f 100644 --- a/service/airport-codes.json +++ b/service/airport-codes.json @@ -34817,6 +34817,7 @@ }, { "icao": "KIAH", + "full_name": "George Bush Intercontinental Airport", "category": "large_airport", "point": { @@ -36735,23 +36736,6 @@ "iata_code": "", "local_code": "K19" }, - { - "icao": "KK20", - "category": "small_airport", - "full_name": "Wendell H Ford Airport", - "point": - { - "y": 37.384838, - "x": -83.259662 - }, - "elevation_ft": 1256, - "iso_country": "US", - "iso_region": "US-KY", - "municipality": "Chavies", - "gps_code": "", - "iata_code": "", - "local_code": "CPF" - }, { "icao": "KK22", "category": "small_airport", diff --git a/service/src/airports/model.rs b/service/src/airports/model.rs index 8e7da74..7fa4e78 100644 --- a/service/src/airports/model.rs +++ b/service/src/airports/model.rs @@ -14,7 +14,6 @@ pub struct Airport { pub icao: String, pub category: String, pub full_name: String, - pub point: Point, pub elevation_ft: Option, pub iso_country: String, pub iso_region: String, @@ -22,6 +21,7 @@ pub struct Airport { pub gps_code: String, pub iata_code: String, pub local_code: String, + pub point: Point, pub tower: Option, } @@ -31,13 +31,13 @@ impl Into for Airport { icao: self.icao.clone(), category: self.category.clone(), full_name: self.full_name.clone(), - point: self.point.clone(), iso_country: self.iso_country.clone(), iso_region: self.iso_region.clone(), municipality: self.municipality.clone(), gps_code: self.gps_code.clone(), iata_code: self.iata_code.clone(), local_code: self.local_code.clone(), + point: self.point.clone(), data: match serde_json::to_value(&self) { Ok(d) => d, Err(err) => { diff --git a/service/src/db/mod.rs b/service/src/db/mod.rs index b085820..dbfa8cc 100644 --- a/service/src/db/mod.rs +++ b/service/src/db/mod.rs @@ -1,4 +1,4 @@ -use crate::{error_handler::ServiceError, airports::QueryAirport}; +use crate::{error_handler::ServiceError, airports::{QueryAirport, Airport}}; use diesel::{r2d2::ConnectionManager, PgConnection}; use redis::{Client as RedisClient, aio::Connection as RedisConnection}; use serde::{Deserialize, Serialize}; @@ -63,10 +63,10 @@ pub fn import_data() -> i32 { let path = "airport-codes.json"; debug!("Importing data from {}", path); let contents: String = std::fs::read_to_string(path).expect("Failed to read file"); - let airports: Vec = serde_json::from_str(&contents).expect("JSON was not well formed."); + let airports: Vec = serde_json::from_str(&contents).expect("JSON was not well formed."); let mut count = 0; for airport in airports { - match QueryAirport::insert(airport) { + match QueryAirport::insert(airport.into()) { Ok(_) => count += 1, Err(err) => error!("Error inserting airport; {}", err) }; diff --git a/service/src/metars/model.rs b/service/src/metars/model.rs index d9d1761..97fe42e 100644 --- a/service/src/metars/model.rs +++ b/service/src/metars/model.rs @@ -176,7 +176,8 @@ impl Metar { if metar_parts[0] == "AUTO" { metar.quality_control_flags.auto = Some(true); metar_parts.remove(0); - } else if metar_parts[0] == "COR" { + } + if metar_parts[0] == "COR" { metar.quality_control_flags.corrected = Some(true); metar_parts.remove(0); } @@ -270,7 +271,10 @@ impl Metar { } // Weather Phenomena - let wx_re = regex::Regex::new(r"^(?:[+-]|VC|MI|PR|BC|DR|BL|SH|TS|FZ)?(?:DZ|RA|SN|SG|IC|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PY|PO|SQ|FC|SS|DS)$").unwrap(); + 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 wx_re.is_match(metar_parts[0]) { metar.weather_phenomena.push(metar_parts[0].to_string()); metar_parts.remove(0); @@ -396,7 +400,7 @@ impl Metar { } // Flight Category - if metar.visibility_statute_mi.is_none() || metar.sky_condition.is_empty() { + 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 { @@ -407,7 +411,7 @@ impl Metar { v.parse::().unwrap() } } - None => 0.0 + None => 5.0 // Assume VFR if no visibility is present }; let ceiling = match metar.sky_condition.first() { Some(s) => { diff --git a/service/src/scheduler.rs b/service/src/scheduler.rs index 1982013..f7d732a 100644 --- a/service/src/scheduler.rs +++ b/service/src/scheduler.rs @@ -55,9 +55,10 @@ pub fn update_airports() { } } debug!("METAR update complete"); - // Sleep until the observation time is 1 hour old + // Sleep until the earliest observation time is 1 hour old + // Bounded by 1 and 3600 seconds let now = chrono::Utc::now().timestamp(); - let sleep_time = now - (observation_time + (3600)); + let sleep_time = std::cmp::min(std::cmp::max(1, now - (observation_time + (3600))), 3600); debug!("Next update in {} seconds", sleep_time); sleep(Duration::from_secs(sleep_time as u64)).await; } diff --git a/ui/src/api/airport.types.ts b/ui/src/api/airport.types.ts index 93d20e4..0632e68 100644 --- a/ui/src/api/airport.types.ts +++ b/ui/src/api/airport.types.ts @@ -46,7 +46,6 @@ export interface Airport { category: AirportCategory; full_name: string; elevation_ft: number; - continent: string; iso_country: string; iso_region: string; municipality: string; @@ -58,7 +57,7 @@ export interface Airport { y: number; srid: number; }; - metar?: Metar; + latest_metar?: Metar; } export interface GetAirportResponse { diff --git a/ui/src/api/metar.types.ts b/ui/src/api/metar.types.ts index 02afa45..631b1c7 100644 --- a/ui/src/api/metar.types.ts +++ b/ui/src/api/metar.types.ts @@ -5,30 +5,39 @@ export interface SkyCondition { export interface QualityControlFlags { auto: boolean; - auto_station: boolean; + auto_station_without_precipitation: boolean; + auto_station_with_precipication: boolean; + maintenance_indicator_on: boolean; + corrected: boolean; +} + +export interface RunwayVisualRange { + runway: string; + visibility_ft: string; + variable_visibility_high_ft: string; + variable_visibility_low_ft: string; } export interface Metar { raw_text: string; station_id: string; observation_time: string; - latitude: number; - longitude: number; temp_c: number; dewpoint_c: number; wind_dir_degrees: string; wind_speed_kt: number; + wind_gust_kt: number; + variable_wind_dir_degrees: string; visibility_statute_mi: string; + runway_visual_range: RunwayVisualRange[]; altim_in_hg: number; sea_level_pressure_mb: number; quality_control_flags: QualityControlFlags; - wx_string: string; + weather_phenomena: string[]; sky_condition: SkyCondition[]; flight_category: 'VFR' | 'MVFR' | 'LIFR' | 'IFR' | 'UNKN'; three_hr_pressure_tendency_mb: number; - metar_type: string; maxT_c: number; minT_c: number; precip_in: number; - elevation_m: number; } diff --git a/ui/src/components/Admin/AirportTablePanel.tsx b/ui/src/components/Admin/AirportTablePanel.tsx index cdf62ff..0915516 100644 --- a/ui/src/components/Admin/AirportTablePanel.tsx +++ b/ui/src/components/Admin/AirportTablePanel.tsx @@ -39,7 +39,6 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport {airport.icao} {airport.full_name} {airportCategoryToText(airport.category)} - {airport.continent} {airport.iso_country} {airport.iso_region} {airport.municipality} @@ -64,7 +63,6 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport ICAO Full Name Category - Continent ISO Country ISO Region Municipality diff --git a/ui/src/components/Admin/CreateAirportPanel.tsx b/ui/src/components/Admin/CreateAirportPanel.tsx index 46a12ef..04f4836 100644 --- a/ui/src/components/Admin/CreateAirportPanel.tsx +++ b/ui/src/components/Admin/CreateAirportPanel.tsx @@ -2,7 +2,6 @@ import { createAirport } from "@/api/airport"; import { Airport, AirportCategory } from "@/api/airport.types"; import { Card, TextInput, Select, Group, Flex, Space, Button } from "@mantine/core"; import { useForm } from "@mantine/form"; -import { useEffect } from "react"; export default function CreateAirportPanel() { const form = useForm({ @@ -11,7 +10,6 @@ export default function CreateAirportPanel() { category: AirportCategory.SMALL, full_name: '', elevation_ft: 0, - continent: '', iso_country: '', iso_region: '', municipality: '', @@ -64,12 +62,6 @@ export default function CreateAirportPanel() { {...form.getInputProps('elevation_ft')} /> - + - - { airportData.forEach((airport) => { if (metar.station_id == airport.icao) { - airport.metar = metar; + airport.latest_metar = metar; } }); }); @@ -82,16 +82,18 @@ export default function MapTiles() { className: 'metar-marker-icon' }); } - if (airport.metar?.flight_category == 'VFR') { + if (airport.latest_metar?.flight_category == 'VFR') { return innerIcon({ tag: 'V', color: 'green' }); - } else if (airport.metar?.flight_category == 'MVFR') { + } else if (airport.latest_metar?.flight_category == 'MVFR') { return innerIcon({ tag: 'M', color: 'blue' }); - } else if (airport.metar?.flight_category == 'IFR') { + } else if (airport.latest_metar?.flight_category == 'IFR') { return innerIcon({ tag: 'I', color: 'red' }); - } else if (airport.metar?.flight_category == 'LIFR') { + } else if (airport.latest_metar?.flight_category == 'LIFR') { return innerIcon({ tag: 'L', color: 'purple' }); - } else { + } else if (airport.latest_metar?.flight_category == 'UNKN') { return innerIcon({ tag: 'U', color: 'black', size: 'xs' }); + } else { + return innerIcon({tag: ' ', color: 'black', size: 'xs' }); } } diff --git a/ui/src/components/Metars/MetarModal.tsx b/ui/src/components/Metars/MetarModal.tsx index 7a41e7c..5fff48a 100644 --- a/ui/src/components/Metars/MetarModal.tsx +++ b/ui/src/components/Metars/MetarModal.tsx @@ -61,7 +61,7 @@ export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps
- {airport.metar && } + {airport.latest_metar && }
); @@ -164,8 +164,8 @@ function MetarInfo({ metar }: { metar: Metar }) { - {metar.wx_string && - metar.wx_string.split(' ').map((wx) => ( + {metar.weather_phenomena && + metar.weather_phenomena.map((wx) => (