@aguacerowx/javascript-sdk 0.0.21 → 0.0.23

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aguacerowx/javascript-sdk",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -25,6 +25,11 @@
25
25
  "import": "./src/nexrad_support.js",
26
26
  "require": "./src/nexrad_support.js",
27
27
  "default": "./src/nexrad_support.js"
28
+ },
29
+ "./nexradTiltCoalesce.js": {
30
+ "import": "./src/nexradTiltCoalesce.js",
31
+ "require": "./src/nexradTiltCoalesce.js",
32
+ "default": "./src/nexradTiltCoalesce.js"
28
33
  }
29
34
  },
30
35
  "files": [
@@ -23,7 +23,9 @@ import {
23
23
  nexradColormapFldKey,
24
24
  variableToNexradGroup,
25
25
  getAvailableNexradTilts,
26
+ getRawNexradTiltsForCoalesce,
26
27
  } from './nexrad_support.js';
28
+ import { nexradLayerTiltToDisplayOption } from './nexradTiltCoalesce.js';
27
29
  import { NEXRAD_LEVEL3_ELEV, getNexradLevel3EntryByRadarKey } from './nexrad_level3_catalog.js';
28
30
  import {
29
31
  setRadarTiltsManifest,
@@ -142,12 +144,7 @@ export class AguaceroCore extends EventEmitter {
142
144
  const initialSatellite = initialMode === 'satellite';
143
145
  const initialNexrad = initialMode === 'nexrad';
144
146
  const initialNexradProd = userLayerOptions.nexradProduct || 'REF';
145
- const initialNexradDs =
146
- userLayerOptions.nexradDataSource != null
147
- ? userLayerOptions.nexradDataSource === 'level3'
148
- ? 'level3'
149
- : 'level2'
150
- : inferNexradDataSourceForProduct(initialNexradProd);
147
+ const initialNexradDs = inferNexradDataSourceForProduct(initialNexradProd);
151
148
  const initialNexradFld = initialNexrad
152
149
  ? nexradColormapFldKey(initialNexradDs, initialNexradProd)
153
150
  : initialVariable;
@@ -224,6 +221,27 @@ export class AguaceroCore extends EventEmitter {
224
221
  async setState(newState) {
225
222
  const patch = { ...newState };
226
223
  if ('satelliteKey' in patch) delete patch.satelliteKey;
224
+ if ('nexradDataSource' in patch && !('nexradProduct' in patch)) {
225
+ delete patch.nexradDataSource;
226
+ }
227
+ const willBeNexrad =
228
+ patch.isNexrad !== undefined ? Boolean(patch.isNexrad) : this.state.isNexrad;
229
+ if (willBeNexrad && 'nexradProduct' in patch && patch.nexradProduct != null) {
230
+ const p = (patch.nexradProduct || 'REF').toUpperCase();
231
+ const prevP = (this.state.nexradProduct || 'REF').toUpperCase();
232
+ const ds = inferNexradDataSourceForProduct(p);
233
+ patch.nexradProduct = p;
234
+ patch.nexradDataSource = ds;
235
+ patch.variable = nexradColormapFldKey(ds, p);
236
+ if (!('nexradStormRelative' in newState)) {
237
+ // Default: base radial velocity (L3 N0G only) — one fetch per time; fast scrub.
238
+ // SRV (N0G + N0S) is opt-in: setNexradStormRelative(true) or pass nexradStormRelative in setState.
239
+ // Re-selecting the same product (e.g. VEL → VEL) leaves the current SRV flag alone.
240
+ if (p !== 'VEL' || prevP !== 'VEL') {
241
+ patch.nexradStormRelative = false;
242
+ }
243
+ }
244
+ }
227
245
  if ('forecastHour' in patch && patch.forecastHour != null) {
228
246
  patch.forecastHour = Number(patch.forecastHour);
229
247
  }
@@ -363,13 +381,6 @@ export class AguaceroCore extends EventEmitter {
363
381
  return `${site}_${group}_${elevNormUse}`;
364
382
  }
365
383
 
366
- /** Storm-relative Level-III velocity (N0G + N0S) — matches aguacero-frontend `level3StormRelative` for VEL. */
367
- _nexradStormRelativeFor(nexradDataSource, nexradProduct) {
368
- const ds = nexradDataSource === 'level3' ? 'level3' : 'level2';
369
- const p = (nexradProduct || 'REF').toUpperCase();
370
- return ds === 'level3' && p === 'VEL';
371
- }
372
-
373
384
  _emitStateChange() {
374
385
  const { colormap, baseUnit } = this._getColormapForVariable(this.state.variable);
375
386
  const toUnit = this._getTargetUnit(baseUnit, this.state.units);
@@ -922,13 +933,7 @@ export class AguaceroCore extends EventEmitter {
922
933
  };
923
934
  } else if (mode === 'nexrad') {
924
935
  const nexradProduct = options.nexradProduct || 'REF';
925
- const nexradDataSource =
926
- options.nexradDataSource != null
927
- ? options.nexradDataSource === 'level3'
928
- ? 'level3'
929
- : 'level2'
930
- : inferNexradDataSourceForProduct(nexradProduct);
931
- const fld = nexradColormapFldKey(nexradDataSource, nexradProduct);
936
+ const p = nexradProduct.toUpperCase();
932
937
  const site = options.nexradSite ?? null;
933
938
  let tilt =
934
939
  options.nexradTilt != null
@@ -940,10 +945,8 @@ export class AguaceroCore extends EventEmitter {
940
945
  isNexrad: true,
941
946
  isMRMS: false,
942
947
  isSatellite: false,
943
- variable: fld,
944
948
  nexradSite: site,
945
- nexradDataSource,
946
- nexradProduct,
949
+ nexradProduct: p,
947
950
  nexradTilt: tilt,
948
951
  nexradTimestamp: options.nexradTimestamp != null ? Number(options.nexradTimestamp) : null,
949
952
  nexradStormRelative: options.nexradStormRelative === true,
@@ -985,26 +988,23 @@ export class AguaceroCore extends EventEmitter {
985
988
  }
986
989
 
987
990
  /**
988
- * Keep `nexradTilt` on an angle returned by {@link getAvailableNexradTilts} (matches aguacero-frontend elevation buttons).
991
+ * Keep `nexradTilt` on a coalesced elevation (same rules as aguacero-frontend tilt controls).
989
992
  */
990
993
  async _snapNexradTiltToAvailableOptions() {
991
994
  const s = this.state;
992
995
  if (!s.isNexrad || !s.nexradSite) return;
993
- const tilts = getAvailableNexradTilts(
996
+ const raw = getRawNexradTiltsForCoalesce(
994
997
  s.nexradSite,
995
998
  s.nexradDataSource || 'level2',
996
999
  s.nexradProduct || 'REF',
997
1000
  );
998
- if (!tilts.length) return;
1001
+ if (!raw.length) return;
999
1002
  const t = s.nexradTilt;
1000
- const match = (a, b) => Math.abs(Number(a) - Number(b)) < 1e-4;
1001
- if (t != null && tilts.some((x) => match(x, t))) return;
1002
1003
  const target = t != null && Number.isFinite(Number(t)) ? Number(t) : getDefaultRadarTilt(s.nexradSite);
1003
- let best = tilts[0];
1004
- for (const x of tilts) {
1005
- if (Math.abs(x - target) < Math.abs(best - target)) best = x;
1006
- }
1007
- await this.setState({ nexradTilt: best });
1004
+ const canonical = nexradLayerTiltToDisplayOption(target, raw);
1005
+ const match = (a, b) => Math.abs(Number(a) - Number(b)) < 1e-4;
1006
+ if (match(s.nexradTilt, canonical)) return;
1007
+ await this.setState({ nexradTilt: canonical });
1008
1008
  }
1009
1009
 
1010
1010
  async refreshNexradTimes() {
@@ -1044,6 +1044,18 @@ export class AguaceroCore extends EventEmitter {
1044
1044
  listWindowHours: listingHours,
1045
1045
  };
1046
1046
  }
1047
+ const filtered = this._getFilteredNexradTimestampsForVariable(out.unixTimes || []);
1048
+ const map = out.timeToKeyMap || {};
1049
+ if (filtered.length > 0) {
1050
+ const cur = this.state.nexradTimestamp != null ? Number(this.state.nexradTimestamp) : null;
1051
+ const hasKey = cur != null && Object.prototype.hasOwnProperty.call(map, String(cur));
1052
+ const inWindow = cur != null && filtered.includes(cur);
1053
+ if (cur == null || !inWindow || !hasKey) {
1054
+ this.state.nexradTimestamp = filtered[filtered.length - 1];
1055
+ }
1056
+ } else if (this.state.nexradTimestamp != null) {
1057
+ this.state.nexradTimestamp = null;
1058
+ }
1047
1059
  this._emitStateChange();
1048
1060
  }
1049
1061
 
@@ -1067,10 +1079,7 @@ export class AguaceroCore extends EventEmitter {
1067
1079
  async setNexradProduct(product) {
1068
1080
  if (!this.state.isNexrad) return;
1069
1081
  const p = (product || 'REF').toUpperCase();
1070
- const ds = inferNexradDataSourceForProduct(p);
1071
- const fld = nexradColormapFldKey(ds, p);
1072
- const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
1073
- await this.setState({ nexradProduct: p, nexradDataSource: ds, variable: fld, nexradStormRelative });
1082
+ await this.setState({ nexradProduct: p });
1074
1083
  await this.refreshNexradTimes();
1075
1084
  const filtered = this._getFilteredNexradTimestampsForVariable(
1076
1085
  this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
@@ -1080,48 +1089,19 @@ export class AguaceroCore extends EventEmitter {
1080
1089
  }
1081
1090
  }
1082
1091
 
1083
- async setNexradDataSource(source) {
1084
- if (!this.state.isNexrad) return;
1085
- const ds = source === 'level3' ? 'level3' : 'level2';
1086
- const p = (this.state.nexradProduct || 'REF').toUpperCase();
1087
- const fld = nexradColormapFldKey(ds, p);
1088
- const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
1089
- await this.setState({ nexradDataSource: ds, variable: fld, nexradStormRelative });
1092
+ async setNexradTilt(tilt) {
1093
+ if (!this.state.isNexrad || !this.state.nexradSite) return;
1094
+ await this.setState({ nexradTilt: tilt != null ? Number(tilt) : null });
1090
1095
  await this.refreshNexradTimes();
1091
- const filtered = this._getFilteredNexradTimestampsForVariable(
1092
- this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
1093
- );
1094
- if (filtered.length > 0) {
1095
- await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
1096
- }
1097
1096
  }
1098
1097
 
1099
1098
  /**
1100
- * Sets NEXRAD product and archive source together (single refresh). Prefer {@link setNexradProduct}
1101
- * for product-only changes (Level II vs III is inferred). Use this when you must force a specific
1102
- * archive source (rare).
1103
- * @param {'level2'|'level3'} dataSource
1104
- * @param {string} product - Level-II variable (REF, VEL, …) or Level-III radar key (N0H, HHC, …)
1099
+ * Opt-in storm-relative velocity (L3: N0G + N0S). When off (default), only base radial velocity is used (faster).
1100
+ * @param {boolean} enabled
1105
1101
  */
1106
- async setNexradProductMode(dataSource, product) {
1102
+ async setNexradStormRelative(enabled) {
1107
1103
  if (!this.state.isNexrad) return;
1108
- const ds = dataSource === 'level3' ? 'level3' : 'level2';
1109
- const p = (product || 'REF').toUpperCase();
1110
- const fld = nexradColormapFldKey(ds, p);
1111
- const nexradStormRelative = this._nexradStormRelativeFor(ds, p);
1112
- await this.setState({ nexradDataSource: ds, nexradProduct: p, variable: fld, nexradStormRelative });
1113
- await this.refreshNexradTimes();
1114
- const filtered = this._getFilteredNexradTimestampsForVariable(
1115
- this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
1116
- );
1117
- if (filtered.length > 0) {
1118
- await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
1119
- }
1120
- }
1121
-
1122
- async setNexradTilt(tilt) {
1123
- if (!this.state.isNexrad || !this.state.nexradSite) return;
1124
- await this.setState({ nexradTilt: tilt != null ? Number(tilt) : null });
1104
+ await this.setState({ nexradStormRelative: Boolean(enabled) });
1125
1105
  await this.refreshNexradTimes();
1126
1106
  }
1127
1107
 
package/src/index.js CHANGED
@@ -39,4 +39,5 @@ export {
39
39
 
40
40
  /** NEXRAD tilt + listing helpers (also importable via subpaths; root re-export fixes Vite/esbuild subpath resolution). */
41
41
  export * from './nexradTilts.js';
42
+ export * from './nexradTiltCoalesce.js';
42
43
  export * from './nexrad_support.js';
@@ -0,0 +1,95 @@
1
+ /**
2
+ * NEXRAD tilt coalescing (matches aguacero-frontend `nexradTiltCoalesce.ts`).
3
+ * Adjacent manifest tilts within {@link NEXRAD_TILT_COALESCE_MAX_GAP}° are merged for UI/listings:
4
+ * pairs → higher tilt; runs of three or more consecutive steps merge the first two into the lower tilt.
5
+ */
6
+
7
+ /** Adjacent tilts (sorted) within this gap share one listing / control. */
8
+ export const NEXRAD_TILT_COALESCE_MAX_GAP = 0.1;
9
+
10
+ /**
11
+ * @param {number[]} segment
12
+ * @param {Map<number, number>} map
13
+ */
14
+ function mergeCoalesceSegment(segment, map) {
15
+ if (segment.length === 0) return;
16
+ if (segment.length === 1) {
17
+ map.set(segment[0], segment[0]);
18
+ return;
19
+ }
20
+ if (segment.length === 2) {
21
+ const canonical = Math.max(segment[0], segment[1]);
22
+ map.set(segment[0], canonical);
23
+ map.set(segment[1], canonical);
24
+ return;
25
+ }
26
+ const low = segment[0];
27
+ map.set(segment[0], low);
28
+ map.set(segment[1], low);
29
+ mergeCoalesceSegment(segment.slice(2), map);
30
+ }
31
+
32
+ /**
33
+ * @param {number[]} sortedUnique
34
+ * @returns {Map<number, number>}
35
+ */
36
+ export function chainCoalesceToCanonical(sortedUnique) {
37
+ const map = new Map();
38
+ let i = 0;
39
+ while (i < sortedUnique.length) {
40
+ let j = i + 1;
41
+ while (
42
+ j < sortedUnique.length &&
43
+ sortedUnique[j] - sortedUnique[j - 1] <= NEXRAD_TILT_COALESCE_MAX_GAP + 1e-9
44
+ ) {
45
+ j++;
46
+ }
47
+ const segment = sortedUnique.slice(i, j);
48
+ mergeCoalesceSegment(segment, map);
49
+ i = j;
50
+ }
51
+ return map;
52
+ }
53
+
54
+ /**
55
+ * Manifest tilt list with adjacent options merged (pairs → higher; triple+ splits per merge rules).
56
+ * @param {number[]} tilts
57
+ * @returns {number[]}
58
+ */
59
+ export function coalesceNexradTiltOptionsForDisplay(tilts) {
60
+ const sorted = [...new Set(tilts)]
61
+ .filter((t) => Number.isFinite(t))
62
+ .sort((a, b) => a - b);
63
+ if (sorted.length <= 1) return sorted;
64
+ const tiltToCanonical = chainCoalesceToCanonical(sorted);
65
+ const reps = [];
66
+ let prevRep;
67
+ for (const t of sorted) {
68
+ const c = tiltToCanonical.get(t);
69
+ if (c === undefined) continue;
70
+ if (c !== prevRep) {
71
+ reps.push(c);
72
+ prevRep = c;
73
+ }
74
+ }
75
+ return reps;
76
+ }
77
+
78
+ /**
79
+ * Map stored/clamped tilt to the coalesced elevation value (same rules as {@link coalesceNexradTiltOptionsForDisplay}).
80
+ * @param {number} layerTilt
81
+ * @param {number[]} manifestTilts raw manifest tilts (before coalescing)
82
+ * @returns {number}
83
+ */
84
+ export function nexradLayerTiltToDisplayOption(layerTilt, manifestTilts) {
85
+ const sorted = [...new Set(manifestTilts)]
86
+ .filter((t) => Number.isFinite(t))
87
+ .sort((a, b) => a - b);
88
+ if (sorted.length === 0) return layerTilt;
89
+ const tiltToCanonical = chainCoalesceToCanonical(sorted);
90
+ if (tiltToCanonical.has(layerTilt)) return tiltToCanonical.get(layerTilt);
91
+ const nearest = sorted.reduce((best, x) =>
92
+ Math.abs(x - layerTilt) < Math.abs(best - layerTilt) ? x : best,
93
+ sorted[0]);
94
+ return tiltToCanonical.get(nearest) ?? layerTilt;
95
+ }
@@ -8,6 +8,7 @@ import {
8
8
  getDefaultRadarTilt,
9
9
  getRadarTilts,
10
10
  } from './nexradTilts.js';
11
+ import { coalesceNexradTiltOptionsForDisplay } from './nexradTiltCoalesce.js';
11
12
  import {
12
13
  NEXRAD_LEVEL3_ELEV,
13
14
  NEXRAD_LEVEL3_MOTION_PRODUCT,
@@ -16,7 +17,7 @@ import {
16
17
 
17
18
  /**
18
19
  * Whether listings and archive fetches for this product use Level-II sweep lambda vs Level-III S3 products.
19
- * AguaceroCore applies this from the product key; set `nexradDataSource` in layer options only to override (rare).
20
+ * AguaceroCore derives this from the product key only (not user-configurable).
20
21
  * @param {string} [nexradProduct] - Radar variable key (e.g. REF, KDP, VEL).
21
22
  * @returns {'level2'|'level3'}
22
23
  */
@@ -263,25 +264,43 @@ export async function fetchNexradTimesListing(opts) {
263
264
  const L3_TILT_INDEX_MANIFEST_SLOTS = 6;
264
265
 
265
266
  /**
266
- * Tilt angles for the UI (manifest-driven L2 list; Level III KDP/N0H use the first slots like aguacero-frontend).
267
+ * Raw manifest tilt list before coalescing (same rules as aguacero-frontend `radarTiltOptionsRaw`).
268
+ * Use {@link coalesceNexradTiltOptionsForDisplay} or {@link getAvailableNexradTilts} for UI controls.
269
+ *
267
270
  * @param {string} siteId
268
271
  * @param {'level2'|'level3'} nexradDataSource
269
272
  * @param {string} [nexradProduct]
270
273
  * @returns {number[]}
271
274
  */
272
- export function getAvailableNexradTilts(siteId, nexradDataSource, nexradProduct) {
275
+ export function getRawNexradTiltsForCoalesce(siteId, nexradDataSource, nexradProduct) {
273
276
  if (!siteId) return [];
274
277
  const v = (nexradProduct || 'REF').toUpperCase();
275
278
  const ds = nexradDataSource === 'level3' ? 'level3' : 'level2';
276
279
  const tilts = getRadarTilts(siteId, v);
280
+ /** L3 super-res VEL (N*G) uses the same first-N manifest slots as KDP/N0H — not the full L2 g2-only list. */
281
+ if (v === 'VEL') {
282
+ return tilts.slice(0, L3_TILT_INDEX_MANIFEST_SLOTS);
283
+ }
277
284
  if (ds === 'level2') {
278
285
  return [...tilts];
279
286
  }
280
287
  if (v === 'KDP' || v === 'N0H') {
281
288
  return tilts.slice(0, L3_TILT_INDEX_MANIFEST_SLOTS);
282
289
  }
283
- if (v === 'VEL') {
284
- return [...tilts];
285
- }
286
290
  return [];
287
291
  }
292
+
293
+ /**
294
+ * Tilt angles for the UI: manifest tilts with adjacent 0.1° steps merged like aguacero-frontend
295
+ * ({@link coalesceNexradTiltOptionsForDisplay}).
296
+ *
297
+ * @param {string} siteId
298
+ * @param {'level2'|'level3'} nexradDataSource
299
+ * @param {string} [nexradProduct]
300
+ * @returns {number[]}
301
+ */
302
+ export function getAvailableNexradTilts(siteId, nexradDataSource, nexradProduct) {
303
+ const raw = getRawNexradTiltsForCoalesce(siteId, nexradDataSource, nexradProduct);
304
+ if (!raw.length) return [];
305
+ return coalesceNexradTiltOptionsForDisplay(raw);
306
+ }