@aeroswarm/weather 0.0.0-stage → 0.1.1

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 CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @aeroswarm/weather
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Decode raw weather messages (AFTN-wrapped or plain METAR, SPECI, TAF) and turn them into animation
4
+ parameters. No network, no API: it only takes text in and gives data out.
5
+
6
+ ```ts
7
+ import { decodeWeatherMessage, animationSnapshot } from "@aeroswarm/weather";
8
+
9
+ const reports = decodeWeatherMessage(rawAftnText, receivedAt);
10
+ const metar = reports.find((r) => r.kind !== "TAF");
11
+ const taf = reports.find((r) => r.kind === "TAF");
12
+
13
+ const snapshot = animationSnapshot({ observation: metar, forecast: taf, at: new Date() });
14
+ // snapshot.state.wind / clouds / visibility / precipitation / thunderstorm / obscuration
15
+ ```
16
+
17
+ - `extractReports(text)` unwraps AFTN (`ZCZC` ... `NNNN`, address lines, bulletin headers) and splits
18
+ the reports on `=`.
19
+ - `parseMetar`, `parseTaf` read one report. METAR/TAF carry only day and time, so pass the time the
20
+ message was received as `reference`.
21
+ - `forecastAt(taf, at)` returns the conditions at a time: base, FM, BECMG (blended while it
22
+ changes), TEMPO, and PROB groups only when `includeProbable` is set.
23
+ - `animationFromConditions` maps conditions to renderer-agnostic parameters. `animationSnapshot`
24
+ chooses the best source for a time: a fresh METAR/SPECI, else the TAF, else a stale observation.
25
+
26
+ METAR/TAF describe one point (the aerodrome), not a weather field, so the animation is a
27
+ representation around the airport, not the real position of clouds or rain.
package/dist/aftn.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { ReportKind } from "./types.js";
2
+ export interface RawReport {
3
+ kind: ReportKind;
4
+ /** Teks laporan dari kata kunci (METAR/SPECI/TAF) sampai sebelum `=`, spasi dirapikan. */
5
+ text: string;
6
+ }
7
+ /**
8
+ * Ambil laporan cuaca dari teks mentah: boleh berupa laporan polos, buletin dengan header
9
+ * (`SAID31 WARR 021030`), atau pesan AFTN lengkap (`ZCZC ... NNNN` dengan baris alamat). Satu pesan
10
+ * bisa memuat beberapa laporan yang masing-masing diakhiri `=`.
11
+ */
12
+ export declare function extractReports(message: string): RawReport[];
13
+ //# sourceMappingURL=aftn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aftn.d.ts","sourceRoot":"","sources":["../src/aftn.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,UAAU,CAAC;IACjB,0FAA0F;IAC1F,IAAI,EAAE,MAAM,CAAC;CACd;AAID;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAe3D"}
@@ -0,0 +1,86 @@
1
+ import type { Conditions, Metar, Taf } from "./types.js";
2
+ export type PrecipitationKind = "drizzle" | "rain" | "snow" | "hail" | "ice";
3
+ export type ObscurationKind = "fog" | "mist" | "haze" | "smoke" | "dust" | "sand" | "ash";
4
+ export interface CloudAnimation {
5
+ /** Seberapa tertutup lapisan ini, 0-1. */
6
+ coverage: number;
7
+ baseFt: number | null;
8
+ /** low < 2.500 ft, mid < 8.000 ft, selain itu high. */
9
+ band: "low" | "mid" | "high";
10
+ /** Awan konvektif (CB/TCU): digambar lebih gelap dan menjulang. */
11
+ convective: boolean;
12
+ }
13
+ /** Parameter animasi yang siap dipakai renderer apa pun (canvas, WebGL, layer peta). */
14
+ export interface AnimationState {
15
+ wind: {
16
+ /** Arah ASAL angin (derajat); null bila berubah-ubah. */
17
+ fromDeg: number | null;
18
+ /** Arah ke mana partikel bergerak (derajat); null bila berubah-ubah. */
19
+ toDeg: number | null;
20
+ speedKt: number;
21
+ /** 0-1: seberapa kuat hembusan (gust) menambah kecepatan; untuk denyut animasi. */
22
+ gustiness: number;
23
+ /** Angin berubah-ubah arah (VRB atau rentang arah): partikel berputar acak. */
24
+ turbulent: boolean;
25
+ };
26
+ clouds: {
27
+ layers: CloudAnimation[];
28
+ /** Tutupan total gabungan semua lapisan, 0-1. */
29
+ total: number;
30
+ };
31
+ visibility: {
32
+ meters: number | null;
33
+ /** Opasitas kabut tipis akibat jarak pandang rendah, 0-1. */
34
+ haze: number;
35
+ };
36
+ obscuration: {
37
+ kind: ObscurationKind;
38
+ density: number;
39
+ } | null;
40
+ precipitation: {
41
+ kind: PrecipitationKind;
42
+ /** 0-1 (ringan 0,3 · sedang 0,6 · lebat 1). */
43
+ intensity: number;
44
+ /** Hujan lokal (SH atau TS): turun dan berhenti, bukan merata. */
45
+ showers: boolean;
46
+ freezing: boolean;
47
+ } | null;
48
+ thunderstorm: {
49
+ /** Badai di bandara itu sendiri. */
50
+ overhead: boolean;
51
+ /** Badai di sekitar bandara (VCTS). */
52
+ vicinity: boolean;
53
+ lightningPerMinute: number;
54
+ } | null;
55
+ }
56
+ /** Ubah kondisi cuaca (dari METAR, SPECI, atau TAF) menjadi parameter animasi. */
57
+ export declare function animationFromConditions(conditions: Conditions): AnimationState;
58
+ export type AnimationSource = "METAR" | "SPECI" | "TAF";
59
+ export interface AnimationSnapshot {
60
+ state: AnimationState;
61
+ source: AnimationSource;
62
+ station: string;
63
+ /** Waktu yang diwakili data ini. */
64
+ validAt: Date;
65
+ /** Pengamatan lebih tua dari batas, dan tidak ada TAF yang berlaku sebagai gantinya. */
66
+ stale: boolean;
67
+ /** Dari TAF: kondisi sementara (TEMPO) sedang berlaku. */
68
+ temporary: boolean;
69
+ /** Dari TAF: 0-1 selama transisi BECMG berjalan. */
70
+ transition: number | null;
71
+ }
72
+ export interface SnapshotInput {
73
+ observation?: Metar | null;
74
+ forecast?: Taf | null;
75
+ /** Waktu yang ingin ditampilkan (bawaan: sekarang). Geser untuk memutar TAF ke depan. */
76
+ at?: Date;
77
+ /** Pengamatan lebih tua dari ini dianggap kedaluwarsa dan TAF dipakai bila ada. Bawaan 3 jam. */
78
+ maxObservationAgeMs?: number;
79
+ includeProbable?: boolean;
80
+ }
81
+ /**
82
+ * Pilih sumber terbaik untuk waktu `at`: pengamatan (METAR/SPECI) yang masih segar, lalu TAF yang
83
+ * berlaku, lalu pengamatan lama (ditandai stale). Null bila tidak ada data sama sekali.
84
+ */
85
+ export declare function animationSnapshot(input: SnapshotInput): AnimationSnapshot | null;
86
+ //# sourceMappingURL=animation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animation.d.ts","sourceRoot":"","sources":["../src/animation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,UAAU,EAAE,KAAK,EAAkB,GAAG,EAAE,MAAM,YAAY,CAAC;AAErF,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAC7E,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAE1F,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uDAAuD;IACvD,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;IAC7B,mEAAmE;IACnE,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE;QACJ,yDAAyD;QACzD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,wEAAwE;QACxE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,mFAAmF;QACnF,SAAS,EAAE,MAAM,CAAC;QAClB,+EAA+E;QAC/E,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,MAAM,EAAE;QACN,MAAM,EAAE,cAAc,EAAE,CAAC;QACzB,iDAAiD;QACjD,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,UAAU,EAAE;QACV,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,6DAA6D;QAC7D,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,WAAW,EAAE;QAAE,IAAI,EAAE,eAAe,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/D,aAAa,EAAE;QACb,IAAI,EAAE,iBAAiB,CAAC;QACxB,+CAA+C;QAC/C,SAAS,EAAE,MAAM,CAAC;QAClB,kEAAkE;QAClE,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,OAAO,CAAC;KACnB,GAAG,IAAI,CAAC;IACT,YAAY,EAAE;QACZ,oCAAoC;QACpC,QAAQ,EAAE,OAAO,CAAC;QAClB,uCAAuC;QACvC,QAAQ,EAAE,OAAO,CAAC;QAClB,kBAAkB,EAAE,MAAM,CAAC;KAC5B,GAAG,IAAI,CAAC;CACV;AAuED,kFAAkF;AAClF,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,UAAU,GAAG,cAAc,CA+D9E;AAED,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;AAExD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,OAAO,EAAE,IAAI,CAAC;IACd,wFAAwF;IACxF,KAAK,EAAE,OAAO,CAAC;IACf,0DAA0D;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IACtB,yFAAyF;IACzF,EAAE,CAAC,EAAE,IAAI,CAAC;IACV,iGAAiG;IACjG,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,iBAAiB,GAAG,IAAI,CA6ChF"}
@@ -0,0 +1,19 @@
1
+ import type { CloudLayer, Conditions, PresentWeather, Wind } from "./types.js";
2
+ export declare function emptyConditions(): Conditions;
3
+ export interface ConditionTokens {
4
+ wind?: Wind;
5
+ visibilityM?: number;
6
+ cavok?: boolean;
7
+ clouds?: CloudLayer[];
8
+ weather?: PresentWeather[];
9
+ noSignificantWeather?: boolean;
10
+ /** Token yang tidak dikenali (diteruskan ke pemanggil, mis. suhu atau QNH). */
11
+ rest: string[];
12
+ }
13
+ /** Baca token kondisi (angin, jarak pandang, awan, cuaca) dan kembalikan sisanya. */
14
+ export declare function parseConditionTokens(tokens: string[]): ConditionTokens;
15
+ /** Gabungkan grup yang lebih baru di atas kondisi dasar; yang tidak disebut diwarisi. */
16
+ export declare function mergeConditions(base: Conditions, next: Partial<Conditions> & {
17
+ noSignificantWeather?: boolean;
18
+ }): Conditions;
19
+ //# sourceMappingURL=conditions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditions.d.ts","sourceRoot":"","sources":["../src/conditions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,UAAU,EACV,UAAU,EACV,cAAc,EACd,IAAI,EAEL,MAAM,YAAY,CAAC;AAYpB,wBAAgB,eAAe,IAAI,UAAU,CAa5C;AAgCD,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IACtB,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,+EAA+E;IAC/E,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,eAAe,CAkFtE;AAED,yFAAyF;AACzF,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG;IAAE,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7D,UAAU,CAqBZ"}
@@ -0,0 +1,14 @@
1
+ import type { Report } from "./types.js";
2
+ export * from "./aftn.js";
3
+ export * from "./animation.js";
4
+ export * from "./conditions.js";
5
+ export * from "./metar.js";
6
+ export * from "./taf.js";
7
+ export * from "./time.js";
8
+ export * from "./types.js";
9
+ /**
10
+ * Baca semua laporan cuaca (METAR, SPECI, TAF) dari satu pesan mentah, termasuk bungkus AFTN.
11
+ * Laporan yang tidak terbaca dilewati. `reference` = waktu pesan diterima (menentukan bulan/tahun).
12
+ */
13
+ export declare function decodeWeatherMessage(message: string, reference?: Date): Report[];
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAE3B;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,IAAiB,GAC3B,MAAM,EAAE,CAUV"}
package/dist/index.js ADDED
@@ -0,0 +1,584 @@
1
+ // src/aftn.ts
2
+ var KEYWORD = /\b(METAR|SPECI|TAF)\b/;
3
+ function extractReports(message) {
4
+ const text = message.replace(/\r/g, "\n").replace(/\bZCZC\b[^\n]*/g, " ").replace(/\bNNNN\b/g, " ");
5
+ const reports = [];
6
+ for (const segment of text.split("=")) {
7
+ const flat = segment.replace(/\s+/g, " ").trim();
8
+ const match = KEYWORD.exec(flat);
9
+ if (!match) continue;
10
+ const body = flat.slice(match.index);
11
+ reports.push({ kind: match[1], text: body });
12
+ }
13
+ return reports;
14
+ }
15
+
16
+ // src/conditions.ts
17
+ var KT_PER_MPS = 1.943844;
18
+ var WIND = /^(\d{3}|VRB)(\d{2,3})(?:G(\d{2,3}))?(KT|MPS)$/;
19
+ var WIND_VARIABLE = /^(\d{3})V(\d{3})$/;
20
+ var VISIBILITY = /^\d{4}$/;
21
+ var CLOUD = /^(FEW|SCT|BKN|OVC)(\d{3}|\/\/\/)(CB|TCU|\/\/\/)?$/;
22
+ var VERTICAL_VISIBILITY = /^VV(\d{3}|\/\/\/)$/;
23
+ var WEATHER = /^([-+]|VC)?(MI|BC|PR|DR|BL|SH|TS|FZ)?((?:DZ|RA|SN|SG|IC|PL|GR|GS|UP|BR|FG|FU|VA|DU|SA|HZ|PO|SQ|FC|SS|DS)*)$/;
24
+ function emptyConditions() {
25
+ return {
26
+ wind: {
27
+ directionDeg: null,
28
+ variable: false,
29
+ speedKt: 0,
30
+ gustKt: null
31
+ },
32
+ visibilityM: null,
33
+ cavok: false,
34
+ clouds: [],
35
+ weather: []
36
+ };
37
+ }
38
+ function toKnots(value, unit) {
39
+ return unit === "MPS" ? Math.round(value * KT_PER_MPS) : value;
40
+ }
41
+ function parseWeather(token) {
42
+ const match = token.match(WEATHER);
43
+ if (!match) return null;
44
+ const [, prefix, descriptor, phenomena] = match;
45
+ if (!descriptor && !phenomena) return null;
46
+ const intensity = prefix === "-" ? "light" : prefix === "+" ? "heavy" : prefix === "VC" ? "vicinity" : "moderate";
47
+ const codes = [];
48
+ for (let i = 0; i < (phenomena ?? "").length; i += 2) {
49
+ codes.push((phenomena ?? "").slice(i, i + 2));
50
+ }
51
+ return {
52
+ intensity,
53
+ descriptor: descriptor || void 0,
54
+ phenomena: codes,
55
+ code: token
56
+ };
57
+ }
58
+ function parseConditionTokens(tokens) {
59
+ const out = { rest: [] };
60
+ const clouds = [];
61
+ const weather = [];
62
+ let sawCloudGroup = false;
63
+ for (const token of tokens) {
64
+ const wind = token.match(WIND);
65
+ if (wind) {
66
+ const variable = wind[1] === "VRB";
67
+ out.wind = {
68
+ directionDeg: variable ? null : Number(wind[1]),
69
+ variable,
70
+ speedKt: toKnots(Number(wind[2]), wind[4] ?? "KT"),
71
+ gustKt: wind[3] ? toKnots(Number(wind[3]), wind[4] ?? "KT") : null
72
+ };
73
+ continue;
74
+ }
75
+ const range = token.match(WIND_VARIABLE);
76
+ if (range && out.wind) {
77
+ out.wind.variableFromDeg = Number(range[1]);
78
+ out.wind.variableToDeg = Number(range[2]);
79
+ continue;
80
+ }
81
+ if (token === "CAVOK") {
82
+ out.cavok = true;
83
+ out.visibilityM = 1e4;
84
+ continue;
85
+ }
86
+ if (VISIBILITY.test(token)) {
87
+ out.visibilityM = token === "9999" ? 1e4 : Number(token);
88
+ continue;
89
+ }
90
+ const cloud = token.match(CLOUD);
91
+ if (cloud) {
92
+ sawCloudGroup = true;
93
+ clouds.push({
94
+ cover: cloud[1],
95
+ baseFt: cloud[2] === "///" ? null : Number(cloud[2]) * 100,
96
+ type: cloud[3] === "CB" || cloud[3] === "TCU" ? cloud[3] : void 0
97
+ });
98
+ continue;
99
+ }
100
+ const vv = token.match(VERTICAL_VISIBILITY);
101
+ if (vv) {
102
+ sawCloudGroup = true;
103
+ clouds.push({
104
+ cover: "VV",
105
+ baseFt: vv[1] === "///" ? null : Number(vv[1]) * 100
106
+ });
107
+ continue;
108
+ }
109
+ if (token === "SKC" || token === "NSC" || token === "NCD") {
110
+ sawCloudGroup = true;
111
+ out.clouds = [];
112
+ continue;
113
+ }
114
+ if (token === "NSW") {
115
+ out.noSignificantWeather = true;
116
+ continue;
117
+ }
118
+ const present = parseWeather(token);
119
+ if (present) {
120
+ weather.push(present);
121
+ continue;
122
+ }
123
+ out.rest.push(token);
124
+ }
125
+ if (clouds.length > 0) out.clouds = clouds;
126
+ else if (sawCloudGroup) out.clouds = [];
127
+ if (weather.length > 0) out.weather = weather;
128
+ return out;
129
+ }
130
+ function mergeConditions(base, next) {
131
+ const merged = {
132
+ wind: next.wind ?? base.wind,
133
+ visibilityM: next.visibilityM ?? base.visibilityM,
134
+ cavok: next.cavok ?? base.cavok,
135
+ clouds: next.clouds ?? base.clouds,
136
+ weather: next.weather ?? base.weather
137
+ };
138
+ if (next.noSignificantWeather) merged.weather = [];
139
+ if (next.cavok) {
140
+ merged.clouds = [];
141
+ merged.weather = [];
142
+ merged.visibilityM = 1e4;
143
+ } else if (next.visibilityM !== void 0 || next.clouds !== void 0 || next.weather !== void 0) {
144
+ merged.cavok = false;
145
+ }
146
+ return merged;
147
+ }
148
+
149
+ // src/time.ts
150
+ function resolveDayTime(reference, day, hour, minute = 0) {
151
+ let best = null;
152
+ for (const offset of [-1, 0, 1]) {
153
+ const year = reference.getUTCFullYear();
154
+ const month = reference.getUTCMonth() + offset;
155
+ if (new Date(Date.UTC(year, month, day)).getUTCDate() !== day) continue;
156
+ const candidate = new Date(Date.UTC(year, month, day, hour, minute));
157
+ if (best === null || Math.abs(candidate.getTime() - reference.getTime()) < Math.abs(best.getTime() - reference.getTime())) {
158
+ best = candidate;
159
+ }
160
+ }
161
+ return best ?? new Date(Number.NaN);
162
+ }
163
+
164
+ // src/metar.ts
165
+ var TIME = /^(\d{2})(\d{2})(\d{2})Z$/;
166
+ var TEMPERATURE = /^(M?\d{2})\/(M?\d{2}|\/\/)?$/;
167
+ var QNH = /^Q(\d{4})$/;
168
+ var ALTIMETER = /^A(\d{4})$/;
169
+ var STOP = /* @__PURE__ */ new Set(["RMK"]);
170
+ var signed = (value) => value.startsWith("M") ? -Number(value.slice(1)) : Number(value);
171
+ function parseMetar(raw, reference = /* @__PURE__ */ new Date()) {
172
+ const tokens = raw.replace(/=$/, "").trim().split(/\s+/).filter(Boolean);
173
+ const kind = tokens[0];
174
+ if (kind !== "METAR" && kind !== "SPECI") return null;
175
+ let index = 1;
176
+ let corrected = false;
177
+ if (tokens[index] === "COR") {
178
+ corrected = true;
179
+ index += 1;
180
+ }
181
+ const station = tokens[index++];
182
+ const timeToken = tokens[index++]?.match(TIME);
183
+ if (!station || !/^[A-Z]{4}$/.test(station) || !timeToken) return null;
184
+ const observedAt = resolveDayTime(
185
+ reference,
186
+ Number(timeToken[1]),
187
+ Number(timeToken[2]),
188
+ Number(timeToken[3])
189
+ );
190
+ if (Number.isNaN(observedAt.getTime())) return null;
191
+ let auto = false;
192
+ let temperatureC = null;
193
+ let dewPointC = null;
194
+ let qnhHpa = null;
195
+ let trend = null;
196
+ const body = [];
197
+ for (const token of tokens.slice(index)) {
198
+ if (STOP.has(token)) break;
199
+ if (token === "NOSIG" || token === "BECMG" || token === "TEMPO") {
200
+ trend = token;
201
+ break;
202
+ }
203
+ if (token === "AUTO") {
204
+ auto = true;
205
+ continue;
206
+ }
207
+ const temp = token.match(TEMPERATURE);
208
+ if (temp?.[1]) {
209
+ temperatureC = signed(temp[1]);
210
+ dewPointC = temp[2] && temp[2] !== "//" ? signed(temp[2]) : null;
211
+ continue;
212
+ }
213
+ const qnh = token.match(QNH);
214
+ if (qnh?.[1]) {
215
+ qnhHpa = Number(qnh[1]);
216
+ continue;
217
+ }
218
+ const altimeter = token.match(ALTIMETER);
219
+ if (altimeter?.[1]) {
220
+ qnhHpa = Math.round(Number(altimeter[1]) / 100 * 33.8639);
221
+ continue;
222
+ }
223
+ body.push(token);
224
+ }
225
+ const parsed = parseConditionTokens(body);
226
+ return {
227
+ kind,
228
+ station,
229
+ observedAt,
230
+ auto,
231
+ corrected,
232
+ conditions: mergeConditions(emptyConditions(), parsed),
233
+ temperatureC,
234
+ dewPointC,
235
+ qnhHpa,
236
+ trend,
237
+ raw: tokens.join(" ")
238
+ };
239
+ }
240
+
241
+ // src/taf.ts
242
+ var ISSUED = /^(\d{2})(\d{2})(\d{2})Z$/;
243
+ var VALIDITY = /^(\d{2})(\d{2})\/(\d{2})(\d{2})$/;
244
+ var FROM = /^FM(\d{2})(\d{2})(\d{2})$/;
245
+ var PROB = /^PROB(30|40)$/;
246
+ var TEMPERATURE_GROUP = /^T[XN]M?\d{2}\/\d{4}Z$/;
247
+ function parseTaf(raw, reference = /* @__PURE__ */ new Date()) {
248
+ const tokens = raw.replace(/=$/, "").trim().split(/\s+/).filter(Boolean);
249
+ if (tokens[0] !== "TAF") return null;
250
+ let index = 1;
251
+ let amended = false;
252
+ let corrected = false;
253
+ while (tokens[index] === "AMD" || tokens[index] === "COR") {
254
+ if (tokens[index] === "AMD") amended = true;
255
+ else corrected = true;
256
+ index += 1;
257
+ }
258
+ const station = tokens[index++];
259
+ const issued = tokens[index++]?.match(ISSUED);
260
+ if (!station || !/^[A-Z]{4}$/.test(station) || !issued) return null;
261
+ const issuedAt = resolveDayTime(
262
+ reference,
263
+ Number(issued[1]),
264
+ Number(issued[2]),
265
+ Number(issued[3])
266
+ );
267
+ const validity = tokens[index]?.match(VALIDITY);
268
+ if (!validity || Number.isNaN(issuedAt.getTime())) return null;
269
+ index += 1;
270
+ const validFrom = resolveDayTime(issuedAt, Number(validity[1]), Number(validity[2]));
271
+ const validTo = resolveDayTime(issuedAt, Number(validity[3]), Number(validity[4]));
272
+ const base = {
273
+ station,
274
+ issuedAt,
275
+ validFrom,
276
+ validTo,
277
+ amended,
278
+ corrected,
279
+ raw: tokens.join(" ")
280
+ };
281
+ if (tokens[index] === "CNL") {
282
+ return { kind: "TAF", ...base, cancelled: true, periods: [] };
283
+ }
284
+ const groups = [
285
+ { change: "BASE", from: validFrom, to: validTo, tokens: [] }
286
+ ];
287
+ let current = groups[0];
288
+ let probability;
289
+ const startGroup = (change, from, to) => {
290
+ current = { change, probability, from, to, tokens: [] };
291
+ probability = void 0;
292
+ groups.push(current);
293
+ };
294
+ for (let i = index; i < tokens.length; i++) {
295
+ const token = tokens[i];
296
+ if (TEMPERATURE_GROUP.test(token)) continue;
297
+ const prob = token.match(PROB);
298
+ if (prob) {
299
+ probability = Number(prob[1]);
300
+ if (tokens[i + 1] !== "TEMPO") {
301
+ const period = tokens[i + 1]?.match(VALIDITY);
302
+ if (period) {
303
+ startGroup(
304
+ "PROB",
305
+ resolveDayTime(issuedAt, Number(period[1]), Number(period[2])),
306
+ resolveDayTime(issuedAt, Number(period[3]), Number(period[4]))
307
+ );
308
+ i += 1;
309
+ }
310
+ }
311
+ continue;
312
+ }
313
+ const from = token.match(FROM);
314
+ if (from) {
315
+ startGroup(
316
+ "FM",
317
+ resolveDayTime(issuedAt, Number(from[1]), Number(from[2]), Number(from[3])),
318
+ validTo
319
+ );
320
+ continue;
321
+ }
322
+ if (token === "BECMG" || token === "TEMPO") {
323
+ const period = tokens[i + 1]?.match(VALIDITY);
324
+ if (!period) continue;
325
+ startGroup(
326
+ token,
327
+ resolveDayTime(issuedAt, Number(period[1]), Number(period[2])),
328
+ resolveDayTime(issuedAt, Number(period[3]), Number(period[4]))
329
+ );
330
+ i += 1;
331
+ continue;
332
+ }
333
+ current.tokens.push(token);
334
+ }
335
+ const periods = groups.map((group) => {
336
+ const { rest: _rest, ...conditions } = parseConditionTokens(group.tokens);
337
+ return {
338
+ change: group.change,
339
+ probability: group.probability,
340
+ from: group.from,
341
+ to: group.to,
342
+ conditions,
343
+ raw: group.tokens.join(" ")
344
+ };
345
+ });
346
+ return { kind: "TAF", ...base, cancelled: false, periods };
347
+ }
348
+ var lerp = (a, b, t) => a + (b - a) * t;
349
+ function lerpAngle(a, b, t) {
350
+ const delta = (b - a + 540) % 360 - 180;
351
+ return (a + delta * t + 360) % 360;
352
+ }
353
+ function blendConditions(a, b, t) {
354
+ const direction = a.wind.directionDeg !== null && b.wind.directionDeg !== null ? lerpAngle(a.wind.directionDeg, b.wind.directionDeg, t) : t < 0.5 ? a.wind.directionDeg : b.wind.directionDeg;
355
+ const gust = a.wind.gustKt !== null && b.wind.gustKt !== null ? lerp(a.wind.gustKt, b.wind.gustKt, t) : t < 0.5 ? a.wind.gustKt : b.wind.gustKt;
356
+ const visibility = a.visibilityM !== null && b.visibilityM !== null ? lerp(a.visibilityM, b.visibilityM, t) : t < 0.5 ? a.visibilityM : b.visibilityM;
357
+ const later = t < 0.5 ? a : b;
358
+ return {
359
+ wind: {
360
+ directionDeg: direction,
361
+ variable: direction === null,
362
+ speedKt: lerp(a.wind.speedKt, b.wind.speedKt, t),
363
+ gustKt: gust
364
+ },
365
+ visibilityM: visibility,
366
+ cavok: later.cavok,
367
+ clouds: later.clouds,
368
+ weather: later.weather
369
+ };
370
+ }
371
+ function forecastAt(taf, at, options = {}) {
372
+ if (taf.cancelled) return null;
373
+ const time = at.getTime();
374
+ if (time < taf.validFrom.getTime() || time >= taf.validTo.getTime()) return null;
375
+ const ordered = [...taf.periods].sort((x, y) => x.from.getTime() - y.from.getTime());
376
+ let state = emptyConditions();
377
+ let transition = null;
378
+ let temporary = false;
379
+ for (const period of ordered) {
380
+ if (period.change === "BASE") {
381
+ state = mergeConditions(emptyConditions(), period.conditions);
382
+ } else if (period.change === "FM" && period.from.getTime() <= time) {
383
+ state = mergeConditions(emptyConditions(), period.conditions);
384
+ transition = null;
385
+ } else if (period.change === "BECMG" && period.from.getTime() <= time) {
386
+ const target = mergeConditions(state, period.conditions);
387
+ if (time >= period.to.getTime()) {
388
+ state = target;
389
+ transition = null;
390
+ } else {
391
+ const progress = (time - period.from.getTime()) / (period.to.getTime() - period.from.getTime());
392
+ state = blendConditions(state, target, progress);
393
+ transition = progress;
394
+ }
395
+ }
396
+ }
397
+ for (const period of ordered) {
398
+ const overlay = period.change === "TEMPO" || period.change === "PROB" && options.includeProbable === true;
399
+ if (!overlay) continue;
400
+ if (period.probability !== void 0 && !options.includeProbable) continue;
401
+ if (time >= period.from.getTime() && time < period.to.getTime()) {
402
+ state = mergeConditions(state, period.conditions);
403
+ temporary = true;
404
+ }
405
+ }
406
+ return { conditions: state, temporary, transition };
407
+ }
408
+
409
+ // src/animation.ts
410
+ var COVERAGE = {
411
+ FEW: 0.19,
412
+ SCT: 0.44,
413
+ BKN: 0.81,
414
+ OVC: 1,
415
+ VV: 1
416
+ };
417
+ var INTENSITY = {
418
+ light: 0.3,
419
+ moderate: 0.6,
420
+ heavy: 1,
421
+ vicinity: 0.3
422
+ };
423
+ var PRECIPITATION = {
424
+ DZ: "drizzle",
425
+ RA: "rain",
426
+ SN: "snow",
427
+ SG: "snow",
428
+ GR: "hail",
429
+ GS: "hail",
430
+ IC: "ice",
431
+ PL: "ice"
432
+ };
433
+ var OBSCURATION = {
434
+ FG: "fog",
435
+ BR: "mist",
436
+ HZ: "haze",
437
+ FU: "smoke",
438
+ DU: "dust",
439
+ SA: "sand",
440
+ VA: "ash"
441
+ };
442
+ var OBSCURATION_DENSITY = {
443
+ fog: 1,
444
+ mist: 0.4,
445
+ haze: 0.35,
446
+ smoke: 0.5,
447
+ dust: 0.5,
448
+ sand: 0.6,
449
+ ash: 0.7
450
+ };
451
+ var clamp = (value, min = 0, max = 1) => Math.min(max, Math.max(min, value));
452
+ var band = (baseFt) => baseFt === null || baseFt < 2500 ? "low" : baseFt < 8e3 ? "mid" : "high";
453
+ function cloudAnimation(layer) {
454
+ const coverage = COVERAGE[layer.cover];
455
+ if (coverage === void 0) return null;
456
+ return {
457
+ coverage,
458
+ baseFt: layer.baseFt,
459
+ band: band(layer.baseFt),
460
+ convective: layer.type === "CB" || layer.type === "TCU"
461
+ };
462
+ }
463
+ function hazeFor(visibilityM) {
464
+ if (visibilityM === null) return 0;
465
+ return clamp((8e3 - visibilityM) / (8e3 - 500)) * 0.9;
466
+ }
467
+ function animationFromConditions(conditions) {
468
+ const { wind } = conditions;
469
+ const turbulent = wind.variable || wind.variableFromDeg !== void 0;
470
+ const gustiness = wind.gustKt === null ? 0 : clamp((wind.gustKt - wind.speedKt) / 20);
471
+ const layers = conditions.clouds.map(cloudAnimation).filter((layer) => layer !== null);
472
+ const total = clamp(1 - layers.reduce((open, l) => open * (1 - l.coverage), 1));
473
+ let precipitation = null;
474
+ let thunderstorm = null;
475
+ let obscuration = null;
476
+ for (const weather of conditions.weather) {
477
+ const intensity = INTENSITY[weather.intensity];
478
+ const vicinity = weather.intensity === "vicinity";
479
+ if (weather.descriptor === "TS") {
480
+ const rate = vicinity ? 2 : Math.round(3 + 9 * intensity);
481
+ if (!thunderstorm || rate > thunderstorm.lightningPerMinute) {
482
+ thunderstorm = { overhead: !vicinity, vicinity, lightningPerMinute: rate };
483
+ }
484
+ }
485
+ if (vicinity) continue;
486
+ for (const code of weather.phenomena) {
487
+ const kind = PRECIPITATION[code];
488
+ if (kind && (!precipitation || intensity > precipitation.intensity)) {
489
+ precipitation = {
490
+ kind,
491
+ intensity,
492
+ showers: weather.descriptor === "SH" || weather.descriptor === "TS",
493
+ freezing: weather.descriptor === "FZ"
494
+ };
495
+ }
496
+ const obscure = OBSCURATION[code];
497
+ if (obscure) {
498
+ const density = OBSCURATION_DENSITY[obscure];
499
+ if (!obscuration || density > obscuration.density) {
500
+ obscuration = { kind: obscure, density };
501
+ }
502
+ }
503
+ }
504
+ }
505
+ return {
506
+ wind: {
507
+ fromDeg: wind.directionDeg,
508
+ toDeg: wind.directionDeg === null ? null : (wind.directionDeg + 180) % 360,
509
+ speedKt: wind.speedKt,
510
+ gustiness,
511
+ turbulent
512
+ },
513
+ clouds: { layers, total },
514
+ visibility: { meters: conditions.visibilityM, haze: hazeFor(conditions.visibilityM) },
515
+ obscuration,
516
+ precipitation,
517
+ thunderstorm
518
+ };
519
+ }
520
+ function animationSnapshot(input) {
521
+ const at = input.at ?? /* @__PURE__ */ new Date();
522
+ const maxAge = input.maxObservationAgeMs ?? 3 * 36e5;
523
+ const { observation, forecast } = input;
524
+ const age = observation ? at.getTime() - observation.observedAt.getTime() : null;
525
+ if (observation && age !== null && age >= 0 && age <= maxAge) {
526
+ return {
527
+ state: animationFromConditions(observation.conditions),
528
+ source: observation.kind,
529
+ station: observation.station,
530
+ validAt: observation.observedAt,
531
+ stale: false,
532
+ temporary: false,
533
+ transition: null
534
+ };
535
+ }
536
+ const predicted = forecast ? forecastAt(forecast, at, { includeProbable: input.includeProbable }) : null;
537
+ if (forecast && predicted) {
538
+ return {
539
+ state: animationFromConditions(predicted.conditions),
540
+ source: "TAF",
541
+ station: forecast.station,
542
+ validAt: at,
543
+ stale: false,
544
+ temporary: predicted.temporary,
545
+ transition: predicted.transition
546
+ };
547
+ }
548
+ if (observation) {
549
+ return {
550
+ state: animationFromConditions(observation.conditions),
551
+ source: observation.kind,
552
+ station: observation.station,
553
+ validAt: observation.observedAt,
554
+ stale: true,
555
+ temporary: false,
556
+ transition: null
557
+ };
558
+ }
559
+ return null;
560
+ }
561
+
562
+ // src/index.ts
563
+ function decodeWeatherMessage(message, reference = /* @__PURE__ */ new Date()) {
564
+ const reports = [];
565
+ for (const raw of extractReports(message)) {
566
+ const report = raw.kind === "TAF" ? parseTaf(raw.text, reference) : parseMetar(raw.text, reference);
567
+ if (report) reports.push(report);
568
+ }
569
+ return reports;
570
+ }
571
+ export {
572
+ animationFromConditions,
573
+ animationSnapshot,
574
+ blendConditions,
575
+ decodeWeatherMessage,
576
+ emptyConditions,
577
+ extractReports,
578
+ forecastAt,
579
+ mergeConditions,
580
+ parseConditionTokens,
581
+ parseMetar,
582
+ parseTaf,
583
+ resolveDayTime
584
+ };
@@ -0,0 +1,8 @@
1
+ import type { Metar } from "./types.js";
2
+ /**
3
+ * Baca satu laporan METAR/SPECI. `reference` menentukan bulan dan tahun (laporan hanya memuat
4
+ * tanggal); isi dengan waktu pesan diterima. Mengembalikan null bila bukan METAR/SPECI atau waktunya
5
+ * tidak terbaca.
6
+ */
7
+ export declare function parseMetar(raw: string, reference?: Date): Metar | null;
8
+ //# sourceMappingURL=metar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metar.d.ts","sourceRoot":"","sources":["../src/metar.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAWxC;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,GAAE,IAAiB,GAAG,KAAK,GAAG,IAAI,CA0ElF"}
package/dist/taf.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { Conditions, Taf } from "./types.js";
2
+ /**
3
+ * Baca satu TAF (termasuk AMD/COR/CNL). `reference` menentukan bulan dan tahun; isi dengan waktu
4
+ * pesan diterima. Mengembalikan null bila bukan TAF yang bisa dibaca.
5
+ */
6
+ export declare function parseTaf(raw: string, reference?: Date): Taf | null;
7
+ export interface ForecastAt {
8
+ conditions: Conditions;
9
+ /** True bila kondisi ini hanya sementara (TEMPO atau PROB yang sedang berlaku). */
10
+ temporary: boolean;
11
+ /** 0-1 selama transisi BECMG yang sedang berjalan; null di luar transisi. */
12
+ transition: number | null;
13
+ }
14
+ export interface ForecastOptions {
15
+ /** Ikutkan grup PROB30/PROB40 (bawaan: tidak, karena belum tentu terjadi). */
16
+ includeProbable?: boolean;
17
+ }
18
+ /** Campur dua kondisi: angka dan arah berpindah halus; awan dan cuaca berganti di tengah transisi. */
19
+ export declare function blendConditions(a: Conditions, b: Conditions, t: number): Conditions;
20
+ /** Kondisi yang berlaku pada waktu `at` menurut TAF. Null bila di luar masa berlaku atau dibatalkan. */
21
+ export declare function forecastAt(taf: Taf, at: Date, options?: ForecastOptions): ForecastAt | null;
22
+ //# sourceMappingURL=taf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"taf.d.ts","sourceRoot":"","sources":["../src/taf.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,GAAG,EAAwB,MAAM,YAAY,CAAC;AAgBxE;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,GAAE,IAAiB,GAAG,GAAG,GAAG,IAAI,CAiH9E;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,mFAAmF;IACnF,SAAS,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AASD,sGAAsG;AACtG,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,CAgCnF;AAED,wGAAwG;AACxG,wBAAgB,UAAU,CACxB,GAAG,EAAE,GAAG,EACR,EAAE,EAAE,IAAI,EACR,OAAO,GAAE,eAAoB,GAC5B,UAAU,GAAG,IAAI,CA2CnB"}
package/dist/time.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Tanggal lengkap dari hari-jam-menit (DDHHMM) tanpa bulan/tahun: ambil bulan (sebelumnya, ini,
3
+ * berikutnya) yang hasilnya paling dekat ke `reference`. Jam 24 berarti tengah malam hari berikutnya.
4
+ */
5
+ export declare function resolveDayTime(reference: Date, day: number, hour: number, minute?: number): Date;
6
+ //# sourceMappingURL=time.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../src/time.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,IAAI,EACf,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,MAAM,SAAI,GACT,IAAI,CAiBN"}
@@ -0,0 +1,77 @@
1
+ export type ReportKind = "METAR" | "SPECI" | "TAF";
2
+ export type CloudCover = "FEW" | "SCT" | "BKN" | "OVC" | "VV" | "SKC" | "NSC" | "NCD";
3
+ export interface CloudLayer {
4
+ cover: CloudCover;
5
+ /** Tinggi dasar awan (ft AGL); null bila tidak diketahui (`///`) atau tanpa awan. */
6
+ baseFt: number | null;
7
+ type?: "CB" | "TCU";
8
+ }
9
+ export interface Wind {
10
+ /** Arah ASAL angin (derajat, 0-360); null bila berubah-ubah (VRB). */
11
+ directionDeg: number | null;
12
+ variable: boolean;
13
+ /** Rentang arah berubah-ubah (mis. 180V240), bila dilaporkan. */
14
+ variableFromDeg?: number;
15
+ variableToDeg?: number;
16
+ speedKt: number;
17
+ gustKt: number | null;
18
+ }
19
+ export type WeatherIntensity = "light" | "moderate" | "heavy" | "vicinity";
20
+ export interface PresentWeather {
21
+ intensity: WeatherIntensity;
22
+ /** Deskriptor (SH, TS, FZ, ...), bila ada. */
23
+ descriptor?: string;
24
+ /** Fenomena (RA, SN, FG, ...), kosong untuk deskriptor tunggal seperti TS. */
25
+ phenomena: string[];
26
+ /** Kode asli, mis. `+TSRA`. */
27
+ code: string;
28
+ }
29
+ /** Kondisi cuaca pada satu titik waktu. */
30
+ export interface Conditions {
31
+ wind: Wind;
32
+ /** Jarak pandang (meter); 10000 untuk 9999 dan CAVOK. */
33
+ visibilityM: number | null;
34
+ cavok: boolean;
35
+ clouds: CloudLayer[];
36
+ weather: PresentWeather[];
37
+ }
38
+ export interface Metar {
39
+ kind: "METAR" | "SPECI";
40
+ station: string;
41
+ observedAt: Date;
42
+ auto: boolean;
43
+ corrected: boolean;
44
+ conditions: Conditions;
45
+ temperatureC: number | null;
46
+ dewPointC: number | null;
47
+ qnhHpa: number | null;
48
+ trend: "NOSIG" | "BECMG" | "TEMPO" | null;
49
+ raw: string;
50
+ }
51
+ export type TafChange = "BASE" | "FM" | "BECMG" | "TEMPO" | "PROB";
52
+ export interface TafPeriod {
53
+ change: TafChange;
54
+ /** Peluang (30/40) untuk PROB, atau PROB TEMPO. */
55
+ probability?: number;
56
+ from: Date;
57
+ to: Date;
58
+ /** Hanya bagian yang disebut grup ini; sisanya diwarisi dari grup sebelumnya. */
59
+ conditions: Partial<Conditions> & {
60
+ noSignificantWeather?: boolean;
61
+ };
62
+ raw: string;
63
+ }
64
+ export interface Taf {
65
+ kind: "TAF";
66
+ station: string;
67
+ issuedAt: Date;
68
+ validFrom: Date;
69
+ validTo: Date;
70
+ amended: boolean;
71
+ corrected: boolean;
72
+ cancelled: boolean;
73
+ periods: TafPeriod[];
74
+ raw: string;
75
+ }
76
+ export type Report = Metar | Taf;
77
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=weather.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"weather.test.d.ts","sourceRoot":"","sources":["../src/weather.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@aeroswarm/weather",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Decode raw AFTN/METAR/SPECI/TAF messages and turn them into animation parameters.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "main": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup src/index.ts --format esm --clean && tsc --emitDeclarationOnly",
24
+ "dev": "tsup src/index.ts --format esm --dts --watch",
25
+ "test": "vitest run --passWithNoTests",
26
+ "typecheck": "tsc --noEmit",
27
+ "clean": "rm -rf dist",
28
+ "prepublishOnly": "pnpm run typecheck && pnpm run test && pnpm run build",
29
+ "release": "release-it",
30
+ "release:dry": "release-it --dry-run",
31
+ "release:patch": "release-it patch --no-git.requireCleanWorkingDir",
32
+ "release:minor": "release-it minor --no-git.requireCleanWorkingDir",
33
+ "release:major": "release-it major --no-git.requireCleanWorkingDir"
34
+ },
35
+ "devDependencies": {
36
+ "release-it": "^18.1.2",
37
+ "tsup": "^8.5.1",
38
+ "vitest": "^4.1.6"
39
+ }
40
+ }