@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.
|
|
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
|
-
"
|
|
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) {
|
|
@@ -722,7 +722,132 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
722
722
|
// the active time.
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
-
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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 (
|
|
787
|
-
|
|
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.
|
|
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
|
+
};
|