Updated airport data format, dataset, and queries
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,11 @@ CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE TABLE IF NOT EXISTS airports (
|
||||
icao TEXT PRIMARY KEY NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
elevation_ft REAL NOT NULL,
|
||||
iso_country TEXT NOT NULL,
|
||||
iso_region TEXT NOT NULL,
|
||||
municipality TEXT NOT NULL,
|
||||
gps_code TEXT NOT NULL,
|
||||
iata_code TEXT NOT NULL,
|
||||
local_code TEXT NOT NULL,
|
||||
point GEOMETRY(POINT,4326) NOT NULL,
|
||||
|
||||
@@ -4,24 +4,24 @@ use crate::db;
|
||||
use crate::error_handler::ServiceError;
|
||||
use crate::db::schema::airports;
|
||||
use diesel::prelude::*;
|
||||
use diesel::sql_query;
|
||||
use log::error;
|
||||
use postgis_diesel::types::*;
|
||||
use postgis_diesel::functions::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Airport {
|
||||
pub icao: String,
|
||||
pub category: String,
|
||||
pub full_name: String,
|
||||
pub elevation_ft: Option<i32>,
|
||||
pub name: String,
|
||||
pub elevation_ft: f32,
|
||||
pub iso_country: String,
|
||||
pub iso_region: String,
|
||||
pub municipality: String,
|
||||
pub gps_code: String,
|
||||
pub iata_code: String,
|
||||
pub local_code: String,
|
||||
pub point: Point,
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
pub has_tower: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@ impl Into<QueryAirport> for Airport {
|
||||
return QueryAirport {
|
||||
icao: self.icao.clone(),
|
||||
category: self.category.clone(),
|
||||
full_name: self.full_name.clone(),
|
||||
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(),
|
||||
gps_code: self.gps_code.clone(),
|
||||
iata_code: self.iata_code.clone(),
|
||||
local_code: self.local_code.clone(),
|
||||
point: self.point.clone(),
|
||||
point: Point::new(self.longitude, self.latitude, Some(4326)),
|
||||
data: match serde_json::to_value(&self) {
|
||||
Ok(d) => d,
|
||||
Err(err) => {
|
||||
@@ -60,11 +60,11 @@ impl From<QueryAirport> for Airport {
|
||||
pub struct QueryAirport {
|
||||
pub icao: String,
|
||||
pub category: String,
|
||||
pub full_name: String,
|
||||
pub name: String,
|
||||
pub elevation_ft: f32,
|
||||
pub iso_country: String,
|
||||
pub iso_region: String,
|
||||
pub municipality: String,
|
||||
pub gps_code: String,
|
||||
pub iata_code: String,
|
||||
pub local_code: String,
|
||||
pub point: Point,
|
||||
@@ -75,7 +75,7 @@ pub struct QueryAirport {
|
||||
pub struct QueryFilters {
|
||||
pub search: Option<String>,
|
||||
pub bounds: Option<Polygon<Point>>,
|
||||
pub category: Option<String>,
|
||||
pub categories: Option<Vec<String>>,
|
||||
pub order_field: Option<QueryOrderField>,
|
||||
pub order_by: Option<QueryOrderBy>
|
||||
}
|
||||
@@ -85,7 +85,7 @@ impl Default for QueryFilters {
|
||||
QueryFilters {
|
||||
search: None,
|
||||
bounds: None,
|
||||
category: None,
|
||||
categories: None,
|
||||
order_field: None,
|
||||
order_by: None
|
||||
}
|
||||
@@ -117,7 +117,6 @@ pub enum QueryOrderField {
|
||||
Country,
|
||||
Region,
|
||||
Municipality,
|
||||
GPS,
|
||||
Iata,
|
||||
Local,
|
||||
}
|
||||
@@ -132,7 +131,6 @@ impl FromStr for QueryOrderField {
|
||||
"iso_country" => Ok(QueryOrderField::Country),
|
||||
"iso_region" => Ok(QueryOrderField::Region),
|
||||
"municipality" => Ok(QueryOrderField::Municipality),
|
||||
"gps_code" => Ok(QueryOrderField::GPS),
|
||||
"iata_code" => Ok(QueryOrderField::Iata),
|
||||
"local_code" => Ok(QueryOrderField::Local),
|
||||
_ => Err(())
|
||||
@@ -143,94 +141,96 @@ impl FromStr for QueryOrderField {
|
||||
impl QueryAirport {
|
||||
pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> Result<Vec<Self>, ServiceError> {
|
||||
let mut conn = db::connection()?;
|
||||
|
||||
let mut query = airports::table.limit(limit as i64).into_boxed();
|
||||
// Limit query to page and limit
|
||||
let offset = (page - 1) * limit;
|
||||
query = query.offset(offset as i64);
|
||||
|
||||
if let Some(bounds) = &filters.bounds {
|
||||
query = query.filter(st_contains(bounds, airports::point));
|
||||
}
|
||||
if let Some(category) = &filters.category {
|
||||
query = query.filter(airports::category.eq(category));
|
||||
}
|
||||
if let Some(search) = &filters.search {
|
||||
query = query.filter(
|
||||
airports::icao.ilike(format!("%{}%", search))
|
||||
.or(airports::full_name.ilike(format!("%{}%", search)))
|
||||
.or(airports::iso_country.ilike(format!("%{}%", search)))
|
||||
.or(airports::iso_region.ilike(format!("%{}%", search)))
|
||||
.or(airports::municipality.ilike(format!("%{}%", search)))
|
||||
.or(airports::gps_code.ilike(format!("%{}%", search)))
|
||||
.or(airports::iata_code.ilike(format!("%{}%", search)))
|
||||
.or(airports::local_code.ilike(format!("%{}%", search)))
|
||||
)
|
||||
}
|
||||
let mut query: String = "SELECT * FROM airports".to_string();
|
||||
query = format!("{} {}", query, QueryAirport::build_filter_query(&filters)?);
|
||||
|
||||
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 => query.order(airports::icao.asc()),
|
||||
QueryOrderField::Name => query.order(airports::full_name.asc()),
|
||||
QueryOrderField::Category => query.order(airports::category.asc()),
|
||||
QueryOrderField::Country => query.order(airports::iso_country.asc()),
|
||||
QueryOrderField::Region => query.order(airports::iso_region.asc()),
|
||||
QueryOrderField::Municipality => query.order(airports::municipality.asc()),
|
||||
QueryOrderField::GPS => query.order(airports::gps_code.asc()),
|
||||
QueryOrderField::Iata => query.order(airports::iata_code.asc()),
|
||||
QueryOrderField::Local => query.order(airports::local_code.asc()),
|
||||
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),
|
||||
};
|
||||
};
|
||||
},
|
||||
QueryOrderBy::Desc => {
|
||||
if let Some(order_field) = &filters.order_field {
|
||||
query = match order_field {
|
||||
QueryOrderField::Icao => query.order(airports::icao.desc()),
|
||||
QueryOrderField::Name => query.order(airports::full_name.desc()),
|
||||
QueryOrderField::Category => query.order(airports::category.desc()),
|
||||
QueryOrderField::Country => query.order(airports::iso_country.desc()),
|
||||
QueryOrderField::Region => query.order(airports::iso_region.desc()),
|
||||
QueryOrderField::Municipality => query.order(airports::municipality.desc()),
|
||||
QueryOrderField::GPS => query.order(airports::gps_code.desc()),
|
||||
QueryOrderField::Iata => query.order(airports::iata_code.desc()),
|
||||
QueryOrderField::Local => query.order(airports::local_code.desc()),
|
||||
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),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
let airports: Vec<QueryAirport> = query.load::<QueryAirport>(&mut conn)?;
|
||||
// Limit query to page and limit
|
||||
query = format!("{} LIMIT {} OFFSET {}", query, limit, (page - 1) * limit);
|
||||
|
||||
let airports: Vec<QueryAirport> = match sql_query(query).load(&mut conn) {
|
||||
Ok(a) => a,
|
||||
Err(err) => return Err(ServiceError { status: 500, message: format!("{}", err) })
|
||||
};
|
||||
Ok(airports)
|
||||
}
|
||||
|
||||
pub fn get_count(filters: &QueryFilters) -> Result<i64, ServiceError> {
|
||||
let mut conn = db::connection()?;
|
||||
let mut query = airports::table.count().into_boxed();
|
||||
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,
|
||||
Err(err) => return Err(ServiceError { status: 500, message: format!("{}", err) })
|
||||
};
|
||||
return Ok(count);
|
||||
}
|
||||
|
||||
// TODO: Unsafe query, need to sanitize inputs
|
||||
fn build_filter_query(filters: &QueryFilters) -> Result<String, ServiceError> {
|
||||
let mut query = "".to_string();
|
||||
let mut parts: Vec<String> = vec![];
|
||||
|
||||
if let Some(bounds) = &filters.bounds {
|
||||
query = query.filter(st_contains(bounds, airports::point));
|
||||
// convert bounds to a WKT polygon
|
||||
if bounds.rings.len() > 1 {
|
||||
return Err(ServiceError { status: 400, message: "Only one polygon is allowed".to_string() })
|
||||
} else {
|
||||
let mut points: Vec<String> = vec![];
|
||||
bounds.rings.iter().for_each(|ring| {
|
||||
ring.iter().for_each(|point| {
|
||||
points.push(format!("{} {}", point.get_x(), point.get_y()));
|
||||
});
|
||||
});
|
||||
let bounds = format!("POLYGON(({}))", points.join(","));
|
||||
parts.push(format!("ST_Contains(ST_GeomFromText('{}', 4326), point)", bounds));
|
||||
}
|
||||
}
|
||||
if let Some(category) = &filters.category {
|
||||
query = query.filter(airports::category.eq(category));
|
||||
if let Some(categories) = &filters.categories {
|
||||
parts.push(format!("({})", categories.iter().map(|category| format!("category = '{}'", category)).collect::<Vec<String>>().join(" OR ")));
|
||||
}
|
||||
if let Some(search) = &filters.search {
|
||||
query = query.filter(
|
||||
airports::icao.ilike(format!("%{}%", search))
|
||||
.or(airports::full_name.ilike(format!("%{}%", search)))
|
||||
.or(airports::iso_country.ilike(format!("%{}%", search)))
|
||||
.or(airports::iso_region.ilike(format!("%{}%", search)))
|
||||
.or(airports::municipality.ilike(format!("%{}%", search)))
|
||||
.or(airports::gps_code.ilike(format!("%{}%", search)))
|
||||
.or(airports::iata_code.ilike(format!("%{}%", search)))
|
||||
.or(airports::local_code.ilike(format!("%{}%", search)))
|
||||
)
|
||||
let search_strs = vec!["icao", "name", "iso_country", "iso_region", "municipality", "iata_code", "local_code"];
|
||||
parts.push(format!("({})", search_strs.iter().map(|s| format!("{} ILIKE '%{}%'", s, search)).collect::<Vec<String>>().join(" OR ")));
|
||||
}
|
||||
|
||||
let count: i64 = query.get_result(&mut conn)?;
|
||||
return Ok(count);
|
||||
if parts.len() > 0 {
|
||||
query = format!("{} WHERE {}", query, parts.join(" AND "));
|
||||
}
|
||||
|
||||
return Ok(query);
|
||||
}
|
||||
|
||||
pub fn find(icao: String) -> Result<Self, ServiceError> {
|
||||
|
||||
@@ -12,7 +12,7 @@ use serde::{Serialize, Deserialize};
|
||||
struct GetAllParameters {
|
||||
search: Option<String>,
|
||||
bounds: Option<String>,
|
||||
category: Option<String>,
|
||||
categories: Option<String>,
|
||||
order_field: Option<String>,
|
||||
order_by: Option<String>,
|
||||
limit: Option<i32>,
|
||||
@@ -69,7 +69,10 @@ async fn get_all(req: HttpRequest) -> HttpResponse {
|
||||
let params = web::Query::<GetAllParameters>::from_query(req.query_string()).unwrap();
|
||||
let mut filters = QueryFilters::default();
|
||||
filters.search = params.search.clone();
|
||||
filters.category = params.category.clone();
|
||||
filters.categories = match ¶ms.categories {
|
||||
Some(c) => Some(c.split(",").map(|s| s.to_string()).collect()),
|
||||
None => None
|
||||
};
|
||||
filters.bounds = match ¶ms.bounds {
|
||||
Some(b) => {
|
||||
let bounds: Vec<&str> = b.split(",").collect();
|
||||
|
||||
@@ -4,11 +4,11 @@ diesel::table! {
|
||||
airports (icao) {
|
||||
icao -> Text,
|
||||
category -> Text,
|
||||
full_name -> Text,
|
||||
name -> Text,
|
||||
elevation_ft -> Float,
|
||||
iso_country -> Text,
|
||||
iso_region -> Text,
|
||||
municipality -> Text,
|
||||
gps_code -> Text,
|
||||
iata_code -> Text,
|
||||
local_code -> Text,
|
||||
point -> Geometry,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -195,11 +195,11 @@ impl Metar {
|
||||
break;
|
||||
}
|
||||
// Report Modifiers
|
||||
if metar_parts[0] == "AUTO" {
|
||||
if !metar_parts.is_empty() && metar_parts[0] == "AUTO" {
|
||||
metar.quality_control_flags.auto = Some(true);
|
||||
metar_parts.remove(0);
|
||||
}
|
||||
if metar_parts[0] == "COR" {
|
||||
if !metar_parts.is_empty() && metar_parts[0] == "COR" {
|
||||
metar.quality_control_flags.corrected = Some(true);
|
||||
metar_parts.remove(0);
|
||||
}
|
||||
@@ -207,14 +207,14 @@ impl Metar {
|
||||
// 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();
|
||||
if wind_re.is_match(metar_parts[0]) {
|
||||
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::<i32>().unwrap());
|
||||
} else if wind_gust_re.is_match(metar_parts[0]) {
|
||||
} 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];
|
||||
@@ -228,14 +228,14 @@ impl Metar {
|
||||
|
||||
// Variable Wind Direction
|
||||
let variable_wind_re = regex::Regex::new(r"^[0-9]{3}V[0-9]{3}$").unwrap();
|
||||
if variable_wind_re.is_match(metar_parts[0]) {
|
||||
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").unwrap();
|
||||
if visibility_re.is_match(metar_parts[0]) {
|
||||
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("/") {
|
||||
@@ -253,7 +253,7 @@ impl Metar {
|
||||
visibility_str.to_string()
|
||||
};
|
||||
metar.visibility_statute_mi = Some(visibility);
|
||||
} else if metar_parts[0].parse::<f64>().is_ok() && metar_parts.len() > 1 && visibility_re.is_match(metar_parts[1]) {
|
||||
} 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>().unwrap();
|
||||
metar_parts.remove(0);
|
||||
let visibility_parts: Vec<&str> = metar_parts[0].split("/").collect();
|
||||
@@ -273,7 +273,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 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();
|
||||
@@ -297,14 +297,14 @@ impl Metar {
|
||||
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]) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Sky Condition
|
||||
let sky_condition_re = regex::Regex::new(r"^(?:CLR|SKC|(?:FEW|SCT|BKN|OVC|VV)([0-9]{3})?)$").unwrap();
|
||||
while sky_condition_re.is_match(metar_parts[0]) {
|
||||
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();
|
||||
@@ -318,7 +318,7 @@ impl Metar {
|
||||
|
||||
// Temperature and Dewpoint
|
||||
let temp_re = regex::Regex::new(r"^(?:M?[0-9]{2})?/(?:M?[0-9]{2})?$").unwrap();
|
||||
if temp_re.is_match(metar_parts[0]) {
|
||||
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();
|
||||
@@ -360,7 +360,7 @@ impl Metar {
|
||||
|
||||
// Altimeter
|
||||
let altim_re = regex::Regex::new(r"^A[0-9]{4}$").unwrap();
|
||||
if altim_re.is_match(metar_parts[0]) {
|
||||
if !metar_parts.is_empty() && altim_re.is_match(metar_parts[0]) {
|
||||
let altim = metar_parts[0];
|
||||
metar_parts.remove(0);
|
||||
metar.altim_in_hg = Some(altim[1..altim.len()].parse::<f64>().unwrap() / 100.0);
|
||||
|
||||
Reference in New Issue
Block a user