diff --git a/gulp/publish-frontend.mjs b/gulp/publish-frontend.mjs index ce8021c..5a316a9 100644 --- a/gulp/publish-frontend.mjs +++ b/gulp/publish-frontend.mjs @@ -58,6 +58,15 @@ const jsVendorSources = [ 'server/scripts/vendor/auto/suncalc.js', ]; +// Copy metar-taf-parser separately since it's an ES module with locale dependencies +const metarVendorSources = [ + 'server/scripts/vendor/auto/metar-taf-parser.mjs', + 'server/scripts/vendor/auto/locale/en.js', +]; + +const copyMetarVendor = () => src(metarVendorSources, { base: 'server/scripts/vendor/auto' }) + .pipe(dest(`${RESOURCES_PATH}/vendor/auto`)); + const compressJsVendor = () => src(jsVendorSources) .pipe(concat('vendor.min.js')) .pipe(terser()) @@ -212,7 +221,7 @@ const buildPlaylist = async () => { return file('playlist.json', JSON.stringify(playlist)).pipe(dest('./dist')); }; -const buildDist = series(clean, parallel(buildJs, buildWorkers, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles, copyImageSources, buildPlaylist)); +const buildDist = series(clean, parallel(buildJs, buildWorkers, compressJsVendor, copyMetarVendor, copyCss, compressHtml, copyOtherFiles, copyImageSources, buildPlaylist)); // upload_images could be in parallel with upload, but _images logs a lot and has little changes // by running upload last the majority of the changes will be at the bottom of the log for easy viewing diff --git a/gulp/update-vendor.mjs b/gulp/update-vendor.mjs index 18b952e..550ff21 100644 --- a/gulp/update-vendor.mjs +++ b/gulp/update-vendor.mjs @@ -13,6 +13,12 @@ const vendorFiles = [ './node_modules/swiped-events/src/swiped-events.js', ]; +// Special handling for metar-taf-parser - only copy main file and English locale +const metarFiles = [ + './node_modules/metar-taf-parser/metar-taf-parser.js', + './node_modules/metar-taf-parser/locale/en.js', +]; + const copy = () => src(vendorFiles) .pipe(rename((path) => { path.dirname = path.dirname.toLowerCase(); @@ -22,6 +28,14 @@ const copy = () => src(vendorFiles) })) .pipe(dest('./server/scripts/vendor/auto')); -const updateVendor = series(clean, copy); +const copyMetar = () => src(metarFiles, { base: './node_modules/metar-taf-parser' }) + .pipe(rename((path) => { + path.basename = path.basename.toLowerCase(); + path.extname = path.extname.toLowerCase(); + if (path.basename === 'metar-taf-parser') path.extname = '.mjs'; + })) + .pipe(dest('./server/scripts/vendor/auto')); + +const updateVendor = series(clean, copy, copyMetar); export default updateVendor; diff --git a/package-lock.json b/package-lock.json index e815fbf..490115b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "dotenv": "^16.5.0", "ejs": "^3.1.5", - "express": "^5.1.0" + "express": "^5.1.0", + "metar-taf-parser": "^6.1.2" }, "devDependencies": { "@aws-sdk/client-cloudfront": "^3.609.0", @@ -7277,6 +7278,12 @@ "node": ">= 8" } }, + "node_modules/metar-taf-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/metar-taf-parser/-/metar-taf-parser-6.1.2.tgz", + "integrity": "sha512-NwsIQAlojiHhaGsF9tW/LxEEqAADTj4xy6B4RQiyaBtw9+IInOVMJwi9378iRHg6dGB+WlKRALRiX/vLu9fZjA==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", diff --git a/package.json b/package.json index 87095fe..b935d37 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "dependencies": { "dotenv": "^16.5.0", "ejs": "^3.1.5", - "express": "^5.1.0" + "express": "^5.1.0", + "metar-taf-parser": "^6.1.2" } } diff --git a/server/scripts/vendor/auto/locale/en.js b/server/scripts/vendor/auto/locale/en.js new file mode 100644 index 0000000..a772567 --- /dev/null +++ b/server/scripts/vendor/auto/locale/en.js @@ -0,0 +1,398 @@ +var en = { + CloudQuantity: { + BKN: "broken", + FEW: "few", + NSC: "no significant clouds.", + OVC: "overcast", + SCT: "scattered", + SKC: "sky clear", + }, + CloudType: { + AC: "Altocumulus", + AS: "Altostratus", + CB: "Cumulonimbus", + CC: "CirroCumulus", + CI: "Cirrus", + CS: "Cirrostratus", + CU: "Cumulus", + NS: "Nimbostratus", + SC: "Stratocumulus", + ST: "Stratus", + TCU: "Towering cumulus", + }, + Converter: { + D: "decreasing", + E: "East", + ENE: "East North East", + ESE: "East South East", + N: "North", + NE: "North East", + NNE: "North North East", + NNW: "North North West", + NSC: "no significant change", + NW: "North West", + S: "South", + SE: "South East", + SSE: "South South East", + SSW: "South South West", + SW: "South West", + U: "up rising", + VRB: "Variable", + W: "West", + WNW: "West North West", + WSW: "West South West", + }, + DepositBrakingCapacity: { + GOOD: "good", + MEDIUM: "medium", + MEDIUM_GOOD: "medium/good", + MEDIUM_POOR: "poor/medium", + NOT_REPORTED: "not reported", + POOR: "poor", + UNRELIABLE: "figures unreliable", + }, + DepositCoverage: { + FROM_11_TO_25: "from 11% to 25%", + FROM_26_TO_50: "from 26% to 50%", + FROM_51_TO_100: "from 51% to 100%", + LESS_10: "less than 10%", + NOT_REPORTED: "not reported", + }, + DepositThickness: { + CLOSED: "closed", + LESS_1_MM: "less than 1 mm", + NOT_REPORTED: "not reported", + THICKNESS_10: "10 cm", + THICKNESS_15: "15 cm", + THICKNESS_20: "20 cm", + THICKNESS_25: "25 cm", + THICKNESS_30: "30 cm", + THICKNESS_35: "35 cm", + THICKNESS_40: "40 cm or more", + }, + DepositType: { + CLEAR_DRY: "clear and dry", + COMPACTED_SNOW: "compacted or rolled snow", + DAMP: "damp", + DRY_SNOW: "dry snow", + FROZEN_RIDGES: "frozen ruts or ridges", + ICE: "ice", + NOT_REPORTED: "not reported", + RIME_FROST_COVERED: "rime or frost covered", + SLUSH: "slush", + WET_SNOW: "wet snow", + WET_WATER_PATCHES: "wet or water patches", + }, + Descriptive: { + BC: "patches", + BL: "blowing", + DR: "low drifting", + FZ: "freezing", + MI: "shallow", + PR: "partial", + SH: "showers of", + TS: "thunderstorm", + }, + Error: { + prefix: "An error occured. Error code n°", + }, + ErrorCode: { + AirportNotFound: "The airport was not found for this message.", + InvalidMessage: "The entered message is invalid.", + }, + Indicator: { + M: "less than", + P: "greater than", + }, + "intensity-plus": "Heavy", + Intensity: { + "-": "Light", + VC: "In the vicinity", + }, + MetarFacade: { + InvalidIcao: "Icao code is invalid.", + }, + Phenomenon: { + BR: "mist", + DS: "duststorm", + DU: "widespread dust", + DZ: "drizzle", + FC: "funnel cloud", + FG: "fog", + FU: "smoke", + GR: "hail", + GS: "small hail and/or snow pellets", + HZ: "haze", + IC: "ice crystals", + PL: "ice pellets", + PO: "dust or sand whirls", + PY: "spray", + RA: "rain", + SA: "sand", + SG: "snow grains", + SN: "snow", + SQ: "squall", + SS: "sandstorm", + TS: "thunderstorm", + UP: "unknown precipitation", + VA: "volcanic ash", + }, + Remark: { + ALQDS: "all quadrants", + AO1: "automated stations without a precipitation discriminator", + AO2: "automated station with a precipitation discriminator", + BASED: "based", + Barometer: [ + "Increase, then decrease", + "Increase, then steady, or increase then Increase more slowly", + "steady or unsteady increase", + "Decrease or steady, then increase; or increase then increase more rapidly", + "Steady", + "Decrease, then increase", + "Decrease then steady; or decrease then decrease more slowly", + "Steady or unsteady decrease", + "Steady or increase, then decrease; or decrease then decrease more rapidly", + ], + Ceiling: { + Height: "ceiling varying between {0} and {1} feet", + Second: { + Location: "ceiling of {0} feet mesured by a second sensor located at {1}", + }, + }, + DSNT: "distant", + FCST: "forecast", + FUNNELCLOUD: "funnel cloud", + HVY: "heavy", + Hail: { + "0": "largest hailstones with a diameter of {0} inches", + LesserThan: "largest hailstones with a diameter less than {0} inches", + }, + Hourly: { + Maximum: { + Minimum: { + Temperature: "24-hour maximum temperature of {0}°C and 24-hour minimum temperature of {1}°C", + }, + Temperature: "6-hourly maximum temperature of {0}°C", + }, + Minimum: { + Temperature: "6-hourly minimum temperature of {0}°C", + }, + Temperature: { + "0": "hourly temperature of {0}°C", + Dew: { + Point: "hourly temperature of {0}°C and dew point of {1}°C", + }, + }, + }, + Ice: { + Accretion: { + Amount: "{0}/100 of an inch of ice accretion in the past {1} hour(s)", + }, + }, + LGT: "light", + LTG: "lightning", + MOD: "moderate", + NXT: "next", + ON: "on", + Obscuration: "{0} layer at {1} feet composed of {2}", + PRESFR: "pressure falling rapidly", + PRESRR: "pressure rising rapidly", + PeakWind: "peak wind of {1} knots from {0} degrees at {2}:{3}", + Precipitation: { + Amount: { + "24": "{0} inches of precipitation fell in the last 24 hours", + "3": { + "6": "{1} inches of precipitation fell in the last {0} hours", + }, + Hourly: "{0}/100 of an inch of precipitation fell in the last hour", + }, + Beg: { + "0": "{0} {1} beginning at {2}:{3}", + End: "{0} {1} beginning at {2}:{3} ending at {4}:{5}", + }, + End: "{0} {1} ending at {2}:{3}", + }, + Pressure: { + Tendency: "of {0} hectopascals in the past 3 hours", + }, + SLPNO: "sea level pressure not available", + Sea: { + Level: { + Pressure: "sea level pressure of {0} HPa", + }, + }, + Second: { + Location: { + Visibility: "visibility of {0} SM mesured by a second sensor located at {1}", + }, + }, + Sector: { + Visibility: "visibility of {1} SM in the {0} direction", + }, + Snow: { + Depth: "snow depth of {0} inches", + Increasing: { + Rapidly: "snow depth increase of {0} inches in the past hour with a total depth on the ground of {1} inches", + }, + Pellets: "{0} snow pellets", + }, + Sunshine: { + Duration: "{0} minutes of sunshine", + }, + Surface: { + Visibility: "surface visibility of {0} statute miles", + }, + TORNADO: "tornado", + Thunderstorm: { + Location: { + "0": "thunderstorm {0} of the station", + Moving: "thunderstorm {0} of the station moving towards {1}", + }, + }, + Tornadic: { + Activity: { + BegEnd: "{0} beginning at {1}:{2} ending at {3}:{4} {5} SM {6} of the station", + Beginning: "{0} beginning at {1}:{2} {3} SM {4} of the station", + Ending: "{0} ending at {1}:{2} {3} SM {4} of the station", + }, + }, + Tower: { + Visibility: "control tower visibility of {0} statute miles", + }, + VIRGA: "virga", + Variable: { + Prevailing: { + Visibility: "variable prevailing visibility between {0} and {1} SM", + }, + Sky: { + Condition: { + "0": "cloud layer varying between {0} and {1}", + Height: "cloud layer at {0} feet varying between {1} and {2}", + }, + }, + }, + Virga: { + Direction: "virga {0} from the station", + }, + WATERSPOUT: "waterspout", + Water: { + Equivalent: { + Snow: { + Ground: "water equivalent of {0} inches of snow", + }, + }, + }, + WindShift: { + "0": "wind shift at {0}:{1}", + FROPA: "wind shift accompanied by frontal passage at {0}:{1}", + }, + }, + TimeIndicator: { + AT: "at", + FM: "From", + TL: "until", + }, + ToString: { + airport: "airport", + altimeter: "altimeter (hPa)", + amendment: "amendment", + auto: "auto", + cavok: "cavok", + clouds: "clouds", + day: { + hour: "hour of the day", + month: "day of the month", + }, + deposit: { + braking: "braking capacity", + coverage: "coverage", + thickness: "thickness", + type: "type of deposit", + }, + descriptive: "descriptive", + dew: { + point: "dew point", + }, + end: { + day: { + month: "end day of the month", + }, + hour: { + day: "end hour of the day", + }, + }, + height: { + feet: "height (ft)", + meter: "height (m)", + }, + indicator: "indicator", + intensity: "intensity", + message: "original message", + name: "name", + nosig: "nosig", + phenomenons: "phenomenons", + probability: "probability", + quantity: "quantity", + remark: "remarks", + report: { + time: "time of report", + }, + runway: { + info: "runways information", + }, + start: { + day: { + month: "starting day of the month", + }, + hour: { + day: "starting hour of the day", + }, + minute: "starting minute", + }, + temperature: { + "0": "temperature (°C)", + max: "maximum temperature (°C)", + min: "minimum temperature (°C)", + }, + trend: "trend", + trends: "trends", + type: "type", + vertical: { + visibility: "vertical visibility (ft)", + }, + visibility: { + main: "main visibility", + max: "maximum visibility", + min: { + "0": "minimum visibility", + direction: "minimum visibility direction", + }, + }, + weather: { + conditions: "weather conditions", + }, + wind: { + direction: { + "0": "direction", + degrees: "direction (degrees)", + }, + gusts: "gusts", + max: { + variation: "maximal wind variation", + }, + min: { + variation: "minimal wind variation", + }, + speed: "speed", + unit: "unit", + }, + }, + WeatherChangeType: { + BECMG: "Becoming", + FM: "From", + PROB: "Probability", + TEMPO: "Temporary", + }, +}; + +export { en as default }; diff --git a/server/scripts/vendor/auto/metar-taf-parser.mjs b/server/scripts/vendor/auto/metar-taf-parser.mjs new file mode 100644 index 0000000..c6a17aa --- /dev/null +++ b/server/scripts/vendor/auto/metar-taf-parser.mjs @@ -0,0 +1,2840 @@ +import en from './locale/en.js'; + +/****************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */ + +function __classPrivateFieldGet(receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +} + +class ParseError extends Error { + constructor(message) { + super(message); + this.name = "ParseError"; + Object.setPrototypeOf(this, new.target.prototype); + } +} +class InvalidWeatherStatementError extends ParseError { + constructor(cause) { + super(typeof cause === "string" + ? `Invalid weather string: ${cause}` + : "Invalid weather string"); + this.name = "InvalidWeatherStatementError"; + Object.setPrototypeOf(this, new.target.prototype); + if (typeof cause !== "string") + this.cause = cause; + } +} +/** + * Thrown when command marked as canParse, but couldn't parse when + * executing (for example, an invalid CloudQuantity) + */ +class CommandExecutionError extends ParseError { + constructor(message) { + super(message); + this.name = "CommandExecutionError"; + Object.setPrototypeOf(this, new.target.prototype); + } +} +/** + * Should never occur + */ +class UnexpectedParseError extends ParseError { + constructor(message) { + super(message); + this.name = "UnexpectedParseError"; + Object.setPrototypeOf(this, new.target.prototype); + } +} + +/** + * Split behaving similar to Python's implementation + */ +function pySplit(string, separator, n) { + let split = string.split(separator); + // Note: Python implementation will automatically trim empty values if + // separator is undefined. Since this function is kinda meh, we'll just do it + // for any spaces (pretty close to their implementation, since a space is the + // default character to split on) + // + // https://docs.python.org/3/library/stdtypes.html?highlight=split#str.split + if (separator === " ") + split = split.filter((n) => n); + if (n == null || split.length <= n) + return split; + const out = split.slice(0, n); + out.push(split.slice(n).join(separator)); + return out; +} +/** + * Access nested object properties by string path + * + * https://stackoverflow.com/a/22129960 + */ +function resolve(obj, path, separator = ".") { + const properties = Array.isArray(path) ? path : path.split(separator); + return properties.reduce((prev, curr) => prev?.[curr], obj); +} +/** + * For safely casting input values + * @param input String that is expected to be in the snum + * @param enumExpected The enum to cast the input value to + * @throws RemarkExecutionError when input is not a key of enum + */ +function as(input, enumExpected) { + if (!Object.values(enumExpected).includes(input)) + throw new CommandExecutionError(`${input} not found in ${Object.values(enumExpected)}`); + return input; +} + +function _(path, lang) { + const translation = resolve(lang, path); + if (!translation || typeof translation !== "string") + return undefined; + return translation; +} +function format(message, ...args) { + if (!message) + return; + // All arguments must be defined, otherwise nothing is returned + for (const arg of args) { + if (arg === undefined) + return; + } + return message.replace(/{\d+}/g, (match) => { + const index = +match.slice(1, -1); + return `${args[index]}`; + }); +} + +class Command { + constructor(locale) { + this.locale = locale; + } +} + +var _CeilingHeightCommand_regex; +class CeilingHeightCommand extends Command { + constructor() { + super(...arguments); + _CeilingHeightCommand_regex.set(this, /^CIG (\d{3})V(\d{3})\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _CeilingHeightCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _CeilingHeightCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const min = +matches[1] * 100; + const max = +matches[2] * 100; + const description = format(_("Remark.Ceiling.Height", this.locale), min, max); + remark.push({ + type: RemarkType.CeilingHeight, + description, + raw: matches[0], + min, + max, + }); + return [code.replace(__classPrivateFieldGet(this, _CeilingHeightCommand_regex, "f"), "").trim(), remark]; + } +} +_CeilingHeightCommand_regex = new WeakMap(); + +var _CeilingSecondLocationCommand_regex; +class CeilingSecondLocationCommand extends Command { + constructor() { + super(...arguments); + _CeilingSecondLocationCommand_regex.set(this, /^CIG (\d{3}) (\w+)\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _CeilingSecondLocationCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _CeilingSecondLocationCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const height = +matches[1] * 100; + const location = matches[2]; + const description = format(_("Remark.Ceiling.Second.Location", this.locale), height, location); + remark.push({ + type: RemarkType.CeilingSecondLocation, + description, + raw: matches[0], + height, + location, + }); + return [code.replace(__classPrivateFieldGet(this, _CeilingSecondLocationCommand_regex, "f"), "").trim(), remark]; + } +} +_CeilingSecondLocationCommand_regex = new WeakMap(); + +var CloudQuantity; +(function (CloudQuantity) { + /** + * Sky clear + */ + CloudQuantity["SKC"] = "SKC"; + /** + * Few + */ + CloudQuantity["FEW"] = "FEW"; + /** + * Broken + */ + CloudQuantity["BKN"] = "BKN"; + /** + * Scattered + */ + CloudQuantity["SCT"] = "SCT"; + /** + * Overcast + */ + CloudQuantity["OVC"] = "OVC"; + /** + * No significant cloud + */ + CloudQuantity["NSC"] = "NSC"; +})(CloudQuantity || (CloudQuantity = {})); +var CloudType; +(function (CloudType) { + /** + * Cumulonimbus + */ + CloudType["CB"] = "CB"; + /** + * Towering cumulus, cumulus congestus + */ + CloudType["TCU"] = "TCU"; + /** + * Cirrus + */ + CloudType["CI"] = "CI"; + /** + * Cirrocumulus + */ + CloudType["CC"] = "CC"; + /** + * Cirrostratus + */ + CloudType["CS"] = "CS"; + /** + * Altocumulus + */ + CloudType["AC"] = "AC"; + /** + * Stratus + */ + CloudType["ST"] = "ST"; + /** + * Cumulus + */ + CloudType["CU"] = "CU"; + /** + * Astrostratus + */ + CloudType["AS"] = "AS"; + /** + * Nimbostratus + */ + CloudType["NS"] = "NS"; + /** + * Stratocumulus + */ + CloudType["SC"] = "SC"; +})(CloudType || (CloudType = {})); +/** + * Moderate has no qualifier. + */ +var Intensity; +(function (Intensity) { + Intensity["LIGHT"] = "-"; + /** + * Heavy or well-developed + */ + Intensity["HEAVY"] = "+"; + Intensity["IN_VICINITY"] = "VC"; +})(Intensity || (Intensity = {})); +var Descriptive; +(function (Descriptive) { + Descriptive["SHOWERS"] = "SH"; + Descriptive["SHALLOW"] = "MI"; + Descriptive["PATCHES"] = "BC"; + Descriptive["PARTIAL"] = "PR"; + Descriptive["DRIFTING"] = "DR"; + Descriptive["THUNDERSTORM"] = "TS"; + Descriptive["BLOWING"] = "BL"; + Descriptive["FREEZING"] = "FZ"; +})(Descriptive || (Descriptive = {})); +var Phenomenon; +(function (Phenomenon) { + Phenomenon["RAIN"] = "RA"; + Phenomenon["DRIZZLE"] = "DZ"; + Phenomenon["SNOW"] = "SN"; + Phenomenon["SNOW_GRAINS"] = "SG"; + Phenomenon["ICE_PELLETS"] = "PL"; + Phenomenon["ICE_CRYSTALS"] = "IC"; + Phenomenon["HAIL"] = "GR"; + Phenomenon["SMALL_HAIL"] = "GS"; + Phenomenon["UNKNOW_PRECIPITATION"] = "UP"; + Phenomenon["FOG"] = "FG"; + Phenomenon["VOLCANIC_ASH"] = "VA"; + Phenomenon["MIST"] = "BR"; + Phenomenon["HAZE"] = "HZ"; + Phenomenon["WIDESPREAD_DUST"] = "DU"; + Phenomenon["SMOKE"] = "FU"; + Phenomenon["SAND"] = "SA"; + Phenomenon["SPRAY"] = "PY"; + Phenomenon["SQUALL"] = "SQ"; + Phenomenon["SAND_WHIRLS"] = "PO"; + Phenomenon["THUNDERSTORM"] = "TS"; + Phenomenon["DUSTSTORM"] = "DS"; + Phenomenon["SANDSTORM"] = "SS"; + Phenomenon["FUNNEL_CLOUD"] = "FC"; +})(Phenomenon || (Phenomenon = {})); +var TimeIndicator; +(function (TimeIndicator) { + TimeIndicator["AT"] = "AT"; + TimeIndicator["FM"] = "FM"; + TimeIndicator["TL"] = "TL"; +})(TimeIndicator || (TimeIndicator = {})); +/** + * https://www.aviationweather.gov/taf/decoder + */ +var WeatherChangeType; +(function (WeatherChangeType) { + /** + * FROM Group + * + * ie. `FM1600` + * + * The FM group is used when a rapid change, usually occuring in less than one + * hour, in prevailing conditions is expected. Typically, a rapid change of + * prevailing conditions to more or less a completely new set of prevailing + * conditions is associated with a synoptic feature passing through the + * terminal area (cold or warm frontal passage). Appended to the FM indicator + * is the four-digit hour and minute the change is expected to begin and + * continues until the next change group or until the end of the current + * forecast. + * + * A FM group will mark the beginning of a new line in a TAF report. Each FM + * group contains all the required elements -- wind, visibility, weather, and + * sky condition. Weather will be omitted in FM groups when it is not + * significant to aviation. FM groups will not include the contraction NSW. + * + * Examples: + * + * 1. `FM0100 SKC` - After 0100Z sky clear + * 2. `FM1430 OVC020` - After 1430Z ceiling two thousand overcast + */ + WeatherChangeType["FM"] = "FM"; + /** + * BECOMING Group + * + * ie. `BECMG 2224` + * + * The BECMG group is used when a gradual change in conditions is expected + * over a longer time period, usually two hours. The time period when the + * change is expected is a four-digit group with the beginning hour and ending + * hour of the change period which follows the BECMG indicator. The gradual + * change will occur at an unspecified time within this time period. Only the + * conditions are carried over from the previous time group. + * + * Example: + * + * 1. `OVC012 BECMG 1416 BKN020` - Ceiling one thousand two hundred overcast. + * Then a gradual change to ceiling two thousand broken between 1400Z and + * 1600Z. + */ + WeatherChangeType["BECMG"] = "BECMG"; + /** + * TEMPORARY Group + * + * ie. `TEMPO 1316` + * + * The TEMPO group is used for any conditions in wind, visibility, weather, or + * sky condition which are expected to last for generally less than an hour at + * a time (occasional), and are expected to occur during less than half the + * time period. The TEMPO indicator is followed by a four-digit group giving + * the beginning hour and ending hour of the time period during which the + * temporary conditions are expected. Only the changing forecast + * meteorological conditions are included in TEMPO groups. The omitted + * conditions are carried over from the previous time group. + * + * Examples: + * + * 1. `SCT030 TEMPO 1923 BKN030` - Three thousand scattered with occasional + * ceilings three thousand broken between 1900Z and 2300Z. + * 2. `4SM HZ TEMPO 0006 2SM BR HZ` - Visibility four in haze with occasional + * visibility two in mist and haze between 0000Z and 0600Z. + */ + WeatherChangeType["TEMPO"] = "TEMPO"; + /** + * Probability Forecast + * + * ie. `PROB40 0006` + * + * The probability or chance of thunderstorms or other precipitation events + * occuring, along with associated weather conditions (wind, visibility, and + * sky conditions). + * + * The PROB40 group is used when the occurrence of thunderstorms or + * precipitation is in the 30% to less than 50% range, thus the probability + * value 40 is appended to the PROB contraction. This is followed by a + * four-digit group giving the beginning hour and ending hour of the time + * period during which the thunderstorms or precipitation is expected. + * + * Note: PROB40 will not be shown during the first six hours of a forecast. + * + * Examples: + * + * 1. `PROB40 2102 1/2SM +TSRA` - Chance between 2100Z and 0200Z of + * visibility one-half thunderstorm, heavy rain. + * 2. `PROB40 1014 1SM RASN` - Chance between 1000Z and 1400Z of visibility + * one rain and snow. + * 3. `PROB40 2024 2SM FZRA` - Chance between 2000Z and 0000Z of visibility + * two freezing rain. + + */ + WeatherChangeType["PROB"] = "PROB"; +})(WeatherChangeType || (WeatherChangeType = {})); +var Direction; +(function (Direction) { + Direction["E"] = "E"; + Direction["ENE"] = "ENE"; + Direction["ESE"] = "ESE"; + Direction["N"] = "N"; + Direction["NE"] = "NE"; + Direction["NNE"] = "NNE"; + Direction["NNW"] = "NNW"; + Direction["NW"] = "NW"; + Direction["S"] = "S"; + Direction["SE"] = "SE"; + Direction["SSE"] = "SSE"; + Direction["SSW"] = "SSW"; + Direction["SW"] = "SW"; + Direction["W"] = "W"; + Direction["WNW"] = "WNW"; + Direction["WSW"] = "WSW"; +})(Direction || (Direction = {})); +var DistanceUnit; +(function (DistanceUnit) { + DistanceUnit["Meters"] = "m"; + DistanceUnit["StatuteMiles"] = "SM"; +})(DistanceUnit || (DistanceUnit = {})); +var SpeedUnit; +(function (SpeedUnit) { + SpeedUnit["Knot"] = "KT"; + SpeedUnit["MetersPerSecond"] = "MPS"; + SpeedUnit["KilometersPerHour"] = "KM/H"; +})(SpeedUnit || (SpeedUnit = {})); +/** + * Used to indicate the actual value is greater than or less than the value written + * + * For example, + * + * 1. `P6SM` = visibility greater than 6 statute miles + * 2. `M1/4SM` = visibility less than 1/4 statute mile + */ +var ValueIndicator; +(function (ValueIndicator) { + ValueIndicator["GreaterThan"] = "P"; + ValueIndicator["LessThan"] = "M"; +})(ValueIndicator || (ValueIndicator = {})); +var RunwayInfoTrend; +(function (RunwayInfoTrend) { + RunwayInfoTrend["Uprising"] = "U"; + RunwayInfoTrend["Decreasing"] = "D"; + RunwayInfoTrend["NoSignificantChange"] = "N"; +})(RunwayInfoTrend || (RunwayInfoTrend = {})); +var RunwayInfoUnit; +(function (RunwayInfoUnit) { + RunwayInfoUnit["Feet"] = "FT"; + RunwayInfoUnit["Meters"] = "m"; +})(RunwayInfoUnit || (RunwayInfoUnit = {})); + +function degreesToCardinal(input) { + const degrees = +input; + if (isNaN(degrees)) + return "VRB"; + const dirs = [ + "N", + "NNE", + "NE", + "ENE", + "E", + "ESE", + "SE", + "SSE", + "S", + "SSW", + "SW", + "WSW", + "W", + "WNW", + "NW", + "NNW", + ]; + const ix = Math.floor((degrees + 11.25) / 22.5); + return dirs[ix % 16]; +} +function convertVisibility(input) { + if (input === "9999") + return { + indicator: ValueIndicator.GreaterThan, + value: +input, + unit: DistanceUnit.Meters, + }; + return { + value: +input, + unit: DistanceUnit.Meters, + }; +} +/** + * @param input May start with P or M, and must end with SM + * @returns Distance + */ +function convertNauticalMilesVisibility(input) { + let indicator; + let index = 0; + if (input.startsWith("P")) { + indicator = ValueIndicator.GreaterThan; + index = 1; + } + else if (input.startsWith("M")) { + indicator = ValueIndicator.LessThan; + index = 1; + } + return { + indicator, + value: convertFractionalAmount(input.slice(index, -2)), + unit: DistanceUnit.StatuteMiles, + }; +} +/** + * Converts fractional and/or whole amounts + * + * Example "1/3", "1 1/3" and "1" + */ +function convertFractionalAmount(input) { + const [whole, fraction] = input.split(" "); + if (!fraction) + return parseFraction(whole); + return +whole + parseFraction(fraction); +} +function parseFraction(input) { + const [top, bottom] = input.split("/"); + if (!bottom) + return +top; + return Math.round((+top / +bottom) * 100) / 100; +} +function convertTemperature(input) { + if (input.startsWith("M")) + return -pySplit(input, "M")[1]; + return +input; +} +function convertInchesMercuryToPascal(input) { + return 33.8639 * input; +} +/** + * Converts number `.toFixed(1)` before outputting to match python implementation + */ +function convertTemperatureRemarks(sign, temperature) { + const temp = +temperature / 10; + if (sign === "0") + return temp; + return -temp; +} +function convertPrecipitationAmount(amount) { + return +amount / 100; +} + +var _HailSizeCommand_regex; +class HailSizeCommand extends Command { + constructor() { + super(...arguments); + _HailSizeCommand_regex.set(this, /^GR ((\d\/\d)|((\d) ?(\d\/\d)?))/); + } + canParse(code) { + return __classPrivateFieldGet(this, _HailSizeCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _HailSizeCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const description = format(_("Remark.Hail.0", this.locale), matches[1]); + remark.push({ + type: RemarkType.HailSize, + description, + raw: matches[0], + size: convertFractionalAmount(matches[1]), + }); + return [code.replace(__classPrivateFieldGet(this, _HailSizeCommand_regex, "f"), "").trim(), remark]; + } +} +_HailSizeCommand_regex = new WeakMap(); + +var _HourlyMaximumMinimumTemperatureCommand_regex; +class HourlyMaximumMinimumTemperatureCommand extends Command { + constructor() { + super(...arguments); + _HourlyMaximumMinimumTemperatureCommand_regex.set(this, /^4([01])(\d{3})([01])(\d{3})\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _HourlyMaximumMinimumTemperatureCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _HourlyMaximumMinimumTemperatureCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const description = format(_("Remark.Hourly.Maximum.Minimum.Temperature", this.locale), convertTemperatureRemarks(matches[1], matches[2]).toFixed(1), convertTemperatureRemarks(matches[3], matches[4]).toFixed(1)); + remark.push({ + type: RemarkType.HourlyMaximumMinimumTemperature, + description: description, + raw: matches[0], + max: convertTemperatureRemarks(matches[1], matches[2]), + min: convertTemperatureRemarks(matches[3], matches[4]), + }); + return [code.replace(__classPrivateFieldGet(this, _HourlyMaximumMinimumTemperatureCommand_regex, "f"), "").trim(), remark]; + } +} +_HourlyMaximumMinimumTemperatureCommand_regex = new WeakMap(); + +var _HourlyMaximumTemperatureCommand_regex; +class HourlyMaximumTemperatureCommand extends Command { + constructor() { + super(...arguments); + _HourlyMaximumTemperatureCommand_regex.set(this, /^1([01])(\d{3})\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _HourlyMaximumTemperatureCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _HourlyMaximumTemperatureCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const description = format(_("Remark.Hourly.Maximum.Temperature", this.locale), convertTemperatureRemarks(matches[1], matches[2]).toFixed(1)); + remark.push({ + type: RemarkType.HourlyMaximumTemperature, + description: description, + raw: matches[0], + max: convertTemperatureRemarks(matches[1], matches[2]), + }); + return [code.replace(__classPrivateFieldGet(this, _HourlyMaximumTemperatureCommand_regex, "f"), "").trim(), remark]; + } +} +_HourlyMaximumTemperatureCommand_regex = new WeakMap(); + +var _HourlyMinimumTemperatureCommand_regex; +class HourlyMinimumTemperatureCommand extends Command { + constructor() { + super(...arguments); + _HourlyMinimumTemperatureCommand_regex.set(this, /^2([01])(\d{3})\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _HourlyMinimumTemperatureCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _HourlyMinimumTemperatureCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const description = format(_("Remark.Hourly.Minimum.Temperature", this.locale), convertTemperatureRemarks(matches[1], matches[2]).toFixed(1)); + remark.push({ + type: RemarkType.HourlyMinimumTemperature, + description, + raw: matches[0], + min: convertTemperatureRemarks(matches[1], matches[2]), + }); + return [code.replace(__classPrivateFieldGet(this, _HourlyMinimumTemperatureCommand_regex, "f"), "").trim(), remark]; + } +} +_HourlyMinimumTemperatureCommand_regex = new WeakMap(); + +var _HourlyPrecipitationAmountCommand_regex; +class HourlyPrecipitationAmountCommand extends Command { + constructor() { + super(...arguments); + _HourlyPrecipitationAmountCommand_regex.set(this, /^P(\d{4})\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _HourlyPrecipitationAmountCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _HourlyPrecipitationAmountCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const amount = +matches[1]; + const description = format(_("Remark.Precipitation.Amount.Hourly", this.locale), amount); + remark.push({ + type: RemarkType.HourlyPrecipitationAmount, + description, + raw: matches[0], + amount: amount / 100, + }); + return [code.replace(__classPrivateFieldGet(this, _HourlyPrecipitationAmountCommand_regex, "f"), "").trim(), remark]; + } +} +_HourlyPrecipitationAmountCommand_regex = new WeakMap(); + +var _HourlyPressureCommand_regex; +class HourlyPressureCommand extends Command { + constructor() { + super(...arguments); + _HourlyPressureCommand_regex.set(this, /^5(\d)(\d{3})\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _HourlyPressureCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _HourlyPressureCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const part1 = _(`Remark.Barometer.${+matches[1]}`, this.locale); + const part2 = format(_("Remark.Pressure.Tendency", this.locale), +matches[2] / 10); + const description = part1 != null && part2 != null ? `${part1} ${part2}` : undefined; + remark.push({ + type: RemarkType.HourlyPressure, + description, + raw: matches[0], + code: +matches[1], + pressureChange: +matches[2] / 10, + }); + return [code.replace(__classPrivateFieldGet(this, _HourlyPressureCommand_regex, "f"), "").trim(), remark]; + } +} +_HourlyPressureCommand_regex = new WeakMap(); + +var _HourlyTemperatureDewPointCommand_regex; +class HourlyTemperatureDewPointCommand extends Command { + constructor() { + super(...arguments); + _HourlyTemperatureDewPointCommand_regex.set(this, /^T([01])(\d{3})(([01])(\d{3}))?/); + } + canParse(code) { + return __classPrivateFieldGet(this, _HourlyTemperatureDewPointCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _HourlyTemperatureDewPointCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const temperature = convertTemperatureRemarks(matches[1], matches[2]); + if (!matches[3]) { + const description = format(_("Remark.Hourly.Temperature.0", this.locale), temperature.toFixed(1)); + remark.push({ + type: RemarkType.HourlyTemperatureDewPoint, + description, + raw: matches[0], + temperature, + }); + } + else { + const dewPoint = convertTemperatureRemarks(matches[4], matches[5]); + const description = format(_("Remark.Hourly.Temperature.Dew.Point", this.locale), temperature.toFixed(1), dewPoint.toFixed(1)); + remark.push({ + type: RemarkType.HourlyTemperatureDewPoint, + description, + raw: matches[0], + temperature, + dewPoint, + }); + } + return [code.replace(__classPrivateFieldGet(this, _HourlyTemperatureDewPointCommand_regex, "f"), "").trim(), remark]; + } +} +_HourlyTemperatureDewPointCommand_regex = new WeakMap(); + +var _IceAccretionCommand_regex; +class IceAccretionCommand extends Command { + constructor() { + super(...arguments); + _IceAccretionCommand_regex.set(this, /^l(\d)(\d{3})\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _IceAccretionCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _IceAccretionCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const description = format(_("Remark.Ice.Accretion.Amount", this.locale), +matches[2], +matches[1]); + remark.push({ + type: RemarkType.IceAccretion, + description, + raw: matches[0], + amount: +matches[2] / 100, + periodInHours: +matches[1], + }); + return [code.replace(__classPrivateFieldGet(this, _IceAccretionCommand_regex, "f"), "").trim(), remark]; + } +} +_IceAccretionCommand_regex = new WeakMap(); + +var _ObscurationCommand_regex; +class ObscurationCommand extends Command { + constructor() { + super(...arguments); + _ObscurationCommand_regex.set(this, /^([A-Z]{2}) ([A-Z]{3})(\d{3})/); + } + canParse(code) { + return __classPrivateFieldGet(this, _ObscurationCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _ObscurationCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const quantity = as(matches[2], CloudQuantity); + const height = 100 * +matches[3]; + const phenomenon = as(matches[1], Phenomenon); + const description = format(_("Remark.Obscuration", this.locale), _(`CloudQuantity.${quantity}`, this.locale), height, _(`Phenomenon.${phenomenon}`, this.locale)); + remark.push({ + type: RemarkType.Obscuration, + description, + raw: matches[0], + quantity, + height, + phenomenon, + }); + return [code.replace(__classPrivateFieldGet(this, _ObscurationCommand_regex, "f"), "").trim(), remark]; + } +} +_ObscurationCommand_regex = new WeakMap(); + +var _PrecipitationAmount24HourCommand_regex; +class PrecipitationAmount24HourCommand extends Command { + constructor() { + super(...arguments); + _PrecipitationAmount24HourCommand_regex.set(this, /^7(\d{4})\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _PrecipitationAmount24HourCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _PrecipitationAmount24HourCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const amount = convertPrecipitationAmount(matches[1]); + const description = format(_("Remark.Precipitation.Amount.24", this.locale), amount); + remark.push({ + type: RemarkType.PrecipitationAmount24Hour, + description, + raw: matches[0], + amount, + }); + return [code.replace(__classPrivateFieldGet(this, _PrecipitationAmount24HourCommand_regex, "f"), "").trim(), remark]; + } +} +_PrecipitationAmount24HourCommand_regex = new WeakMap(); + +var _PrecipitationAmount36HourCommand_regex; +class PrecipitationAmount36HourCommand extends Command { + constructor() { + super(...arguments); + _PrecipitationAmount36HourCommand_regex.set(this, /^([36])(\d{4})\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _PrecipitationAmount36HourCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _PrecipitationAmount36HourCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const periodInHours = +matches[1]; + const amount = convertPrecipitationAmount(matches[2]); + const description = format(_("Remark.Precipitation.Amount.3.6", this.locale), periodInHours, amount); + remark.push({ + type: RemarkType.PrecipitationAmount36Hour, + description, + raw: matches[0], + periodInHours, + amount, + }); + return [code.replace(__classPrivateFieldGet(this, _PrecipitationAmount36HourCommand_regex, "f"), "").trim(), remark]; + } +} +_PrecipitationAmount36HourCommand_regex = new WeakMap(); + +var _PrecipitationBegEndCommand_regex; +class PrecipitationBegEndCommand extends Command { + constructor() { + super(...arguments); + _PrecipitationBegEndCommand_regex.set(this, /^(([A-Z]{2})?([A-Z]{2})B(\d{2})?(\d{2})E(\d{2})?(\d{2}))/); + } + canParse(code) { + return __classPrivateFieldGet(this, _PrecipitationBegEndCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _PrecipitationBegEndCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const descriptive = matches[2] ? as(matches[2], Descriptive) : undefined; + const phenomenon = as(matches[3], Phenomenon); + const description = format(_("Remark.Precipitation.Beg.End", this.locale), descriptive ? _(`Descriptive.${descriptive}`, this.locale) : "", _(`Phenomenon.${phenomenon}`, this.locale), matches[4] || "", matches[5], matches[6] || "", matches[7]); + remark.push({ + type: RemarkType.PrecipitationBegEnd, + description, + raw: matches[0], + descriptive, + phenomenon, + startHour: matches[4] ? +matches[4] : undefined, + startMin: +matches[5], + endHour: matches[6] ? +matches[6] : undefined, + endMin: +matches[7], + }); + return [code.replace(__classPrivateFieldGet(this, _PrecipitationBegEndCommand_regex, "f"), "").trim(), remark]; + } +} +_PrecipitationBegEndCommand_regex = new WeakMap(); + +var _PrevailingVisibilityCommand_regex; +class PrevailingVisibilityCommand extends Command { + constructor() { + super(...arguments); + _PrevailingVisibilityCommand_regex.set(this, /^VIS ((\d)*( )?(\d?\/?\d))V((\d)*( )?(\d?\/?\d))/); + } + canParse(code) { + return __classPrivateFieldGet(this, _PrevailingVisibilityCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _PrevailingVisibilityCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const minVisibility = matches[1]; + const maxVisibility = matches[5]; + const description = format(_("Remark.Variable.Prevailing.Visibility", this.locale), minVisibility, maxVisibility); + remark.push({ + type: RemarkType.PrevailingVisibility, + description, + raw: matches[0], + minVisibility: convertFractionalAmount(minVisibility), + maxVisibility: convertFractionalAmount(maxVisibility), + }); + return [code.replace(__classPrivateFieldGet(this, _PrevailingVisibilityCommand_regex, "f"), "").trim(), remark]; + } +} +_PrevailingVisibilityCommand_regex = new WeakMap(); + +var _SeaLevelPressureCommand_regex; +class SeaLevelPressureCommand extends Command { + constructor() { + super(...arguments); + _SeaLevelPressureCommand_regex.set(this, /^SLP(\d{2})(\d)/); + } + canParse(code) { + return __classPrivateFieldGet(this, _SeaLevelPressureCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _SeaLevelPressureCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + let pressure = matches[1].startsWith("9") ? "9" : "10"; + pressure += matches[1] + "." + matches[2]; + const description = format(_("Remark.Sea.Level.Pressure", this.locale), pressure); + remark.push({ + type: RemarkType.SeaLevelPressure, + description, + raw: matches[0], + pressure: +pressure, + }); + return [code.replace(__classPrivateFieldGet(this, _SeaLevelPressureCommand_regex, "f"), "").trim(), remark]; + } +} +_SeaLevelPressureCommand_regex = new WeakMap(); + +var _SecondLocationVisibilityCommand_regex; +class SecondLocationVisibilityCommand extends Command { + constructor() { + super(...arguments); + _SecondLocationVisibilityCommand_regex.set(this, /^VIS ((\d)*( )?(\d?\/?\d)) (\w+)/); + } + canParse(code) { + return __classPrivateFieldGet(this, _SecondLocationVisibilityCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _SecondLocationVisibilityCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const distance = matches[1]; + const location = matches[5]; + const description = format(_("Remark.Second.Location.Visibility", this.locale), distance, location); + remark.push({ + type: RemarkType.SecondLocationVisibility, + description, + raw: matches[0], + distance: convertFractionalAmount(distance), + location, + }); + return [code.replace(__classPrivateFieldGet(this, _SecondLocationVisibilityCommand_regex, "f"), "").trim(), remark]; + } +} +_SecondLocationVisibilityCommand_regex = new WeakMap(); + +var _SectorVisibilityCommand_regex; +class SectorVisibilityCommand extends Command { + constructor() { + super(...arguments); + _SectorVisibilityCommand_regex.set(this, /^VIS ([A-Z]{1,2}) ((\d)*( )?(\d?\/?\d))/); + } + canParse(code) { + return __classPrivateFieldGet(this, _SectorVisibilityCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _SectorVisibilityCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const direction = as(matches[1], Direction); + const description = format(_("Remark.Sector.Visibility", this.locale), _(`Converter.${direction}`, this.locale), matches[2]); + remark.push({ + type: RemarkType.SectorVisibility, + description, + raw: matches[0], + direction, + distance: convertFractionalAmount(matches[2]), + }); + return [code.replace(__classPrivateFieldGet(this, _SectorVisibilityCommand_regex, "f"), "").trim(), remark]; + } +} +_SectorVisibilityCommand_regex = new WeakMap(); + +var _SmallHailSizeCommand_regex; +class SmallHailSizeCommand extends Command { + constructor() { + super(...arguments); + _SmallHailSizeCommand_regex.set(this, /^GR LESS THAN ((\d )?(\d\/\d)?)/); + } + canParse(code) { + return __classPrivateFieldGet(this, _SmallHailSizeCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _SmallHailSizeCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const description = format(_("Remark.Hail.LesserThan", this.locale), matches[1]); + remark.push({ + type: RemarkType.SmallHailSize, + description, + raw: matches[0], + size: convertFractionalAmount(matches[1]), + }); + return [code.replace(__classPrivateFieldGet(this, _SmallHailSizeCommand_regex, "f"), "").trim(), remark]; + } +} +_SmallHailSizeCommand_regex = new WeakMap(); + +var _SnowDepthCommand_regex; +class SnowDepthCommand extends Command { + constructor() { + super(...arguments); + _SnowDepthCommand_regex.set(this, /^4\/(\d{3})/); + } + canParse(code) { + return __classPrivateFieldGet(this, _SnowDepthCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _SnowDepthCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const depth = +matches[1]; + const description = format(_("Remark.Snow.Depth", this.locale), depth); + remark.push({ + type: RemarkType.SnowDepth, + description, + raw: matches[0], + depth, + }); + return [code.replace(__classPrivateFieldGet(this, _SnowDepthCommand_regex, "f"), "").trim(), remark]; + } +} +_SnowDepthCommand_regex = new WeakMap(); + +var _SnowIncreaseCommand_regex; +class SnowIncreaseCommand extends Command { + constructor() { + super(...arguments); + _SnowIncreaseCommand_regex.set(this, /^SNINCR (\d+)\/(\d+)/); + } + canParse(code) { + return __classPrivateFieldGet(this, _SnowIncreaseCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _SnowIncreaseCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const inchesLastHour = +matches[1]; + const totalDepth = +matches[2]; + const description = format(_("Remark.Snow.Increasing.Rapidly", this.locale), inchesLastHour, totalDepth); + remark.push({ + type: RemarkType.SnowIncrease, + description, + raw: matches[0], + inchesLastHour, + totalDepth, + }); + return [code.replace(__classPrivateFieldGet(this, _SnowIncreaseCommand_regex, "f"), "").trim(), remark]; + } +} +_SnowIncreaseCommand_regex = new WeakMap(); + +var _SnowPelletsCommand_regex; +class SnowPelletsCommand extends Command { + constructor() { + super(...arguments); + _SnowPelletsCommand_regex.set(this, /^GS (LGT|MOD|HVY)/); + } + canParse(code) { + return __classPrivateFieldGet(this, _SnowPelletsCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _SnowPelletsCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const description = format(_("Remark.Snow.Pellets", this.locale), _(`Remark.${matches[1]}`, this.locale)); + remark.push({ + type: RemarkType.SnowPellets, + description, + raw: matches[0], + amount: matches[1], + }); + return [code.replace(__classPrivateFieldGet(this, _SnowPelletsCommand_regex, "f"), "").trim(), remark]; + } +} +_SnowPelletsCommand_regex = new WeakMap(); + +var _SunshineDurationCommand_regex; +class SunshineDurationCommand extends Command { + constructor() { + super(...arguments); + _SunshineDurationCommand_regex.set(this, /^98(\d{3})/); + } + canParse(code) { + return __classPrivateFieldGet(this, _SunshineDurationCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _SunshineDurationCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const duration = +matches[1]; + const description = format(_("Remark.Sunshine.Duration", this.locale), duration); + remark.push({ + type: RemarkType.SunshineDuration, + description, + raw: matches[0], + duration, + }); + return [code.replace(__classPrivateFieldGet(this, _SunshineDurationCommand_regex, "f"), "").trim(), remark]; + } +} +_SunshineDurationCommand_regex = new WeakMap(); + +var _SurfaceVisibilityCommand_regex; +class SurfaceVisibilityCommand extends Command { + constructor() { + super(...arguments); + _SurfaceVisibilityCommand_regex.set(this, /^SFC VIS ((\d)*( )?(\d?\/?\d))/); + } + canParse(code) { + return __classPrivateFieldGet(this, _SurfaceVisibilityCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _SurfaceVisibilityCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const distance = matches[1]; + const description = format(_("Remark.Surface.Visibility", this.locale), distance); + remark.push({ + type: RemarkType.SurfaceVisibility, + description, + raw: matches[0], + distance: convertFractionalAmount(distance), + }); + return [code.replace(__classPrivateFieldGet(this, _SurfaceVisibilityCommand_regex, "f"), "").trim(), remark]; + } +} +_SurfaceVisibilityCommand_regex = new WeakMap(); + +var _ThunderStormLocationCommand_regex; +class ThunderStormLocationCommand extends Command { + constructor() { + super(...arguments); + _ThunderStormLocationCommand_regex.set(this, /^TS ([A-Z]{2})/); + } + canParse(code) { + return __classPrivateFieldGet(this, _ThunderStormLocationCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _ThunderStormLocationCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const location = as(matches[1], Direction); + const description = format(_("Remark.Thunderstorm.Location.0", this.locale), _(`Converter.${location}`, this.locale)); + remark.push({ + type: RemarkType.ThunderStormLocation, + description, + raw: matches[0], + location, + }); + return [code.replace(__classPrivateFieldGet(this, _ThunderStormLocationCommand_regex, "f"), "").trim(), remark]; + } +} +_ThunderStormLocationCommand_regex = new WeakMap(); + +var _ThunderStormLocationMovingCommand_regex; +class ThunderStormLocationMovingCommand extends Command { + constructor() { + super(...arguments); + _ThunderStormLocationMovingCommand_regex.set(this, /^TS ([A-Z]{2}) MOV ([A-Z]{2})/); + } + canParse(code) { + return __classPrivateFieldGet(this, _ThunderStormLocationMovingCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _ThunderStormLocationMovingCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const location = as(matches[1], Direction); + const moving = as(matches[2], Direction); + const description = format(_("Remark.Thunderstorm.Location.Moving", this.locale), _(`Converter.${location}`, this.locale), _(`Converter.${moving}`, this.locale)); + remark.push({ + type: RemarkType.ThunderStormLocationMoving, + description, + raw: matches[0], + location, + moving, + }); + return [code.replace(__classPrivateFieldGet(this, _ThunderStormLocationMovingCommand_regex, "f"), "").trim(), remark]; + } +} +_ThunderStormLocationMovingCommand_regex = new WeakMap(); + +var _TornadicActivityBegCommand_regex; +class TornadicActivityBegCommand extends Command { + constructor() { + super(...arguments); + _TornadicActivityBegCommand_regex.set(this, /^(TORNADO|FUNNEL CLOUD|WATERSPOUT) (B(\d{2})?(\d{2}))( (\d+)? ([A-Z]{1,2})?)?/); + } + canParse(code) { + return __classPrivateFieldGet(this, _TornadicActivityBegCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _TornadicActivityBegCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const direction = as(matches[7], Direction); + const description = format(_("Remark.Tornadic.Activity.Beginning", this.locale), _(`Remark.${matches[1].replace(" ", "")}`, this.locale), matches[3] || "", matches[4], matches[6], _(`Converter.${direction}`, this.locale)); + remark.push({ + type: RemarkType.TornadicActivityBeg, + description, + raw: matches[0], + tornadicType: matches[1], + startHour: matches[3] ? +matches[3] : undefined, + startMinute: +matches[4], + distance: +matches[6], + direction, + }); + return [code.replace(__classPrivateFieldGet(this, _TornadicActivityBegCommand_regex, "f"), "").trim(), remark]; + } +} +_TornadicActivityBegCommand_regex = new WeakMap(); + +var _TornadicActivityBegEndCommand_regex; +class TornadicActivityBegEndCommand extends Command { + constructor() { + super(...arguments); + _TornadicActivityBegEndCommand_regex.set(this, /^(TORNADO|FUNNEL CLOUD|WATERSPOUT) (B(\d{2})?(\d{2}))(E(\d{2})?(\d{2}))( (\d+)? ([A-Z]{1,2})?)?/); + } + canParse(code) { + return __classPrivateFieldGet(this, _TornadicActivityBegEndCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _TornadicActivityBegEndCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const direction = as(matches[10], Direction); + const description = format(_("Remark.Tornadic.Activity.BegEnd", this.locale), _(`Remark.${matches[1].replace(" ", "")}`, this.locale), matches[3] || "", matches[4], matches[6] || "", matches[7], matches[9], _(`Converter.${direction}`, this.locale)); + remark.push({ + type: RemarkType.TornadicActivityBegEnd, + description, + raw: matches[0], + tornadicType: matches[1], + startHour: matches[3] ? +matches[3] : undefined, + startMinute: +matches[4], + endHour: matches[6] ? +matches[6] : undefined, + endMinute: +matches[7], + distance: +matches[9], + direction, + }); + return [code.replace(__classPrivateFieldGet(this, _TornadicActivityBegEndCommand_regex, "f"), "").trim(), remark]; + } +} +_TornadicActivityBegEndCommand_regex = new WeakMap(); + +var _TornadicActivityEndCommand_regex; +class TornadicActivityEndCommand extends Command { + constructor() { + super(...arguments); + _TornadicActivityEndCommand_regex.set(this, /^(TORNADO|FUNNEL CLOUD|WATERSPOUT) (E(\d{2})?(\d{2}))( (\d+)? ([A-Z]{1,2})?)?/); + } + canParse(code) { + return __classPrivateFieldGet(this, _TornadicActivityEndCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _TornadicActivityEndCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const direction = as(matches[7], Direction); + const description = format(_("Remark.Tornadic.Activity.Ending", this.locale), _(`Remark.${matches[1].replace(" ", "")}`, this.locale), matches[3] || "", matches[4], matches[6], _(`Converter.${direction}`, this.locale)); + remark.push({ + type: RemarkType.TornadicActivityEnd, + description, + raw: matches[0], + tornadicType: matches[1], + endHour: matches[3] ? +matches[3] : undefined, + endMinute: +matches[4], + distance: +matches[6], + direction, + }); + return [code.replace(__classPrivateFieldGet(this, _TornadicActivityEndCommand_regex, "f"), "").trim(), remark]; + } +} +_TornadicActivityEndCommand_regex = new WeakMap(); + +var _TowerVisibilityCommand_regex; +class TowerVisibilityCommand extends Command { + constructor() { + super(...arguments); + _TowerVisibilityCommand_regex.set(this, /^TWR VIS ((\d)*( )?(\d?\/?\d))/); + } + canParse(code) { + return __classPrivateFieldGet(this, _TowerVisibilityCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _TowerVisibilityCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const distance = matches[1]; + const description = format(_("Remark.Tower.Visibility", this.locale), distance); + remark.push({ + type: RemarkType.TowerVisibility, + description, + raw: matches[0], + distance: convertFractionalAmount(distance), + }); + return [code.replace(__classPrivateFieldGet(this, _TowerVisibilityCommand_regex, "f"), "").trim(), remark]; + } +} +_TowerVisibilityCommand_regex = new WeakMap(); + +var _VariableSkyCommand_regex; +class VariableSkyCommand extends Command { + constructor() { + super(...arguments); + _VariableSkyCommand_regex.set(this, /^([A-Z]{3}) V ([A-Z]{3})/); + } + canParse(code) { + return __classPrivateFieldGet(this, _VariableSkyCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _VariableSkyCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const firstQuantity = as(matches[1], CloudQuantity); + const secondQuantity = as(matches[2], CloudQuantity); + const description = format(_("Remark.Variable.Sky.Condition.0", this.locale), _(`CloudQuantity.${firstQuantity}`, this.locale), _(`CloudQuantity.${secondQuantity}`, this.locale)); + remark.push({ + type: RemarkType.VariableSky, + description, + raw: matches[0], + cloudQuantityRange: [firstQuantity, secondQuantity], + }); + return [code.replace(__classPrivateFieldGet(this, _VariableSkyCommand_regex, "f"), "").trim(), remark]; + } +} +_VariableSkyCommand_regex = new WeakMap(); + +var _VariableSkyHeightCommand_regex; +class VariableSkyHeightCommand extends Command { + constructor() { + super(...arguments); + _VariableSkyHeightCommand_regex.set(this, /^([A-Z]{3})(\d{3}) V ([A-Z]{3})/); + } + canParse(code) { + return __classPrivateFieldGet(this, _VariableSkyHeightCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _VariableSkyHeightCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const firstQuantity = as(matches[1], CloudQuantity); + const secondQuantity = as(matches[3], CloudQuantity); + const height = 100 * +matches[2]; + const description = format(_("Remark.Variable.Sky.Condition.Height", this.locale), height, _(`CloudQuantity.${firstQuantity}`, this.locale), _(`CloudQuantity.${secondQuantity}`, this.locale)); + remark.push({ + type: RemarkType.VariableSkyHeight, + description, + raw: matches[0], + height, + cloudQuantityRange: [firstQuantity, secondQuantity], + }); + return [code.replace(__classPrivateFieldGet(this, _VariableSkyHeightCommand_regex, "f"), "").trim(), remark]; + } +} +_VariableSkyHeightCommand_regex = new WeakMap(); + +var _VirgaDirectionCommand_regex; +class VirgaDirectionCommand extends Command { + constructor() { + super(...arguments); + _VirgaDirectionCommand_regex.set(this, /^VIRGA ([A-Z]{2})/); + } + canParse(code) { + return __classPrivateFieldGet(this, _VirgaDirectionCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _VirgaDirectionCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const direction = as(matches[1], Direction); + const description = format(_("Remark.Virga.Direction", this.locale), _(`Converter.${direction}`, this.locale)); + remark.push({ + type: RemarkType.VirgaDirection, + description, + raw: matches[0], + direction, + }); + return [code.replace(__classPrivateFieldGet(this, _VirgaDirectionCommand_regex, "f"), "").trim(), remark]; + } +} +_VirgaDirectionCommand_regex = new WeakMap(); + +var _WaterEquivalentSnowCommand_regex; +class WaterEquivalentSnowCommand extends Command { + constructor() { + super(...arguments); + _WaterEquivalentSnowCommand_regex.set(this, /^933(\d{3})\b/); + } + canParse(code) { + return __classPrivateFieldGet(this, _WaterEquivalentSnowCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _WaterEquivalentSnowCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const amount = +matches[1] / 10; + const description = format(_("Remark.Water.Equivalent.Snow.Ground", this.locale), amount); + remark.push({ + type: RemarkType.WaterEquivalentSnow, + description, + raw: matches[0], + amount, + }); + return [code.replace(__classPrivateFieldGet(this, _WaterEquivalentSnowCommand_regex, "f"), "").trim(), remark]; + } +} +_WaterEquivalentSnowCommand_regex = new WeakMap(); + +var _WindPeakCommand_regex; +class WindPeakCommand extends Command { + constructor() { + super(...arguments); + _WindPeakCommand_regex.set(this, /^PK WND (\d{3})(\d{2,3})\/(\d{2})?(\d{2})/); + } + canParse(code) { + return __classPrivateFieldGet(this, _WindPeakCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _WindPeakCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const degrees = +matches[1]; + const speed = +matches[2]; + const description = format(_("Remark.PeakWind", this.locale), degrees, speed, matches[3] || "", matches[4]); + remark.push({ + type: RemarkType.WindPeak, + description, + raw: matches[0], + speed, + degrees, + startHour: matches[3] ? +matches[3] : undefined, + startMinute: +matches[4], + }); + return [code.replace(__classPrivateFieldGet(this, _WindPeakCommand_regex, "f"), "").trim(), remark]; + } +} +_WindPeakCommand_regex = new WeakMap(); + +var _WindShiftCommand_regex; +class WindShiftCommand extends Command { + constructor() { + super(...arguments); + _WindShiftCommand_regex.set(this, /^WSHFT (\d{2})?(\d{2})/); + } + canParse(code) { + return __classPrivateFieldGet(this, _WindShiftCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _WindShiftCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const description = format(_("Remark.WindShift.0", this.locale), matches[1] || "", matches[2]); + remark.push({ + type: RemarkType.WindShift, + description, + raw: matches[0], + startHour: matches[1] ? +matches[1] : undefined, + startMinute: +matches[2], + }); + return [code.replace(__classPrivateFieldGet(this, _WindShiftCommand_regex, "f"), "").trim(), remark]; + } +} +_WindShiftCommand_regex = new WeakMap(); + +var _WindShiftFropaCommand_regex; +class WindShiftFropaCommand extends Command { + constructor() { + super(...arguments); + _WindShiftFropaCommand_regex.set(this, /^WSHFT (\d{2})?(\d{2}) FROPA/); + } + canParse(code) { + return __classPrivateFieldGet(this, _WindShiftFropaCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _WindShiftFropaCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const description = format(_("Remark.WindShift.FROPA", this.locale), matches[1] || "", matches[2]); + remark.push({ + type: RemarkType.WindShiftFropa, + description, + raw: matches[0], + startHour: matches[1] ? +matches[1] : undefined, + startMinute: +matches[2], + }); + return [code.replace(__classPrivateFieldGet(this, _WindShiftFropaCommand_regex, "f"), "").trim(), remark]; + } +} +_WindShiftFropaCommand_regex = new WeakMap(); + +class DefaultCommand extends Command { + canParse() { + return true; + } + execute(code, remark) { + const rmkSplit = pySplit(code, " ", 1); + const rem = _(`Remark.${rmkSplit[0]}`, this.locale); + if (RemarkType[rmkSplit[0]]) { + remark.push({ + type: rmkSplit[0], + description: rem, + raw: rmkSplit[0], + }); + } + else { + const lastRemark = remark[remark.length - 1]; + if (lastRemark?.type === RemarkType.Unknown) { + // Merge with last unknown value + lastRemark.raw = `${lastRemark.raw} ${rmkSplit[0]}`; + } + else { + remark.push({ + type: RemarkType.Unknown, + raw: rmkSplit[0], + }); + } + } + return [rmkSplit.length === 1 ? "" : rmkSplit[1], remark]; + } +} + +var _PrecipitationBegCommand_regex; +class PrecipitationBegCommand extends Command { + constructor() { + super(...arguments); + _PrecipitationBegCommand_regex.set(this, /^(([A-Z]{2})?([A-Z]{2})B(\d{2})?(\d{2}))/); + } + canParse(code) { + return __classPrivateFieldGet(this, _PrecipitationBegCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _PrecipitationBegCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const descriptive = matches[2] ? as(matches[2], Descriptive) : undefined; + const phenomenon = as(matches[3], Phenomenon); + const description = format(_("Remark.Precipitation.Beg.0", this.locale), descriptive ? _(`Descriptive.${descriptive}`, this.locale) : "", _(`Phenomenon.${phenomenon}`, this.locale), matches[4] || "", matches[5])?.trim(); + remark.push({ + type: RemarkType.PrecipitationBeg, + description, + raw: matches[0], + descriptive, + phenomenon, + startHour: matches[4] ? +matches[4] : undefined, + startMin: +matches[5], + }); + return [code.replace(__classPrivateFieldGet(this, _PrecipitationBegCommand_regex, "f"), "").trim(), remark]; + } +} +_PrecipitationBegCommand_regex = new WeakMap(); + +var _PrecipitationEndCommand_regex; +class PrecipitationEndCommand extends Command { + constructor() { + super(...arguments); + _PrecipitationEndCommand_regex.set(this, /^(([A-Z]{2})?([A-Z]{2})E(\d{2})?(\d{2}))/); + } + canParse(code) { + return __classPrivateFieldGet(this, _PrecipitationEndCommand_regex, "f").test(code); + } + execute(code, remark) { + const matches = code.match(__classPrivateFieldGet(this, _PrecipitationEndCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const descriptive = matches[2] ? as(matches[2], Descriptive) : undefined; + const phenomenon = as(matches[3], Phenomenon); + const description = format(_("Remark.Precipitation.End", this.locale), descriptive ? _(`Descriptive.${descriptive}`, this.locale) : "", _(`Phenomenon.${phenomenon}`, this.locale), matches[4] || "", matches[5])?.trim(); + remark.push({ + type: RemarkType.PrecipitationEnd, + description, + raw: matches[0], + descriptive, + phenomenon, + endHour: matches[4] ? +matches[4] : undefined, + endMin: +matches[5], + }); + return [code.replace(__classPrivateFieldGet(this, _PrecipitationEndCommand_regex, "f"), "").trim(), remark]; + } +} +_PrecipitationEndCommand_regex = new WeakMap(); + +class RemarkCommandSupplier { + constructor(locale) { + this.locale = locale; + this.defaultCommand = new DefaultCommand(locale); + this.commandList = [ + new WindPeakCommand(locale), + new WindShiftFropaCommand(locale), + new WindShiftCommand(locale), + new TowerVisibilityCommand(locale), + new SurfaceVisibilityCommand(locale), + new PrevailingVisibilityCommand(locale), + new SecondLocationVisibilityCommand(locale), + new SectorVisibilityCommand(locale), + new TornadicActivityBegEndCommand(locale), + new TornadicActivityBegCommand(locale), + new TornadicActivityEndCommand(locale), + new PrecipitationBegEndCommand(locale), + new PrecipitationBegCommand(locale), + new PrecipitationEndCommand(locale), + new ThunderStormLocationMovingCommand(locale), + new ThunderStormLocationCommand(locale), + new SmallHailSizeCommand(locale), + new HailSizeCommand(locale), + new SnowPelletsCommand(locale), + new VirgaDirectionCommand(locale), + new CeilingHeightCommand(locale), + new ObscurationCommand(locale), + new VariableSkyHeightCommand(locale), + new VariableSkyCommand(locale), + new CeilingSecondLocationCommand(locale), + new SeaLevelPressureCommand(locale), + new SnowIncreaseCommand(locale), + new HourlyMaximumMinimumTemperatureCommand(locale), + new HourlyMaximumTemperatureCommand(locale), + new HourlyMinimumTemperatureCommand(locale), + new HourlyPrecipitationAmountCommand(locale), + new HourlyTemperatureDewPointCommand(locale), + new HourlyPressureCommand(locale), + new IceAccretionCommand(locale), + new PrecipitationAmount36HourCommand(locale), + new PrecipitationAmount24HourCommand(locale), + new SnowDepthCommand(locale), + new SunshineDurationCommand(locale), + new WaterEquivalentSnowCommand(locale), + ]; + } + get(code) { + for (const command of this.commandList) { + if (command.canParse(code)) + return command; + } + return this.defaultCommand; + } +} +var RemarkType; +(function (RemarkType) { + // Unknown processed with default command + RemarkType["Unknown"] = "Unknown"; + // Processed with default command + RemarkType["AO1"] = "AO1"; + RemarkType["AO2"] = "AO2"; + RemarkType["PRESFR"] = "PRESFR"; + RemarkType["PRESRR"] = "PRESRR"; + RemarkType["TORNADO"] = "TORNADO"; + RemarkType["FUNNELCLOUD"] = "FUNNELCLOUD"; + RemarkType["WATERSPOUT"] = "WATERSPOUT"; + RemarkType["VIRGA"] = "VIRGA"; + // Regular commands below + RemarkType["WindPeak"] = "WindPeak"; + RemarkType["WindShiftFropa"] = "WindShiftFropa"; + RemarkType["WindShift"] = "WindShift"; + RemarkType["TowerVisibility"] = "TowerVisibility"; + RemarkType["SurfaceVisibility"] = "SurfaceVisibility"; + RemarkType["PrevailingVisibility"] = "PrevailingVisibility"; + RemarkType["SecondLocationVisibility"] = "SecondLocationVisibility"; + RemarkType["SectorVisibility"] = "SectorVisibility"; + RemarkType["TornadicActivityBegEnd"] = "TornadicActivityBegEnd"; + RemarkType["TornadicActivityBeg"] = "TornadicActivityBeg"; + RemarkType["TornadicActivityEnd"] = "TornadicActivityEnd"; + RemarkType["PrecipitationBeg"] = "PrecipitationBeg"; + RemarkType["PrecipitationBegEnd"] = "PrecipitationBegEnd"; + RemarkType["PrecipitationEnd"] = "PrecipitationEnd"; + RemarkType["ThunderStormLocationMoving"] = "ThunderStormLocationMoving"; + RemarkType["ThunderStormLocation"] = "ThunderStormLocation"; + RemarkType["SmallHailSize"] = "SmallHailSize"; + RemarkType["HailSize"] = "HailSize"; + RemarkType["SnowPellets"] = "SnowPellets"; + RemarkType["VirgaDirection"] = "VirgaDirection"; + RemarkType["CeilingHeight"] = "CeilingHeight"; + RemarkType["Obscuration"] = "Obscuration"; + RemarkType["VariableSkyHeight"] = "VariableSkyHeight"; + RemarkType["VariableSky"] = "VariableSky"; + RemarkType["CeilingSecondLocation"] = "CeilingSecondLocation"; + RemarkType["SeaLevelPressure"] = "SeaLevelPressure"; + RemarkType["SnowIncrease"] = "SnowIncrease"; + RemarkType["HourlyMaximumMinimumTemperature"] = "HourlyMaximumMinimumTemperature"; + RemarkType["HourlyMaximumTemperature"] = "HourlyMaximumTemperature"; + RemarkType["HourlyMinimumTemperature"] = "HourlyMinimumTemperature"; + RemarkType["HourlyPrecipitationAmount"] = "HourlyPrecipitationAmount"; + RemarkType["HourlyTemperatureDewPoint"] = "HourlyTemperatureDewPoint"; + RemarkType["HourlyPressure"] = "HourlyPressure"; + RemarkType["IceAccretion"] = "IceAccretion"; + RemarkType["PrecipitationAmount36Hour"] = "PrecipitationAmount36Hour"; + RemarkType["PrecipitationAmount24Hour"] = "PrecipitationAmount24Hour"; + RemarkType["SnowDepth"] = "SnowDepth"; + RemarkType["SunshineDuration"] = "SunshineDuration"; + RemarkType["WaterEquivalentSnow"] = "WaterEquivalentSnow"; +})(RemarkType || (RemarkType = {})); + +function isWeatherConditionValid(weather) { + return (weather.phenomenons.length !== 0 || + weather.descriptive == Descriptive.THUNDERSTORM || + (weather.intensity === Intensity.IN_VICINITY && + weather.descriptive == Descriptive.SHOWERS)); +} + +var _CloudCommand_cloudRegex, _MainVisibilityCommand_regex, _WindCommand_regex, _WindVariationCommand_regex, _WindShearCommand_regex, _VerticalVisibilityCommand_regex, _MinimalVisibilityCommand_regex, _MainVisibilityNauticalMilesCommand_regex, _CommandSupplier_commands$1; +/** + * This function creates a wind element. + * @param wind The wind object + * @param direction The direction in degrees + * @param speed The speed + * @param gust The speed of the gust. + * @param unit The speed unit + */ +function makeWind(direction, speed, gust, unit) { + return { + speed: +speed, + direction: degreesToCardinal(direction), + degrees: direction !== "VRB" ? +direction : undefined, + gust: gust ? +gust : undefined, + unit, + }; +} +class CloudCommand { + constructor() { + _CloudCommand_cloudRegex.set(this, /^([A-Z]{3})(\d{3})?([A-Z]{2,3})?$/); + } + parse(cloudString) { + const m = cloudString.match(__classPrivateFieldGet(this, _CloudCommand_cloudRegex, "f")); + if (!m) + return; + const quantity = CloudQuantity[m[1]]; + const height = 100 * +m[2] || undefined; + const type = CloudType[m[3]]; + if (!quantity) + return; + return { quantity, height, type }; + } + execute(container, cloudString) { + const cloud = this.parse(cloudString); + if (cloud) { + container.clouds.push(cloud); + return true; + } + return false; + } + canParse(cloudString) { + return __classPrivateFieldGet(this, _CloudCommand_cloudRegex, "f").test(cloudString); + } +} +_CloudCommand_cloudRegex = new WeakMap(); +class MainVisibilityCommand { + constructor() { + _MainVisibilityCommand_regex.set(this, /^(\d{4})(|NDV)$/); + } + canParse(visibilityString) { + return __classPrivateFieldGet(this, _MainVisibilityCommand_regex, "f").test(visibilityString); + } + execute(container, visibilityString) { + const matches = visibilityString.match(__classPrivateFieldGet(this, _MainVisibilityCommand_regex, "f")); + if (!matches) + return false; + const distance = convertVisibility(matches[1]); + if (!container.visibility) + container.visibility = distance; + container.visibility = { ...container.visibility, ...distance }; + if (matches[2] === "NDV") + container.visibility.ndv = true; + return true; + } +} +_MainVisibilityCommand_regex = new WeakMap(); +class WindCommand { + constructor() { + _WindCommand_regex.set(this, /^(VRB|\d{3})(\d{2})G?(\d{2})?(KT|MPS|KM\/H)?/); + } + canParse(windString) { + return __classPrivateFieldGet(this, _WindCommand_regex, "f").test(windString); + } + parseWind(windString) { + const matches = windString.match(__classPrivateFieldGet(this, _WindCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Wind should be defined"); + return makeWind(matches[1], matches[2], matches[3], as(matches[4] || "KT", SpeedUnit)); + } + execute(container, windString) { + const wind = this.parseWind(windString); + container.wind = wind; + return true; + } +} +_WindCommand_regex = new WeakMap(); +class WindVariationCommand { + constructor() { + _WindVariationCommand_regex.set(this, /^(\d{3})V(\d{3})/); + } + canParse(windString) { + return __classPrivateFieldGet(this, _WindVariationCommand_regex, "f").test(windString); + } + parseWindVariation(wind, windString) { + const matches = windString.match(__classPrivateFieldGet(this, _WindVariationCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Wind should be defined"); + wind.minVariation = +matches[1]; + wind.maxVariation = +matches[2]; + } + execute(container, windString) { + if (!container.wind) + throw new UnexpectedParseError(); + this.parseWindVariation(container.wind, windString); + return true; + } +} +_WindVariationCommand_regex = new WeakMap(); +class WindShearCommand { + constructor() { + _WindShearCommand_regex.set(this, /^WS(\d{3})\/(\w{3})(\d{2})G?(\d{2})?(KT|MPS|KM\/H)/); + } + canParse(windString) { + return __classPrivateFieldGet(this, _WindShearCommand_regex, "f").test(windString); + } + parseWindShear(windString) { + const matches = windString.match(__classPrivateFieldGet(this, _WindShearCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Wind shear should be defined"); + return { + ...makeWind(matches[2], matches[3], matches[4], as(matches[5], SpeedUnit)), + height: 100 * +matches[1], + }; + } + execute(container, windString) { + container.windShear = this.parseWindShear(windString); + return true; + } +} +_WindShearCommand_regex = new WeakMap(); +class VerticalVisibilityCommand { + constructor() { + _VerticalVisibilityCommand_regex.set(this, /^VV(\d{3})$/); + } + execute(container, visibilityString) { + const matches = visibilityString.match(__classPrivateFieldGet(this, _VerticalVisibilityCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Vertical visibility should be defined"); + container.verticalVisibility = 100 * +matches[1]; + return true; + } + canParse(windString) { + return __classPrivateFieldGet(this, _VerticalVisibilityCommand_regex, "f").test(windString); + } +} +_VerticalVisibilityCommand_regex = new WeakMap(); +class MinimalVisibilityCommand { + constructor() { + _MinimalVisibilityCommand_regex.set(this, /^(\d{4}[a-zA-Z]{1,2})$/); + } + execute(container, visibilityString) { + const matches = visibilityString.match(__classPrivateFieldGet(this, _MinimalVisibilityCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Vertical visibility should be defined"); + if (!container.visibility) + throw new UnexpectedParseError("container.visibility not instantiated"); + container.visibility.min = { + value: +matches[1].slice(0, 4), + direction: matches[1].slice(4), + }; + return true; + } + canParse(windString) { + return __classPrivateFieldGet(this, _MinimalVisibilityCommand_regex, "f").test(windString); + } +} +_MinimalVisibilityCommand_regex = new WeakMap(); +class MainVisibilityNauticalMilesCommand { + constructor() { + _MainVisibilityNauticalMilesCommand_regex.set(this, /^(P|M)?(\d)*(\s)?((\d\/\d)?SM)$/); + } + execute(container, visibilityString) { + const distance = convertNauticalMilesVisibility(visibilityString); + container.visibility = distance; + return true; + } + canParse(windString) { + return __classPrivateFieldGet(this, _MainVisibilityNauticalMilesCommand_regex, "f").test(windString); + } +} +_MainVisibilityNauticalMilesCommand_regex = new WeakMap(); +class CommandSupplier$1 { + constructor() { + _CommandSupplier_commands$1.set(this, [ + new WindShearCommand(), + new WindCommand(), + new WindVariationCommand(), + new MainVisibilityCommand(), + new MainVisibilityNauticalMilesCommand(), + new MinimalVisibilityCommand(), + new VerticalVisibilityCommand(), + new CloudCommand(), + ]); + } + get(input) { + for (const command of __classPrivateFieldGet(this, _CommandSupplier_commands$1, "f")) { + if (command.canParse(input)) + return command; + } + } +} +_CommandSupplier_commands$1 = new WeakMap(); + +var _AltimeterCommand_regex; +class AltimeterCommand { + constructor() { + _AltimeterCommand_regex.set(this, /^Q(\d{4})$/); + } + canParse(input) { + return __classPrivateFieldGet(this, _AltimeterCommand_regex, "f").test(input); + } + execute(metar, input) { + const matches = input.match(__classPrivateFieldGet(this, _AltimeterCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + metar.altimeter = Math.trunc(+matches[1]); + } +} +_AltimeterCommand_regex = new WeakMap(); + +var _AltimeterMercuryCommand_regex; +class AltimeterMercuryCommand { + constructor() { + _AltimeterMercuryCommand_regex.set(this, /^A(\d{4})$/); + } + canParse(input) { + return __classPrivateFieldGet(this, _AltimeterMercuryCommand_regex, "f").test(input); + } + execute(metar, input) { + const matches = input.match(__classPrivateFieldGet(this, _AltimeterMercuryCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + const mercury = +matches[1] / 100; + metar.altimeter = Math.trunc(convertInchesMercuryToPascal(mercury)); + } +} +_AltimeterMercuryCommand_regex = new WeakMap(); + +var _RunwayCommand_genericRegex, _RunwayCommand_runwayMaxRangeRegex, _RunwayCommand_runwayRegex; +class RunwayCommand { + constructor() { + _RunwayCommand_genericRegex.set(this, /^(R\d{2}\w?\/)/); + _RunwayCommand_runwayMaxRangeRegex.set(this, /^R(\d{2}\w?)\/(\d{4})V(\d{3,4})([UDN])?(FT)?/); + _RunwayCommand_runwayRegex.set(this, /^R(\d{2}\w?)\/([MP])?(\d{4})([UDN])?(FT)?$/); + } + canParse(input) { + return __classPrivateFieldGet(this, _RunwayCommand_genericRegex, "f").test(input); + } + execute(metar, input) { + // TODO idk if this matches super well... + if (__classPrivateFieldGet(this, _RunwayCommand_runwayRegex, "f").test(input)) { + const matches = input.match(__classPrivateFieldGet(this, _RunwayCommand_runwayRegex, "f")); + if (!matches) + throw new UnexpectedParseError("Should be able to parse"); + const indicator = matches[2] ? as(matches[2], ValueIndicator) : undefined; + const trend = matches[4] ? as(matches[4], RunwayInfoTrend) : undefined; + const unit = matches[5] + ? as(matches[5], RunwayInfoUnit) + : RunwayInfoUnit.Meters; + metar.runwaysInfo.push({ + name: matches[1], + indicator, + minRange: +matches[3], + trend, + unit, + }); + } + else if (__classPrivateFieldGet(this, _RunwayCommand_runwayMaxRangeRegex, "f").test(input)) { + const matches = input.match(__classPrivateFieldGet(this, _RunwayCommand_runwayMaxRangeRegex, "f")); + if (!matches) + throw new UnexpectedParseError("Should be able to parse"); + const trend = matches[4] ? as(matches[4], RunwayInfoTrend) : undefined; + const unit = matches[5] + ? as(matches[5], RunwayInfoUnit) + : RunwayInfoUnit.Meters; + metar.runwaysInfo.push({ + name: matches[1], + minRange: +matches[2], + maxRange: +matches[3], + trend, + unit, + }); + } + } +} +_RunwayCommand_genericRegex = new WeakMap(), _RunwayCommand_runwayMaxRangeRegex = new WeakMap(), _RunwayCommand_runwayRegex = new WeakMap(); + +var _TemperatureCommand_regex; +class TemperatureCommand { + constructor() { + _TemperatureCommand_regex.set(this, /^(M?\d{2})\/(M?\d{2})$/); + } + canParse(input) { + return __classPrivateFieldGet(this, _TemperatureCommand_regex, "f").test(input); + } + execute(metar, input) { + const matches = input.match(__classPrivateFieldGet(this, _TemperatureCommand_regex, "f")); + if (!matches) + throw new UnexpectedParseError("Match not found"); + metar.temperature = convertTemperature(matches[1]); + metar.dewPoint = convertTemperature(matches[2]); + } +} +_TemperatureCommand_regex = new WeakMap(); + +var _CommandSupplier_commands; +class CommandSupplier { + constructor() { + _CommandSupplier_commands.set(this, [ + new RunwayCommand(), + new TemperatureCommand(), + new AltimeterCommand(), + new AltimeterMercuryCommand(), + ]); + } + get(input) { + for (const command of __classPrivateFieldGet(this, _CommandSupplier_commands, "f")) { + if (command.canParse(input)) + return command; + } + } +} +_CommandSupplier_commands = new WeakMap(); + +var _AbstractParser_INTENSITY_REGEX, _AbstractParser_CAVOK, _AbstractParser_commonSupplier, _MetarParser_commandSupplier, _TAFParser_validityPattern, _RemarkParser_supplier; +/** + * Parses the delivery time of a METAR/TAF + * @param abstractWeatherCode The TAF or METAR object + * @param timeString The string representing the delivery time + */ +function parseDeliveryTime(timeString) { + const day = +timeString.slice(0, 2); + const hour = +timeString.slice(2, 4); + const minute = +timeString.slice(4, 6); + if (isNaN(day) || isNaN(hour) || isNaN(minute)) + return; + return { + day, + hour, + minute, + }; +} +function parseFlags(abstractWeatherCode, flag) { + const flags = findFlags(flag); + if (flags) + Object.assign(abstractWeatherCode, flags); + return !!flags; +} +var FlagMap; +(function (FlagMap) { + FlagMap["AMD"] = "amendment"; + FlagMap["AUTO"] = "auto"; + FlagMap["CNL"] = "canceled"; + FlagMap["COR"] = "corrected"; + FlagMap["NIL"] = "nil"; +})(FlagMap || (FlagMap = {})); +function findFlags(flag) { + if (flag in FlagMap) + return { [FlagMap[flag]]: true }; +} +/** + * This function parses the array containing the remark and concat the array into a string + * @param container the metar, taf or taf trend to update + * @param line The array containing the current line tokens + * @param index the index starting the remark ie token RMK + */ +function parseRemark(container, line, index, locale) { + const remarks = new RemarkParser(locale).parse(line.slice(index + 1).join(" ")); + container.remarks = remarks; + container.remark = remarks + .map(({ description, raw }) => description || raw) + .join(" "); +} +/** + * Parses the temperature in a TAF + * @param input the string containing the temperature + * @returns TemperatureDated object + */ +function parseTemperature(input) { + const parts = pySplit(input, "/"); + return { + temperature: convertTemperature(parts[0].slice(2)), + day: +parts[1].slice(0, 2), + hour: +parts[1].slice(2, 4), + }; +} +/** + * Parses validity of a TAF or a TAFTrend + * @param input the string containing the validity + * @returns Validity object + */ +function parseValidity(input) { + const parts = pySplit(input, "/"); + return { + startDay: +parts[0].slice(0, 2), + startHour: +parts[0].slice(2), + endDay: +parts[1].slice(0, 2), + endHour: +parts[1].slice(2), + }; +} +/** + * Parses the validity for a FROM taf trend + * @param input the string containing the validity + * @returns a Validity object + */ +function parseFromValidity(input) { + return { + startDay: +input.slice(2, 4), + startHour: +input.slice(4, 6), + startMinutes: +input.slice(6, 8), + }; +} +/** + * Abstract class. + * Base parser. + */ +class AbstractParser { + constructor(locale) { + this.locale = locale; + this.FM = "FM"; + this.TEMPO = "TEMPO"; + this.BECMG = "BECMG"; + this.RMK = "RMK"; + // Safari does not currently support negative lookbehind + // #TOKENIZE_REGEX = /\s((?=\d\/\dSM)(? v); + // Hack for safari below... + const splitRegex = /\s|=/; + const smRegex = /^\d\/\dSM$/; + const digitRegex = /^(P|M)?\d$/; + // return input.split(this.#TOKENIZE_REGEX).filter((v) => v); + const splitted = input.split(splitRegex); + for (let i = 0; i < splitted.length; i++) { + if (digitRegex.test(splitted[i])) { + if (splitted[i + 1] && smRegex.test(splitted[i + 1])) { + splitted.splice(i, 2, `${splitted[i]} ${splitted[i + 1]}`); + } + } + } + return splitted.filter((t) => t); + } + /** + * Common parse method for METAR, TAF and trends object + * @param abstractWeatherCode the object to update + * @param input The token to parse + * @returns True if the token was parsed false otherwise + */ + generalParse(abstractWeatherContainer, input) { + if (input === __classPrivateFieldGet(this, _AbstractParser_CAVOK, "f")) { + abstractWeatherContainer.cavok = true; + abstractWeatherContainer.visibility = { + indicator: ValueIndicator.GreaterThan, + value: 9999, + unit: DistanceUnit.Meters, + }; + return true; + } + const command = __classPrivateFieldGet(this, _AbstractParser_commonSupplier, "f").get(input); + if (command) { + return command.execute(abstractWeatherContainer, input); + } + const weatherCondition = this.parseWeatherCondition(input); + if (isWeatherConditionValid(weatherCondition)) { + abstractWeatherContainer.weatherConditions.push(weatherCondition); + return true; + } + return false; + } +} +_AbstractParser_INTENSITY_REGEX = new WeakMap(), _AbstractParser_CAVOK = new WeakMap(), _AbstractParser_commonSupplier = new WeakMap(); +class MetarParser extends AbstractParser { + constructor() { + super(...arguments); + this.AT = "AT"; + this.TL = "TL"; + _MetarParser_commandSupplier.set(this, new CommandSupplier()); + } + /** + * Parses a trend of a metar + * @param index the index starting the trend in the list + * @param trend The trend to update + * @param trendParts array of tokens + * @returns the last index of the token that was last parsed + */ + parseTrend(index, trend, trendParts) { + let i = index + 1; + while (i < trendParts.length && + trendParts[i] !== this.TEMPO && + trendParts[i] !== this.BECMG) { + if (trendParts[i].startsWith(this.FM) || + trendParts[i].startsWith(this.TL) || + trendParts[i].startsWith(this.AT)) { + const trendTime = { + type: TimeIndicator[trendParts[i].slice(0, 2)], + hour: +trendParts[i].slice(2, 4), + minute: +trendParts[i].slice(4, 6), + }; + trend.times.push(trendTime); + } + else { + this.generalParse(trend, trendParts[i]); + } + i = i + 1; + } + return i - 1; + } + /** + * Parses an message and returns a METAR + * @param input The message to parse + * @returns METAR + */ + parse(input) { + const metarTab = this.tokenize(input); + const metar = { + ...parseDeliveryTime(metarTab[1]), + station: metarTab[0], + message: input, + remarks: [], + clouds: [], + weatherConditions: [], + trends: [], + runwaysInfo: [], + }; + let index = 2; + while (index < metarTab.length) { + if (!super.generalParse(metar, metarTab[index]) && + !parseFlags(metar, metarTab[index])) { + if (metarTab[index] === "NOSIG") { + metar.nosig = true; + } + else if (metarTab[index] === this.TEMPO || + metarTab[index] === this.BECMG) { + const trend = { + type: WeatherChangeType[metarTab[index]], + weatherConditions: [], + clouds: [], + times: [], + remarks: [], + raw: input, + }; + index = this.parseTrend(index, trend, metarTab); + metar.trends.push(trend); + } + else if (metarTab[index] === this.RMK) { + parseRemark(metar, metarTab, index, this.locale); + break; + } + else { + const command = __classPrivateFieldGet(this, _MetarParser_commandSupplier, "f").get(metarTab[index]); + if (command) + command.execute(metar, metarTab[index]); + } + } + index = index + 1; + } + return metar; + } +} +_MetarParser_commandSupplier = new WeakMap(); +/** + * Parser for TAF messages + */ +class TAFParser extends AbstractParser { + constructor() { + super(...arguments); + this.TAF = "TAF"; + this.PROB = "PROB"; + this.TX = "TX"; + this.TN = "TN"; + this.NSW = "NSW"; + _TAFParser_validityPattern.set(this, /^\d{4}\/\d{4}$/); + } + /** + * the message to parse + * @param input + * @returns a TAF object + * @throws ParseError if the message is invalid + */ + parse(input) { + const lines = this.extractLinesTokens(input); + let index = 0; + if (lines[0][0] === this.TAF) + index = 1; + if (lines[0][1] === this.TAF) + index = 2; + const flags = findFlags(lines[0][index]); + if (flags) { + index += 1; + } + const station = lines[0][index]; + index += 1; + const time = parseDeliveryTime(lines[0][index]); + if (time) + index += 1; + const validity = parseValidity(lines[0][index]); + const taf = { + station, + ...flags, + ...time, + validity, + message: input, + trends: [], + remarks: [], + clouds: [], + weatherConditions: [], + initialRaw: lines[0].join(" "), + }; + for (let i = index + 1; i < lines[0].length; i++) { + const token = lines[0][i]; + if (token == this.RMK) { + parseRemark(taf, lines[0], i, this.locale); + break; + } + else { + parseFlags(taf, token); + this.generalParse(taf, token); + } + } + const minMaxTemperatureLines = [ + lines[0].slice(index + 1), // EU countries have min/max in first line + ]; + // US military bases have min/max in last line + if (lines.length > 1) + minMaxTemperatureLines.push(lines[lines.length - 1]); + this.parseMaxMinTemperatures(taf, minMaxTemperatureLines); + // Handle the other lines + for (let i = 1; i < lines.length; i++) { + this.parseLine(taf, lines[i]); + } + return taf; + } + parseMaxMinTemperatures(taf, lines) { + for (const line of lines) { + for (const token of line) { + if (token == this.RMK) + break; + else if (token.startsWith(this.TX)) + taf.maxTemperature = parseTemperature(token); + else if (token.startsWith(this.TN)) + taf.minTemperature = parseTemperature(token); + } + } + } + /** + * Format the message as a multiple line code so each line can be parsed + * @param tafCode The base message + * @returns a list of string representing the lines of the message + */ + extractLinesTokens(tafCode) { + const singleLine = tafCode.replace(/\n/g, " "); + const cleanLine = singleLine.replace(/\s{2,}/g, " "); + const lines = joinProbIfNeeded(cleanLine + .replace(/\s(?=PROB\d{2}\sTEMPO|TEMPO|BECMG|FM|PROB)/g, "\n") + .split(/\n/)); + // TODO cleanup + function joinProbIfNeeded(ls) { + for (let i = 0; i < ls.length; i++) { + if (/^PROB\d{2}$/.test(ls[i]) && /^TEMPO/.test(ls[i + 1])) { + ls.splice(i, 2, `${ls[i]} ${ls[i + 1]}`); + } + } + return ls; + } + const linesToken = lines.map(this.tokenize); + return linesToken; + } + /** + * Parses the tokens of the line and updates the TAF object + * @param taf TAF object to update + * @param lineTokens the array of tokens representing a line + */ + parseLine(taf, lineTokens) { + let index = 1; + let trend; + if (lineTokens[0].startsWith(this.FM)) { + trend = { + ...this.makeEmptyTAFTrend(), + type: WeatherChangeType.FM, + validity: parseFromValidity(lineTokens[0]), + raw: lineTokens.join(" "), + }; + } + else if (lineTokens[0].startsWith(this.PROB)) { + const validity = this.findLineValidity(index, lineTokens); + if (!validity) + return; + trend = { + ...this.makeEmptyTAFTrend(), + type: WeatherChangeType.PROB, + validity, + raw: lineTokens.join(" "), + }; + if (lineTokens.length > 1 && lineTokens[1] === this.TEMPO) { + trend = { + ...this.makeEmptyTAFTrend(), + type: WeatherChangeType[lineTokens[1]], + validity, + raw: lineTokens.join(" "), + }; + index = 2; + } + trend.probability = +lineTokens[0].slice(4); + } + else { + const validity = this.findLineValidity(index, lineTokens); + if (!validity) + return; + trend = { + ...this.makeEmptyTAFTrend(), + type: WeatherChangeType[lineTokens[0]], + validity, + raw: lineTokens.join(" "), + }; + } + this.parseTrend(index, lineTokens, trend); + taf.trends.push(trend); + } + /** + * Finds a non-FM validity in a line + * @param index the index at which the array should be parsed + * @param line The array of string containing the line + * @param trend The trend object to update + */ + findLineValidity(index, line) { + let validity; + for (let i = index; i < line.length; i++) { + if (__classPrivateFieldGet(this, _TAFParser_validityPattern, "f").test(line[i])) + validity = parseValidity(line[i]); + } + return validity; + } + /** + * Parses a trend of the TAF + * @param index the index at which the array should be parsed + * @param line The array of string containing the line + * @param trend The trend object to update + */ + parseTrend(index, line, trend) { + for (let i = index; i < line.length; i++) { + if (line[i] === this.RMK) { + parseRemark(trend, line, i, this.locale); + break; + } + else if (line[i] === this.NSW) + trend.nsw = true; + // already parsed + else if (__classPrivateFieldGet(this, _TAFParser_validityPattern, "f").test(line[i])) + continue; + else + super.generalParse(trend, line[i]); + } + } + makeEmptyTAFTrend() { + return { + remarks: [], + clouds: [], + weatherConditions: [], + }; + } +} +_TAFParser_validityPattern = new WeakMap(); +class RemarkParser { + constructor(locale) { + this.locale = locale; + _RemarkParser_supplier.set(this, new RemarkCommandSupplier(this.locale)); + } + parse(code) { + let rmkStr = code; + let rmkList = []; + while (rmkStr) { + try { + [rmkStr, rmkList] = __classPrivateFieldGet(this, _RemarkParser_supplier, "f").get(rmkStr).execute(rmkStr, rmkList); + } + catch (e) { + if (e instanceof CommandExecutionError) { + [rmkStr, rmkList] = __classPrivateFieldGet(this, _RemarkParser_supplier, "f").defaultCommand.execute(rmkStr, rmkList); + } + else { + throw e; + } + } + } + return rmkList; + } +} +_RemarkParser_supplier = new WeakMap(); + +/** + * + * @param date Ideally the date the report was issued. However, any date within + * ~14 days of the report will work. + * @param day Day of the month (from the report) + * @param hour Hour (from the report) + * @param minute Minute (from the report) + * @returns + */ +function determineReportIssuedDate(date, day, hour, minute) { + // Some TAF reports do not include a delivery time + if (day == null || hour == null) + return date; + const months = [ + setDateComponents(addMonthsUTC(date, -1), day, hour, minute), + setDateComponents(new Date(date), day, hour, minute), + setDateComponents(addMonthsUTC(date, 1), day, hour, minute), + ]; + return months + .map((d) => ({ + date: d, + difference: Math.abs(d.getTime() - date.getTime()), + })) + .sort((a, b) => a.difference - b.difference)[0].date; +} +function getReportDate(issued, day, hour, minute = 0) { + let date = new Date(issued); + if (day < date.getUTCDate()) { + date = addMonthsUTC(date, 1); + } + date.setUTCDate(day); + date.setUTCHours(hour); + if (minute != null) + date.setUTCMinutes(minute); + return date; +} +function setDateComponents(date, day, hour, minute) { + date.setUTCDate(day); + date.setUTCHours(hour); + if (minute != null) + date.setUTCMinutes(minute); + return date; +} +function addMonthsUTC(date, count) { + if (date && count) { + let m, d = (date = new Date(+date)).getUTCDate(); + date.setUTCMonth(date.getUTCMonth() + count, 1); + m = date.getUTCMonth(); + date.setUTCDate(d); + if (date.getUTCMonth() !== m) + date.setUTCDate(0); + } + return date; +} + +function metarDatesHydrator(report, date) { + return { + ...report, + issued: determineReportIssuedDate(date, report.day, report.hour, report.minute), + }; +} + +function tafDatesHydrator(report, date) { + const issued = determineReportIssuedDate(date, report.day, report.hour, report.minute); + return { + ...report, + issued, + validity: { + ...report.validity, + start: getReportDate(issued, report.validity.startDay, report.validity.startHour), + end: getReportDate(issued, report.validity.endDay, report.validity.endHour), + }, + minTemperature: report.minTemperature + ? { + ...report.minTemperature, + date: getReportDate(issued, report.minTemperature.day, report.minTemperature.hour), + } + : undefined, + maxTemperature: report.maxTemperature + ? { + ...report.maxTemperature, + date: getReportDate(issued, report.maxTemperature.day, report.maxTemperature.hour), + } + : undefined, + trends: report.trends.map((trend) => ({ + ...trend, + validity: (() => { + switch (trend.type) { + case WeatherChangeType.FM: + return { + ...trend.validity, + start: getReportDate(issued, trend.validity.startDay, trend.validity.startHour, trend.validity.startMinutes), + }; + default: + return { + ...trend.validity, + start: getReportDate(issued, trend.validity.startDay, trend.validity.startHour), + end: getReportDate(issued, trend.validity.endDay, trend.validity.endHour), + }; + } + })(), + })), + }; +} + +function getForecastFromTAF(taf) { + return { + ...taf, + start: getReportDate(taf.issued, taf.validity.startDay, taf.validity.startHour), + end: getReportDate(taf.issued, taf.validity.endDay, taf.validity.endHour), + forecast: hydrateEndDates([makeInitialForecast(taf), ...taf.trends], taf.validity), + }; +} +/** + * Treat the base of the TAF as a FM + */ +function makeInitialForecast(taf) { + return { + wind: taf.wind, + visibility: taf.visibility, + verticalVisibility: taf.verticalVisibility, + windShear: taf.windShear, + cavok: taf.cavok, + remark: taf.remark, + remarks: taf.remarks, + clouds: taf.clouds, + weatherConditions: taf.weatherConditions, + raw: taf.initialRaw, + validity: { + // End day/hour are for end of the entire TAF + startDay: taf.validity.startDay, + startHour: taf.validity.startHour, + startMinutes: 0, + start: taf.validity.start, + }, + }; +} +function hasImplicitEnd({ type }) { + return (type === WeatherChangeType.FM || + // BECMG are special - the "end" date in the validity isn't actually + // the end date, it's when the change that's "becoming" is expected to + // finish transition. The actual "end" date of the BECMG is determined by + // the next FM/BECMG/end of the report validity, just like a FM + type === WeatherChangeType.BECMG || + // Special case for beginning of report conditions + type === undefined); +} +function hydrateEndDates(trends, reportValidity) { + function findNext(index) { + for (let i = index; i < trends.length; i++) { + if (hasImplicitEnd(trends[i])) + return trends[i]; + } + } + const forecasts = []; + let previouslyHydratedTrend; + for (let i = 0; i < trends.length; i++) { + const currentTrend = trends[i]; + const nextTrend = findNext(i + 1); + if (!hasImplicitEnd(currentTrend)) { + forecasts.push({ + ...currentTrend, + start: currentTrend.validity.start, + // Has a type and not a FM/BECMG/undefined, so always has an end + end: currentTrend.validity.end, + }); + continue; + } + let forecast; + if (nextTrend === undefined) { + forecast = hydrateWithPreviousContextIfNeeded({ + ...currentTrend, + start: currentTrend.validity.start, + end: reportValidity.end, + ...byIfNeeded(currentTrend), + }, previouslyHydratedTrend); + } + else { + forecast = hydrateWithPreviousContextIfNeeded({ + ...currentTrend, + start: currentTrend.validity.start, + end: new Date(nextTrend.validity.start), + ...byIfNeeded(currentTrend), + }, previouslyHydratedTrend); + } + forecasts.push(forecast); + previouslyHydratedTrend = forecast; + } + return forecasts; +} +/** + * BECMG doesn't always have all the context for the period, so + * it needs to be populated + */ +function hydrateWithPreviousContextIfNeeded(forecast, context) { + if (forecast.type !== WeatherChangeType.BECMG || !context) + return forecast; + // Remarks should not be carried over + context = { ...context }; + delete context.remark; + context.remarks = []; + delete context.nsw; + forecast = { + ...context, + ...forecast, + }; + if (!forecast.clouds.length) + forecast.clouds = context.clouds; + // If NSW = true, previous weather conditions have stopped and should + // not be carried over + if (!forecast.weatherConditions.length && !forecast.nsw) + forecast.weatherConditions = context.weatherConditions; + return forecast; +} +class TimestampOutOfBoundsError extends ParseError { + constructor(message) { + super(message); + this.name = "TimestampOutOfBoundsError"; + Object.setPrototypeOf(this, new.target.prototype); + } +} +function getCompositeForecastForDate(date, forecastContainer) { + // Validity bounds check + if (date.getTime() > forecastContainer.end.getTime() || + date.getTime() < forecastContainer.start.getTime()) + throw new TimestampOutOfBoundsError("Provided timestamp is outside the report validity period"); + let base; + let additional = []; + for (const forecast of forecastContainer.forecast) { + if (hasImplicitEnd(forecast) && + forecast.start.getTime() <= date.getTime()) { + // Is FM or initial forecast + base = forecast; + } + if (!hasImplicitEnd(forecast) && + forecast.end && + forecast.end.getTime() - date.getTime() > 0 && + forecast.start.getTime() - date.getTime() <= 0) { + // Is TEMPO, BECMG etc + additional.push(forecast); + } + } + if (!base) + throw new UnexpectedParseError("Unable to find trend for date"); + return { base, additional }; +} +function byIfNeeded(forecast) { + if (forecast.type !== WeatherChangeType.BECMG) + return {}; + return { by: forecast.validity.end }; +} + +function parseMetar(rawMetar, options) { + return parse(rawMetar, options, MetarParser, metarDatesHydrator); +} +function parseTAF(rawTAF, options) { + return parse(rawTAF, options, TAFParser, tafDatesHydrator); +} +function parseTAFAsForecast(rawTAF, options) { + const taf = parseTAF(rawTAF, options); + return getForecastFromTAF(taf); +} +function parse(rawReport, options, parser, datesHydrator) { + const lang = options?.locale || en; + try { + const report = new parser(lang).parse(rawReport); + if (options && "issued" in options) { + return datesHydrator(report, options.issued); + } + return report; + } + catch (e) { + if (e instanceof ParseError) + throw e; + throw new InvalidWeatherStatementError(e); + } +} + +export { CloudQuantity, CloudType, CommandExecutionError, Descriptive, Direction, DistanceUnit, Intensity, InvalidWeatherStatementError, ParseError, Phenomenon, RemarkType, RunwayInfoTrend, RunwayInfoUnit, SpeedUnit, TimeIndicator, TimestampOutOfBoundsError, UnexpectedParseError, ValueIndicator, WeatherChangeType, getCompositeForecastForDate, isWeatherConditionValid, parseMetar, parseTAF, parseTAFAsForecast };