@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 +13 -0
- package/dist/grid.d.ts +116 -0
- package/dist/grid.d.ts.map +1 -0
- package/dist/grid.test.d.ts +2 -0
- package/dist/grid.test.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +437 -1
- package/dist/store.d.ts +91 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.test.d.ts +2 -0
- package/dist/store.test.d.ts.map +1 -0
- package/package.json +1 -1
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 @@
|
|
|
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
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
-
|
|
1011
|
+
pointOf,
|
|
1012
|
+
resolveDayTime,
|
|
1013
|
+
sampleScalar,
|
|
1014
|
+
sampleWind,
|
|
1015
|
+
speedOf,
|
|
1016
|
+
stationWindsFromMetars,
|
|
1017
|
+
stationWindsFromModelPoints,
|
|
1018
|
+
uvFromSpeedDirection,
|
|
1019
|
+
windGridFromStations
|
|
584
1020
|
};
|
package/dist/store.d.ts
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"store.test.d.ts","sourceRoot":"","sources":["../src/store.test.ts"],"names":[],"mappings":""}
|