@aeroswarm/weather 0.1.1 → 0.1.3

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
@@ -25,3 +25,16 @@ const snapshot = animationSnapshot({ observation: metar, forecast: taf, at: new
25
25
 
26
26
  METAR/TAF describe one point (the aerodrome), not a weather field, so the animation is a
27
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/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":""}
package/dist/index.d.ts CHANGED
@@ -2,8 +2,10 @@ import type { Report } from "./types.js";
2
2
  export * from "./aftn.js";
3
3
  export * from "./animation.js";
4
4
  export * from "./conditions.js";
5
+ export * from "./grid.js";
5
6
  export * from "./metar.js";
6
7
  export * from "./taf.js";
8
+ export * from "./store.js";
7
9
  export * from "./time.js";
8
10
  export * from "./types.js";
9
11
  /**
@@ -1 +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"}
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,YAAY,CAAC;AAC3B,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 CHANGED
@@ -559,6 +559,427 @@ function animationSnapshot(input) {
559
559
  return null;
560
560
  }
561
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/store.ts
770
+ var MAX_OBSERVATIONS_PER_STATION = 48;
771
+ var MAX_FORECASTS_PER_STATION = 8;
772
+ var rainRate = (intensity) => intensity * intensity * 15;
773
+ var hhmm = (d) => `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
774
+ var WeatherStore = class {
775
+ constructor(locate) {
776
+ this.locate = locate;
777
+ }
778
+ locate;
779
+ observations = /* @__PURE__ */ new Map();
780
+ forecasts = /* @__PURE__ */ new Map();
781
+ listeners = /* @__PURE__ */ new Set();
782
+ /** Naik tiap data berubah; berguna untuk memicu render ulang. */
783
+ version = 0;
784
+ /** Terima pesan mentah (laporan polos, buletin, atau pesan AFTN; boleh banyak laporan). */
785
+ ingest(message, options = {}) {
786
+ const receivedAt = options.receivedAt ?? /* @__PURE__ */ new Date();
787
+ const manual = options.manual === true;
788
+ const result = { metars: 0, tafs: 0, skipped: 0, stations: [] };
789
+ const touched = /* @__PURE__ */ new Set();
790
+ for (const raw of extractReports(message)) {
791
+ if (raw.kind === "TAF") {
792
+ const taf = parseTaf(raw.text, receivedAt);
793
+ if (!taf) {
794
+ result.skipped++;
795
+ continue;
796
+ }
797
+ this.add(this.forecasts, taf.station, { report: taf, manual, receivedAt }, MAX_FORECASTS_PER_STATION);
798
+ touched.add(taf.station);
799
+ result.tafs++;
800
+ } else {
801
+ const metar = parseMetar(raw.text, receivedAt);
802
+ if (!metar) {
803
+ result.skipped++;
804
+ continue;
805
+ }
806
+ this.add(this.observations, metar.station, { report: metar, manual, receivedAt }, MAX_OBSERVATIONS_PER_STATION);
807
+ touched.add(metar.station);
808
+ result.metars++;
809
+ }
810
+ }
811
+ result.stations = [...touched];
812
+ if (result.metars + result.tafs > 0) this.changed();
813
+ return result;
814
+ }
815
+ /** Hapus semua data manual (seluruh stasiun, atau satu stasiun). */
816
+ clearManual(station) {
817
+ let removed = false;
818
+ for (const store of [this.observations, this.forecasts]) {
819
+ for (const [icao, list] of store) {
820
+ if (station && icao !== station) continue;
821
+ const kept = list.filter((s) => !s.manual);
822
+ if (kept.length !== list.length) removed = true;
823
+ store.set(icao, kept);
824
+ }
825
+ }
826
+ if (removed) this.changed();
827
+ }
828
+ /** Hapus semua data (otomatis dan manual). */
829
+ clear() {
830
+ this.observations.clear();
831
+ this.forecasts.clear();
832
+ this.changed();
833
+ }
834
+ hasManual() {
835
+ for (const store of [this.observations, this.forecasts]) {
836
+ for (const list of store.values()) if (list.some((s) => s.manual)) return true;
837
+ }
838
+ return false;
839
+ }
840
+ stations() {
841
+ return [.../* @__PURE__ */ new Set([...this.observations.keys(), ...this.forecasts.keys()])].filter(
842
+ (icao) => (this.observations.get(icao)?.length ?? 0) + (this.forecasts.get(icao)?.length ?? 0) > 0
843
+ );
844
+ }
845
+ onChange(listener) {
846
+ this.listeners.add(listener);
847
+ return () => this.listeners.delete(listener);
848
+ }
849
+ /** Cuaca satu stasiun pada waktu `at`; null bila tidak ada data. */
850
+ snapshotAt(station, at, options = {}) {
851
+ return animationSnapshot({
852
+ observation: this.pickObservation(station, at, options.maxObservationAgeMs),
853
+ forecast: this.pickForecast(station, at),
854
+ at,
855
+ maxObservationAgeMs: options.maxObservationAgeMs,
856
+ includeProbable: options.includeProbable
857
+ });
858
+ }
859
+ /** Rentang waktu dan pembagian periode (BASE/BECMG/TEMPO/FM) dari TAF, untuk timeline. */
860
+ timeline() {
861
+ const segments = [];
862
+ const observations = [];
863
+ let from = null;
864
+ let to = null;
865
+ const widen = (a, b) => {
866
+ from = from === null ? a.getTime() : Math.min(from, a.getTime());
867
+ to = to === null ? b.getTime() : Math.max(to, b.getTime());
868
+ };
869
+ for (const [station, list] of this.forecasts) {
870
+ const taf = this.latest(list, (t) => t.issuedAt);
871
+ if (!taf || taf.cancelled) continue;
872
+ widen(taf.validFrom, taf.validTo);
873
+ for (const period of taf.periods) {
874
+ if (period.change === "BASE") continue;
875
+ const label = period.change === "FM" ? `FM ${hhmm(period.from)}` : `${period.probability ? `PROB${period.probability} ` : ""}${period.change} ${hhmm(period.from)}-${hhmm(period.to)}`;
876
+ segments.push({ station, change: period.change, from: period.from, to: period.to, label });
877
+ }
878
+ }
879
+ for (const list of this.observations.values()) {
880
+ for (const source of list) {
881
+ observations.push(source.report.observedAt);
882
+ widen(source.report.observedAt, source.report.observedAt);
883
+ }
884
+ }
885
+ if (from === null || to === null) return null;
886
+ observations.sort((a, b) => a.getTime() - b.getTime());
887
+ return { from: new Date(from), to: new Date(to), segments, observations };
888
+ }
889
+ /**
890
+ * Medan angin, awan, hujan, dan suhu pada waktu `at`, dari semua stasiun yang punya data dan
891
+ * koordinat, diinterpolasi ke grid. Mengembalikan null bila tidak ada stasiun yang bisa dipakai.
892
+ */
893
+ fieldsAt(at, grid, options = {}) {
894
+ const winds = [];
895
+ const clouds = [];
896
+ const rains = [];
897
+ const temperatures = [];
898
+ const used = [];
899
+ for (const station of this.stations()) {
900
+ const place = this.locate(station);
901
+ if (!place) continue;
902
+ const snapshot = this.snapshotAt(station, at, options);
903
+ if (!snapshot) continue;
904
+ used.push(station);
905
+ const { state } = snapshot;
906
+ winds.push({
907
+ lat: place.lat,
908
+ lon: place.lon,
909
+ speedKt: state.wind.speedKt,
910
+ directionDeg: state.wind.fromDeg
911
+ });
912
+ clouds.push({ lat: place.lat, lon: place.lon, value: state.clouds.total * 100 });
913
+ rains.push({
914
+ lat: place.lat,
915
+ lon: place.lon,
916
+ value: state.precipitation ? rainRate(state.precipitation.intensity) : 0
917
+ });
918
+ const observed = this.pickObservation(station, at);
919
+ if (observed?.temperatureC !== null && observed?.temperatureC !== void 0) {
920
+ temperatures.push({ lat: place.lat, lon: place.lon, value: observed.temperatureC });
921
+ }
922
+ }
923
+ if (used.length === 0) return null;
924
+ return {
925
+ at,
926
+ stations: used,
927
+ wind: windGridFromStations(winds, grid),
928
+ cloud: interpolateScalar(clouds, grid),
929
+ rain: interpolateScalar(rains, grid),
930
+ temperature: temperatures.length > 0 ? interpolateScalar(temperatures, grid) : null
931
+ };
932
+ }
933
+ // ---- internal -------------------------------------------------------------
934
+ changed() {
935
+ this.version++;
936
+ for (const listener of this.listeners) listener();
937
+ }
938
+ add(store, station, source, limit) {
939
+ const list = store.get(station) ?? [];
940
+ const key = (s) => {
941
+ const report = s.report;
942
+ return (report.observedAt ?? report.issuedAt)?.getTime() ?? 0;
943
+ };
944
+ const next = list.filter((s) => !(s.manual === source.manual && key(s) === key(source)));
945
+ next.push(source);
946
+ next.sort((a, b) => key(a) - key(b));
947
+ store.set(station, next.slice(-limit));
948
+ }
949
+ latest(list, timeOf) {
950
+ const pool = list.some((s) => s.manual) ? list.filter((s) => s.manual) : list;
951
+ let best = null;
952
+ for (const source of pool) {
953
+ if (!best || timeOf(source.report).getTime() >= timeOf(best.report).getTime()) best = source;
954
+ }
955
+ return best?.report ?? null;
956
+ }
957
+ pickObservation(station, at, maxAgeMs = 3 * 36e5) {
958
+ const list = this.observations.get(station) ?? [];
959
+ const usable = (s) => {
960
+ const age = at.getTime() - s.report.observedAt.getTime();
961
+ return age >= 0 && age <= maxAgeMs;
962
+ };
963
+ const manual = list.filter((s) => s.manual && usable(s));
964
+ const pool = manual.length > 0 ? manual : list.filter(usable);
965
+ let best = null;
966
+ for (const source of pool) {
967
+ if (!best || source.report.observedAt.getTime() >= best.report.observedAt.getTime()) best = source;
968
+ }
969
+ if (best) return best.report;
970
+ const before = list.filter((s) => s.report.observedAt.getTime() <= at.getTime());
971
+ return before.length > 0 ? before[before.length - 1].report : null;
972
+ }
973
+ pickForecast(station, at) {
974
+ const list = this.forecasts.get(station) ?? [];
975
+ const covers = (s) => !s.report.cancelled && at.getTime() >= s.report.validFrom.getTime() && at.getTime() < s.report.validTo.getTime();
976
+ const manual = list.filter((s) => s.manual && covers(s));
977
+ const pool = manual.length > 0 ? manual : list.filter(covers);
978
+ if (pool.length === 0) return null;
979
+ return this.latest(pool, (t) => t.issuedAt);
980
+ }
981
+ };
982
+
562
983
  // src/index.ts
563
984
  function decodeWeatherMessage(message, reference = /* @__PURE__ */ new Date()) {
564
985
  const reports = [];
@@ -569,16 +990,31 @@ function decodeWeatherMessage(message, reference = /* @__PURE__ */ new Date()) {
569
990
  return reports;
570
991
  }
571
992
  export {
993
+ WeatherStore,
572
994
  animationFromConditions,
573
995
  animationSnapshot,
574
996
  blendConditions,
997
+ blendWindGrids,
998
+ decodeGridJson,
575
999
  decodeWeatherMessage,
1000
+ directionFromUV,
576
1001
  emptyConditions,
577
1002
  extractReports,
578
1003
  forecastAt,
1004
+ interpolateScalar,
579
1005
  mergeConditions,
580
1006
  parseConditionTokens,
1007
+ parseGrib2Json,
1008
+ parseGridJson,
581
1009
  parseMetar,
582
1010
  parseTaf,
583
- resolveDayTime
1011
+ pointOf,
1012
+ resolveDayTime,
1013
+ sampleScalar,
1014
+ sampleWind,
1015
+ speedOf,
1016
+ stationWindsFromMetars,
1017
+ stationWindsFromModelPoints,
1018
+ uvFromSpeedDirection,
1019
+ windGridFromStations
584
1020
  };
@@ -0,0 +1,91 @@
1
+ import { type AnimationSnapshot } from "./animation.js";
2
+ import { type GeoPoint, type GridBounds, type ScalarGrid, type WindGrid } from "./grid.js";
3
+ import type { TafChange } from "./types.js";
4
+ export type Locate = (icao: string) => GeoPoint | undefined;
5
+ export interface IngestOptions {
6
+ /** Waktu pesan diterima (menentukan bulan/tahun laporan). Bawaan: sekarang. */
7
+ receivedAt?: Date;
8
+ /** Data manual selalu menimpa data otomatis untuk stasiun dan waktu yang sama. */
9
+ manual?: boolean;
10
+ }
11
+ export interface IngestResult {
12
+ metars: number;
13
+ tafs: number;
14
+ /** Laporan yang dikenali kata kuncinya tetapi tidak terbaca. */
15
+ skipped: number;
16
+ stations: string[];
17
+ }
18
+ export interface TimelineSegment {
19
+ station: string;
20
+ change: TafChange;
21
+ from: Date;
22
+ to: Date;
23
+ label: string;
24
+ }
25
+ export interface Timeline {
26
+ from: Date;
27
+ to: Date;
28
+ segments: TimelineSegment[];
29
+ /** Waktu pengamatan (METAR/SPECI) yang ada. */
30
+ observations: Date[];
31
+ }
32
+ export interface WeatherFields {
33
+ at: Date;
34
+ /** Stasiun yang punya data pada waktu itu. */
35
+ stations: string[];
36
+ wind: WindGrid;
37
+ /** Tutupan awan 0-100. */
38
+ cloud: ScalarGrid;
39
+ /** Curah hujan (mm/jam), perkiraan dari intensitas pada laporan. */
40
+ rain: ScalarGrid;
41
+ /** Suhu (°C); null bila tidak ada pengamatan bersuhu. */
42
+ temperature: ScalarGrid | null;
43
+ }
44
+ export interface FieldGridOptions {
45
+ bounds: GridBounds;
46
+ nx: number;
47
+ ny: number;
48
+ maxDistanceKm?: number;
49
+ }
50
+ /**
51
+ * Menampung laporan cuaca (METAR, SPECI, TAF) dari mana pun, lalu menjawab "bagaimana cuacanya di
52
+ * waktu T": per stasiun maupun sebagai medan grid. Data manual selalu menimpa data otomatis.
53
+ */
54
+ export declare class WeatherStore {
55
+ private readonly locate;
56
+ private readonly observations;
57
+ private readonly forecasts;
58
+ private readonly listeners;
59
+ /** Naik tiap data berubah; berguna untuk memicu render ulang. */
60
+ version: number;
61
+ constructor(locate: Locate);
62
+ /** Terima pesan mentah (laporan polos, buletin, atau pesan AFTN; boleh banyak laporan). */
63
+ ingest(message: string, options?: IngestOptions): IngestResult;
64
+ /** Hapus semua data manual (seluruh stasiun, atau satu stasiun). */
65
+ clearManual(station?: string): void;
66
+ /** Hapus semua data (otomatis dan manual). */
67
+ clear(): void;
68
+ hasManual(): boolean;
69
+ stations(): string[];
70
+ onChange(listener: () => void): () => void;
71
+ /** Cuaca satu stasiun pada waktu `at`; null bila tidak ada data. */
72
+ snapshotAt(station: string, at: Date, options?: {
73
+ maxObservationAgeMs?: number;
74
+ includeProbable?: boolean;
75
+ }): AnimationSnapshot | null;
76
+ /** Rentang waktu dan pembagian periode (BASE/BECMG/TEMPO/FM) dari TAF, untuk timeline. */
77
+ timeline(): Timeline | null;
78
+ /**
79
+ * Medan angin, awan, hujan, dan suhu pada waktu `at`, dari semua stasiun yang punya data dan
80
+ * koordinat, diinterpolasi ke grid. Mengembalikan null bila tidak ada stasiun yang bisa dipakai.
81
+ */
82
+ fieldsAt(at: Date, grid: FieldGridOptions, options?: {
83
+ includeProbable?: boolean;
84
+ }): WeatherFields | null;
85
+ private changed;
86
+ private add;
87
+ private latest;
88
+ private pickObservation;
89
+ private pickForecast;
90
+ }
91
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=store.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.test.d.ts","sourceRoot":"","sources":["../src/store.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeroswarm/weather",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Decode raw AFTN/METAR/SPECI/TAF messages and turn them into animation parameters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",