@aguacerowx/react-native 0.0.0

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.
Files changed (87) hide show
  1. package/README.md +16 -0
  2. package/android/build/.transforms/78b892a9dae44f36e51ff0649e9c6e36/results.bin +1 -0
  3. package/android/build/.transforms/78b892a9dae44f36e51ff0649e9c6e36/transformed/classes/classes_dex/classes.dex +0 -0
  4. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/results.bin +1 -0
  5. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/aguacerowx/reactnative/AguaceroPackage.dex +0 -0
  6. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/aguacerowx/reactnative/BuildConfig.dex +0 -0
  7. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/aguacerowx/reactnative/GridRenderLayer$VertexInfo.dex +0 -0
  8. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/aguacerowx/reactnative/GridRenderLayer.dex +0 -0
  9. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/aguacerowx/reactnative/GridRenderLayerView.dex +0 -0
  10. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/aguacerowx/reactnative/GridRenderManager.dex +0 -0
  11. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/aguacerowx/reactnative/InspectorModule.dex +0 -0
  12. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/aguacerowx/reactnative/ShaderUtils.dex +0 -0
  13. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/com/aguacerowx/reactnative/WeatherFrameProcessorModule.dex +0 -0
  14. package/android/build/.transforms/f4de14556a82e99f0d27ddcab762b219/transformed/bundleLibRuntimeToDirDebug/desugar_graph.bin +0 -0
  15. package/android/build/generated/source/buildConfig/debug/com/aguacerowx/reactnative/BuildConfig.java +10 -0
  16. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/AndroidManifest.xml +8 -0
  17. package/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json +18 -0
  18. package/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties +6 -0
  19. package/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json +1 -0
  20. package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
  21. package/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar +0 -0
  22. package/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt +4 -0
  23. package/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/raw_debug_fragment_shader.glsl.flat +0 -0
  24. package/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/raw_debug_vertex_shader.glsl.flat +0 -0
  25. package/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/raw_fragment_shader.glsl.flat +0 -0
  26. package/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/raw_vertex_shader.glsl.flat +0 -0
  27. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +5 -0
  28. package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +2 -0
  29. package/android/build/intermediates/incremental/mergeDebugAssets/merger.xml +2 -0
  30. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +2 -0
  31. package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +2 -0
  32. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/aguacerowx/reactnative/AguaceroPackage.class +0 -0
  33. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/aguacerowx/reactnative/BuildConfig.class +0 -0
  34. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/aguacerowx/reactnative/GridRenderLayer$VertexInfo.class +0 -0
  35. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/aguacerowx/reactnative/GridRenderLayer.class +0 -0
  36. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/aguacerowx/reactnative/GridRenderLayerView.class +0 -0
  37. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/aguacerowx/reactnative/GridRenderManager.class +0 -0
  38. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/aguacerowx/reactnative/InspectorModule.class +0 -0
  39. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/aguacerowx/reactnative/ShaderUtils.class +0 -0
  40. package/android/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/com/aguacerowx/reactnative/WeatherFrameProcessorModule.class +0 -0
  41. package/android/build/intermediates/local_only_symbol_list/debug/parseDebugLocalResources/R-def.txt +6 -0
  42. package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +8 -0
  43. package/android/build/intermediates/merged_manifest/debug/processDebugManifest/AndroidManifest.xml +8 -0
  44. package/android/build/intermediates/navigation_json/debug/extractDeepLinksDebug/navigation.json +1 -0
  45. package/android/build/intermediates/nested_resources_validation_report/debug/generateDebugResources/nestedResourcesValidationReport.txt +1 -0
  46. package/android/build/intermediates/packaged_res/debug/packageDebugResources/raw/debug_fragment_shader.glsl +13 -0
  47. package/android/build/intermediates/packaged_res/debug/packageDebugResources/raw/debug_vertex_shader.glsl +13 -0
  48. package/android/build/intermediates/packaged_res/debug/packageDebugResources/raw/fragment_shader.glsl +87 -0
  49. package/android/build/intermediates/packaged_res/debug/packageDebugResources/raw/vertex_shader.glsl +20 -0
  50. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/aguacerowx/reactnative/AguaceroPackage.class +0 -0
  51. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/aguacerowx/reactnative/BuildConfig.class +0 -0
  52. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/aguacerowx/reactnative/GridRenderLayer$VertexInfo.class +0 -0
  53. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/aguacerowx/reactnative/GridRenderLayer.class +0 -0
  54. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/aguacerowx/reactnative/GridRenderLayerView.class +0 -0
  55. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/aguacerowx/reactnative/GridRenderManager.class +0 -0
  56. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/aguacerowx/reactnative/InspectorModule.class +0 -0
  57. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/aguacerowx/reactnative/ShaderUtils.class +0 -0
  58. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/com/aguacerowx/reactnative/WeatherFrameProcessorModule.class +0 -0
  59. package/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar +0 -0
  60. package/android/build/intermediates/symbol_list_with_package_name/debug/generateDebugRFile/package-aware-r.txt +5 -0
  61. package/android/build/outputs/logs/manifest-merger-debug-report.txt +17 -0
  62. package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/AguaceroPackage.class.uniqueId1 +0 -0
  63. package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/GridRenderLayerView.class.uniqueId2 +0 -0
  64. package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/GridRenderManager.class.uniqueId3 +0 -0
  65. package/android/build/tmp/compileDebugJavaWithJavac/compileTransaction/stash-dir/InspectorModule.class.uniqueId0 +0 -0
  66. package/android/build/tmp/compileDebugJavaWithJavac/previous-compilation-data.bin +0 -0
  67. package/android/build.gradle +47 -0
  68. package/android/src/main/AndroidManifest.xml +7 -0
  69. package/android/src/main/java/com/aguacerowx/reactnative/AguaceroPackage.java +34 -0
  70. package/android/src/main/java/com/aguacerowx/reactnative/GridRenderLayer.java +639 -0
  71. package/android/src/main/java/com/aguacerowx/reactnative/GridRenderLayerView.java +287 -0
  72. package/android/src/main/java/com/aguacerowx/reactnative/GridRenderManager.java +111 -0
  73. package/android/src/main/java/com/aguacerowx/reactnative/InspectorModule.java +64 -0
  74. package/android/src/main/java/com/aguacerowx/reactnative/ShaderUtils.java +107 -0
  75. package/android/src/main/java/com/aguacerowx/reactnative/WeatherFrameProcessorModule.java +145 -0
  76. package/android/src/main/res/raw/debug_fragment_shader.glsl +13 -0
  77. package/android/src/main/res/raw/debug_vertex_shader.glsl +13 -0
  78. package/android/src/main/res/raw/fragment_shader.glsl +87 -0
  79. package/android/src/main/res/raw/vertex_shader.glsl +20 -0
  80. package/index.js +2 -0
  81. package/package.json +35 -0
  82. package/src/AguaceroContext.js +4 -0
  83. package/src/GridRenderLayer.js +121 -0
  84. package/src/MapManager.js +158 -0
  85. package/src/MapRegistry.js +35 -0
  86. package/src/StyleApplicator.js +241 -0
  87. package/src/WeatherLayerManager.js +754 -0
@@ -0,0 +1,754 @@
1
+ // packages/react-native/src/WeatherLayerManager.js
2
+
3
+ import React, { useState, useRef, useContext, useEffect, forwardRef, useImperativeHandle, useMemo } from 'react';
4
+ import { AguaceroCore, getUnitConversionFunction } from '@aguacerowx/javascript-sdk';
5
+ import { AguaceroContext } from './AguaceroContext';
6
+ import { GridRenderLayer } from './GridRenderLayer';
7
+ import { fromByteArray } from 'base64-js';
8
+ import { NativeModules } from 'react-native';
9
+ import { mapRegistry } from './MapRegistry';
10
+ const { WeatherFrameProcessorModule, InspectorModule } = NativeModules;
11
+
12
+ /**
13
+ * A helper function to generate the raw RGBA byte buffer for the colormap texture.
14
+ */
15
+ const _generateColormapBytes = (colormap) => {
16
+ const width = 256;
17
+ const data = new Uint8Array(width * 4);
18
+ const stops = colormap.reduce((acc, _, i) => (i % 2 === 0 ? [...acc, { value: colormap[i], color: colormap[i + 1] }] : acc), []);
19
+
20
+ if (stops.length === 0) return data;
21
+
22
+ const minVal = stops[0].value;
23
+ const maxVal = stops[stops.length - 1].value;
24
+
25
+ const hexToRgb = (hex) => {
26
+ const r = parseInt(hex.slice(1, 3), 16);
27
+ const g = parseInt(hex.slice(3, 5), 16);
28
+ const b = parseInt(hex.slice(5, 7), 16);
29
+ return [r, g, b];
30
+ };
31
+
32
+ for (let i = 0; i < width; i++) {
33
+ const val = minVal + (i / (width - 1)) * (maxVal - minVal);
34
+ let lower = stops[0];
35
+ let upper = stops[stops.length - 1];
36
+ for (let j = 0; j < stops.length - 1; j++) {
37
+ if (val >= stops[j].value && val <= stops[j + 1].value) {
38
+ lower = stops[j];
39
+ upper = stops[j + 1];
40
+ break;
41
+ }
42
+ }
43
+ const t = (val - lower.value) / (upper.value - lower.value || 1);
44
+ const lowerRgb = hexToRgb(lower.color);
45
+ const upperRgb = hexToRgb(upper.color);
46
+ const rgb = lowerRgb.map((c, idx) => c * (1 - t) + upperRgb[idx] * t);
47
+
48
+ const offset = i * 4;
49
+ data[offset + 0] = Math.round(rgb[0]);
50
+ data[offset + 1] = Math.round(rgb[1]);
51
+ data[offset + 2] = Math.round(rgb[2]);
52
+ data[offset + 3] = 255;
53
+ }
54
+ return data;
55
+ };
56
+
57
+ AguaceroCore.prototype.setMapCenter = function(center) {
58
+ this.emit('map:move', center);
59
+ };
60
+
61
+ export const WeatherLayerManager = forwardRef((props, ref) => {
62
+ const { inspectorEnabled, onInspect, apiKey, customColormaps, ...restProps } = props;
63
+ const context = useContext(AguaceroContext);
64
+
65
+ // Create the core here instead of getting it from context
66
+ const core = useMemo(() => new AguaceroCore({
67
+ apiKey: apiKey,
68
+ customColormaps: customColormaps
69
+ }), [apiKey]);
70
+
71
+ const gridLayerRef = useRef(null);
72
+ const currentGridDataRef = useRef(null);
73
+
74
+ // Cache for preloaded grid data - stores the processed data ready for GPU upload
75
+ const preloadedDataCache = useRef(new Map());
76
+
77
+ // Track what we're currently preloading to avoid duplicates
78
+ const preloadingSet = useRef(new Set());
79
+
80
+ // Store geometry and colormap that don't change with forecast hour
81
+ const cachedGeometry = useRef(null);
82
+ const cachedColormap = useRef(null);
83
+ const cachedDataRange = useRef([0, 1]);
84
+
85
+ // Track if we've done the initial load
86
+ const hasInitialLoad = useRef(false);
87
+
88
+ // Track the last state we processed to avoid redundant updates
89
+ const lastProcessedState = useRef(null);
90
+ const previousStateRef = useRef(null);
91
+
92
+ const [renderProps, setRenderProps] = useState({
93
+ opacity: 1,
94
+ dataRange: [0, 1]
95
+ });
96
+
97
+ useImperativeHandle(ref, () => ({
98
+ play: () => core.play(),
99
+ pause: () => core.pause(),
100
+ togglePlay: () => core.togglePlay(),
101
+ step: (direction) => core.step(direction),
102
+ setPlaybackSpeed: (speed) => core.setPlaybackSpeed(speed),
103
+ setOpacity: (opacity) => core.setOpacity(opacity),
104
+ setUnits: (units) => core.setUnits(units),
105
+ switchMode: (options) => core.switchMode(options),
106
+ getAvailableVariables: (model) => core.getAvailableVariables(model),
107
+ getVariableDisplayName: (code) => core.getVariableDisplayName(code),
108
+ setRun: (runString) => core.setState({ run: runString.split(':')[1] }),
109
+ setState: (newState) => core.setState(newState),
110
+ setMRMSTimestamp: (timestamp) => core.setMRMSTimestamp(timestamp),
111
+ setSmoothing: (enabled) => {
112
+ if (gridLayerRef.current) {
113
+ gridLayerRef.current.setSmoothing(enabled);
114
+ }
115
+ },
116
+ }));
117
+
118
+ const preloadAllFramesToDisk = (state) => {
119
+ const { isMRMS, model, date, run, variable, units, availableHours, availableTimestamps } = state;
120
+
121
+ const allFrames = isMRMS ? availableTimestamps : availableHours;
122
+ if (!allFrames || allFrames.length === 0) {
123
+ console.warn('🟡 [Preload To Disk] No frames available to download.');
124
+ return;
125
+ }
126
+
127
+ const { corners, gridDef } = core._getGridCornersAndDef(isMRMS ? 'mrms' : model);
128
+ const { nx, ny } = gridDef.grid_params;
129
+
130
+ for (const frame of allFrames) {
131
+ const cacheKey = isMRMS ? `mrms-${frame}-${variable}-${units}` : `${model}-${date}-${run}-${frame}-${variable}-${units}`;
132
+ if (preloadedDataCache.current.has(cacheKey)) {
133
+ continue; // Skip if already cached
134
+ }
135
+
136
+ let resourcePath;
137
+ if (isMRMS) {
138
+ const frameDate = new Date(frame * 1000);
139
+ const y = frameDate.getUTCFullYear();
140
+ const m = (frameDate.getUTCMonth() + 1).toString().padStart(2, '0');
141
+ const d = frameDate.getUTCDate().toString().padStart(2, '0');
142
+ resourcePath = `/grids/mrms/${y}${m}${d}/${frame}/0/${variable}/0`;
143
+ } else {
144
+ resourcePath = `/grids/${model}/${date}/${run}/${frame}/${variable}/0`;
145
+ }
146
+
147
+ const url = `${core.baseGridUrl}${resourcePath}?apiKey=${core.apiKey}`;
148
+ const options = { url, apiKey: core.apiKey, bundleId: core.bundleId };
149
+
150
+ // Assumes your native module has a method 'processFrameAndSave' that saves to a file
151
+ // and returns a { filePath, scale, offset, missing } object in the callback.
152
+ WeatherFrameProcessorModule.processFrame(options, (error, result) => {
153
+ if (error || !result.filePath) {
154
+ console.error(`❌ [Native Save Error] for frame ${frame}:`, error || "Result has no filePath");
155
+ return;
156
+ }
157
+
158
+ // Cache is now extremely lightweight, just storing metadata and a path.
159
+ preloadedDataCache.current.set(cacheKey, {
160
+ filePath: result.filePath,
161
+ nx, ny,
162
+ scale: result.scale, offset: result.offset, missing: result.missing,
163
+ corners, gridDef
164
+ });
165
+ });
166
+ }
167
+ };
168
+
169
+ /**
170
+ * FINAL VERSION: Updates the GPU by reading from the cache and calling the correct
171
+ * native render function (either from a file path or from a Base64 string).
172
+ */
173
+ const updateGPUWithCachedData = (state) => {
174
+ const { model, date, run, forecastHour, variable, units, isMRMS, mrmsTimestamp } = state;
175
+
176
+ const cacheKey = isMRMS
177
+ ? `mrms-${mrmsTimestamp}-${variable}-${units}`
178
+ : `${model}-${date}-${run}-${forecastHour}-${variable}-${units}`;
179
+
180
+ const cachedData = preloadedDataCache.current.get(cacheKey);
181
+
182
+ if (!cachedData) {
183
+ const timeKey = isMRMS ? `timestamp ${mrmsTimestamp}` : `hour +${forecastHour}`;
184
+ console.warn(`⚠️ [GPU Update] No cached data for ${timeKey}. Key not found: ${cacheKey}`);
185
+ return false;
186
+ }
187
+
188
+ if (!gridLayerRef.current) {
189
+ console.error(`❌ [GPU Update] GridLayer ref not available`);
190
+ return false;
191
+ }
192
+
193
+ if (!cachedGeometry.current || cachedGeometry.current.model !== (isMRMS ? 'mrms' : model) || cachedGeometry.current.variable !== variable) {
194
+ gridLayerRef.current.updateGeometry(cachedData.corners, cachedData.gridDef);
195
+ cachedGeometry.current = { model: (isMRMS ? 'mrms' : model), variable };
196
+ }
197
+
198
+ const colormapKey = `${variable}-${units}`;
199
+ if (!cachedColormap.current || cachedColormap.current.key !== colormapKey) {
200
+ const { colormap, baseUnit } = core._getColormapForVariable(variable);
201
+ const toUnit = core._getTargetUnit(baseUnit, units);
202
+ const finalColormap = core._convertColormapUnits(colormap, baseUnit, toUnit);
203
+ const dataRange = [finalColormap[0], finalColormap[finalColormap.length - 2]];
204
+ const colormapBytes = _generateColormapBytes(finalColormap);
205
+ const colormapAsBase64 = fromByteArray(colormapBytes);
206
+
207
+ gridLayerRef.current.updateColormapTexture(colormapAsBase64);
208
+ cachedColormap.current = { key: colormapKey };
209
+ cachedDataRange.current = dataRange;
210
+
211
+ setRenderProps(prev => ({ ...prev, dataRange }));
212
+ }
213
+
214
+ // UPDATE GPU
215
+ if (cachedData.filePath) {
216
+ gridLayerRef.current.updateDataTextureFromFile(
217
+ cachedData.filePath,
218
+ cachedData.nx, cachedData.ny,
219
+ cachedData.scale, cachedData.offset, cachedData.missing
220
+ );
221
+ } else if (cachedData.dataAsBase64) {
222
+ gridLayerRef.current.updateDataTexture(
223
+ cachedData.dataAsBase64,
224
+ cachedData.nx, cachedData.ny,
225
+ cachedData.scale, cachedData.offset, cachedData.missing
226
+ );
227
+
228
+ // ADD THIS: Update the inspector cache when using dataAsBase64
229
+ const binaryString = atob(cachedData.dataAsBase64);
230
+ const uint8Array = new Uint8Array(binaryString.length);
231
+
232
+ for (let i = 0; i < binaryString.length; i++) {
233
+ uint8Array[i] = binaryString.charCodeAt(i);
234
+ }
235
+
236
+ currentGridDataRef.current = {
237
+ data: uint8Array,
238
+ nx: cachedData.nx,
239
+ ny: cachedData.ny,
240
+ scale: cachedData.scale,
241
+ offset: cachedData.offset,
242
+ missing: cachedData.missing,
243
+ gridDef: cachedData.gridDef,
244
+ variable: variable,
245
+ units: units
246
+ };
247
+ } else {
248
+ console.error(`❌ [GPU Update] Cached data for key ${cacheKey} has no filePath or dataAsBase64.`);
249
+ return false;
250
+ }
251
+
252
+ // ADD THIS: Update inspector parameters for file-based data too
253
+ if (gridLayerRef.current && gridLayerRef.current.updateDataParameters) {
254
+ gridLayerRef.current.updateDataParameters(cachedData.scale, cachedData.offset, cachedData.missing);
255
+ }
256
+
257
+ return true;
258
+ };
259
+
260
+ const handleStateChangeRef = useRef(null);
261
+ const debounceTimeoutRef = useRef(null);
262
+
263
+ useEffect(() => {
264
+ if (core && props.customColormaps) {
265
+ core.customColormaps = props.customColormaps;
266
+ // Trigger a re-render if we already have data loaded
267
+ if (hasInitialLoad.current) {
268
+ core._emitStateChange();
269
+ }
270
+ }
271
+ }, [core, props.customColormaps]);
272
+
273
+ const getValueAtPoint = async (lng, lat) => {
274
+ if (!core) {
275
+ console.warn('🔍 [Inspector] Core not available');
276
+ return null;
277
+ }
278
+
279
+ try {
280
+ const gridIndices = core._getGridIndexFromLngLat(lng, lat);
281
+ if (!gridIndices) return null;
282
+
283
+ const { i, j } = gridIndices;
284
+
285
+ // The native module uses the cached scale/offset which we update via updateDataParameters
286
+ const value = await InspectorModule.getValueAtGridIndex(i, j);
287
+
288
+ if (value === null) {
289
+ return null;
290
+ }
291
+
292
+ const { colormap, baseUnit } = core._getColormapForVariable(core.state.variable);
293
+ const displayUnit = core._getTargetUnit(baseUnit, core.state.units);
294
+
295
+ // Get the converted colormap to check min threshold
296
+ const finalColormap = core._convertColormapUnits(colormap, baseUnit, displayUnit);
297
+ const minThreshold = finalColormap[0]; // First value in colormap is the minimum
298
+
299
+ // Filter out values below the minimum threshold (matching shader behavior)
300
+ if (value < minThreshold) {
301
+ return null;
302
+ }
303
+
304
+ // Also check if value is NaN or effectively missing
305
+ if (!isFinite(value)) {
306
+ return null;
307
+ }
308
+
309
+ return {
310
+ value: value,
311
+ unit: displayUnit,
312
+ variable: {
313
+ code: core.state.variable,
314
+ name: core.getVariableDisplayName(core.state.variable)
315
+ },
316
+ lngLat: { lng, lat }
317
+ };
318
+ } catch (error) {
319
+ console.error('🔍 [Inspector] Error:', error);
320
+ return null;
321
+ }
322
+ };
323
+
324
+ useEffect(() => {
325
+ if (!core) {
326
+ console.warn('⚠️ [useEffect] Core is not available yet');
327
+ return;
328
+ }
329
+
330
+ const handleStateChange = async (newState) => {
331
+ if (!previousStateRef.current) {
332
+ previousStateRef.current = core.state;
333
+ }
334
+
335
+ const stateKey = `${newState.model}-${newState.variable}-${newState.date}-${newState.run}-${newState.forecastHour}-${newState.units}-${newState.mrmsTimestamp}`;
336
+
337
+ const isOpacityOnlyChange =
338
+ hasInitialLoad.current &&
339
+ newState.opacity !== renderProps.opacity &&
340
+ newState.variable === previousStateRef.current?.variable &&
341
+ newState.forecastHour === previousStateRef.current?.forecastHour &&
342
+ newState.mrmsTimestamp === previousStateRef.current?.mrmsTimestamp &&
343
+ newState.model === previousStateRef.current?.model &&
344
+ newState.units === previousStateRef.current?.units;
345
+
346
+ if (!isOpacityOnlyChange && lastProcessedState.current === stateKey) {
347
+ return;
348
+ }
349
+
350
+ if (!isOpacityOnlyChange) {
351
+ lastProcessedState.current = stateKey;
352
+ }
353
+
354
+ props.onStateChange?.(newState);
355
+
356
+ if (isOpacityOnlyChange) {
357
+ setRenderProps(prev => ({ ...prev, opacity: newState.opacity }));
358
+ previousStateRef.current = newState;
359
+ return;
360
+ }
361
+ // Check if only units changed
362
+ const isUnitsOnlyChange =
363
+ hasInitialLoad.current &&
364
+ newState.model === previousStateRef.current.model &&
365
+ newState.isMRMS === previousStateRef.current.isMRMS &&
366
+ newState.variable === previousStateRef.current.variable &&
367
+ newState.date === previousStateRef.current.date &&
368
+ newState.run === previousStateRef.current.run &&
369
+ newState.forecastHour === previousStateRef.current.forecastHour &&
370
+ newState.mrmsTimestamp === previousStateRef.current.mrmsTimestamp &&
371
+ newState.units !== previousStateRef.current.units;
372
+
373
+ if (isUnitsOnlyChange) {
374
+ const { variable, units, isMRMS, mrmsTimestamp, model, date, run, forecastHour } = newState;
375
+
376
+ const oldCacheKey = isMRMS
377
+ ? `mrms-${mrmsTimestamp}-${variable}-${previousStateRef.current.units}`
378
+ : `${model}-${date}-${run}-${forecastHour}-${variable}-${previousStateRef.current.units}`;
379
+
380
+ const cachedData = preloadedDataCache.current.get(oldCacheKey);
381
+
382
+ if (cachedData && cachedData.originalScale !== undefined && cachedData.originalOffset !== undefined) {
383
+ const { baseUnit } = core._getColormapForVariable(variable);
384
+ const toUnit = core._getTargetUnit(baseUnit, units);
385
+
386
+ let dataScale = cachedData.originalScale;
387
+ let dataOffset = cachedData.originalOffset;
388
+
389
+ if (baseUnit !== toUnit) {
390
+ const conversionFunc = getUnitConversionFunction(baseUnit, toUnit); // ✅ Use SDK function
391
+ if (conversionFunc) {
392
+ const convertedOffset = conversionFunc(dataOffset);
393
+ const convertedOffsetPlusScale = conversionFunc(dataOffset + dataScale);
394
+ dataScale = convertedOffsetPlusScale - convertedOffset;
395
+ dataOffset = convertedOffset;
396
+ }
397
+ }
398
+
399
+ // ONLY update the inspector cache parameters - don't touch GPU or colormap!
400
+ if (gridLayerRef.current && gridLayerRef.current.updateDataParameters) {
401
+ gridLayerRef.current.updateDataParameters(dataScale, dataOffset, cachedData.missing);
402
+ }
403
+
404
+ // Create new cache entry with new units
405
+ const newCacheKey = isMRMS
406
+ ? `mrms-${mrmsTimestamp}-${variable}-${units}`
407
+ : `${model}-${date}-${run}-${forecastHour}-${variable}-${units}`;
408
+
409
+ preloadedDataCache.current.set(newCacheKey, {
410
+ ...cachedData,
411
+ scale: dataScale,
412
+ offset: dataOffset
413
+ });
414
+ }
415
+
416
+ previousStateRef.current = newState;
417
+
418
+ if (newState.opacity !== renderProps.opacity) {
419
+ setRenderProps(prev => ({ ...prev, opacity: newState.opacity }));
420
+ }
421
+
422
+ return;
423
+ }
424
+
425
+ const needsFullLoad =
426
+ !hasInitialLoad.current ||
427
+ newState.model !== previousStateRef.current.model ||
428
+ newState.isMRMS !== previousStateRef.current.isMRMS ||
429
+ newState.variable !== previousStateRef.current.variable ||
430
+ newState.date !== previousStateRef.current.date ||
431
+ newState.run !== previousStateRef.current.run;
432
+ // REMOVED: || newState.units !== previousStateRef.current.units;
433
+
434
+ if (needsFullLoad) {
435
+ preloadedDataCache.current.clear();
436
+ cachedGeometry.current = null;
437
+ cachedColormap.current = null;
438
+ WeatherFrameProcessorModule.cancelAllFrames();
439
+
440
+ if (!newState.variable) {
441
+ previousStateRef.current = newState;
442
+ return;
443
+ }
444
+
445
+ try {
446
+ const { model, date, run, forecastHour, variable, units, isMRMS, mrmsTimestamp } = newState;
447
+
448
+ const gridModel = isMRMS ? 'mrms' : model;
449
+ const { corners, gridDef } = core._getGridCornersAndDef(gridModel);
450
+ const { nx, ny } = gridDef.grid_params;
451
+
452
+ let resourcePath;
453
+ if (isMRMS) {
454
+ if (!mrmsTimestamp) {
455
+ previousStateRef.current = newState;
456
+ return;
457
+ }
458
+ const mrmsDate = new Date(mrmsTimestamp * 1000);
459
+ const y = mrmsDate.getUTCFullYear();
460
+ const m = (mrmsDate.getUTCMonth() + 1).toString().padStart(2, '0');
461
+ const d = mrmsDate.getUTCDate().toString().padStart(2, '0');
462
+ resourcePath = `/grids/mrms/${y}${m}${d}/${mrmsTimestamp}/0/${variable}/0`;
463
+ } else {
464
+ resourcePath = `/grids/${model}/${date}/${run}/${forecastHour}/${variable}/0`;
465
+ }
466
+
467
+ const url = `${core.baseGridUrl}${resourcePath}?apiKey=${core.apiKey}`;
468
+ const options = { url, apiKey: core.apiKey, bundleId: core.bundleId };
469
+
470
+ const result = await new Promise((resolve, reject) => {
471
+ WeatherFrameProcessorModule.processFrame(options, (error, res) => {
472
+ if (error) reject(new Error(error));
473
+ else resolve(res);
474
+ });
475
+ });
476
+
477
+ if (result && gridLayerRef.current) {
478
+ gridLayerRef.current.updateGeometry(corners, gridDef);
479
+ cachedGeometry.current = { model: gridModel, variable };
480
+
481
+ const { colormap, baseUnit } = core._getColormapForVariable(variable);
482
+ const toUnit = core._getTargetUnit(baseUnit, units);
483
+
484
+ let dataScale = result.scale;
485
+ let dataOffset = result.offset;
486
+
487
+ // Store original scale/offset for unit conversion later
488
+ const originalScale = result.scale;
489
+ const originalOffset = result.offset;
490
+
491
+ if (baseUnit !== toUnit) {
492
+ const conversionFunc = getUnitConversionFunction(baseUnit, toUnit); // ✅ Use SDK function
493
+ if (conversionFunc) {
494
+ const convertedOffset = conversionFunc(dataOffset);
495
+ const convertedOffsetPlusScale = conversionFunc(dataOffset + dataScale);
496
+ dataScale = convertedOffsetPlusScale - convertedOffset;
497
+ dataOffset = convertedOffset;
498
+ }
499
+ }
500
+
501
+ if (result.dataAsBase64) {
502
+ const binaryString = atob(result.dataAsBase64);
503
+ const uint8Array = new Uint8Array(binaryString.length);
504
+
505
+ for (let i = 0; i < binaryString.length; i++) {
506
+ uint8Array[i] = binaryString.charCodeAt(i);
507
+ }
508
+
509
+ currentGridDataRef.current = {
510
+ data: uint8Array,
511
+ nx, ny,
512
+ scale: dataScale,
513
+ offset: dataOffset,
514
+ missing: result.missing,
515
+ gridDef: gridDef,
516
+ variable: variable,
517
+ units: units
518
+ };
519
+ }
520
+
521
+ // Now upload to GPU
522
+ if (result.filePath) {
523
+ gridLayerRef.current.updateDataTextureFromFile(
524
+ result.filePath, nx, ny,
525
+ dataScale, dataOffset, result.missing
526
+ );
527
+ } else if (result.dataAsBase64) {
528
+ gridLayerRef.current.updateDataTexture(
529
+ result.dataAsBase64, nx, ny,
530
+ dataScale, dataOffset, result.missing
531
+ );
532
+ }
533
+
534
+ const finalColormap = core._convertColormapUnits(colormap, baseUnit, toUnit);
535
+ const dataRange = [finalColormap[0], finalColormap[finalColormap.length - 2]];
536
+ const colormapBytes = _generateColormapBytes(finalColormap);
537
+ const colormapAsBase64 = fromByteArray(colormapBytes);
538
+ gridLayerRef.current.updateColormapTexture(colormapAsBase64);
539
+ cachedColormap.current = { key: `${variable}-${units}` };
540
+ cachedDataRange.current = dataRange;
541
+
542
+ setRenderProps({ opacity: newState.opacity, dataRange: dataRange });
543
+
544
+ hasInitialLoad.current = true;
545
+
546
+ const cacheKey = isMRMS
547
+ ? `mrms-${mrmsTimestamp}-${variable}-${units}`
548
+ : `${model}-${date}-${run}-${forecastHour}-${variable}-${units}`;
549
+
550
+ const dataToCache = {
551
+ nx, ny,
552
+ scale: dataScale,
553
+ offset: dataOffset,
554
+ originalScale: originalScale, // Store original for unit conversion
555
+ originalOffset: originalOffset, // Store original for unit conversion
556
+ missing: result.missing,
557
+ corners, gridDef
558
+ };
559
+ if (result.filePath) dataToCache.filePath = result.filePath;
560
+ if (result.dataAsBase64) dataToCache.dataAsBase64 = result.dataAsBase64;
561
+
562
+ preloadedDataCache.current.set(cacheKey, dataToCache);
563
+
564
+ preloadAllFramesToDisk(newState);
565
+ }
566
+ } catch (error) {
567
+ console.error("❌ [State Change] Failed to load initial frame via native module:", error);
568
+ }
569
+
570
+ } else if (newState.forecastHour !== previousStateRef.current.forecastHour || (newState.isMRMS && newState.mrmsTimestamp !== previousStateRef.current.mrmsTimestamp)) {
571
+ updateGPUWithCachedData(newState);
572
+
573
+ if (newState.opacity !== renderProps.opacity) {
574
+ setRenderProps(prev => ({ ...prev, opacity: newState.opacity }));
575
+ }
576
+ }
577
+ previousStateRef.current = newState;
578
+ };
579
+
580
+ handleStateChangeRef.current = handleStateChange;
581
+
582
+ const stableHandler = (newState) => {
583
+ const isOpacityOnlyChange =
584
+ previousStateRef.current &&
585
+ newState.opacity !== previousStateRef.current.opacity &&
586
+ newState.variable === previousStateRef.current.variable &&
587
+ newState.forecastHour === previousStateRef.current.forecastHour &&
588
+ newState.mrmsTimestamp === previousStateRef.current.mrmsTimestamp &&
589
+ newState.model === previousStateRef.current.model &&
590
+ newState.units === previousStateRef.current.units &&
591
+ newState.date === previousStateRef.current.date &&
592
+ newState.run === previousStateRef.current.run;
593
+
594
+ if (isOpacityOnlyChange) {
595
+ if (handleStateChangeRef.current) {
596
+ handleStateChangeRef.current(newState);
597
+ }
598
+ return;
599
+ }
600
+
601
+ if (debounceTimeoutRef.current) {
602
+ clearTimeout(debounceTimeoutRef.current);
603
+ }
604
+
605
+ debounceTimeoutRef.current = setTimeout(() => {
606
+ if (handleStateChangeRef.current) {
607
+ handleStateChangeRef.current(newState);
608
+ }
609
+ debounceTimeoutRef.current = null;
610
+ }, 50);
611
+ };
612
+
613
+ core.on('state:change', stableHandler);
614
+
615
+ return () => {
616
+ core.off('state:change', stableHandler);
617
+ if (debounceTimeoutRef.current) {
618
+ clearTimeout(debounceTimeoutRef.current);
619
+ }
620
+ };
621
+ }, [core]);
622
+
623
+ useEffect(() => {
624
+ return () => {
625
+ preloadedDataCache.current.clear();
626
+ hasInitialLoad.current = false;
627
+ lastProcessedState.current = null;
628
+ };
629
+ }, []);
630
+
631
+ const lastInspectorUpdateRef = useRef(0);
632
+ const INSPECTOR_THROTTLE_MS = 50;
633
+
634
+ useEffect(() => {
635
+ if (!core || !inspectorEnabled) {
636
+ return;
637
+ }
638
+
639
+ const handleMapMove = async (center) => {
640
+ if (!center || !Array.isArray(center) || center.length !== 2) {
641
+ return;
642
+ }
643
+
644
+ // Throttle updates
645
+ const now = Date.now();
646
+ if (now - lastInspectorUpdateRef.current < INSPECTOR_THROTTLE_MS) {
647
+ return;
648
+ }
649
+ lastInspectorUpdateRef.current = now;
650
+
651
+ const [longitude, latitude] = center;
652
+
653
+ const payload = await getValueAtPoint(longitude, latitude);
654
+ onInspect?.(payload);
655
+ };
656
+
657
+ core.on('map:move', handleMapMove);
658
+
659
+ if (context && context.getCenter) {
660
+ const center = context.getCenter();
661
+ if (center) {
662
+ handleMapMove(center);
663
+ }
664
+ }
665
+
666
+ return () => {
667
+ core.off('map:move', handleMapMove);
668
+ };
669
+ }, [inspectorEnabled, onInspect, core, context]);
670
+
671
+ // Add this new useEffect after the existing inspector useEffect
672
+ useEffect(() => {
673
+ // Trigger re-inspection when state changes (variable, model, forecast hour, etc.)
674
+ if (!core || !inspectorEnabled) {
675
+ return;
676
+ }
677
+
678
+ const triggerReinspection = () => {
679
+ const mapRef = mapRegistry.getMap();
680
+ const center = mapRef?._currentCenter;
681
+
682
+ if (center && Array.isArray(center) && center.length === 2) {
683
+ const [longitude, latitude] = center;
684
+
685
+ getValueAtPoint(longitude, latitude).then(payload => {
686
+ onInspect?.(payload);
687
+ });
688
+ }
689
+ };
690
+
691
+ // Small delay to ensure data is loaded before re-inspecting
692
+ const timer = setTimeout(triggerReinspection, 100);
693
+
694
+ return () => clearTimeout(timer);
695
+ }, [
696
+ core?.state?.variable,
697
+ core?.state?.model,
698
+ core?.state?.forecastHour,
699
+ core?.state?.mrmsTimestamp,
700
+ core?.state?.units,
701
+ inspectorEnabled,
702
+ onInspect
703
+ ]);
704
+
705
+ useEffect(() => {
706
+ if (!core) {
707
+ return;
708
+ }
709
+
710
+ const handleCameraChange = (center) => {
711
+ if (core && center) {
712
+ core.setMapCenter(center);
713
+ }
714
+ };
715
+
716
+ // Register with the global registry
717
+ mapRegistry.addCameraListener(handleCameraChange);
718
+
719
+ // Try to get initial center
720
+ const mapRef = mapRegistry.getMap();
721
+ if (mapRef?._currentCenter) {
722
+ handleCameraChange(mapRef._currentCenter);
723
+ }
724
+
725
+ return () => {
726
+ mapRegistry.removeCameraListener(handleCameraChange);
727
+ };
728
+ }, [core]);
729
+
730
+ useEffect(() => {
731
+ core.initialize();
732
+ return () => {
733
+ core.destroy();
734
+ };
735
+ }, [core]);
736
+
737
+ return (
738
+ <GridRenderLayer
739
+ ref={gridLayerRef}
740
+ opacity={renderProps.opacity}
741
+ dataRange={renderProps.dataRange}
742
+ belowID="AML_-_terrain"
743
+ />
744
+ );
745
+ });
746
+
747
+ WeatherLayerManager.getAvailableVariables = (options) => {
748
+ if (!options || !options.apiKey) {
749
+ console.error("API key must be provided to get available variables.");
750
+ return [];
751
+ }
752
+ const core = new AguaceroCore({ apiKey: options.apiKey });
753
+ return core.getAvailableVariables('mrms');
754
+ };