Working on queries to get latest metar airports first
This commit is contained in:
@@ -14,7 +14,8 @@ CREATE TABLE IF NOT EXISTS airports (
|
|||||||
latitude REAL NOT NULL,
|
latitude REAL NOT NULL,
|
||||||
has_tower BOOLEAN DEFAULT false,
|
has_tower BOOLEAN DEFAULT false,
|
||||||
has_beacon BOOLEAN DEFAULT false,
|
has_beacon BOOLEAN DEFAULT false,
|
||||||
public BOOLEAN DEFAULT false
|
public BOOLEAN DEFAULT false,
|
||||||
|
metar_observation_time TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX ON airports (iata);
|
CREATE INDEX ON airports (iata);
|
||||||
@@ -25,6 +26,7 @@ CREATE INDEX ON airports (iso_country);
|
|||||||
CREATE INDEX ON airports (iso_region);
|
CREATE INDEX ON airports (iso_region);
|
||||||
CREATE INDEX ON airports (municipality);
|
CREATE INDEX ON airports (municipality);
|
||||||
CREATE INDEX ON airports (longitude, latitude);
|
CREATE INDEX ON airports (longitude, latitude);
|
||||||
|
CREATE INDEX ON airports (metar_observation_time);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS runways (
|
CREATE TABLE IF NOT EXISTS runways (
|
||||||
id UUID PRIMARY KEY NOT NULL,
|
id UUID PRIMARY KEY NOT NULL,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use futures_util::try_join;
|
use futures_util::try_join;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -121,6 +122,7 @@ struct AirportRow {
|
|||||||
pub has_tower: Option<bool>,
|
pub has_tower: Option<bool>,
|
||||||
pub has_beacon: Option<bool>,
|
pub has_beacon: Option<bool>,
|
||||||
pub public: bool,
|
pub public: bool,
|
||||||
|
pub metar_observation_time: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -141,6 +143,7 @@ pub struct UpdateAirport {
|
|||||||
pub runways: Option<Vec<UpdateRunway>>,
|
pub runways: Option<Vec<UpdateRunway>>,
|
||||||
pub frequencies: Option<Vec<UpdateFrequency>>,
|
pub frequencies: Option<Vec<UpdateFrequency>>,
|
||||||
pub public: Option<bool>,
|
pub public: Option<bool>,
|
||||||
|
pub latest_metar_observation: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<AirportRow> for Airport {
|
impl Into<AirportRow> for Airport {
|
||||||
@@ -160,6 +163,10 @@ impl Into<AirportRow> for Airport {
|
|||||||
has_tower: self.has_tower,
|
has_tower: self.has_tower,
|
||||||
has_beacon: self.has_beacon,
|
has_beacon: self.has_beacon,
|
||||||
public: self.public,
|
public: self.public,
|
||||||
|
metar_observation_time: match self.latest_metar {
|
||||||
|
Some(m) => Some(m.observation_time),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,7 +310,8 @@ impl Airport {
|
|||||||
Self::push_condition_bounds(&mut builder, &mut has_where, &query.bounds)?;
|
Self::push_condition_bounds(&mut builder, &mut has_where, &query.bounds)?;
|
||||||
|
|
||||||
// Order by AircraftCategory
|
// Order by AircraftCategory
|
||||||
builder.push(" ORDER BY CASE category ");
|
builder.push(" ORDER BY (metar_observation_time IS NULL), ");
|
||||||
|
builder.push(" CASE category ");
|
||||||
builder.push(" WHEN 'large_airport' THEN 1 ");
|
builder.push(" WHEN 'large_airport' THEN 1 ");
|
||||||
builder.push(" WHEN 'medium_airport' THEN 2 ");
|
builder.push(" WHEN 'medium_airport' THEN 2 ");
|
||||||
builder.push(" WHEN 'small_airport' THEN 3 ");
|
builder.push(" WHEN 'small_airport' THEN 3 ");
|
||||||
@@ -516,7 +524,20 @@ impl Airport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
pub async fn update(_icao: &str, _airport: &UpdateAirport) -> ApiResult<()> {
|
pub async fn update(icao: &str, airport: &UpdateAirport) -> ApiResult<()> {
|
||||||
|
let pool = db::pool();
|
||||||
|
|
||||||
|
let mut query_builder: QueryBuilder<Postgres> =
|
||||||
|
QueryBuilder::new(format!("UPDATE {} SET ", TABLE_NAME));
|
||||||
|
if let Some(latest_metar_observation) = airport.latest_metar_observation {
|
||||||
|
query_builder.push("metar_observation_time = ");
|
||||||
|
query_builder.push_bind(latest_metar_observation);
|
||||||
|
}
|
||||||
|
|
||||||
|
query_builder.push(" WHERE icao = ").push_bind(icao);
|
||||||
|
let query = query_builder.build();
|
||||||
|
query.execute(pool).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use std::str::FromStr;
|
|||||||
use redis::{AsyncCommands, RedisResult};
|
use redis::{AsyncCommands, RedisResult};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::airports::{Airport, UpdateAirport};
|
||||||
use crate::db::redis_async_connection;
|
use crate::db::redis_async_connection;
|
||||||
|
|
||||||
const TABLE_NAME: &str = "metars";
|
const TABLE_NAME: &str = "metars";
|
||||||
@@ -63,7 +64,7 @@ pub enum ReportModifier {
|
|||||||
#[serde(rename = "AUTO")]
|
#[serde(rename = "AUTO")]
|
||||||
Auto,
|
Auto,
|
||||||
#[serde(rename = "COR")]
|
#[serde(rename = "COR")]
|
||||||
Corrected
|
Corrected,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ReportModifier {
|
impl FromStr for ReportModifier {
|
||||||
@@ -72,7 +73,7 @@ impl FromStr for ReportModifier {
|
|||||||
match s {
|
match s {
|
||||||
"AUTO" => Ok(ReportModifier::Auto),
|
"AUTO" => Ok(ReportModifier::Auto),
|
||||||
"COR" => Ok(ReportModifier::Corrected),
|
"COR" => Ok(ReportModifier::Corrected),
|
||||||
_ => Err(Error::new(400, format!("Invalid report modifier '{}'", s)))
|
_ => Err(Error::new(400, format!("Invalid report modifier '{}'", s))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,10 @@ impl FromStr for AutomatedStationType {
|
|||||||
match s {
|
match s {
|
||||||
"AO1" => Ok(AutomatedStationType::WithoutPrecipitationDiscriminator),
|
"AO1" => Ok(AutomatedStationType::WithoutPrecipitationDiscriminator),
|
||||||
"AO2" => Ok(AutomatedStationType::WithPrecipitationDiscriminator),
|
"AO2" => Ok(AutomatedStationType::WithPrecipitationDiscriminator),
|
||||||
_ => Err(Error::new(400, format!("Invalid automated station type '{}'", s)))
|
_ => Err(Error::new(
|
||||||
|
400,
|
||||||
|
format!("Invalid automated station type '{}'", s),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,9 +526,7 @@ impl Metar {
|
|||||||
format!(
|
format!(
|
||||||
"P{}",
|
"P{}",
|
||||||
visibility_whole
|
visibility_whole
|
||||||
+ (visibility_left[1..visibility_left.len()]
|
+ (visibility_left[1..visibility_left.len()].parse::<f64>()? / visibility_right)
|
||||||
.parse::<f64>()?
|
|
||||||
/ visibility_right)
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
@@ -895,10 +897,7 @@ impl Metar {
|
|||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
let mut missing_metar_icaos: Vec<String> = vec![];
|
let mut missing_metar_icaos: Vec<String> = vec![];
|
||||||
let current_time = chrono::Local::now().naive_local().and_utc().timestamp();
|
let current_time = chrono::Local::now().naive_local().and_utc().timestamp();
|
||||||
let db_metars_set: HashSet<&str> = db_metars
|
let db_metars_set: HashSet<&str> = db_metars.iter().map(|icao| icao.icao.as_str()).collect();
|
||||||
.iter()
|
|
||||||
.map(|icao| icao.icao.as_str())
|
|
||||||
.collect();
|
|
||||||
let station_icaos_set: HashSet<&str> = station_icaos.iter().map(|s| s.as_str()).collect();
|
let station_icaos_set: HashSet<&str> = station_icaos.iter().map(|s| s.as_str()).collect();
|
||||||
for difference in db_metars_set.symmetric_difference(&station_icaos_set) {
|
for difference in db_metars_set.symmetric_difference(&station_icaos_set) {
|
||||||
missing_metar_icaos.push(difference.to_string());
|
missing_metar_icaos.push(difference.to_string());
|
||||||
@@ -986,17 +985,63 @@ impl Metar {
|
|||||||
let pool = db::pool();
|
let pool = db::pool();
|
||||||
let metar_rows: Vec<MetarRow> = sqlx::query_as::<_, MetarRow>(&format!(
|
let metar_rows: Vec<MetarRow> = sqlx::query_as::<_, MetarRow>(&format!(
|
||||||
r#"
|
r#"
|
||||||
SELECT DISTINCT ON (icao) * FROM {} WHERE icao = ANY($1) ORDER BY icao, observation_time DESC
|
SELECT DISTINCT ON (icao) * FROM {}
|
||||||
"#,
|
WHERE icao = ANY($1)
|
||||||
|
AND observation_time >= NOW() - INTERVAL '50 minutes'
|
||||||
|
ORDER BY icao, observation_time DESC
|
||||||
|
"#,
|
||||||
TABLE_NAME
|
TABLE_NAME
|
||||||
))
|
))
|
||||||
.bind(icao_list)
|
.bind(icao_list)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.await?;
|
||||||
metars = metar_rows
|
for metar_row in metar_rows {
|
||||||
.into_iter()
|
let metar = match Metar::from_db(metar_row) {
|
||||||
.filter_map(|metar_db| Metar::from_db(metar_db).ok())
|
Ok(m) => m,
|
||||||
.collect();
|
Err(err) => {
|
||||||
|
log::error!("{}", err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let icao = metar.icao.clone();
|
||||||
|
let observation_time = metar.observation_time.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match Airport::update(
|
||||||
|
&icao,
|
||||||
|
&UpdateAirport {
|
||||||
|
icao: None,
|
||||||
|
iata: None,
|
||||||
|
local: None,
|
||||||
|
name: None,
|
||||||
|
category: None,
|
||||||
|
iso_country: None,
|
||||||
|
iso_region: None,
|
||||||
|
municipality: None,
|
||||||
|
elevation_ft: None,
|
||||||
|
longitude: None,
|
||||||
|
latitude: None,
|
||||||
|
has_tower: None,
|
||||||
|
has_beacon: None,
|
||||||
|
runways: None,
|
||||||
|
frequencies: None,
|
||||||
|
public: None,
|
||||||
|
latest_metar_observation: Some(observation_time),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => log::error!(
|
||||||
|
"Unable to update airport {} with the latest observation time: {}",
|
||||||
|
icao,
|
||||||
|
err
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
metars.push(metar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut conn = redis_async_connection().await?;
|
let mut conn = redis_async_connection().await?;
|
||||||
@@ -1011,17 +1056,19 @@ impl Metar {
|
|||||||
let result: RedisResult<Option<bool>> = conn.get(icao).await;
|
let result: RedisResult<Option<bool>> = conn.get(icao).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(Some(value)) => {
|
Ok(Some(value)) => {
|
||||||
if value {
|
if !value {
|
||||||
updated_missing_icao_list.push(icao);
|
updated_missing_icao_list.push(icao);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => updated_missing_icao_list.push(icao),
|
||||||
updated_missing_icao_list.push(icao);
|
Err(err) => {
|
||||||
|
log::error!("{}", err);
|
||||||
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dbg!(&updated_missing_icao_list);
|
||||||
if !updated_missing_icao_list.is_empty() {
|
if !updated_missing_icao_list.is_empty() {
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"Retrieving missing METAR data for {:?}",
|
"Retrieving missing METAR data for {:?}",
|
||||||
|
|||||||
4
ui/package-lock.json
generated
4
ui/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "aviation-ui",
|
"name": "aviation-ui",
|
||||||
"version": "0.1.2",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aviation-ui",
|
"name": "aviation-ui",
|
||||||
"version": "0.1.2",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^7.17.2",
|
"@mantine/core": "^7.17.2",
|
||||||
"@mantine/form": "^7.17.2",
|
"@mantine/form": "^7.17.2",
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Divider, Drawer, Group } from '@mantine/core';
|
import { Box, Divider, Drawer, Group, Text } from '@mantine/core';
|
||||||
import { Airport, AirportCategory } from '@lib/airport.types.ts';
|
import { Airport, AirportCategory } from '@lib/airport.types.ts';
|
||||||
|
import { getMarkerColor, Metar } from '@lib/metar.types.ts';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { getMetars } from '@lib/metar.ts';
|
||||||
|
|
||||||
export default function AirportDrawer({
|
export default function AirportDrawer({
|
||||||
airport,
|
airport,
|
||||||
@@ -8,9 +11,24 @@ export default function AirportDrawer({
|
|||||||
airport: Airport | null;
|
airport: Airport | null;
|
||||||
setAirport: (airport: Airport | null) => void;
|
setAirport: (airport: Airport | null) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [metar, setMetar] = useState<Metar | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (airport != null) {
|
||||||
|
getMetars({ icaos: [airport.icao] }).then((m) => {
|
||||||
|
if (m.length > 0) {
|
||||||
|
setMetar(m[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [airport]);
|
||||||
|
|
||||||
if (!airport) {
|
if (!airport) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metarColor = getMarkerColor(metar?.flight_category || 'UNKN');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
opened={true}
|
opened={true}
|
||||||
@@ -25,27 +43,35 @@ export default function AirportDrawer({
|
|||||||
withOverlay={false}
|
withOverlay={false}
|
||||||
closeOnClickOutside={false}
|
closeOnClickOutside={false}
|
||||||
>
|
>
|
||||||
<Group>
|
<Box mb='lg'>
|
||||||
<div>ICAO: {airport.icao}</div>
|
{metar && metar.flight_category && (
|
||||||
<div>Category: {airportCategoryToText(airport.category)}</div>
|
<Group justify='space-between' mb='md'>
|
||||||
<div>
|
<Text style={{ color: metarColor }}>{metar.flight_category}</Text>
|
||||||
Country / Region: {airport.iso_country}, {airport.iso_region}
|
<Text size='sm'>{metar.observation_time ? new Date(metar.observation_time).toLocaleString() : 'N/A'}</Text>
|
||||||
</div>
|
</Group>
|
||||||
<div>Municipality: {airport.municipality || 'N/A'}</div>
|
|
||||||
<div>Local Code: {airport.local || 'N/A'}</div>
|
|
||||||
<div>Elevation: {airport.elevation_ft}</div>
|
|
||||||
<div>
|
|
||||||
Coordinates: {airport.latitude.toFixed(4)}, {airport.longitude.toFixed(4)}
|
|
||||||
</div>
|
|
||||||
<div>Control Tower: {airport.has_tower ? 'Yes' : 'No'}</div>
|
|
||||||
<div>Beacon: {airport.has_beacon ? 'Yes' : 'No'}</div>
|
|
||||||
{airport.latest_metar && airport.latest_metar.flight_category && (
|
|
||||||
<>
|
|
||||||
<Divider my='sm' />
|
|
||||||
<div>Flight Category: {airport.latest_metar.flight_category}</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Group>
|
<Group>
|
||||||
|
<div>ICAO: {airport.icao}</div>
|
||||||
|
<div>Category: {airportCategoryToText(airport.category)}</div>
|
||||||
|
<div>
|
||||||
|
Country / Region: {airport.iso_country}, {airport.iso_region}
|
||||||
|
</div>
|
||||||
|
<div>Municipality: {airport.municipality || 'N/A'}</div>
|
||||||
|
<div>Local Code: {airport.local || 'N/A'}</div>
|
||||||
|
<div>Elevation: {airport.elevation_ft}</div>
|
||||||
|
<div>
|
||||||
|
Coordinates: {airport.latitude.toFixed(4)}, {airport.longitude.toFixed(4)}
|
||||||
|
</div>
|
||||||
|
<div>Control Tower: {airport.has_tower ? 'Yes' : 'No'}</div>
|
||||||
|
<div>Beacon: {airport.has_beacon ? 'Yes' : 'No'}</div>
|
||||||
|
{metar && metar.flight_category && (
|
||||||
|
<>
|
||||||
|
<Divider my='sm' />
|
||||||
|
<div>Flight Category: {metar.flight_category}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Airport, AirportCategory } from '@lib/airport.types.ts';
|
|||||||
import { Marker, Popup } from 'react-leaflet';
|
import { Marker, Popup } from 'react-leaflet';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import { getMarkerColor } from '@lib/metar.types.ts';
|
||||||
|
|
||||||
export default function AirportMarker({
|
export default function AirportMarker({
|
||||||
index,
|
index,
|
||||||
@@ -34,21 +35,6 @@ export default function AirportMarker({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMarkerInfo(flightCategory: 'VFR' | 'MVFR' | 'LIFR' | 'IFR' | 'UNKN'): [string, number] {
|
|
||||||
switch (flightCategory) {
|
|
||||||
case 'IFR':
|
|
||||||
return ['#ff0100', 5];
|
|
||||||
case 'LIFR':
|
|
||||||
return ['#7f007f', 6];
|
|
||||||
case 'MVFR':
|
|
||||||
return ['#00f', 7];
|
|
||||||
case 'VFR':
|
|
||||||
return ['#018000', 8];
|
|
||||||
case 'UNKN':
|
|
||||||
return ['#696969', 4];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCustomIcon(airport: Airport): L.DivIcon {
|
function createCustomIcon(airport: Airport): L.DivIcon {
|
||||||
if (airport.category === AirportCategory.HELIPORT) {
|
if (airport.category === AirportCategory.HELIPORT) {
|
||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
@@ -73,12 +59,12 @@ function createCustomIcon(airport: Airport): L.DivIcon {
|
|||||||
} else {
|
} else {
|
||||||
// Default to a filled circle.
|
// Default to a filled circle.
|
||||||
const flightCategory = airport.latest_metar?.flight_category || 'UNKN';
|
const flightCategory = airport.latest_metar?.flight_category || 'UNKN';
|
||||||
const info = getMarkerInfo(flightCategory);
|
const color = getMarkerColor(flightCategory);
|
||||||
if (flightCategory == 'UNKN') {
|
if (flightCategory == 'UNKN') {
|
||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
html: `
|
html: `
|
||||||
<div style="
|
<div style="
|
||||||
background-color: ${info[0]};
|
background-color: ${color};
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -93,7 +79,7 @@ function createCustomIcon(airport: Airport): L.DivIcon {
|
|||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
html: `
|
html: `
|
||||||
<div style="
|
<div style="
|
||||||
background-color: ${info[0]};
|
background-color: ${color};
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
10
ui/src/lib/metar.ts
Normal file
10
ui/src/lib/metar.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Metar } from '@lib/metar.types.ts';
|
||||||
|
import { getRequest } from '@lib/index.ts';
|
||||||
|
|
||||||
|
export async function getMetars({ icaos, force }: { icaos: string[]; force?: boolean }): Promise<Metar[]> {
|
||||||
|
const response = await getRequest('metars', {
|
||||||
|
icaos: icaos,
|
||||||
|
force: force
|
||||||
|
});
|
||||||
|
return response?.json() || {};
|
||||||
|
}
|
||||||
@@ -3,12 +3,24 @@ export interface SkyCondition {
|
|||||||
cloud_base_ft_agl: number;
|
cloud_base_ft_agl: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QualityControlFlags {
|
export interface PeakWind {
|
||||||
auto: boolean;
|
degrees: number;
|
||||||
auto_station_without_precipitation: boolean;
|
speed: number;
|
||||||
auto_station_with_precipication: boolean;
|
hour?: number;
|
||||||
maintenance_indicator_on: boolean;
|
minute?: number;
|
||||||
corrected: boolean;
|
}
|
||||||
|
|
||||||
|
export interface Remarks {
|
||||||
|
peak_wind?: PeakWind;
|
||||||
|
auto_station_type?: string;
|
||||||
|
maintenance_indicator_on?: boolean;
|
||||||
|
rvr_missing?: boolean;
|
||||||
|
precipitation_identifier_information_not_available?: boolean;
|
||||||
|
precipitation_information_not_available?: boolean;
|
||||||
|
freezing_rain_information_not_available?: boolean;
|
||||||
|
thunderstorm_information_not_available?: boolean;
|
||||||
|
visibility_at_secondary_location_not_available?: boolean;
|
||||||
|
sky_condition_at_secondary_location_not_available?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunwayVisualRange {
|
export interface RunwayVisualRange {
|
||||||
@@ -19,25 +31,44 @@ export interface RunwayVisualRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Metar {
|
export interface Metar {
|
||||||
|
icao: string;
|
||||||
raw_text: string;
|
raw_text: string;
|
||||||
station_id: string;
|
|
||||||
observation_time: string;
|
observation_time: string;
|
||||||
temp_c: number;
|
flight_category: 'VFR' | 'MVFR' | 'LIFR' | 'IFR' | 'UNKN';
|
||||||
dewpoint_c: number;
|
report_modifier?: string;
|
||||||
wind_dir_degrees: string;
|
becoming_change?: boolean;
|
||||||
wind_speed_kt: number;
|
no_significant_change?: boolean;
|
||||||
wind_gust_kt: number;
|
temporary_change?: boolean;
|
||||||
variable_wind_dir_degrees: string;
|
temp_c?: number;
|
||||||
visibility_statute_mi: string;
|
dew_point_c?: number;
|
||||||
|
estimated_humidity?: 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[];
|
runway_visual_range: RunwayVisualRange[];
|
||||||
altim_in_hg: number;
|
altimeter_in_hg?: number;
|
||||||
sea_level_pressure_mb: number;
|
sea_level_pressure_mb?: number;
|
||||||
quality_control_flags: QualityControlFlags;
|
remarks: Remarks;
|
||||||
weather_phenomena: string[];
|
weather_phenomena: string[];
|
||||||
sky_condition: SkyCondition[];
|
sky_condition: SkyCondition[];
|
||||||
flight_category: 'VFR' | 'MVFR' | 'LIFR' | 'IFR' | 'UNKN';
|
max_temp_c?: number;
|
||||||
three_hr_pressure_tendency_mb: number;
|
min_temp_c?: number;
|
||||||
max_t_c: number;
|
density_altutude?: number;
|
||||||
min_t_c: number;
|
}
|
||||||
precip_in: number;
|
|
||||||
|
export function getMarkerColor(flightCategory: 'VFR' | 'MVFR' | 'LIFR' | 'IFR' | 'UNKN'): string {
|
||||||
|
switch (flightCategory) {
|
||||||
|
case 'IFR':
|
||||||
|
return '#ff0100';
|
||||||
|
case 'LIFR':
|
||||||
|
return '#7f007f';
|
||||||
|
case 'MVFR':
|
||||||
|
return '#00f';
|
||||||
|
case 'VFR':
|
||||||
|
return '#018000';
|
||||||
|
case 'UNKN':
|
||||||
|
return '#696969';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user