@aguacerowx/mapsgl 0.0.41 → 0.0.42

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.41",
3
+ "version": "0.0.42",
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
- "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",
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.ts';
10
- import { prepareRadarFrameForGpuReadout } from './nexrad/radarFrameGpuMatch.ts';
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) {
@@ -722,7 +722,132 @@ export class WeatherLayerManager extends EventEmitter {
722
722
  // the active time.
723
723
  }
724
724
 
725
- async _rebuildLayerAndPreload(state) {
725
+ /**
726
+ * MRMS timestamps or model forecast hours for the active timeline (ordering preserved from source lists).
727
+ * @returns {number[]}
728
+ */
729
+ _collectNormalizedTimelineSteps(state) {
730
+ let fromCore = [];
731
+ try {
732
+ if (!state.isMRMS && typeof this.core.getAvailableForecastHours === 'function') {
733
+ fromCore = this.core.getAvailableForecastHours();
734
+ }
735
+ } catch (err) {
736
+ // ignore
737
+ }
738
+
739
+ const fromState = state.isMRMS
740
+ ? (state.availableTimestamps || [])
741
+ : (state.availableHours || []);
742
+
743
+ let timeSteps;
744
+ if (state.isMRMS) {
745
+ timeSteps = fromState.length ? fromState : fromCore;
746
+ } else {
747
+ timeSteps = fromCore.length > 0 ? fromCore : fromState;
748
+ }
749
+
750
+ return (timeSteps || [])
751
+ .map(t => Number(t))
752
+ .filter(t => !Number.isNaN(t));
753
+ }
754
+
755
+ /**
756
+ * @param {object} state
757
+ * @param {number[]} times
758
+ * @param {{ rebuildId: number, mode: 'rebuild' | 'append' }} options
759
+ */
760
+ _runParallelGridFrameLoads(state, times, options) {
761
+ const { rebuildId, mode } = options;
762
+ if (!times.length || !this.shaderLayer) {
763
+ if (mode === 'rebuild') {
764
+ this._initialGridLoadPending = false;
765
+ }
766
+ return;
767
+ }
768
+
769
+ const currentFrameTime = Number(state.isMRMS ? state.mrmsTimestamp : state.forecastHour);
770
+ /** When the active timestep is unset/NaN, paint the first timeline step (legacy single-fetch behavior). */
771
+ let primaryTimeForRebuild = Number.NaN;
772
+ if (mode === 'rebuild') {
773
+ primaryTimeForRebuild = Number.isFinite(currentFrameTime)
774
+ ? currentFrameTime
775
+ : times[0];
776
+ }
777
+ const tsKey = state.isMRMS ? 'mrmsTimestamp' : 'forecastHour';
778
+ const gridModel = state.isMRMS ? 'mrms' : state.model;
779
+ const { gridDef } = this.core._getGridCornersAndDef(gridModel);
780
+
781
+ times.forEach((time) => {
782
+ const stateForTime = { ...state, [tsKey]: time };
783
+ this.core._loadGridData(stateForTime)
784
+ .then((grid) => {
785
+ if (rebuildId !== this.currentRebuildId || !this.shaderLayer) {
786
+ return;
787
+ }
788
+
789
+ const isPrimaryFrame =
790
+ mode === 'rebuild' &&
791
+ Number.isFinite(primaryTimeForRebuild) &&
792
+ time === primaryTimeForRebuild;
793
+
794
+ if (isPrimaryFrame) {
795
+ const coreTimeKey = state.isMRMS
796
+ ? (this.core.state.mrmsTimestamp == null
797
+ ? null
798
+ : Number(this.core.state.mrmsTimestamp))
799
+ : Number(this.core.state.forecastHour);
800
+ if (coreTimeKey !== this._rebuildTargetTimeKey) {
801
+ return;
802
+ }
803
+ if (!grid?.data) {
804
+ this._initialGridLoadPending = false;
805
+ return;
806
+ }
807
+ this.shaderLayer.updateDataTexture(
808
+ grid.data,
809
+ grid.encoding,
810
+ gridDef.grid_params.nx,
811
+ gridDef.grid_params.ny,
812
+ );
813
+ this.currentLoadedTimeKey = time;
814
+ this.shaderLayer.registerCurrentDataTextureAsPreloaded(time);
815
+ this._initialGridLoadPending = false;
816
+ this.map.triggerRepaint();
817
+ return;
818
+ }
819
+
820
+ if (grid?.data) {
821
+ this.shaderLayer.storePreloadedTexture(
822
+ time,
823
+ grid.data,
824
+ grid.encoding,
825
+ gridDef.grid_params.nx,
826
+ gridDef.grid_params.ny,
827
+ );
828
+ const s = this.core.state;
829
+ const activeTime = s.isMRMS
830
+ ? (s.mrmsTimestamp == null ? null : Number(s.mrmsTimestamp))
831
+ : Number(s.forecastHour);
832
+ if (time === activeTime && this.shaderLayer.switchToPreloadedTexture(time)) {
833
+ this.currentLoadedTimeKey = time;
834
+ this.map.triggerRepaint();
835
+ }
836
+ }
837
+ })
838
+ .catch(() => {
839
+ if (
840
+ mode === 'rebuild' &&
841
+ Number.isFinite(primaryTimeForRebuild) &&
842
+ time === primaryTimeForRebuild
843
+ ) {
844
+ this._initialGridLoadPending = false;
845
+ }
846
+ });
847
+ });
848
+ }
849
+
850
+ _rebuildLayerAndPreload(state) {
726
851
  if (state.isSatellite) {
727
852
  return;
728
853
  }
@@ -775,75 +900,28 @@ export class WeatherLayerManager extends EventEmitter {
775
900
  : Number(state.forecastHour);
776
901
  this._initialGridLoadPending = true;
777
902
 
778
- let grid;
779
- try {
780
- grid = await this.core._loadGridData(state);
781
- } catch (e) {
782
- this._initialGridLoadPending = false;
783
- throw e;
903
+ const normalized = this._collectNormalizedTimelineSteps(state);
904
+ const currentFrameTime = Number(state.isMRMS ? state.mrmsTimestamp : state.forecastHour);
905
+ const timesSet = new Set(normalized);
906
+ if (!Number.isNaN(currentFrameTime)) {
907
+ timesSet.add(currentFrameTime);
784
908
  }
785
-
786
- if (rebuildId !== this.currentRebuildId) {
787
- this._initialGridLoadPending = false;
788
- return;
909
+ let timesToLoad = [...timesSet];
910
+ if (timesToLoad.length === 0 && !Number.isNaN(currentFrameTime)) {
911
+ timesToLoad = [currentFrameTime];
789
912
  }
790
-
791
- const coreTimeKey = state.isMRMS
792
- ? (this.core.state.mrmsTimestamp == null ? null : Number(this.core.state.mrmsTimestamp))
793
- : Number(this.core.state.forecastHour);
794
- if (coreTimeKey !== this._rebuildTargetTimeKey) {
913
+ if (timesToLoad.length === 0) {
795
914
  this._initialGridLoadPending = false;
796
915
  return;
797
916
  }
798
917
 
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
918
  if (rebuildId === this.currentRebuildId) {
818
- this._preloadAllTimeSteps(state);
919
+ this._runParallelGridFrameLoads(state, timesToLoad, { rebuildId, mode: 'rebuild' });
819
920
  }
820
921
  }
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
922
 
923
+ _preloadAllTimeSteps(state) {
924
+ const normalized = this._collectNormalizedTimelineSteps(state);
847
925
  const currentFrameTime = Number(state.isMRMS ? state.mrmsTimestamp : state.forecastHour);
848
926
  const stepsToPreload = normalized.filter(t => t !== currentFrameTime);
849
927
 
@@ -856,38 +934,7 @@ export class WeatherLayerManager extends EventEmitter {
856
934
  }
857
935
 
858
936
  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
- });
937
+ this._runParallelGridFrameLoads(state, stepsToPreload, { rebuildId: capturedRebuildId, mode: 'append' });
891
938
  }
892
939
 
893
940
  _updateLayerData(state) {
@@ -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
+ };