metar-taf-parser 5.1.0 → 6.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 +27 -6
- package/metar-taf-parser.d.ts +34 -6
- package/metar-taf-parser.js +100 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ const datedMetar = parseMetar(rawMetarString, { issued });
|
|
|
45
45
|
|
|
46
46
|
#### `parseTAF`
|
|
47
47
|
|
|
48
|
-
👉 **Note:** One of the common use cases for TAF reports is to get relevant forecast data for a given
|
|
48
|
+
> 👉 **Note:** One of the common use cases for TAF reports is to get relevant forecast data for a given `Date`, or display the various forecast groups to the user. Check out [the `Forecast` abstraction](#higher-level-parsing-the-forecast-abstraction) below which may provide TAF data in a more normalized and easier to use format, depending on your use case.
|
|
49
49
|
|
|
50
50
|
```ts
|
|
51
51
|
import { parseTAF } from "metar-taf-parser";
|
|
@@ -61,11 +61,32 @@ const datedTAF = parseTAF(rawTAFString, { issued });
|
|
|
61
61
|
|
|
62
62
|
### Higher level parsing: The Forecast abstraction
|
|
63
63
|
|
|
64
|
-
TAF reports are a little funky... FM, BECMG, PROB, etc. You may find the `Forecast` abstraction more helpful.
|
|
64
|
+
TAF reports are a little funky... FM, BECMG, PROB, weird validity periods, etc. You may find the higher level `Forecast` abstraction more helpful.
|
|
65
|
+
|
|
66
|
+
⚠️ **Important:** The `Forecast` abstraction makes some assumptions in order to make it easier to consume the TAF. If you want different behavior, you may want to use the lower level `parseTAF` function directly (see above). Below are some of the assumptions the `Forecast` API makes:
|
|
67
|
+
|
|
68
|
+
1. The `validity` object found from `parseTAF`'s `trends[]` is too low level, so it is removed. Instead, you will find `start` and `end` on the base `Forecast` object. The end of a `FM` and `BECMG` group is derived from the start of the next `FM`/`BECMG` trend, or the end of the report validity if the last.
|
|
69
|
+
|
|
70
|
+
Additionally, there is a property, `by`, on `BECMG` trends for when conditions are expected to finish transitioning. You will need to type guard `type = BECMG` to access this property.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const firstForecast = report.forecast[1];
|
|
74
|
+
if (firstForecast.type === WeatherChangeType.BECMG) {
|
|
75
|
+
// Can now access `by`
|
|
76
|
+
console.log(firstForecast.by);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
2. `BECMG` trends are hydrated with the context of previous trends. For example, if:
|
|
81
|
+
|
|
82
|
+
TAF SBBR 221500Z 2218/2318 15008KT 9999 FEW045
|
|
83
|
+
BECMG 2308/2310 09002KT
|
|
84
|
+
|
|
85
|
+
Then the `BECMG` group will also have visibility and clouds from previously found conditions, with updated winds.
|
|
65
86
|
|
|
66
87
|
#### `parseTAFAsForecast`
|
|
67
88
|
|
|
68
|
-
Returns a more normalized TAF report
|
|
89
|
+
Returns a more normalized TAF report than `parseTAF`. Most notably: while the `parseTAF` function returns initial weather conditions on the base of the returned result (and further conditions on `trends[]`), the `parseTAFAsForecast` function returns the initial weather conditions as the first element of the `forecast[]` property (with `type = undefined`), followed by subsequent trends. (For more, please see the above about the forecast abstraction.) This makes it much easier to render a UI similar to the [aviationweather.gov](https://www.aviationweather.gov/taf/data?ids=SBPJ&format=decoded&metars=off&layout=on) TAF decoder.
|
|
69
90
|
|
|
70
91
|
```ts
|
|
71
92
|
import { parseTAFAsForecast } from "metar-taf-parser";
|
|
@@ -80,11 +101,11 @@ console.log(report.forecast);
|
|
|
80
101
|
|
|
81
102
|
> ⚠️ **Warning:** Experimental API
|
|
82
103
|
|
|
83
|
-
Provides all relevant weather conditions for a given timestamp. It returns a `ICompositeForecast` with a `base` and `additional` component. The `base` component is the base weather condition period (
|
|
104
|
+
Provides all relevant weather conditions for a given timestamp. It returns a `ICompositeForecast` with a `base` and `additional` component. The `base` component is the base weather condition period (type = `FM`, `BECMG`, or `undefined`) - and there will always be one.
|
|
84
105
|
|
|
85
|
-
The `additional` property is an array of weather condition periods valid for the given timestamp (any `
|
|
106
|
+
The `additional` property is an array of weather condition periods valid for the given timestamp (any `PROB` and/or `TEMPO`)
|
|
86
107
|
|
|
87
|
-
You will still need to write some logic to use this API to determine what data to use - for example, if `additional[0].visibility` exists, use it over `base.visibility`.
|
|
108
|
+
You will still need to write some logic to use this API to determine what data to use - for example, if `additional[0].visibility` exists, you may want to use it over `base.visibility`.
|
|
88
109
|
|
|
89
110
|
#### Example
|
|
90
111
|
|
package/metar-taf-parser.d.ts
CHANGED
|
@@ -1045,6 +1045,9 @@ interface IAirport {
|
|
|
1045
1045
|
interface IWind {
|
|
1046
1046
|
speed: number;
|
|
1047
1047
|
direction: string;
|
|
1048
|
+
/**
|
|
1049
|
+
* If undefined, direction is variable
|
|
1050
|
+
*/
|
|
1048
1051
|
degrees?: number;
|
|
1049
1052
|
gust?: number;
|
|
1050
1053
|
minVariation?: number;
|
|
@@ -1077,11 +1080,14 @@ interface IWeatherCondition {
|
|
|
1077
1080
|
phenomenons: Phenomenon[];
|
|
1078
1081
|
}
|
|
1079
1082
|
declare function isWeatherConditionValid(weather: IWeatherCondition): boolean;
|
|
1080
|
-
interface
|
|
1083
|
+
interface ITemperature {
|
|
1081
1084
|
temperature: number;
|
|
1082
1085
|
day: number;
|
|
1083
1086
|
hour: number;
|
|
1084
1087
|
}
|
|
1088
|
+
interface ITemperatureDated extends ITemperature {
|
|
1089
|
+
date: Date;
|
|
1090
|
+
}
|
|
1085
1091
|
interface IRunwayInfo {
|
|
1086
1092
|
name: string;
|
|
1087
1093
|
minRange: number;
|
|
@@ -1168,8 +1174,8 @@ interface IMetar extends IAbstractWeatherCode {
|
|
|
1168
1174
|
}
|
|
1169
1175
|
interface ITAF extends IAbstractWeatherCode {
|
|
1170
1176
|
validity: IValidity;
|
|
1171
|
-
maxTemperature?:
|
|
1172
|
-
minTemperature?:
|
|
1177
|
+
maxTemperature?: ITemperature;
|
|
1178
|
+
minTemperature?: ITemperature;
|
|
1173
1179
|
trends: TAFTrend[];
|
|
1174
1180
|
/**
|
|
1175
1181
|
* Just the first part of the TAF message without trends (FM, BECMG, etc)
|
|
@@ -1265,6 +1271,8 @@ interface ITAFDated extends ITAF {
|
|
|
1265
1271
|
start: Date;
|
|
1266
1272
|
end: Date;
|
|
1267
1273
|
};
|
|
1274
|
+
minTemperature?: ITemperatureDated;
|
|
1275
|
+
maxTemperature?: ITemperatureDated;
|
|
1268
1276
|
trends: TAFTrendDated[];
|
|
1269
1277
|
}
|
|
1270
1278
|
|
|
@@ -1297,14 +1305,34 @@ declare class UnexpectedParseError extends ParseError {
|
|
|
1297
1305
|
* The initial forecast, extracted from the first line of the TAF, does not have
|
|
1298
1306
|
* a trend type (FM, BECMG, etc)
|
|
1299
1307
|
*/
|
|
1300
|
-
declare type
|
|
1301
|
-
|
|
1308
|
+
declare type ForecastWithoutDates = Omit<TAFTrendDated, "type"> & Partial<Pick<TAFTrendDated, "type">>;
|
|
1309
|
+
declare type ForecastWithoutValidity = Omit<ForecastWithoutDates, "validity">;
|
|
1310
|
+
declare type Forecast = Omit<ForecastWithoutValidity, "type"> & {
|
|
1311
|
+
start: Date;
|
|
1312
|
+
end: Date;
|
|
1313
|
+
} & ({
|
|
1314
|
+
type: Exclude<WeatherChangeType, WeatherChangeType.BECMG> | undefined;
|
|
1315
|
+
} | {
|
|
1316
|
+
type: WeatherChangeType.BECMG;
|
|
1317
|
+
/**
|
|
1318
|
+
* BECMG has a special date, `by`, for when the transition will finish
|
|
1319
|
+
*
|
|
1320
|
+
* For example, a BECMG trend may `start` at 1:00PM and `end` at 5:00PM, but
|
|
1321
|
+
* `by` may be `3:00PM` to denote that conditions will transition from a period of
|
|
1322
|
+
* 1:00PM to 3:00PM
|
|
1323
|
+
*/
|
|
1324
|
+
by: Date;
|
|
1325
|
+
});
|
|
1326
|
+
interface IForecastContainer extends IFlags {
|
|
1302
1327
|
station: string;
|
|
1303
1328
|
issued: Date;
|
|
1304
1329
|
start: Date;
|
|
1305
1330
|
end: Date;
|
|
1306
1331
|
message: string;
|
|
1307
1332
|
forecast: Forecast[];
|
|
1333
|
+
amendment?: true;
|
|
1334
|
+
maxTemperature?: ITemperatureDated;
|
|
1335
|
+
minTemperature?: ITemperatureDated;
|
|
1308
1336
|
}
|
|
1309
1337
|
interface ICompositeForecast {
|
|
1310
1338
|
/**
|
|
@@ -1344,4 +1372,4 @@ declare function parseTAF(rawTAF: string, options?: IMetarTAFParserOptions): ITA
|
|
|
1344
1372
|
declare function parseTAF(rawTAF: string, options?: IMetarTAFParserOptionsDated): ITAFDated;
|
|
1345
1373
|
declare function parseTAFAsForecast(rawTAF: string, options: IMetarTAFParserOptionsDated): IForecastContainer;
|
|
1346
1374
|
|
|
1347
|
-
export { CloudQuantity, CloudType, CommandExecutionError, Descriptive, Direction, Distance, DistanceUnit, Forecast, IAbstractTrend, IAbstractValidity, IAbstractWeatherCode, IAbstractWeatherCodeDated, IAbstractWeatherContainer, IAirport, IBaseRemark, IBaseTAFTrend, ICeilingHeightRemark, ICeilingSecondLocationRemark, ICloud, ICompositeForecast, ICountry, IEndValidity, IFMValidity, IFlags, IForecastContainer, IHourlyMaximumMinimumTemperatureRemark, IHourlyMaximumTemperatureRemark, IHourlyMinimumTemperatureRemark, IHourlyPrecipitationAmountRemark, IHourlyPressureRemark, IHourlyTemperatureDewPointRemark, IIceAccretionRemark, IMetar, IMetarTAFParserOptions, IMetarTAFParserOptionsDated, IMetarTrend, IMetarTrendTime, IObscurationRemark, IPrecipitationAmount24HourRemark, IPrecipitationAmount36HourRemark, IPrecipitationBegEndRemark, IPrevailingVisibilityRemark, IRunwayInfo, ISeaLevelPressureRemark, ISecondLocationVisibilityRemark, ISectorVisibilityRemark, ISmallHailSizeRemark, ISnowIncreaseRemark, ISnowPelletsRemark, ISunshineDurationRemark, ISurfaceVisibilityRemark, ITAF, ITAFDated, ITemperatureDated, IThunderStormLocationMovingRemark, IThunderStormLocationRemark, ITime, ITornadicActivityBegEndRemark, ITornadicActivityBegRemark, ITornadicActivityEndRemark, ITowerVisibilityRemark, IUnknownRemark, IValidity, IValidityDated, IVariableSkyHeightRemark, IVariableSkyRemark, IVirgaDirectionRemark, IWaterEquivalentSnowRemark, IWeatherCondition, IWind, IWindPeakCommandRemark, IWindShear, IWindShiftFropaRemark, Intensity, InvalidWeatherStatementError, Locale, ParseError, Phenomenon, Remark, RemarkType, RunwayInfoTrend, RunwayInfoUnit, TAFTrend, TAFTrendDated, TimeIndicator, TimestampOutOfBoundsError, UnexpectedParseError, ValueIndicator, Visibility, WeatherChangeType, getCompositeForecastForDate, isWeatherConditionValid, parseMetar, parseTAF, parseTAFAsForecast };
|
|
1375
|
+
export { CloudQuantity, CloudType, CommandExecutionError, Descriptive, Direction, Distance, DistanceUnit, Forecast, IAbstractTrend, IAbstractValidity, IAbstractWeatherCode, IAbstractWeatherCodeDated, IAbstractWeatherContainer, IAirport, IBaseRemark, IBaseTAFTrend, ICeilingHeightRemark, ICeilingSecondLocationRemark, ICloud, ICompositeForecast, ICountry, IEndValidity, IFMValidity, IFlags, IForecastContainer, IHourlyMaximumMinimumTemperatureRemark, IHourlyMaximumTemperatureRemark, IHourlyMinimumTemperatureRemark, IHourlyPrecipitationAmountRemark, IHourlyPressureRemark, IHourlyTemperatureDewPointRemark, IIceAccretionRemark, IMetar, IMetarTAFParserOptions, IMetarTAFParserOptionsDated, IMetarTrend, IMetarTrendTime, IObscurationRemark, IPrecipitationAmount24HourRemark, IPrecipitationAmount36HourRemark, IPrecipitationBegEndRemark, IPrevailingVisibilityRemark, IRunwayInfo, ISeaLevelPressureRemark, ISecondLocationVisibilityRemark, ISectorVisibilityRemark, ISmallHailSizeRemark, ISnowIncreaseRemark, ISnowPelletsRemark, ISunshineDurationRemark, ISurfaceVisibilityRemark, ITAF, ITAFDated, ITemperature, ITemperatureDated, IThunderStormLocationMovingRemark, IThunderStormLocationRemark, ITime, ITornadicActivityBegEndRemark, ITornadicActivityBegRemark, ITornadicActivityEndRemark, ITowerVisibilityRemark, IUnknownRemark, IValidity, IValidityDated, IVariableSkyHeightRemark, IVariableSkyRemark, IVirgaDirectionRemark, IWaterEquivalentSnowRemark, IWeatherCondition, IWind, IWindPeakCommandRemark, IWindShear, IWindShiftFropaRemark, Intensity, InvalidWeatherStatementError, Locale, ParseError, Phenomenon, Remark, RemarkType, RunwayInfoTrend, RunwayInfoUnit, TAFTrend, TAFTrendDated, TimeIndicator, TimestampOutOfBoundsError, UnexpectedParseError, ValueIndicator, Visibility, WeatherChangeType, getCompositeForecastForDate, isWeatherConditionValid, parseMetar, parseTAF, parseTAFAsForecast };
|
package/metar-taf-parser.js
CHANGED
|
@@ -2615,6 +2615,18 @@ function tafDatesHydrator(report, date) {
|
|
|
2615
2615
|
start: getReportDate(issued, report.validity.startDay, report.validity.startHour),
|
|
2616
2616
|
end: getReportDate(issued, report.validity.endDay, report.validity.endHour),
|
|
2617
2617
|
},
|
|
2618
|
+
minTemperature: report.minTemperature
|
|
2619
|
+
? {
|
|
2620
|
+
...report.minTemperature,
|
|
2621
|
+
date: getReportDate(issued, report.minTemperature.day, report.minTemperature.hour),
|
|
2622
|
+
}
|
|
2623
|
+
: undefined,
|
|
2624
|
+
maxTemperature: report.maxTemperature
|
|
2625
|
+
? {
|
|
2626
|
+
...report.maxTemperature,
|
|
2627
|
+
date: getReportDate(issued, report.maxTemperature.day, report.maxTemperature.hour),
|
|
2628
|
+
}
|
|
2629
|
+
: undefined,
|
|
2618
2630
|
trends: report.trends.map((trend) => ({
|
|
2619
2631
|
...trend,
|
|
2620
2632
|
validity: (() => {
|
|
@@ -2638,12 +2650,10 @@ function tafDatesHydrator(report, date) {
|
|
|
2638
2650
|
|
|
2639
2651
|
function getForecastFromTAF(taf) {
|
|
2640
2652
|
return {
|
|
2641
|
-
|
|
2642
|
-
station: taf.station,
|
|
2643
|
-
message: taf.message,
|
|
2653
|
+
...taf,
|
|
2644
2654
|
start: getReportDate(taf.issued, taf.validity.startDay, taf.validity.startHour),
|
|
2645
2655
|
end: getReportDate(taf.issued, taf.validity.endDay, taf.validity.endHour),
|
|
2646
|
-
forecast: [makeInitialForecast(taf), ...taf.trends],
|
|
2656
|
+
forecast: hydrateEndDates([makeInitialForecast(taf), ...taf.trends], taf.validity),
|
|
2647
2657
|
};
|
|
2648
2658
|
}
|
|
2649
2659
|
/**
|
|
@@ -2670,6 +2680,80 @@ function makeInitialForecast(taf) {
|
|
|
2670
2680
|
},
|
|
2671
2681
|
};
|
|
2672
2682
|
}
|
|
2683
|
+
function hasImplicitEnd({ type }) {
|
|
2684
|
+
return (type === WeatherChangeType.FM ||
|
|
2685
|
+
// BECMG are special - the "end" date in the validity isn't actually
|
|
2686
|
+
// the end date, it's when the change that's "becoming" is expected to
|
|
2687
|
+
// finish transition. The actual "end" date of the BECMG is determined by
|
|
2688
|
+
// the next FM/BECMG/end of the report validity, just like a FM
|
|
2689
|
+
type === WeatherChangeType.BECMG ||
|
|
2690
|
+
// Special case for beginning of report conditions
|
|
2691
|
+
type === undefined);
|
|
2692
|
+
}
|
|
2693
|
+
function hydrateEndDates(trends, reportValidity) {
|
|
2694
|
+
function findNext(index) {
|
|
2695
|
+
for (let i = index; i < trends.length; i++) {
|
|
2696
|
+
if (hasImplicitEnd(trends[i]))
|
|
2697
|
+
return trends[i];
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
const forecasts = [];
|
|
2701
|
+
let previouslyHydratedTrend;
|
|
2702
|
+
for (let i = 0; i < trends.length; i++) {
|
|
2703
|
+
const currentTrend = trends[i];
|
|
2704
|
+
const nextTrend = findNext(i + 1);
|
|
2705
|
+
if (!hasImplicitEnd(currentTrend)) {
|
|
2706
|
+
forecasts.push({
|
|
2707
|
+
...currentTrend,
|
|
2708
|
+
start: currentTrend.validity.start,
|
|
2709
|
+
// Has a type and not a FM/BECMG/undefined, so always has an end
|
|
2710
|
+
end: currentTrend.validity.end,
|
|
2711
|
+
});
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
let forecast;
|
|
2715
|
+
if (nextTrend === undefined) {
|
|
2716
|
+
forecast = hydrateWithPreviousContextIfNeeded({
|
|
2717
|
+
...currentTrend,
|
|
2718
|
+
start: currentTrend.validity.start,
|
|
2719
|
+
end: reportValidity.end,
|
|
2720
|
+
...byIfNeeded(currentTrend),
|
|
2721
|
+
}, previouslyHydratedTrend);
|
|
2722
|
+
}
|
|
2723
|
+
else {
|
|
2724
|
+
forecast = hydrateWithPreviousContextIfNeeded({
|
|
2725
|
+
...currentTrend,
|
|
2726
|
+
start: currentTrend.validity.start,
|
|
2727
|
+
end: new Date(nextTrend.validity.start),
|
|
2728
|
+
...byIfNeeded(currentTrend),
|
|
2729
|
+
}, previouslyHydratedTrend);
|
|
2730
|
+
}
|
|
2731
|
+
forecasts.push(forecast);
|
|
2732
|
+
previouslyHydratedTrend = forecast;
|
|
2733
|
+
}
|
|
2734
|
+
return forecasts;
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* BECMG doesn't always have all the context for the period, so
|
|
2738
|
+
* it needs to be populated
|
|
2739
|
+
*/
|
|
2740
|
+
function hydrateWithPreviousContextIfNeeded(forecast, context) {
|
|
2741
|
+
if (forecast.type !== WeatherChangeType.BECMG || !context)
|
|
2742
|
+
return forecast;
|
|
2743
|
+
// Remarks should not be carried over
|
|
2744
|
+
context = { ...context };
|
|
2745
|
+
delete context.remark;
|
|
2746
|
+
context.remarks = [];
|
|
2747
|
+
forecast = {
|
|
2748
|
+
...context,
|
|
2749
|
+
...forecast,
|
|
2750
|
+
};
|
|
2751
|
+
if (!forecast.clouds.length)
|
|
2752
|
+
forecast.clouds = context.clouds;
|
|
2753
|
+
if (!forecast.weatherConditions.length)
|
|
2754
|
+
forecast.weatherConditions = context.weatherConditions;
|
|
2755
|
+
return forecast;
|
|
2756
|
+
}
|
|
2673
2757
|
class TimestampOutOfBoundsError extends ParseError {
|
|
2674
2758
|
constructor(message) {
|
|
2675
2759
|
super(message);
|
|
@@ -2685,15 +2769,16 @@ function getCompositeForecastForDate(date, forecastContainer) {
|
|
|
2685
2769
|
let base;
|
|
2686
2770
|
let additional = [];
|
|
2687
2771
|
for (const forecast of forecastContainer.forecast) {
|
|
2688
|
-
if (
|
|
2689
|
-
forecast.
|
|
2772
|
+
if (hasImplicitEnd(forecast) &&
|
|
2773
|
+
forecast.start.getTime() <= date.getTime()) {
|
|
2690
2774
|
// Is FM or initial forecast
|
|
2691
2775
|
base = forecast;
|
|
2692
2776
|
}
|
|
2693
|
-
if (forecast
|
|
2694
|
-
forecast.
|
|
2695
|
-
forecast.
|
|
2696
|
-
|
|
2777
|
+
if (!hasImplicitEnd(forecast) &&
|
|
2778
|
+
forecast.end &&
|
|
2779
|
+
forecast.end.getTime() - date.getTime() > 0 &&
|
|
2780
|
+
forecast.start.getTime() - date.getTime() <= 0) {
|
|
2781
|
+
// Is TEMPO, BECMG etc
|
|
2697
2782
|
additional.push(forecast);
|
|
2698
2783
|
}
|
|
2699
2784
|
}
|
|
@@ -2701,6 +2786,11 @@ function getCompositeForecastForDate(date, forecastContainer) {
|
|
|
2701
2786
|
throw new UnexpectedParseError("Unable to find trend for date");
|
|
2702
2787
|
return { base, additional };
|
|
2703
2788
|
}
|
|
2789
|
+
function byIfNeeded(forecast) {
|
|
2790
|
+
if (forecast.type !== WeatherChangeType.BECMG)
|
|
2791
|
+
return {};
|
|
2792
|
+
return { by: forecast.validity.end };
|
|
2793
|
+
}
|
|
2704
2794
|
|
|
2705
2795
|
function parseMetar(rawMetar, options) {
|
|
2706
2796
|
return parse(rawMetar, options, MetarParser, metarDatesHydrator);
|