Updated airport data format, dataset, and queries
This commit is contained in:
@@ -16,5 +16,7 @@ The following resources were used to help decode METARS.
|
|||||||
- [Metar Decode (NPS EDU)](https://met.nps.edu/~bcreasey/mr3222/files/helpful/DecodeMETAR-TAF.html)
|
- [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)
|
- [Weather Phenomena](http://www.moratech.com/aviation/metar-class/metar-pg9-ww.html)
|
||||||
|
|
||||||
|
- Airport dataset is based on [mborsetti/airportsdata](https://github.com/mborsetti/airportsdata)
|
||||||
|
|
||||||
## OpenMapTiles
|
## OpenMapTiles
|
||||||
[Generate Vector Tiles](https://openmaptiles.org/docs/generate/generate-openmaptiles/)
|
[Generate Vector Tiles](https://openmaptiles.org/docs/generate/generate-openmaptiles/)
|
||||||
63
data/airports.py
Normal file
63
data/airports.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from datetime import date
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Load the airports.csv file from the web
|
||||||
|
url = 'https://davidmegginson.github.io/ourairports-data/airports.csv'
|
||||||
|
df = pd.read_csv(url, index_col=0)
|
||||||
|
|
||||||
|
# convert the dataframe to a dictionary
|
||||||
|
airports = df.to_dict('index')
|
||||||
|
formated_airports = {}
|
||||||
|
for airport in airports:
|
||||||
|
|
||||||
|
category = airports[airport]['type']
|
||||||
|
if pd.isnull(category) or category == 'nan' or category == 'NAN':
|
||||||
|
category = 'UNKNOWN'
|
||||||
|
|
||||||
|
elevation = airports[airport]['elevation_ft']
|
||||||
|
if pd.isnull(elevation) or elevation == 'nan' or elevation == 'NAN':
|
||||||
|
elevation = 0
|
||||||
|
|
||||||
|
country = airports[airport]['iso_country']
|
||||||
|
if pd.isnull(country) or country == 'nan' or country == 'NAN':
|
||||||
|
country = 'UNKNOWN'
|
||||||
|
|
||||||
|
region = airports[airport]['iso_region']
|
||||||
|
if pd.isnull(region) or region == 'nan' or region == 'NAN':
|
||||||
|
region = 'UNKNOWN'
|
||||||
|
|
||||||
|
municipality = airports[airport]['municipality']
|
||||||
|
if pd.isnull(municipality) or municipality == 'nan' or municipality == 'NAN':
|
||||||
|
municipality = 'UNKNOWN'
|
||||||
|
|
||||||
|
iata = airports[airport]['iata_code']
|
||||||
|
if pd.isnull(iata) or iata == 'nan' or iata == 'NAN':
|
||||||
|
iata = ''
|
||||||
|
|
||||||
|
local = airports[airport]['local_code']
|
||||||
|
if pd.isnull(local) or local == 'nan' or local == 'NAN':
|
||||||
|
local = ''
|
||||||
|
|
||||||
|
formated_airports[airport] = {
|
||||||
|
'icao': airports[airport]['ident'],
|
||||||
|
'category': category,
|
||||||
|
'name': airports[airport]['name'],
|
||||||
|
'elevation_ft': elevation,
|
||||||
|
'iso_country': country,
|
||||||
|
'iso_region': region,
|
||||||
|
'municipality': municipality,
|
||||||
|
'iata_code': iata,
|
||||||
|
'local_code': local,
|
||||||
|
'latitude': airports[airport]['latitude_deg'],
|
||||||
|
'longitude': airports[airport]['longitude_deg']
|
||||||
|
}
|
||||||
|
|
||||||
|
# convert the dictionary to a list of dictionaries
|
||||||
|
formated_airports = list(formated_airports.values())
|
||||||
|
|
||||||
|
# convert the list of dictionaries to a json file
|
||||||
|
today = date.today()
|
||||||
|
date = today.strftime("%Y-%m-%d")
|
||||||
|
with open(f'airports_{date}.json', 'wb') as file:
|
||||||
|
file.write(json.dumps(formated_airports).encode('utf-8'))
|
||||||
1
data/airports_2023-12-18.json
Normal file
1
data/airports_2023-12-18.json
Normal file
File diff suppressed because one or more lines are too long
0
data/requirements.txt
Normal file
0
data/requirements.txt
Normal file
@@ -2,11 +2,11 @@ CREATE EXTENSION IF NOT EXISTS postgis;
|
|||||||
CREATE TABLE IF NOT EXISTS airports (
|
CREATE TABLE IF NOT EXISTS airports (
|
||||||
icao TEXT PRIMARY KEY NOT NULL,
|
icao TEXT PRIMARY KEY NOT NULL,
|
||||||
category TEXT 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_country TEXT NOT NULL,
|
||||||
iso_region TEXT NOT NULL,
|
iso_region TEXT NOT NULL,
|
||||||
municipality TEXT NOT NULL,
|
municipality TEXT NOT NULL,
|
||||||
gps_code TEXT NOT NULL,
|
|
||||||
iata_code TEXT NOT NULL,
|
iata_code TEXT NOT NULL,
|
||||||
local_code TEXT NOT NULL,
|
local_code TEXT NOT NULL,
|
||||||
point GEOMETRY(POINT,4326) NOT NULL,
|
point GEOMETRY(POINT,4326) NOT NULL,
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ use crate::db;
|
|||||||
use crate::error_handler::ServiceError;
|
use crate::error_handler::ServiceError;
|
||||||
use crate::db::schema::airports;
|
use crate::db::schema::airports;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
use diesel::sql_query;
|
||||||
use log::error;
|
use log::error;
|
||||||
use postgis_diesel::types::*;
|
use postgis_diesel::types::*;
|
||||||
use postgis_diesel::functions::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Airport {
|
pub struct Airport {
|
||||||
pub icao: String,
|
pub icao: String,
|
||||||
pub category: String,
|
pub category: String,
|
||||||
pub full_name: String,
|
pub name: String,
|
||||||
pub elevation_ft: Option<i32>,
|
pub elevation_ft: f32,
|
||||||
pub iso_country: String,
|
pub iso_country: String,
|
||||||
pub iso_region: String,
|
pub iso_region: String,
|
||||||
pub municipality: String,
|
pub municipality: String,
|
||||||
pub gps_code: String,
|
|
||||||
pub iata_code: String,
|
pub iata_code: String,
|
||||||
pub local_code: String,
|
pub local_code: String,
|
||||||
pub point: Point,
|
pub latitude: f64,
|
||||||
|
pub longitude: f64,
|
||||||
pub has_tower: Option<bool>,
|
pub has_tower: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,14 +30,14 @@ impl Into<QueryAirport> for Airport {
|
|||||||
return QueryAirport {
|
return QueryAirport {
|
||||||
icao: self.icao.clone(),
|
icao: self.icao.clone(),
|
||||||
category: self.category.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_country: self.iso_country.clone(),
|
||||||
iso_region: self.iso_region.clone(),
|
iso_region: self.iso_region.clone(),
|
||||||
municipality: self.municipality.clone(),
|
municipality: self.municipality.clone(),
|
||||||
gps_code: self.gps_code.clone(),
|
|
||||||
iata_code: self.iata_code.clone(),
|
iata_code: self.iata_code.clone(),
|
||||||
local_code: self.local_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) {
|
data: match serde_json::to_value(&self) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -60,11 +60,11 @@ impl From<QueryAirport> for Airport {
|
|||||||
pub struct QueryAirport {
|
pub struct QueryAirport {
|
||||||
pub icao: String,
|
pub icao: String,
|
||||||
pub category: String,
|
pub category: String,
|
||||||
pub full_name: String,
|
pub name: String,
|
||||||
|
pub elevation_ft: f32,
|
||||||
pub iso_country: String,
|
pub iso_country: String,
|
||||||
pub iso_region: String,
|
pub iso_region: String,
|
||||||
pub municipality: String,
|
pub municipality: String,
|
||||||
pub gps_code: String,
|
|
||||||
pub iata_code: String,
|
pub iata_code: String,
|
||||||
pub local_code: String,
|
pub local_code: String,
|
||||||
pub point: Point,
|
pub point: Point,
|
||||||
@@ -75,7 +75,7 @@ pub struct QueryAirport {
|
|||||||
pub struct QueryFilters {
|
pub struct QueryFilters {
|
||||||
pub search: Option<String>,
|
pub search: Option<String>,
|
||||||
pub bounds: Option<Polygon<Point>>,
|
pub bounds: Option<Polygon<Point>>,
|
||||||
pub category: Option<String>,
|
pub categories: Option<Vec<String>>,
|
||||||
pub order_field: Option<QueryOrderField>,
|
pub order_field: Option<QueryOrderField>,
|
||||||
pub order_by: Option<QueryOrderBy>
|
pub order_by: Option<QueryOrderBy>
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ impl Default for QueryFilters {
|
|||||||
QueryFilters {
|
QueryFilters {
|
||||||
search: None,
|
search: None,
|
||||||
bounds: None,
|
bounds: None,
|
||||||
category: None,
|
categories: None,
|
||||||
order_field: None,
|
order_field: None,
|
||||||
order_by: None
|
order_by: None
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,6 @@ pub enum QueryOrderField {
|
|||||||
Country,
|
Country,
|
||||||
Region,
|
Region,
|
||||||
Municipality,
|
Municipality,
|
||||||
GPS,
|
|
||||||
Iata,
|
Iata,
|
||||||
Local,
|
Local,
|
||||||
}
|
}
|
||||||
@@ -132,7 +131,6 @@ impl FromStr for QueryOrderField {
|
|||||||
"iso_country" => Ok(QueryOrderField::Country),
|
"iso_country" => Ok(QueryOrderField::Country),
|
||||||
"iso_region" => Ok(QueryOrderField::Region),
|
"iso_region" => Ok(QueryOrderField::Region),
|
||||||
"municipality" => Ok(QueryOrderField::Municipality),
|
"municipality" => Ok(QueryOrderField::Municipality),
|
||||||
"gps_code" => Ok(QueryOrderField::GPS),
|
|
||||||
"iata_code" => Ok(QueryOrderField::Iata),
|
"iata_code" => Ok(QueryOrderField::Iata),
|
||||||
"local_code" => Ok(QueryOrderField::Local),
|
"local_code" => Ok(QueryOrderField::Local),
|
||||||
_ => Err(())
|
_ => Err(())
|
||||||
@@ -143,94 +141,96 @@ impl FromStr for QueryOrderField {
|
|||||||
impl QueryAirport {
|
impl QueryAirport {
|
||||||
pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> Result<Vec<Self>, ServiceError> {
|
pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> Result<Vec<Self>, ServiceError> {
|
||||||
let mut conn = db::connection()?;
|
let mut conn = db::connection()?;
|
||||||
|
let mut query: String = "SELECT * FROM airports".to_string();
|
||||||
let mut query = airports::table.limit(limit as i64).into_boxed();
|
query = format!("{} {}", query, QueryAirport::build_filter_query(&filters)?);
|
||||||
// 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)))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(order_by) = &filters.order_by {
|
if let Some(order_by) = &filters.order_by {
|
||||||
match order_by {
|
match order_by {
|
||||||
QueryOrderBy::Asc => {
|
QueryOrderBy::Asc => {
|
||||||
if let Some(order_field) = &filters.order_field {
|
if let Some(order_field) = &filters.order_field {
|
||||||
query = match order_field {
|
query = match order_field {
|
||||||
QueryOrderField::Icao => query.order(airports::icao.asc()),
|
QueryOrderField::Icao => format!("{} ORDER BY icao ASC", query),
|
||||||
QueryOrderField::Name => query.order(airports::full_name.asc()),
|
QueryOrderField::Name => format!("{} ORDER BY name ASC", query),
|
||||||
QueryOrderField::Category => query.order(airports::category.asc()),
|
QueryOrderField::Category => format!("{} ORDER BY category ASC", query),
|
||||||
QueryOrderField::Country => query.order(airports::iso_country.asc()),
|
QueryOrderField::Country => format!("{} ORDER BY iso_country ASC", query),
|
||||||
QueryOrderField::Region => query.order(airports::iso_region.asc()),
|
QueryOrderField::Region => format!("{} ORDER BY iso_region ASC", query),
|
||||||
QueryOrderField::Municipality => query.order(airports::municipality.asc()),
|
QueryOrderField::Municipality => format!("{} ORDER BY municipality ASC", query),
|
||||||
QueryOrderField::GPS => query.order(airports::gps_code.asc()),
|
QueryOrderField::Iata => format!("{} ORDER BY iata_code ASC", query),
|
||||||
QueryOrderField::Iata => query.order(airports::iata_code.asc()),
|
QueryOrderField::Local => format!("{} ORDER BY local_code ASC", query),
|
||||||
QueryOrderField::Local => query.order(airports::local_code.asc()),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
QueryOrderBy::Desc => {
|
QueryOrderBy::Desc => {
|
||||||
if let Some(order_field) = &filters.order_field {
|
if let Some(order_field) = &filters.order_field {
|
||||||
query = match order_field {
|
query = match order_field {
|
||||||
QueryOrderField::Icao => query.order(airports::icao.desc()),
|
QueryOrderField::Icao => format!("{} ORDER BY icao DESC", query),
|
||||||
QueryOrderField::Name => query.order(airports::full_name.desc()),
|
QueryOrderField::Name => format!("{} ORDER BY name DESC", query),
|
||||||
QueryOrderField::Category => query.order(airports::category.desc()),
|
QueryOrderField::Category => format!("{} ORDER BY category DESC", query),
|
||||||
QueryOrderField::Country => query.order(airports::iso_country.desc()),
|
QueryOrderField::Country => format!("{} ORDER BY iso_country DESC", query),
|
||||||
QueryOrderField::Region => query.order(airports::iso_region.desc()),
|
QueryOrderField::Region => format!("{} ORDER BY iso_region DESC", query),
|
||||||
QueryOrderField::Municipality => query.order(airports::municipality.desc()),
|
QueryOrderField::Municipality => format!("{} ORDER BY municipality DESC", query),
|
||||||
QueryOrderField::GPS => query.order(airports::gps_code.desc()),
|
QueryOrderField::Iata => format!("{} ORDER BY iata_code DESC", query),
|
||||||
QueryOrderField::Iata => query.order(airports::iata_code.desc()),
|
QueryOrderField::Local => format!("{} ORDER BY local_code DESC", query),
|
||||||
QueryOrderField::Local => query.order(airports::local_code.desc()),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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)
|
Ok(airports)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_count(filters: &QueryFilters) -> Result<i64, ServiceError> {
|
pub fn get_count(filters: &QueryFilters) -> Result<i64, ServiceError> {
|
||||||
let mut conn = db::connection()?;
|
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 {
|
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 {
|
if let Some(categories) = &filters.categories {
|
||||||
query = query.filter(airports::category.eq(category));
|
parts.push(format!("({})", categories.iter().map(|category| format!("category = '{}'", category)).collect::<Vec<String>>().join(" OR ")));
|
||||||
}
|
}
|
||||||
if let Some(search) = &filters.search {
|
if let Some(search) = &filters.search {
|
||||||
query = query.filter(
|
let search_strs = vec!["icao", "name", "iso_country", "iso_region", "municipality", "iata_code", "local_code"];
|
||||||
airports::icao.ilike(format!("%{}%", search))
|
parts.push(format!("({})", search_strs.iter().map(|s| format!("{} ILIKE '%{}%'", s, search)).collect::<Vec<String>>().join(" OR ")));
|
||||||
.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 count: i64 = query.get_result(&mut conn)?;
|
if parts.len() > 0 {
|
||||||
return Ok(count);
|
query = format!("{} WHERE {}", query, parts.join(" AND "));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find(icao: String) -> Result<Self, ServiceError> {
|
pub fn find(icao: String) -> Result<Self, ServiceError> {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use serde::{Serialize, Deserialize};
|
|||||||
struct GetAllParameters {
|
struct GetAllParameters {
|
||||||
search: Option<String>,
|
search: Option<String>,
|
||||||
bounds: Option<String>,
|
bounds: Option<String>,
|
||||||
category: Option<String>,
|
categories: Option<String>,
|
||||||
order_field: Option<String>,
|
order_field: Option<String>,
|
||||||
order_by: Option<String>,
|
order_by: Option<String>,
|
||||||
limit: Option<i32>,
|
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 params = web::Query::<GetAllParameters>::from_query(req.query_string()).unwrap();
|
||||||
let mut filters = QueryFilters::default();
|
let mut filters = QueryFilters::default();
|
||||||
filters.search = params.search.clone();
|
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 {
|
filters.bounds = match ¶ms.bounds {
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
let bounds: Vec<&str> = b.split(",").collect();
|
let bounds: Vec<&str> = b.split(",").collect();
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ diesel::table! {
|
|||||||
airports (icao) {
|
airports (icao) {
|
||||||
icao -> Text,
|
icao -> Text,
|
||||||
category -> Text,
|
category -> Text,
|
||||||
full_name -> Text,
|
name -> Text,
|
||||||
|
elevation_ft -> Float,
|
||||||
iso_country -> Text,
|
iso_country -> Text,
|
||||||
iso_region -> Text,
|
iso_region -> Text,
|
||||||
municipality -> Text,
|
municipality -> Text,
|
||||||
gps_code -> Text,
|
|
||||||
iata_code -> Text,
|
iata_code -> Text,
|
||||||
local_code -> Text,
|
local_code -> Text,
|
||||||
point -> Geometry,
|
point -> Geometry,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,service=info"));
|
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "warn,service=info"));
|
||||||
db::init();
|
db::init();
|
||||||
scheduler::update_airports();
|
// scheduler::update_airports();
|
||||||
|
|
||||||
let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string());
|
let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string());
|
||||||
let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string());
|
let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string());
|
||||||
|
|||||||
@@ -195,11 +195,11 @@ impl Metar {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Report Modifiers
|
// Report Modifiers
|
||||||
if metar_parts[0] == "AUTO" {
|
if !metar_parts.is_empty() && metar_parts[0] == "AUTO" {
|
||||||
metar.quality_control_flags.auto = Some(true);
|
metar.quality_control_flags.auto = Some(true);
|
||||||
metar_parts.remove(0);
|
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.quality_control_flags.corrected = Some(true);
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
}
|
}
|
||||||
@@ -207,14 +207,14 @@ impl Metar {
|
|||||||
// Wind Direction and Speed
|
// Wind Direction and Speed
|
||||||
let wind_re = regex::Regex::new(r"^(?:[0-9]{3}|VRB)[0-9]{2}KT$").unwrap();
|
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_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];
|
let wind = metar_parts[0];
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
let wind_dir_degrees = &wind[0..3];
|
let wind_dir_degrees = &wind[0..3];
|
||||||
let wind_speed_kt = &wind[3..5];
|
let wind_speed_kt = &wind[3..5];
|
||||||
metar.wind_dir_degrees = Some(wind_dir_degrees.to_string());
|
metar.wind_dir_degrees = Some(wind_dir_degrees.to_string());
|
||||||
metar.wind_speed_kt = Some(wind_speed_kt.parse::<i32>().unwrap());
|
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];
|
let wind = metar_parts[0];
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
let wind_dir_degrees = &wind[0..3];
|
let wind_dir_degrees = &wind[0..3];
|
||||||
@@ -228,14 +228,14 @@ impl Metar {
|
|||||||
|
|
||||||
// Variable Wind Direction
|
// Variable Wind Direction
|
||||||
let variable_wind_re = regex::Regex::new(r"^[0-9]{3}V[0-9]{3}$").unwrap();
|
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.variable_wind_dir_degrees = Some(metar_parts[0].to_string());
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visibility
|
// 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();
|
||||||
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];
|
let visibility_str = &metar_parts[0][0..metar_parts[0].len() - 2];
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
let visibility: String = if visibility_str.contains("/") {
|
let visibility: String = if visibility_str.contains("/") {
|
||||||
@@ -253,7 +253,7 @@ impl Metar {
|
|||||||
visibility_str.to_string()
|
visibility_str.to_string()
|
||||||
};
|
};
|
||||||
metar.visibility_statute_mi = Some(visibility);
|
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();
|
let visibility_whole = metar_parts[0].parse::<f64>().unwrap();
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
let visibility_parts: Vec<&str> = metar_parts[0].split("/").collect();
|
let visibility_parts: Vec<&str> = metar_parts[0].split("/").collect();
|
||||||
@@ -273,7 +273,7 @@ impl Metar {
|
|||||||
// Runway Visual Range
|
// 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 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();
|
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];
|
let rvr_string = metar_parts[0];
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
let mut rvr = RunwayVisualRange::default();
|
let mut rvr = RunwayVisualRange::default();
|
||||||
@@ -297,14 +297,14 @@ impl Metar {
|
|||||||
let wx_descriptor = "(?:MI|PR|BC|DR|BL|SH|TS|FZ)?";
|
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_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();
|
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.weather_phenomena.push(metar_parts[0].to_string());
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sky Condition
|
// 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|(?: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];
|
let sky_condition_string = metar_parts[0];
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
let mut sky_condition = SkyCondition::default();
|
let mut sky_condition = SkyCondition::default();
|
||||||
@@ -318,7 +318,7 @@ impl Metar {
|
|||||||
|
|
||||||
// Temperature and Dewpoint
|
// Temperature and Dewpoint
|
||||||
let temp_re = regex::Regex::new(r"^(?:M?[0-9]{2})?/(?:M?[0-9]{2})?$").unwrap();
|
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];
|
let temp_string = metar_parts[0];
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
let temp_parts: Vec<&str> = temp_string.split("/").collect();
|
let temp_parts: Vec<&str> = temp_string.split("/").collect();
|
||||||
@@ -360,7 +360,7 @@ impl Metar {
|
|||||||
|
|
||||||
// Altimeter
|
// Altimeter
|
||||||
let altim_re = regex::Regex::new(r"^A[0-9]{4}$").unwrap();
|
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];
|
let altim = metar_parts[0];
|
||||||
metar_parts.remove(0);
|
metar_parts.remove(0);
|
||||||
metar.altim_in_hg = Some(altim[1..altim.len()].parse::<f64>().unwrap() / 100.0);
|
metar.altim_in_hg = Some(altim[1..altim.len()].parse::<f64>().unwrap() / 100.0);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export async function getAirport({ icao }: GetAirportProps): Promise<GetAirportR
|
|||||||
|
|
||||||
interface GetAirportsProps {
|
interface GetAirportsProps {
|
||||||
bounds?: Bounds;
|
bounds?: Bounds;
|
||||||
category?: string;
|
categories?: string[];
|
||||||
search?: string;
|
search?: string;
|
||||||
order_field?: AirportOrderField;
|
order_field?: AirportOrderField;
|
||||||
order_by?: 'asc' | 'desc';
|
order_by?: 'asc' | 'desc';
|
||||||
@@ -22,7 +22,7 @@ interface GetAirportsProps {
|
|||||||
|
|
||||||
export async function getAirports({
|
export async function getAirports({
|
||||||
bounds,
|
bounds,
|
||||||
category,
|
categories,
|
||||||
search,
|
search,
|
||||||
order_field,
|
order_field,
|
||||||
order_by,
|
order_by,
|
||||||
@@ -33,7 +33,7 @@ export async function getAirports({
|
|||||||
bounds: bounds
|
bounds: bounds
|
||||||
? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}`
|
? `${bounds?.northEast.lat},${bounds?.northEast.lon},${bounds?.southWest.lat},${bounds?.southWest.lon}`
|
||||||
: undefined,
|
: undefined,
|
||||||
category: category ?? undefined,
|
categories: categories ?? undefined,
|
||||||
search: search ?? undefined,
|
search: search ?? undefined,
|
||||||
order_field: order_field ?? undefined,
|
order_field: order_field ?? undefined,
|
||||||
order_by: order_by ?? undefined,
|
order_by: order_by ?? undefined,
|
||||||
@@ -63,11 +63,11 @@ export async function updateAirport({ airport }: { airport: Airport }): Promise<
|
|||||||
return response?.json() || { data: undefined };
|
return response?.json() || { data: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importAirports(payload: File): Promise<any> {
|
export async function importAirports(payload: File): Promise<boolean> {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('data', payload);
|
data.append('data', payload);
|
||||||
const response = await postRequest('airports/import', data, {
|
const response = await postRequest('airports/import', data, {
|
||||||
type: 'form'
|
type: 'form'
|
||||||
});
|
});
|
||||||
return response?.status == 200;
|
return response ? response.status === 200 : false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export enum AirportOrderField {
|
|||||||
ISO_COUNTRY = 'iso_country',
|
ISO_COUNTRY = 'iso_country',
|
||||||
ISO_REGION = 'iso_region',
|
ISO_REGION = 'iso_region',
|
||||||
MUNICIPALITY = 'municipality',
|
MUNICIPALITY = 'municipality',
|
||||||
GPS_CODE = 'gps_code',
|
|
||||||
IATA_CODE = 'iata_code',
|
IATA_CODE = 'iata_code',
|
||||||
LOCAL_CODE = 'local_code',
|
LOCAL_CODE = 'local_code',
|
||||||
}
|
}
|
||||||
@@ -44,19 +43,15 @@ export interface Coordinate {
|
|||||||
export interface Airport {
|
export interface Airport {
|
||||||
icao: string;
|
icao: string;
|
||||||
category: AirportCategory;
|
category: AirportCategory;
|
||||||
full_name: string;
|
name: string;
|
||||||
elevation_ft: number;
|
elevation_ft: number;
|
||||||
iso_country: string;
|
iso_country: string;
|
||||||
iso_region: string;
|
iso_region: string;
|
||||||
municipality: string;
|
municipality: string;
|
||||||
gps_code: string;
|
|
||||||
iata_code: string;
|
iata_code: string;
|
||||||
local_code: string;
|
local_code: string;
|
||||||
point: {
|
latitude: number;
|
||||||
x: number;
|
longitude: number;
|
||||||
y: number;
|
|
||||||
srid: number;
|
|
||||||
};
|
|
||||||
latest_metar?: Metar;
|
latest_metar?: Metar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function Page({ params }: { params: { icao: string } }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className=''>
|
<div className=''>
|
||||||
<h3 className=''>{airport.full_name}</h3>
|
<h3 className=''>{airport.name}</h3>
|
||||||
{metar && <SkyConditions metar={metar} />}
|
{metar && <SkyConditions metar={metar} />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import RecoilRootWrapper from '@app/recoil-root-wrapper';
|
import RecoilRootWrapper from '@app/recoil-root-wrapper';
|
||||||
import Sidebar from '@/components/Sidebar';
|
|
||||||
import Header from '@/components/Header';
|
import Header from '@/components/Header';
|
||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider } from '@mantine/core';
|
||||||
@@ -8,6 +7,7 @@ import { ModalsProvider } from '@mantine/modals';
|
|||||||
import 'styles/globals.css';
|
import 'styles/globals.css';
|
||||||
import 'styles/leaflet.css';
|
import 'styles/leaflet.css';
|
||||||
import '@mantine/core/styles.css';
|
import '@mantine/core/styles.css';
|
||||||
|
import { Notifications } from '@mantine/notifications';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Aviation Weather',
|
title: 'Aviation Weather',
|
||||||
@@ -23,15 +23,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
<title>Aviation Weather</title>
|
<title>Aviation Weather</title>
|
||||||
</head>
|
</head>
|
||||||
<body className={`${inter.className} wrapper h-full`}>
|
<body className={`${inter.className} wrapper h-full`}>
|
||||||
<RecoilRootWrapper>
|
<MantineProvider>
|
||||||
<MantineProvider>
|
<Notifications />
|
||||||
<ModalsProvider>
|
<ModalsProvider>
|
||||||
|
<RecoilRootWrapper>
|
||||||
<Header />
|
<Header />
|
||||||
<Sidebar />
|
|
||||||
{children}
|
{children}
|
||||||
</ModalsProvider>
|
</RecoilRootWrapper>
|
||||||
</MantineProvider>
|
</ModalsProvider>
|
||||||
</RecoilRootWrapper>
|
</MantineProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Text, Button, Card, Group, Pagination, Table, TextInput, rem, UnstyledB
|
|||||||
import { HiChevronUp, HiChevronDown, HiSelector } from "react-icons/hi";
|
import { HiChevronUp, HiChevronDown, HiSelector } from "react-icons/hi";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { CiSearch } from "react-icons/ci";
|
import { CiSearch } from "react-icons/ci";
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
|
||||||
|
|
||||||
export default function AirportTablePanel({ setAirport }: { setAirport: (airport: Airport) => void }) {
|
export default function AirportTablePanel({ setAirport }: { setAirport: (airport: Airport) => void }) {
|
||||||
@@ -37,12 +38,11 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport
|
|||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
<Table.Td>{airport.icao}</Table.Td>
|
<Table.Td>{airport.icao}</Table.Td>
|
||||||
<Table.Td>{airport.full_name}</Table.Td>
|
<Table.Td>{airport.name}</Table.Td>
|
||||||
<Table.Td>{airportCategoryToText(airport.category)}</Table.Td>
|
<Table.Td>{airportCategoryToText(airport.category)}</Table.Td>
|
||||||
<Table.Td>{airport.iso_country}</Table.Td>
|
<Table.Td>{airport.iso_country}</Table.Td>
|
||||||
<Table.Td>{airport.iso_region}</Table.Td>
|
<Table.Td>{airport.iso_region}</Table.Td>
|
||||||
<Table.Td>{airport.municipality}</Table.Td>
|
<Table.Td>{airport.municipality}</Table.Td>
|
||||||
<Table.Td>{airport.gps_code}</Table.Td>
|
|
||||||
<Table.Td>{airport.iata_code}</Table.Td>
|
<Table.Td>{airport.iata_code}</Table.Td>
|
||||||
<Table.Td>{airport.local_code}</Table.Td>
|
<Table.Td>{airport.local_code}</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
@@ -61,12 +61,11 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport
|
|||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th>ICAO</Table.Th>
|
<Table.Th>ICAO</Table.Th>
|
||||||
<Table.Th>Full Name</Table.Th>
|
<Table.Th>Name</Table.Th>
|
||||||
<Table.Th>Category</Table.Th>
|
<Table.Th>Category</Table.Th>
|
||||||
<Table.Th>ISO Country</Table.Th>
|
<Table.Th>ISO Country</Table.Th>
|
||||||
<Table.Th>ISO Region</Table.Th>
|
<Table.Th>ISO Region</Table.Th>
|
||||||
<Table.Th>Municipality</Table.Th>
|
<Table.Th>Municipality</Table.Th>
|
||||||
<Table.Th>GPS Code</Table.Th>
|
|
||||||
<Table.Th>IATA Code</Table.Th>
|
<Table.Th>IATA Code</Table.Th>
|
||||||
<Table.Th>Local Code</Table.Th>
|
<Table.Th>Local Code</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
@@ -83,8 +82,17 @@ export default function AirportTablePanel({ setAirport }: { setAirport: (airport
|
|||||||
<Space mr={'sm'}>
|
<Space mr={'sm'}>
|
||||||
<PanelFileButton accept={'.json'} onChange={async (payload) => {
|
<PanelFileButton accept={'.json'} onChange={async (payload) => {
|
||||||
if (payload instanceof File) {
|
if (payload instanceof File) {
|
||||||
await importAirports(payload);
|
const response = await importAirports(payload);
|
||||||
await getAirportData();
|
if (response) {
|
||||||
|
await getAirportData();
|
||||||
|
} else {
|
||||||
|
notifications.show({
|
||||||
|
title: `Failed to import airports`,
|
||||||
|
message: `Please try again.`,
|
||||||
|
color: 'red',
|
||||||
|
autoClose: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
Import
|
Import
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { getAirport, getAirports } from '@/api/airport';
|
import { getAirport, getAirports } from '@/api/airport';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { Autocomplete, Avatar, Button, Card, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core';
|
import { Autocomplete, Avatar, Button, Card, FileButton, Grid, Group, Menu, Text, UnstyledButton } from '@mantine/core';
|
||||||
import './header.css';
|
import './header.css';
|
||||||
import { refresh, refreshLoggedIn, logout } from '@/api/auth';
|
import { refresh, refreshLoggedIn, logout } from '@/api/auth';
|
||||||
@@ -17,6 +16,7 @@ import { favoritesState } from '@/state/user';
|
|||||||
import { coordinatesState, zoomState } from '@/state/map';
|
import { coordinatesState, zoomState } from '@/state/map';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]);
|
const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]);
|
||||||
const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']);
|
const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']);
|
||||||
@@ -24,7 +24,6 @@ export default function Header() {
|
|||||||
const [favorites, setFavorites] = useRecoilState(favoritesState);
|
const [favorites, setFavorites] = useRecoilState(favoritesState);
|
||||||
const [refreshId, setRefreshId] = useState<NodeJS.Timeout | undefined>(undefined);
|
const [refreshId, setRefreshId] = useState<NodeJS.Timeout | undefined>(undefined);
|
||||||
const [profilePicture, setProfilePicture] = useState<File | null>(null);
|
const [profilePicture, setProfilePicture] = useState<File | null>(null);
|
||||||
const router = useRouter();
|
|
||||||
const [coordinates, setCoordinates] = useRecoilState(coordinatesState);
|
const [coordinates, setCoordinates] = useRecoilState(coordinatesState);
|
||||||
const [zoom, setZoom] = useRecoilState(zoomState);
|
const [zoom, setZoom] = useRecoilState(zoomState);
|
||||||
|
|
||||||
@@ -49,6 +48,7 @@ export default function Header() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
async function onChange(value: string) {
|
async function onChange(value: string) {
|
||||||
@@ -58,7 +58,7 @@ export default function Header() {
|
|||||||
airportData.data.map((airport) => ({
|
airportData.data.map((airport) => ({
|
||||||
key: airport.icao,
|
key: airport.icao,
|
||||||
value: airport.icao,
|
value: airport.icao,
|
||||||
label: `${airport.icao} - ${airport.full_name}`
|
label: `${airport.icao} - ${airport.name}`
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ export default function Header() {
|
|||||||
async function onClick(value: string) {
|
async function onClick(value: string) {
|
||||||
const airport = await getAirport({ icao: value });
|
const airport = await getAirport({ icao: value });
|
||||||
if (airport) {
|
if (airport) {
|
||||||
setCoordinates({ lat: airport.data.point.y, lon: airport.data.point.x });
|
setCoordinates({ lat: airport.data.latitude, lon: airport.data.longitude });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +96,7 @@ export default function Header() {
|
|||||||
toggle={toggle}
|
toggle={toggle}
|
||||||
setFavorites={setFavorites}
|
setFavorites={setFavorites}
|
||||||
refreshId={refreshId}
|
refreshId={refreshId}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
<HeaderModal
|
<HeaderModal
|
||||||
@@ -128,114 +129,121 @@ interface UserSectionProps {
|
|||||||
toggle: (type: string) => void;
|
toggle: (type: string) => void;
|
||||||
setFavorites: (favorites: string[]) => void;
|
setFavorites: (favorites: string[]) => void;
|
||||||
refreshId: NodeJS.Timeout | undefined;
|
refreshId: NodeJS.Timeout | undefined;
|
||||||
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserSection({ profilePicture, setProfilePicture, setFavorites, refreshId, toggle }: UserSectionProps) {
|
function UserSection({ profilePicture, setProfilePicture, setFavorites, refreshId, toggle, loading }: UserSectionProps) {
|
||||||
const [user, setUser] = useRecoilState(userState);
|
const [user, setUser] = useRecoilState(userState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='user-section'>
|
<div className='user-section'>
|
||||||
{user ? (
|
{loading ? (
|
||||||
<Menu shadow='md' width={200} openDelay={100} closeDelay={400}>
|
<></>
|
||||||
<Menu.Target>
|
) : (
|
||||||
<UnstyledButton className='user user-button'>
|
<>
|
||||||
<Group>
|
{user ? (
|
||||||
<Avatar src={profilePicture ? URL.createObjectURL(profilePicture) : undefined} />
|
<Menu shadow='md' width={200} openDelay={100} closeDelay={400}>
|
||||||
<div style={{ flex: 1 }}>
|
<Menu.Target>
|
||||||
<Text size='sm' fw={500}>
|
<UnstyledButton className='user user-button'>
|
||||||
{user.first_name} {user.last_name}
|
<Group>
|
||||||
</Text>
|
<Avatar src={profilePicture ? URL.createObjectURL(profilePicture) : undefined} />
|
||||||
<Text c='dimmed' size='xs' style={{ textTransform: 'uppercase' }}>
|
<div style={{ flex: 1 }}>
|
||||||
{user.role}
|
<Text size='sm' fw={500}>
|
||||||
</Text>
|
{user.first_name} {user.last_name}
|
||||||
</div>
|
</Text>
|
||||||
</Group>
|
<Text c='dimmed' size='xs' style={{ textTransform: 'uppercase' }}>
|
||||||
</UnstyledButton>
|
{user.role}
|
||||||
</Menu.Target>
|
</Text>
|
||||||
<Menu.Dropdown p={0}>
|
</div>
|
||||||
<Card>
|
</Group>
|
||||||
<Card.Section h={140} style={{ backgroundColor: '#4481e3' }} />
|
</UnstyledButton>
|
||||||
<FileButton
|
</Menu.Target>
|
||||||
onChange={(payload) => {
|
<Menu.Dropdown p={0}>
|
||||||
if (payload) {
|
<Card>
|
||||||
setPicture(payload).then((response) => {
|
<Card.Section h={140} style={{ backgroundColor: '#4481e3' }} />
|
||||||
if (response) {
|
<FileButton
|
||||||
setProfilePicture(payload);
|
onChange={(payload) => {
|
||||||
}
|
if (payload) {
|
||||||
});
|
setPicture(payload).then((response) => {
|
||||||
}
|
if (response) {
|
||||||
}}
|
setProfilePicture(payload);
|
||||||
accept='image/png,image/jpeg,image/jpg'
|
}
|
||||||
multiple={false}
|
});
|
||||||
>
|
|
||||||
{(props) => (
|
|
||||||
<Avatar
|
|
||||||
{...props}
|
|
||||||
component='button'
|
|
||||||
size={80}
|
|
||||||
radius={80}
|
|
||||||
mx={'auto'}
|
|
||||||
mt={-30}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
bg={profilePicture ? 'transparent' : 'white'}
|
|
||||||
src={profilePicture ? URL.createObjectURL(profilePicture) : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FileButton>
|
|
||||||
<Text ta='center' fz='lg' fw={500} mt='sm'>
|
|
||||||
{user.first_name} {user.last_name}
|
|
||||||
</Text>
|
|
||||||
<Text ta='center' fz='sm' c='dimmed' style={{ textTransform: 'uppercase' }}>
|
|
||||||
{user.role}
|
|
||||||
</Text>
|
|
||||||
<Grid mt='xl'>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<Link href='/profile'>
|
|
||||||
<Button fullWidth radius='md' size='xs' variant='default'>
|
|
||||||
Profile
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
radius='md'
|
|
||||||
size='xs'
|
|
||||||
variant='default'
|
|
||||||
onClick={async () => {
|
|
||||||
await logout();
|
|
||||||
Cookies.remove('logged_in');
|
|
||||||
setUser(undefined);
|
|
||||||
setFavorites([]);
|
|
||||||
setProfilePicture(null);
|
|
||||||
if (refreshId) {
|
|
||||||
clearInterval(refreshId);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
accept='image/png,image/jpeg,image/jpg'
|
||||||
|
multiple={false}
|
||||||
>
|
>
|
||||||
Logout
|
{(props) => (
|
||||||
</Button>
|
<Avatar
|
||||||
</Grid.Col>
|
{...props}
|
||||||
{user.role == 'admin' && (
|
component='button'
|
||||||
<Grid.Col span={12}>
|
size={80}
|
||||||
<Link href='/admin'>
|
radius={80}
|
||||||
<Button fullWidth radius='md' size='xs' variant='default'>
|
mx={'auto'}
|
||||||
Administration
|
mt={-30}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
bg={profilePicture ? 'transparent' : 'white'}
|
||||||
|
src={profilePicture ? URL.createObjectURL(profilePicture) : undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
|
<Text ta='center' fz='lg' fw={500} mt='sm'>
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</Text>
|
||||||
|
<Text ta='center' fz='sm' c='dimmed' style={{ textTransform: 'uppercase' }}>
|
||||||
|
{user.role}
|
||||||
|
</Text>
|
||||||
|
<Grid mt='xl'>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Link href='/profile'>
|
||||||
|
<Button fullWidth radius='md' size='xs' variant='default'>
|
||||||
|
Profile
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
radius='md'
|
||||||
|
size='xs'
|
||||||
|
variant='default'
|
||||||
|
onClick={async () => {
|
||||||
|
await logout();
|
||||||
|
Cookies.remove('logged_in');
|
||||||
|
setUser(undefined);
|
||||||
|
setFavorites([]);
|
||||||
|
setProfilePicture(null);
|
||||||
|
if (refreshId) {
|
||||||
|
clearInterval(refreshId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Grid.Col>
|
||||||
</Grid.Col>
|
{user.role == 'admin' && (
|
||||||
)}
|
<Grid.Col span={12}>
|
||||||
</Grid>
|
<Link href='/admin'>
|
||||||
</Card>
|
<Button fullWidth radius='md' size='xs' variant='default'>
|
||||||
</Menu.Dropdown>
|
Administration
|
||||||
</Menu>
|
</Button>
|
||||||
) : (
|
</Link>
|
||||||
<Group className='user'>
|
</Grid.Col>
|
||||||
<Button onClick={() => toggle('login')}>Login</Button>
|
)}
|
||||||
<Button variant='outline' onClick={() => toggle('register')}>
|
</Grid>
|
||||||
Sign up
|
</Card>
|
||||||
</Button>
|
</Menu.Dropdown>
|
||||||
</Group>
|
</Menu>
|
||||||
|
) : (
|
||||||
|
<Group className='user'>
|
||||||
|
<Button onClick={() => toggle('login')}>Login</Button>
|
||||||
|
<Button variant='outline' onClick={() => toggle('register')}>
|
||||||
|
Sign up
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export default function MapTiles() {
|
|||||||
northEast: { lat: ne.lat, lon: ne.lng },
|
northEast: { lat: ne.lat, lon: ne.lng },
|
||||||
southWest: { lat: sw.lat, lon: sw.lng }
|
southWest: { lat: sw.lat, lon: sw.lng }
|
||||||
},
|
},
|
||||||
|
categories: ['small_airport', 'medium_airport', 'large_airport'],
|
||||||
order_field: AirportOrderField.CATEGORY,
|
order_field: AirportOrderField.CATEGORY,
|
||||||
order_by: 'asc',
|
order_by: 'asc',
|
||||||
limit: 250,
|
limit: 250,
|
||||||
@@ -111,7 +112,7 @@ export default function MapTiles() {
|
|||||||
{airports.map((airport) => (
|
{airports.map((airport) => (
|
||||||
<Marker
|
<Marker
|
||||||
key={airport.icao}
|
key={airport.icao}
|
||||||
position={[airport.point.y, airport.point.x]}
|
position={[airport.latitude, airport.longitude]}
|
||||||
icon={metarIcon(airport)}
|
icon={metarIcon(airport)}
|
||||||
eventHandlers={{
|
eventHandlers={{
|
||||||
click: () => handleOpen(airport)
|
click: () => handleOpen(airport)
|
||||||
@@ -119,7 +120,7 @@ export default function MapTiles() {
|
|||||||
>
|
>
|
||||||
{!isOpen && (
|
{!isOpen && (
|
||||||
<Tooltip className='metar-tooltip' direction='top' offset={[5, -5]} opacity={1}>
|
<Tooltip className='metar-tooltip' direction='top' offset={[5, -5]} opacity={1}>
|
||||||
<b>{airport.icao}</b> - {airport.full_name}
|
<b>{airport.icao}</b> - {airport.name}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Marker>
|
</Marker>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function MetarModal({ airport, isOpen, onClose }: MetarModalProps
|
|||||||
>
|
>
|
||||||
<span className='title'>
|
<span className='title'>
|
||||||
<Link href={`/airport/${airport.icao}`}>
|
<Link href={`/airport/${airport.icao}`}>
|
||||||
{airport.icao} {airport.full_name}
|
{airport.icao} {airport.name}
|
||||||
</Link>
|
</Link>
|
||||||
{isFavorite ? (
|
{isFavorite ? (
|
||||||
<AiFillStar size={24} className='star' onClick={() => handleFavorite(false)} />
|
<AiFillStar size={24} className='star' onClick={() => handleFavorite(false)} />
|
||||||
|
|||||||
Reference in New Issue
Block a user