metar-taf-parser 6.1.3 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/metar-taf-parser.d.ts +17 -8
- package/metar-taf-parser.js +59 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,11 +101,13 @@ console.log(report.forecast);
|
|
|
101
101
|
|
|
102
102
|
> ⚠️ **Warning:** Experimental API
|
|
103
103
|
|
|
104
|
-
Provides all relevant weather conditions for a given timestamp. It returns
|
|
104
|
+
Provides all relevant weather conditions for a given timestamp. It returns an `ICompositeForecast` with a `prevailing` and `supplemental` component. The `prevailing` component is the prevailing weather condition period (type = `FM`, `BECMG`, or `undefined`) - and there will always be one.
|
|
105
105
|
|
|
106
|
-
The `
|
|
106
|
+
The `supplemental` property is an array of weather condition periods valid for the given timestamp (any `PROB`, `TEMPO` and/or `INTER`) - conditions that are ephemeral and/or lower probability.
|
|
107
107
|
|
|
108
|
-
You will still need to write some logic to
|
|
108
|
+
You will still need to write some logic to determine what data to use - for example, if `supplemental[0].visibility` exists, you may want to use it over `prevailing.visibility`, or otherwise present it to the user.
|
|
109
|
+
|
|
110
|
+
This function throws a `TimestampOutOfBoundsError` if the provided date is outside of the report validity period.
|
|
109
111
|
|
|
110
112
|
#### Example
|
|
111
113
|
|
package/metar-taf-parser.d.ts
CHANGED
|
@@ -192,6 +192,12 @@ declare enum WeatherChangeType {
|
|
|
192
192
|
* visibility two in mist and haze between 0000Z and 0600Z.
|
|
193
193
|
*/
|
|
194
194
|
TEMPO = "TEMPO",
|
|
195
|
+
/**
|
|
196
|
+
* For periods up to 30 minutes (`INTER` or intermittent).
|
|
197
|
+
*
|
|
198
|
+
* Otherwise, similar to `TEMPO`
|
|
199
|
+
*/
|
|
200
|
+
INTER = "INTER",
|
|
195
201
|
/**
|
|
196
202
|
* Probability Forecast
|
|
197
203
|
*
|
|
@@ -811,6 +817,9 @@ interface IPrevailingVisibilityRemark extends IBaseRemark {
|
|
|
811
817
|
|
|
812
818
|
interface ISeaLevelPressureRemark extends IBaseRemark {
|
|
813
819
|
type: RemarkType.SeaLevelPressure;
|
|
820
|
+
/**
|
|
821
|
+
* Sea level pressure, in hPa/millibars
|
|
822
|
+
*/
|
|
814
823
|
pressure: number;
|
|
815
824
|
}
|
|
816
825
|
|
|
@@ -1211,7 +1220,7 @@ interface IBaseTAFTrend extends IAbstractTrend {
|
|
|
1211
1220
|
*
|
|
1212
1221
|
* - FM trends also have `startMinutes`. They **DO NOT** have an explicit end
|
|
1213
1222
|
* validity (it is implied by the following FM).
|
|
1214
|
-
* - All others (PROB, TEMPO, BECMG) have `endDay` and `endHour`.
|
|
1223
|
+
* - All others (PROB, TEMPO, BECMG, INTER) have `endDay` and `endHour`.
|
|
1215
1224
|
*
|
|
1216
1225
|
* All properties are allowed to be accessed (as optionals), but if you want
|
|
1217
1226
|
* type guarantees, you can check the trend type. For example:
|
|
@@ -1224,6 +1233,7 @@ interface IBaseTAFTrend extends IAbstractTrend {
|
|
|
1224
1233
|
* case WeatherChangeType.PROB:
|
|
1225
1234
|
* case WeatherChangeType.BECMG:
|
|
1226
1235
|
* case WeatherChangeType.TEMPO:
|
|
1236
|
+
* case WeatherChangeType.INTER:
|
|
1227
1237
|
* // trend.validity now has endHour, endDay defined
|
|
1228
1238
|
* }
|
|
1229
1239
|
* ```
|
|
@@ -1343,17 +1353,16 @@ interface IForecastContainer extends IFlags {
|
|
|
1343
1353
|
}
|
|
1344
1354
|
interface ICompositeForecast {
|
|
1345
1355
|
/**
|
|
1346
|
-
*
|
|
1356
|
+
* Prevailing conditions: type is `FM`, `BECMG` or initial conditions (`undefined` type)
|
|
1347
1357
|
*/
|
|
1348
|
-
|
|
1358
|
+
prevailing: Forecast;
|
|
1349
1359
|
/**
|
|
1350
|
-
*
|
|
1351
|
-
*
|
|
1352
|
-
* precipitation events
|
|
1360
|
+
* supplemental forecasts may have probabilities of occuring, be temporary,
|
|
1361
|
+
* or otherwise notable change of conditions. They enhance the prevailing forecast.
|
|
1353
1362
|
*
|
|
1354
|
-
* `type` is (`
|
|
1363
|
+
* `type` is (`TEMPO`, `INTER` or `PROB`)
|
|
1355
1364
|
*/
|
|
1356
|
-
|
|
1365
|
+
supplemental: Forecast[];
|
|
1357
1366
|
}
|
|
1358
1367
|
declare class TimestampOutOfBoundsError extends ParseError {
|
|
1359
1368
|
name: string;
|
package/metar-taf-parser.js
CHANGED
|
@@ -384,6 +384,12 @@ var WeatherChangeType;
|
|
|
384
384
|
* visibility two in mist and haze between 0000Z and 0600Z.
|
|
385
385
|
*/
|
|
386
386
|
WeatherChangeType["TEMPO"] = "TEMPO";
|
|
387
|
+
/**
|
|
388
|
+
* For periods up to 30 minutes (`INTER` or intermittent).
|
|
389
|
+
*
|
|
390
|
+
* Otherwise, similar to `TEMPO`
|
|
391
|
+
*/
|
|
392
|
+
WeatherChangeType["INTER"] = "INTER";
|
|
387
393
|
/**
|
|
388
394
|
* Probability Forecast
|
|
389
395
|
*
|
|
@@ -1797,7 +1803,6 @@ class MainVisibilityCommand {
|
|
|
1797
1803
|
const distance = convertVisibility(matches[1]);
|
|
1798
1804
|
if (!container.visibility)
|
|
1799
1805
|
container.visibility = distance;
|
|
1800
|
-
container.visibility = { ...container.visibility, ...distance };
|
|
1801
1806
|
if (matches[2] === "NDV")
|
|
1802
1807
|
container.visibility.ndv = true;
|
|
1803
1808
|
return true;
|
|
@@ -2159,6 +2164,7 @@ class AbstractParser {
|
|
|
2159
2164
|
this.locale = locale;
|
|
2160
2165
|
this.FM = "FM";
|
|
2161
2166
|
this.TEMPO = "TEMPO";
|
|
2167
|
+
this.INTER = "INTER";
|
|
2162
2168
|
this.BECMG = "BECMG";
|
|
2163
2169
|
this.RMK = "RMK";
|
|
2164
2170
|
// Safari does not currently support negative lookbehind
|
|
@@ -2263,6 +2269,7 @@ class MetarParser extends AbstractParser {
|
|
|
2263
2269
|
let i = index + 1;
|
|
2264
2270
|
while (i < trendParts.length &&
|
|
2265
2271
|
trendParts[i] !== this.TEMPO &&
|
|
2272
|
+
trendParts[i] !== this.INTER &&
|
|
2266
2273
|
trendParts[i] !== this.BECMG) {
|
|
2267
2274
|
if (trendParts[i].startsWith(this.FM) ||
|
|
2268
2275
|
trendParts[i].startsWith(this.TL) ||
|
|
@@ -2306,16 +2313,19 @@ class MetarParser extends AbstractParser {
|
|
|
2306
2313
|
metar.nosig = true;
|
|
2307
2314
|
}
|
|
2308
2315
|
else if (metarTab[index] === this.TEMPO ||
|
|
2316
|
+
metarTab[index] === this.INTER ||
|
|
2309
2317
|
metarTab[index] === this.BECMG) {
|
|
2318
|
+
const startIndex = index;
|
|
2310
2319
|
const trend = {
|
|
2311
2320
|
type: WeatherChangeType[metarTab[index]],
|
|
2312
2321
|
weatherConditions: [],
|
|
2313
2322
|
clouds: [],
|
|
2314
2323
|
times: [],
|
|
2315
2324
|
remarks: [],
|
|
2316
|
-
raw:
|
|
2325
|
+
raw: "",
|
|
2317
2326
|
};
|
|
2318
2327
|
index = this.parseTrend(index, trend, metarTab);
|
|
2328
|
+
trend.raw = metarTab.slice(startIndex, index + 1).join(" ");
|
|
2319
2329
|
metar.trends.push(trend);
|
|
2320
2330
|
}
|
|
2321
2331
|
else if (metarTab[index] === this.RMK) {
|
|
@@ -2346,6 +2356,27 @@ class TAFParser extends AbstractParser {
|
|
|
2346
2356
|
this.TN = "TN";
|
|
2347
2357
|
_TAFParser_validityPattern.set(this, /^\d{4}\/\d{4}$/);
|
|
2348
2358
|
}
|
|
2359
|
+
/**
|
|
2360
|
+
* TAF messages can be formatted poorly
|
|
2361
|
+
*
|
|
2362
|
+
* Attempt to handle those situations gracefully
|
|
2363
|
+
*/
|
|
2364
|
+
parseMessageStart(input) {
|
|
2365
|
+
let index = 0;
|
|
2366
|
+
if (input[index] === this.TAF)
|
|
2367
|
+
index += 1;
|
|
2368
|
+
if (input[index + 1] === this.TAF)
|
|
2369
|
+
index += 2;
|
|
2370
|
+
const flags1 = findFlags(input[index]);
|
|
2371
|
+
if (flags1)
|
|
2372
|
+
index += 1;
|
|
2373
|
+
if (input[index] === this.TAF)
|
|
2374
|
+
index += 1;
|
|
2375
|
+
const flags2 = findFlags(input[index]);
|
|
2376
|
+
if (flags2)
|
|
2377
|
+
index += 1;
|
|
2378
|
+
return [index, { ...flags1, ...flags2 }];
|
|
2379
|
+
}
|
|
2349
2380
|
/**
|
|
2350
2381
|
* the message to parse
|
|
2351
2382
|
* @param input
|
|
@@ -2354,15 +2385,7 @@ class TAFParser extends AbstractParser {
|
|
|
2354
2385
|
*/
|
|
2355
2386
|
parse(input) {
|
|
2356
2387
|
const lines = this.extractLinesTokens(input);
|
|
2357
|
-
let index = 0;
|
|
2358
|
-
if (lines[0][0] === this.TAF)
|
|
2359
|
-
index = 1;
|
|
2360
|
-
if (lines[0][1] === this.TAF)
|
|
2361
|
-
index = 2;
|
|
2362
|
-
const flags = findFlags(lines[0][index]);
|
|
2363
|
-
if (flags) {
|
|
2364
|
-
index += 1;
|
|
2365
|
-
}
|
|
2388
|
+
let [index, flags] = this.parseMessageStart(lines[0]);
|
|
2366
2389
|
const station = lines[0][index];
|
|
2367
2390
|
index += 1;
|
|
2368
2391
|
const time = parseDeliveryTime(lines[0][index]);
|
|
@@ -2426,12 +2449,12 @@ class TAFParser extends AbstractParser {
|
|
|
2426
2449
|
const singleLine = tafCode.replace(/\n/g, " ");
|
|
2427
2450
|
const cleanLine = singleLine.replace(/\s{2,}/g, " ");
|
|
2428
2451
|
const lines = joinProbIfNeeded(cleanLine
|
|
2429
|
-
.replace(/\s(?=PROB\d{2}\
|
|
2452
|
+
.replace(/\s(?=PROB\d{2}\s(?=TEMPO|INTER)|TEMPO|INTER|BECMG|FM|PROB)/g, "\n")
|
|
2430
2453
|
.split(/\n/));
|
|
2431
2454
|
// TODO cleanup
|
|
2432
2455
|
function joinProbIfNeeded(ls) {
|
|
2433
2456
|
for (let i = 0; i < ls.length; i++) {
|
|
2434
|
-
if (/^PROB\d{2}$/.test(ls[i]) && /^TEMPO/.test(ls[i + 1])) {
|
|
2457
|
+
if (/^PROB\d{2}$/.test(ls[i]) && /^TEMPO|INTER/.test(ls[i + 1])) {
|
|
2435
2458
|
ls.splice(i, 2, `${ls[i]} ${ls[i + 1]}`);
|
|
2436
2459
|
}
|
|
2437
2460
|
}
|
|
@@ -2466,7 +2489,8 @@ class TAFParser extends AbstractParser {
|
|
|
2466
2489
|
validity,
|
|
2467
2490
|
raw: lineTokens.join(" "),
|
|
2468
2491
|
};
|
|
2469
|
-
if (lineTokens.length > 1 &&
|
|
2492
|
+
if (lineTokens.length > 1 &&
|
|
2493
|
+
(lineTokens[1] === this.TEMPO || lineTokens[1] === this.INTER)) {
|
|
2470
2494
|
trend = {
|
|
2471
2495
|
...this.makeEmptyTAFTrend(),
|
|
2472
2496
|
type: WeatherChangeType[lineTokens[1]],
|
|
@@ -2665,8 +2689,9 @@ function tafDatesHydrator(report, date) {
|
|
|
2665
2689
|
}
|
|
2666
2690
|
|
|
2667
2691
|
function getForecastFromTAF(taf) {
|
|
2692
|
+
const { trends, wind, visibility, verticalVisibility, windShear, cavok, remark, remarks, clouds, weatherConditions, initialRaw, validity, ...tafWithoutBaseProperties } = taf;
|
|
2668
2693
|
return {
|
|
2669
|
-
...
|
|
2694
|
+
...tafWithoutBaseProperties,
|
|
2670
2695
|
start: getReportDate(taf.issued, taf.validity.startDay, taf.validity.startHour),
|
|
2671
2696
|
end: getReportDate(taf.issued, taf.validity.endDay, taf.validity.endHour),
|
|
2672
2697
|
forecast: hydrateEndDates([makeInitialForecast(taf), ...taf.trends], taf.validity),
|
|
@@ -2719,8 +2744,9 @@ function hydrateEndDates(trends, reportValidity) {
|
|
|
2719
2744
|
const currentTrend = trends[i];
|
|
2720
2745
|
const nextTrend = findNext(i + 1);
|
|
2721
2746
|
if (!hasImplicitEnd(currentTrend)) {
|
|
2747
|
+
const { validity, ...trend } = currentTrend;
|
|
2722
2748
|
forecasts.push({
|
|
2723
|
-
...
|
|
2749
|
+
...trend,
|
|
2724
2750
|
start: currentTrend.validity.start,
|
|
2725
2751
|
// Has a type and not a FM/BECMG/undefined, so always has an end
|
|
2726
2752
|
end: currentTrend.validity.end,
|
|
@@ -2728,9 +2754,10 @@ function hydrateEndDates(trends, reportValidity) {
|
|
|
2728
2754
|
continue;
|
|
2729
2755
|
}
|
|
2730
2756
|
let forecast;
|
|
2757
|
+
const { validity, ...trendWithoutValidity } = currentTrend;
|
|
2731
2758
|
if (nextTrend === undefined) {
|
|
2732
2759
|
forecast = hydrateWithPreviousContextIfNeeded({
|
|
2733
|
-
...
|
|
2760
|
+
...trendWithoutValidity,
|
|
2734
2761
|
start: currentTrend.validity.start,
|
|
2735
2762
|
end: reportValidity.end,
|
|
2736
2763
|
...byIfNeeded(currentTrend),
|
|
@@ -2738,7 +2765,7 @@ function hydrateEndDates(trends, reportValidity) {
|
|
|
2738
2765
|
}
|
|
2739
2766
|
else {
|
|
2740
2767
|
forecast = hydrateWithPreviousContextIfNeeded({
|
|
2741
|
-
...
|
|
2768
|
+
...trendWithoutValidity,
|
|
2742
2769
|
start: currentTrend.validity.start,
|
|
2743
2770
|
end: new Date(nextTrend.validity.start),
|
|
2744
2771
|
...byIfNeeded(currentTrend),
|
|
@@ -2760,12 +2787,16 @@ function hydrateWithPreviousContextIfNeeded(forecast, context) {
|
|
|
2760
2787
|
context = { ...context };
|
|
2761
2788
|
delete context.remark;
|
|
2762
2789
|
context.remarks = [];
|
|
2790
|
+
// vertical visibility should not be carried over, if clouds exist
|
|
2791
|
+
if (forecast.clouds.length)
|
|
2792
|
+
delete context.verticalVisibility;
|
|
2763
2793
|
forecast = {
|
|
2764
2794
|
...context,
|
|
2765
2795
|
...forecast,
|
|
2766
2796
|
};
|
|
2767
|
-
if (!forecast.clouds.length)
|
|
2797
|
+
if (!forecast.clouds.length) {
|
|
2768
2798
|
forecast.clouds = context.clouds;
|
|
2799
|
+
}
|
|
2769
2800
|
if (!forecast.weatherConditions.length)
|
|
2770
2801
|
forecast.weatherConditions = context.weatherConditions;
|
|
2771
2802
|
return forecast;
|
|
@@ -2782,25 +2813,25 @@ function getCompositeForecastForDate(date, forecastContainer) {
|
|
|
2782
2813
|
if (date.getTime() > forecastContainer.end.getTime() ||
|
|
2783
2814
|
date.getTime() < forecastContainer.start.getTime())
|
|
2784
2815
|
throw new TimestampOutOfBoundsError("Provided timestamp is outside the report validity period");
|
|
2785
|
-
let
|
|
2786
|
-
let
|
|
2816
|
+
let prevailing;
|
|
2817
|
+
let supplemental = [];
|
|
2787
2818
|
for (const forecast of forecastContainer.forecast) {
|
|
2788
2819
|
if (hasImplicitEnd(forecast) &&
|
|
2789
2820
|
forecast.start.getTime() <= date.getTime()) {
|
|
2790
|
-
// Is FM or initial forecast
|
|
2791
|
-
|
|
2821
|
+
// Is FM, BECMG or initial forecast
|
|
2822
|
+
prevailing = forecast;
|
|
2792
2823
|
}
|
|
2793
2824
|
if (!hasImplicitEnd(forecast) &&
|
|
2794
2825
|
forecast.end &&
|
|
2795
2826
|
forecast.end.getTime() - date.getTime() > 0 &&
|
|
2796
2827
|
forecast.start.getTime() - date.getTime() <= 0) {
|
|
2797
|
-
// Is TEMPO,
|
|
2798
|
-
|
|
2828
|
+
// Is TEMPO, INTER, PROB etc
|
|
2829
|
+
supplemental.push(forecast);
|
|
2799
2830
|
}
|
|
2800
2831
|
}
|
|
2801
|
-
if (!
|
|
2832
|
+
if (!prevailing)
|
|
2802
2833
|
throw new UnexpectedParseError("Unable to find trend for date");
|
|
2803
|
-
return {
|
|
2834
|
+
return { prevailing, supplemental };
|
|
2804
2835
|
}
|
|
2805
2836
|
function byIfNeeded(forecast) {
|
|
2806
2837
|
if (forecast.type !== WeatherChangeType.BECMG)
|
|
@@ -2822,7 +2853,7 @@ function parse(rawReport, options, parser, datesHydrator) {
|
|
|
2822
2853
|
const lang = options?.locale || en;
|
|
2823
2854
|
try {
|
|
2824
2855
|
const report = new parser(lang).parse(rawReport);
|
|
2825
|
-
if (options && "issued" in options) {
|
|
2856
|
+
if (options && "issued" in options && options.issued) {
|
|
2826
2857
|
return datesHydrator(report, options.issued);
|
|
2827
2858
|
}
|
|
2828
2859
|
return report;
|