@aeroswarm/weather 0.1.3 → 0.1.5

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.
@@ -0,0 +1,68 @@
1
+ # AeroSwarm Weather Grid (`aeroswarm.weather-grid`) v1
2
+
3
+ One JSON format for gridded weather forecasts, whatever the source (a model API, a converted GRIB
4
+ file, your own service). A source only has to produce this document; the renderer and the timeline
5
+ never need to know where the data came from.
6
+
7
+ - Read: `parseWeatherGridSeries(json)` (strict, throws a message naming the bad part).
8
+ - Write: `serializeWeatherGridSeries(series)`.
9
+ - Use: `seriesFieldsAt(series, time)` blends the two nearest frames; `null` outside the time range.
10
+ - Open-Meteo: `latticePoints(lattice)` gives the points to request, `weatherGridFromOpenMeteo(response,
11
+ lattice)` turns the answer into this format. Nothing here calls the network.
12
+
13
+ ## Document
14
+
15
+ ```json
16
+ {
17
+ "standard": "aeroswarm.weather-grid",
18
+ "version": 1,
19
+ "source": {
20
+ "name": "open-meteo",
21
+ "model": "icon_seamless",
22
+ "runAt": "2026-11-02T00:00:00Z",
23
+ "generatedAt": "2026-11-02T04:12:00Z"
24
+ },
25
+ "grid": { "west": 105, "south": -11, "east": 118, "north": -5, "nx": 14, "ny": 13 },
26
+ "times": ["2026-11-02T04:00:00Z", "2026-11-02T05:00:00Z"],
27
+ "fields": {
28
+ "windU": [[/* nx*ny values for time 0 */], [/* time 1 */]],
29
+ "windV": [[...], [...]],
30
+ "cloud": [[...], [...]],
31
+ "rain": [[...], [...]],
32
+ "temperature": [[...], [...]]
33
+ }
34
+ }
35
+ ```
36
+
37
+ ## Rules
38
+
39
+ | Part | Rule |
40
+ |---|---|
41
+ | `standard`, `version` | Exactly `"aeroswarm.weather-grid"` and `1`. |
42
+ | `source.name` | Required. `model`, `runAt`, `generatedAt` are optional (ISO 8601). |
43
+ | `grid` | `west < east`, `south < north`, `nx` and `ny` integers >= 2. Cells sit on the lattice points: column 0 is `west`, the last column is `east`; row 0 is `north`, the last row is `south`. |
44
+ | `times` | At least one ISO 8601 time in UTC, strictly increasing. |
45
+ | `fields` | At least one quantity. Each present quantity has exactly one array per time, and each array has exactly `nx * ny` values in row-major order (row 0 first). `null` means no data. |
46
+ | `windU` / `windV` | Both or neither. m/s. `windU` blows towards the east, `windV` towards the north. |
47
+ | `cloud` | Total cloud cover, percent 0-100. |
48
+ | `rain` | Precipitation rate, mm per hour. |
49
+ | `temperature` | Degrees Celsius. |
50
+
51
+ Wind is stored as components, not speed and direction, so frames can be blended and interpolated
52
+ without wrapping problems. To convert: `u = -speed * sin(from)`, `v = -speed * cos(from)`, where
53
+ `from` is the direction the wind comes FROM (meteorological, degrees from north).
54
+
55
+ ## Versioning
56
+
57
+ A change that breaks readers (a different layout, a changed unit) raises `version`. Adding an optional
58
+ quantity or an optional `source` field does not. A reader must reject a `version` it does not know
59
+ instead of guessing.
60
+
61
+ ## Producing it
62
+
63
+ - **Open-Meteo (in an app, not in this package):** request the lattice points with
64
+ `hourly=wind_speed_10m,wind_direction_10m,cloud_cover,precipitation,temperature_2m`,
65
+ `wind_speed_unit=ms`, `timezone=GMT`, then call `weatherGridFromOpenMeteo`.
66
+ - **GRIB:** convert to JSON on the server (`grib2json` or `wgrib2`), then build the document from the
67
+ two wind records and any scalar fields.
68
+ - **Your own service:** return this document directly; the app loads it from a URL or a file.
package/README.md CHANGED
@@ -38,3 +38,11 @@ representation around the airport, not the real position of clouds or rain.
38
38
  - `sampleWind`, `sampleScalar` (bilinear) and `blendWindGrids` (between two forecast hours).
39
39
 
40
40
  Row 0 is north, wind is in m/s (`u` east, `v` north), cells without data are NaN.
41
+
42
+ ## Gridded forecast standard
43
+
44
+ `GRID_STANDARD.md` defines `aeroswarm.weather-grid` v1: one JSON format (grid + times + wind/cloud/rain/
45
+ temperature) for any model source. `parseWeatherGridSeries`, `serializeWeatherGridSeries`,
46
+ `seriesFieldsAt`, and the Open-Meteo adapter (`latticePoints`, `weatherGridFromOpenMeteo`) are in the
47
+ package; the network call stays in the app.
48
+
package/dist/grid.d.ts CHANGED
@@ -69,6 +69,18 @@ export declare function sampleWind(grid: WindGrid, lon: number, lat: number): {
69
69
  u: number;
70
70
  v: number;
71
71
  } | null;
72
+ export interface NudgeOptions {
73
+ /** Jarak pengaruh sebuah stasiun (km). Di luar jarak ini grid tidak berubah. Bawaan 80. */
74
+ radiusKm?: number;
75
+ }
76
+ /**
77
+ * Sesuaikan grid model dengan pengamatan di stasiun: di dekat stasiun nilai grid ditarik ke nilai
78
+ * pengamatan (persis di stasiun), makin jauh makin kecil, dan di luar `radiusKm` tidak berubah. Stasiun
79
+ * di luar grid atau di sel tanpa data dilewati.
80
+ */
81
+ export declare function nudgeScalar(grid: ScalarGrid, points: readonly StationValue[], options?: NudgeOptions): ScalarGrid;
82
+ /** Sesuaikan grid angin model dengan angin di stasiun (pada komponen u dan v). */
83
+ export declare function nudgeWind(grid: WindGrid, stations: readonly StationWind[], options?: NudgeOptions): WindGrid;
72
84
  /** Campur dua grid angin yang bentuknya sama (mis. dua jam prakiraan), `t` 0-1. */
73
85
  export declare function blendWindGrids(a: WindGrid, b: WindGrid, t: number): WindGrid;
74
86
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../src/grid.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../src/grid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;;;;;GAMG;AAEH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,EAAE,YAAY,CAAC;IAChB,CAAC,EAAE,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,YAAa,SAAQ,QAAQ;IAC5C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,UAAU,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sGAAsG;IACtG,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAKD,eAAO,MAAM,OAAO,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,MAA0B,CAAC;AAE1E,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,6DAA6D;AAC7D,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAG1B;AAED,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAKtG;AAQD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,YAAY,EAAE,EAC/B,OAAO,EAAE,oBAAoB,GAC5B,UAAU,CA2BZ;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,OAAO,EAAE,oBAAoB,GAC5B,QAAQ,CAUV;AA2BD,+FAA+F;AAC/F,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEtF;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAIpG;AAED,MAAM,WAAW,YAAY;IAC3B,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAsCD;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,SAAS,YAAY,EAAE,EAC/B,OAAO,GAAE,YAAiB,GACzB,UAAU,CAOZ;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CACvB,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,OAAO,GAAE,YAAiB,GACzB,QAAQ,CAgBV;AAED,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,QAAQ,CAK5E;AAgCD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,CAuCvD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,SAAS,KAAK,EAAE,EACzB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,QAAQ,GAAG,SAAS,GAC7C,WAAW,EAAE,CAcf;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,UAAU,CAiBnE;AAED,QAAA,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKlD,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,OAAO,EACjB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,OAAO,sBAAsB,CAAA;CAAO,GACpF,WAAW,EAAE,CAuBf;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AAEzC;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,OAAO,EACd,QAAQ,CAAC,EAAE,oBAAoB,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,OAAO,sBAAsB,CAAA;CAAE,GACxG,WAAW,CAiBb"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Standar data medan cuaca AeroSwarm (`aeroswarm.weather-grid` versi 1).
3
+ *
4
+ * Satu format untuk semua sumber model (Open-Meteo, hasil konversi GRIB, layanan sendiri): grid
5
+ * beraturan, beberapa waktu, dan beberapa besaran. Sumber apa pun cukup menghasilkan format ini; renderer
6
+ * dan timeline tidak perlu tahu asalnya. Lihat GRID_STANDARD.md untuk bentuk JSON-nya.
7
+ *
8
+ * Konvensi: baris 0 = utara, kolom 0 = barat. Angin m/s (`windU` ke timur, `windV` ke utara), awan %,
9
+ * hujan mm/jam, suhu °C. Nilai `null` = tanpa data. Waktu UTC.
10
+ */
11
+ import { type GridBounds, type ScalarGrid, type WindGrid } from "./grid.js";
12
+ export declare const WEATHER_GRID_STANDARD = "aeroswarm.weather-grid";
13
+ export declare const WEATHER_GRID_VERSION = 1;
14
+ export type GridQuantity = "windU" | "windV" | "cloud" | "rain" | "temperature";
15
+ export interface WeatherGridSource {
16
+ /** Nama sumber, mis. `open-meteo`. */
17
+ name: string;
18
+ /** Nama model, mis. `icon_seamless`. */
19
+ model?: string;
20
+ /** Waktu model dijalankan (run), bila diketahui. */
21
+ runAt?: Date;
22
+ /** Waktu data diambil/dibuat. */
23
+ generatedAt?: Date;
24
+ }
25
+ export interface WeatherGridSeries {
26
+ source: WeatherGridSource;
27
+ bounds: GridBounds;
28
+ nx: number;
29
+ ny: number;
30
+ /** Waktu tiap bingkai, naik. */
31
+ times: Date[];
32
+ /** Per besaran: satu larik nx*ny per waktu. Besaran yang tidak ada dibiarkan kosong. */
33
+ fields: Partial<Record<GridQuantity, Float32Array[]>>;
34
+ }
35
+ export interface SeriesFields {
36
+ at: Date;
37
+ wind: WindGrid | null;
38
+ cloud: ScalarGrid | null;
39
+ rain: ScalarGrid | null;
40
+ temperature: ScalarGrid | null;
41
+ }
42
+ /**
43
+ * Baca dan periksa JSON berstandar `aeroswarm.weather-grid` v1. Melempar galat yang menyebut bagian
44
+ * mana yang salah. Nilai di luar angka (null) menjadi NaN (tanpa data).
45
+ */
46
+ export declare function parseWeatherGridSeries(input: unknown): WeatherGridSeries;
47
+ /** Bentuk JSON berstandar dari seri (kebalikan `parseWeatherGridSeries`); NaN menjadi null. */
48
+ export declare function serializeWeatherGridSeries(series: WeatherGridSeries): Record<string, unknown>;
49
+ /** Rentang waktu seri. */
50
+ export declare function seriesRange(series: WeatherGridSeries): {
51
+ from: Date;
52
+ to: Date;
53
+ };
54
+ /**
55
+ * Medan pada waktu `at`, dicampur linear antar dua bingkai terdekat. Null di luar rentang waktu seri.
56
+ * Besaran yang tidak ada di seri dikembalikan null.
57
+ */
58
+ export declare function seriesFieldsAt(series: WeatherGridSeries, at: Date): SeriesFields | null;
59
+ //# sourceMappingURL=gridSeries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gridSeries.d.ts","sourceRoot":"","sources":["../src/gridSeries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,QAAQ,EACd,MAAM,WAAW,CAAC;AAEnB,eAAO,MAAM,qBAAqB,2BAA2B,CAAC;AAC9D,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,aAAa,CAAC;AAEhF,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,iCAAiC;IACjC,WAAW,CAAC,EAAE,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,wFAAwF;IACxF,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,IAAI,CAAC;IACT,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,UAAU,GAAG,IAAI,CAAC;CAChC;AAaD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAgExE;AAED,+FAA+F;AAC/F,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsB7F;AAED,0BAA0B;AAC1B,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,CAK/E;AAqBD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,EAAE,EAAE,EAAE,IAAI,GAAG,YAAY,GAAG,IAAI,CAwBvF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gridSeries.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gridSeries.test.d.ts","sourceRoot":"","sources":["../src/gridSeries.test.ts"],"names":[],"mappings":""}
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export * from "./aftn.js";
3
3
  export * from "./animation.js";
4
4
  export * from "./conditions.js";
5
5
  export * from "./grid.js";
6
+ export * from "./gridSeries.js";
7
+ export * from "./openMeteo.js";
6
8
  export * from "./metar.js";
7
9
  export * from "./taf.js";
8
10
  export * from "./store.js";
@@ -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,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"}
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,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,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
@@ -643,6 +643,52 @@ function sampleWind(grid, lon, lat) {
643
643
  const v = bilinear(grid.v, grid.bounds, grid.nx, grid.ny, lon, lat);
644
644
  return u === null || v === null ? null : { u, v };
645
645
  }
646
+ var influence = (distance, radius) => distance >= radius ? 0 : (radius * radius - distance * distance) / (radius * radius + distance * distance);
647
+ function nudgeValues(values, bounds, nx, ny, deltas, radius) {
648
+ const out = Float32Array.from(values);
649
+ if (deltas.length === 0) return out;
650
+ for (let row = 0; row < ny; row++) {
651
+ for (let col = 0; col < nx; col++) {
652
+ const index = row * nx + col;
653
+ if (Number.isNaN(out[index])) continue;
654
+ const cell = pointOf(bounds, nx, ny, col, row);
655
+ let weights = 0;
656
+ let weighted = 0;
657
+ for (const d of deltas) {
658
+ const w = influence(distanceKm(cell, d), radius);
659
+ weights += w;
660
+ weighted += w * d.delta;
661
+ }
662
+ if (weights > 0) out[index] = out[index] + weighted / weights * Math.min(1, weights);
663
+ }
664
+ }
665
+ return out;
666
+ }
667
+ function nudgeScalar(grid, points, options = {}) {
668
+ const deltas = [];
669
+ for (const p of points) {
670
+ const model = sampleScalar(grid, p.lon, p.lat);
671
+ if (model !== null) deltas.push({ lat: p.lat, lon: p.lon, delta: p.value - model });
672
+ }
673
+ return { ...grid, values: nudgeValues(grid.values, grid.bounds, grid.nx, grid.ny, deltas, options.radiusKm ?? 80) };
674
+ }
675
+ function nudgeWind(grid, stations, options = {}) {
676
+ const du = [];
677
+ const dv = [];
678
+ for (const s of stations) {
679
+ const model = sampleWind(grid, s.lon, s.lat);
680
+ if (!model) continue;
681
+ const obs = s.directionDeg === null ? { u: 0, v: 0 } : uvFromSpeedDirection(s.speedKt * KT_TO_MS, s.directionDeg);
682
+ du.push({ lat: s.lat, lon: s.lon, delta: obs.u - model.u });
683
+ dv.push({ lat: s.lat, lon: s.lon, delta: obs.v - model.v });
684
+ }
685
+ const radius = options.radiusKm ?? 80;
686
+ return {
687
+ ...grid,
688
+ u: nudgeValues(grid.u, grid.bounds, grid.nx, grid.ny, du, radius),
689
+ v: nudgeValues(grid.v, grid.bounds, grid.nx, grid.ny, dv, radius)
690
+ };
691
+ }
646
692
  function blendWindGrids(a, b, t) {
647
693
  if (a.nx !== b.nx || a.ny !== b.ny) throw new Error("Grid angin harus berukuran sama");
648
694
  const mix = (x, y) => Float32Array.from(x, (value, i) => value + (y[i] - value) * t);
@@ -688,10 +734,10 @@ function parseGrib2Json(input) {
688
734
  v: orient(vRecord.data)
689
735
  };
690
736
  }
691
- function stationWindsFromMetars(reports, locate) {
737
+ function stationWindsFromMetars(reports, locate2) {
692
738
  const out = [];
693
739
  for (const report of reports) {
694
- const place = locate(report.station);
740
+ const place = locate2(report.station);
695
741
  if (!place) continue;
696
742
  const { wind } = report.conditions;
697
743
  out.push({
@@ -766,14 +812,222 @@ function decodeGridJson(input, fallback) {
766
812
  throw new Error("Format medan tidak dikenal: harus grib2json, JSON grid {bounds,nx,ny,u,v|values}, atau respons banyak lokasi");
767
813
  }
768
814
 
815
+ // src/gridSeries.ts
816
+ var WEATHER_GRID_STANDARD = "aeroswarm.weather-grid";
817
+ var WEATHER_GRID_VERSION = 1;
818
+ var QUANTITIES = ["windU", "windV", "cloud", "rain", "temperature"];
819
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
820
+ function parseDate(value, where) {
821
+ const date = typeof value === "string" ? new Date(value) : new Date(Number.NaN);
822
+ if (Number.isNaN(date.getTime())) throw new Error(`${where}: bukan tanggal ISO 8601 yang valid`);
823
+ return date;
824
+ }
825
+ function parseWeatherGridSeries(input) {
826
+ if (!isRecord2(input)) throw new Error("Bukan objek JSON");
827
+ if (input.standard !== WEATHER_GRID_STANDARD) {
828
+ throw new Error(`standard harus "${WEATHER_GRID_STANDARD}"`);
829
+ }
830
+ if (input.version !== WEATHER_GRID_VERSION) {
831
+ throw new Error(`version ${String(input.version)} tidak didukung (didukung: ${WEATHER_GRID_VERSION})`);
832
+ }
833
+ const src = isRecord2(input.source) ? input.source : null;
834
+ if (!src || typeof src.name !== "string" || src.name === "") throw new Error("source.name wajib diisi");
835
+ const source = {
836
+ name: src.name,
837
+ model: typeof src.model === "string" ? src.model : void 0,
838
+ runAt: src.runAt === void 0 ? void 0 : parseDate(src.runAt, "source.runAt"),
839
+ generatedAt: src.generatedAt === void 0 ? void 0 : parseDate(src.generatedAt, "source.generatedAt")
840
+ };
841
+ const grid = isRecord2(input.grid) ? input.grid : null;
842
+ if (!grid) throw new Error("grid wajib diisi");
843
+ const { west, south, east, north } = grid;
844
+ const nx = Number(grid.nx);
845
+ const ny = Number(grid.ny);
846
+ if (![west, south, east, north].every((n) => typeof n === "number" && Number.isFinite(n))) {
847
+ throw new Error("grid.west/south/east/north harus angka");
848
+ }
849
+ if (!Number.isInteger(nx) || !Number.isInteger(ny) || nx < 2 || ny < 2) {
850
+ throw new Error("grid.nx dan grid.ny harus bilangan bulat >= 2");
851
+ }
852
+ const bounds = { west, south, east, north };
853
+ if (bounds.west >= bounds.east || bounds.south >= bounds.north) {
854
+ throw new Error("grid: west < east dan south < north");
855
+ }
856
+ if (!Array.isArray(input.times) || input.times.length === 0) throw new Error("times wajib berisi minimal satu waktu");
857
+ const times = input.times.map((t, i) => parseDate(t, `times[${i}]`));
858
+ for (let i = 1; i < times.length; i++) {
859
+ if (times[i].getTime() <= times[i - 1].getTime()) {
860
+ throw new Error(`times[${i}] harus lebih baru dari times[${i - 1}]`);
861
+ }
862
+ }
863
+ const rawFields = isRecord2(input.fields) ? input.fields : null;
864
+ if (!rawFields) throw new Error("fields wajib diisi");
865
+ const fields = {};
866
+ for (const quantity of QUANTITIES) {
867
+ const frames = rawFields[quantity];
868
+ if (frames === void 0) continue;
869
+ if (!Array.isArray(frames) || frames.length !== times.length) {
870
+ throw new Error(`fields.${quantity} harus berisi ${times.length} bingkai (satu per waktu)`);
871
+ }
872
+ fields[quantity] = frames.map((frame, t) => {
873
+ if (!Array.isArray(frame) || frame.length !== nx * ny) {
874
+ throw new Error(`fields.${quantity}[${t}] harus berisi ${nx * ny} nilai (nx * ny)`);
875
+ }
876
+ return Float32Array.from(frame, (v) => typeof v === "number" && Number.isFinite(v) ? v : Number.NaN);
877
+ });
878
+ }
879
+ if (fields.windU === void 0 !== (fields.windV === void 0)) {
880
+ throw new Error("windU dan windV harus ada berdua");
881
+ }
882
+ if (Object.keys(fields).length === 0) throw new Error("fields tidak berisi satu besaran pun");
883
+ return { source, bounds, nx, ny, times, fields };
884
+ }
885
+ function serializeWeatherGridSeries(series) {
886
+ const fields = {};
887
+ for (const quantity of QUANTITIES) {
888
+ const frames = series.fields[quantity];
889
+ if (!frames) continue;
890
+ fields[quantity] = frames.map(
891
+ (frame) => Array.from(frame, (v) => Number.isNaN(v) ? null : Math.round(v * 1e3) / 1e3)
892
+ );
893
+ }
894
+ return {
895
+ standard: WEATHER_GRID_STANDARD,
896
+ version: WEATHER_GRID_VERSION,
897
+ source: {
898
+ name: series.source.name,
899
+ ...series.source.model ? { model: series.source.model } : {},
900
+ ...series.source.runAt ? { runAt: series.source.runAt.toISOString() } : {},
901
+ ...series.source.generatedAt ? { generatedAt: series.source.generatedAt.toISOString() } : {}
902
+ },
903
+ grid: { ...series.bounds, nx: series.nx, ny: series.ny },
904
+ times: series.times.map((t) => t.toISOString()),
905
+ fields
906
+ };
907
+ }
908
+ function seriesRange(series) {
909
+ return {
910
+ from: series.times[0],
911
+ to: series.times[series.times.length - 1]
912
+ };
913
+ }
914
+ function locate(times, at) {
915
+ const ms = at.getTime();
916
+ const first = times[0].getTime();
917
+ const last = times[times.length - 1].getTime();
918
+ if (ms < first || ms > last) return null;
919
+ for (let i = 1; i < times.length; i++) {
920
+ const hi = times[i].getTime();
921
+ if (ms <= hi) {
922
+ const lo = times[i - 1].getTime();
923
+ return { a: i - 1, b: i, t: (ms - lo) / (hi - lo || 1) };
924
+ }
925
+ }
926
+ return { a: times.length - 1, b: times.length - 1, t: 0 };
927
+ }
928
+ var mixFrames = (frames, a, b, t) => Float32Array.from(frames[a], (value, i) => value + (frames[b][i] - value) * t);
929
+ function seriesFieldsAt(series, at) {
930
+ const where = locate(series.times, at);
931
+ if (!where) return null;
932
+ const { bounds, nx, ny } = series;
933
+ const scalar = (quantity) => {
934
+ const frames = series.fields[quantity];
935
+ return frames ? { bounds, nx, ny, values: mixFrames(frames, where.a, where.b, where.t) } : null;
936
+ };
937
+ const u = series.fields.windU;
938
+ const v = series.fields.windV;
939
+ let wind = null;
940
+ if (u && v) {
941
+ const from = { bounds, nx, ny, u: u[where.a], v: v[where.a] };
942
+ const to = { bounds, nx, ny, u: u[where.b], v: v[where.b] };
943
+ wind = where.a === where.b ? from : blendWindGrids(from, to, where.t);
944
+ }
945
+ return {
946
+ at,
947
+ wind,
948
+ cloud: scalar("cloud"),
949
+ rain: scalar("rain"),
950
+ temperature: scalar("temperature")
951
+ };
952
+ }
953
+
954
+ // src/openMeteo.ts
955
+ function latticePoints(lattice) {
956
+ const latitude = [];
957
+ const longitude = [];
958
+ for (let row = 0; row < lattice.ny; row++) {
959
+ const lat = lattice.north - (lattice.north - lattice.south) * row / (lattice.ny - 1);
960
+ for (let col = 0; col < lattice.nx; col++) {
961
+ latitude.push(round(lat));
962
+ longitude.push(round(lattice.west + (lattice.east - lattice.west) * col / (lattice.nx - 1)));
963
+ }
964
+ }
965
+ return { latitude, longitude };
966
+ }
967
+ var round = (value) => Math.round(value * 1e4) / 1e4;
968
+ var SPEED_TO_MS = { "m/s": 1, "km/h": 1 / 3.6, kn: 0.514444, mph: 0.44704 };
969
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
970
+ var toUtc = (iso) => new Date(/[zZ]|[+-]\d\d:?\d\d$/.test(iso) ? iso : `${iso}Z`);
971
+ function weatherGridFromOpenMeteo(response, lattice, options = {}) {
972
+ const items = Array.isArray(response) ? response : [response];
973
+ const expected = lattice.nx * lattice.ny;
974
+ if (items.length !== expected) {
975
+ throw new Error(`Respons berisi ${items.length} titik, kisi memerlukan ${expected} (${lattice.nx} x ${lattice.ny})`);
976
+ }
977
+ const hourly = items.map((item, i) => {
978
+ const h = isRecord3(item) && isRecord3(item.hourly) ? item.hourly : null;
979
+ if (!h || !Array.isArray(h.time)) throw new Error(`Titik ${i}: tidak ada data hourly.time`);
980
+ return h;
981
+ });
982
+ const timesIso = hourly[0].time;
983
+ if (timesIso.length === 0) throw new Error("hourly.time kosong");
984
+ for (const [i, h] of hourly.entries()) {
985
+ if (h.time.length !== timesIso.length) throw new Error(`Titik ${i}: jumlah waktu berbeda dari titik pertama`);
986
+ }
987
+ const times = timesIso.map(toUtc);
988
+ const toMs = SPEED_TO_MS[options.speedUnit ?? "m/s"] ?? 1;
989
+ const frame = (pick) => times.map((_, t) => Float32Array.from(hourly, (h) => pick(h, t)));
990
+ const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : Number.NaN;
991
+ const fields = {};
992
+ const has = (key) => hourly.every((h) => Array.isArray(h[key]));
993
+ if (has("wind_speed_10m") && has("wind_direction_10m")) {
994
+ const uv = (h, t) => {
995
+ const speed = num(h.wind_speed_10m?.[t]) * toMs;
996
+ const dir = num(h.wind_direction_10m?.[t]);
997
+ return Number.isNaN(speed) || Number.isNaN(dir) ? { u: Number.NaN, v: Number.NaN } : uvFromSpeedDirection(speed, dir);
998
+ };
999
+ fields.windU = frame((h, t) => uv(h, t).u);
1000
+ fields.windV = frame((h, t) => uv(h, t).v);
1001
+ }
1002
+ if (has("cloud_cover")) fields.cloud = frame((h, t) => num(h.cloud_cover?.[t]));
1003
+ if (has("precipitation")) fields.rain = frame((h, t) => num(h.precipitation?.[t]));
1004
+ if (has("temperature_2m")) fields.temperature = frame((h, t) => num(h.temperature_2m?.[t]));
1005
+ if (Object.keys(fields).length === 0) {
1006
+ throw new Error("Respons tidak memuat wind_speed_10m, wind_direction_10m, cloud_cover, precipitation, atau temperature_2m");
1007
+ }
1008
+ return {
1009
+ source: {
1010
+ name: "open-meteo",
1011
+ model: options.model,
1012
+ runAt: options.runAt,
1013
+ generatedAt: options.generatedAt ?? /* @__PURE__ */ new Date()
1014
+ },
1015
+ bounds: { west: lattice.west, south: lattice.south, east: lattice.east, north: lattice.north },
1016
+ nx: lattice.nx,
1017
+ ny: lattice.ny,
1018
+ times,
1019
+ fields
1020
+ };
1021
+ }
1022
+
769
1023
  // src/store.ts
770
1024
  var MAX_OBSERVATIONS_PER_STATION = 48;
771
1025
  var MAX_FORECASTS_PER_STATION = 8;
772
1026
  var rainRate = (intensity) => intensity * intensity * 15;
773
1027
  var hhmm = (d) => `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
774
1028
  var WeatherStore = class {
775
- constructor(locate) {
776
- this.locate = locate;
1029
+ constructor(locate2) {
1030
+ this.locate = locate2;
777
1031
  }
778
1032
  locate;
779
1033
  observations = /* @__PURE__ */ new Map();
@@ -891,24 +1145,48 @@ var WeatherStore = class {
891
1145
  * koordinat, diinterpolasi ke grid. Mengembalikan null bila tidak ada stasiun yang bisa dipakai.
892
1146
  */
893
1147
  fieldsAt(at, grid, options = {}) {
1148
+ const points = this.pointsAt(at, options);
1149
+ if (points.stations.length === 0) return null;
1150
+ return {
1151
+ at,
1152
+ stations: points.stations,
1153
+ wind: windGridFromStations(points.winds, grid),
1154
+ cloud: interpolateScalar(points.clouds, grid),
1155
+ rain: interpolateScalar(points.rains, grid),
1156
+ temperature: points.temperatures.length > 0 ? interpolateScalar(points.temperatures, grid) : null
1157
+ };
1158
+ }
1159
+ /**
1160
+ * Gabungkan medan model dengan laporan: grid model disesuaikan ke pengamatan (METAR/SPECI) atau
1161
+ * prakiraan (TAF) di stasiun yang punya koordinat, dalam jarak `radiusKm` dari tiap stasiun. Tanpa
1162
+ * stasiun yang bisa dipakai, medan model dikembalikan apa adanya.
1163
+ */
1164
+ blendInto(model, modelName, at, options = {}) {
1165
+ const points = this.pointsAt(at, options);
1166
+ return {
1167
+ at,
1168
+ stations: [modelName, ...points.stations],
1169
+ wind: model.wind ? nudgeWind(model.wind, points.winds, options) : null,
1170
+ cloud: model.cloud ? nudgeScalar(model.cloud, points.clouds, options) : null,
1171
+ rain: model.rain ? nudgeScalar(model.rain, points.rains, options) : null,
1172
+ temperature: model.temperature ? nudgeScalar(model.temperature, points.temperatures, options) : null
1173
+ };
1174
+ }
1175
+ /** Nilai tiap stasiun yang punya koordinat dan data pada waktu `at`. */
1176
+ pointsAt(at, options) {
894
1177
  const winds = [];
895
1178
  const clouds = [];
896
1179
  const rains = [];
897
1180
  const temperatures = [];
898
- const used = [];
1181
+ const stations = [];
899
1182
  for (const station of this.stations()) {
900
1183
  const place = this.locate(station);
901
1184
  if (!place) continue;
902
1185
  const snapshot = this.snapshotAt(station, at, options);
903
1186
  if (!snapshot) continue;
904
- used.push(station);
1187
+ stations.push(station);
905
1188
  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
- });
1189
+ winds.push({ lat: place.lat, lon: place.lon, speedKt: state.wind.speedKt, directionDeg: state.wind.fromDeg });
912
1190
  clouds.push({ lat: place.lat, lon: place.lon, value: state.clouds.total * 100 });
913
1191
  rains.push({
914
1192
  lat: place.lat,
@@ -920,15 +1198,7 @@ var WeatherStore = class {
920
1198
  temperatures.push({ lat: place.lat, lon: place.lon, value: observed.temperatureC });
921
1199
  }
922
1200
  }
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
- };
1201
+ return { stations, winds, clouds, rains, temperatures };
932
1202
  }
933
1203
  // ---- internal -------------------------------------------------------------
934
1204
  changed() {
@@ -990,6 +1260,8 @@ function decodeWeatherMessage(message, reference = /* @__PURE__ */ new Date()) {
990
1260
  return reports;
991
1261
  }
992
1262
  export {
1263
+ WEATHER_GRID_STANDARD,
1264
+ WEATHER_GRID_VERSION,
993
1265
  WeatherStore,
994
1266
  animationFromConditions,
995
1267
  animationSnapshot,
@@ -1002,19 +1274,27 @@ export {
1002
1274
  extractReports,
1003
1275
  forecastAt,
1004
1276
  interpolateScalar,
1277
+ latticePoints,
1005
1278
  mergeConditions,
1279
+ nudgeScalar,
1280
+ nudgeWind,
1006
1281
  parseConditionTokens,
1007
1282
  parseGrib2Json,
1008
1283
  parseGridJson,
1009
1284
  parseMetar,
1010
1285
  parseTaf,
1286
+ parseWeatherGridSeries,
1011
1287
  pointOf,
1012
1288
  resolveDayTime,
1013
1289
  sampleScalar,
1014
1290
  sampleWind,
1291
+ serializeWeatherGridSeries,
1292
+ seriesFieldsAt,
1293
+ seriesRange,
1015
1294
  speedOf,
1016
1295
  stationWindsFromMetars,
1017
1296
  stationWindsFromModelPoints,
1018
1297
  uvFromSpeedDirection,
1298
+ weatherGridFromOpenMeteo,
1019
1299
  windGridFromStations
1020
1300
  };
@@ -0,0 +1,40 @@
1
+ import { type WeatherGridSeries } from "./gridSeries.js";
2
+ /**
3
+ * Adaptor respons layanan model bergaya Open-Meteo menjadi standar `aeroswarm.weather-grid`. Murni:
4
+ * tidak memanggil jaringan. Pemanggilnya (aplikasi) yang mengambil datanya.
5
+ *
6
+ * Titik diminta sebagai kisi beraturan (lihat `latticePoints`), jadi responsnya langsung menjadi
7
+ * grid tanpa interpolasi.
8
+ */
9
+ export interface Lattice {
10
+ west: number;
11
+ south: number;
12
+ east: number;
13
+ north: number;
14
+ nx: number;
15
+ ny: number;
16
+ }
17
+ /**
18
+ * Titik-titik kisi, baris demi baris dari utara ke selatan, kolom dari barat ke timur. Urutan ini
19
+ * sama dengan urutan respons banyak lokasi, dan dengan urutan sel pada grid standar.
20
+ */
21
+ export declare function latticePoints(lattice: Lattice): {
22
+ latitude: number[];
23
+ longitude: number[];
24
+ };
25
+ declare const SPEED_TO_MS: Record<string, number>;
26
+ export interface OpenMeteoOptions {
27
+ /** Satuan kecepatan angin di respons. Bawaan `m/s` (minta dengan `wind_speed_unit=ms`). */
28
+ speedUnit?: keyof typeof SPEED_TO_MS;
29
+ model?: string;
30
+ runAt?: Date;
31
+ generatedAt?: Date;
32
+ }
33
+ /**
34
+ * Susun seri standar dari respons banyak lokasi (satu elemen per titik kisi, urutan `latticePoints`).
35
+ * Meminta `hourly=wind_speed_10m,wind_direction_10m,cloud_cover,precipitation,temperature_2m`,
36
+ * `timezone=GMT`, dan `wind_speed_unit=ms`.
37
+ */
38
+ export declare function weatherGridFromOpenMeteo(response: unknown, lattice: Lattice, options?: OpenMeteoOptions): WeatherGridSeries;
39
+ export {};
40
+ //# sourceMappingURL=openMeteo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openMeteo.d.ts","sourceRoot":"","sources":["../src/openMeteo.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;GAMG;AAEH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAW3F;AAID,QAAA,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAA6D,CAAC;AAEtG,MAAM,WAAW,gBAAgB;IAC/B,2FAA2F;IAC3F,SAAS,CAAC,EAAE,MAAM,OAAO,WAAW,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,WAAW,CAAC,EAAE,IAAI,CAAC;CACpB;AAiBD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,gBAAqB,GAC7B,iBAAiB,CAyDnB"}
package/dist/store.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type AnimationSnapshot } from "./animation.js";
2
- import { type GeoPoint, type GridBounds, type ScalarGrid, type WindGrid } from "./grid.js";
2
+ import { type GeoPoint, type GridBounds, type NudgeOptions, type ScalarGrid, type WindGrid } from "./grid.js";
3
3
  import type { TafChange } from "./types.js";
4
4
  export type Locate = (icao: string) => GeoPoint | undefined;
5
5
  export interface IngestOptions {
@@ -41,6 +41,16 @@ export interface WeatherFields {
41
41
  /** Suhu (°C); null bila tidak ada pengamatan bersuhu. */
42
42
  temperature: ScalarGrid | null;
43
43
  }
44
+ /** Medan dengan besaran yang boleh tidak ada (mis. seri model tanpa data suhu). */
45
+ export interface FieldSet {
46
+ at: Date;
47
+ /** Sumber yang dipakai: stasiun, atau nama model diikuti stasiun yang menyesuaikannya. */
48
+ stations: string[];
49
+ wind: WindGrid | null;
50
+ cloud: ScalarGrid | null;
51
+ rain: ScalarGrid | null;
52
+ temperature: ScalarGrid | null;
53
+ }
44
54
  export interface FieldGridOptions {
45
55
  bounds: GridBounds;
46
56
  nx: number;
@@ -82,6 +92,16 @@ export declare class WeatherStore {
82
92
  fieldsAt(at: Date, grid: FieldGridOptions, options?: {
83
93
  includeProbable?: boolean;
84
94
  }): WeatherFields | null;
95
+ /**
96
+ * Gabungkan medan model dengan laporan: grid model disesuaikan ke pengamatan (METAR/SPECI) atau
97
+ * prakiraan (TAF) di stasiun yang punya koordinat, dalam jarak `radiusKm` dari tiap stasiun. Tanpa
98
+ * stasiun yang bisa dipakai, medan model dikembalikan apa adanya.
99
+ */
100
+ blendInto(model: Omit<FieldSet, "stations" | "at">, modelName: string, at: Date, options?: NudgeOptions & {
101
+ includeProbable?: boolean;
102
+ }): FieldSet;
103
+ /** Nilai tiap stasiun yang punya koordinat dan data pada waktu `at`. */
104
+ private pointsAt;
85
105
  private changed;
86
106
  private add;
87
107
  private latest;
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeroswarm/weather",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -13,7 +13,8 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
- "dist"
16
+ "dist",
17
+ "GRID_STANDARD.md"
17
18
  ],
18
19
  "publishConfig": {
19
20
  "access": "public",