@aeroswarm/weather 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +222 -1
- 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
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,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,213 @@ 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
|
+
|
|
562
769
|
// src/index.ts
|
|
563
770
|
function decodeWeatherMessage(message, reference = /* @__PURE__ */ new Date()) {
|
|
564
771
|
const reports = [];
|
|
@@ -572,13 +779,27 @@ export {
|
|
|
572
779
|
animationFromConditions,
|
|
573
780
|
animationSnapshot,
|
|
574
781
|
blendConditions,
|
|
782
|
+
blendWindGrids,
|
|
783
|
+
decodeGridJson,
|
|
575
784
|
decodeWeatherMessage,
|
|
785
|
+
directionFromUV,
|
|
576
786
|
emptyConditions,
|
|
577
787
|
extractReports,
|
|
578
788
|
forecastAt,
|
|
789
|
+
interpolateScalar,
|
|
579
790
|
mergeConditions,
|
|
580
791
|
parseConditionTokens,
|
|
792
|
+
parseGrib2Json,
|
|
793
|
+
parseGridJson,
|
|
581
794
|
parseMetar,
|
|
582
795
|
parseTaf,
|
|
583
|
-
|
|
796
|
+
pointOf,
|
|
797
|
+
resolveDayTime,
|
|
798
|
+
sampleScalar,
|
|
799
|
+
sampleWind,
|
|
800
|
+
speedOf,
|
|
801
|
+
stationWindsFromMetars,
|
|
802
|
+
stationWindsFromModelPoints,
|
|
803
|
+
uvFromSpeedDirection,
|
|
804
|
+
windGridFromStations
|
|
584
805
|
};
|