@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
|
@@ -1420,18 +1420,23 @@ function decodeLevel3RasterProduct(buffer: ArrayBuffer, objectKey: string, radar
|
|
|
1420
1420
|
}
|
|
1421
1421
|
}
|
|
1422
1422
|
|
|
1423
|
-
const GROUP2_VARS = ['
|
|
1423
|
+
const GROUP2_VARS = ['SW'];
|
|
1424
|
+
/** g2 split-cut in clear-air mode (VCP 35): REF + VEL + SW in one file (legacy archives only). */
|
|
1425
|
+
const GROUP2_COMBINED_3_VARS = ['REF', 'VEL', 'SW'];
|
|
1424
1426
|
/** g1 dual-pol + REF; order must match writer. KDP is Level-III (N0K), not in g1 bins. */
|
|
1425
1427
|
const GROUP1_VARS = ['REF', 'ZDR', 'RHO', 'PHI'] as const;
|
|
1426
1428
|
/** g2 combined tilt with KDP (7 slots); order must match writer. */
|
|
1427
1429
|
const GROUP2_COMBINED_7_VARS = ['REF', 'ZDR', 'RHO', 'PHI', 'KDP', 'VEL', 'SW'];
|
|
1428
1430
|
const GROUP2_COMBINED_VARS = ['REF', 'ZDR', 'RHO', 'PHI', 'VEL', 'SW'];
|
|
1429
1431
|
|
|
1430
|
-
// Fixed sizes matching the Python writer
|
|
1431
|
-
const FILE_HDR_BYTES
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1432
|
+
// Fixed sizes matching the Python writer (`lambda_function.py` MAX_RAYS / AZ_BLOCK_BYTES).
|
|
1433
|
+
const FILE_HDR_BYTES = 64;
|
|
1434
|
+
const LEVEL2_AZ_BLOCK_RAYS = 720;
|
|
1435
|
+
const LEVEL2_AZ_BLOCK_BYTES = LEVEL2_AZ_BLOCK_RAYS * 4;
|
|
1436
|
+
/** Per-sweep Nyquist (m/s), big-endian float32 after azimuth block and before slot index (writer `nyquist_bytes`). */
|
|
1437
|
+
const LEVEL2_FILE_NYQUIST_BYTES = 4;
|
|
1438
|
+
const SLOT_INDEX_ENTRY = 18;
|
|
1439
|
+
const MAX_SLOTS = 7; // g2 combined tilt can write 7 fields (GROUP2_COMBINED_7_VARS)
|
|
1435
1440
|
|
|
1436
1441
|
interface FileHeader {
|
|
1437
1442
|
unixTime: number;
|
|
@@ -1441,8 +1446,10 @@ interface FileHeader {
|
|
|
1441
1446
|
firstGateKm: number;
|
|
1442
1447
|
gateWidthKm: number;
|
|
1443
1448
|
nSlots: number;
|
|
1444
|
-
azimuthsBuffer: ArrayBuffer; // raw big-endian float32 bytes
|
|
1449
|
+
azimuthsBuffer: ArrayBuffer; // raw big-endian float32 bytes (720*4)
|
|
1445
1450
|
slots: Array<{ offset: number; compressedSize: number; uncompressedSize: number }>;
|
|
1451
|
+
/** Nyquist velocity (m/s) from file header after azimuths; null if non-finite / out of band. */
|
|
1452
|
+
embeddedNyquistMs: number | null;
|
|
1446
1453
|
}
|
|
1447
1454
|
|
|
1448
1455
|
function int16ToFloat16(val: number): number {
|
|
@@ -1455,7 +1462,30 @@ function int16ToFloat16(val: number): number {
|
|
|
1455
1462
|
return Math.pow(-1, sign) * Math.pow(2, exponent - 16) * (1 + fraction / Math.pow(2, 10));
|
|
1456
1463
|
}
|
|
1457
1464
|
|
|
1458
|
-
|
|
1465
|
+
/**
|
|
1466
|
+
* Total Level-II object size when known.
|
|
1467
|
+
* For 206 partial responses, only `Content-Range: bytes a-b/total` gives `total` — `Content-Length` is the segment size.
|
|
1468
|
+
*/
|
|
1469
|
+
function nexradLevel2ResourceTotalBytes(resp: Response): number | null {
|
|
1470
|
+
const cr = resp.headers.get('Content-Range');
|
|
1471
|
+
if (cr) {
|
|
1472
|
+
const m = cr.trim().match(/\/(\d+)\s*$/);
|
|
1473
|
+
if (m) {
|
|
1474
|
+
const n = Number(m[1]);
|
|
1475
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (resp.status === 200) {
|
|
1479
|
+
const cl = resp.headers.get('Content-Length');
|
|
1480
|
+
if (cl) {
|
|
1481
|
+
const n = Number(cl);
|
|
1482
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function parseFileHeader(buffer: ArrayBuffer, azBlockBytes: number): FileHeader {
|
|
1459
1489
|
const view = new DataView(buffer);
|
|
1460
1490
|
const magic = view.getUint32(0, false);
|
|
1461
1491
|
if (magic !== 0x4E584244) throw new Error(`Bad magic: 0x${magic.toString(16)}`);
|
|
@@ -1468,24 +1498,42 @@ function parseFileHeader(buffer: ArrayBuffer): FileHeader {
|
|
|
1468
1498
|
const gateWidthKm = view.getFloat32(20, false);
|
|
1469
1499
|
const nSlots = view.getUint16(24, false);
|
|
1470
1500
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1501
|
+
const azimuthsBuffer = buffer.slice(FILE_HDR_BYTES, FILE_HDR_BYTES + azBlockBytes);
|
|
1502
|
+
|
|
1503
|
+
const azEnd = FILE_HDR_BYTES + azBlockBytes;
|
|
1504
|
+
const idxStart = azEnd + LEVEL2_FILE_NYQUIST_BYTES;
|
|
1505
|
+
const needBytes = idxStart + nSlots * SLOT_INDEX_ENTRY;
|
|
1506
|
+
if (buffer.byteLength < needBytes) {
|
|
1507
|
+
throw new Error(`level2 header buffer too small: ${buffer.byteLength} < ${needBytes}`);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const nyqCandidate = view.getFloat32(azEnd, false);
|
|
1511
|
+
const embeddedNyquistMs =
|
|
1512
|
+
Number.isFinite(nyqCandidate) && nyqCandidate > 0.5 && nyqCandidate < 128 ? nyqCandidate : null;
|
|
1473
1513
|
|
|
1474
|
-
|
|
1475
|
-
const idxStart = FILE_HDR_BYTES + AZ_BLOCK_BYTES;
|
|
1476
|
-
const slots = [];
|
|
1514
|
+
const slots: Array<{ offset: number; compressedSize: number; uncompressedSize: number }> = [];
|
|
1477
1515
|
for (let i = 0; i < nSlots; i++) {
|
|
1478
1516
|
const base = idxStart + i * SLOT_INDEX_ENTRY;
|
|
1479
|
-
const offsetHigh = view.getUint32(base,
|
|
1480
|
-
const offsetLow
|
|
1517
|
+
const offsetHigh = view.getUint32(base, false);
|
|
1518
|
+
const offsetLow = view.getUint32(base + 4, false);
|
|
1481
1519
|
const offset = offsetHigh * 2 ** 32 + offsetLow;
|
|
1482
|
-
const compressedSize
|
|
1520
|
+
const compressedSize = view.getUint32(base + 8, false);
|
|
1483
1521
|
const uncompressedSize = view.getUint32(base + 12, false);
|
|
1484
1522
|
slots.push({ offset, compressedSize, uncompressedSize });
|
|
1485
1523
|
}
|
|
1486
1524
|
|
|
1487
|
-
return {
|
|
1488
|
-
|
|
1525
|
+
return {
|
|
1526
|
+
unixTime,
|
|
1527
|
+
nRays,
|
|
1528
|
+
nGates,
|
|
1529
|
+
elevAngle,
|
|
1530
|
+
firstGateKm,
|
|
1531
|
+
gateWidthKm,
|
|
1532
|
+
nSlots,
|
|
1533
|
+
azimuthsBuffer,
|
|
1534
|
+
slots,
|
|
1535
|
+
embeddedNyquistMs,
|
|
1536
|
+
};
|
|
1489
1537
|
}
|
|
1490
1538
|
|
|
1491
1539
|
function decodeSweepInWorker(
|
|
@@ -1550,11 +1598,34 @@ function resolveLevel3SrvMotionObjectKey(
|
|
|
1550
1598
|
return pickNearestLevel3ObjectKey(unixTime, motionMap);
|
|
1551
1599
|
}
|
|
1552
1600
|
|
|
1601
|
+
/** Reuse parsed N0S motion across time steps (SRV: same motion key is common while scrubbing). */
|
|
1602
|
+
const N0S_MOTION_CACHE_MAX = 128;
|
|
1603
|
+
const n0sMotionVectorCache = new Map<string, { speedMs: number; directionDeg: number }>();
|
|
1604
|
+
|
|
1605
|
+
function getCachedN0sMotion(motionObjectKey: string) {
|
|
1606
|
+
return n0sMotionVectorCache.get(motionObjectKey);
|
|
1607
|
+
}
|
|
1608
|
+
function setCachedN0sMotion(
|
|
1609
|
+
motionObjectKey: string,
|
|
1610
|
+
motion: { speedMs: number; directionDeg: number },
|
|
1611
|
+
) {
|
|
1612
|
+
n0sMotionVectorCache.set(motionObjectKey, motion);
|
|
1613
|
+
while (n0sMotionVectorCache.size > N0S_MOTION_CACHE_MAX) {
|
|
1614
|
+
const first = n0sMotionVectorCache.keys().next().value as string | undefined;
|
|
1615
|
+
if (first === undefined) break;
|
|
1616
|
+
n0sMotionVectorCache.delete(first);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1553
1620
|
async function applyStormMotionFromN0sObjectKey(
|
|
1554
1621
|
frame: DecodedRadarFrame,
|
|
1555
1622
|
motionObjectKey: string,
|
|
1556
1623
|
logLabel: string,
|
|
1557
1624
|
): Promise<DecodedRadarFrame> {
|
|
1625
|
+
const cached = getCachedN0sMotion(motionObjectKey);
|
|
1626
|
+
if (cached) {
|
|
1627
|
+
return applyLevel3StormRelativeToFrame(frame, cached.speedMs, cached.directionDeg);
|
|
1628
|
+
}
|
|
1558
1629
|
const motionUrl = objectKeyToUrl(motionObjectKey, 'level3');
|
|
1559
1630
|
try {
|
|
1560
1631
|
const motionResp = await fetch(motionUrl);
|
|
@@ -1562,6 +1633,7 @@ async function applyStormMotionFromN0sObjectKey(
|
|
|
1562
1633
|
const motionBuf = await motionResp.arrayBuffer();
|
|
1563
1634
|
const motion = parseLevel3StormMotionFromBuffer(motionBuf);
|
|
1564
1635
|
if (motion) {
|
|
1636
|
+
setCachedN0sMotion(motionObjectKey, motion);
|
|
1565
1637
|
return applyLevel3StormRelativeToFrame(frame, motion.speedMs, motion.directionDeg);
|
|
1566
1638
|
}
|
|
1567
1639
|
}
|
|
@@ -1648,7 +1720,9 @@ export async function fetchAndParseArchive(
|
|
|
1648
1720
|
|
|
1649
1721
|
// ── Level-2 two-request range path ───────────────────────────────────────
|
|
1650
1722
|
// Request 1: fetch just the header + slot index to find byte offsets
|
|
1651
|
-
const
|
|
1723
|
+
const azBlockBytes = LEVEL2_AZ_BLOCK_BYTES;
|
|
1724
|
+
const INDEX_FETCH_BYTES =
|
|
1725
|
+
FILE_HDR_BYTES + azBlockBytes + LEVEL2_FILE_NYQUIST_BYTES + MAX_SLOTS * SLOT_INDEX_ENTRY;
|
|
1652
1726
|
const indexResp = await fetch(url, {
|
|
1653
1727
|
headers: {
|
|
1654
1728
|
'x-api-key': NEXRAD_ARCHIVE_API_KEY,
|
|
@@ -1659,7 +1733,21 @@ export async function fetchAndParseArchive(
|
|
|
1659
1733
|
throw new Error(`HTTP ${indexResp.status} fetching level2 index ${url}`);
|
|
1660
1734
|
}
|
|
1661
1735
|
const indexBuffer = await indexResp.arrayBuffer();
|
|
1662
|
-
const
|
|
1736
|
+
const indexResourceTotal = nexradLevel2ResourceTotalBytes(indexResp);
|
|
1737
|
+
const header = parseFileHeader(indexBuffer, azBlockBytes);
|
|
1738
|
+
|
|
1739
|
+
const slotIndexBytes =
|
|
1740
|
+
FILE_HDR_BYTES + azBlockBytes + LEVEL2_FILE_NYQUIST_BYTES + header.nSlots * SLOT_INDEX_ENTRY;
|
|
1741
|
+
if (indexBuffer.byteLength < slotIndexBytes) {
|
|
1742
|
+
console.warn('[RadarLayer] level2 index response shorter than slot table', {
|
|
1743
|
+
objectKey,
|
|
1744
|
+
byteLength: indexBuffer.byteLength,
|
|
1745
|
+
need: slotIndexBytes,
|
|
1746
|
+
nSlots: header.nSlots,
|
|
1747
|
+
});
|
|
1748
|
+
setArchiveCache(cacheKey, null);
|
|
1749
|
+
return null;
|
|
1750
|
+
}
|
|
1663
1751
|
|
|
1664
1752
|
// ── Step 2: find slot for this variable ─────────────────────
|
|
1665
1753
|
let varList: string[];
|
|
@@ -1667,6 +1755,8 @@ export async function fetchAndParseArchive(
|
|
|
1667
1755
|
varList = GROUP2_COMBINED_7_VARS;
|
|
1668
1756
|
} else if (groupId === 2 && header.nSlots === 6) {
|
|
1669
1757
|
varList = GROUP2_COMBINED_VARS;
|
|
1758
|
+
} else if (groupId === 2 && header.nSlots === 3) {
|
|
1759
|
+
varList = GROUP2_COMBINED_3_VARS;
|
|
1670
1760
|
} else if (groupId === 2) {
|
|
1671
1761
|
varList = GROUP2_VARS;
|
|
1672
1762
|
} else {
|
|
@@ -1688,17 +1778,44 @@ export async function fetchAndParseArchive(
|
|
|
1688
1778
|
}
|
|
1689
1779
|
|
|
1690
1780
|
// ── Step 3: fetch exactly the slot bytes ────────────────────
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1781
|
+
const slotEndExclusive = slot.offset + slot.compressedSize;
|
|
1782
|
+
if (indexResourceTotal != null && slotEndExclusive > indexResourceTotal) {
|
|
1783
|
+
console.warn('[RadarLayer] level2 slot extends past object size (bad index or stale CDN)', {
|
|
1784
|
+
objectKey,
|
|
1785
|
+
radarVariable,
|
|
1786
|
+
slotIdx,
|
|
1787
|
+
offset: slot.offset,
|
|
1788
|
+
compressedSize: slot.compressedSize,
|
|
1789
|
+
indexResourceTotal,
|
|
1790
|
+
});
|
|
1791
|
+
setArchiveCache(cacheKey, null);
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
const slotRangeEnd = slotEndExclusive - 1;
|
|
1796
|
+
const slotHeaders: Record<string, string> = {
|
|
1797
|
+
'x-api-key': NEXRAD_ARCHIVE_API_KEY,
|
|
1798
|
+
'Range': `bytes=${slot.offset}-${slotRangeEnd}`,
|
|
1799
|
+
};
|
|
1800
|
+
let slotResp = await fetch(url, { headers: slotHeaders });
|
|
1801
|
+
let slotBuffer: ArrayBuffer;
|
|
1802
|
+
if (slotResp.ok || slotResp.status === 206) {
|
|
1803
|
+
slotBuffer = await slotResp.arrayBuffer();
|
|
1804
|
+
} else if (slotResp.status === 416) {
|
|
1805
|
+
const fullResp = await fetch(url, { headers: { 'x-api-key': NEXRAD_ARCHIVE_API_KEY } });
|
|
1806
|
+
if (!fullResp.ok) {
|
|
1807
|
+
throw new Error(`HTTP ${fullResp.status} full fetch after 416 for level2 ${url}`);
|
|
1808
|
+
}
|
|
1809
|
+
const fullBuf = await fullResp.arrayBuffer();
|
|
1810
|
+
if (slot.offset >= fullBuf.byteLength || slotEndExclusive > fullBuf.byteLength) {
|
|
1811
|
+
throw new Error(
|
|
1812
|
+
`level2 slot out of bounds after 416 fallback (offset=${slot.offset}, end=${slotEndExclusive}, file=${fullBuf.byteLength}) for ${url}`,
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
slotBuffer = fullBuf.slice(slot.offset, slotEndExclusive);
|
|
1816
|
+
} else {
|
|
1699
1817
|
throw new Error(`HTTP ${slotResp.status} fetching level2 slot ${url}`);
|
|
1700
1818
|
}
|
|
1701
|
-
const slotBuffer = await slotResp.arrayBuffer();
|
|
1702
1819
|
|
|
1703
1820
|
// ── Step 4: decode in worker ────────────────────────────────
|
|
1704
1821
|
const sites = await loadNexradSites();
|
|
@@ -1709,6 +1826,7 @@ export async function fetchAndParseArchive(
|
|
|
1709
1826
|
);
|
|
1710
1827
|
|
|
1711
1828
|
if (!decoded) { setArchiveCache(cacheKey, null); return null; }
|
|
1829
|
+
decoded = { ...decoded, embeddedNyquistMs: header.embeddedNyquistMs };
|
|
1712
1830
|
const l2MotionKey = options?.level3MotionObjectKey;
|
|
1713
1831
|
if (l2MotionKey && radarVariable === 'VEL') {
|
|
1714
1832
|
decoded = await applyStormMotionFromN0sObjectKey(
|
package/src/nwsAlertsSupport.js
CHANGED
|
@@ -1190,6 +1190,56 @@ export function cloneNwwsFeatureCollectionStrippingVolatile(data) {
|
|
|
1190
1190
|
};
|
|
1191
1191
|
}
|
|
1192
1192
|
|
|
1193
|
+
/**
|
|
1194
|
+
* Parse `properties.parameters` (JSON string or object from NWWS / CAP-style extensions).
|
|
1195
|
+
* @param {unknown} raw
|
|
1196
|
+
* @returns {Record<string, unknown>|null}
|
|
1197
|
+
*/
|
|
1198
|
+
function parseNwsParametersJson(raw) {
|
|
1199
|
+
if (raw == null) return null;
|
|
1200
|
+
if (typeof raw === 'object' && !Array.isArray(raw)) return /** @type {Record<string, unknown>} */ (raw);
|
|
1201
|
+
if (typeof raw === 'string') {
|
|
1202
|
+
try {
|
|
1203
|
+
const o = JSON.parse(raw);
|
|
1204
|
+
return o && typeof o === 'object' && !Array.isArray(o) ? o : null;
|
|
1205
|
+
} catch {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Human-oriented subset of `parameters` for severe wx / common warning types (hail, wind, radar source).
|
|
1214
|
+
* @param {Record<string, unknown>|null} pObj
|
|
1215
|
+
* @returns {Record<string, string> | null}
|
|
1216
|
+
*/
|
|
1217
|
+
function buildHazardDetailsFromParameters(pObj) {
|
|
1218
|
+
if (!pObj || typeof pObj !== 'object') return null;
|
|
1219
|
+
/** @type {Record<string, string>} */
|
|
1220
|
+
const out = {};
|
|
1221
|
+
const set = (key, from) => {
|
|
1222
|
+
const v = pObj[from];
|
|
1223
|
+
if (v != null && String(v).trim() !== '') out[key] = String(v);
|
|
1224
|
+
};
|
|
1225
|
+
set('source', 'source');
|
|
1226
|
+
set('maxHailSize', 'max_hail_size');
|
|
1227
|
+
set('maxWindGust', 'max_wind_gust');
|
|
1228
|
+
set('damageThreat', 'damage_threat');
|
|
1229
|
+
set('wmo', 'wmo');
|
|
1230
|
+
if (pObj.tornado_detection != null && String(pObj.tornado_detection).trim() !== '')
|
|
1231
|
+
out.tornadoDetection = String(pObj.tornado_detection);
|
|
1232
|
+
if (pObj.flood_detection != null && String(pObj.flood_detection).trim() !== '')
|
|
1233
|
+
out.floodDetection = String(pObj.flood_detection);
|
|
1234
|
+
return Object.keys(out).length ? out : null;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Normalized payload for `nws:alert:click`. Top-level fields are derived from `feature.properties`
|
|
1239
|
+
* so you rarely need the duplicate `properties` object (kept for advanced / forward-compatible use).
|
|
1240
|
+
*
|
|
1241
|
+
* @param {GeoJSON.Feature|{ properties?: Record<string, unknown> }} feature
|
|
1242
|
+
*/
|
|
1193
1243
|
export function buildAlertClickPayload(feature) {
|
|
1194
1244
|
const properties = feature?.properties ? { ...feature.properties } : {};
|
|
1195
1245
|
const eventName = getNwsAlertEventLabelFromProperties(properties);
|
|
@@ -1204,9 +1254,10 @@ export function buildAlertClickPayload(feature) {
|
|
|
1204
1254
|
if (!Array.isArray(tags)) {
|
|
1205
1255
|
tags = tags != null ? [String(tags)] : [];
|
|
1206
1256
|
}
|
|
1207
|
-
const headline = properties.headline != null ? String(properties.headline) : '';
|
|
1208
1257
|
const name =
|
|
1209
|
-
headline
|
|
1258
|
+
(properties.headline != null && String(properties.headline).trim() !== ''
|
|
1259
|
+
? String(properties.headline)
|
|
1260
|
+
: '') ||
|
|
1210
1261
|
eventName ||
|
|
1211
1262
|
(properties.event != null ? String(properties.event) : '') ||
|
|
1212
1263
|
(properties.event_name != null ? String(properties.event_name) : '');
|
|
@@ -1216,8 +1267,6 @@ export function buildAlertClickPayload(feature) {
|
|
|
1216
1267
|
: properties.raw_text != null
|
|
1217
1268
|
? String(properties.raw_text)
|
|
1218
1269
|
: '';
|
|
1219
|
-
const summary = properties.summary != null ? String(properties.summary) : '';
|
|
1220
|
-
const instruction = properties.instruction != null ? String(properties.instruction) : '';
|
|
1221
1270
|
|
|
1222
1271
|
const startUnix =
|
|
1223
1272
|
typeof properties.start_unix === 'number'
|
|
@@ -1232,17 +1281,29 @@ export function buildAlertClickPayload(feature) {
|
|
|
1232
1281
|
? properties.active_start_unix
|
|
1233
1282
|
: getNwsActiveStartUnix(properties);
|
|
1234
1283
|
|
|
1284
|
+
const parametersParsed = parseNwsParametersJson(properties.parameters);
|
|
1285
|
+
const hazardDetails = buildHazardDetailsFromParameters(parametersParsed);
|
|
1286
|
+
|
|
1235
1287
|
return {
|
|
1236
1288
|
name,
|
|
1237
1289
|
eventName,
|
|
1238
|
-
headline,
|
|
1239
|
-
summary,
|
|
1240
1290
|
description,
|
|
1241
|
-
instruction,
|
|
1242
1291
|
tags,
|
|
1243
1292
|
startUnix,
|
|
1244
1293
|
endUnix,
|
|
1245
1294
|
activeStartUnix,
|
|
1295
|
+
/** ISO-ish strings from the feed (see also unix fields above). */
|
|
1296
|
+
issued: properties.issued != null ? String(properties.issued) : '',
|
|
1297
|
+
updatedAt: properties.updated_at != null ? String(properties.updated_at) : '',
|
|
1298
|
+
expiresAt: properties.expires != null ? String(properties.expires) : '',
|
|
1299
|
+
alertId: properties.alert_id != null ? String(properties.alert_id) : '',
|
|
1300
|
+
office: properties.office != null ? String(properties.office) : '',
|
|
1301
|
+
nwsProductKey: properties.nws_product_key != null ? String(properties.nws_product_key) : '',
|
|
1302
|
+
/** Parsed `properties.parameters` JSON when present (hail/wind/source keys vary by product). */
|
|
1303
|
+
parametersParsed,
|
|
1304
|
+
/** Short labels for SV / similar products: source, maxHailSize, maxWindGust, damageThreat, … */
|
|
1305
|
+
hazardDetails,
|
|
1306
|
+
/** Same as `feature.properties` — prefer top-level fields above when possible. */
|
|
1246
1307
|
properties,
|
|
1247
1308
|
};
|
|
1248
1309
|
}
|