metar-taf-parser 6.1.4 → 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 +56 -27
- 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,6 +2313,7 @@ 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) {
|
|
2310
2318
|
const startIndex = index;
|
|
2311
2319
|
const trend = {
|
|
@@ -2348,6 +2356,27 @@ class TAFParser extends AbstractParser {
|
|
|
2348
2356
|
this.TN = "TN";
|
|
2349
2357
|
_TAFParser_validityPattern.set(this, /^\d{4}\/\d{4}$/);
|
|
2350
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
|
+
}
|
|
2351
2380
|
/**
|
|
2352
2381
|
* the message to parse
|
|
2353
2382
|
* @param input
|
|
@@ -2356,15 +2385,7 @@ class TAFParser extends AbstractParser {
|
|
|
2356
2385
|
*/
|
|
2357
2386
|
parse(input) {
|
|
2358
2387
|
const lines = this.extractLinesTokens(input);
|
|
2359
|
-
let index = 0;
|
|
2360
|
-
if (lines[0][0] === this.TAF)
|
|
2361
|
-
index = 1;
|
|
2362
|
-
if (lines[0][1] === this.TAF)
|
|
2363
|
-
index = 2;
|
|
2364
|
-
const flags = findFlags(lines[0][index]);
|
|
2365
|
-
if (flags) {
|
|
2366
|
-
index += 1;
|
|
2367
|
-
}
|
|
2388
|
+
let [index, flags] = this.parseMessageStart(lines[0]);
|
|
2368
2389
|
const station = lines[0][index];
|
|
2369
2390
|
index += 1;
|
|
2370
2391
|
const time = parseDeliveryTime(lines[0][index]);
|
|
@@ -2428,12 +2449,12 @@ class TAFParser extends AbstractParser {
|
|
|
2428
2449
|
const singleLine = tafCode.replace(/\n/g, " ");
|
|
2429
2450
|
const cleanLine = singleLine.replace(/\s{2,}/g, " ");
|
|
2430
2451
|
const lines = joinProbIfNeeded(cleanLine
|
|
2431
|
-
.replace(/\s(?=PROB\d{2}\
|
|
2452
|
+
.replace(/\s(?=PROB\d{2}\s(?=TEMPO|INTER)|TEMPO|INTER|BECMG|FM|PROB)/g, "\n")
|
|
2432
2453
|
.split(/\n/));
|
|
2433
2454
|
// TODO cleanup
|
|
2434
2455
|
function joinProbIfNeeded(ls) {
|
|
2435
2456
|
for (let i = 0; i < ls.length; i++) {
|
|
2436
|
-
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])) {
|
|
2437
2458
|
ls.splice(i, 2, `${ls[i]} ${ls[i + 1]}`);
|
|
2438
2459
|
}
|
|
2439
2460
|
}
|
|
@@ -2468,7 +2489,8 @@ class TAFParser extends AbstractParser {
|
|
|
2468
2489
|
validity,
|
|
2469
2490
|
raw: lineTokens.join(" "),
|
|
2470
2491
|
};
|
|
2471
|
-
if (lineTokens.length > 1 &&
|
|
2492
|
+
if (lineTokens.length > 1 &&
|
|
2493
|
+
(lineTokens[1] === this.TEMPO || lineTokens[1] === this.INTER)) {
|
|
2472
2494
|
trend = {
|
|
2473
2495
|
...this.makeEmptyTAFTrend(),
|
|
2474
2496
|
type: WeatherChangeType[lineTokens[1]],
|
|
@@ -2667,8 +2689,9 @@ function tafDatesHydrator(report, date) {
|
|
|
2667
2689
|
}
|
|
2668
2690
|
|
|
2669
2691
|
function getForecastFromTAF(taf) {
|
|
2692
|
+
const { trends, wind, visibility, verticalVisibility, windShear, cavok, remark, remarks, clouds, weatherConditions, initialRaw, validity, ...tafWithoutBaseProperties } = taf;
|
|
2670
2693
|
return {
|
|
2671
|
-
...
|
|
2694
|
+
...tafWithoutBaseProperties,
|
|
2672
2695
|
start: getReportDate(taf.issued, taf.validity.startDay, taf.validity.startHour),
|
|
2673
2696
|
end: getReportDate(taf.issued, taf.validity.endDay, taf.validity.endHour),
|
|
2674
2697
|
forecast: hydrateEndDates([makeInitialForecast(taf), ...taf.trends], taf.validity),
|
|
@@ -2721,8 +2744,9 @@ function hydrateEndDates(trends, reportValidity) {
|
|
|
2721
2744
|
const currentTrend = trends[i];
|
|
2722
2745
|
const nextTrend = findNext(i + 1);
|
|
2723
2746
|
if (!hasImplicitEnd(currentTrend)) {
|
|
2747
|
+
const { validity, ...trend } = currentTrend;
|
|
2724
2748
|
forecasts.push({
|
|
2725
|
-
...
|
|
2749
|
+
...trend,
|
|
2726
2750
|
start: currentTrend.validity.start,
|
|
2727
2751
|
// Has a type and not a FM/BECMG/undefined, so always has an end
|
|
2728
2752
|
end: currentTrend.validity.end,
|
|
@@ -2730,9 +2754,10 @@ function hydrateEndDates(trends, reportValidity) {
|
|
|
2730
2754
|
continue;
|
|
2731
2755
|
}
|
|
2732
2756
|
let forecast;
|
|
2757
|
+
const { validity, ...trendWithoutValidity } = currentTrend;
|
|
2733
2758
|
if (nextTrend === undefined) {
|
|
2734
2759
|
forecast = hydrateWithPreviousContextIfNeeded({
|
|
2735
|
-
...
|
|
2760
|
+
...trendWithoutValidity,
|
|
2736
2761
|
start: currentTrend.validity.start,
|
|
2737
2762
|
end: reportValidity.end,
|
|
2738
2763
|
...byIfNeeded(currentTrend),
|
|
@@ -2740,7 +2765,7 @@ function hydrateEndDates(trends, reportValidity) {
|
|
|
2740
2765
|
}
|
|
2741
2766
|
else {
|
|
2742
2767
|
forecast = hydrateWithPreviousContextIfNeeded({
|
|
2743
|
-
...
|
|
2768
|
+
...trendWithoutValidity,
|
|
2744
2769
|
start: currentTrend.validity.start,
|
|
2745
2770
|
end: new Date(nextTrend.validity.start),
|
|
2746
2771
|
...byIfNeeded(currentTrend),
|
|
@@ -2762,12 +2787,16 @@ function hydrateWithPreviousContextIfNeeded(forecast, context) {
|
|
|
2762
2787
|
context = { ...context };
|
|
2763
2788
|
delete context.remark;
|
|
2764
2789
|
context.remarks = [];
|
|
2790
|
+
// vertical visibility should not be carried over, if clouds exist
|
|
2791
|
+
if (forecast.clouds.length)
|
|
2792
|
+
delete context.verticalVisibility;
|
|
2765
2793
|
forecast = {
|
|
2766
2794
|
...context,
|
|
2767
2795
|
...forecast,
|
|
2768
2796
|
};
|
|
2769
|
-
if (!forecast.clouds.length)
|
|
2797
|
+
if (!forecast.clouds.length) {
|
|
2770
2798
|
forecast.clouds = context.clouds;
|
|
2799
|
+
}
|
|
2771
2800
|
if (!forecast.weatherConditions.length)
|
|
2772
2801
|
forecast.weatherConditions = context.weatherConditions;
|
|
2773
2802
|
return forecast;
|
|
@@ -2784,25 +2813,25 @@ function getCompositeForecastForDate(date, forecastContainer) {
|
|
|
2784
2813
|
if (date.getTime() > forecastContainer.end.getTime() ||
|
|
2785
2814
|
date.getTime() < forecastContainer.start.getTime())
|
|
2786
2815
|
throw new TimestampOutOfBoundsError("Provided timestamp is outside the report validity period");
|
|
2787
|
-
let
|
|
2788
|
-
let
|
|
2816
|
+
let prevailing;
|
|
2817
|
+
let supplemental = [];
|
|
2789
2818
|
for (const forecast of forecastContainer.forecast) {
|
|
2790
2819
|
if (hasImplicitEnd(forecast) &&
|
|
2791
2820
|
forecast.start.getTime() <= date.getTime()) {
|
|
2792
|
-
// Is FM or initial forecast
|
|
2793
|
-
|
|
2821
|
+
// Is FM, BECMG or initial forecast
|
|
2822
|
+
prevailing = forecast;
|
|
2794
2823
|
}
|
|
2795
2824
|
if (!hasImplicitEnd(forecast) &&
|
|
2796
2825
|
forecast.end &&
|
|
2797
2826
|
forecast.end.getTime() - date.getTime() > 0 &&
|
|
2798
2827
|
forecast.start.getTime() - date.getTime() <= 0) {
|
|
2799
|
-
// Is TEMPO,
|
|
2800
|
-
|
|
2828
|
+
// Is TEMPO, INTER, PROB etc
|
|
2829
|
+
supplemental.push(forecast);
|
|
2801
2830
|
}
|
|
2802
2831
|
}
|
|
2803
|
-
if (!
|
|
2832
|
+
if (!prevailing)
|
|
2804
2833
|
throw new UnexpectedParseError("Unable to find trend for date");
|
|
2805
|
-
return {
|
|
2834
|
+
return { prevailing, supplemental };
|
|
2806
2835
|
}
|
|
2807
2836
|
function byIfNeeded(forecast) {
|
|
2808
2837
|
if (forecast.type !== WeatherChangeType.BECMG)
|
|
@@ -2824,7 +2853,7 @@ function parse(rawReport, options, parser, datesHydrator) {
|
|
|
2824
2853
|
const lang = options?.locale || en;
|
|
2825
2854
|
try {
|
|
2826
2855
|
const report = new parser(lang).parse(rawReport);
|
|
2827
|
-
if (options && "issued" in options) {
|
|
2856
|
+
if (options && "issued" in options && options.issued) {
|
|
2828
2857
|
return datesHydrator(report, options.issued);
|
|
2829
2858
|
}
|
|
2830
2859
|
return report;
|