Updated metars

This commit is contained in:
2023-12-19 15:32:45 -05:00
parent 6ecd9598e7
commit 0b4145ac30
5 changed files with 124 additions and 47 deletions

View File

@@ -245,11 +245,23 @@ impl QueryAirport {
let mut query = "SELECT COUNT(*) FROM airports".to_string();
query = format!("{} {}", query, QueryAirport::build_filter_query(&filters)?);
let count: i64 = match sql_query(query).execute(&mut conn) {
Ok(c) => c as i64,
// TODO: Fix this to use get_result() instead of building this table to do the load()
diesel::table! {
airports (count) {
count -> BigInt,
}
}
#[derive(Debug, Queryable, QueryableByName)]
#[diesel(table_name = airports)]
struct Count {
count: i64
}
let count: Vec<Count> = match sql_query(query).load(&mut conn) {
Ok(a) => a,
Err(err) => return Err(ServiceError { status: 500, message: format!("{}", err) })
};
return Ok(count);
return Ok(count[0].count);
}
// TODO: Unsafe query, need to sanitize inputs

View File

@@ -1,10 +1,10 @@
use crate::{error_handler::ServiceError, airports::{QueryAirport, Airport}};
use crate::error_handler::ServiceError;
use diesel::{r2d2::ConnectionManager, PgConnection};
use redis::{Client as RedisClient, aio::Connection as RedisConnection};
use serde::{Deserialize, Serialize};
use crate::diesel_migrations::MigrationHarness;
use lazy_static::lazy_static;
use log::{error, debug, info};
use log::{error, info};
use r2d2;
use std::env;
@@ -59,22 +59,6 @@ pub async fn redis_async_connection() -> Result<RedisConnection, ServiceError> {
Ok(conn)
}
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<Airport> = serde_json::from_str(&contents).expect("JSON was not well formed.");
let mut count = 0;
for airport in airports {
match QueryAirport::insert(airport.into()) {
Ok(_) => count += 1,
Err(err) => error!("Error inserting airport; {}", err)
};
}
debug!("Import complete");
return count;
}
#[derive(Serialize, Deserialize)]
pub struct Response<T> {
pub data: T,

View File

@@ -22,7 +22,7 @@ async fn main() -> std::io::Result<()> {
dotenv().ok();
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,service=info"));
db::init();
// scheduler::update_airports();
scheduler::update_airports();
let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string());
let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string());

View File

@@ -21,6 +21,8 @@ pub struct QualityControlFlags {
pub corrected: 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>,
}
impl Default for QualityControlFlags {
@@ -32,6 +34,7 @@ impl Default for QualityControlFlags {
maintenance_indicator_on: None,
corrected: None,
no_significant_change: None,
temporary_change: None,
}
}
}
@@ -172,6 +175,10 @@ impl Metar {
// Date/Time
let observation_time = metar_parts[0];
metar_parts.remove(0);
if observation_time.len() != 7 {
warn!("Unable to parse observation time in {}: {}", observation_time, metar_string);
continue;
}
let observation_time_day = &observation_time[0..2];
let observation_time_hour = &observation_time[2..4];
let observation_time_minute = &observation_time[4..6];
@@ -253,7 +260,8 @@ impl Metar {
}
// Visibility
let visibility_re = regex::Regex::new(r"^M?(?:[0-9]+|[0-9]+/[0-9]+)SM").unwrap();
let visibility_re = regex::Regex::new(r"^M?(?:[0-9]+|[0-9]+/[0-9]+)SM$").unwrap();
let visibility_re_m = regex::Regex::new(r"^[0-9]{4}(:?N|NE|NW|S|SE|SW)?$").unwrap();
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);
@@ -287,6 +295,16 @@ impl Metar {
format!("{}", visibility_whole + (visibility_left.parse::<f64>().unwrap() / 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>().unwrap() * 0.000621371;
metar.visibility_statute_mi = Some(format!("{:.2}", visibility));
}
}
// Runway Visual Range
@@ -322,16 +340,32 @@ impl Metar {
}
// Sky Condition
let sky_condition_re = regex::Regex::new(r"^(?:CLR|SKC|CAVOK|NSC|NCD|(?:FEW|SCT|BKN|OVC|VV)([0-9]{3})?(?:CB|TCU)?)$").unwrap();
if !metar_parts.is_empty() && metar_parts[0] == "CAVOK" {
metar.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]) {
let sky_condition_string = metar_parts[0];
metar_parts.remove(0);
let mut sky_condition = SkyCondition::default();
let sky_cover = &sky_condition_string[0..3];
sky_condition.sky_cover = sky_cover.to_string();
if sky_condition_string.len() > 3 {
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 {
// Parse out the next three digits
let cloud_base_ft_agl = &sky_condition_string[3..6];
let cloud_base_ft_agl = &sky_condition_string[3 - vv_offset..6 - vv_offset];
if cloud_base_ft_agl == "///" {
sky_condition.cloud_base_ft_agl = None;
} else {
sky_condition.cloud_base_ft_agl = match cloud_base_ft_agl.parse::<i32>() {
Ok(c) => Some(c * 100),
Err(err) => {
@@ -339,9 +373,10 @@ impl Metar {
None
}
};
if sky_condition_string.len() > 6 {
}
if sky_condition_string.len() > 6 - vv_offset {
// Parse out the next two digits
let scc = &sky_condition_string[6..8];
let scc = &sky_condition_string[6 - vv_offset..8 - vv_offset];
sky_condition.significant_convective_clouds = Some(scc.to_string());
}
}
@@ -398,6 +433,20 @@ impl Metar {
metar.altim_in_hg = Some(altim[1..altim.len()].parse::<f64>().unwrap() / 100.0);
}
// Pressure
let pressure_re = regex::Regex::new(r"^Q[0-9]{4}$").unwrap();
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>().unwrap());
}
// Temporary Change
if !metar_parts.is_empty() && metar_parts[0] == "TEMPO" {
metar.quality_control_flags.temporary_change = Some(true);
metar_parts.remove(0);
}
// Remarks
if !metar_parts.is_empty() && metar_parts[0] == "RMK" {
metar_parts.remove(0);
@@ -469,7 +518,7 @@ impl Metar {
};
let ceiling = match metar.sky_condition.first() {
Some(s) => {
if s.sky_cover == "CLR" || s.sky_cover == "SKC" {
if s.sky_cover == "CLR" || s.sky_cover == "SKC" || s.sky_cover == "NSC" || s.sky_cover == "NCD" {
3000.0
} else if s.sky_cover == "VV" {
0.0
@@ -515,9 +564,40 @@ impl Metar {
return missing_metar_icaos;
}
async fn get_remote_metars(icaos: String) -> Vec<Metar> {
async fn get_remote_metars(icaos: Vec<String>) -> Vec<Metar> {
let gov_api_url = std::env::var("GOV_API_URL").expect("GOV_API_URL must be set");
let url = format!("{}/metar.php?ids={}", gov_api_url, icaos);
// 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<Metar> = vec![];
for icao_chunk in icao_chunks {
let url = format!("{}/metar.php?ids={}", gov_api_url, icao_chunk);
let mut m = match reqwest::get(url).await {
Ok(r) => match r.text().await {
Ok(r) => {
let metar_chunk = r.trim().split("\n").filter(|m| !m.trim().is_empty()).collect();
match Metar::parse(metar_chunk) {
Ok(m) => m,
Err(err) => {
warn!("{}", err);
return metars;
}
}
},
Err(err) => {
warn!("Unable to parse METAR request: {}", err);
return metars;
}
},
Err(err) => {
warn!("Unable to get METAR request: {}", err);
return metars;
}
};
metars.append(&mut m);
}
let icaos_string = icaos.join(",");
let url = format!("{}/metar.php?ids={}", gov_api_url, icaos_string);
match reqwest::get(url).await {
Ok(r) => match r.text().await {
Ok(r) => {
@@ -526,18 +606,18 @@ impl Metar {
Ok(m) => m,
Err(err) => {
warn!("{}", err);
vec![]
return metars;
}
}
},
Err(err) => {
warn!("Unable to parse METAR request: {}", err);
vec![]
return metars;
}
},
Err(err) => {
warn!("Unable to get METAR request: {}", err);
vec![]
return metars;
}
}
}
@@ -591,7 +671,7 @@ impl Metar {
Err(_) => {}
}
});
let mut missing_metars = Self::get_remote_metars(missing_icaos_string.join(",")).await;
let mut missing_metars = Self::get_remote_metars(missing_icaos_string).await;
if missing_metars.len() > 0 {
let insert_metars = Self::to_insert(&missing_metars);
match InsertMetar::insert(&insert_metars) {

View File

@@ -48,15 +48,16 @@ export default function MapTiles() {
async function updateAirports(bounds: LatLngBounds) {
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();
console.log('zoom', zoom)
const { data: airportData } = await getAirports({
bounds: {
northEast: { lat: ne.lat, lon: ne.lng },
southWest: { lat: sw.lat, lon: sw.lng }
},
categories: ['large_airport'],
categories: ['large_airport', 'medium_airport', 'small_airport'],
order_field: AirportOrderField.CATEGORY,
order_by: 'asc',
limit: 250,
limit: zoom < 4 ? 200 : 100,
page: 1
});
const { data: metars } = await getMetars(airportData.map((a) => a.icao));
@@ -94,7 +95,7 @@ export default function MapTiles() {
} else if (airport.latest_metar?.flight_category == 'UNKN') {
return innerIcon({ tag: 'U', color: 'black', size: 'xs' });
} else {
return innerIcon({tag: ' ', color: 'black', size: 'xs' });
return innerIcon({tag: ' ', color: 'grey', size: 'xs' });
}
}