@aguacerowx/mapsgl 0.0.42 → 0.0.43

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/mapsgl",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -8,6 +8,48 @@ import { NexradSitesOverlay } from './NexradSitesOverlay.js';
8
8
  import { NwsWatchesWarningsOverlay } from './NwsWatchesWarningsOverlay.js';
9
9
  import WorkerPool from './WorkerPool.js';
10
10
 
11
+ const DEBUG_NS = '[WeatherLayerManager:debug]';
12
+
13
+ /**
14
+ * Redact secrets for console output.
15
+ * @param {object} options
16
+ */
17
+ function _debugSanitizeOptions(options) {
18
+ if (!options || typeof options !== 'object') return options;
19
+ const o = { ...options };
20
+ if (typeof o.apiKey === 'string' && o.apiKey.length > 0) {
21
+ o.apiKey = `${o.apiKey.slice(0, 4)}…(${o.apiKey.length} chars)`;
22
+ }
23
+ return o;
24
+ }
25
+
26
+ /**
27
+ * Compact summary of timestamp / hour arrays (MRMS timeline debugging).
28
+ * @param {unknown[]} arr
29
+ */
30
+ function _debugSummarizeNumericSeries(arr) {
31
+ if (!Array.isArray(arr) || arr.length === 0) {
32
+ return { length: 0, min: null, max: null, spanSec: null, head: [], tail: undefined };
33
+ }
34
+ const nums = arr.map((x) => Number(x)).filter((n) => !Number.isNaN(n));
35
+ if (nums.length === 0) {
36
+ return { length: 0, min: null, max: null, spanSec: null, head: [], tail: undefined };
37
+ }
38
+ const sorted = [...nums].sort((a, b) => a - b);
39
+ const min = sorted[0];
40
+ const max = sorted[sorted.length - 1];
41
+ const head = sorted.slice(0, Math.min(8, sorted.length));
42
+ const tail = sorted.length > 16 ? sorted.slice(-8) : [];
43
+ return {
44
+ length: sorted.length,
45
+ min,
46
+ max,
47
+ spanSec: max != null && min != null ? max - min : null,
48
+ head,
49
+ tail: tail.length ? tail : undefined,
50
+ };
51
+ }
52
+
11
53
  function findLatestModelRun(modelsData, modelName) {
12
54
  const model = modelsData?.[modelName];
13
55
  if (!model) return null;
@@ -33,12 +75,17 @@ function findLatestModelRun(modelsData, modelName) {
33
75
  * @param {string} [options.belowID] - Style layer id to insert Aguacero weather layers **below** (default `AML_-_terrain` when present). Alias of `weatherBeforeLayerId`.
34
76
  * @param {string} [options.weatherBeforeLayerId] - Same as `belowID`.
35
77
  * @param {string} [options.nexradLayerId] - Override Mapbox id for the NEXRAD custom layer (default: derived from `layerId`).
78
+ * @param {boolean} [options.debug] - When `true`, logs detailed diagnostics to the console (prefix `[WeatherLayerManager:debug]`). Off by default.
36
79
  */
37
80
  export class WeatherLayerManager extends EventEmitter {
38
81
  constructor(map, options = {}) {
39
82
  super();
40
83
  if (!map) throw new Error('A Mapbox GL map instance is required.');
41
84
  this.map = map;
85
+ /** @private When true, emit verbose `[WeatherLayerManager:debug]` logs. */
86
+ this._debug = options.debug === true;
87
+ /** @private Monotonic counter for correlating state:change logs. */
88
+ this._debugStateSeq = 0;
42
89
  this.layerId =
43
90
  options.layerId ||
44
91
  options.id ||
@@ -109,6 +156,29 @@ export class WeatherLayerManager extends EventEmitter {
109
156
  // 1. CREATE an instance of the core engine
110
157
  this.core = new AguaceroCore(options);
111
158
 
159
+ if (this._debug) {
160
+ const layerOpts = options.layerOptions || {};
161
+ console.log(DEBUG_NS, 'constructor', {
162
+ layerId: this.layerId,
163
+ nexradLayerId: this._nexradLayerId,
164
+ mapLoaded: typeof this.map?.loaded === 'function' ? this.map.loaded() : undefined,
165
+ styleLoaded: this.map?.isStyleLoaded?.() ?? undefined,
166
+ weatherBeforeLayerId: this._weatherBeforeLayerId,
167
+ options: _debugSanitizeOptions(options),
168
+ layerOptions: layerOpts,
169
+ coreStateSnapshot: {
170
+ isMRMS: this.core.state?.isMRMS,
171
+ isSatellite: this.core.state?.isSatellite,
172
+ isNexrad: this.core.state?.isNexrad,
173
+ model: this.core.state?.model,
174
+ variable: this.core.state?.variable,
175
+ mrmsDurationValue: this.core.state?.mrmsDurationValue,
176
+ mrmsTimestamp: this.core.state?.mrmsTimestamp,
177
+ satelliteDurationValue: this.core.state?.satelliteDurationValue,
178
+ },
179
+ });
180
+ }
181
+
112
182
  // 2. LISTEN for events from the core engine
113
183
  this.core.on('state:change', (newState) => {
114
184
  this._lastEmittedState = newState;
@@ -128,6 +198,20 @@ export class WeatherLayerManager extends EventEmitter {
128
198
  this.map.on('mousemove', this._handleMouseMove);
129
199
  }
130
200
 
201
+ /**
202
+ * Structured debug log (no-op unless `options.debug === true` on construction).
203
+ * @param {string} scope
204
+ * @param {object} [data]
205
+ */
206
+ _debugLog(scope, data) {
207
+ if (!this._debug) return;
208
+ if (data !== undefined) {
209
+ console.log(DEBUG_NS, scope, data);
210
+ } else {
211
+ console.log(DEBUG_NS, scope);
212
+ }
213
+ }
214
+
131
215
  /**
132
216
  * Resolves which style layer id to pass as Mapbox `addLayer(..., beforeId)` for satellite / grid / NEXRAD.
133
217
  * Prefers an explicit id (custom styles), then the default Aguacero `AML_-_terrain` anchor when present.
@@ -174,9 +258,39 @@ export class WeatherLayerManager extends EventEmitter {
174
258
  * @private
175
259
  */
176
260
  _handleStateChange(state) {
261
+ const seq = this._debug ? ++this._debugStateSeq : 0;
177
262
  /** NEXRAD setup is async (`sites.show`, frame fetch). `finally` would run before the radar layer exists, so NWS fill stacks above terrain instead of under the custom layer — defer sync until the handler settles. */
178
263
  let deferNwsSyncUntilNexradReady = false;
179
264
  try {
265
+ if (this._debug) {
266
+ const tsSummary = state.isMRMS
267
+ ? _debugSummarizeNumericSeries(state.availableTimestamps || [])
268
+ : null;
269
+ this._debugLog('state:change', {
270
+ seq,
271
+ mode: state.isSatellite
272
+ ? 'satellite'
273
+ : state.isNexrad
274
+ ? 'nexrad'
275
+ : state.isMRMS
276
+ ? 'mrms'
277
+ : 'model',
278
+ model: state.model,
279
+ variable: state.variable,
280
+ runKey: `${state.model}-${state.date}-${state.run}-${state.variable}`,
281
+ timeKey: state.isMRMS
282
+ ? (state.mrmsTimestamp == null ? null : Number(state.mrmsTimestamp))
283
+ : Number(state.forecastHour),
284
+ mrmsDurationValue: state.mrmsDurationValue,
285
+ availableTimestampsSummary: tsSummary,
286
+ availableHoursLen: Array.isArray(state.availableHours) ? state.availableHours.length : 0,
287
+ shaderLayerRunKey: this.shaderLayer?.runKey ?? null,
288
+ currentLoadedTimeKey: this.currentLoadedTimeKey,
289
+ rebuildId: this.currentRebuildId,
290
+ initialGridLoadPending: this._initialGridLoadPending,
291
+ });
292
+ }
293
+
180
294
  if (state.isSatellite) {
181
295
  this._handleSatelliteStateChange(state);
182
296
  return;
@@ -206,6 +320,21 @@ export class WeatherLayerManager extends EventEmitter {
206
320
  : Number(state.forecastHour);
207
321
  const runKey = `${state.model}-${state.date}-${state.run}-${state.variable}`;
208
322
 
323
+ if (this._debug && state.isMRMS) {
324
+ this._debugLog('grid path (mrms/model)', {
325
+ seq,
326
+ prevMrmsDurationValue: prevMrmsDur,
327
+ mrmsDurationChanged,
328
+ willRebuild: !this.shaderLayer || this.shaderLayer.runKey !== runKey,
329
+ willUpdateData:
330
+ this.shaderLayer &&
331
+ this.shaderLayer.runKey === runKey &&
332
+ this.currentLoadedTimeKey !== timeKey,
333
+ shaderRunKey: this.shaderLayer?.runKey,
334
+ targetRunKey: runKey,
335
+ });
336
+ }
337
+
209
338
  if (!this.shaderLayer || this.shaderLayer.runKey !== runKey) {
210
339
  this._rebuildLayerAndPreload(state);
211
340
  } else if (this.currentLoadedTimeKey !== timeKey) {
@@ -215,6 +344,12 @@ export class WeatherLayerManager extends EventEmitter {
215
344
  timeKey === this._rebuildTargetTimeKey;
216
345
  if (!duplicateBeforeFirstPaint) {
217
346
  this._updateLayerData(state);
347
+ } else if (this._debug) {
348
+ this._debugLog('skip _updateLayerData (duplicate before first paint)', {
349
+ seq,
350
+ timeKey,
351
+ _rebuildTargetTimeKey: this._rebuildTargetTimeKey,
352
+ });
218
353
  }
219
354
  }
220
355
 
@@ -225,6 +360,13 @@ export class WeatherLayerManager extends EventEmitter {
225
360
  }
226
361
 
227
362
  if (state.isMRMS && this.shaderLayer && mrmsDurationChanged) {
363
+ if (this._debug) {
364
+ this._debugLog('_preloadAllTimeSteps (mrms duration changed)', {
365
+ seq,
366
+ from: prevMrmsDur,
367
+ to: state.mrmsDurationValue,
368
+ });
369
+ }
228
370
  this._preloadAllTimeSteps(state);
229
371
  }
230
372
  } finally {
@@ -638,10 +780,68 @@ export class WeatherLayerManager extends EventEmitter {
638
780
  // to the core engine. This keeps the API consistent for your users.
639
781
 
640
782
  async initialize(options) {
783
+ const t0 =
784
+ typeof performance !== 'undefined' && typeof performance.now === 'function'
785
+ ? performance.now()
786
+ : Date.now();
787
+ this._debugLog('initialize:start', {
788
+ autoRefreshEnabled: this.autoRefreshEnabled,
789
+ autoRefreshIntervalSeconds: this.autoRefreshIntervalSeconds,
790
+ passedOptions: options && typeof options === 'object' ? { ...options } : options,
791
+ coreStateBefore: {
792
+ isMRMS: this.core.state?.isMRMS,
793
+ variable: this.core.state?.variable,
794
+ mrmsTimestamp: this.core.state?.mrmsTimestamp,
795
+ mrmsDurationValue: this.core.state?.mrmsDurationValue,
796
+ },
797
+ mrmsStatusVariableLen: this.core.state?.variable
798
+ ? (this.core.mrmsStatus?.[this.core.state.variable]?.length ?? 'n/a')
799
+ : 'n/a',
800
+ });
641
801
  if (this.autoRefreshEnabled) {
642
802
  this.setAutoRefresh(true, this.autoRefreshIntervalSeconds);
643
803
  }
644
- return this.core.initialize({ ...options, autoRefresh: false }); // <-- add spread + autoRefresh: false
804
+ try {
805
+ const result = await this.core.initialize({ ...options, autoRefresh: false });
806
+ const t1 =
807
+ typeof performance !== 'undefined' && typeof performance.now === 'function'
808
+ ? performance.now()
809
+ : Date.now();
810
+ this._debugLog('initialize:done', {
811
+ elapsedMs: Math.round(t1 - t0),
812
+ coreStateAfter: {
813
+ isMRMS: this.core.state?.isMRMS,
814
+ variable: this.core.state?.variable,
815
+ model: this.core.state?.model,
816
+ date: this.core.state?.date,
817
+ run: this.core.state?.run,
818
+ forecastHour: this.core.state?.forecastHour,
819
+ mrmsTimestamp: this.core.state?.mrmsTimestamp,
820
+ mrmsDurationValue: this.core.state?.mrmsDurationValue,
821
+ },
822
+ availableTimestampsSummary: _debugSummarizeNumericSeries(
823
+ this._lastEmittedState?.availableTimestamps || [],
824
+ ),
825
+ availableHoursLen: Array.isArray(this._lastEmittedState?.availableHours)
826
+ ? this._lastEmittedState.availableHours.length
827
+ : 0,
828
+ modelStatusLoaded: this.core.modelStatus != null,
829
+ mrmsStatusKeys:
830
+ this.core.mrmsStatus && typeof this.core.mrmsStatus === 'object'
831
+ ? Object.keys(this.core.mrmsStatus).length
832
+ : 0,
833
+ mrmsStatusVariableLen: this.core.state?.variable
834
+ ? (this.core.mrmsStatus?.[this.core.state.variable]?.length ?? 0)
835
+ : 0,
836
+ });
837
+ return result;
838
+ } catch (err) {
839
+ this._debugLog('initialize:error', {
840
+ message: err?.message || String(err),
841
+ stack: err?.stack,
842
+ });
843
+ throw err;
844
+ }
645
845
  }
646
846
  async setState(newState) { return this.core.setState(newState); }
647
847
  play() { this.core.play(); }
@@ -741,15 +941,50 @@ export class WeatherLayerManager extends EventEmitter {
741
941
  : (state.availableHours || []);
742
942
 
743
943
  let timeSteps;
944
+ let mrmsTimelineSource = '';
744
945
  if (state.isMRMS) {
745
- timeSteps = fromState.length ? fromState : fromCore;
946
+ if (fromState.length) {
947
+ timeSteps = fromState;
948
+ mrmsTimelineSource = 'state.availableTimestamps';
949
+ } else if (
950
+ state.variable &&
951
+ typeof this.core._getFilteredMrmsTimestampsForVariable === 'function'
952
+ ) {
953
+ /**
954
+ * Same list {@link AguaceroCore} puts on `state:change` as `availableTimestamps`.
955
+ * When that array is missing or empty on this snapshot (ordering / first paint),
956
+ * derive from core so rebuild preload is not stuck with only `currentFrameTime`.
957
+ */
958
+ try {
959
+ timeSteps = this.core._getFilteredMrmsTimestampsForVariable(state.variable);
960
+ mrmsTimelineSource = 'core._getFilteredMrmsTimestampsForVariable';
961
+ } catch {
962
+ timeSteps = fromCore;
963
+ mrmsTimelineSource = 'core forecast hours fallback (error)';
964
+ }
965
+ } else {
966
+ timeSteps = fromCore;
967
+ mrmsTimelineSource = 'empty';
968
+ }
746
969
  } else {
747
970
  timeSteps = fromCore.length > 0 ? fromCore : fromState;
748
971
  }
749
972
 
750
- return (timeSteps || [])
973
+ const out = (timeSteps || [])
751
974
  .map(t => Number(t))
752
975
  .filter(t => !Number.isNaN(t));
976
+ if (this._debug && state.isMRMS) {
977
+ this._debugLog('_collectNormalizedTimelineSteps', {
978
+ variable: state.variable,
979
+ mrmsDurationValue: state.mrmsDurationValue,
980
+ fromStateLen: fromState.length,
981
+ fromCoreLen: fromCore.length,
982
+ chosenSource: mrmsTimelineSource || (state.isMRMS ? 'n/a' : 'model'),
983
+ normalizedLen: out.length,
984
+ summary: _debugSummarizeNumericSeries(out),
985
+ });
986
+ }
987
+ return out;
753
988
  }
754
989
 
755
990
  /**
@@ -763,6 +998,12 @@ export class WeatherLayerManager extends EventEmitter {
763
998
  if (mode === 'rebuild') {
764
999
  this._initialGridLoadPending = false;
765
1000
  }
1001
+ this._debugLog('_runParallelGridFrameLoads:skip', {
1002
+ reason: !times.length ? 'no times' : 'no shaderLayer',
1003
+ mode,
1004
+ rebuildId,
1005
+ timesLen: times.length,
1006
+ });
766
1007
  return;
767
1008
  }
768
1009
 
@@ -778,6 +1019,25 @@ export class WeatherLayerManager extends EventEmitter {
778
1019
  const gridModel = state.isMRMS ? 'mrms' : state.model;
779
1020
  const { gridDef } = this.core._getGridCornersAndDef(gridModel);
780
1021
 
1022
+ this._debugLog('_runParallelGridFrameLoads', {
1023
+ mode,
1024
+ rebuildId,
1025
+ gridModel,
1026
+ timesLen: times.length,
1027
+ timesSummary: _debugSummarizeNumericSeries(times),
1028
+ currentFrameTime,
1029
+ primaryTimeForRebuild:
1030
+ mode === 'rebuild'
1031
+ ? Number.isFinite(currentFrameTime)
1032
+ ? currentFrameTime
1033
+ : times[0]
1034
+ : undefined,
1035
+ tsKey,
1036
+ gridNxNy: gridDef?.grid_params
1037
+ ? { nx: gridDef.grid_params.nx, ny: gridDef.grid_params.ny }
1038
+ : null,
1039
+ });
1040
+
781
1041
  times.forEach((time) => {
782
1042
  const stateForTime = { ...state, [tsKey]: time };
783
1043
  this.core._loadGridData(stateForTime)
@@ -798,9 +1058,20 @@ export class WeatherLayerManager extends EventEmitter {
798
1058
  : Number(this.core.state.mrmsTimestamp))
799
1059
  : Number(this.core.state.forecastHour);
800
1060
  if (coreTimeKey !== this._rebuildTargetTimeKey) {
1061
+ this._debugLog('_loadGridData:skip primary (core time drifted vs rebuild target)', {
1062
+ time,
1063
+ coreTimeKey,
1064
+ _rebuildTargetTimeKey: this._rebuildTargetTimeKey,
1065
+ rebuildId,
1066
+ });
801
1067
  return;
802
1068
  }
803
1069
  if (!grid?.data) {
1070
+ this._debugLog('_loadGridData:primary frame missing grid.data', {
1071
+ time,
1072
+ rebuildId,
1073
+ mode,
1074
+ });
804
1075
  this._initialGridLoadPending = false;
805
1076
  return;
806
1077
  }
@@ -833,9 +1104,17 @@ export class WeatherLayerManager extends EventEmitter {
833
1104
  this.currentLoadedTimeKey = time;
834
1105
  this.map.triggerRepaint();
835
1106
  }
1107
+ } else if (this._debug && mode === 'append') {
1108
+ this._debugLog('_loadGridData:empty grid (append)', { time, mode, rebuildId });
836
1109
  }
837
1110
  })
838
- .catch(() => {
1111
+ .catch((err) => {
1112
+ this._debugLog('_loadGridData:error', {
1113
+ time,
1114
+ mode,
1115
+ rebuildId,
1116
+ message: err?.message || String(err),
1117
+ });
839
1118
  if (
840
1119
  mode === 'rebuild' &&
841
1120
  Number.isFinite(primaryTimeForRebuild) &&
@@ -910,8 +1189,26 @@ export class WeatherLayerManager extends EventEmitter {
910
1189
  if (timesToLoad.length === 0 && !Number.isNaN(currentFrameTime)) {
911
1190
  timesToLoad = [currentFrameTime];
912
1191
  }
1192
+ this._debugLog('_rebuildLayerAndPreload:timeline', {
1193
+ rebuildId,
1194
+ isMRMS: state.isMRMS,
1195
+ variable: state.variable,
1196
+ normalizedLen: normalized.length,
1197
+ normalizedSummary: _debugSummarizeNumericSeries(normalized),
1198
+ currentFrameTime,
1199
+ timesToLoadLen: timesToLoad.length,
1200
+ timesToLoadSummary: _debugSummarizeNumericSeries(timesToLoad),
1201
+ insertBeforeId: beforeId ?? '(stack top)',
1202
+ });
913
1203
  if (timesToLoad.length === 0) {
914
1204
  this._initialGridLoadPending = false;
1205
+ this._debugLog('_rebuildLayerAndPreload:no times to load — check availableTimestamps / duration window', {
1206
+ rebuildId,
1207
+ availableTimestampsLen: Array.isArray(state.availableTimestamps)
1208
+ ? state.availableTimestamps.length
1209
+ : 0,
1210
+ mrmsDurationValue: state.mrmsDurationValue,
1211
+ });
915
1212
  return;
916
1213
  }
917
1214
 
@@ -925,11 +1222,21 @@ export class WeatherLayerManager extends EventEmitter {
925
1222
  const currentFrameTime = Number(state.isMRMS ? state.mrmsTimestamp : state.forecastHour);
926
1223
  const stepsToPreload = normalized.filter(t => t !== currentFrameTime);
927
1224
 
1225
+ this._debugLog('_preloadAllTimeSteps', {
1226
+ normalizedLen: normalized.length,
1227
+ currentFrameTime,
1228
+ stepsToPreloadLen: stepsToPreload.length,
1229
+ stepsSummary: _debugSummarizeNumericSeries(stepsToPreload),
1230
+ });
1231
+
928
1232
  if (normalized.length === 0) {
929
1233
  return;
930
1234
  }
931
1235
 
932
1236
  if (stepsToPreload.length === 0) {
1237
+ this._debugLog('_preloadAllTimeSteps:skip (nothing to preload besides current)', {
1238
+ currentFrameTime,
1239
+ });
933
1240
  return;
934
1241
  }
935
1242
 
@@ -1171,6 +1478,7 @@ export class WeatherLayerManager extends EventEmitter {
1171
1478
  * Cleans up all resources.
1172
1479
  */
1173
1480
  destroy() {
1481
+ this._debugLog('destroy');
1174
1482
  this.setAutoRefresh(false);
1175
1483
 
1176
1484
  // 1. Unbind the map's mousemove event listener