@aguacerowx/mapsgl 0.0.52 → 0.0.54
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 +31 -31
- package/src/NexradWeatherController.js +510 -510
- package/src/NwsWatchesWarningsOverlay.js +54 -3
- package/src/WeatherLayerManager.js +14 -1
- package/src/nexrad/nexradArchiveCache.ts +286 -66
- package/src/nexrad/nexradLevel3Products.ts +581 -581
- package/src/nexrad/nexradMapboxFrameOpts.bundled.js +244 -0
- package/src/nexrad/radarArchiveCore.bundled.js +83 -5
- package/src/nwsAlertsFetchSpec.js +114 -0
- package/src/nwsAlertsSupport.js +28 -0
|
@@ -1,581 +1,581 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clampNexradTiltForVariable,
|
|
3
|
-
formatTiltForApi,
|
|
4
|
-
getDefaultRadarTilt,
|
|
5
|
-
getRadarTilts,
|
|
6
|
-
isTerminalRadar,
|
|
7
|
-
} from '@aguacerowx/javascript-sdk';
|
|
8
|
-
|
|
9
|
-
/** API elevation string for Level-III listings (lowest tilt / nominal 0.5°). */
|
|
10
|
-
export const NEXRAD_LEVEL3_ELEV = '0.50';
|
|
11
|
-
|
|
12
|
-
function nearestTiltInSortedList(tilts: readonly number[], target: number): number {
|
|
13
|
-
if (!tilts.length) return target;
|
|
14
|
-
let best = tilts[0]!;
|
|
15
|
-
let bestD = Math.abs(best - target);
|
|
16
|
-
for (let i = 1; i < tilts.length; i++) {
|
|
17
|
-
const t = tilts[i]!;
|
|
18
|
-
const d = Math.abs(t - target);
|
|
19
|
-
if (d < bestD || (d === bestD && t < best)) {
|
|
20
|
-
best = t;
|
|
21
|
-
bestD = d;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return best;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const NEXRAD_LEVEL3_MOTION_PRODUCT = 'N0S';
|
|
28
|
-
|
|
29
|
-
export type Level3DecodeMode =
|
|
30
|
-
| 'velocity'
|
|
31
|
-
| 'dbz'
|
|
32
|
-
| 'precip'
|
|
33
|
-
| 'categorical'
|
|
34
|
-
| 'tops_kft'
|
|
35
|
-
| 'vil'
|
|
36
|
-
/** PDB/generic-digital products already in physical units (e.g. KDP °/km); fine texture packing, not dBZ scale. */
|
|
37
|
-
| 'generic_physical';
|
|
38
|
-
|
|
39
|
-
export type NexradLevel3MenuEntry = {
|
|
40
|
-
/** Stored as `layer.radarVariable` and used in time-anchor keys `SITE_${radarKey}_${elev}_level3`. */
|
|
41
|
-
radarKey: string;
|
|
42
|
-
/** S3 object-key product mnemonic (e.g. N0B, N1P). */
|
|
43
|
-
product: string;
|
|
44
|
-
menuLabel: string;
|
|
45
|
-
/** DICTIONARIES.fld key for titles / metadata. */
|
|
46
|
-
fldKey: string;
|
|
47
|
-
/** layerProperties key (after variable_cmap resolution from fldKey). */
|
|
48
|
-
cmapPropertyKey: string;
|
|
49
|
-
/** Omit for velocity (VEL): use the same mph / km/h defaults as Level-2 velocity. */
|
|
50
|
-
defaultUnit?: string;
|
|
51
|
-
decodeMode: Level3DecodeMode;
|
|
52
|
-
/**
|
|
53
|
-
* When true, this entry uses storm-relative processing (N0G + N0S).
|
|
54
|
-
* Only the `VEL` radarKey uses this; `N0G` is base velocity from the same grid without N0S.
|
|
55
|
-
*/
|
|
56
|
-
stormRelativeVelocity?: boolean;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Level-III menu: subset aligned with products we decode as radial symbology (packets 0x0010 / 0xAF1F).
|
|
61
|
-
* See product codes vs mnemonics: https://supercell-wx.readthedocs.io/en/stable/features/nexrad-l3.html
|
|
62
|
-
*
|
|
63
|
-
* Intentionally omitted for now: legacy/digital precip variants (OHA/DPA/DSP/DU6/DOD/DSD/DPR) not wired in menu,
|
|
64
|
-
* NET/N0M, and base N0G-only velocity (menu keeps storm-relative VEL = N0G + N0S only).
|
|
65
|
-
*
|
|
66
|
-
* Terminal (TDWR) radars: only storm-relative velocity ({@link nexradStormRelativeVelocityUsesLevel2TiltAndFetch}) is
|
|
67
|
-
* exposed as Level-III in the UI; other Level-III products are WSR-88D-only in this app.
|
|
68
|
-
*/
|
|
69
|
-
export const NEXRAD_LEVEL3_MENU: NexradLevel3MenuEntry[] = [
|
|
70
|
-
{
|
|
71
|
-
radarKey: 'VEL',
|
|
72
|
-
product: 'N0G',
|
|
73
|
-
menuLabel: 'Storm Relative Velocity',
|
|
74
|
-
fldKey: 'nexrad_vel',
|
|
75
|
-
cmapPropertyKey: 'nexrad_vel',
|
|
76
|
-
decodeMode: 'velocity',
|
|
77
|
-
stormRelativeVelocity: true,
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
radarKey: 'KDP',
|
|
81
|
-
product: 'N0K',
|
|
82
|
-
menuLabel: 'Specific Differential Phase',
|
|
83
|
-
fldKey: 'nexrad_l3_n0k',
|
|
84
|
-
cmapPropertyKey: 'nexrad_kdp',
|
|
85
|
-
defaultUnit: 'deg/km',
|
|
86
|
-
decodeMode: 'generic_physical',
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
radarKey: 'N0H',
|
|
90
|
-
product: 'N0H',
|
|
91
|
-
menuLabel: 'Hydrometeor Classification',
|
|
92
|
-
fldKey: 'nexrad_l3_n0h',
|
|
93
|
-
cmapPropertyKey: 'nexrad_l3_n0h',
|
|
94
|
-
defaultUnit: 'None',
|
|
95
|
-
decodeMode: 'categorical',
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
radarKey: 'HHC',
|
|
99
|
-
product: 'HHC',
|
|
100
|
-
menuLabel: 'Hybrid Hydrometeor Classification',
|
|
101
|
-
fldKey: 'nexrad_l3_hhc',
|
|
102
|
-
cmapPropertyKey: 'nexrad_l3_hhc',
|
|
103
|
-
defaultUnit: 'None',
|
|
104
|
-
decodeMode: 'categorical',
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
radarKey: 'EET',
|
|
108
|
-
product: 'EET',
|
|
109
|
-
menuLabel: 'Enhanced Echo Tops',
|
|
110
|
-
fldKey: 'nexrad_l3_eet',
|
|
111
|
-
cmapPropertyKey: 'nexrad_l3_eet',
|
|
112
|
-
defaultUnit: 'kft',
|
|
113
|
-
decodeMode: 'tops_kft',
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
radarKey: 'DVL',
|
|
117
|
-
product: 'DVL',
|
|
118
|
-
menuLabel: 'Vertically Integrated Liquid',
|
|
119
|
-
fldKey: 'nexrad_l3_dvl',
|
|
120
|
-
cmapPropertyKey: 'nexrad_l3_dvl',
|
|
121
|
-
defaultUnit: 'kg/m²',
|
|
122
|
-
decodeMode: 'vil',
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
radarKey: 'DAA',
|
|
126
|
-
product: 'DAA',
|
|
127
|
-
menuLabel: '1-Hour Precipitation',
|
|
128
|
-
fldKey: 'nexrad_l3_daa',
|
|
129
|
-
cmapPropertyKey: 'tp_0_1',
|
|
130
|
-
defaultUnit: 'in',
|
|
131
|
-
decodeMode: 'precip',
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
radarKey: 'DU3',
|
|
135
|
-
product: 'DU3',
|
|
136
|
-
menuLabel: '3-Hour Precipitation',
|
|
137
|
-
fldKey: 'nexrad_l3_du3',
|
|
138
|
-
cmapPropertyKey: 'tp_0_total',
|
|
139
|
-
defaultUnit: 'in',
|
|
140
|
-
decodeMode: 'precip',
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
radarKey: 'DTA',
|
|
144
|
-
product: 'DTA',
|
|
145
|
-
menuLabel: 'Storm Total Precipitation',
|
|
146
|
-
fldKey: 'nexrad_l3_dta',
|
|
147
|
-
cmapPropertyKey: 'tp_0_total',
|
|
148
|
-
defaultUnit: 'in',
|
|
149
|
-
decodeMode: 'precip',
|
|
150
|
-
},
|
|
151
|
-
];
|
|
152
|
-
|
|
153
|
-
const byRadarKey = new Map<string, NexradLevel3MenuEntry>();
|
|
154
|
-
for (const e of NEXRAD_LEVEL3_MENU) {
|
|
155
|
-
byRadarKey.set(e.radarKey, e);
|
|
156
|
-
// Backward compatibility: older saved layers used radarVariable=NVL.
|
|
157
|
-
if (e.radarKey === 'DVL') byRadarKey.set('NVL', e);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function getNexradLevel3EntryByRadarKey(radarKey: string): NexradLevel3MenuEntry | undefined {
|
|
161
|
-
return byRadarKey.get(radarKey);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Digital Specific Differential Phase (163): S3 mnemonic per elevation slot (ICD order, low → high). */
|
|
165
|
-
export const NEXRAD_LEVEL3_KDP_PRODUCTS: readonly string[] = [
|
|
166
|
-
'NXK',
|
|
167
|
-
'NYK',
|
|
168
|
-
'NZK',
|
|
169
|
-
'N0K',
|
|
170
|
-
'NAK',
|
|
171
|
-
'N1K',
|
|
172
|
-
'NBK',
|
|
173
|
-
'N2K',
|
|
174
|
-
'N3K',
|
|
175
|
-
];
|
|
176
|
-
|
|
177
|
-
/** Digital Hydrometeor Classification (165): S3 mnemonic per elevation slot (ICD order, low → high). */
|
|
178
|
-
export const NEXRAD_LEVEL3_DHC_PRODUCTS: readonly string[] = [
|
|
179
|
-
'NXH',
|
|
180
|
-
'NYH',
|
|
181
|
-
'NZH',
|
|
182
|
-
'N0H',
|
|
183
|
-
'NAH',
|
|
184
|
-
'N1H',
|
|
185
|
-
'NBH',
|
|
186
|
-
'N2H',
|
|
187
|
-
'N3H',
|
|
188
|
-
];
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Level-III KDP/DHC products aligned with {@link getRadarTilts} indices (lowest manifest tilt first).
|
|
192
|
-
* NXK/NYK/NZK (and NXH…) are sub–0.5° cuts; our tilt manifest for KDP/N0H starts at ~0.5° like REF,
|
|
193
|
-
* so index 0 must map to N0K/N0H — matching historical behavior before multi-tilt wiring.
|
|
194
|
-
*/
|
|
195
|
-
const NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST: readonly string[] = NEXRAD_LEVEL3_KDP_PRODUCTS.slice(3);
|
|
196
|
-
const NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST: readonly string[] = NEXRAD_LEVEL3_DHC_PRODUCTS.slice(3);
|
|
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
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Level-III KDP / digital HC (N0H): one S3 mnemonic per manifest slot in
|
|
206
|
-
* {@link NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST} / {@link NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST}.
|
|
207
|
-
* The UI and composite keys use the first this many tilts from {@link getRadarTilts} (site order), not a fixed elevation angle.
|
|
208
|
-
*/
|
|
209
|
-
export const NEXRAD_LEVEL3_MANIFEST_TILT_COUNT = NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST.length;
|
|
210
|
-
|
|
211
|
-
/** Tilts available for Level-III KDP / N0H: same order as Level II, truncated after the Nth tilt ({@link NEXRAD_LEVEL3_MANIFEST_TILT_COUNT}). */
|
|
212
|
-
export function getNexradLevel3RadarTilts(siteId: string, radarVariable: string): number[] {
|
|
213
|
-
return getRadarTilts(siteId, radarVariable).slice(0, NEXRAD_LEVEL3_MANIFEST_TILT_COUNT);
|
|
214
|
-
}
|
|
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
|
-
|
|
242
|
-
export function nexradLevel3IsTiltIndexedKdpProduct(product: string): boolean {
|
|
243
|
-
return NEXRAD_LEVEL3_KDP_PRODUCTS.includes(product);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function nexradLevel3IsTiltIndexedDhcProduct(product: string): boolean {
|
|
247
|
-
return NEXRAD_LEVEL3_DHC_PRODUCTS.includes(product);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/** Layer variable is KDP or digital HC (N0H); user tilt maps to NXK..N3K / NXH..N3H. */
|
|
251
|
-
export function nexradLevel3UsesTiltIndexedS3Products(radarVariable: string | undefined): boolean {
|
|
252
|
-
const v = radarVariable || '';
|
|
253
|
-
return v === 'KDP' || v === 'N0H';
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Clamp stored tilt for composite keys / fetches when `radarVariable` is KDP or N0H on true Level III.
|
|
258
|
-
*/
|
|
259
|
-
export function clampTiltForLevel3CompositeKey(siteId: string, radarVariable: string, tilt: number): number {
|
|
260
|
-
if (!nexradLevel3UsesTiltIndexedS3Products(radarVariable)) {
|
|
261
|
-
return clampNexradTiltForVariable(siteId, radarVariable, tilt);
|
|
262
|
-
}
|
|
263
|
-
const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
|
|
264
|
-
const c = clampNexradTiltForVariable(siteId, radarVariable, tilt);
|
|
265
|
-
if (!tilts.length) return c;
|
|
266
|
-
return nearestTiltInSortedList(tilts, c);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Cross section fetches: one S3 product per manifest tilt in {@link getRadarTilts} order (capped by
|
|
271
|
-
* ICD mnemonic count). Uses the full NXK–N3K / NXH–N3H tables; when the lowest tilt is at or above
|
|
272
|
-
* ~0.5°, the first slot is N0K / N0H (skipping NX, NY, NZ sub-half-degree products).
|
|
273
|
-
*/
|
|
274
|
-
export function nexradLevel3CrossSectionTiltProductJobs(
|
|
275
|
-
siteId: string,
|
|
276
|
-
radarVariable: 'KDP' | 'N0H',
|
|
277
|
-
): { tiltDeg: number; product: string }[] {
|
|
278
|
-
const products =
|
|
279
|
-
radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS : NEXRAD_LEVEL3_DHC_PRODUCTS;
|
|
280
|
-
const tilts = getRadarTilts(siteId, radarVariable);
|
|
281
|
-
if (!tilts.length) return [];
|
|
282
|
-
const low = tilts[0]!;
|
|
283
|
-
const startSlot = low >= 0.45 ? 3 : 0;
|
|
284
|
-
const nSlots = Math.min(tilts.length, products.length - startSlot);
|
|
285
|
-
const out: { tiltDeg: number; product: string }[] = [];
|
|
286
|
-
for (let i = 0; i < nSlots; i++) {
|
|
287
|
-
out.push({ tiltDeg: tilts[i]!, product: products[startSlot + i]! });
|
|
288
|
-
}
|
|
289
|
-
return out;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Map site tilt to Level-III S3 product (N0K, NAK, … / N0H, …) for map tiles and time keys.
|
|
294
|
-
*/
|
|
295
|
-
export function nexradLevel3S3ProductForSiteTilt(
|
|
296
|
-
siteId: string,
|
|
297
|
-
radarVariable: 'KDP' | 'N0H',
|
|
298
|
-
tilt: number,
|
|
299
|
-
): string {
|
|
300
|
-
const products =
|
|
301
|
-
radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST : NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST;
|
|
302
|
-
const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
|
|
303
|
-
if (!tilts.length) {
|
|
304
|
-
return products[0]!;
|
|
305
|
-
}
|
|
306
|
-
const clamped = clampTiltForLevel3CompositeKey(siteId, radarVariable, tilt);
|
|
307
|
-
let idx = tilts.indexOf(clamped);
|
|
308
|
-
if (idx === -1) {
|
|
309
|
-
idx = tilts.reduce(
|
|
310
|
-
(bestI, t, i) => (Math.abs(t - clamped) < Math.abs(tilts[bestI]! - clamped) ? i : bestI),
|
|
311
|
-
0,
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
idx = Math.min(idx, products.length - 1);
|
|
315
|
-
return products[idx]!;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** Elevation string for `SITE_VAR_elev_level3` keys / UI — matches manifest slot for this S3 product. */
|
|
319
|
-
export function nexradLevel3DisplayElevForS3Product(
|
|
320
|
-
siteId: string,
|
|
321
|
-
radarVariable: 'KDP' | 'N0H',
|
|
322
|
-
s3Product: string,
|
|
323
|
-
): string {
|
|
324
|
-
const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
|
|
325
|
-
if (!tilts.length) return NEXRAD_LEVEL3_ELEV;
|
|
326
|
-
|
|
327
|
-
const manifestProducts =
|
|
328
|
-
radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST : NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST;
|
|
329
|
-
let idx = manifestProducts.indexOf(s3Product);
|
|
330
|
-
if (idx >= 0) {
|
|
331
|
-
const t = tilts[Math.min(idx, tilts.length - 1)]!;
|
|
332
|
-
return formatTiltForApi(t);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** NXK/NYK/NZK (or NXH…) from listings: not in manifest-ordered slice — use lowest tilt for keying. */
|
|
336
|
-
const full = radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS : NEXRAD_LEVEL3_DHC_PRODUCTS;
|
|
337
|
-
const fullIdx = full.indexOf(s3Product);
|
|
338
|
-
if (fullIdx >= 0 && fullIdx < 3) {
|
|
339
|
-
return formatTiltForApi(tilts[0]!);
|
|
340
|
-
}
|
|
341
|
-
if (fullIdx >= 3) {
|
|
342
|
-
const mIdx = fullIdx - 3;
|
|
343
|
-
const t = tilts[Math.min(mIdx, tilts.length - 1)]!;
|
|
344
|
-
return formatTiltForApi(t);
|
|
345
|
-
}
|
|
346
|
-
return NEXRAD_LEVEL3_ELEV;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Level-III products that are not tied to a selectable sweep (precip accumulations, echo tops, VIL,
|
|
351
|
-
* hybrid HC, etc.). Hide tilt controls and ignore tilt stepping for these.
|
|
352
|
-
* Digital HC (N0H) and KDP use {@link nexradLevel3UsesTiltIndexedS3Products}.
|
|
353
|
-
*/
|
|
354
|
-
const LEVEL3_DECODE_MODES_WITHOUT_USER_TILT: ReadonlySet<Level3DecodeMode> = new Set([
|
|
355
|
-
'precip',
|
|
356
|
-
'tops_kft',
|
|
357
|
-
'vil',
|
|
358
|
-
'categorical',
|
|
359
|
-
]);
|
|
360
|
-
|
|
361
|
-
export function nexradLevel3DisallowsUserTiltChange(radarVariable: string | undefined): boolean {
|
|
362
|
-
if (nexradLevel3UsesTiltIndexedS3Products(radarVariable)) return false;
|
|
363
|
-
const entry = getNexradLevel3EntryByRadarKey(radarVariable || '');
|
|
364
|
-
if (!entry) return false;
|
|
365
|
-
if (entry.stormRelativeVelocity) return false;
|
|
366
|
-
return LEVEL3_DECODE_MODES_WITHOUT_USER_TILT.has(entry.decodeMode);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Cross sections stack multiple elevation sweeps. True Level-III products that are a single grid
|
|
371
|
-
* (precipitation totals, echo tops, VIL, hybrid HC, etc.) cannot produce a pseudo-RHI — skip fetches.
|
|
372
|
-
*/
|
|
373
|
-
export function nexradCrossSectionBlockedReason(layer: {
|
|
374
|
-
radar?: string | null;
|
|
375
|
-
radarDataSource?: string;
|
|
376
|
-
radarVariable?: string;
|
|
377
|
-
level3StormRelative?: boolean;
|
|
378
|
-
}): string | null {
|
|
379
|
-
if (!nexradLayerUsesTrueLevel3Listing(layer)) return null;
|
|
380
|
-
if (!nexradLevel3DisallowsUserTiltChange(layer.radarVariable)) return null;
|
|
381
|
-
const entry = getNexradLevel3EntryByRadarKey(layer.radarVariable || '');
|
|
382
|
-
const label = entry?.menuLabel ?? layer.radarVariable ?? 'this product';
|
|
383
|
-
return `Cross sections are not available for ${label}.`;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/** True Level-III map products (not storm-relative velocity, which uses Level II + motion). */
|
|
387
|
-
export function nexradLayerUsesTrueLevel3Listing(layer: {
|
|
388
|
-
radar?: string | null;
|
|
389
|
-
radarDataSource?: string;
|
|
390
|
-
radarVariable?: string;
|
|
391
|
-
level3StormRelative?: boolean;
|
|
392
|
-
}): boolean {
|
|
393
|
-
return (
|
|
394
|
-
layer.radarDataSource === 'level3' && !nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer)
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/** Terminal radars do not support true Level-III map products in this app (SRV only). */
|
|
399
|
-
export function terminalRadarBlocksTrueLevel3(layer: {
|
|
400
|
-
radar?: string | null;
|
|
401
|
-
radarDataSource?: string;
|
|
402
|
-
radarVariable?: string;
|
|
403
|
-
level3StormRelative?: boolean;
|
|
404
|
-
}): boolean {
|
|
405
|
-
const site = layer.radar || '';
|
|
406
|
-
return isTerminalRadar(site) && nexradLayerUsesTrueLevel3Listing(layer);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/** Digital (N0H) and hybrid (HHC) hydrometeor classification — discrete classes, not a continuous field. */
|
|
410
|
-
export function nexradLevel3IsHydrometeorClassification(radarVariable: string | undefined): boolean {
|
|
411
|
-
const v = radarVariable || '';
|
|
412
|
-
return v === 'N0H' || v === 'HHC';
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
export type Level3ExpandTarget = { radarKey: string; attachMotionMap: boolean };
|
|
416
|
-
|
|
417
|
-
/** Map raw S3 product code to one or more formatted time-anchor variables. */
|
|
418
|
-
export function getLevel3ExpansionTargetsForS3Product(product: string): Level3ExpandTarget[] {
|
|
419
|
-
if (product === NEXRAD_LEVEL3_MOTION_PRODUCT) {
|
|
420
|
-
return [];
|
|
421
|
-
}
|
|
422
|
-
if (product === 'DVL') {
|
|
423
|
-
// Emit both keys so old NVL layers and new DVL layers resolve the same time map.
|
|
424
|
-
return [
|
|
425
|
-
{ radarKey: 'DVL', attachMotionMap: false },
|
|
426
|
-
{ radarKey: 'NVL', attachMotionMap: false },
|
|
427
|
-
];
|
|
428
|
-
}
|
|
429
|
-
if (product === 'N0G') {
|
|
430
|
-
return [
|
|
431
|
-
{ radarKey: 'N0G', attachMotionMap: false },
|
|
432
|
-
{ radarKey: 'VEL', attachMotionMap: true },
|
|
433
|
-
];
|
|
434
|
-
}
|
|
435
|
-
if (nexradLevel3IsTiltIndexedKdpProduct(product)) {
|
|
436
|
-
return [{ radarKey: 'KDP', attachMotionMap: false }];
|
|
437
|
-
}
|
|
438
|
-
if (nexradLevel3IsTiltIndexedDhcProduct(product)) {
|
|
439
|
-
return [{ radarKey: 'N0H', attachMotionMap: false }];
|
|
440
|
-
}
|
|
441
|
-
return [{ radarKey: product, attachMotionMap: false }];
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Product 135 (EET): MetPy `DigitalEETMapper` uses PDB thr1–thr3 as data mask, scale, and offset:
|
|
446
|
-
* `mapped = ((level & mask) - offset) / scale` (raw signed halfwords — no /10; unlike dBZ min/inc).
|
|
447
|
-
*
|
|
448
|
-
* Output is treated as **kilometers** end-to-end with the rest of `tops_kft` packing.
|
|
449
|
-
*/
|
|
450
|
-
export function level3EetHeightKmFromLevel(
|
|
451
|
-
level: number,
|
|
452
|
-
dataMaskRaw: number,
|
|
453
|
-
scaleRaw: number,
|
|
454
|
-
offsetRaw: number,
|
|
455
|
-
): number | null {
|
|
456
|
-
if (level < 2) return null;
|
|
457
|
-
const masked = level & (dataMaskRaw & 0xffff);
|
|
458
|
-
if (!Number.isFinite(scaleRaw) || scaleRaw === 0) {
|
|
459
|
-
return level - 2;
|
|
460
|
-
}
|
|
461
|
-
const mapped = (masked - offsetRaw) / scaleRaw;
|
|
462
|
-
if (!Number.isFinite(mapped)) return null;
|
|
463
|
-
return mapped;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
export function level3ValuePacking(decodeMode: Level3DecodeMode): { valueScale: number; valueOffset: number } {
|
|
467
|
-
switch (decodeMode) {
|
|
468
|
-
case 'velocity':
|
|
469
|
-
return { valueScale: 0.01, valueOffset: 0 };
|
|
470
|
-
case 'dbz':
|
|
471
|
-
return { valueScale: 0.5, valueOffset: -32 };
|
|
472
|
-
case 'precip':
|
|
473
|
-
return { valueScale: 0.001, valueOffset: 0 };
|
|
474
|
-
case 'categorical':
|
|
475
|
-
return { valueScale: 1, valueOffset: 0 };
|
|
476
|
-
case 'tops_kft':
|
|
477
|
-
// Shader physical = raw * valueScale (km). raw = round(km / valueScale) must stay within int16.
|
|
478
|
-
// 0.001 implied raw = meters → tops >32.767 km clamped (seen on EET ~48 km). Use 2 m/LSb → max ~65.5 km.
|
|
479
|
-
return { valueScale: 0.002, valueOffset: 0 };
|
|
480
|
-
case 'vil':
|
|
481
|
-
return { valueScale: 0.1, valueOffset: 0 };
|
|
482
|
-
case 'generic_physical':
|
|
483
|
-
return { valueScale: 0.01, valueOffset: 0 };
|
|
484
|
-
default:
|
|
485
|
-
return { valueScale: 0.5, valueOffset: -32 };
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Opt-in storm-relative velocity (super-res VEL + N0S motion). Set in localStorage to trace: DEBUG_NEXRAD_VEL=1
|
|
491
|
-
*/
|
|
492
|
-
function nexradVelDebug(label: string, data: Record<string, unknown>) {
|
|
493
|
-
try {
|
|
494
|
-
if (typeof localStorage !== 'undefined' && localStorage.getItem('DEBUG_NEXRAD_VEL') === '1') {
|
|
495
|
-
console.log(`[NEXRAD_VEL] ${label}`, data);
|
|
496
|
-
}
|
|
497
|
-
} catch {
|
|
498
|
-
/* ignore */
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Storm-relative velocity is rendered from Level-2 super-res VEL at the user’s tilt plus L3 N0S motion.
|
|
504
|
-
* Layers may still have `radarDataSource: 'level3'` when chosen from the Level-III menu.
|
|
505
|
-
*
|
|
506
|
-
* `level3StormRelative === undefined` must NOT default to “on” for Level-II velocity (that broke base VEL).
|
|
507
|
-
* For legacy layers with L3-menu VEL and no flag, infer SRV only when `radarDataSource === 'level3'`.
|
|
508
|
-
*/
|
|
509
|
-
export function nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer: {
|
|
510
|
-
radar?: string | null;
|
|
511
|
-
radarVariable?: string;
|
|
512
|
-
level3StormRelative?: boolean;
|
|
513
|
-
radarDataSource?: string;
|
|
514
|
-
}): boolean {
|
|
515
|
-
if ((layer.radarVariable || 'REF') !== 'VEL') {
|
|
516
|
-
return false;
|
|
517
|
-
}
|
|
518
|
-
if (layer.level3StormRelative === true) return true;
|
|
519
|
-
if (layer.level3StormRelative === false) return false;
|
|
520
|
-
const inferred = (layer.radarDataSource || 'level2') === 'level3';
|
|
521
|
-
nexradVelDebug('VEL level3StormRelative missing — infer from radarDataSource', {
|
|
522
|
-
radar: layer.radar,
|
|
523
|
-
radarDataSource: layer.radarDataSource,
|
|
524
|
-
useStormRelativeVelocity: inferred,
|
|
525
|
-
});
|
|
526
|
-
return inferred;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* True when composite time keys and map wiring should use `layer.radarTilt` (like Level II), not a fixed 0.5° token.
|
|
531
|
-
*/
|
|
532
|
-
export function nexradLevel3LayerUsesRadarTiltInCompositeKeys(layer: {
|
|
533
|
-
radar?: string | null;
|
|
534
|
-
radarVariable?: string;
|
|
535
|
-
radarDataSource?: string;
|
|
536
|
-
level3StormRelative?: boolean;
|
|
537
|
-
}): boolean {
|
|
538
|
-
if (layer.radarDataSource !== 'level3') return false;
|
|
539
|
-
if (nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer)) return false;
|
|
540
|
-
return nexradLevel3UsesTiltIndexedS3Products(layer.radarVariable);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
export function nexradEffectiveFetchSource(layer: {
|
|
544
|
-
radar?: string | null;
|
|
545
|
-
radarVariable?: string;
|
|
546
|
-
radarDataSource?: string;
|
|
547
|
-
level3StormRelative?: boolean;
|
|
548
|
-
}): 'level2' | 'level3' {
|
|
549
|
-
if (terminalRadarBlocksTrueLevel3(layer)) return 'level2';
|
|
550
|
-
if (nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer)) return 'level2';
|
|
551
|
-
return layer.radarDataSource === 'level3' ? 'level3' : 'level2';
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
export function nexradTimesFetchOptionsFromLayer(layer: {
|
|
555
|
-
radar?: string | null;
|
|
556
|
-
radarVariable?: string;
|
|
557
|
-
radarDataSource?: string;
|
|
558
|
-
level3StormRelative?: boolean;
|
|
559
|
-
radarTilt?: number | null;
|
|
560
|
-
}): { level3Product?: string; level3StormRelative?: boolean } | undefined {
|
|
561
|
-
if (terminalRadarBlocksTrueLevel3(layer)) return undefined;
|
|
562
|
-
/** L2 fetch + N0S time alignment for storm-relative velocity (super-res VEL + motion). */
|
|
563
|
-
if (nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer)) {
|
|
564
|
-
return { level3StormRelative: true };
|
|
565
|
-
}
|
|
566
|
-
if (layer.radarDataSource !== 'level3') return undefined;
|
|
567
|
-
const v = layer.radarVariable || 'REF';
|
|
568
|
-
const entry = getNexradLevel3EntryByRadarKey(v);
|
|
569
|
-
let product = entry?.product ?? (v === 'VEL' ? 'N0G' : v);
|
|
570
|
-
const site = layer.radar || '';
|
|
571
|
-
if (site && nexradLevel3UsesTiltIndexedS3Products(v)) {
|
|
572
|
-
const tilt = layer.radarTilt ?? getDefaultRadarTilt(site);
|
|
573
|
-
product = nexradLevel3S3ProductForSiteTilt(site, v as 'KDP' | 'N0H', tilt);
|
|
574
|
-
}
|
|
575
|
-
const stormRelative = v === 'VEL' ? nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer) : false;
|
|
576
|
-
return { level3Product: product, level3StormRelative: stormRelative };
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
export function formatLevel3TiltForApi(): string {
|
|
580
|
-
return NEXRAD_LEVEL3_ELEV;
|
|
581
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
clampNexradTiltForVariable,
|
|
3
|
+
formatTiltForApi,
|
|
4
|
+
getDefaultRadarTilt,
|
|
5
|
+
getRadarTilts,
|
|
6
|
+
isTerminalRadar,
|
|
7
|
+
} from '@aguacerowx/javascript-sdk';
|
|
8
|
+
|
|
9
|
+
/** API elevation string for Level-III listings (lowest tilt / nominal 0.5°). */
|
|
10
|
+
export const NEXRAD_LEVEL3_ELEV = '0.50';
|
|
11
|
+
|
|
12
|
+
function nearestTiltInSortedList(tilts: readonly number[], target: number): number {
|
|
13
|
+
if (!tilts.length) return target;
|
|
14
|
+
let best = tilts[0]!;
|
|
15
|
+
let bestD = Math.abs(best - target);
|
|
16
|
+
for (let i = 1; i < tilts.length; i++) {
|
|
17
|
+
const t = tilts[i]!;
|
|
18
|
+
const d = Math.abs(t - target);
|
|
19
|
+
if (d < bestD || (d === bestD && t < best)) {
|
|
20
|
+
best = t;
|
|
21
|
+
bestD = d;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return best;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const NEXRAD_LEVEL3_MOTION_PRODUCT = 'N0S';
|
|
28
|
+
|
|
29
|
+
export type Level3DecodeMode =
|
|
30
|
+
| 'velocity'
|
|
31
|
+
| 'dbz'
|
|
32
|
+
| 'precip'
|
|
33
|
+
| 'categorical'
|
|
34
|
+
| 'tops_kft'
|
|
35
|
+
| 'vil'
|
|
36
|
+
/** PDB/generic-digital products already in physical units (e.g. KDP °/km); fine texture packing, not dBZ scale. */
|
|
37
|
+
| 'generic_physical';
|
|
38
|
+
|
|
39
|
+
export type NexradLevel3MenuEntry = {
|
|
40
|
+
/** Stored as `layer.radarVariable` and used in time-anchor keys `SITE_${radarKey}_${elev}_level3`. */
|
|
41
|
+
radarKey: string;
|
|
42
|
+
/** S3 object-key product mnemonic (e.g. N0B, N1P). */
|
|
43
|
+
product: string;
|
|
44
|
+
menuLabel: string;
|
|
45
|
+
/** DICTIONARIES.fld key for titles / metadata. */
|
|
46
|
+
fldKey: string;
|
|
47
|
+
/** layerProperties key (after variable_cmap resolution from fldKey). */
|
|
48
|
+
cmapPropertyKey: string;
|
|
49
|
+
/** Omit for velocity (VEL): use the same mph / km/h defaults as Level-2 velocity. */
|
|
50
|
+
defaultUnit?: string;
|
|
51
|
+
decodeMode: Level3DecodeMode;
|
|
52
|
+
/**
|
|
53
|
+
* When true, this entry uses storm-relative processing (N0G + N0S).
|
|
54
|
+
* Only the `VEL` radarKey uses this; `N0G` is base velocity from the same grid without N0S.
|
|
55
|
+
*/
|
|
56
|
+
stormRelativeVelocity?: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Level-III menu: subset aligned with products we decode as radial symbology (packets 0x0010 / 0xAF1F).
|
|
61
|
+
* See product codes vs mnemonics: https://supercell-wx.readthedocs.io/en/stable/features/nexrad-l3.html
|
|
62
|
+
*
|
|
63
|
+
* Intentionally omitted for now: legacy/digital precip variants (OHA/DPA/DSP/DU6/DOD/DSD/DPR) not wired in menu,
|
|
64
|
+
* NET/N0M, and base N0G-only velocity (menu keeps storm-relative VEL = N0G + N0S only).
|
|
65
|
+
*
|
|
66
|
+
* Terminal (TDWR) radars: only storm-relative velocity ({@link nexradStormRelativeVelocityUsesLevel2TiltAndFetch}) is
|
|
67
|
+
* exposed as Level-III in the UI; other Level-III products are WSR-88D-only in this app.
|
|
68
|
+
*/
|
|
69
|
+
export const NEXRAD_LEVEL3_MENU: NexradLevel3MenuEntry[] = [
|
|
70
|
+
{
|
|
71
|
+
radarKey: 'VEL',
|
|
72
|
+
product: 'N0G',
|
|
73
|
+
menuLabel: 'Storm Relative Velocity',
|
|
74
|
+
fldKey: 'nexrad_vel',
|
|
75
|
+
cmapPropertyKey: 'nexrad_vel',
|
|
76
|
+
decodeMode: 'velocity',
|
|
77
|
+
stormRelativeVelocity: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
radarKey: 'KDP',
|
|
81
|
+
product: 'N0K',
|
|
82
|
+
menuLabel: 'Specific Differential Phase',
|
|
83
|
+
fldKey: 'nexrad_l3_n0k',
|
|
84
|
+
cmapPropertyKey: 'nexrad_kdp',
|
|
85
|
+
defaultUnit: 'deg/km',
|
|
86
|
+
decodeMode: 'generic_physical',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
radarKey: 'N0H',
|
|
90
|
+
product: 'N0H',
|
|
91
|
+
menuLabel: 'Hydrometeor Classification',
|
|
92
|
+
fldKey: 'nexrad_l3_n0h',
|
|
93
|
+
cmapPropertyKey: 'nexrad_l3_n0h',
|
|
94
|
+
defaultUnit: 'None',
|
|
95
|
+
decodeMode: 'categorical',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
radarKey: 'HHC',
|
|
99
|
+
product: 'HHC',
|
|
100
|
+
menuLabel: 'Hybrid Hydrometeor Classification',
|
|
101
|
+
fldKey: 'nexrad_l3_hhc',
|
|
102
|
+
cmapPropertyKey: 'nexrad_l3_hhc',
|
|
103
|
+
defaultUnit: 'None',
|
|
104
|
+
decodeMode: 'categorical',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
radarKey: 'EET',
|
|
108
|
+
product: 'EET',
|
|
109
|
+
menuLabel: 'Enhanced Echo Tops',
|
|
110
|
+
fldKey: 'nexrad_l3_eet',
|
|
111
|
+
cmapPropertyKey: 'nexrad_l3_eet',
|
|
112
|
+
defaultUnit: 'kft',
|
|
113
|
+
decodeMode: 'tops_kft',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
radarKey: 'DVL',
|
|
117
|
+
product: 'DVL',
|
|
118
|
+
menuLabel: 'Vertically Integrated Liquid',
|
|
119
|
+
fldKey: 'nexrad_l3_dvl',
|
|
120
|
+
cmapPropertyKey: 'nexrad_l3_dvl',
|
|
121
|
+
defaultUnit: 'kg/m²',
|
|
122
|
+
decodeMode: 'vil',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
radarKey: 'DAA',
|
|
126
|
+
product: 'DAA',
|
|
127
|
+
menuLabel: '1-Hour Precipitation',
|
|
128
|
+
fldKey: 'nexrad_l3_daa',
|
|
129
|
+
cmapPropertyKey: 'tp_0_1',
|
|
130
|
+
defaultUnit: 'in',
|
|
131
|
+
decodeMode: 'precip',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
radarKey: 'DU3',
|
|
135
|
+
product: 'DU3',
|
|
136
|
+
menuLabel: '3-Hour Precipitation',
|
|
137
|
+
fldKey: 'nexrad_l3_du3',
|
|
138
|
+
cmapPropertyKey: 'tp_0_total',
|
|
139
|
+
defaultUnit: 'in',
|
|
140
|
+
decodeMode: 'precip',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
radarKey: 'DTA',
|
|
144
|
+
product: 'DTA',
|
|
145
|
+
menuLabel: 'Storm Total Precipitation',
|
|
146
|
+
fldKey: 'nexrad_l3_dta',
|
|
147
|
+
cmapPropertyKey: 'tp_0_total',
|
|
148
|
+
defaultUnit: 'in',
|
|
149
|
+
decodeMode: 'precip',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const byRadarKey = new Map<string, NexradLevel3MenuEntry>();
|
|
154
|
+
for (const e of NEXRAD_LEVEL3_MENU) {
|
|
155
|
+
byRadarKey.set(e.radarKey, e);
|
|
156
|
+
// Backward compatibility: older saved layers used radarVariable=NVL.
|
|
157
|
+
if (e.radarKey === 'DVL') byRadarKey.set('NVL', e);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getNexradLevel3EntryByRadarKey(radarKey: string): NexradLevel3MenuEntry | undefined {
|
|
161
|
+
return byRadarKey.get(radarKey);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Digital Specific Differential Phase (163): S3 mnemonic per elevation slot (ICD order, low → high). */
|
|
165
|
+
export const NEXRAD_LEVEL3_KDP_PRODUCTS: readonly string[] = [
|
|
166
|
+
'NXK',
|
|
167
|
+
'NYK',
|
|
168
|
+
'NZK',
|
|
169
|
+
'N0K',
|
|
170
|
+
'NAK',
|
|
171
|
+
'N1K',
|
|
172
|
+
'NBK',
|
|
173
|
+
'N2K',
|
|
174
|
+
'N3K',
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
/** Digital Hydrometeor Classification (165): S3 mnemonic per elevation slot (ICD order, low → high). */
|
|
178
|
+
export const NEXRAD_LEVEL3_DHC_PRODUCTS: readonly string[] = [
|
|
179
|
+
'NXH',
|
|
180
|
+
'NYH',
|
|
181
|
+
'NZH',
|
|
182
|
+
'N0H',
|
|
183
|
+
'NAH',
|
|
184
|
+
'N1H',
|
|
185
|
+
'NBH',
|
|
186
|
+
'N2H',
|
|
187
|
+
'N3H',
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Level-III KDP/DHC products aligned with {@link getRadarTilts} indices (lowest manifest tilt first).
|
|
192
|
+
* NXK/NYK/NZK (and NXH…) are sub–0.5° cuts; our tilt manifest for KDP/N0H starts at ~0.5° like REF,
|
|
193
|
+
* so index 0 must map to N0K/N0H — matching historical behavior before multi-tilt wiring.
|
|
194
|
+
*/
|
|
195
|
+
const NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST: readonly string[] = NEXRAD_LEVEL3_KDP_PRODUCTS.slice(3);
|
|
196
|
+
const NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST: readonly string[] = NEXRAD_LEVEL3_DHC_PRODUCTS.slice(3);
|
|
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
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Level-III KDP / digital HC (N0H): one S3 mnemonic per manifest slot in
|
|
206
|
+
* {@link NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST} / {@link NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST}.
|
|
207
|
+
* The UI and composite keys use the first this many tilts from {@link getRadarTilts} (site order), not a fixed elevation angle.
|
|
208
|
+
*/
|
|
209
|
+
export const NEXRAD_LEVEL3_MANIFEST_TILT_COUNT = NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST.length;
|
|
210
|
+
|
|
211
|
+
/** Tilts available for Level-III KDP / N0H: same order as Level II, truncated after the Nth tilt ({@link NEXRAD_LEVEL3_MANIFEST_TILT_COUNT}). */
|
|
212
|
+
export function getNexradLevel3RadarTilts(siteId: string, radarVariable: string): number[] {
|
|
213
|
+
return getRadarTilts(siteId, radarVariable).slice(0, NEXRAD_LEVEL3_MANIFEST_TILT_COUNT);
|
|
214
|
+
}
|
|
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
|
+
|
|
242
|
+
export function nexradLevel3IsTiltIndexedKdpProduct(product: string): boolean {
|
|
243
|
+
return NEXRAD_LEVEL3_KDP_PRODUCTS.includes(product);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function nexradLevel3IsTiltIndexedDhcProduct(product: string): boolean {
|
|
247
|
+
return NEXRAD_LEVEL3_DHC_PRODUCTS.includes(product);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Layer variable is KDP or digital HC (N0H); user tilt maps to NXK..N3K / NXH..N3H. */
|
|
251
|
+
export function nexradLevel3UsesTiltIndexedS3Products(radarVariable: string | undefined): boolean {
|
|
252
|
+
const v = radarVariable || '';
|
|
253
|
+
return v === 'KDP' || v === 'N0H';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Clamp stored tilt for composite keys / fetches when `radarVariable` is KDP or N0H on true Level III.
|
|
258
|
+
*/
|
|
259
|
+
export function clampTiltForLevel3CompositeKey(siteId: string, radarVariable: string, tilt: number): number {
|
|
260
|
+
if (!nexradLevel3UsesTiltIndexedS3Products(radarVariable)) {
|
|
261
|
+
return clampNexradTiltForVariable(siteId, radarVariable, tilt);
|
|
262
|
+
}
|
|
263
|
+
const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
|
|
264
|
+
const c = clampNexradTiltForVariable(siteId, radarVariable, tilt);
|
|
265
|
+
if (!tilts.length) return c;
|
|
266
|
+
return nearestTiltInSortedList(tilts, c);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Cross section fetches: one S3 product per manifest tilt in {@link getRadarTilts} order (capped by
|
|
271
|
+
* ICD mnemonic count). Uses the full NXK–N3K / NXH–N3H tables; when the lowest tilt is at or above
|
|
272
|
+
* ~0.5°, the first slot is N0K / N0H (skipping NX, NY, NZ sub-half-degree products).
|
|
273
|
+
*/
|
|
274
|
+
export function nexradLevel3CrossSectionTiltProductJobs(
|
|
275
|
+
siteId: string,
|
|
276
|
+
radarVariable: 'KDP' | 'N0H',
|
|
277
|
+
): { tiltDeg: number; product: string }[] {
|
|
278
|
+
const products =
|
|
279
|
+
radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS : NEXRAD_LEVEL3_DHC_PRODUCTS;
|
|
280
|
+
const tilts = getRadarTilts(siteId, radarVariable);
|
|
281
|
+
if (!tilts.length) return [];
|
|
282
|
+
const low = tilts[0]!;
|
|
283
|
+
const startSlot = low >= 0.45 ? 3 : 0;
|
|
284
|
+
const nSlots = Math.min(tilts.length, products.length - startSlot);
|
|
285
|
+
const out: { tiltDeg: number; product: string }[] = [];
|
|
286
|
+
for (let i = 0; i < nSlots; i++) {
|
|
287
|
+
out.push({ tiltDeg: tilts[i]!, product: products[startSlot + i]! });
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Map site tilt to Level-III S3 product (N0K, NAK, … / N0H, …) for map tiles and time keys.
|
|
294
|
+
*/
|
|
295
|
+
export function nexradLevel3S3ProductForSiteTilt(
|
|
296
|
+
siteId: string,
|
|
297
|
+
radarVariable: 'KDP' | 'N0H',
|
|
298
|
+
tilt: number,
|
|
299
|
+
): string {
|
|
300
|
+
const products =
|
|
301
|
+
radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST : NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST;
|
|
302
|
+
const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
|
|
303
|
+
if (!tilts.length) {
|
|
304
|
+
return products[0]!;
|
|
305
|
+
}
|
|
306
|
+
const clamped = clampTiltForLevel3CompositeKey(siteId, radarVariable, tilt);
|
|
307
|
+
let idx = tilts.indexOf(clamped);
|
|
308
|
+
if (idx === -1) {
|
|
309
|
+
idx = tilts.reduce(
|
|
310
|
+
(bestI, t, i) => (Math.abs(t - clamped) < Math.abs(tilts[bestI]! - clamped) ? i : bestI),
|
|
311
|
+
0,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
idx = Math.min(idx, products.length - 1);
|
|
315
|
+
return products[idx]!;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Elevation string for `SITE_VAR_elev_level3` keys / UI — matches manifest slot for this S3 product. */
|
|
319
|
+
export function nexradLevel3DisplayElevForS3Product(
|
|
320
|
+
siteId: string,
|
|
321
|
+
radarVariable: 'KDP' | 'N0H',
|
|
322
|
+
s3Product: string,
|
|
323
|
+
): string {
|
|
324
|
+
const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
|
|
325
|
+
if (!tilts.length) return NEXRAD_LEVEL3_ELEV;
|
|
326
|
+
|
|
327
|
+
const manifestProducts =
|
|
328
|
+
radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST : NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST;
|
|
329
|
+
let idx = manifestProducts.indexOf(s3Product);
|
|
330
|
+
if (idx >= 0) {
|
|
331
|
+
const t = tilts[Math.min(idx, tilts.length - 1)]!;
|
|
332
|
+
return formatTiltForApi(t);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** NXK/NYK/NZK (or NXH…) from listings: not in manifest-ordered slice — use lowest tilt for keying. */
|
|
336
|
+
const full = radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS : NEXRAD_LEVEL3_DHC_PRODUCTS;
|
|
337
|
+
const fullIdx = full.indexOf(s3Product);
|
|
338
|
+
if (fullIdx >= 0 && fullIdx < 3) {
|
|
339
|
+
return formatTiltForApi(tilts[0]!);
|
|
340
|
+
}
|
|
341
|
+
if (fullIdx >= 3) {
|
|
342
|
+
const mIdx = fullIdx - 3;
|
|
343
|
+
const t = tilts[Math.min(mIdx, tilts.length - 1)]!;
|
|
344
|
+
return formatTiltForApi(t);
|
|
345
|
+
}
|
|
346
|
+
return NEXRAD_LEVEL3_ELEV;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Level-III products that are not tied to a selectable sweep (precip accumulations, echo tops, VIL,
|
|
351
|
+
* hybrid HC, etc.). Hide tilt controls and ignore tilt stepping for these.
|
|
352
|
+
* Digital HC (N0H) and KDP use {@link nexradLevel3UsesTiltIndexedS3Products}.
|
|
353
|
+
*/
|
|
354
|
+
const LEVEL3_DECODE_MODES_WITHOUT_USER_TILT: ReadonlySet<Level3DecodeMode> = new Set([
|
|
355
|
+
'precip',
|
|
356
|
+
'tops_kft',
|
|
357
|
+
'vil',
|
|
358
|
+
'categorical',
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
export function nexradLevel3DisallowsUserTiltChange(radarVariable: string | undefined): boolean {
|
|
362
|
+
if (nexradLevel3UsesTiltIndexedS3Products(radarVariable)) return false;
|
|
363
|
+
const entry = getNexradLevel3EntryByRadarKey(radarVariable || '');
|
|
364
|
+
if (!entry) return false;
|
|
365
|
+
if (entry.stormRelativeVelocity) return false;
|
|
366
|
+
return LEVEL3_DECODE_MODES_WITHOUT_USER_TILT.has(entry.decodeMode);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Cross sections stack multiple elevation sweeps. True Level-III products that are a single grid
|
|
371
|
+
* (precipitation totals, echo tops, VIL, hybrid HC, etc.) cannot produce a pseudo-RHI — skip fetches.
|
|
372
|
+
*/
|
|
373
|
+
export function nexradCrossSectionBlockedReason(layer: {
|
|
374
|
+
radar?: string | null;
|
|
375
|
+
radarDataSource?: string;
|
|
376
|
+
radarVariable?: string;
|
|
377
|
+
level3StormRelative?: boolean;
|
|
378
|
+
}): string | null {
|
|
379
|
+
if (!nexradLayerUsesTrueLevel3Listing(layer)) return null;
|
|
380
|
+
if (!nexradLevel3DisallowsUserTiltChange(layer.radarVariable)) return null;
|
|
381
|
+
const entry = getNexradLevel3EntryByRadarKey(layer.radarVariable || '');
|
|
382
|
+
const label = entry?.menuLabel ?? layer.radarVariable ?? 'this product';
|
|
383
|
+
return `Cross sections are not available for ${label}.`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** True Level-III map products (not storm-relative velocity, which uses Level II + motion). */
|
|
387
|
+
export function nexradLayerUsesTrueLevel3Listing(layer: {
|
|
388
|
+
radar?: string | null;
|
|
389
|
+
radarDataSource?: string;
|
|
390
|
+
radarVariable?: string;
|
|
391
|
+
level3StormRelative?: boolean;
|
|
392
|
+
}): boolean {
|
|
393
|
+
return (
|
|
394
|
+
layer.radarDataSource === 'level3' && !nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Terminal radars do not support true Level-III map products in this app (SRV only). */
|
|
399
|
+
export function terminalRadarBlocksTrueLevel3(layer: {
|
|
400
|
+
radar?: string | null;
|
|
401
|
+
radarDataSource?: string;
|
|
402
|
+
radarVariable?: string;
|
|
403
|
+
level3StormRelative?: boolean;
|
|
404
|
+
}): boolean {
|
|
405
|
+
const site = layer.radar || '';
|
|
406
|
+
return isTerminalRadar(site) && nexradLayerUsesTrueLevel3Listing(layer);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Digital (N0H) and hybrid (HHC) hydrometeor classification — discrete classes, not a continuous field. */
|
|
410
|
+
export function nexradLevel3IsHydrometeorClassification(radarVariable: string | undefined): boolean {
|
|
411
|
+
const v = radarVariable || '';
|
|
412
|
+
return v === 'N0H' || v === 'HHC';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export type Level3ExpandTarget = { radarKey: string; attachMotionMap: boolean };
|
|
416
|
+
|
|
417
|
+
/** Map raw S3 product code to one or more formatted time-anchor variables. */
|
|
418
|
+
export function getLevel3ExpansionTargetsForS3Product(product: string): Level3ExpandTarget[] {
|
|
419
|
+
if (product === NEXRAD_LEVEL3_MOTION_PRODUCT) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
if (product === 'DVL') {
|
|
423
|
+
// Emit both keys so old NVL layers and new DVL layers resolve the same time map.
|
|
424
|
+
return [
|
|
425
|
+
{ radarKey: 'DVL', attachMotionMap: false },
|
|
426
|
+
{ radarKey: 'NVL', attachMotionMap: false },
|
|
427
|
+
];
|
|
428
|
+
}
|
|
429
|
+
if (product === 'N0G') {
|
|
430
|
+
return [
|
|
431
|
+
{ radarKey: 'N0G', attachMotionMap: false },
|
|
432
|
+
{ radarKey: 'VEL', attachMotionMap: true },
|
|
433
|
+
];
|
|
434
|
+
}
|
|
435
|
+
if (nexradLevel3IsTiltIndexedKdpProduct(product)) {
|
|
436
|
+
return [{ radarKey: 'KDP', attachMotionMap: false }];
|
|
437
|
+
}
|
|
438
|
+
if (nexradLevel3IsTiltIndexedDhcProduct(product)) {
|
|
439
|
+
return [{ radarKey: 'N0H', attachMotionMap: false }];
|
|
440
|
+
}
|
|
441
|
+
return [{ radarKey: product, attachMotionMap: false }];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Product 135 (EET): MetPy `DigitalEETMapper` uses PDB thr1–thr3 as data mask, scale, and offset:
|
|
446
|
+
* `mapped = ((level & mask) - offset) / scale` (raw signed halfwords — no /10; unlike dBZ min/inc).
|
|
447
|
+
*
|
|
448
|
+
* Output is treated as **kilometers** end-to-end with the rest of `tops_kft` packing.
|
|
449
|
+
*/
|
|
450
|
+
export function level3EetHeightKmFromLevel(
|
|
451
|
+
level: number,
|
|
452
|
+
dataMaskRaw: number,
|
|
453
|
+
scaleRaw: number,
|
|
454
|
+
offsetRaw: number,
|
|
455
|
+
): number | null {
|
|
456
|
+
if (level < 2) return null;
|
|
457
|
+
const masked = level & (dataMaskRaw & 0xffff);
|
|
458
|
+
if (!Number.isFinite(scaleRaw) || scaleRaw === 0) {
|
|
459
|
+
return level - 2;
|
|
460
|
+
}
|
|
461
|
+
const mapped = (masked - offsetRaw) / scaleRaw;
|
|
462
|
+
if (!Number.isFinite(mapped)) return null;
|
|
463
|
+
return mapped;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function level3ValuePacking(decodeMode: Level3DecodeMode): { valueScale: number; valueOffset: number } {
|
|
467
|
+
switch (decodeMode) {
|
|
468
|
+
case 'velocity':
|
|
469
|
+
return { valueScale: 0.01, valueOffset: 0 };
|
|
470
|
+
case 'dbz':
|
|
471
|
+
return { valueScale: 0.5, valueOffset: -32 };
|
|
472
|
+
case 'precip':
|
|
473
|
+
return { valueScale: 0.001, valueOffset: 0 };
|
|
474
|
+
case 'categorical':
|
|
475
|
+
return { valueScale: 1, valueOffset: 0 };
|
|
476
|
+
case 'tops_kft':
|
|
477
|
+
// Shader physical = raw * valueScale (km). raw = round(km / valueScale) must stay within int16.
|
|
478
|
+
// 0.001 implied raw = meters → tops >32.767 km clamped (seen on EET ~48 km). Use 2 m/LSb → max ~65.5 km.
|
|
479
|
+
return { valueScale: 0.002, valueOffset: 0 };
|
|
480
|
+
case 'vil':
|
|
481
|
+
return { valueScale: 0.1, valueOffset: 0 };
|
|
482
|
+
case 'generic_physical':
|
|
483
|
+
return { valueScale: 0.01, valueOffset: 0 };
|
|
484
|
+
default:
|
|
485
|
+
return { valueScale: 0.5, valueOffset: -32 };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Opt-in storm-relative velocity (super-res VEL + N0S motion). Set in localStorage to trace: DEBUG_NEXRAD_VEL=1
|
|
491
|
+
*/
|
|
492
|
+
function nexradVelDebug(label: string, data: Record<string, unknown>) {
|
|
493
|
+
try {
|
|
494
|
+
if (typeof localStorage !== 'undefined' && localStorage.getItem('DEBUG_NEXRAD_VEL') === '1') {
|
|
495
|
+
console.log(`[NEXRAD_VEL] ${label}`, data);
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
/* ignore */
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Storm-relative velocity is rendered from Level-2 super-res VEL at the user’s tilt plus L3 N0S motion.
|
|
504
|
+
* Layers may still have `radarDataSource: 'level3'` when chosen from the Level-III menu.
|
|
505
|
+
*
|
|
506
|
+
* `level3StormRelative === undefined` must NOT default to “on” for Level-II velocity (that broke base VEL).
|
|
507
|
+
* For legacy layers with L3-menu VEL and no flag, infer SRV only when `radarDataSource === 'level3'`.
|
|
508
|
+
*/
|
|
509
|
+
export function nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer: {
|
|
510
|
+
radar?: string | null;
|
|
511
|
+
radarVariable?: string;
|
|
512
|
+
level3StormRelative?: boolean;
|
|
513
|
+
radarDataSource?: string;
|
|
514
|
+
}): boolean {
|
|
515
|
+
if ((layer.radarVariable || 'REF') !== 'VEL') {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
if (layer.level3StormRelative === true) return true;
|
|
519
|
+
if (layer.level3StormRelative === false) return false;
|
|
520
|
+
const inferred = (layer.radarDataSource || 'level2') === 'level3';
|
|
521
|
+
nexradVelDebug('VEL level3StormRelative missing — infer from radarDataSource', {
|
|
522
|
+
radar: layer.radar,
|
|
523
|
+
radarDataSource: layer.radarDataSource,
|
|
524
|
+
useStormRelativeVelocity: inferred,
|
|
525
|
+
});
|
|
526
|
+
return inferred;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* True when composite time keys and map wiring should use `layer.radarTilt` (like Level II), not a fixed 0.5° token.
|
|
531
|
+
*/
|
|
532
|
+
export function nexradLevel3LayerUsesRadarTiltInCompositeKeys(layer: {
|
|
533
|
+
radar?: string | null;
|
|
534
|
+
radarVariable?: string;
|
|
535
|
+
radarDataSource?: string;
|
|
536
|
+
level3StormRelative?: boolean;
|
|
537
|
+
}): boolean {
|
|
538
|
+
if (layer.radarDataSource !== 'level3') return false;
|
|
539
|
+
if (nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer)) return false;
|
|
540
|
+
return nexradLevel3UsesTiltIndexedS3Products(layer.radarVariable);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function nexradEffectiveFetchSource(layer: {
|
|
544
|
+
radar?: string | null;
|
|
545
|
+
radarVariable?: string;
|
|
546
|
+
radarDataSource?: string;
|
|
547
|
+
level3StormRelative?: boolean;
|
|
548
|
+
}): 'level2' | 'level3' {
|
|
549
|
+
if (terminalRadarBlocksTrueLevel3(layer)) return 'level2';
|
|
550
|
+
if (nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer)) return 'level2';
|
|
551
|
+
return layer.radarDataSource === 'level3' ? 'level3' : 'level2';
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function nexradTimesFetchOptionsFromLayer(layer: {
|
|
555
|
+
radar?: string | null;
|
|
556
|
+
radarVariable?: string;
|
|
557
|
+
radarDataSource?: string;
|
|
558
|
+
level3StormRelative?: boolean;
|
|
559
|
+
radarTilt?: number | null;
|
|
560
|
+
}): { level3Product?: string; level3StormRelative?: boolean } | undefined {
|
|
561
|
+
if (terminalRadarBlocksTrueLevel3(layer)) return undefined;
|
|
562
|
+
/** L2 fetch + N0S time alignment for storm-relative velocity (super-res VEL + motion). */
|
|
563
|
+
if (nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer)) {
|
|
564
|
+
return { level3StormRelative: true };
|
|
565
|
+
}
|
|
566
|
+
if (layer.radarDataSource !== 'level3') return undefined;
|
|
567
|
+
const v = layer.radarVariable || 'REF';
|
|
568
|
+
const entry = getNexradLevel3EntryByRadarKey(v);
|
|
569
|
+
let product = entry?.product ?? (v === 'VEL' ? 'N0G' : v);
|
|
570
|
+
const site = layer.radar || '';
|
|
571
|
+
if (site && nexradLevel3UsesTiltIndexedS3Products(v)) {
|
|
572
|
+
const tilt = layer.radarTilt ?? getDefaultRadarTilt(site);
|
|
573
|
+
product = nexradLevel3S3ProductForSiteTilt(site, v as 'KDP' | 'N0H', tilt);
|
|
574
|
+
}
|
|
575
|
+
const stormRelative = v === 'VEL' ? nexradStormRelativeVelocityUsesLevel2TiltAndFetch(layer) : false;
|
|
576
|
+
return { level3Product: product, level3StormRelative: stormRelative };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function formatLevel3TiltForApi(): string {
|
|
580
|
+
return NEXRAD_LEVEL3_ELEV;
|
|
581
|
+
}
|