@aguacerowx/mapsgl 0.0.49 → 0.0.51
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 +26 -7
- package/src/NwsWatchesWarningsOverlay.js +858 -789
- package/src/WeatherLayerManager.js +61 -298
- package/src/nwsAlertsSupport.js +160 -7
package/src/nwsAlertsSupport.js
CHANGED
|
@@ -442,6 +442,98 @@ export function getNwsAlertInstanceIdFromFeature(feature) {
|
|
|
442
442
|
);
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/**
|
|
446
|
+
* SVR revision visibility: superseded geometry only before cutover unix; updated geometry only at/after cutover.
|
|
447
|
+
* Returns null when this feature does not use revision split (fall back to start_unix / end_unix).
|
|
448
|
+
* Matches aguacero-frontend {@link nwsRevisionVisibilityActive}.
|
|
449
|
+
*/
|
|
450
|
+
export function nwsRevisionVisibilityActive(properties, referenceUnix) {
|
|
451
|
+
if (!properties) return null;
|
|
452
|
+
const role = properties._nws_revision_role;
|
|
453
|
+
const cut = properties._nws_revision_cutover_unix;
|
|
454
|
+
if (cut == null || typeof cut !== 'number' || !role) return null;
|
|
455
|
+
if (role === 'superseded') {
|
|
456
|
+
return referenceUnix < cut;
|
|
457
|
+
}
|
|
458
|
+
if (role === 'update') {
|
|
459
|
+
return referenceUnix >= cut;
|
|
460
|
+
}
|
|
461
|
+
if (role === 'current') {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** nwws-server stores ids as `${vtecBaseId}.${issuedTs}`; strip the issued suffix for revision grouping. */
|
|
468
|
+
function stripNwwsIssuedMillisSuffixFromId(id) {
|
|
469
|
+
const m = String(id).match(/^(.+)\.(\d{10,})$/);
|
|
470
|
+
return m ? m[1] : id;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* One key per logical warning product (VTEC sequence / base id). Matches aguacero-frontend
|
|
475
|
+
* {@link getNwwsLogicalProductKey} for deduping superseded vs update rows on the time slider.
|
|
476
|
+
*/
|
|
477
|
+
export function getNwwsLogicalProductKey(feature) {
|
|
478
|
+
const p = feature?.properties ?? {};
|
|
479
|
+
const params = p.parameters;
|
|
480
|
+
const vtecRaw = params != null && typeof params === 'object' && !Array.isArray(params) ? params.VTEC : undefined;
|
|
481
|
+
const vtec = Array.isArray(vtecRaw)
|
|
482
|
+
? vtecRaw[0]
|
|
483
|
+
: p.vtec ?? (p.details && typeof p.details === 'object' ? p.details.pvtec : undefined);
|
|
484
|
+
if (typeof vtec === 'string' && vtec.trim()) {
|
|
485
|
+
const parts = vtec.split('.');
|
|
486
|
+
if (parts.length >= 6) {
|
|
487
|
+
return `${parts[2]}.${parts[3]}.${parts[4]}.${parts[5]}`;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const base = getNwsBaseAlertIdFromFeature(feature);
|
|
491
|
+
if (base) return stripNwwsIssuedMillisSuffixFromId(base);
|
|
492
|
+
if (feature?.id != null && feature.id !== '') return stripNwwsIssuedMillisSuffixFromId(String(feature.id));
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function getNwwsProductKeyFromFeature(feature) {
|
|
497
|
+
const pk = feature?.properties?.nws_product_key;
|
|
498
|
+
if (typeof pk === 'string' && pk.trim() !== '') return pk.trim();
|
|
499
|
+
return getNwwsLogicalProductKey(feature);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* When superseded + update rows both pass time checks, pick one per reference instant (same product key).
|
|
504
|
+
* Matches aguacero-frontend {@link pickBestNwsRevisionAmongConflictingFeatures}.
|
|
505
|
+
*/
|
|
506
|
+
export function pickBestNwsRevisionAmongConflictingFeatures(features, referenceUnix) {
|
|
507
|
+
if (!Array.isArray(features) || features.length === 0) return null;
|
|
508
|
+
if (features.length === 1) return features[0];
|
|
509
|
+
const scored = features.map((f) => {
|
|
510
|
+
const p = f?.properties ?? {};
|
|
511
|
+
const role = String(p._nws_revision_role ?? '');
|
|
512
|
+
const cut =
|
|
513
|
+
typeof p._nws_revision_cutover_unix === 'number' && Number.isFinite(p._nws_revision_cutover_unix)
|
|
514
|
+
? p._nws_revision_cutover_unix
|
|
515
|
+
: null;
|
|
516
|
+
let slotScore = 0;
|
|
517
|
+
if (role === 'superseded' && cut != null) {
|
|
518
|
+
slotScore = referenceUnix < cut ? 2 : 0;
|
|
519
|
+
} else if (role === 'update' && cut != null) {
|
|
520
|
+
slotScore = referenceUnix >= cut ? 2 : 0;
|
|
521
|
+
} else if (role === 'current') {
|
|
522
|
+
slotScore = 1;
|
|
523
|
+
} else {
|
|
524
|
+
slotScore = 1;
|
|
525
|
+
}
|
|
526
|
+
const t =
|
|
527
|
+
parseNwsTimeToUnix(getNwsTimeProp(p, ['sent', 'updated_at', 'issued_at', 'issued'])) ?? 0;
|
|
528
|
+
return { f, slotScore, t };
|
|
529
|
+
});
|
|
530
|
+
scored.sort((a, b) => {
|
|
531
|
+
if (b.slotScore !== a.slotScore) return b.slotScore - a.slotScore;
|
|
532
|
+
return b.t - a.t;
|
|
533
|
+
});
|
|
534
|
+
return scored[0].f;
|
|
535
|
+
}
|
|
536
|
+
|
|
445
537
|
export function isUsableNwwsAlertGeometry(geometry) {
|
|
446
538
|
if (geometry == null || typeof geometry !== 'object') return false;
|
|
447
539
|
const g = geometry;
|
|
@@ -1098,6 +1190,56 @@ export function cloneNwwsFeatureCollectionStrippingVolatile(data) {
|
|
|
1098
1190
|
};
|
|
1099
1191
|
}
|
|
1100
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
|
+
*/
|
|
1101
1243
|
export function buildAlertClickPayload(feature) {
|
|
1102
1244
|
const properties = feature?.properties ? { ...feature.properties } : {};
|
|
1103
1245
|
const eventName = getNwsAlertEventLabelFromProperties(properties);
|
|
@@ -1112,9 +1254,10 @@ export function buildAlertClickPayload(feature) {
|
|
|
1112
1254
|
if (!Array.isArray(tags)) {
|
|
1113
1255
|
tags = tags != null ? [String(tags)] : [];
|
|
1114
1256
|
}
|
|
1115
|
-
const headline = properties.headline != null ? String(properties.headline) : '';
|
|
1116
1257
|
const name =
|
|
1117
|
-
headline
|
|
1258
|
+
(properties.headline != null && String(properties.headline).trim() !== ''
|
|
1259
|
+
? String(properties.headline)
|
|
1260
|
+
: '') ||
|
|
1118
1261
|
eventName ||
|
|
1119
1262
|
(properties.event != null ? String(properties.event) : '') ||
|
|
1120
1263
|
(properties.event_name != null ? String(properties.event_name) : '');
|
|
@@ -1124,8 +1267,6 @@ export function buildAlertClickPayload(feature) {
|
|
|
1124
1267
|
: properties.raw_text != null
|
|
1125
1268
|
? String(properties.raw_text)
|
|
1126
1269
|
: '';
|
|
1127
|
-
const summary = properties.summary != null ? String(properties.summary) : '';
|
|
1128
|
-
const instruction = properties.instruction != null ? String(properties.instruction) : '';
|
|
1129
1270
|
|
|
1130
1271
|
const startUnix =
|
|
1131
1272
|
typeof properties.start_unix === 'number'
|
|
@@ -1140,17 +1281,29 @@ export function buildAlertClickPayload(feature) {
|
|
|
1140
1281
|
? properties.active_start_unix
|
|
1141
1282
|
: getNwsActiveStartUnix(properties);
|
|
1142
1283
|
|
|
1284
|
+
const parametersParsed = parseNwsParametersJson(properties.parameters);
|
|
1285
|
+
const hazardDetails = buildHazardDetailsFromParameters(parametersParsed);
|
|
1286
|
+
|
|
1143
1287
|
return {
|
|
1144
1288
|
name,
|
|
1145
1289
|
eventName,
|
|
1146
|
-
headline,
|
|
1147
|
-
summary,
|
|
1148
1290
|
description,
|
|
1149
|
-
instruction,
|
|
1150
1291
|
tags,
|
|
1151
1292
|
startUnix,
|
|
1152
1293
|
endUnix,
|
|
1153
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. */
|
|
1154
1307
|
properties,
|
|
1155
1308
|
};
|
|
1156
1309
|
}
|