simple website with default airports

This commit is contained in:
2023-08-24 10:00:40 -04:00
parent cb005a1b98
commit 08bcce3813
31 changed files with 9244 additions and 855 deletions

View File

@@ -1,10 +0,0 @@
pub struct Airport {
pub name: String,
pub icao: String
}
impl Airport {
pub fn new(name: String, icao: String) -> Airport {
Airport { name, icao }
}
}

View File

View File

@@ -0,0 +1,49 @@
import { Airport } from "@/js/airport";
import { Metar, getMetars } from "@/js/weather"
import Link from "next/link"
export default async function MetarCard({airports}: {airports: Airport[]}) {
// await getMetars(defaultAirports).then((result) => {
// setMetars(result);
// });
const metars = await getMetars(airports);
for (let i = 0; i < airports.length; i++) {
airports[i].metar = metars[i];
}
function metarBGColor(metar: Metar | undefined) {
if (metar?.flight_category == 'VFR') {
return 'bg-emerald-600'
} else if (metar?.flight_category == 'MVFR') {
return 'bg-blue-600'
} else if (metar?.flight_category == 'IFR') {
return 'bg-red-600'
} else if (metar?.flight_category == 'LIFR') {
return 'bg-purple-600'
} else {
return 'bg-black'
}
}
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{airports.map((airport) => (
<div
key={airport.metar?.station_id}
className={`relative flex items-center space-x-3 rounded-lg border border-gray-300 bg-white px-4 py-2 shadow-sm focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:border-gray-400`}
>
<div className="min-w-0 flex-1">
<Link href={'#'}>
<span className="absolute inset-0" aria-hidden="true" />
<p className="text-gray-900 pb-1">{airport.metar?.station_id} - <span>{airport.name}</span></p>
<p className='text-sm font-medium text-gray-500'>{airport.metar?.raw_text}</p>
<div className='mt-2'>
<span className={`truncate text-sm text-white ${metarBGColor(airport.metar)} inline-block py-2 px-4 rounded-full`}>{airport.metar?.flight_category}</span>
</div>
</Link>
</div>
</div>
))}
</div>
)
}

14
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,14 @@
import React from 'react';
import RecoilRootWrapper from '@app/recoil-root-wrapper';
import 'styles/globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className='bg-gray-200'>
<RecoilRootWrapper>{children}</RecoilRootWrapper>
</body>
</html>
);
}

55
src/app/page.tsx Normal file
View File

@@ -0,0 +1,55 @@
// 'use client';
import { Airport } from '@/js/airport';
import React from 'react';
import MetarCard from './components/MetarCard';
export default function Page() {
// const [airports, setAirports] = useRecoilState(airportsState);
// useEffect(() => {
// const defaultAirports = [
// new Airport('Leesburg Executive Airport', 'KJYO'),
// new Airport('Manassas Regional Airpoirt', 'KHEF'),
// new Airport('Dulles International Airport', 'KIAD'),
// new Airport('Frederick Municipal Airport', 'KFDK'),
// new Airport('Eastern West Virginia Regional Airport', 'KMRB'),
// new Airport('Winchester Regional Airport', 'KOKV'),
// new Airport('Front Royal-Warren County Airport', 'KFRR'),
// new Airport('Luray Caverns Airport', 'KLUA'),
// new Airport('Shenandoah Valley Airport', 'KSHD'),
// new Airport('Charlottesville-Albemarle Airport', 'KCHO'),
// new Airport('Culpeper Regional Airport', 'KCJR'),
// new Airport('Warrenton-Fauquier Airport', 'KHWY'),
// new Airport('Stafford Regional Airport', 'KRMN'),
// new Airport('Shannon Airport', 'KEZF'),
// ];
// setAirports(defaultAirports);
// }, []);
const defaultAirports = [
new Airport('Leesburg Executive Airport', 'KJYO'),
new Airport('Manassas Regional Airpoirt', 'KHEF'),
new Airport('Dulles International Airport', 'KIAD'),
new Airport('Frederick Municipal Airport', 'KFDK'),
new Airport('Eastern West Virginia Regional Airport', 'KMRB'),
new Airport('Winchester Regional Airport', 'KOKV'),
new Airport('Front Royal-Warren County Airport', 'KFRR'),
new Airport('Luray Caverns Airport', 'KLUA'),
new Airport('Shenandoah Valley Airport', 'KSHD'),
new Airport('Charlottesville-Albemarle Airport', 'KCHO'),
new Airport('Culpeper Regional Airport', 'KCJR'),
new Airport('Warrenton-Fauquier Airport', 'KHWY'),
new Airport('Stafford Regional Airport', 'KRMN'),
new Airport('Shannon Airport', 'KEZF'),
];
return <>
<div className="border-b border-gray-200 bg-gray-400 px-4 py-5 sm:px-6">
<h3 className="text-base font-semibold leading-6 text-gray-900">Airports</h3>
</div>
<div className='p-4'>
<MetarCard airports={defaultAirports}/>
</div>
</>;
}

View File

@@ -0,0 +1,10 @@
'use client';
import { RecoilRoot } from 'recoil';
import React, { ReactNode } from 'react';
const RecoilRootWrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>{children}</RecoilRoot>
);
export default RecoilRootWrapper;

13
src/js/airport.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Metar } from "./weather";
export class Airport {
name: string;
icao: string;
metar: Metar | undefined;
constructor(name: string, icao: string) {
this.name = name;
this.icao = icao;
this.metar = undefined;
}
}

7
src/js/state.ts Normal file
View File

@@ -0,0 +1,7 @@
import { atom } from "recoil";
import { Airport } from "./airport";
export const airportsState = atom({
key: 'airportsState',
default: [] as Airport[]
});

104
src/js/weather.ts Normal file
View File

@@ -0,0 +1,104 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import axios from 'axios';
import { xml2json } from 'xml-js';
import { Airport } from './airport';
const base_url = 'https://beta.aviationweather.gov/cgi-bin/data';
export async function getMetars(airports: Airport[]): Promise<Metar[]> {
const stationICAOs: string = airports
.map((airport) => airport.icao)
.join(',');
const url = `${base_url}/metar.php?ids=${stationICAOs}&format=xml`;
const response = await axios
.get(`${url}`)
.catch((error) => console.error(`${error}`));
// const metars = new Map<string, Metar>();
const metars: Metar[] = [];
if (response != null && response != undefined) {
const json = xml2json(response.data, { compact: true });
const jsonObject = JSON.parse(json);
const metarData = jsonObject?.response?.data?.METAR;
for (const data of metarData) {
const sky_condition: {
sky_cover: string;
cloud_base_ft_agl: number;
}[] = [];
if (Array.isArray(data.sky_condition)) {
for (const sc of data.sky_condition) {
sky_condition.push({
sky_cover: sc.sky_cover,
cloud_base_ft_agl: Number(sc.cloud_base_ft_agl)
})
}
} else {
sky_condition.push({
sky_cover: data.sky_condition?.sky_cover,
cloud_base_ft_agl: Number(data.sky_condition?.cloud_base_ft_agl)
})
}
const metar: Metar = {
raw_text: data.raw_text._text,
station_id: data.station_id._text,
observation_time: data.observation_time._text,
latitude: Number(data.latitude._text),
longitude: Number(data.longitude._text),
temp_c: Number(data.temp_c._text),
dewpoint_c: Number(data.dewpoint_c._text),
wind_dir_degrees: Number(data.wind_dir_degrees._text),
wind_speed_kt: Number(data.wind_speed_kt._text),
visibility_statute_mi: data.visibility_statute_mi._text,
altim_in_hg: Number(data.altim_in_hg._text),
sea_level_pressure_mb: data.sea_level_pressure_mb?._text,
quality_control_flags: {
auto: data.quality_control_flags?.auto?._text == 'TRUE',
auto_station: data.quality_control_flags?.auto_station?._text == 'TRUE',
},
wx_string: data.wx_string?._text,
sky_condition: sky_condition,
flight_category: data.flight_category._text,
three_hr_pressure_tendency_mb: data.three_hr_pressure_tendency_mb?._text,
metar_type: data.metar_type._text,
maxT_c: Number(data.maxT_c?._text),
minT_c: Number(data.minT_c?._text),
precip_in: Number(data.precip_in?._text),
elevation_m: Number(data.elevation_m._text),
};
metars.push(metar);
}
}
return metars;
}
export interface Metar {
raw_text: string;
station_id: string;
observation_time: string;
latitude: number;
longitude: number;
temp_c: number;
dewpoint_c: number;
wind_dir_degrees: number;
wind_speed_kt: number;
visibility_statute_mi: string;
altim_in_hg: number;
sea_level_pressure_mb: number;
quality_control_flags: {
auto: boolean;
auto_station: boolean;
};
wx_string: string;
sky_condition: {
sky_cover: string;
cloud_base_ft_agl: number;
}[];
flight_category: string;
three_hr_pressure_tendency_mb: number;
metar_type: string;
maxT_c: number;
minT_c: number;
precip_in: number;
elevation_m: number;
}

162
src/lib.rs Normal file
View File

@@ -0,0 +1,162 @@
use std::error::Error;
use std::fmt;
use log::warn;
use std::io::BufRead;
use quick_xml::{Reader, events::{Event, BytesStart}, Writer, de::Deserializer};
use serde::Deserialize;
pub struct Airport {
pub name: String,
pub icao: String
}
impl Airport {
pub fn new(name: String, icao: String) -> Airport {
Airport { name, icao }
}
}
#[derive(Debug)]
pub struct WeatherError(pub String);
impl fmt::Display for WeatherError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for WeatherError {}
#[derive(Deserialize, Debug)]
pub struct Metar {
pub raw_text: String,
pub station_id: String,
pub observation_time: String,
pub latitude: f32,
pub longitude: f32,
pub temp_c: f32,
pub dewpoint_c: f32,
pub wind_dir_degrees: i32,
pub wind_speed_kt: i32,
pub visibility_statute_mi: String,
pub altim_in_hg: f32,
pub sea_level_pressure_mb: Option<f32>,
pub quality_control_flags: Option<QualityControlFlags>,
pub wx_string: Option<String>,
// pub sky_con dition: Option<Vec<String>>, // TODO work on attributes
pub flight_category: String,
pub three_hr_pressure_tendency_mb: Option<f32>,
pub metar_type: String,
#[serde(rename = "maxT_c")]
pub max_t_c: Option<f32>,
#[serde(rename = "minT_c")]
pub min_t_c: Option<f32>,
pub precip_in: Option<f32>,
pub elevation_m: i32
}
#[derive(Deserialize, Debug)]
pub struct QualityControlFlags {
pub auto: Option<bool>,
pub auto_station: Option<bool>
}
impl Metar {
pub fn parse(input: String) -> Result<Vec<Metar>, WeatherError> {
if input.is_empty() {
return Err(WeatherError("Input is empty".to_string()))
}
let mut reader = Reader::from_str(&input);
let mut buf = Vec::new();
let mut junk_buf: Vec<u8> = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Err(e) => panic!("Error at position: {}: {:?}", reader.buffer_position(), e),
Ok(Event::Eof) => break,
Ok(Event::Start(e)) => {
match e.name().as_ref() {
b"METAR" => {
let metar_bytes = Metar::read_to_end_into_buffer(&mut reader, &e, &mut junk_buf).unwrap();
let str = std::str::from_utf8(&metar_bytes).unwrap();
let mut deserializer = Deserializer::from_str(str);
let metar = Metar::deserialize(&mut deserializer).unwrap();
println!("{:#?}", metar);
},
_ => ()
}
},
_ => ()
}
}
return Ok(vec![])
}
// https://capnfabs.net/posts/parsing-huge-xml-quickxml-rust-serde/
pub fn read_to_end_into_buffer<R: BufRead>(reader: &mut Reader<R>, start_tag: &BytesStart, junk_buf: &mut Vec<u8>) -> Result<Vec<u8>, quick_xml::Error> {
let mut depth = 0;
let mut output_buf: Vec<u8> = Vec::new();
let mut w = Writer::new(&mut output_buf);
let tag_name = start_tag.name();
w.write_event(Event::Start(start_tag.clone()))?;
loop {
junk_buf.clear();
let event = reader.read_event_into(junk_buf)?;
w.write_event(&event)?;
match event {
Event::Start(e) if e.name() == tag_name => depth += 1,
Event::End(e) if e.name() == tag_name => {
if depth == 0 {
return Ok(output_buf);
}
depth -= 1;
}
Event::Eof => {
panic!("EOF")
}
_ => {}
}
}
}
}
pub struct Weather {
pub base_url: String
}
impl Weather {
pub async fn metar(&mut self, airports: Vec<Airport>) -> Vec<Metar> {
let mut station_icaos: Vec<&str> = vec![];
for station in airports.iter() {
station_icaos.push(&station.icao);
}
let station_string = station_icaos.join(",");
let url = format!("{}/metar.php?ids={}&format=xml", self.base_url, station_string);
let metars: Vec<Metar> = match reqwest::get(url).await {
Ok(r) => match r.text().await {
Ok(r) => {
match Metar::parse(r) {
Ok(m) => m,
Err(err) => {
warn!("{}", err);
vec![]
}
}
},
Err(err) => {
warn!("Unable to parse METAR request: {}", err);
vec![]
}
},
Err(err) => {
warn!("Unable to get METAR request: {}", err);
vec![]
}
};
return metars;
}
}

View File

@@ -1,37 +0,0 @@
use std::error::Error;
use airport::Airport;
use log::debug;
use crate::weather::{Weather, metar::Metar};
mod airport;
mod weather;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
env_logger::init();
let mut weather = Weather {
base_url: "https://beta.aviationweather.gov/cgi-bin/data".to_string()
};
let airports: Vec<Airport> = vec![
Airport::new("Leesburg Executive Airport".to_string(), "KJYO".to_string()),
Airport::new("Manassas Regional Airpoirt".to_string(), "KHEF".to_string()),
Airport::new("Dulles International Airport".to_string(), "KIAD".to_string()),
Airport::new("Frederick Municipal Airport".to_string(), "KFDK".to_string()),
Airport::new("Eastern West Virginia Regional Airport".to_string(), "KMRB".to_string()),
Airport::new("Winchester Regional Airport".to_string(), "KOKV".to_string()),
Airport::new("Front Royal-Warren County Airport".to_string(), "KFRR".to_string()),
Airport::new("Luray Caverns Airport".to_string(), "KLUA".to_string()),
Airport::new("Shenandoah Valley Airport".to_string(), "KSHD".to_string()),
Airport::new("Charlottesville-Albemarle Airport".to_string(), "KCHO".to_string()),
Airport::new("Culpeper Regional Airport".to_string(), "KCJR".to_string()),
Airport::new("Warrenton-Fauquier Airport".to_string(), "KHWY".to_string()),
Airport::new("Stafford Regional Airport".to_string(), "KRMN".to_string()),
Airport::new("Shannon Airport".to_string(), "KEZF".to_string()),
];
let metars: Vec<Metar> = weather.metar(airports).await;
debug!("{:#?}", metars);
Ok(())
}

View File

@@ -1,102 +0,0 @@
use regex::Regex;
use super::WeatherError;
#[derive(Debug)]
pub struct Metar {
pub icao: String,
pub date_time: String,
pub report_modifier: String,
pub wind_direction: String,
pub wind_speed: String,
pub wind_direction_variable: String,
pub visibility: String,
pub runway_visual_range: String,
pub weather_phenomena: Vec<String>,
pub sky_condition: Vec<String>,
pub temperature: String,
pub dew_point: String,
pub altimeter: String,
pub remarks: Vec<String>
}
impl Metar {
pub fn new(input: String) -> Result<Metar, WeatherError> {
if input.is_empty() {
return Err(WeatherError("Input is empty".to_string()))
}
let mut offset: usize = 0;
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.len() < 5 {
return Err(WeatherError("Unable to parse input".to_string()))
}
let mut report_modifier = "";
if parts[2] == "AUTO" {
offset += 1;
report_modifier = parts[2];
}
let wind_re = Regex::new(r"^\d{3}V\d{3}$").unwrap();
let wind_direction_variable = match wind_re.find(parts[3 + offset]) {
Some(_) => {
offset += 1;
parts[3 + offset]
},
None => ""
};
let mut runway_visual_range = "";
if parts[4 + offset].ends_with("FT") {
offset += 1;
runway_visual_range = parts[4 + offset];
}
let mut weather_phenomena: Vec<String> = vec![];
let weather_re = Regex::new(r"^(-|\+)?[A-Z]{2}(?:[A-Z]{2})?$").unwrap();
let mut sky_condition: Vec<String> = vec![];
let sky_re = Regex::new(r"^CLR|(FEW|SCT|BKN|OVC)\d{3}$").unwrap();
for n in (4 + offset)..parts.len() {
match weather_re.find(parts[n]) {
Some(_) => {
offset += 1;
weather_phenomena.push(parts[n].to_string());
},
None => {}
}
match sky_re.find(parts[n]) {
Some(_) => {
offset += 1;
sky_condition.push(parts[n].to_string());
},
None => {}
}
}
let temp_dew: Vec<&str> = parts[4 + offset].split("/").collect();
let mut remarks: Vec<String> = vec![];
if parts.len() > 6 + offset {
// Skip the RMK string for remarks, starting at index + 1
for n in (6 + offset + 1)..parts.len() {
remarks.push(parts[n].to_string())
}
}
Ok(Metar {
icao: parts[0].to_string(),
date_time: parts[1].to_string(),
report_modifier: report_modifier.to_string(),
wind_direction: parts[2 + offset][..3].to_string(),
wind_speed: parts[2 + offset][3..].to_string(),
wind_direction_variable: wind_direction_variable.to_string(),
visibility: parts[3 + offset].to_string(),
runway_visual_range: runway_visual_range.to_string(),
weather_phenomena,
sky_condition,
temperature: temp_dew[0].to_string(),
dew_point: temp_dew[1].to_string(),
altimeter: parts[5 + offset].to_string(),
remarks
})
}
}

View File

@@ -1,52 +0,0 @@
use std::error::Error;
use std::fmt;
use log::warn;
use crate::airport::Airport;
use self::metar::Metar;
pub mod metar;
#[derive(Debug)]
pub struct WeatherError(pub String);
impl fmt::Display for WeatherError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for WeatherError {}
pub struct Weather {
pub base_url: String
}
impl Weather {
pub async fn metar(&mut self, airports: Vec<Airport>) -> Vec<Metar> {
let mut station_icaos: Vec<&str> = vec![];
for station in airports.iter() {
station_icaos.push(&station.icao);
}
let station_string = station_icaos.join(",");
let url = format!("{}/metar.php?ids={}", self.base_url, station_string);
let mut metars: Vec<Metar> = vec![];
match reqwest::get(url).await {
Ok(r) => match r.text().await {
Ok(r) => {
let lines: Vec<&str> = r.split("\n").collect();
for line in lines.iter() {
match Metar::new(line.to_string()) {
Ok(m) => metars.push(m),
Err(err) => warn!("{}", err)
};
}
},
Err(err) => warn!("Unable to parse METAR request: {}", err)
},
Err(err) => warn!("Unable to get METAR request: {}", err)
}
return metars;
}
}