@aguacerowx/mapsgl 0.0.50 → 0.0.52
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/package.json +2 -2
- package/src/NexradWeatherController.js +27 -8
- package/src/NwsWatchesWarningsOverlay.js +922 -852
- package/src/WeatherLayerManager.js +36 -21
- package/src/nexrad/nexradArchiveCache.ts +2 -0
- package/src/nexrad/nexradLevel3Products.ts +32 -0
- package/src/nexrad/{nexradMapboxFrameOpts.js → nexradMapboxFrameOpts.ts} +38 -19
- package/src/nexrad/radarArchiveCore.bundled.js +118 -17
- package/src/nexrad/radarArchiveCore.ts +147 -29
- package/src/nwsAlertsSupport.js +68 -7
|
@@ -103,6 +103,8 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
103
103
|
...(options.watchesWarnings ?? {}),
|
|
104
104
|
};
|
|
105
105
|
this._nwsOverlay = null;
|
|
106
|
+
/** Dedupes {@link NwsWatchesWarningsOverlay#updateOptions} when only the observation timeline (slider) changes. */
|
|
107
|
+
this._nwsOptsKey = null;
|
|
106
108
|
|
|
107
109
|
/**
|
|
108
110
|
* Style layer id to insert custom weather layers below when using a non-Aguacero Mapbox style
|
|
@@ -288,8 +290,9 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
/**
|
|
291
|
-
* NWS watches/warnings: same NWWS feed as aguacero-frontend
|
|
292
|
-
*
|
|
293
|
+
* NWS watches/warnings: same NWWS feed as aguacero-frontend. Shown whenever `enabled` is true (including
|
|
294
|
+
* model-only maps — time filter uses wall clock until a satellite / MRMS / NEXRAD scrub timeline exists).
|
|
295
|
+
* By default `nwsAlertSettings.alertScope` is `'all'`. Use `'user'` with
|
|
293
296
|
* `includedAlerts` (exact NWS `event_name` strings) to show only selected products.
|
|
294
297
|
*
|
|
295
298
|
* @param {object} partial
|
|
@@ -320,8 +323,9 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
320
323
|
_syncNwsOverlay(state) {
|
|
321
324
|
const wopt = this._watchesWarningsOptions ?? {};
|
|
322
325
|
const masterEnabled = wopt.enabled === true;
|
|
323
|
-
const
|
|
324
|
-
|
|
326
|
+
const hasObservationTimeline = state.isSatellite || state.isNexrad || state.isMRMS;
|
|
327
|
+
/** Watches/warnings may run on model-only maps: use wall-clock “active now” when no sat/MRMS/NEXRAD timeline. */
|
|
328
|
+
const enabled = masterEnabled;
|
|
325
329
|
|
|
326
330
|
let timelineUnix = null;
|
|
327
331
|
/** Same list the core uses for the time slider — enables NWS “live edge” (wall clock at latest step) like aguacero-frontend. */
|
|
@@ -349,22 +353,38 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
349
353
|
? wopt.fillBeforeLayerId ?? null
|
|
350
354
|
: null;
|
|
351
355
|
|
|
352
|
-
|
|
356
|
+
const nwsAlertSettingsForOpts = {
|
|
357
|
+
...(wopt.nwsAlertSettings ?? {}),
|
|
358
|
+
...(wopt.activeOnlyRealtime !== undefined ? { activeOnlyRealtime: wopt.activeOnlyRealtime } : {}),
|
|
359
|
+
};
|
|
360
|
+
const nwsOptsKey = JSON.stringify({
|
|
361
|
+
weatherLayerId,
|
|
362
|
+
fillBeforeLayerId,
|
|
363
|
+
lineBeforeLayerId: wopt.lineBeforeLayerId,
|
|
353
364
|
alertsBaseUrl: wopt.alertsBaseUrl,
|
|
354
365
|
alertInteractionEnabled: wopt.alertInteractionEnabled,
|
|
355
366
|
deltaDebounceMs: wopt.deltaDebounceMs,
|
|
356
367
|
fillOpacity: wopt.fillOpacity,
|
|
357
368
|
lineOpacity: wopt.lineOpacity,
|
|
358
369
|
lineWidth: wopt.lineWidth,
|
|
359
|
-
|
|
360
|
-
fillBeforeLayerId,
|
|
361
|
-
lineBeforeLayerId: wopt.lineBeforeLayerId,
|
|
362
|
-
nwsAlertSettings: {
|
|
363
|
-
...(wopt.nwsAlertSettings ?? {}),
|
|
364
|
-
...(wopt.activeOnlyRealtime !== undefined ? { activeOnlyRealtime: wopt.activeOnlyRealtime } : {}),
|
|
365
|
-
},
|
|
370
|
+
nwsAlertSettings: nwsAlertSettingsForOpts,
|
|
366
371
|
});
|
|
367
|
-
this.
|
|
372
|
+
if (this._nwsOptsKey !== nwsOptsKey) {
|
|
373
|
+
this._nwsOptsKey = nwsOptsKey;
|
|
374
|
+
this._nwsOverlay.updateOptions({
|
|
375
|
+
alertsBaseUrl: wopt.alertsBaseUrl,
|
|
376
|
+
alertInteractionEnabled: wopt.alertInteractionEnabled,
|
|
377
|
+
deltaDebounceMs: wopt.deltaDebounceMs,
|
|
378
|
+
fillOpacity: wopt.fillOpacity,
|
|
379
|
+
lineOpacity: wopt.lineOpacity,
|
|
380
|
+
lineWidth: wopt.lineWidth,
|
|
381
|
+
weatherLayerId,
|
|
382
|
+
fillBeforeLayerId,
|
|
383
|
+
lineBeforeLayerId: wopt.lineBeforeLayerId,
|
|
384
|
+
nwsAlertSettings: nwsAlertSettingsForOpts,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
this._nwsOverlay.syncWithMode({ enabled, timelineUnix, timelineTimes, hasObservationTimeline });
|
|
368
388
|
}
|
|
369
389
|
|
|
370
390
|
/**
|
|
@@ -745,17 +765,12 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
745
765
|
async setNexradProduct(product) {
|
|
746
766
|
return this.core.setNexradProduct(product);
|
|
747
767
|
}
|
|
748
|
-
/** @param {'level2'|'level3'} dataSource @param {string} product */
|
|
749
|
-
async setNexradProductMode(dataSource, product) {
|
|
750
|
-
return this.core.setNexradProductMode(dataSource, product);
|
|
751
|
-
}
|
|
752
|
-
/** @param {'level2'|'level3'} source */
|
|
753
|
-
async setNexradDataSource(source) {
|
|
754
|
-
return this.core.setNexradDataSource(source);
|
|
755
|
-
}
|
|
756
768
|
async setNexradTilt(tilt) {
|
|
757
769
|
return this.core.setNexradTilt(tilt);
|
|
758
770
|
}
|
|
771
|
+
async setNexradStormRelative(enabled) {
|
|
772
|
+
return this.core.setNexradStormRelative(enabled);
|
|
773
|
+
}
|
|
759
774
|
async setNexradTimestamp(timestamp) {
|
|
760
775
|
return this.core.setNexradTimestamp(timestamp);
|
|
761
776
|
}
|
|
@@ -19,6 +19,8 @@ export type DecodedRadarFrame = {
|
|
|
19
19
|
valueScale: number;
|
|
20
20
|
valueOffset: number;
|
|
21
21
|
rayBoundariesDeg: Float32Array;
|
|
22
|
+
/** Nyquist velocity (m/s) from Level-II archive header; used for VEL readout (aguacero-frontend parity). */
|
|
23
|
+
embeddedNyquistMs?: number | null;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
// ─── Cache storage ────────────────────────────────────────────────────────────
|
|
@@ -195,6 +195,12 @@ export const NEXRAD_LEVEL3_DHC_PRODUCTS: readonly string[] = [
|
|
|
195
195
|
const NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST: readonly string[] = NEXRAD_LEVEL3_KDP_PRODUCTS.slice(3);
|
|
196
196
|
const NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST: readonly string[] = NEXRAD_LEVEL3_DHC_PRODUCTS.slice(3);
|
|
197
197
|
|
|
198
|
+
/** Super-res base radial velocity (154): S3 mnemonic per elevation (ICD order, low → high). */
|
|
199
|
+
export const NEXRAD_LEVEL3_VELOCITY_PRODUCTS: readonly string[] = [
|
|
200
|
+
'NXG', 'NYG', 'NZG', 'N0G', 'NAG', 'N1G', 'NBG', 'N2G', 'N3G',
|
|
201
|
+
];
|
|
202
|
+
const NEXRAD_LEVEL3_VELOCITY_PRODUCTS_FOR_MANIFEST: readonly string[] = NEXRAD_LEVEL3_VELOCITY_PRODUCTS.slice(3);
|
|
203
|
+
|
|
198
204
|
/**
|
|
199
205
|
* Level-III KDP / digital HC (N0H): one S3 mnemonic per manifest slot in
|
|
200
206
|
* {@link NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST} / {@link NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST}.
|
|
@@ -207,6 +213,32 @@ export function getNexradLevel3RadarTilts(siteId: string, radarVariable: string)
|
|
|
207
213
|
return getRadarTilts(siteId, radarVariable).slice(0, NEXRAD_LEVEL3_MANIFEST_TILT_COUNT);
|
|
208
214
|
}
|
|
209
215
|
|
|
216
|
+
/** Tilts for super-res L3 velocity (N*G), same manifest cap as KDP/N0H. */
|
|
217
|
+
export function getNexradLevel3RadarTiltsForVelocity(siteId: string): number[] {
|
|
218
|
+
return getRadarTilts(siteId, 'VEL').slice(0, NEXRAD_LEVEL3_MANIFEST_TILT_COUNT);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Map site tilt to L3 super-res base velocity mnemonic (N0G, NAG, …) for Unidata S3 keys / stable GPU layout — aguacero-frontend parity.
|
|
223
|
+
*/
|
|
224
|
+
export function nexradLevel3S3VelocityProductForSiteTilt(siteId: string, tilt: number): string {
|
|
225
|
+
const products = NEXRAD_LEVEL3_VELOCITY_PRODUCTS_FOR_MANIFEST;
|
|
226
|
+
const tilts = getNexradLevel3RadarTiltsForVelocity(siteId);
|
|
227
|
+
if (!tilts.length) {
|
|
228
|
+
return products[0]!;
|
|
229
|
+
}
|
|
230
|
+
const clamped = clampNexradTiltForVariable(siteId, 'VEL', tilt);
|
|
231
|
+
let idx = tilts.indexOf(clamped);
|
|
232
|
+
if (idx === -1) {
|
|
233
|
+
idx = tilts.reduce(
|
|
234
|
+
(bestI, t, i) => (Math.abs(t - clamped) < Math.abs(tilts[bestI]! - clamped) ? i : bestI),
|
|
235
|
+
0,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
idx = Math.min(idx, products.length - 1);
|
|
239
|
+
return products[idx]!;
|
|
240
|
+
}
|
|
241
|
+
|
|
210
242
|
export function nexradLevel3IsTiltIndexedKdpProduct(product: string): boolean {
|
|
211
243
|
return NEXRAD_LEVEL3_KDP_PRODUCTS.includes(product);
|
|
212
244
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MapboxRadarLayer upload options for NEXRAD — matches aguacero-frontend `RadarLayer` / MapboxRadarLayer docs:
|
|
3
|
-
* Level-III KDP/N0H
|
|
4
|
-
* Other Level-III radials
|
|
3
|
+
* Level-III KDP/N0H/VEL use {@link geometryLayoutKey} so frames use canonical azimuth bins (stable mesh; fast scrub).
|
|
4
|
+
* Other Level-III radials use ray-boundary-keyed geometry when layout cannot be fixed to a manifest product.
|
|
5
5
|
*/
|
|
6
6
|
import { clampNexradTiltForVariable, getDefaultRadarTilt, getRadarTilts } from '@aguacerowx/javascript-sdk';
|
|
7
|
+
import { nexradLevel3S3VelocityProductForSiteTilt } from './nexradLevel3Products.js';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
export type MapboxRadarFrameUploadOptions = {
|
|
10
|
+
geometryLayoutKey?: string;
|
|
11
|
+
geometryCacheKeysRayBoundaries?: boolean;
|
|
12
|
+
};
|
|
9
13
|
|
|
10
14
|
const NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST = [
|
|
11
15
|
'N0K',
|
|
@@ -27,12 +31,12 @@ const NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST = [
|
|
|
27
31
|
|
|
28
32
|
const NEXRAD_LEVEL3_MANIFEST_TILT_COUNT = NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST.length;
|
|
29
33
|
|
|
30
|
-
function nearestTiltInSortedList(tilts, target) {
|
|
34
|
+
function nearestTiltInSortedList(tilts: number[], target: number): number {
|
|
31
35
|
if (!tilts.length) return target;
|
|
32
|
-
let best = tilts[0]
|
|
36
|
+
let best = tilts[0]!;
|
|
33
37
|
let bestD = Math.abs(best - target);
|
|
34
38
|
for (let i = 1; i < tilts.length; i++) {
|
|
35
|
-
const t = tilts[i]
|
|
39
|
+
const t = tilts[i]!;
|
|
36
40
|
const d = Math.abs(t - target);
|
|
37
41
|
if (d < bestD || (d === bestD && t < best)) {
|
|
38
42
|
best = t;
|
|
@@ -42,16 +46,16 @@ function nearestTiltInSortedList(tilts, target) {
|
|
|
42
46
|
return best;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
function getNexradLevel3RadarTilts(siteId, radarVariable) {
|
|
49
|
+
function getNexradLevel3RadarTilts(siteId: string, radarVariable: string): number[] {
|
|
46
50
|
return getRadarTilts(siteId, radarVariable).slice(0, NEXRAD_LEVEL3_MANIFEST_TILT_COUNT);
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
export function nexradLevel3UsesTiltIndexedS3Products(radarVariable) {
|
|
53
|
+
export function nexradLevel3UsesTiltIndexedS3Products(radarVariable: string | undefined): boolean {
|
|
50
54
|
const v = radarVariable || '';
|
|
51
55
|
return v === 'KDP' || v === 'N0H';
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
function clampTiltForLevel3CompositeKey(siteId, radarVariable, tilt) {
|
|
58
|
+
function clampTiltForLevel3CompositeKey(siteId: string, radarVariable: string, tilt: number): number {
|
|
55
59
|
if (!nexradLevel3UsesTiltIndexedS3Products(radarVariable)) {
|
|
56
60
|
return clampNexradTiltForVariable(siteId, radarVariable, tilt);
|
|
57
61
|
}
|
|
@@ -63,34 +67,43 @@ function clampTiltForLevel3CompositeKey(siteId, radarVariable, tilt) {
|
|
|
63
67
|
|
|
64
68
|
/**
|
|
65
69
|
* Map site tilt to Level-III S3 product mnemonic (N0K, NAK, … / N0H, …) — aguacero-frontend parity.
|
|
66
|
-
* @param {string} siteId
|
|
67
|
-
* @param {'KDP' | 'N0H'} radarVariable
|
|
68
|
-
* @param {number} tilt
|
|
69
70
|
*/
|
|
70
|
-
export function nexradLevel3S3ProductForSiteTilt(
|
|
71
|
+
export function nexradLevel3S3ProductForSiteTilt(
|
|
72
|
+
siteId: string,
|
|
73
|
+
radarVariable: 'KDP' | 'N0H',
|
|
74
|
+
tilt: number,
|
|
75
|
+
): string {
|
|
71
76
|
const products =
|
|
72
77
|
radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST : NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST;
|
|
73
78
|
const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
|
|
74
79
|
if (!tilts.length) {
|
|
75
|
-
return products[0]
|
|
80
|
+
return products[0]!;
|
|
76
81
|
}
|
|
77
82
|
const clamped = clampTiltForLevel3CompositeKey(siteId, radarVariable, tilt);
|
|
78
83
|
let idx = tilts.indexOf(clamped);
|
|
79
84
|
if (idx === -1) {
|
|
80
85
|
idx = tilts.reduce(
|
|
81
|
-
(bestI, t, i) => (Math.abs(t - clamped) < Math.abs(tilts[bestI] - clamped) ? i : bestI),
|
|
86
|
+
(bestI, t, i) => (Math.abs(t - clamped) < Math.abs(tilts[bestI]! - clamped) ? i : bestI),
|
|
82
87
|
0,
|
|
83
88
|
);
|
|
84
89
|
}
|
|
85
90
|
idx = Math.min(idx, products.length - 1);
|
|
86
|
-
return products[idx]
|
|
91
|
+
return products[idx]!;
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
type NexradStateForMapbox = {
|
|
95
|
+
nexradDataSource?: string;
|
|
96
|
+
nexradSite?: string | null;
|
|
97
|
+
nexradProduct?: string | null;
|
|
98
|
+
nexradTilt?: number | null;
|
|
99
|
+
};
|
|
100
|
+
|
|
89
101
|
/**
|
|
90
|
-
* @param
|
|
91
|
-
* @returns {MapboxRadarFrameUploadOptions | undefined}
|
|
102
|
+
* @param state - AguaceroCore state (or state:change payload) with NEXRAD fields
|
|
92
103
|
*/
|
|
93
|
-
export function mapboxFrameUploadOptionsForNexradState(
|
|
104
|
+
export function mapboxFrameUploadOptionsForNexradState(
|
|
105
|
+
state: NexradStateForMapbox | null | undefined,
|
|
106
|
+
): MapboxRadarFrameUploadOptions | undefined {
|
|
94
107
|
if (!state || state.nexradDataSource !== 'level3') return undefined;
|
|
95
108
|
const site = state.nexradSite;
|
|
96
109
|
const radarVar = (state.nexradProduct || 'REF').toUpperCase();
|
|
@@ -102,5 +115,11 @@ export function mapboxFrameUploadOptionsForNexradState(state) {
|
|
|
102
115
|
return { geometryLayoutKey: `${site}|${radarVar}|${mnemonic}` };
|
|
103
116
|
}
|
|
104
117
|
|
|
118
|
+
if (radarVar === 'VEL' && site) {
|
|
119
|
+
const tilt = Number.isFinite(state.nexradTilt) ? Number(state.nexradTilt) : getDefaultRadarTilt(site);
|
|
120
|
+
const mnemonic = nexradLevel3S3VelocityProductForSiteTilt(site, tilt);
|
|
121
|
+
return { geometryLayoutKey: `${site}|${radarVar}|${mnemonic}` };
|
|
122
|
+
}
|
|
123
|
+
|
|
105
124
|
return { geometryCacheKeysRayBoundaries: true };
|
|
106
125
|
}
|
|
@@ -2779,6 +2779,18 @@ var NEXRAD_LEVEL3_DHC_PRODUCTS = [
|
|
|
2779
2779
|
];
|
|
2780
2780
|
var NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST = NEXRAD_LEVEL3_KDP_PRODUCTS.slice(3);
|
|
2781
2781
|
var NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST = NEXRAD_LEVEL3_DHC_PRODUCTS.slice(3);
|
|
2782
|
+
var NEXRAD_LEVEL3_VELOCITY_PRODUCTS = [
|
|
2783
|
+
"NXG",
|
|
2784
|
+
"NYG",
|
|
2785
|
+
"NZG",
|
|
2786
|
+
"N0G",
|
|
2787
|
+
"NAG",
|
|
2788
|
+
"N1G",
|
|
2789
|
+
"NBG",
|
|
2790
|
+
"N2G",
|
|
2791
|
+
"N3G"
|
|
2792
|
+
];
|
|
2793
|
+
var NEXRAD_LEVEL3_VELOCITY_PRODUCTS_FOR_MANIFEST = NEXRAD_LEVEL3_VELOCITY_PRODUCTS.slice(3);
|
|
2782
2794
|
var NEXRAD_LEVEL3_MANIFEST_TILT_COUNT = NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST.length;
|
|
2783
2795
|
function level3EetHeightKmFromLevel(level, dataMaskRaw, scaleRaw, offsetRaw) {
|
|
2784
2796
|
if (level < 2) return null;
|
|
@@ -3932,14 +3944,17 @@ function decodeLevel3RasterProduct(buffer, objectKey, radarVariable) {
|
|
|
3932
3944
|
return null;
|
|
3933
3945
|
}
|
|
3934
3946
|
}
|
|
3935
|
-
var GROUP2_VARS = ["
|
|
3947
|
+
var GROUP2_VARS = ["SW"];
|
|
3948
|
+
var GROUP2_COMBINED_3_VARS = ["REF", "VEL", "SW"];
|
|
3936
3949
|
var GROUP1_VARS = ["REF", "ZDR", "RHO", "PHI"];
|
|
3937
3950
|
var GROUP2_COMBINED_7_VARS = ["REF", "ZDR", "RHO", "PHI", "KDP", "VEL", "SW"];
|
|
3938
3951
|
var GROUP2_COMBINED_VARS = ["REF", "ZDR", "RHO", "PHI", "VEL", "SW"];
|
|
3939
3952
|
var FILE_HDR_BYTES = 64;
|
|
3940
|
-
var
|
|
3953
|
+
var LEVEL2_AZ_BLOCK_RAYS = 720;
|
|
3954
|
+
var LEVEL2_AZ_BLOCK_BYTES = LEVEL2_AZ_BLOCK_RAYS * 4;
|
|
3955
|
+
var LEVEL2_FILE_NYQUIST_BYTES = 4;
|
|
3941
3956
|
var SLOT_INDEX_ENTRY = 18;
|
|
3942
|
-
var MAX_SLOTS =
|
|
3957
|
+
var MAX_SLOTS = 7;
|
|
3943
3958
|
function int16ToFloat16(val) {
|
|
3944
3959
|
const sign = (val & 32768) / 32768;
|
|
3945
3960
|
const exponent = (val & 31744) / 1024;
|
|
@@ -3949,7 +3964,25 @@ function int16ToFloat16(val) {
|
|
|
3949
3964
|
}
|
|
3950
3965
|
return Math.pow(-1, sign) * Math.pow(2, exponent - 16) * (1 + fraction / Math.pow(2, 10));
|
|
3951
3966
|
}
|
|
3952
|
-
function
|
|
3967
|
+
function nexradLevel2ResourceTotalBytes(resp) {
|
|
3968
|
+
const cr = resp.headers.get("Content-Range");
|
|
3969
|
+
if (cr) {
|
|
3970
|
+
const m = cr.trim().match(/\/(\d+)\s*$/);
|
|
3971
|
+
if (m) {
|
|
3972
|
+
const n = Number(m[1]);
|
|
3973
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
if (resp.status === 200) {
|
|
3977
|
+
const cl = resp.headers.get("Content-Length");
|
|
3978
|
+
if (cl) {
|
|
3979
|
+
const n = Number(cl);
|
|
3980
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
return null;
|
|
3984
|
+
}
|
|
3985
|
+
function parseFileHeader(buffer, azBlockBytes) {
|
|
3953
3986
|
const view = new DataView(buffer);
|
|
3954
3987
|
const magic = view.getUint32(0, false);
|
|
3955
3988
|
if (magic !== 1314406980) throw new Error(`Bad magic: 0x${magic.toString(16)}`);
|
|
@@ -3960,8 +3993,15 @@ function parseFileHeader(buffer) {
|
|
|
3960
3993
|
const firstGateKm = view.getFloat32(16, false);
|
|
3961
3994
|
const gateWidthKm = view.getFloat32(20, false);
|
|
3962
3995
|
const nSlots = view.getUint16(24, false);
|
|
3963
|
-
const azimuthsBuffer = buffer.slice(FILE_HDR_BYTES, FILE_HDR_BYTES +
|
|
3964
|
-
const
|
|
3996
|
+
const azimuthsBuffer = buffer.slice(FILE_HDR_BYTES, FILE_HDR_BYTES + azBlockBytes);
|
|
3997
|
+
const azEnd = FILE_HDR_BYTES + azBlockBytes;
|
|
3998
|
+
const idxStart = azEnd + LEVEL2_FILE_NYQUIST_BYTES;
|
|
3999
|
+
const needBytes = idxStart + nSlots * SLOT_INDEX_ENTRY;
|
|
4000
|
+
if (buffer.byteLength < needBytes) {
|
|
4001
|
+
throw new Error(`level2 header buffer too small: ${buffer.byteLength} < ${needBytes}`);
|
|
4002
|
+
}
|
|
4003
|
+
const nyqCandidate = view.getFloat32(azEnd, false);
|
|
4004
|
+
const embeddedNyquistMs = Number.isFinite(nyqCandidate) && nyqCandidate > 0.5 && nyqCandidate < 128 ? nyqCandidate : null;
|
|
3965
4005
|
const slots = [];
|
|
3966
4006
|
for (let i = 0; i < nSlots; i++) {
|
|
3967
4007
|
const base = idxStart + i * SLOT_INDEX_ENTRY;
|
|
@@ -3981,7 +4021,8 @@ function parseFileHeader(buffer) {
|
|
|
3981
4021
|
gateWidthKm,
|
|
3982
4022
|
nSlots,
|
|
3983
4023
|
azimuthsBuffer,
|
|
3984
|
-
slots
|
|
4024
|
+
slots,
|
|
4025
|
+
embeddedNyquistMs
|
|
3985
4026
|
};
|
|
3986
4027
|
}
|
|
3987
4028
|
function decodeSweepInWorker(objectKey, slotBuffer, header, sites, priority, signal) {
|
|
@@ -4030,7 +4071,24 @@ function decodeSweepInWorker(objectKey, slotBuffer, header, sites, priority, sig
|
|
|
4030
4071
|
}, [slotBuffer, azCopy]);
|
|
4031
4072
|
});
|
|
4032
4073
|
}
|
|
4074
|
+
var N0S_MOTION_CACHE_MAX = 128;
|
|
4075
|
+
var n0sMotionVectorCache = /* @__PURE__ */ new Map();
|
|
4076
|
+
function getCachedN0sMotion(motionObjectKey) {
|
|
4077
|
+
return n0sMotionVectorCache.get(motionObjectKey);
|
|
4078
|
+
}
|
|
4079
|
+
function setCachedN0sMotion(motionObjectKey, motion) {
|
|
4080
|
+
n0sMotionVectorCache.set(motionObjectKey, motion);
|
|
4081
|
+
while (n0sMotionVectorCache.size > N0S_MOTION_CACHE_MAX) {
|
|
4082
|
+
const first = n0sMotionVectorCache.keys().next().value;
|
|
4083
|
+
if (first === void 0) break;
|
|
4084
|
+
n0sMotionVectorCache.delete(first);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4033
4087
|
async function applyStormMotionFromN0sObjectKey(frame, motionObjectKey, logLabel) {
|
|
4088
|
+
const cached = getCachedN0sMotion(motionObjectKey);
|
|
4089
|
+
if (cached) {
|
|
4090
|
+
return applyLevel3StormRelativeToFrame(frame, cached.speedMs, cached.directionDeg);
|
|
4091
|
+
}
|
|
4034
4092
|
const motionUrl = objectKeyToUrl(motionObjectKey, "level3");
|
|
4035
4093
|
try {
|
|
4036
4094
|
const motionResp = await fetch(motionUrl);
|
|
@@ -4038,6 +4096,7 @@ async function applyStormMotionFromN0sObjectKey(frame, motionObjectKey, logLabel
|
|
|
4038
4096
|
const motionBuf = await motionResp.arrayBuffer();
|
|
4039
4097
|
const motion = parseLevel3StormMotionFromBuffer(motionBuf);
|
|
4040
4098
|
if (motion) {
|
|
4099
|
+
setCachedN0sMotion(motionObjectKey, motion);
|
|
4041
4100
|
return applyLevel3StormRelativeToFrame(frame, motion.speedMs, motion.directionDeg);
|
|
4042
4101
|
}
|
|
4043
4102
|
}
|
|
@@ -4104,7 +4163,8 @@ async function fetchAndParseArchive(url, objectKey, radarVariable, groupId, rada
|
|
|
4104
4163
|
}
|
|
4105
4164
|
return decoded2;
|
|
4106
4165
|
}
|
|
4107
|
-
const
|
|
4166
|
+
const azBlockBytes = LEVEL2_AZ_BLOCK_BYTES;
|
|
4167
|
+
const INDEX_FETCH_BYTES = FILE_HDR_BYTES + azBlockBytes + LEVEL2_FILE_NYQUIST_BYTES + MAX_SLOTS * SLOT_INDEX_ENTRY;
|
|
4108
4168
|
const indexResp = await fetch(url, {
|
|
4109
4169
|
headers: {
|
|
4110
4170
|
"x-api-key": NEXRAD_ARCHIVE_API_KEY,
|
|
@@ -4115,12 +4175,26 @@ async function fetchAndParseArchive(url, objectKey, radarVariable, groupId, rada
|
|
|
4115
4175
|
throw new Error(`HTTP ${indexResp.status} fetching level2 index ${url}`);
|
|
4116
4176
|
}
|
|
4117
4177
|
const indexBuffer = await indexResp.arrayBuffer();
|
|
4118
|
-
const
|
|
4178
|
+
const indexResourceTotal = nexradLevel2ResourceTotalBytes(indexResp);
|
|
4179
|
+
const header = parseFileHeader(indexBuffer, azBlockBytes);
|
|
4180
|
+
const slotIndexBytes = FILE_HDR_BYTES + azBlockBytes + LEVEL2_FILE_NYQUIST_BYTES + header.nSlots * SLOT_INDEX_ENTRY;
|
|
4181
|
+
if (indexBuffer.byteLength < slotIndexBytes) {
|
|
4182
|
+
console.warn("[RadarLayer] level2 index response shorter than slot table", {
|
|
4183
|
+
objectKey,
|
|
4184
|
+
byteLength: indexBuffer.byteLength,
|
|
4185
|
+
need: slotIndexBytes,
|
|
4186
|
+
nSlots: header.nSlots
|
|
4187
|
+
});
|
|
4188
|
+
setArchiveCache(cacheKey, null);
|
|
4189
|
+
return null;
|
|
4190
|
+
}
|
|
4119
4191
|
let varList;
|
|
4120
4192
|
if (groupId === 2 && header.nSlots === 7) {
|
|
4121
4193
|
varList = GROUP2_COMBINED_7_VARS;
|
|
4122
4194
|
} else if (groupId === 2 && header.nSlots === 6) {
|
|
4123
4195
|
varList = GROUP2_COMBINED_VARS;
|
|
4196
|
+
} else if (groupId === 2 && header.nSlots === 3) {
|
|
4197
|
+
varList = GROUP2_COMBINED_3_VARS;
|
|
4124
4198
|
} else if (groupId === 2) {
|
|
4125
4199
|
varList = GROUP2_VARS;
|
|
4126
4200
|
} else {
|
|
@@ -4138,17 +4212,43 @@ async function fetchAndParseArchive(url, objectKey, radarVariable, groupId, rada
|
|
|
4138
4212
|
setArchiveCache(cacheKey, null);
|
|
4139
4213
|
return null;
|
|
4140
4214
|
}
|
|
4141
|
-
const
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4215
|
+
const slotEndExclusive = slot.offset + slot.compressedSize;
|
|
4216
|
+
if (indexResourceTotal != null && slotEndExclusive > indexResourceTotal) {
|
|
4217
|
+
console.warn("[RadarLayer] level2 slot extends past object size (bad index or stale CDN)", {
|
|
4218
|
+
objectKey,
|
|
4219
|
+
radarVariable,
|
|
4220
|
+
slotIdx,
|
|
4221
|
+
offset: slot.offset,
|
|
4222
|
+
compressedSize: slot.compressedSize,
|
|
4223
|
+
indexResourceTotal
|
|
4224
|
+
});
|
|
4225
|
+
setArchiveCache(cacheKey, null);
|
|
4226
|
+
return null;
|
|
4227
|
+
}
|
|
4228
|
+
const slotRangeEnd = slotEndExclusive - 1;
|
|
4229
|
+
const slotHeaders = {
|
|
4230
|
+
"x-api-key": NEXRAD_ARCHIVE_API_KEY,
|
|
4231
|
+
"Range": `bytes=${slot.offset}-${slotRangeEnd}`
|
|
4232
|
+
};
|
|
4233
|
+
let slotResp = await fetch(url, { headers: slotHeaders });
|
|
4234
|
+
let slotBuffer;
|
|
4235
|
+
if (slotResp.ok || slotResp.status === 206) {
|
|
4236
|
+
slotBuffer = await slotResp.arrayBuffer();
|
|
4237
|
+
} else if (slotResp.status === 416) {
|
|
4238
|
+
const fullResp = await fetch(url, { headers: { "x-api-key": NEXRAD_ARCHIVE_API_KEY } });
|
|
4239
|
+
if (!fullResp.ok) {
|
|
4240
|
+
throw new Error(`HTTP ${fullResp.status} full fetch after 416 for level2 ${url}`);
|
|
4146
4241
|
}
|
|
4147
|
-
|
|
4148
|
-
|
|
4242
|
+
const fullBuf = await fullResp.arrayBuffer();
|
|
4243
|
+
if (slot.offset >= fullBuf.byteLength || slotEndExclusive > fullBuf.byteLength) {
|
|
4244
|
+
throw new Error(
|
|
4245
|
+
`level2 slot out of bounds after 416 fallback (offset=${slot.offset}, end=${slotEndExclusive}, file=${fullBuf.byteLength}) for ${url}`
|
|
4246
|
+
);
|
|
4247
|
+
}
|
|
4248
|
+
slotBuffer = fullBuf.slice(slot.offset, slotEndExclusive);
|
|
4249
|
+
} else {
|
|
4149
4250
|
throw new Error(`HTTP ${slotResp.status} fetching level2 slot ${url}`);
|
|
4150
4251
|
}
|
|
4151
|
-
const slotBuffer = await slotResp.arrayBuffer();
|
|
4152
4252
|
const sites = await loadNexradSites();
|
|
4153
4253
|
let decoded = await decodeSweepInWorker(
|
|
4154
4254
|
objectKey,
|
|
@@ -4162,6 +4262,7 @@ async function fetchAndParseArchive(url, objectKey, radarVariable, groupId, rada
|
|
|
4162
4262
|
setArchiveCache(cacheKey, null);
|
|
4163
4263
|
return null;
|
|
4164
4264
|
}
|
|
4265
|
+
decoded = { ...decoded, embeddedNyquistMs: header.embeddedNyquistMs };
|
|
4165
4266
|
const l2MotionKey = options?.level3MotionObjectKey;
|
|
4166
4267
|
if (l2MotionKey && radarVariable === "VEL") {
|
|
4167
4268
|
decoded = await applyStormMotionFromN0sObjectKey(
|