@aguacerowx/javascript-sdk 0.0.21 → 0.0.22

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.22",
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,19 @@ 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 ds = inferNexradDataSourceForProduct(p);
232
+ patch.nexradProduct = p;
233
+ patch.nexradDataSource = ds;
234
+ patch.variable = nexradColormapFldKey(ds, p);
235
+ patch.nexradStormRelative = this._nexradStormRelativeFor(ds, p);
236
+ }
227
237
  if ('forecastHour' in patch && patch.forecastHour != null) {
228
238
  patch.forecastHour = Number(patch.forecastHour);
229
239
  }
@@ -922,13 +932,7 @@ export class AguaceroCore extends EventEmitter {
922
932
  };
923
933
  } else if (mode === 'nexrad') {
924
934
  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);
935
+ const p = nexradProduct.toUpperCase();
932
936
  const site = options.nexradSite ?? null;
933
937
  let tilt =
934
938
  options.nexradTilt != null
@@ -940,10 +944,8 @@ export class AguaceroCore extends EventEmitter {
940
944
  isNexrad: true,
941
945
  isMRMS: false,
942
946
  isSatellite: false,
943
- variable: fld,
944
947
  nexradSite: site,
945
- nexradDataSource,
946
- nexradProduct,
948
+ nexradProduct: p,
947
949
  nexradTilt: tilt,
948
950
  nexradTimestamp: options.nexradTimestamp != null ? Number(options.nexradTimestamp) : null,
949
951
  nexradStormRelative: options.nexradStormRelative === true,
@@ -985,26 +987,23 @@ export class AguaceroCore extends EventEmitter {
985
987
  }
986
988
 
987
989
  /**
988
- * Keep `nexradTilt` on an angle returned by {@link getAvailableNexradTilts} (matches aguacero-frontend elevation buttons).
990
+ * Keep `nexradTilt` on a coalesced elevation (same rules as aguacero-frontend tilt controls).
989
991
  */
990
992
  async _snapNexradTiltToAvailableOptions() {
991
993
  const s = this.state;
992
994
  if (!s.isNexrad || !s.nexradSite) return;
993
- const tilts = getAvailableNexradTilts(
995
+ const raw = getRawNexradTiltsForCoalesce(
994
996
  s.nexradSite,
995
997
  s.nexradDataSource || 'level2',
996
998
  s.nexradProduct || 'REF',
997
999
  );
998
- if (!tilts.length) return;
1000
+ if (!raw.length) return;
999
1001
  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
1002
  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 });
1003
+ const canonical = nexradLayerTiltToDisplayOption(target, raw);
1004
+ const match = (a, b) => Math.abs(Number(a) - Number(b)) < 1e-4;
1005
+ if (match(s.nexradTilt, canonical)) return;
1006
+ await this.setState({ nexradTilt: canonical });
1008
1007
  }
1009
1008
 
1010
1009
  async refreshNexradTimes() {
@@ -1044,6 +1043,18 @@ export class AguaceroCore extends EventEmitter {
1044
1043
  listWindowHours: listingHours,
1045
1044
  };
1046
1045
  }
1046
+ const filtered = this._getFilteredNexradTimestampsForVariable(out.unixTimes || []);
1047
+ const map = out.timeToKeyMap || {};
1048
+ if (filtered.length > 0) {
1049
+ const cur = this.state.nexradTimestamp != null ? Number(this.state.nexradTimestamp) : null;
1050
+ const hasKey = cur != null && Object.prototype.hasOwnProperty.call(map, String(cur));
1051
+ const inWindow = cur != null && filtered.includes(cur);
1052
+ if (cur == null || !inWindow || !hasKey) {
1053
+ this.state.nexradTimestamp = filtered[filtered.length - 1];
1054
+ }
1055
+ } else if (this.state.nexradTimestamp != null) {
1056
+ this.state.nexradTimestamp = null;
1057
+ }
1047
1058
  this._emitStateChange();
1048
1059
  }
1049
1060
 
@@ -1067,49 +1078,7 @@ export class AguaceroCore extends EventEmitter {
1067
1078
  async setNexradProduct(product) {
1068
1079
  if (!this.state.isNexrad) return;
1069
1080
  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 });
1074
- await this.refreshNexradTimes();
1075
- const filtered = this._getFilteredNexradTimestampsForVariable(
1076
- this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
1077
- );
1078
- if (filtered.length > 0) {
1079
- await this.setState({ nexradTimestamp: filtered[filtered.length - 1] });
1080
- }
1081
- }
1082
-
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 });
1090
- 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
- }
1098
-
1099
- /**
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, …)
1105
- */
1106
- async setNexradProductMode(dataSource, product) {
1107
- 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 });
1081
+ await this.setState({ nexradProduct: p });
1113
1082
  await this.refreshNexradTimes();
1114
1083
  const filtered = this._getFilteredNexradTimestampsForVariable(
1115
1084
  this.nexradTimesByStation[this._nexradTimesCacheKey()]?.unixTimes || [],
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,13 +264,15 @@ 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';
@@ -285,3 +288,18 @@ export function getAvailableNexradTilts(siteId, nexradDataSource, nexradProduct)
285
288
  }
286
289
  return [];
287
290
  }
291
+
292
+ /**
293
+ * Tilt angles for the UI: manifest tilts with adjacent 0.1° steps merged like aguacero-frontend
294
+ * ({@link coalesceNexradTiltOptionsForDisplay}).
295
+ *
296
+ * @param {string} siteId
297
+ * @param {'level2'|'level3'} nexradDataSource
298
+ * @param {string} [nexradProduct]
299
+ * @returns {number[]}
300
+ */
301
+ export function getAvailableNexradTilts(siteId, nexradDataSource, nexradProduct) {
302
+ const raw = getRawNexradTiltsForCoalesce(siteId, nexradDataSource, nexradProduct);
303
+ if (!raw.length) return [];
304
+ return coalesceNexradTiltOptionsForDisplay(raw);
305
+ }