flight-planner
Version:
Plan and route VFR flights
163 lines (162 loc) • 6.19 kB
JavaScript
import { normalizeICAO } from './utils.js';
import { parseMetar } from "metar-taf-parser";
import convert from 'convert-units';
import { FlightRules } from './index.js';
/**
* Creates a Metar object from a raw METAR string.
*
* @param raw The raw METAR string
* @returns A Metar object
*/
export function createMetarFromString(raw) {
const metar = parseMetar(raw);
const observationTime = new Date();
if (metar.day) {
observationTime.setUTCDate(metar.day);
}
else {
observationTime.setUTCDate(observationTime.getUTCDate() - 1);
}
if (metar.hour) {
observationTime.setUTCHours(metar.hour);
}
if (metar.minute) {
observationTime.setUTCMinutes(metar.minute);
}
observationTime.setUTCSeconds(0);
observationTime.setUTCMilliseconds(0);
// TODO: This is where we do all the conversion to the correct units
return {
station: normalizeICAO(metar.station),
observationTime,
raw: metar.message,
wind: {
direction: metar.wind?.degrees,
directionMin: metar.wind?.minVariation,
directionMax: metar.wind?.maxVariation,
speed: metar.wind?.speed,
gust: metar.wind?.gust,
},
temperature: metar.temperature,
dewpoint: metar.dewPoint,
visibility: metar.visibility ? {
value: metar.visibility.value,
unit: metar.visibility.unit === 'm' ? 'm' : 'sm', // TODO: Drop the unit, convert to meters
} : undefined,
qnh: metar.altimeter ? {
value: metar.altimeter.value,
unit: metar.altimeter.unit === 'hPa' ? 'hPa' : 'inHg' // TODO: Drop the unit, convert to hPa
} : undefined,
clouds: metar.clouds?.map((cloud) => ({
quantity: cloud.quantity,
height: cloud.height,
})),
};
}
export function metarCeiling(metar) {
const cloudCeilingQuantity = ['BKN', 'OVC'];
const clouds = metar.clouds || [];
const cloudCeiling = clouds.filter(cloud => cloudCeilingQuantity.includes(cloud.quantity)).sort((a, b) => (a.height ?? 0) - (b.height ?? 0));
if (cloudCeiling.length > 0) {
return cloudCeiling[0].height;
}
return undefined;
}
export function metarFlightRule(metar) {
const ceiling = metarCeiling(metar);
let visibilityMeters;
if (metar.visibility !== undefined) {
visibilityMeters = metar.visibility.value;
if (metar.visibility.unit === 'sm') {
// TODO: Move this to a utility function so it can be used once the METAR is parsed
visibilityMeters = convert(visibilityMeters).from('mi').to('m');
}
}
if ((visibilityMeters !== undefined && visibilityMeters <= 1500) ||
(ceiling !== undefined && ceiling <= 500)) {
return FlightRules.LIFR;
}
if ((visibilityMeters !== undefined && visibilityMeters <= 5000) ||
(ceiling !== undefined && ceiling <= 1000)) {
return FlightRules.IFR;
}
if ((visibilityMeters !== undefined && visibilityMeters <= 8000) ||
(ceiling !== undefined && ceiling <= 3000)) {
return FlightRules.MVFR;
}
return FlightRules.VFR;
}
export function isMetarExpired(metar, options = {}) {
const now = new Date();
const { customMinutes, useStandardRules = true } = options;
if (customMinutes !== undefined) {
const expirationTime = new Date(metar.observationTime);
expirationTime.setMinutes(metar.observationTime.getMinutes() + customMinutes);
return now > expirationTime;
}
if (useStandardRules) {
const isSpecial = metar.raw.includes('SPECI');
const expirationTime = new Date(metar.observationTime);
const expirationMinutes = isSpecial ? 30 : 60;
expirationTime.setMinutes(metar.observationTime.getMinutes() + expirationMinutes);
return now > expirationTime;
}
// Fallback to the old behavior with a default of 60 minutes
const expirationTime = new Date(metar.observationTime);
expirationTime.setMinutes(metar.observationTime.getMinutes() + 60);
return now > expirationTime;
}
export function metarFlightRuleColor(metarData) {
const flightRule = metarFlightRule(metarData);
switch (flightRule) {
case FlightRules.VFR:
return 'green';
case FlightRules.MVFR:
return 'blue';
case FlightRules.IFR:
return 'red';
case FlightRules.LIFR:
return 'purple';
default:
return 'black';
}
}
export function metarColorCode(metarData) {
const visibility = metarData.visibility;
const ceiling = metarCeiling(metarData);
const windSpeed = metarData.wind?.speed;
const gustSpeed = metarData.wind?.gust;
let visibilityMeters;
if (visibility) {
visibilityMeters = visibility.value;
if (visibility.unit === 'sm') {
// TODO: Move this to a utility function so it can be used once the METAR is parsed
visibilityMeters = convert(visibilityMeters).from('mi').to('m');
}
}
if ((visibilityMeters !== undefined && visibilityMeters < 800) ||
(ceiling !== undefined && ceiling < 200) ||
(windSpeed !== undefined && windSpeed > 40) ||
(gustSpeed !== undefined && gustSpeed > 50)) {
return 'red';
}
if ((visibilityMeters !== undefined && visibilityMeters < 1600) ||
(ceiling !== undefined && ceiling < 400) ||
(windSpeed !== undefined && windSpeed > 30) ||
(gustSpeed !== undefined && gustSpeed > 40)) {
return 'amber';
}
if ((visibilityMeters !== undefined && visibilityMeters < 3200) ||
(ceiling !== undefined && ceiling < 700) ||
(windSpeed !== undefined && windSpeed > 20) ||
(gustSpeed !== undefined && gustSpeed > 30)) {
return 'yellow';
}
if ((visibilityMeters !== undefined && visibilityMeters < 5000) ||
(ceiling !== undefined && ceiling < 1500) ||
(windSpeed !== undefined && windSpeed > 15) ||
(gustSpeed !== undefined && gustSpeed > 20)) {
return 'blue';
}
return 'green';
}