@aeroswarm/weather 0.0.0-stage → 0.1.2

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,40 @@
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.
28
+
29
+ ## Gridded fields
30
+
31
+ `grid.ts` turns wind and scalar data into grids, whatever the source:
32
+
33
+ - `windGridFromStations(stations, { bounds, nx, ny })` interpolates the wind of several stations (IDW);
34
+ `stationWindsFromMetars(reports, locate)` takes them from decoded METARs.
35
+ - `parseGrib2Json(json)` reads `grib2json` output (convert GRIB binary first with `grib2json` or
36
+ `wgrib2`); `parseGridJson(json)` reads a plain `{ bounds, nx, ny, u, v }` or `{ ..., values }`.
37
+ - `stationWindsFromModelPoints(response)` reads multi-location model responses (Open-Meteo style).
38
+ - `sampleWind`, `sampleScalar` (bilinear) and `blendWindGrids` (between two forecast hours).
39
+
40
+ Row 0 is north, wind is in m/s (`u` east, `v` north), cells without data are NaN.
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"}
package/dist/grid.d.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { Metar } from "./types.js";
2
+ /**
3
+ * Medan cuaca berbentuk grid (angin, awan, hujan, suhu) dari sumber mana pun: interpolasi banyak
4
+ * stasiun METAR, JSON grid dari layanan luar, atau hasil konversi GRIB. Semua murni, tanpa jaringan.
5
+ *
6
+ * Konvensi: baris 0 = utara, kolom 0 = barat. Angin dalam m/s: `u` ke timur, `v` ke utara.
7
+ * Sel tanpa data berisi NaN.
8
+ */
9
+ export interface GridBounds {
10
+ west: number;
11
+ south: number;
12
+ east: number;
13
+ north: number;
14
+ }
15
+ export interface ScalarGrid {
16
+ bounds: GridBounds;
17
+ nx: number;
18
+ ny: number;
19
+ values: Float32Array;
20
+ }
21
+ export interface WindGrid {
22
+ bounds: GridBounds;
23
+ nx: number;
24
+ ny: number;
25
+ u: Float32Array;
26
+ v: Float32Array;
27
+ }
28
+ export interface GeoPoint {
29
+ lat: number;
30
+ lon: number;
31
+ }
32
+ export interface StationWind extends GeoPoint {
33
+ speedKt: number;
34
+ /** Arah ASAL angin (derajat); null untuk VRB (dianggap tenang). */
35
+ directionDeg: number | null;
36
+ }
37
+ export interface StationValue extends GeoPoint {
38
+ value: number;
39
+ }
40
+ export interface InterpolationOptions {
41
+ bounds: GridBounds;
42
+ nx: number;
43
+ ny: number;
44
+ /** Pangkat bobot 1/d^p. Bawaan 2. */
45
+ power?: number;
46
+ /** Sel yang lebih jauh dari stasiun mana pun dari jarak ini (km) dibiarkan tanpa data. Bawaan 400. */
47
+ maxDistanceKm?: number;
48
+ }
49
+ export declare const speedOf: (u: number, v: number) => number;
50
+ /** Arah ASAL angin (derajat, 0-360) dari komponen u/v. */
51
+ export declare function directionFromUV(u: number, v: number): number;
52
+ /** Komponen u/v (m/s) dari kecepatan dan arah ASAL angin. */
53
+ export declare function uvFromSpeedDirection(speedMs: number, fromDeg: number): {
54
+ u: number;
55
+ v: number;
56
+ };
57
+ export declare function pointOf(bounds: GridBounds, nx: number, ny: number, col: number, row: number): GeoPoint;
58
+ /**
59
+ * Interpolasi bobot-jarak-terbalik (IDW) nilai di banyak titik ke grid. Tepat di sebuah stasiun
60
+ * hasilnya persis nilai stasiun itu. Sel yang jauh dari semua stasiun diberi NaN.
61
+ */
62
+ export declare function interpolateScalar(points: readonly StationValue[], options: InterpolationOptions): ScalarGrid;
63
+ /** Grid angin dari angin di banyak stasiun (mis. METAR beberapa bandara). */
64
+ export declare function windGridFromStations(stations: readonly StationWind[], options: InterpolationOptions): WindGrid;
65
+ /** Nilai grid di (lon, lat) dengan interpolasi bilinear; null di luar grid atau tanpa data. */
66
+ export declare function sampleScalar(grid: ScalarGrid, lon: number, lat: number): number | null;
67
+ /** Angin (m/s) di (lon, lat); null di luar grid atau tanpa data. */
68
+ export declare function sampleWind(grid: WindGrid, lon: number, lat: number): {
69
+ u: number;
70
+ v: number;
71
+ } | null;
72
+ /** Campur dua grid angin yang bentuknya sama (mis. dua jam prakiraan), `t` 0-1. */
73
+ export declare function blendWindGrids(a: WindGrid, b: WindGrid, t: number): WindGrid;
74
+ /**
75
+ * Baca JSON hasil `grib2json` (format yang juga dipakai leaflet-velocity): dua rekaman, komponen angin
76
+ * u (parameter 2) dan v (parameter 3). GRIB biner dikonversi dulu ke JSON ini (grib2json/wgrib2).
77
+ */
78
+ export declare function parseGrib2Json(input: unknown): WindGrid;
79
+ /**
80
+ * Angin stasiun dari beberapa METAR/SPECI. `locate` memberi koordinat dari kode ICAO (mis. dari
81
+ * tabel bandara milik aplikasi); stasiun tanpa koordinat dilewati.
82
+ */
83
+ export declare function stationWindsFromMetars(reports: readonly Metar[], locate: (icao: string) => GeoPoint | undefined): StationWind[];
84
+ /**
85
+ * Format JSON sederhana bawaan: `{ bounds: {west,south,east,north}, nx, ny, u, v }` untuk angin atau
86
+ * `{ bounds, nx, ny, values }` untuk skalar (awan, hujan, suhu). Larik boleh berisi null untuk "tanpa data".
87
+ */
88
+ export declare function parseGridJson(input: unknown): WindGrid | ScalarGrid;
89
+ declare const OPEN_METEO_SPEED_TO_MS: Record<string, number>;
90
+ /**
91
+ * Angin dari respons banyak lokasi layanan model bergaya Open-Meteo: larik `{latitude, longitude,
92
+ * current|hourly: {wind_speed_10m, wind_direction_10m}}`. `hourIndex` memilih jam dari `hourly`.
93
+ * `speedUnit` = satuan kecepatan di respons (bawaan km/h, seperti Open-Meteo).
94
+ */
95
+ export declare function stationWindsFromModelPoints(response: unknown, options?: {
96
+ hourIndex?: number;
97
+ speedUnit?: keyof typeof OPEN_METEO_SPEED_TO_MS;
98
+ }): StationWind[];
99
+ export type DecodedGrid = {
100
+ kind: "wind";
101
+ grid: WindGrid;
102
+ } | {
103
+ kind: "scalar";
104
+ grid: ScalarGrid;
105
+ };
106
+ /**
107
+ * Kenali dan baca JSON medan cuaca dari sumber mana pun: `grib2json`, JSON grid bawaan (`u`/`v` atau
108
+ * `values`), atau respons banyak lokasi layanan model (diinterpolasi ke grid `fallback`, wajib
109
+ * diberikan untuk bentuk terakhir ini). Melempar galat yang menjelaskan bila bentuknya tidak dikenal.
110
+ */
111
+ export declare function decodeGridJson(input: unknown, fallback?: InterpolationOptions & {
112
+ hourIndex?: number;
113
+ speedUnit?: keyof typeof OPEN_METEO_SPEED_TO_MS;
114
+ }): DecodedGrid;
115
+ export {};
116
+ //# sourceMappingURL=grid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../src/grid.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=grid.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grid.test.d.ts","sourceRoot":"","sources":["../src/grid.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
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 "./grid.js";
6
+ export * from "./metar.js";
7
+ export * from "./taf.js";
8
+ export * from "./time.js";
9
+ export * from "./types.js";
10
+ /**
11
+ * Baca semua laporan cuaca (METAR, SPECI, TAF) dari satu pesan mentah, termasuk bungkus AFTN.
12
+ * Laporan yang tidak terbaca dilewati. `reference` = waktu pesan diterima (menentukan bulan/tahun).
13
+ */
14
+ export declare function decodeWeatherMessage(message: string, reference?: Date): Report[];
15
+ //# 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,WAAW,CAAC;AAC1B,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,805 @@
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/grid.ts
563
+ var KT_TO_MS = 0.514444;
564
+ var KM_PER_DEG_LAT = 111.32;
565
+ var speedOf = (u, v) => Math.hypot(u, v);
566
+ function directionFromUV(u, v) {
567
+ return (Math.atan2(-u, -v) * 180 / Math.PI + 360) % 360;
568
+ }
569
+ function uvFromSpeedDirection(speedMs, fromDeg) {
570
+ const rad = fromDeg * Math.PI / 180;
571
+ return { u: -speedMs * Math.sin(rad), v: -speedMs * Math.cos(rad) };
572
+ }
573
+ function pointOf(bounds, nx, ny, col, row) {
574
+ return {
575
+ lon: bounds.west + (bounds.east - bounds.west) * col / Math.max(1, nx - 1),
576
+ lat: bounds.north - (bounds.north - bounds.south) * row / Math.max(1, ny - 1)
577
+ };
578
+ }
579
+ function distanceKm(a, b) {
580
+ const dLat = (a.lat - b.lat) * KM_PER_DEG_LAT;
581
+ const dLon = (a.lon - b.lon) * KM_PER_DEG_LAT * Math.cos((a.lat + b.lat) / 2 * (Math.PI / 180));
582
+ return Math.hypot(dLat, dLon);
583
+ }
584
+ function interpolateScalar(points, options) {
585
+ const { bounds, nx, ny } = options;
586
+ const power = options.power ?? 2;
587
+ const maxKm = options.maxDistanceKm ?? 400;
588
+ const values = new Float32Array(nx * ny).fill(Number.NaN);
589
+ for (let row = 0; row < ny; row++) {
590
+ for (let col = 0; col < nx; col++) {
591
+ const cell = pointOf(bounds, nx, ny, col, row);
592
+ let weightSum = 0;
593
+ let sum = 0;
594
+ let exact = null;
595
+ for (const p of points) {
596
+ const d = distanceKm(cell, p);
597
+ if (d > maxKm) continue;
598
+ if (d < 0.5) {
599
+ exact = p.value;
600
+ break;
601
+ }
602
+ const w = 1 / d ** power;
603
+ weightSum += w;
604
+ sum += w * p.value;
605
+ }
606
+ values[row * nx + col] = exact ?? (weightSum > 0 ? sum / weightSum : Number.NaN);
607
+ }
608
+ }
609
+ return { bounds, nx, ny, values };
610
+ }
611
+ function windGridFromStations(stations, options) {
612
+ const components = stations.map((s) => {
613
+ const speedMs = s.speedKt * KT_TO_MS;
614
+ const { u: u2, v: v2 } = s.directionDeg === null ? { u: 0, v: 0 } : uvFromSpeedDirection(speedMs, s.directionDeg);
615
+ return { lat: s.lat, lon: s.lon, u: u2, v: v2 };
616
+ });
617
+ const u = interpolateScalar(components.map((c) => ({ lat: c.lat, lon: c.lon, value: c.u })), options);
618
+ const v = interpolateScalar(components.map((c) => ({ lat: c.lat, lon: c.lon, value: c.v })), options);
619
+ return { bounds: options.bounds, nx: options.nx, ny: options.ny, u: u.values, v: v.values };
620
+ }
621
+ function bilinear(values, bounds, nx, ny, lon, lat) {
622
+ if (lon < bounds.west || lon > bounds.east || lat < bounds.south || lat > bounds.north) return null;
623
+ const fx = (lon - bounds.west) / (bounds.east - bounds.west || 1) * (nx - 1);
624
+ const fy = (bounds.north - lat) / (bounds.north - bounds.south || 1) * (ny - 1);
625
+ const x0 = Math.min(nx - 1, Math.floor(fx));
626
+ const y0 = Math.min(ny - 1, Math.floor(fy));
627
+ const x1 = Math.min(nx - 1, x0 + 1);
628
+ const y1 = Math.min(ny - 1, y0 + 1);
629
+ const tx = fx - x0;
630
+ const ty = fy - y0;
631
+ const a = values[y0 * nx + x0];
632
+ const b = values[y0 * nx + x1];
633
+ const c = values[y1 * nx + x0];
634
+ const d = values[y1 * nx + x1];
635
+ const result = (a * (1 - tx) + b * tx) * (1 - ty) + (c * (1 - tx) + d * tx) * ty;
636
+ return Number.isNaN(result) ? null : result;
637
+ }
638
+ function sampleScalar(grid, lon, lat) {
639
+ return bilinear(grid.values, grid.bounds, grid.nx, grid.ny, lon, lat);
640
+ }
641
+ function sampleWind(grid, lon, lat) {
642
+ const u = bilinear(grid.u, grid.bounds, grid.nx, grid.ny, lon, lat);
643
+ const v = bilinear(grid.v, grid.bounds, grid.nx, grid.ny, lon, lat);
644
+ return u === null || v === null ? null : { u, v };
645
+ }
646
+ function blendWindGrids(a, b, t) {
647
+ if (a.nx !== b.nx || a.ny !== b.ny) throw new Error("Grid angin harus berukuran sama");
648
+ const mix = (x, y) => Float32Array.from(x, (value, i) => value + (y[i] - value) * t);
649
+ return { bounds: a.bounds, nx: a.nx, ny: a.ny, u: mix(a.u, b.u), v: mix(a.v, b.v) };
650
+ }
651
+ var isRecord = (value) => typeof value === "object" && value !== null;
652
+ function isGrib2Json(input) {
653
+ return Array.isArray(input) && input.length >= 2 && input.every((r) => isRecord(r) && isRecord(r.header) && Array.isArray(r.data));
654
+ }
655
+ function parseGrib2Json(input) {
656
+ if (!isGrib2Json(input)) throw new Error("Bukan JSON grib2json: perlu larik rekaman {header, data}");
657
+ const uRecord = input.find((r) => r.header.parameterNumber === 2 && (r.header.parameterCategory ?? 2) === 2);
658
+ const vRecord = input.find((r) => r.header.parameterNumber === 3 && (r.header.parameterCategory ?? 2) === 2);
659
+ if (!uRecord || !vRecord) throw new Error("Komponen angin u (2) dan v (3) tidak ditemukan");
660
+ const { nx, ny, lo1, la1 } = uRecord.header;
661
+ if (uRecord.data.length !== nx * ny || vRecord.data.length !== nx * ny) {
662
+ throw new Error("Jumlah data tidak sama dengan nx * ny");
663
+ }
664
+ const dx = uRecord.header.dx ?? ((uRecord.header.lo2 ?? lo1) - lo1) / Math.max(1, nx - 1);
665
+ const dy = Math.abs(uRecord.header.dy ?? ((uRecord.header.la2 ?? la1) - la1) / Math.max(1, ny - 1));
666
+ const lo2 = uRecord.header.lo2 ?? lo1 + dx * (nx - 1);
667
+ const la2 = uRecord.header.la2 ?? la1 - dy * (ny - 1);
668
+ const norm = (lon) => lon > 180 ? lon - 360 : lon;
669
+ const north = Math.max(la1, la2);
670
+ const south = Math.min(la1, la2);
671
+ const flip = la1 < la2;
672
+ const orient = (data) => {
673
+ const out = new Float32Array(nx * ny);
674
+ for (let row = 0; row < ny; row++) {
675
+ const source = flip ? ny - 1 - row : row;
676
+ for (let col = 0; col < nx; col++) {
677
+ const value = data[source * nx + col];
678
+ out[row * nx + col] = typeof value === "number" ? value : Number.NaN;
679
+ }
680
+ }
681
+ return out;
682
+ };
683
+ return {
684
+ bounds: { west: norm(lo1), south, east: norm(lo2), north },
685
+ nx,
686
+ ny,
687
+ u: orient(uRecord.data),
688
+ v: orient(vRecord.data)
689
+ };
690
+ }
691
+ function stationWindsFromMetars(reports, locate) {
692
+ const out = [];
693
+ for (const report of reports) {
694
+ const place = locate(report.station);
695
+ if (!place) continue;
696
+ const { wind } = report.conditions;
697
+ out.push({
698
+ lat: place.lat,
699
+ lon: place.lon,
700
+ speedKt: wind.speedKt,
701
+ directionDeg: wind.variable ? null : wind.directionDeg
702
+ });
703
+ }
704
+ return out;
705
+ }
706
+ function parseGridJson(input) {
707
+ if (!isRecord(input) || !isRecord(input.bounds)) throw new Error("JSON grid tidak valid: bounds hilang");
708
+ const { west, south, east, north } = input.bounds;
709
+ const nx = Number(input.nx);
710
+ const ny = Number(input.ny);
711
+ if (![west, south, east, north].every((n) => typeof n === "number") || !(nx > 0) || !(ny > 0)) {
712
+ throw new Error("JSON grid tidak valid: bounds/nx/ny");
713
+ }
714
+ const bounds = { west, south, east, north };
715
+ const toArray = (value) => {
716
+ if (!Array.isArray(value) || value.length !== nx * ny) throw new Error("Ukuran larik tidak sama dengan nx * ny");
717
+ return Float32Array.from(value, (n) => typeof n === "number" ? n : Number.NaN);
718
+ };
719
+ if (input.u !== void 0 && input.v !== void 0) {
720
+ return { bounds, nx, ny, u: toArray(input.u), v: toArray(input.v) };
721
+ }
722
+ return { bounds, nx, ny, values: toArray(input.values) };
723
+ }
724
+ var OPEN_METEO_SPEED_TO_MS = {
725
+ "km/h": 1 / 3.6,
726
+ "m/s": 1,
727
+ kn: KT_TO_MS,
728
+ mph: 0.44704
729
+ };
730
+ function stationWindsFromModelPoints(response, options = {}) {
731
+ const list = Array.isArray(response) ? response : [response];
732
+ const toMs = OPEN_METEO_SPEED_TO_MS[options.speedUnit ?? "km/h"] ?? 1 / 3.6;
733
+ const out = [];
734
+ for (const item of list) {
735
+ if (!isRecord(item)) continue;
736
+ const lat = Number(item.latitude);
737
+ const lon = Number(item.longitude);
738
+ const source = isRecord(item.current) ? item.current : isRecord(item.hourly) ? item.hourly : null;
739
+ if (!source || Number.isNaN(lat) || Number.isNaN(lon)) continue;
740
+ const pick = (value) => Array.isArray(value) ? Number(value[options.hourIndex ?? 0]) : Number(value);
741
+ const speed = pick(source.wind_speed_10m);
742
+ const direction = pick(source.wind_direction_10m);
743
+ if (Number.isNaN(speed)) continue;
744
+ out.push({
745
+ lat,
746
+ lon,
747
+ speedKt: speed * toMs / KT_TO_MS,
748
+ directionDeg: Number.isNaN(direction) ? null : direction
749
+ });
750
+ }
751
+ return out;
752
+ }
753
+ function decodeGridJson(input, fallback) {
754
+ if (isGrib2Json(input)) return { kind: "wind", grid: parseGrib2Json(input) };
755
+ if (isRecord(input) && isRecord(input.bounds)) {
756
+ const grid = parseGridJson(input);
757
+ return "u" in grid ? { kind: "wind", grid } : { kind: "scalar", grid };
758
+ }
759
+ const items = Array.isArray(input) ? input : [input];
760
+ if (items.some((item) => isRecord(item) && item.latitude !== void 0 && item.longitude !== void 0)) {
761
+ if (!fallback) throw new Error("Respons banyak lokasi perlu bounds, nx, dan ny untuk dijadikan grid");
762
+ const stations = stationWindsFromModelPoints(input, fallback);
763
+ if (stations.length === 0) throw new Error("Tidak ada titik dengan wind_speed_10m pada respons");
764
+ return { kind: "wind", grid: windGridFromStations(stations, fallback) };
765
+ }
766
+ throw new Error("Format medan tidak dikenal: harus grib2json, JSON grid {bounds,nx,ny,u,v|values}, atau respons banyak lokasi");
767
+ }
768
+
769
+ // src/index.ts
770
+ function decodeWeatherMessage(message, reference = /* @__PURE__ */ new Date()) {
771
+ const reports = [];
772
+ for (const raw of extractReports(message)) {
773
+ const report = raw.kind === "TAF" ? parseTaf(raw.text, reference) : parseMetar(raw.text, reference);
774
+ if (report) reports.push(report);
775
+ }
776
+ return reports;
777
+ }
778
+ export {
779
+ animationFromConditions,
780
+ animationSnapshot,
781
+ blendConditions,
782
+ blendWindGrids,
783
+ decodeGridJson,
784
+ decodeWeatherMessage,
785
+ directionFromUV,
786
+ emptyConditions,
787
+ extractReports,
788
+ forecastAt,
789
+ interpolateScalar,
790
+ mergeConditions,
791
+ parseConditionTokens,
792
+ parseGrib2Json,
793
+ parseGridJson,
794
+ parseMetar,
795
+ parseTaf,
796
+ pointOf,
797
+ resolveDayTime,
798
+ sampleScalar,
799
+ sampleWind,
800
+ speedOf,
801
+ stationWindsFromMetars,
802
+ stationWindsFromModelPoints,
803
+ uvFromSpeedDirection,
804
+ windGridFromStations
805
+ };
@@ -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.2",
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
+ }