@aguacerowx/mapsgl 0.0.41 → 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.
|
|
3
|
+
"version": "0.0.43",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"
|
|
11
|
+
"prepublishOnly": "npm run bundle-nexrad",
|
|
12
|
+
"bundle-nexrad": "esbuild src/nexrad/radarDecode.worker.ts --bundle --format=esm --platform=browser --outfile=src/nexrad/radarDecode.worker.bundled.js && esbuild src/nexrad/radarArchiveCore.ts --bundle --format=esm --platform=browser --outfile=src/nexrad/radarArchiveCore.bundled.js --external:@aguacerowx/javascript-sdk && esbuild src/nexrad/MapboxRadarLayer.ts --bundle --format=esm --platform=browser --outfile=src/nexrad/MapboxRadarLayer.bundled.js --external:mapbox-gl --external:@aguacerowx/javascript-sdk && esbuild src/nexrad/nexradCrossSectionSampleAtLatLon.ts --format=esm --platform=browser --outfile=src/nexrad/nexradCrossSectionSampleAtLatLon.bundled.js && esbuild src/nexrad/radarFrameGpuMatch.ts --format=esm --platform=browser --outfile=src/nexrad/radarFrameGpuMatch.bundled.js",
|
|
12
13
|
"gen:nws-key": "esbuild ../../../aguacero-frontend/src/components/WarningsMenu/nwsWarningCustomizationKey.ts --bundle --format=esm --platform=neutral --outfile=src/nwsWarningCustomizationKey.gen.js"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
@@ -6,8 +6,8 @@ import { getUnitConversionFunction, getDefaultRadarTilt } from '@aguacerowx/java
|
|
|
6
6
|
import { fetchAndParseArchive, objectKeyToUrl, setNexradArchiveApiKey } from './nexrad/radarArchiveCore.bundled.js';
|
|
7
7
|
import { MapboxRadarLayer } from './nexrad/MapboxRadarLayer.bundled.js';
|
|
8
8
|
import { nexradBinGroupIdForKey, variableToNexradGroup } from '@aguacerowx/javascript-sdk';
|
|
9
|
-
import { sampleNexradFrameAtLatLon } from './nexrad/nexradCrossSectionSampleAtLatLon.
|
|
10
|
-
import { prepareRadarFrameForGpuReadout } from './nexrad/radarFrameGpuMatch.
|
|
9
|
+
import { sampleNexradFrameAtLatLon } from './nexrad/nexradCrossSectionSampleAtLatLon.bundled.js';
|
|
10
|
+
import { prepareRadarFrameForGpuReadout } from './nexrad/radarFrameGpuMatch.bundled.js';
|
|
11
11
|
import { mapboxFrameUploadOptionsForNexradState } from './nexrad/nexradMapboxFrameOpts.js';
|
|
12
12
|
|
|
13
13
|
function pickNearestLevel3ObjectKey(unixTime, timeToKeyMap, maxDeltaSec = 600) {
|
|
@@ -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
|
-
|
|
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(); }
|
|
@@ -722,7 +922,211 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
722
922
|
// the active time.
|
|
723
923
|
}
|
|
724
924
|
|
|
725
|
-
|
|
925
|
+
/**
|
|
926
|
+
* MRMS timestamps or model forecast hours for the active timeline (ordering preserved from source lists).
|
|
927
|
+
* @returns {number[]}
|
|
928
|
+
*/
|
|
929
|
+
_collectNormalizedTimelineSteps(state) {
|
|
930
|
+
let fromCore = [];
|
|
931
|
+
try {
|
|
932
|
+
if (!state.isMRMS && typeof this.core.getAvailableForecastHours === 'function') {
|
|
933
|
+
fromCore = this.core.getAvailableForecastHours();
|
|
934
|
+
}
|
|
935
|
+
} catch (err) {
|
|
936
|
+
// ignore
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const fromState = state.isMRMS
|
|
940
|
+
? (state.availableTimestamps || [])
|
|
941
|
+
: (state.availableHours || []);
|
|
942
|
+
|
|
943
|
+
let timeSteps;
|
|
944
|
+
let mrmsTimelineSource = '';
|
|
945
|
+
if (state.isMRMS) {
|
|
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
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
timeSteps = fromCore.length > 0 ? fromCore : fromState;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const out = (timeSteps || [])
|
|
974
|
+
.map(t => Number(t))
|
|
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;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* @param {object} state
|
|
992
|
+
* @param {number[]} times
|
|
993
|
+
* @param {{ rebuildId: number, mode: 'rebuild' | 'append' }} options
|
|
994
|
+
*/
|
|
995
|
+
_runParallelGridFrameLoads(state, times, options) {
|
|
996
|
+
const { rebuildId, mode } = options;
|
|
997
|
+
if (!times.length || !this.shaderLayer) {
|
|
998
|
+
if (mode === 'rebuild') {
|
|
999
|
+
this._initialGridLoadPending = false;
|
|
1000
|
+
}
|
|
1001
|
+
this._debugLog('_runParallelGridFrameLoads:skip', {
|
|
1002
|
+
reason: !times.length ? 'no times' : 'no shaderLayer',
|
|
1003
|
+
mode,
|
|
1004
|
+
rebuildId,
|
|
1005
|
+
timesLen: times.length,
|
|
1006
|
+
});
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const currentFrameTime = Number(state.isMRMS ? state.mrmsTimestamp : state.forecastHour);
|
|
1011
|
+
/** When the active timestep is unset/NaN, paint the first timeline step (legacy single-fetch behavior). */
|
|
1012
|
+
let primaryTimeForRebuild = Number.NaN;
|
|
1013
|
+
if (mode === 'rebuild') {
|
|
1014
|
+
primaryTimeForRebuild = Number.isFinite(currentFrameTime)
|
|
1015
|
+
? currentFrameTime
|
|
1016
|
+
: times[0];
|
|
1017
|
+
}
|
|
1018
|
+
const tsKey = state.isMRMS ? 'mrmsTimestamp' : 'forecastHour';
|
|
1019
|
+
const gridModel = state.isMRMS ? 'mrms' : state.model;
|
|
1020
|
+
const { gridDef } = this.core._getGridCornersAndDef(gridModel);
|
|
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
|
+
|
|
1041
|
+
times.forEach((time) => {
|
|
1042
|
+
const stateForTime = { ...state, [tsKey]: time };
|
|
1043
|
+
this.core._loadGridData(stateForTime)
|
|
1044
|
+
.then((grid) => {
|
|
1045
|
+
if (rebuildId !== this.currentRebuildId || !this.shaderLayer) {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const isPrimaryFrame =
|
|
1050
|
+
mode === 'rebuild' &&
|
|
1051
|
+
Number.isFinite(primaryTimeForRebuild) &&
|
|
1052
|
+
time === primaryTimeForRebuild;
|
|
1053
|
+
|
|
1054
|
+
if (isPrimaryFrame) {
|
|
1055
|
+
const coreTimeKey = state.isMRMS
|
|
1056
|
+
? (this.core.state.mrmsTimestamp == null
|
|
1057
|
+
? null
|
|
1058
|
+
: Number(this.core.state.mrmsTimestamp))
|
|
1059
|
+
: Number(this.core.state.forecastHour);
|
|
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
|
+
});
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (!grid?.data) {
|
|
1070
|
+
this._debugLog('_loadGridData:primary frame missing grid.data', {
|
|
1071
|
+
time,
|
|
1072
|
+
rebuildId,
|
|
1073
|
+
mode,
|
|
1074
|
+
});
|
|
1075
|
+
this._initialGridLoadPending = false;
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
this.shaderLayer.updateDataTexture(
|
|
1079
|
+
grid.data,
|
|
1080
|
+
grid.encoding,
|
|
1081
|
+
gridDef.grid_params.nx,
|
|
1082
|
+
gridDef.grid_params.ny,
|
|
1083
|
+
);
|
|
1084
|
+
this.currentLoadedTimeKey = time;
|
|
1085
|
+
this.shaderLayer.registerCurrentDataTextureAsPreloaded(time);
|
|
1086
|
+
this._initialGridLoadPending = false;
|
|
1087
|
+
this.map.triggerRepaint();
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (grid?.data) {
|
|
1092
|
+
this.shaderLayer.storePreloadedTexture(
|
|
1093
|
+
time,
|
|
1094
|
+
grid.data,
|
|
1095
|
+
grid.encoding,
|
|
1096
|
+
gridDef.grid_params.nx,
|
|
1097
|
+
gridDef.grid_params.ny,
|
|
1098
|
+
);
|
|
1099
|
+
const s = this.core.state;
|
|
1100
|
+
const activeTime = s.isMRMS
|
|
1101
|
+
? (s.mrmsTimestamp == null ? null : Number(s.mrmsTimestamp))
|
|
1102
|
+
: Number(s.forecastHour);
|
|
1103
|
+
if (time === activeTime && this.shaderLayer.switchToPreloadedTexture(time)) {
|
|
1104
|
+
this.currentLoadedTimeKey = time;
|
|
1105
|
+
this.map.triggerRepaint();
|
|
1106
|
+
}
|
|
1107
|
+
} else if (this._debug && mode === 'append') {
|
|
1108
|
+
this._debugLog('_loadGridData:empty grid (append)', { time, mode, rebuildId });
|
|
1109
|
+
}
|
|
1110
|
+
})
|
|
1111
|
+
.catch((err) => {
|
|
1112
|
+
this._debugLog('_loadGridData:error', {
|
|
1113
|
+
time,
|
|
1114
|
+
mode,
|
|
1115
|
+
rebuildId,
|
|
1116
|
+
message: err?.message || String(err),
|
|
1117
|
+
});
|
|
1118
|
+
if (
|
|
1119
|
+
mode === 'rebuild' &&
|
|
1120
|
+
Number.isFinite(primaryTimeForRebuild) &&
|
|
1121
|
+
time === primaryTimeForRebuild
|
|
1122
|
+
) {
|
|
1123
|
+
this._initialGridLoadPending = false;
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
_rebuildLayerAndPreload(state) {
|
|
726
1130
|
if (state.isSatellite) {
|
|
727
1131
|
return;
|
|
728
1132
|
}
|
|
@@ -775,119 +1179,69 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
775
1179
|
: Number(state.forecastHour);
|
|
776
1180
|
this._initialGridLoadPending = true;
|
|
777
1181
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
:
|
|
794
|
-
|
|
1182
|
+
const normalized = this._collectNormalizedTimelineSteps(state);
|
|
1183
|
+
const currentFrameTime = Number(state.isMRMS ? state.mrmsTimestamp : state.forecastHour);
|
|
1184
|
+
const timesSet = new Set(normalized);
|
|
1185
|
+
if (!Number.isNaN(currentFrameTime)) {
|
|
1186
|
+
timesSet.add(currentFrameTime);
|
|
1187
|
+
}
|
|
1188
|
+
let timesToLoad = [...timesSet];
|
|
1189
|
+
if (timesToLoad.length === 0 && !Number.isNaN(currentFrameTime)) {
|
|
1190
|
+
timesToLoad = [currentFrameTime];
|
|
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
|
+
});
|
|
1203
|
+
if (timesToLoad.length === 0) {
|
|
795
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
|
+
});
|
|
796
1212
|
return;
|
|
797
1213
|
}
|
|
798
1214
|
|
|
799
|
-
if (grid && grid.data) {
|
|
800
|
-
const gridModel = state.isMRMS ? 'mrms' : state.model;
|
|
801
|
-
const { gridDef } = this.core._getGridCornersAndDef(gridModel);
|
|
802
|
-
|
|
803
|
-
this.shaderLayer.updateDataTexture(
|
|
804
|
-
grid.data, grid.encoding,
|
|
805
|
-
gridDef.grid_params.nx, gridDef.grid_params.ny
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
this.currentLoadedTimeKey = state.isMRMS
|
|
809
|
-
? (state.mrmsTimestamp == null ? null : Number(state.mrmsTimestamp))
|
|
810
|
-
: Number(state.forecastHour);
|
|
811
|
-
this.shaderLayer.registerCurrentDataTextureAsPreloaded(this.currentLoadedTimeKey);
|
|
812
|
-
this.map.triggerRepaint();
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
this._initialGridLoadPending = false;
|
|
816
|
-
|
|
817
1215
|
if (rebuildId === this.currentRebuildId) {
|
|
818
|
-
this.
|
|
1216
|
+
this._runParallelGridFrameLoads(state, timesToLoad, { rebuildId, mode: 'rebuild' });
|
|
819
1217
|
}
|
|
820
1218
|
}
|
|
821
|
-
|
|
822
|
-
_preloadAllTimeSteps(state) {
|
|
823
|
-
let fromCore = [];
|
|
824
|
-
try {
|
|
825
|
-
if (!state.isMRMS && typeof this.core.getAvailableForecastHours === 'function') {
|
|
826
|
-
fromCore = this.core.getAvailableForecastHours();
|
|
827
|
-
}
|
|
828
|
-
} catch (err) {
|
|
829
|
-
// ignore
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
const fromState = state.isMRMS
|
|
833
|
-
? (state.availableTimestamps || [])
|
|
834
|
-
: (state.availableHours || []);
|
|
835
|
-
|
|
836
|
-
let timeSteps;
|
|
837
|
-
if (state.isMRMS) {
|
|
838
|
-
timeSteps = fromState.length ? fromState : fromCore;
|
|
839
|
-
} else {
|
|
840
|
-
timeSteps = fromCore.length > 0 ? fromCore : fromState;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
const normalized = (timeSteps || [])
|
|
844
|
-
.map(t => Number(t))
|
|
845
|
-
.filter(t => !Number.isNaN(t));
|
|
846
1219
|
|
|
1220
|
+
_preloadAllTimeSteps(state) {
|
|
1221
|
+
const normalized = this._collectNormalizedTimelineSteps(state);
|
|
847
1222
|
const currentFrameTime = Number(state.isMRMS ? state.mrmsTimestamp : state.forecastHour);
|
|
848
1223
|
const stepsToPreload = normalized.filter(t => t !== currentFrameTime);
|
|
849
1224
|
|
|
1225
|
+
this._debugLog('_preloadAllTimeSteps', {
|
|
1226
|
+
normalizedLen: normalized.length,
|
|
1227
|
+
currentFrameTime,
|
|
1228
|
+
stepsToPreloadLen: stepsToPreload.length,
|
|
1229
|
+
stepsSummary: _debugSummarizeNumericSeries(stepsToPreload),
|
|
1230
|
+
});
|
|
1231
|
+
|
|
850
1232
|
if (normalized.length === 0) {
|
|
851
1233
|
return;
|
|
852
1234
|
}
|
|
853
1235
|
|
|
854
1236
|
if (stepsToPreload.length === 0) {
|
|
1237
|
+
this._debugLog('_preloadAllTimeSteps:skip (nothing to preload besides current)', {
|
|
1238
|
+
currentFrameTime,
|
|
1239
|
+
});
|
|
855
1240
|
return;
|
|
856
1241
|
}
|
|
857
1242
|
|
|
858
1243
|
const capturedRebuildId = this.currentRebuildId;
|
|
859
|
-
|
|
860
|
-
const gridModel = state.isMRMS ? 'mrms' : state.model;
|
|
861
|
-
const { gridDef } = this.core._getGridCornersAndDef(gridModel);
|
|
862
|
-
|
|
863
|
-
stepsToPreload.forEach(time => {
|
|
864
|
-
const stateForTime = {
|
|
865
|
-
...state,
|
|
866
|
-
[state.isMRMS ? 'mrmsTimestamp' : 'forecastHour']: time
|
|
867
|
-
};
|
|
868
|
-
|
|
869
|
-
this.core._loadGridData(stateForTime)
|
|
870
|
-
.then(grid => {
|
|
871
|
-
if (capturedRebuildId !== this.currentRebuildId) {
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
if (grid?.data && this.shaderLayer) {
|
|
875
|
-
this.shaderLayer.storePreloadedTexture(
|
|
876
|
-
time, grid.data, grid.encoding,
|
|
877
|
-
gridDef.grid_params.nx, gridDef.grid_params.ny
|
|
878
|
-
);
|
|
879
|
-
const s = this.core.state;
|
|
880
|
-
const activeTime = s.isMRMS
|
|
881
|
-
? (s.mrmsTimestamp == null ? null : Number(s.mrmsTimestamp))
|
|
882
|
-
: Number(s.forecastHour);
|
|
883
|
-
if (time === activeTime && this.shaderLayer.switchToPreloadedTexture(time)) {
|
|
884
|
-
this.currentLoadedTimeKey = time;
|
|
885
|
-
this.map.triggerRepaint();
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
})
|
|
889
|
-
.catch(() => {});
|
|
890
|
-
});
|
|
1244
|
+
this._runParallelGridFrameLoads(state, stepsToPreload, { rebuildId: capturedRebuildId, mode: 'append' });
|
|
891
1245
|
}
|
|
892
1246
|
|
|
893
1247
|
_updateLayerData(state) {
|
|
@@ -1124,6 +1478,7 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1124
1478
|
* Cleans up all resources.
|
|
1125
1479
|
*/
|
|
1126
1480
|
destroy() {
|
|
1481
|
+
this._debugLog('destroy');
|
|
1127
1482
|
this.setAutoRefresh(false);
|
|
1128
1483
|
|
|
1129
1484
|
// 1. Unbind the map's mousemove event listener
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function wrapAzimuthDeltaDeg(azDeg, azimuthBaseDeg) {
|
|
2
|
+
let da = azDeg - azimuthBaseDeg;
|
|
3
|
+
da -= Math.floor(da / 360) * 360;
|
|
4
|
+
if (da < 0) da += 360;
|
|
5
|
+
return da;
|
|
6
|
+
}
|
|
7
|
+
function snapGateCoord(t, nCells) {
|
|
8
|
+
if (nCells <= 1) return 0;
|
|
9
|
+
const idx = Math.floor(t * (nCells - 1) + 0.5);
|
|
10
|
+
return idx / (nCells - 1);
|
|
11
|
+
}
|
|
12
|
+
function readGateRawSigned(frame, rayIdx, gateIdx) {
|
|
13
|
+
if (rayIdx < 0 || gateIdx < 0 || rayIdx >= frame.nRays || gateIdx >= frame.nGates) return null;
|
|
14
|
+
const byteOffset = (rayIdx * frame.nGates + gateIdx) * 2;
|
|
15
|
+
const hi = frame.gateData[byteOffset];
|
|
16
|
+
const lo = frame.gateData[byteOffset + 1];
|
|
17
|
+
const raw = lo + hi * 256;
|
|
18
|
+
const rawSigned = raw >= 32768 ? raw - 65536 : raw;
|
|
19
|
+
if (rawSigned <= -32768) return null;
|
|
20
|
+
return rawSigned;
|
|
21
|
+
}
|
|
22
|
+
function sampleGateRawBilinearDecoded(frame, gateX, gateY) {
|
|
23
|
+
const w = frame.nGates;
|
|
24
|
+
const h = frame.nRays;
|
|
25
|
+
if (w < 1 || h < 1) return null;
|
|
26
|
+
const gx = Math.min(1, Math.max(0, gateX));
|
|
27
|
+
const gy = Math.min(1, Math.max(0, gateY));
|
|
28
|
+
const sx = gx * Math.max(w - 1, 1);
|
|
29
|
+
const sy = gy * Math.max(h - 1, 1);
|
|
30
|
+
const i0 = Math.floor(sx);
|
|
31
|
+
const j0 = Math.floor(sy);
|
|
32
|
+
const i1 = Math.min(i0 + 1, w - 1);
|
|
33
|
+
const j1 = Math.min(j0 + 1, h - 1);
|
|
34
|
+
const fx = sx - i0;
|
|
35
|
+
const fy = sy - j0;
|
|
36
|
+
const w00 = (1 - fx) * (1 - fy);
|
|
37
|
+
const w10 = fx * (1 - fy);
|
|
38
|
+
const w01 = (1 - fx) * fy;
|
|
39
|
+
const w11 = fx * fy;
|
|
40
|
+
let acc = 0;
|
|
41
|
+
let wsum = 0;
|
|
42
|
+
const add = (r, wt) => {
|
|
43
|
+
if (r !== null) {
|
|
44
|
+
acc += r * wt;
|
|
45
|
+
wsum += wt;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
add(readGateRawSigned(frame, j0, i0), w00);
|
|
49
|
+
add(readGateRawSigned(frame, j0, i1), w10);
|
|
50
|
+
add(readGateRawSigned(frame, j1, i0), w01);
|
|
51
|
+
add(readGateRawSigned(frame, j1, i1), w11);
|
|
52
|
+
if (wsum < 1e-6) return null;
|
|
53
|
+
return acc / wsum;
|
|
54
|
+
}
|
|
55
|
+
function sampleNexradFrameAtLatLon(frame, lat, lon, options) {
|
|
56
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
57
|
+
const EARTH_RADIUS_M = 6378137;
|
|
58
|
+
const dLatDeg = lat - frame.stationLat;
|
|
59
|
+
const dLonDeg = lon - frame.stationLon;
|
|
60
|
+
const cosLat = Math.max(Math.cos(lat * DEG_TO_RAD), 1e-6);
|
|
61
|
+
const xM = dLonDeg * DEG_TO_RAD * EARTH_RADIUS_M * cosLat;
|
|
62
|
+
const yM = dLatDeg * DEG_TO_RAD * EARTH_RADIUS_M;
|
|
63
|
+
const rangeM = Math.hypot(xM, yM);
|
|
64
|
+
const rangeKm = rangeM / 1e3;
|
|
65
|
+
if (rangeKm < frame.firstGateKm) return null;
|
|
66
|
+
const spanKm = frame.nGates * frame.gateWidthKm;
|
|
67
|
+
if (!(spanKm > 0)) return null;
|
|
68
|
+
const maxRangeKm = frame.firstGateKm + spanKm;
|
|
69
|
+
if (rangeKm >= maxRangeKm) return null;
|
|
70
|
+
let azDeg = Math.atan2(xM, yM) * (180 / Math.PI);
|
|
71
|
+
if (azDeg < 0) azDeg += 360;
|
|
72
|
+
const boundaries = frame.rayBoundariesDeg;
|
|
73
|
+
const azimuthBaseDeg = boundaries.length > 0 ? Number(boundaries[0]) : 0;
|
|
74
|
+
const da = wrapAzimuthDeltaDeg(azDeg, azimuthBaseDeg);
|
|
75
|
+
let gateY = da / 360;
|
|
76
|
+
gateY = Math.min(1, Math.max(0, gateY));
|
|
77
|
+
let gateX = (rangeKm - frame.firstGateKm) / spanKm;
|
|
78
|
+
gateX = Math.min(1, Math.max(0, gateX));
|
|
79
|
+
const smoothPolar = options?.smoothPolar === true;
|
|
80
|
+
if (!smoothPolar) {
|
|
81
|
+
gateX = snapGateCoord(gateX, frame.nGates);
|
|
82
|
+
gateY = snapGateCoord(gateY, frame.nRays);
|
|
83
|
+
}
|
|
84
|
+
const rawSigned = sampleGateRawBilinearDecoded(frame, gateX, gateY);
|
|
85
|
+
if (rawSigned === null) return null;
|
|
86
|
+
const physical = rawSigned * frame.valueScale + frame.valueOffset;
|
|
87
|
+
return { value: physical, groundRangeKm: rangeKm };
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
sampleNexradFrameAtLatLon
|
|
91
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
function angularDistanceDeg(a, b) {
|
|
2
|
+
let d = Math.abs(a - b) % 360;
|
|
3
|
+
if (d > 180) d = 360 - d;
|
|
4
|
+
return d;
|
|
5
|
+
}
|
|
6
|
+
function canonicalBinsRadarFrame(frame) {
|
|
7
|
+
const nRays = frame.nRays;
|
|
8
|
+
const nGates = frame.nGates;
|
|
9
|
+
if (nRays <= 0 || nGates <= 0) return frame;
|
|
10
|
+
if (frame.rayBoundariesDeg.length < nRays + 1) return frame;
|
|
11
|
+
const degPerBin = 360 / nRays;
|
|
12
|
+
const bytesPerRay = nGates * 2;
|
|
13
|
+
const centers = new Float32Array(nRays);
|
|
14
|
+
for (let r = 0; r < nRays; r++) {
|
|
15
|
+
const lower = frame.rayBoundariesDeg[r];
|
|
16
|
+
const upper = frame.rayBoundariesDeg[r + 1];
|
|
17
|
+
const center = (lower + upper) * 0.5;
|
|
18
|
+
centers[r] = (center % 360 + 360) % 360;
|
|
19
|
+
}
|
|
20
|
+
const canonicalGateData = new Uint8Array(nRays * bytesPerRay);
|
|
21
|
+
for (let i = 0; i < canonicalGateData.length; i += 2) {
|
|
22
|
+
canonicalGateData[i] = 128;
|
|
23
|
+
}
|
|
24
|
+
const used = new Array(nRays).fill(false);
|
|
25
|
+
for (let bin = 0; bin < nRays; bin++) {
|
|
26
|
+
const targetDeg = ((bin + 0.5) * degPerBin % 360 + 360) % 360;
|
|
27
|
+
let bestR = -1;
|
|
28
|
+
let bestDist = Infinity;
|
|
29
|
+
for (let r = 0; r < nRays; r++) {
|
|
30
|
+
if (used[r]) continue;
|
|
31
|
+
const dist = angularDistanceDeg(centers[r], targetDeg);
|
|
32
|
+
if (dist < bestDist) {
|
|
33
|
+
bestDist = dist;
|
|
34
|
+
bestR = r;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (bestR >= 0) {
|
|
38
|
+
used[bestR] = true;
|
|
39
|
+
canonicalGateData.set(
|
|
40
|
+
frame.gateData.subarray(bestR * bytesPerRay, (bestR + 1) * bytesPerRay),
|
|
41
|
+
bin * bytesPerRay
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const canonicalBoundaries = new Float32Array(nRays + 1);
|
|
46
|
+
for (let i = 0; i <= nRays; i++) {
|
|
47
|
+
canonicalBoundaries[i] = i * degPerBin;
|
|
48
|
+
}
|
|
49
|
+
return { ...frame, gateData: canonicalGateData, rayBoundariesDeg: canonicalBoundaries };
|
|
50
|
+
}
|
|
51
|
+
function sortRadarFrameByAzimuth(frame) {
|
|
52
|
+
const nRays = frame.nRays;
|
|
53
|
+
const nGates = frame.nGates;
|
|
54
|
+
const bytesPerRay = nGates * 2;
|
|
55
|
+
const rayOrder = Array.from({ length: nRays }, (_, i) => i).sort(
|
|
56
|
+
(a, b) => frame.rayBoundariesDeg[a] - frame.rayBoundariesDeg[b]
|
|
57
|
+
);
|
|
58
|
+
const sortedGateData = new Uint8Array(nRays * bytesPerRay);
|
|
59
|
+
const sortedBoundaries = new Float32Array(nRays + 1);
|
|
60
|
+
for (let newIdx = 0; newIdx < nRays; newIdx++) {
|
|
61
|
+
const oldIdx = rayOrder[newIdx];
|
|
62
|
+
sortedGateData.set(
|
|
63
|
+
frame.gateData.subarray(oldIdx * bytesPerRay, (oldIdx + 1) * bytesPerRay),
|
|
64
|
+
newIdx * bytesPerRay
|
|
65
|
+
);
|
|
66
|
+
sortedBoundaries[newIdx] = frame.rayBoundariesDeg[oldIdx];
|
|
67
|
+
}
|
|
68
|
+
sortedBoundaries[nRays] = frame.rayBoundariesDeg[rayOrder[nRays - 1]] + (frame.rayBoundariesDeg[rayOrder[1]] - frame.rayBoundariesDeg[rayOrder[0]]);
|
|
69
|
+
return { ...frame, gateData: sortedGateData, rayBoundariesDeg: sortedBoundaries };
|
|
70
|
+
}
|
|
71
|
+
function prepareRadarFrameForGpuReadout(frame, options) {
|
|
72
|
+
const hasLayoutKey = options?.geometryLayoutKey != null && options.geometryLayoutKey !== "";
|
|
73
|
+
return hasLayoutKey ? canonicalBinsRadarFrame(frame) : sortRadarFrameByAzimuth(frame);
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
canonicalBinsRadarFrame,
|
|
77
|
+
prepareRadarFrameForGpuReadout,
|
|
78
|
+
sortRadarFrameByAzimuth
|
|
79
|
+
};
|