@aguacerowx/mapsgl 0.0.57 → 0.0.58

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.
@@ -1,121 +1,121 @@
1
- /**
2
- * Sample a decoded NEXRAD frame at lat/lon (same geometry as MapboxRadarLayer / readout).
3
- * Extracted from RadarLayer so workers and cross-section code can import without pulling the layer bundle.
4
- */
5
- import type { DecodedRadarFrame } from './nexradArchiveCache.js';
6
-
7
- export type SampleNexradAtLatLonOptions = {
8
- /**
9
- * When true, match `MapboxRadarLayer` with gate smoothing: continuous gateX/gateY + bilinear blend of
10
- * decoded int16 samples (nodata excluded). When false, snap to nearest gate/ray like unsmoothed shader.
11
- */
12
- smoothPolar?: boolean;
13
- };
14
-
15
- function wrapAzimuthDeltaDeg(azDeg: number, azimuthBaseDeg: number): number {
16
- let da = azDeg - azimuthBaseDeg;
17
- da -= Math.floor(da / 360) * 360;
18
- if (da < 0) da += 360;
19
- return da;
20
- }
21
-
22
- /** Same as `snapGateCoord` in MapboxRadarLayer fragment shader. */
23
- function snapGateCoord(t: number, nCells: number): number {
24
- if (nCells <= 1) return 0;
25
- const idx = Math.floor(t * (nCells - 1) + 0.5);
26
- return idx / (nCells - 1);
27
- }
28
-
29
- function readGateRawSigned(frame: DecodedRadarFrame, rayIdx: number, gateIdx: number): number | null {
30
- if (rayIdx < 0 || gateIdx < 0 || rayIdx >= frame.nRays || gateIdx >= frame.nGates) return null;
31
- const byteOffset = (rayIdx * frame.nGates + gateIdx) * 2;
32
- const hi = frame.gateData[byteOffset]!;
33
- const lo = frame.gateData[byteOffset + 1]!;
34
- const raw = lo + hi * 256;
35
- const rawSigned = raw >= 32768 ? raw - 65536 : raw;
36
- if (rawSigned <= -32768) return null;
37
- return rawSigned;
38
- }
39
-
40
- /** Mirrors `sampleGateRawBilinear` in MapboxRadarLayer (NEAREST cells, blend after decode). */
41
- function sampleGateRawBilinearDecoded(frame: DecodedRadarFrame, gateX: number, gateY: number): number | null {
42
- const w = frame.nGates;
43
- const h = frame.nRays;
44
- if (w < 1 || h < 1) return null;
45
- const gx = Math.min(1, Math.max(0, gateX));
46
- const gy = Math.min(1, Math.max(0, gateY));
47
- const sx = gx * Math.max(w - 1, 1);
48
- const sy = gy * Math.max(h - 1, 1);
49
- const i0 = Math.floor(sx);
50
- const j0 = Math.floor(sy);
51
- const i1 = Math.min(i0 + 1, w - 1);
52
- const j1 = Math.min(j0 + 1, h - 1);
53
- const fx = sx - i0;
54
- const fy = sy - j0;
55
- const w00 = (1 - fx) * (1 - fy);
56
- const w10 = fx * (1 - fy);
57
- const w01 = (1 - fx) * fy;
58
- const w11 = fx * fy;
59
- let acc = 0;
60
- let wsum = 0;
61
- const add = (r: number | null, wt: number) => {
62
- if (r !== null) {
63
- acc += r * wt;
64
- wsum += wt;
65
- }
66
- };
67
- add(readGateRawSigned(frame, j0, i0), w00);
68
- add(readGateRawSigned(frame, j0, i1), w10);
69
- add(readGateRawSigned(frame, j1, i0), w01);
70
- add(readGateRawSigned(frame, j1, i1), w11);
71
- if (wsum < 1e-6) return null;
72
- return acc / wsum;
73
- }
74
-
75
- export function sampleNexradFrameAtLatLon(
76
- frame: DecodedRadarFrame,
77
- lat: number,
78
- lon: number,
79
- options?: SampleNexradAtLatLonOptions,
80
- ): { value: number; groundRangeKm: number } | null {
81
- const DEG_TO_RAD = Math.PI / 180;
82
- const EARTH_RADIUS_M = 6378137;
83
-
84
- const dLatDeg = lat - frame.stationLat;
85
- const dLonDeg = lon - frame.stationLon;
86
- const cosLat = Math.max(Math.cos(lat * DEG_TO_RAD), 1e-6);
87
- const xM = dLonDeg * DEG_TO_RAD * EARTH_RADIUS_M * cosLat;
88
- const yM = dLatDeg * DEG_TO_RAD * EARTH_RADIUS_M;
89
- const rangeM = Math.hypot(xM, yM);
90
- const rangeKm = rangeM / 1000;
91
-
92
- if (rangeKm < frame.firstGateKm) return null;
93
- const spanKm = frame.nGates * frame.gateWidthKm;
94
- if (!(spanKm > 0)) return null;
95
- const maxRangeKm = frame.firstGateKm + spanKm;
96
- if (rangeKm >= maxRangeKm) return null;
97
-
98
- let azDeg = Math.atan2(xM, yM) * (180 / Math.PI);
99
- if (azDeg < 0) azDeg += 360;
100
-
101
- const boundaries = frame.rayBoundariesDeg;
102
- const azimuthBaseDeg = boundaries.length > 0 ? Number(boundaries[0]) : 0;
103
- const da = wrapAzimuthDeltaDeg(azDeg, azimuthBaseDeg);
104
- let gateY = da / 360;
105
- gateY = Math.min(1, Math.max(0, gateY));
106
-
107
- let gateX = (rangeKm - frame.firstGateKm) / spanKm;
108
- gateX = Math.min(1, Math.max(0, gateX));
109
-
110
- const smoothPolar = options?.smoothPolar === true;
111
- if (!smoothPolar) {
112
- gateX = snapGateCoord(gateX, frame.nGates);
113
- gateY = snapGateCoord(gateY, frame.nRays);
114
- }
115
-
116
- const rawSigned = sampleGateRawBilinearDecoded(frame, gateX, gateY);
117
- if (rawSigned === null) return null;
118
-
119
- const physical = rawSigned * frame.valueScale + frame.valueOffset;
120
- return { value: physical, groundRangeKm: rangeKm };
121
- }
1
+ /**
2
+ * Sample a decoded NEXRAD frame at lat/lon (same geometry as MapboxRadarLayer / readout).
3
+ * Extracted from RadarLayer so workers and cross-section code can import without pulling the layer bundle.
4
+ */
5
+ import type { DecodedRadarFrame } from './nexradArchiveCache.js';
6
+
7
+ export type SampleNexradAtLatLonOptions = {
8
+ /**
9
+ * When true, match `MapboxRadarLayer` with gate smoothing: continuous gateX/gateY + bilinear blend of
10
+ * decoded int16 samples (nodata excluded). When false, snap to nearest gate/ray like unsmoothed shader.
11
+ */
12
+ smoothPolar?: boolean;
13
+ };
14
+
15
+ function wrapAzimuthDeltaDeg(azDeg: number, azimuthBaseDeg: number): number {
16
+ let da = azDeg - azimuthBaseDeg;
17
+ da -= Math.floor(da / 360) * 360;
18
+ if (da < 0) da += 360;
19
+ return da;
20
+ }
21
+
22
+ /** Same as `snapGateCoord` in MapboxRadarLayer fragment shader. */
23
+ function snapGateCoord(t: number, nCells: number): number {
24
+ if (nCells <= 1) return 0;
25
+ const idx = Math.floor(t * (nCells - 1) + 0.5);
26
+ return idx / (nCells - 1);
27
+ }
28
+
29
+ function readGateRawSigned(frame: DecodedRadarFrame, rayIdx: number, gateIdx: number): number | null {
30
+ if (rayIdx < 0 || gateIdx < 0 || rayIdx >= frame.nRays || gateIdx >= frame.nGates) return null;
31
+ const byteOffset = (rayIdx * frame.nGates + gateIdx) * 2;
32
+ const hi = frame.gateData[byteOffset]!;
33
+ const lo = frame.gateData[byteOffset + 1]!;
34
+ const raw = lo + hi * 256;
35
+ const rawSigned = raw >= 32768 ? raw - 65536 : raw;
36
+ if (rawSigned <= -32768) return null;
37
+ return rawSigned;
38
+ }
39
+
40
+ /** Mirrors `sampleGateRawBilinear` in MapboxRadarLayer (NEAREST cells, blend after decode). */
41
+ function sampleGateRawBilinearDecoded(frame: DecodedRadarFrame, gateX: number, gateY: number): number | null {
42
+ const w = frame.nGates;
43
+ const h = frame.nRays;
44
+ if (w < 1 || h < 1) return null;
45
+ const gx = Math.min(1, Math.max(0, gateX));
46
+ const gy = Math.min(1, Math.max(0, gateY));
47
+ const sx = gx * Math.max(w - 1, 1);
48
+ const sy = gy * Math.max(h - 1, 1);
49
+ const i0 = Math.floor(sx);
50
+ const j0 = Math.floor(sy);
51
+ const i1 = Math.min(i0 + 1, w - 1);
52
+ const j1 = Math.min(j0 + 1, h - 1);
53
+ const fx = sx - i0;
54
+ const fy = sy - j0;
55
+ const w00 = (1 - fx) * (1 - fy);
56
+ const w10 = fx * (1 - fy);
57
+ const w01 = (1 - fx) * fy;
58
+ const w11 = fx * fy;
59
+ let acc = 0;
60
+ let wsum = 0;
61
+ const add = (r: number | null, wt: number) => {
62
+ if (r !== null) {
63
+ acc += r * wt;
64
+ wsum += wt;
65
+ }
66
+ };
67
+ add(readGateRawSigned(frame, j0, i0), w00);
68
+ add(readGateRawSigned(frame, j0, i1), w10);
69
+ add(readGateRawSigned(frame, j1, i0), w01);
70
+ add(readGateRawSigned(frame, j1, i1), w11);
71
+ if (wsum < 1e-6) return null;
72
+ return acc / wsum;
73
+ }
74
+
75
+ export function sampleNexradFrameAtLatLon(
76
+ frame: DecodedRadarFrame,
77
+ lat: number,
78
+ lon: number,
79
+ options?: SampleNexradAtLatLonOptions,
80
+ ): { value: number; groundRangeKm: number } | null {
81
+ const DEG_TO_RAD = Math.PI / 180;
82
+ const EARTH_RADIUS_M = 6378137;
83
+
84
+ const dLatDeg = lat - frame.stationLat;
85
+ const dLonDeg = lon - frame.stationLon;
86
+ const cosLat = Math.max(Math.cos(lat * DEG_TO_RAD), 1e-6);
87
+ const xM = dLonDeg * DEG_TO_RAD * EARTH_RADIUS_M * cosLat;
88
+ const yM = dLatDeg * DEG_TO_RAD * EARTH_RADIUS_M;
89
+ const rangeM = Math.hypot(xM, yM);
90
+ const rangeKm = rangeM / 1000;
91
+
92
+ if (rangeKm < frame.firstGateKm) return null;
93
+ const spanKm = frame.nGates * frame.gateWidthKm;
94
+ if (!(spanKm > 0)) return null;
95
+ const maxRangeKm = frame.firstGateKm + spanKm;
96
+ if (rangeKm >= maxRangeKm) return null;
97
+
98
+ let azDeg = Math.atan2(xM, yM) * (180 / Math.PI);
99
+ if (azDeg < 0) azDeg += 360;
100
+
101
+ const boundaries = frame.rayBoundariesDeg;
102
+ const azimuthBaseDeg = boundaries.length > 0 ? Number(boundaries[0]) : 0;
103
+ const da = wrapAzimuthDeltaDeg(azDeg, azimuthBaseDeg);
104
+ let gateY = da / 360;
105
+ gateY = Math.min(1, Math.max(0, gateY));
106
+
107
+ let gateX = (rangeKm - frame.firstGateKm) / spanKm;
108
+ gateX = Math.min(1, Math.max(0, gateX));
109
+
110
+ const smoothPolar = options?.smoothPolar === true;
111
+ if (!smoothPolar) {
112
+ gateX = snapGateCoord(gateX, frame.nGates);
113
+ gateY = snapGateCoord(gateY, frame.nRays);
114
+ }
115
+
116
+ const rawSigned = sampleGateRawBilinearDecoded(frame, gateX, gateY);
117
+ if (rawSigned === null) return null;
118
+
119
+ const physical = rawSigned * frame.valueScale + frame.valueOffset;
120
+ return { value: physical, groundRangeKm: rangeKm };
121
+ }
@@ -1,126 +1,126 @@
1
- /**
2
- * MapboxRadarLayer upload options for NEXRAD — matches aguacero-frontend `RadarLayer` / MapboxRadarLayer docs:
3
- * Level-III KDP/N0H/VEL use {@link geometryLayoutKey} so frames use canonical azimuth bins (stable mesh; fast scrub).
4
- * Other Level-III radials use ray-boundary-keyed geometry when layout cannot be fixed to a manifest product.
5
- */
6
- import { clampNexradTiltForVariable, getDefaultRadarTilt, getRadarTilts } from '@aguacerowx/javascript-sdk';
7
- import { nexradLevel3S3VelocityProductForSiteTilt } from './nexradLevel3Products.js';
8
-
9
- export type MapboxRadarFrameUploadOptions = {
10
- geometryLayoutKey?: string;
11
- geometryCacheKeysRayBoundaries?: boolean;
12
- };
13
-
14
- const NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST = [
15
- 'N0K',
16
- 'NAK',
17
- 'N1K',
18
- 'NBK',
19
- 'N2K',
20
- 'N3K',
21
- ];
22
-
23
- const NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST = [
24
- 'N0H',
25
- 'NAH',
26
- 'N1H',
27
- 'NBH',
28
- 'N2H',
29
- 'N3H',
30
- ];
31
-
32
- const NEXRAD_LEVEL3_MANIFEST_TILT_COUNT = NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST.length;
33
-
34
- function nearestTiltInSortedList(tilts: number[], target: number): number {
35
- if (!tilts.length) return target;
36
- let best = tilts[0]!;
37
- let bestD = Math.abs(best - target);
38
- for (let i = 1; i < tilts.length; i++) {
39
- const t = tilts[i]!;
40
- const d = Math.abs(t - target);
41
- if (d < bestD || (d === bestD && t < best)) {
42
- best = t;
43
- bestD = d;
44
- }
45
- }
46
- return best;
47
- }
48
-
49
- function getNexradLevel3RadarTilts(siteId: string, radarVariable: string): number[] {
50
- return getRadarTilts(siteId, radarVariable).slice(0, NEXRAD_LEVEL3_MANIFEST_TILT_COUNT);
51
- }
52
-
53
- export function nexradLevel3UsesTiltIndexedS3Products(radarVariable: string | undefined): boolean {
54
- const v = radarVariable || '';
55
- return v === 'KDP' || v === 'N0H';
56
- }
57
-
58
- function clampTiltForLevel3CompositeKey(siteId: string, radarVariable: string, tilt: number): number {
59
- if (!nexradLevel3UsesTiltIndexedS3Products(radarVariable)) {
60
- return clampNexradTiltForVariable(siteId, radarVariable, tilt);
61
- }
62
- const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
63
- const c = clampNexradTiltForVariable(siteId, radarVariable, tilt);
64
- if (!tilts.length) return c;
65
- return nearestTiltInSortedList(tilts, c);
66
- }
67
-
68
- /**
69
- * Map site tilt to Level-III S3 product mnemonic (N0K, NAK, … / N0H, …) — aguacero-frontend parity.
70
- */
71
- export function nexradLevel3S3ProductForSiteTilt(
72
- siteId: string,
73
- radarVariable: 'KDP' | 'N0H',
74
- tilt: number,
75
- ): string {
76
- const products =
77
- radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST : NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST;
78
- const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
79
- if (!tilts.length) {
80
- return products[0]!;
81
- }
82
- const clamped = clampTiltForLevel3CompositeKey(siteId, radarVariable, tilt);
83
- let idx = tilts.indexOf(clamped);
84
- if (idx === -1) {
85
- idx = tilts.reduce(
86
- (bestI, t, i) => (Math.abs(t - clamped) < Math.abs(tilts[bestI]! - clamped) ? i : bestI),
87
- 0,
88
- );
89
- }
90
- idx = Math.min(idx, products.length - 1);
91
- return products[idx]!;
92
- }
93
-
94
- type NexradStateForMapbox = {
95
- nexradDataSource?: string;
96
- nexradSite?: string | null;
97
- nexradProduct?: string | null;
98
- nexradTilt?: number | null;
99
- };
100
-
101
- /**
102
- * @param state - AguaceroCore state (or state:change payload) with NEXRAD fields
103
- */
104
- export function mapboxFrameUploadOptionsForNexradState(
105
- state: NexradStateForMapbox | null | undefined,
106
- ): MapboxRadarFrameUploadOptions | undefined {
107
- if (!state) return { geometryLayoutKey: 'canonical' };
108
- if (state.nexradDataSource !== 'level3') return { geometryLayoutKey: 'canonical' };
109
- const site = state.nexradSite;
110
- const radarVar = (state.nexradProduct || 'REF').toUpperCase();
111
-
112
- if (nexradLevel3UsesTiltIndexedS3Products(radarVar) && site) {
113
- const tilt = Number.isFinite(state.nexradTilt) ? Number(state.nexradTilt) : getDefaultRadarTilt(site);
114
- const kind = radarVar === 'KDP' ? 'KDP' : 'N0H';
115
- const mnemonic = nexradLevel3S3ProductForSiteTilt(site, kind, tilt);
116
- return { geometryLayoutKey: `${site}|${radarVar}|${mnemonic}` };
117
- }
118
-
119
- if (radarVar === 'VEL' && site) {
120
- const tilt = Number.isFinite(state.nexradTilt) ? Number(state.nexradTilt) : getDefaultRadarTilt(site);
121
- const mnemonic = nexradLevel3S3VelocityProductForSiteTilt(site, tilt);
122
- return { geometryLayoutKey: `${site}|${radarVar}|${mnemonic}` };
123
- }
124
-
125
- return { geometryLayoutKey: 'canonical' };
126
- }
1
+ /**
2
+ * MapboxRadarLayer upload options for NEXRAD — matches aguacero-frontend `RadarLayer` / MapboxRadarLayer docs:
3
+ * Level-III KDP/N0H/VEL use {@link geometryLayoutKey} so frames use canonical azimuth bins (stable mesh; fast scrub).
4
+ * Other Level-III radials use ray-boundary-keyed geometry when layout cannot be fixed to a manifest product.
5
+ */
6
+ import { clampNexradTiltForVariable, getDefaultRadarTilt, getRadarTilts } from '@aguacerowx/javascript-sdk';
7
+ import { nexradLevel3S3VelocityProductForSiteTilt } from './nexradLevel3Products.js';
8
+
9
+ export type MapboxRadarFrameUploadOptions = {
10
+ geometryLayoutKey?: string;
11
+ geometryCacheKeysRayBoundaries?: boolean;
12
+ };
13
+
14
+ const NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST = [
15
+ 'N0K',
16
+ 'NAK',
17
+ 'N1K',
18
+ 'NBK',
19
+ 'N2K',
20
+ 'N3K',
21
+ ];
22
+
23
+ const NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST = [
24
+ 'N0H',
25
+ 'NAH',
26
+ 'N1H',
27
+ 'NBH',
28
+ 'N2H',
29
+ 'N3H',
30
+ ];
31
+
32
+ const NEXRAD_LEVEL3_MANIFEST_TILT_COUNT = NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST.length;
33
+
34
+ function nearestTiltInSortedList(tilts: number[], target: number): number {
35
+ if (!tilts.length) return target;
36
+ let best = tilts[0]!;
37
+ let bestD = Math.abs(best - target);
38
+ for (let i = 1; i < tilts.length; i++) {
39
+ const t = tilts[i]!;
40
+ const d = Math.abs(t - target);
41
+ if (d < bestD || (d === bestD && t < best)) {
42
+ best = t;
43
+ bestD = d;
44
+ }
45
+ }
46
+ return best;
47
+ }
48
+
49
+ function getNexradLevel3RadarTilts(siteId: string, radarVariable: string): number[] {
50
+ return getRadarTilts(siteId, radarVariable).slice(0, NEXRAD_LEVEL3_MANIFEST_TILT_COUNT);
51
+ }
52
+
53
+ export function nexradLevel3UsesTiltIndexedS3Products(radarVariable: string | undefined): boolean {
54
+ const v = radarVariable || '';
55
+ return v === 'KDP' || v === 'N0H';
56
+ }
57
+
58
+ function clampTiltForLevel3CompositeKey(siteId: string, radarVariable: string, tilt: number): number {
59
+ if (!nexradLevel3UsesTiltIndexedS3Products(radarVariable)) {
60
+ return clampNexradTiltForVariable(siteId, radarVariable, tilt);
61
+ }
62
+ const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
63
+ const c = clampNexradTiltForVariable(siteId, radarVariable, tilt);
64
+ if (!tilts.length) return c;
65
+ return nearestTiltInSortedList(tilts, c);
66
+ }
67
+
68
+ /**
69
+ * Map site tilt to Level-III S3 product mnemonic (N0K, NAK, … / N0H, …) — aguacero-frontend parity.
70
+ */
71
+ export function nexradLevel3S3ProductForSiteTilt(
72
+ siteId: string,
73
+ radarVariable: 'KDP' | 'N0H',
74
+ tilt: number,
75
+ ): string {
76
+ const products =
77
+ radarVariable === 'KDP' ? NEXRAD_LEVEL3_KDP_PRODUCTS_FOR_MANIFEST : NEXRAD_LEVEL3_DHC_PRODUCTS_FOR_MANIFEST;
78
+ const tilts = getNexradLevel3RadarTilts(siteId, radarVariable);
79
+ if (!tilts.length) {
80
+ return products[0]!;
81
+ }
82
+ const clamped = clampTiltForLevel3CompositeKey(siteId, radarVariable, tilt);
83
+ let idx = tilts.indexOf(clamped);
84
+ if (idx === -1) {
85
+ idx = tilts.reduce(
86
+ (bestI, t, i) => (Math.abs(t - clamped) < Math.abs(tilts[bestI]! - clamped) ? i : bestI),
87
+ 0,
88
+ );
89
+ }
90
+ idx = Math.min(idx, products.length - 1);
91
+ return products[idx]!;
92
+ }
93
+
94
+ type NexradStateForMapbox = {
95
+ nexradDataSource?: string;
96
+ nexradSite?: string | null;
97
+ nexradProduct?: string | null;
98
+ nexradTilt?: number | null;
99
+ };
100
+
101
+ /**
102
+ * @param state - AguaceroCore state (or state:change payload) with NEXRAD fields
103
+ */
104
+ export function mapboxFrameUploadOptionsForNexradState(
105
+ state: NexradStateForMapbox | null | undefined,
106
+ ): MapboxRadarFrameUploadOptions | undefined {
107
+ if (!state) return { geometryLayoutKey: 'canonical' };
108
+ if (state.nexradDataSource !== 'level3') return { geometryLayoutKey: 'canonical' };
109
+ const site = state.nexradSite;
110
+ const radarVar = (state.nexradProduct || 'REF').toUpperCase();
111
+
112
+ if (nexradLevel3UsesTiltIndexedS3Products(radarVar) && site) {
113
+ const tilt = Number.isFinite(state.nexradTilt) ? Number(state.nexradTilt) : getDefaultRadarTilt(site);
114
+ const kind = radarVar === 'KDP' ? 'KDP' : 'N0H';
115
+ const mnemonic = nexradLevel3S3ProductForSiteTilt(site, kind, tilt);
116
+ return { geometryLayoutKey: `${site}|${radarVar}|${mnemonic}` };
117
+ }
118
+
119
+ if (radarVar === 'VEL' && site) {
120
+ const tilt = Number.isFinite(state.nexradTilt) ? Number(state.nexradTilt) : getDefaultRadarTilt(site);
121
+ const mnemonic = nexradLevel3S3VelocityProductForSiteTilt(site, tilt);
122
+ return { geometryLayoutKey: `${site}|${radarVar}|${mnemonic}` };
123
+ }
124
+
125
+ return { geometryLayoutKey: 'canonical' };
126
+ }