@aeroswarm/weather 0.1.2 → 0.1.4

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
+
@@ -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,8 +3,11 @@ 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";
10
+ export * from "./store.js";
8
11
  export * from "./time.js";
9
12
  export * from "./types.js";
10
13
  /**
@@ -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,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
@@ -688,10 +688,10 @@ function parseGrib2Json(input) {
688
688
  v: orient(vRecord.data)
689
689
  };
690
690
  }
691
- function stationWindsFromMetars(reports, locate) {
691
+ function stationWindsFromMetars(reports, locate2) {
692
692
  const out = [];
693
693
  for (const report of reports) {
694
- const place = locate(report.station);
694
+ const place = locate2(report.station);
695
695
  if (!place) continue;
696
696
  const { wind } = report.conditions;
697
697
  out.push({
@@ -766,6 +766,428 @@ function decodeGridJson(input, fallback) {
766
766
  throw new Error("Format medan tidak dikenal: harus grib2json, JSON grid {bounds,nx,ny,u,v|values}, atau respons banyak lokasi");
767
767
  }
768
768
 
769
+ // src/gridSeries.ts
770
+ var WEATHER_GRID_STANDARD = "aeroswarm.weather-grid";
771
+ var WEATHER_GRID_VERSION = 1;
772
+ var QUANTITIES = ["windU", "windV", "cloud", "rain", "temperature"];
773
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
774
+ function parseDate(value, where) {
775
+ const date = typeof value === "string" ? new Date(value) : new Date(Number.NaN);
776
+ if (Number.isNaN(date.getTime())) throw new Error(`${where}: bukan tanggal ISO 8601 yang valid`);
777
+ return date;
778
+ }
779
+ function parseWeatherGridSeries(input) {
780
+ if (!isRecord2(input)) throw new Error("Bukan objek JSON");
781
+ if (input.standard !== WEATHER_GRID_STANDARD) {
782
+ throw new Error(`standard harus "${WEATHER_GRID_STANDARD}"`);
783
+ }
784
+ if (input.version !== WEATHER_GRID_VERSION) {
785
+ throw new Error(`version ${String(input.version)} tidak didukung (didukung: ${WEATHER_GRID_VERSION})`);
786
+ }
787
+ const src = isRecord2(input.source) ? input.source : null;
788
+ if (!src || typeof src.name !== "string" || src.name === "") throw new Error("source.name wajib diisi");
789
+ const source = {
790
+ name: src.name,
791
+ model: typeof src.model === "string" ? src.model : void 0,
792
+ runAt: src.runAt === void 0 ? void 0 : parseDate(src.runAt, "source.runAt"),
793
+ generatedAt: src.generatedAt === void 0 ? void 0 : parseDate(src.generatedAt, "source.generatedAt")
794
+ };
795
+ const grid = isRecord2(input.grid) ? input.grid : null;
796
+ if (!grid) throw new Error("grid wajib diisi");
797
+ const { west, south, east, north } = grid;
798
+ const nx = Number(grid.nx);
799
+ const ny = Number(grid.ny);
800
+ if (![west, south, east, north].every((n) => typeof n === "number" && Number.isFinite(n))) {
801
+ throw new Error("grid.west/south/east/north harus angka");
802
+ }
803
+ if (!Number.isInteger(nx) || !Number.isInteger(ny) || nx < 2 || ny < 2) {
804
+ throw new Error("grid.nx dan grid.ny harus bilangan bulat >= 2");
805
+ }
806
+ const bounds = { west, south, east, north };
807
+ if (bounds.west >= bounds.east || bounds.south >= bounds.north) {
808
+ throw new Error("grid: west < east dan south < north");
809
+ }
810
+ if (!Array.isArray(input.times) || input.times.length === 0) throw new Error("times wajib berisi minimal satu waktu");
811
+ const times = input.times.map((t, i) => parseDate(t, `times[${i}]`));
812
+ for (let i = 1; i < times.length; i++) {
813
+ if (times[i].getTime() <= times[i - 1].getTime()) {
814
+ throw new Error(`times[${i}] harus lebih baru dari times[${i - 1}]`);
815
+ }
816
+ }
817
+ const rawFields = isRecord2(input.fields) ? input.fields : null;
818
+ if (!rawFields) throw new Error("fields wajib diisi");
819
+ const fields = {};
820
+ for (const quantity of QUANTITIES) {
821
+ const frames = rawFields[quantity];
822
+ if (frames === void 0) continue;
823
+ if (!Array.isArray(frames) || frames.length !== times.length) {
824
+ throw new Error(`fields.${quantity} harus berisi ${times.length} bingkai (satu per waktu)`);
825
+ }
826
+ fields[quantity] = frames.map((frame, t) => {
827
+ if (!Array.isArray(frame) || frame.length !== nx * ny) {
828
+ throw new Error(`fields.${quantity}[${t}] harus berisi ${nx * ny} nilai (nx * ny)`);
829
+ }
830
+ return Float32Array.from(frame, (v) => typeof v === "number" && Number.isFinite(v) ? v : Number.NaN);
831
+ });
832
+ }
833
+ if (fields.windU === void 0 !== (fields.windV === void 0)) {
834
+ throw new Error("windU dan windV harus ada berdua");
835
+ }
836
+ if (Object.keys(fields).length === 0) throw new Error("fields tidak berisi satu besaran pun");
837
+ return { source, bounds, nx, ny, times, fields };
838
+ }
839
+ function serializeWeatherGridSeries(series) {
840
+ const fields = {};
841
+ for (const quantity of QUANTITIES) {
842
+ const frames = series.fields[quantity];
843
+ if (!frames) continue;
844
+ fields[quantity] = frames.map(
845
+ (frame) => Array.from(frame, (v) => Number.isNaN(v) ? null : Math.round(v * 1e3) / 1e3)
846
+ );
847
+ }
848
+ return {
849
+ standard: WEATHER_GRID_STANDARD,
850
+ version: WEATHER_GRID_VERSION,
851
+ source: {
852
+ name: series.source.name,
853
+ ...series.source.model ? { model: series.source.model } : {},
854
+ ...series.source.runAt ? { runAt: series.source.runAt.toISOString() } : {},
855
+ ...series.source.generatedAt ? { generatedAt: series.source.generatedAt.toISOString() } : {}
856
+ },
857
+ grid: { ...series.bounds, nx: series.nx, ny: series.ny },
858
+ times: series.times.map((t) => t.toISOString()),
859
+ fields
860
+ };
861
+ }
862
+ function seriesRange(series) {
863
+ return {
864
+ from: series.times[0],
865
+ to: series.times[series.times.length - 1]
866
+ };
867
+ }
868
+ function locate(times, at) {
869
+ const ms = at.getTime();
870
+ const first = times[0].getTime();
871
+ const last = times[times.length - 1].getTime();
872
+ if (ms < first || ms > last) return null;
873
+ for (let i = 1; i < times.length; i++) {
874
+ const hi = times[i].getTime();
875
+ if (ms <= hi) {
876
+ const lo = times[i - 1].getTime();
877
+ return { a: i - 1, b: i, t: (ms - lo) / (hi - lo || 1) };
878
+ }
879
+ }
880
+ return { a: times.length - 1, b: times.length - 1, t: 0 };
881
+ }
882
+ var mixFrames = (frames, a, b, t) => Float32Array.from(frames[a], (value, i) => value + (frames[b][i] - value) * t);
883
+ function seriesFieldsAt(series, at) {
884
+ const where = locate(series.times, at);
885
+ if (!where) return null;
886
+ const { bounds, nx, ny } = series;
887
+ const scalar = (quantity) => {
888
+ const frames = series.fields[quantity];
889
+ return frames ? { bounds, nx, ny, values: mixFrames(frames, where.a, where.b, where.t) } : null;
890
+ };
891
+ const u = series.fields.windU;
892
+ const v = series.fields.windV;
893
+ let wind = null;
894
+ if (u && v) {
895
+ const from = { bounds, nx, ny, u: u[where.a], v: v[where.a] };
896
+ const to = { bounds, nx, ny, u: u[where.b], v: v[where.b] };
897
+ wind = where.a === where.b ? from : blendWindGrids(from, to, where.t);
898
+ }
899
+ return {
900
+ at,
901
+ wind,
902
+ cloud: scalar("cloud"),
903
+ rain: scalar("rain"),
904
+ temperature: scalar("temperature")
905
+ };
906
+ }
907
+
908
+ // src/openMeteo.ts
909
+ function latticePoints(lattice) {
910
+ const latitude = [];
911
+ const longitude = [];
912
+ for (let row = 0; row < lattice.ny; row++) {
913
+ const lat = lattice.north - (lattice.north - lattice.south) * row / (lattice.ny - 1);
914
+ for (let col = 0; col < lattice.nx; col++) {
915
+ latitude.push(round(lat));
916
+ longitude.push(round(lattice.west + (lattice.east - lattice.west) * col / (lattice.nx - 1)));
917
+ }
918
+ }
919
+ return { latitude, longitude };
920
+ }
921
+ var round = (value) => Math.round(value * 1e4) / 1e4;
922
+ var SPEED_TO_MS = { "m/s": 1, "km/h": 1 / 3.6, kn: 0.514444, mph: 0.44704 };
923
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
924
+ var toUtc = (iso) => new Date(/[zZ]|[+-]\d\d:?\d\d$/.test(iso) ? iso : `${iso}Z`);
925
+ function weatherGridFromOpenMeteo(response, lattice, options = {}) {
926
+ const items = Array.isArray(response) ? response : [response];
927
+ const expected = lattice.nx * lattice.ny;
928
+ if (items.length !== expected) {
929
+ throw new Error(`Respons berisi ${items.length} titik, kisi memerlukan ${expected} (${lattice.nx} x ${lattice.ny})`);
930
+ }
931
+ const hourly = items.map((item, i) => {
932
+ const h = isRecord3(item) && isRecord3(item.hourly) ? item.hourly : null;
933
+ if (!h || !Array.isArray(h.time)) throw new Error(`Titik ${i}: tidak ada data hourly.time`);
934
+ return h;
935
+ });
936
+ const timesIso = hourly[0].time;
937
+ if (timesIso.length === 0) throw new Error("hourly.time kosong");
938
+ for (const [i, h] of hourly.entries()) {
939
+ if (h.time.length !== timesIso.length) throw new Error(`Titik ${i}: jumlah waktu berbeda dari titik pertama`);
940
+ }
941
+ const times = timesIso.map(toUtc);
942
+ const toMs = SPEED_TO_MS[options.speedUnit ?? "m/s"] ?? 1;
943
+ const frame = (pick) => times.map((_, t) => Float32Array.from(hourly, (h) => pick(h, t)));
944
+ const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : Number.NaN;
945
+ const fields = {};
946
+ const has = (key) => hourly.every((h) => Array.isArray(h[key]));
947
+ if (has("wind_speed_10m") && has("wind_direction_10m")) {
948
+ const uv = (h, t) => {
949
+ const speed = num(h.wind_speed_10m?.[t]) * toMs;
950
+ const dir = num(h.wind_direction_10m?.[t]);
951
+ return Number.isNaN(speed) || Number.isNaN(dir) ? { u: Number.NaN, v: Number.NaN } : uvFromSpeedDirection(speed, dir);
952
+ };
953
+ fields.windU = frame((h, t) => uv(h, t).u);
954
+ fields.windV = frame((h, t) => uv(h, t).v);
955
+ }
956
+ if (has("cloud_cover")) fields.cloud = frame((h, t) => num(h.cloud_cover?.[t]));
957
+ if (has("precipitation")) fields.rain = frame((h, t) => num(h.precipitation?.[t]));
958
+ if (has("temperature_2m")) fields.temperature = frame((h, t) => num(h.temperature_2m?.[t]));
959
+ if (Object.keys(fields).length === 0) {
960
+ throw new Error("Respons tidak memuat wind_speed_10m, wind_direction_10m, cloud_cover, precipitation, atau temperature_2m");
961
+ }
962
+ return {
963
+ source: {
964
+ name: "open-meteo",
965
+ model: options.model,
966
+ runAt: options.runAt,
967
+ generatedAt: options.generatedAt ?? /* @__PURE__ */ new Date()
968
+ },
969
+ bounds: { west: lattice.west, south: lattice.south, east: lattice.east, north: lattice.north },
970
+ nx: lattice.nx,
971
+ ny: lattice.ny,
972
+ times,
973
+ fields
974
+ };
975
+ }
976
+
977
+ // src/store.ts
978
+ var MAX_OBSERVATIONS_PER_STATION = 48;
979
+ var MAX_FORECASTS_PER_STATION = 8;
980
+ var rainRate = (intensity) => intensity * intensity * 15;
981
+ var hhmm = (d) => `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`;
982
+ var WeatherStore = class {
983
+ constructor(locate2) {
984
+ this.locate = locate2;
985
+ }
986
+ locate;
987
+ observations = /* @__PURE__ */ new Map();
988
+ forecasts = /* @__PURE__ */ new Map();
989
+ listeners = /* @__PURE__ */ new Set();
990
+ /** Naik tiap data berubah; berguna untuk memicu render ulang. */
991
+ version = 0;
992
+ /** Terima pesan mentah (laporan polos, buletin, atau pesan AFTN; boleh banyak laporan). */
993
+ ingest(message, options = {}) {
994
+ const receivedAt = options.receivedAt ?? /* @__PURE__ */ new Date();
995
+ const manual = options.manual === true;
996
+ const result = { metars: 0, tafs: 0, skipped: 0, stations: [] };
997
+ const touched = /* @__PURE__ */ new Set();
998
+ for (const raw of extractReports(message)) {
999
+ if (raw.kind === "TAF") {
1000
+ const taf = parseTaf(raw.text, receivedAt);
1001
+ if (!taf) {
1002
+ result.skipped++;
1003
+ continue;
1004
+ }
1005
+ this.add(this.forecasts, taf.station, { report: taf, manual, receivedAt }, MAX_FORECASTS_PER_STATION);
1006
+ touched.add(taf.station);
1007
+ result.tafs++;
1008
+ } else {
1009
+ const metar = parseMetar(raw.text, receivedAt);
1010
+ if (!metar) {
1011
+ result.skipped++;
1012
+ continue;
1013
+ }
1014
+ this.add(this.observations, metar.station, { report: metar, manual, receivedAt }, MAX_OBSERVATIONS_PER_STATION);
1015
+ touched.add(metar.station);
1016
+ result.metars++;
1017
+ }
1018
+ }
1019
+ result.stations = [...touched];
1020
+ if (result.metars + result.tafs > 0) this.changed();
1021
+ return result;
1022
+ }
1023
+ /** Hapus semua data manual (seluruh stasiun, atau satu stasiun). */
1024
+ clearManual(station) {
1025
+ let removed = false;
1026
+ for (const store of [this.observations, this.forecasts]) {
1027
+ for (const [icao, list] of store) {
1028
+ if (station && icao !== station) continue;
1029
+ const kept = list.filter((s) => !s.manual);
1030
+ if (kept.length !== list.length) removed = true;
1031
+ store.set(icao, kept);
1032
+ }
1033
+ }
1034
+ if (removed) this.changed();
1035
+ }
1036
+ /** Hapus semua data (otomatis dan manual). */
1037
+ clear() {
1038
+ this.observations.clear();
1039
+ this.forecasts.clear();
1040
+ this.changed();
1041
+ }
1042
+ hasManual() {
1043
+ for (const store of [this.observations, this.forecasts]) {
1044
+ for (const list of store.values()) if (list.some((s) => s.manual)) return true;
1045
+ }
1046
+ return false;
1047
+ }
1048
+ stations() {
1049
+ return [.../* @__PURE__ */ new Set([...this.observations.keys(), ...this.forecasts.keys()])].filter(
1050
+ (icao) => (this.observations.get(icao)?.length ?? 0) + (this.forecasts.get(icao)?.length ?? 0) > 0
1051
+ );
1052
+ }
1053
+ onChange(listener) {
1054
+ this.listeners.add(listener);
1055
+ return () => this.listeners.delete(listener);
1056
+ }
1057
+ /** Cuaca satu stasiun pada waktu `at`; null bila tidak ada data. */
1058
+ snapshotAt(station, at, options = {}) {
1059
+ return animationSnapshot({
1060
+ observation: this.pickObservation(station, at, options.maxObservationAgeMs),
1061
+ forecast: this.pickForecast(station, at),
1062
+ at,
1063
+ maxObservationAgeMs: options.maxObservationAgeMs,
1064
+ includeProbable: options.includeProbable
1065
+ });
1066
+ }
1067
+ /** Rentang waktu dan pembagian periode (BASE/BECMG/TEMPO/FM) dari TAF, untuk timeline. */
1068
+ timeline() {
1069
+ const segments = [];
1070
+ const observations = [];
1071
+ let from = null;
1072
+ let to = null;
1073
+ const widen = (a, b) => {
1074
+ from = from === null ? a.getTime() : Math.min(from, a.getTime());
1075
+ to = to === null ? b.getTime() : Math.max(to, b.getTime());
1076
+ };
1077
+ for (const [station, list] of this.forecasts) {
1078
+ const taf = this.latest(list, (t) => t.issuedAt);
1079
+ if (!taf || taf.cancelled) continue;
1080
+ widen(taf.validFrom, taf.validTo);
1081
+ for (const period of taf.periods) {
1082
+ if (period.change === "BASE") continue;
1083
+ const label = period.change === "FM" ? `FM ${hhmm(period.from)}` : `${period.probability ? `PROB${period.probability} ` : ""}${period.change} ${hhmm(period.from)}-${hhmm(period.to)}`;
1084
+ segments.push({ station, change: period.change, from: period.from, to: period.to, label });
1085
+ }
1086
+ }
1087
+ for (const list of this.observations.values()) {
1088
+ for (const source of list) {
1089
+ observations.push(source.report.observedAt);
1090
+ widen(source.report.observedAt, source.report.observedAt);
1091
+ }
1092
+ }
1093
+ if (from === null || to === null) return null;
1094
+ observations.sort((a, b) => a.getTime() - b.getTime());
1095
+ return { from: new Date(from), to: new Date(to), segments, observations };
1096
+ }
1097
+ /**
1098
+ * Medan angin, awan, hujan, dan suhu pada waktu `at`, dari semua stasiun yang punya data dan
1099
+ * koordinat, diinterpolasi ke grid. Mengembalikan null bila tidak ada stasiun yang bisa dipakai.
1100
+ */
1101
+ fieldsAt(at, grid, options = {}) {
1102
+ const winds = [];
1103
+ const clouds = [];
1104
+ const rains = [];
1105
+ const temperatures = [];
1106
+ const used = [];
1107
+ for (const station of this.stations()) {
1108
+ const place = this.locate(station);
1109
+ if (!place) continue;
1110
+ const snapshot = this.snapshotAt(station, at, options);
1111
+ if (!snapshot) continue;
1112
+ used.push(station);
1113
+ const { state } = snapshot;
1114
+ winds.push({
1115
+ lat: place.lat,
1116
+ lon: place.lon,
1117
+ speedKt: state.wind.speedKt,
1118
+ directionDeg: state.wind.fromDeg
1119
+ });
1120
+ clouds.push({ lat: place.lat, lon: place.lon, value: state.clouds.total * 100 });
1121
+ rains.push({
1122
+ lat: place.lat,
1123
+ lon: place.lon,
1124
+ value: state.precipitation ? rainRate(state.precipitation.intensity) : 0
1125
+ });
1126
+ const observed = this.pickObservation(station, at);
1127
+ if (observed?.temperatureC !== null && observed?.temperatureC !== void 0) {
1128
+ temperatures.push({ lat: place.lat, lon: place.lon, value: observed.temperatureC });
1129
+ }
1130
+ }
1131
+ if (used.length === 0) return null;
1132
+ return {
1133
+ at,
1134
+ stations: used,
1135
+ wind: windGridFromStations(winds, grid),
1136
+ cloud: interpolateScalar(clouds, grid),
1137
+ rain: interpolateScalar(rains, grid),
1138
+ temperature: temperatures.length > 0 ? interpolateScalar(temperatures, grid) : null
1139
+ };
1140
+ }
1141
+ // ---- internal -------------------------------------------------------------
1142
+ changed() {
1143
+ this.version++;
1144
+ for (const listener of this.listeners) listener();
1145
+ }
1146
+ add(store, station, source, limit) {
1147
+ const list = store.get(station) ?? [];
1148
+ const key = (s) => {
1149
+ const report = s.report;
1150
+ return (report.observedAt ?? report.issuedAt)?.getTime() ?? 0;
1151
+ };
1152
+ const next = list.filter((s) => !(s.manual === source.manual && key(s) === key(source)));
1153
+ next.push(source);
1154
+ next.sort((a, b) => key(a) - key(b));
1155
+ store.set(station, next.slice(-limit));
1156
+ }
1157
+ latest(list, timeOf) {
1158
+ const pool = list.some((s) => s.manual) ? list.filter((s) => s.manual) : list;
1159
+ let best = null;
1160
+ for (const source of pool) {
1161
+ if (!best || timeOf(source.report).getTime() >= timeOf(best.report).getTime()) best = source;
1162
+ }
1163
+ return best?.report ?? null;
1164
+ }
1165
+ pickObservation(station, at, maxAgeMs = 3 * 36e5) {
1166
+ const list = this.observations.get(station) ?? [];
1167
+ const usable = (s) => {
1168
+ const age = at.getTime() - s.report.observedAt.getTime();
1169
+ return age >= 0 && age <= maxAgeMs;
1170
+ };
1171
+ const manual = list.filter((s) => s.manual && usable(s));
1172
+ const pool = manual.length > 0 ? manual : list.filter(usable);
1173
+ let best = null;
1174
+ for (const source of pool) {
1175
+ if (!best || source.report.observedAt.getTime() >= best.report.observedAt.getTime()) best = source;
1176
+ }
1177
+ if (best) return best.report;
1178
+ const before = list.filter((s) => s.report.observedAt.getTime() <= at.getTime());
1179
+ return before.length > 0 ? before[before.length - 1].report : null;
1180
+ }
1181
+ pickForecast(station, at) {
1182
+ const list = this.forecasts.get(station) ?? [];
1183
+ const covers = (s) => !s.report.cancelled && at.getTime() >= s.report.validFrom.getTime() && at.getTime() < s.report.validTo.getTime();
1184
+ const manual = list.filter((s) => s.manual && covers(s));
1185
+ const pool = manual.length > 0 ? manual : list.filter(covers);
1186
+ if (pool.length === 0) return null;
1187
+ return this.latest(pool, (t) => t.issuedAt);
1188
+ }
1189
+ };
1190
+
769
1191
  // src/index.ts
770
1192
  function decodeWeatherMessage(message, reference = /* @__PURE__ */ new Date()) {
771
1193
  const reports = [];
@@ -776,6 +1198,9 @@ function decodeWeatherMessage(message, reference = /* @__PURE__ */ new Date()) {
776
1198
  return reports;
777
1199
  }
778
1200
  export {
1201
+ WEATHER_GRID_STANDARD,
1202
+ WEATHER_GRID_VERSION,
1203
+ WeatherStore,
779
1204
  animationFromConditions,
780
1205
  animationSnapshot,
781
1206
  blendConditions,
@@ -787,19 +1212,25 @@ export {
787
1212
  extractReports,
788
1213
  forecastAt,
789
1214
  interpolateScalar,
1215
+ latticePoints,
790
1216
  mergeConditions,
791
1217
  parseConditionTokens,
792
1218
  parseGrib2Json,
793
1219
  parseGridJson,
794
1220
  parseMetar,
795
1221
  parseTaf,
1222
+ parseWeatherGridSeries,
796
1223
  pointOf,
797
1224
  resolveDayTime,
798
1225
  sampleScalar,
799
1226
  sampleWind,
1227
+ serializeWeatherGridSeries,
1228
+ seriesFieldsAt,
1229
+ seriesRange,
800
1230
  speedOf,
801
1231
  stationWindsFromMetars,
802
1232
  stationWindsFromModelPoints,
803
1233
  uvFromSpeedDirection,
1234
+ weatherGridFromOpenMeteo,
804
1235
  windGridFromStations
805
1236
  };
@@ -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"}
@@ -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.2",
3
+ "version": "0.1.4",
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",