metar-plot
Version:
METAR parsing and ploting library
292 lines (286 loc) • 9.11 kB
text/typescript
import { Weather, WEATHER } from "./parts/Weather"
import { Cloud , CLOUDS} from "./parts/Cloud"
import { Wind } from "./parts/Wind"
import { RVR } from "./parts/RVR"
import { Console } from "console";
//Meassage types
const TYPES = ["METAR", "SPECI"];
//Metar Object
export class METAR {
//Type of message
public type?: string
//Designates that this metar is a correction
public correction?: boolean;
//Station ID
public station: string;
/*Time of report
*METARs are only accurate to the day, hour, minute, and sec
*This will assume the metar was produced today*/
public time: Date;
//Designated if this metar was from an automated station
public auto?: boolean
//Wind speed, direction and unit
public wind: Wind = new Wind();
//Designated if ceiling & visablility are OK
public cavok?: boolean;
//Visability in meters
public visibility?: number;
public visibilityVariation?: number;
public visibilityVariationDirection?: number;
//List of weather conditions reported
public weather: Array<Weather> = new Array<Weather>();
//List of Cloud observations
public clouds: Array<Cloud> = new Array<Weather>();
//Tempuature (ºC)
public temperature?: number;
//Dew Point (ºC)
public dewpoint?: number;
//Altimeter reading (pressure) in inHg
public altimeter?: number;
public recentSignificantWeather?: string;
public recentSignificantWeatherDescription?: string;
public rvr?: RVR;
/**
* Extracted Metar data in a human readable format.
* @param metarString raw metar string if provided station and time will be ignored and replaced with the content in the raw METAR
* @param station staion name for instance creation
* @param time time for instance creation
*/
constructor(metarString?: string, station?: string, time?: Date) {
this.station = station ?? "----"
this.time = time ?? new Date()
if (metarString != null) {
parseMetar(metarString, this)
}
}
}
/**
* Parses a raw metar and binds or creates a METAR object
* @param metarString Raw METAR string
* @param ref Reference to a METAR object. This objects contents will be shallow replaced with the Raw metars values.
* Meaning values will be updated or added but not removed.
* @returns
*/
export function parseMetar(metarString: string, ref?: METAR): METAR {
let station = parseStation(metarString)
let time = parseDate(metarString);
if (ref != null) {
ref.station = station
ref.time = time
} else {
ref = new METAR(undefined, station, time)
}
//Parse Auto
ref.auto = parseAuto(metarString)
//Parse Wind
ref.wind = parseWind(metarString);
//Parse CAVOK
ref.cavok = parseCavok(metarString)
//Parse Visablility
ref.visibility = parseVisibility(metarString)
//Parse Runway VIS
//TODO
//Parse Weather
ref.weather = parseWeather(metarString);
//Parse Clouds
ref.clouds = parseClouds(metarString);
//Parse Temp Point Internations
let temps_int = parseTempInternation(metarString)
if (temps_int != null) {
ref.temperature = temps_int[0];
ref.dewpoint = temps_int[1];
}
//Parse Temp North american Will overwirte international since it is more precise
let temps_ne = parseTempNA(metarString)
if (temps_ne != null) {
ref.temperature = temps_ne[0];
ref.dewpoint = temps_ne[1];
}
//Parse Altimeter
ref.altimeter = parseAltimeter(metarString)
return ref;
}
/**
* Parses the station name form the metar
* @param metar raw metar
* @returns
*/
export function parseStation(metar: string): string {
let re = /^(METAR\s)?([A-Z]{1,4})\s/g
let matches = re.exec(metar)
if (matches != null) {
return matches[2]
} else {
throw new Error("Station could not be found invalid metar")
}
}
/**
* Parse Date object from metar.
* NOTE: Raw metar data does not contain month or year data. So this function assumes this metar was created in the current month and current year
* @param metar raw metar
* @returns
*/
export function parseDate(metar: string): Date {
let re = /([\d]{2})([\d]{2})([\d]{2})Z/g
let matches = re.exec(metar)
if (matches != null) {
var d = new Date();
d.setUTCDate(parseInt(matches[1]));
d.setUTCHours(parseInt(matches[2]));
d.setUTCMinutes(parseInt(matches[3]));
d.setUTCSeconds(0)
d.setUTCMilliseconds(0)
return d
} else {
throw new Error("Failed to parse Date")
}
}
/**
* Parses for CAVOK (Ceiling and visabiliy OK)
* @param metar raw metar
* @returns
*/
export function parseCavok(metar: string): boolean {
let re = /\sCAVOK\s/g
return metar.match(re) != null ? true : false
}
/**
* Parses for Automation
* @param metar raw metar
* @returns
*/
export function parseAuto(metar: string): boolean {
let re = /\s(AUTO)?(AO1)?(AO2)?\s/g
return metar.match(re) != null ? true : false
}
/**
* Parse international temp dewp point format.
* @param metar raw metar
* @returns
*/
export function parseTempInternation(metar: string): [number, number] | undefined {
let re = /\s(M)?(\d{2})\/(M)?(\d{2})\s/g
let matches = re.exec(metar)
if (matches != null) {
let temp = parseInt(matches[2]) * (matches[1] == null ? 1 : -1)
let dew_point = parseInt(matches[4]) * (matches[3] == null ? 1 : -1)
return [temp, dew_point]
}
}
/**
* Parse North American temp dew point format
* @param metar raw metar
* @returns
*/
export function parseTempNA(metar: string): [number, number] | undefined {
let re = /(T)(\d{1})(\d{2})(\d{1})(\d{1})(\d{2})(\d{1})/g
let matches = re.exec(metar)
if (matches != null) {
let temp = parseFloat(matches[3] + "." + matches[4]) * (matches[2] === "0" ? 1 : -1)
let dew_point = parseFloat(matches[6] + "." + matches[7]) * (matches[5] === "0" ? 1 : -1)
return [temp, dew_point]
}
}
/**
* Parse Weather items
* @param metar raw metar
* @returns
*/
export function parseWeather(metar: string): Array<Weather> {
let obs_keys = Object.keys(WEATHER).join('|').replace(/\+/g, "\\+")
let re = new RegExp(`\\s?(${obs_keys})\\s`, 'g')
let matches = metar.match(re)
if (matches != null) {
return matches.map(match => {
console.log(match)
let key = match.trim()
return {
abbreviation: key,
meaning: WEATHER[key].text
}
})
} else {
return new Array<Weather>()
}
}
/**
* Parse visability
* @param metar raw metar
* @returns
*/
export function parseVisibility(metar: string): number | undefined {
var re = /\s([0-9]{1,2})?\s?([0-9]{1}\/[0-9]{1})?(SM)\s|\s([0-9]{1,4})\s/g;
if (metar.match(re)) {
let vis_parts = re.exec(metar)
if (vis_parts != null) {
let meters = vis_parts[4]
let miles = vis_parts[1];
let frac_miles = vis_parts[2]
//Metric case ex: 1000, 9999
if(meters != null){
return parseInt(meters)
}
//whole miles case ex: 1SM 10SM
else if(frac_miles != null){
let total = 0.0;
if(miles != null){
total += parseFloat(miles)
}
total += parseFloat(eval(frac_miles))
return total * 1609.34
}
//factional miles case "1 1/2SM" "1/4SM"
else{
return parseInt(miles) * 1609.34
}
}
}
return undefined
}
/**
* Parse cloud coverages
* @param metarString raw metar
* @returns
*/
export function parseClouds(metarString: string): Cloud[] {
let re = /(NCD|SKC|CLR|NSC|FEW|SCT|BKN|OVC|VV)(\d{3})/g
let clouds = new Array<Cloud>()
let matches
while((matches = re.exec(metarString)) != null){
let cloud: Cloud = {
abbreviation: matches[1],
meaning: CLOUDS[matches[1]]?.text,
altitude: parseInt(matches[2]) * 100
}
clouds.push(cloud)
}
return clouds
}
/**
* Parse wind data
* @param metar raw metar
* @returns
*/
export function parseWind(metar: string): Wind {
let wind: Wind = new Wind()
let re = /\s(\d{3})(\d{2})(G)?(\d{2})?(KT|MPS)\s/g
let matches = re.exec(metar)
if (matches != null) {
wind.direction = parseInt(matches[1]);
wind.speed = parseInt(matches[2]);
wind.unit = matches[5]
}
return wind
}
export function parseAltimeter(metar: string): number | undefined{
let re = /(A|Q)(\d{2})(\d{2})/g
let matches = re.exec(metar)
if(matches != null){
if(matches[1] === "Q"){
let pressure = parseFloat(matches[2]+matches[3])
return parseFloat((pressure * 0.029529).toFixed(2))
}else{
return parseFloat(matches[2]+"."+matches[3])
}
}
}