react-native-mapsforge-vtm-ext-grib 0.2.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/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/android/CMakeLists.txt +13 -0
  4. package/android/build.gradle +108 -0
  5. package/android/generated/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/NativeWeatherOverlaySpec.java +95 -0
  6. package/android/generated/jni/CMakeLists.txt +28 -0
  7. package/android/generated/jni/RNMapsforgeVtmExtGribSpec-generated.cpp +62 -0
  8. package/android/generated/jni/RNMapsforgeVtmExtGribSpec.h +31 -0
  9. package/android/generated/jni/react/renderer/components/RNMapsforgeVtmExtGribSpec/RNMapsforgeVtmExtGribSpecJSI.h +521 -0
  10. package/android/gradle.properties +5 -0
  11. package/android/src/main/AndroidManifest.xml +2 -0
  12. package/android/src/main/AndroidManifestNew.xml +2 -0
  13. package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/ExtGribPackage.java +62 -0
  14. package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/modules/WeatherOverlay.java +343 -0
  15. package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/tiles/ColorRamp.java +165 -0
  16. package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/tiles/WeatherGridData.java +103 -0
  17. package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/tiles/WeatherTileDataSource.java +147 -0
  18. package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/tiles/WeatherTileSource.java +80 -0
  19. package/lib/commonjs/NativeModules/NativeWeatherOverlay.js +9 -0
  20. package/lib/commonjs/NativeModules/NativeWeatherOverlay.js.map +1 -0
  21. package/lib/commonjs/components/WeatherOverlay.js +180 -0
  22. package/lib/commonjs/components/WeatherOverlay.js.map +1 -0
  23. package/lib/commonjs/index.js +21 -0
  24. package/lib/commonjs/index.js.map +1 -0
  25. package/lib/commonjs/package.json +1 -0
  26. package/lib/commonjs/reanimated/index.js +13 -0
  27. package/lib/commonjs/reanimated/index.js.map +1 -0
  28. package/lib/commonjs/reanimated/useWeatherAnimation.js +86 -0
  29. package/lib/commonjs/reanimated/useWeatherAnimation.js.map +1 -0
  30. package/lib/commonjs/reportNativeError.js +14 -0
  31. package/lib/commonjs/reportNativeError.js.map +1 -0
  32. package/lib/commonjs/types.js +2 -0
  33. package/lib/commonjs/types.js.map +1 -0
  34. package/lib/module/NativeModules/NativeWeatherOverlay.js +5 -0
  35. package/lib/module/NativeModules/NativeWeatherOverlay.js.map +1 -0
  36. package/lib/module/components/WeatherOverlay.js +174 -0
  37. package/lib/module/components/WeatherOverlay.js.map +1 -0
  38. package/lib/module/index.js +16 -0
  39. package/lib/module/index.js.map +1 -0
  40. package/lib/module/package.json +1 -0
  41. package/lib/module/reanimated/index.js +4 -0
  42. package/lib/module/reanimated/index.js.map +1 -0
  43. package/lib/module/reanimated/useWeatherAnimation.js +81 -0
  44. package/lib/module/reanimated/useWeatherAnimation.js.map +1 -0
  45. package/lib/module/reportNativeError.js +10 -0
  46. package/lib/module/reportNativeError.js.map +1 -0
  47. package/lib/module/types.js +2 -0
  48. package/lib/module/types.js.map +1 -0
  49. package/lib/typescript/commonjs/package.json +1 -0
  50. package/lib/typescript/commonjs/src/NativeModules/NativeWeatherOverlay.d.ts +63 -0
  51. package/lib/typescript/commonjs/src/NativeModules/NativeWeatherOverlay.d.ts.map +1 -0
  52. package/lib/typescript/commonjs/src/components/WeatherOverlay.d.ts +10 -0
  53. package/lib/typescript/commonjs/src/components/WeatherOverlay.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/src/index.d.ts +11 -0
  55. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/src/reanimated/index.d.ts +3 -0
  57. package/lib/typescript/commonjs/src/reanimated/index.d.ts.map +1 -0
  58. package/lib/typescript/commonjs/src/reanimated/useWeatherAnimation.d.ts +44 -0
  59. package/lib/typescript/commonjs/src/reanimated/useWeatherAnimation.d.ts.map +1 -0
  60. package/lib/typescript/commonjs/src/reportNativeError.d.ts +3 -0
  61. package/lib/typescript/commonjs/src/reportNativeError.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/src/types.d.ts +51 -0
  63. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  64. package/lib/typescript/module/package.json +1 -0
  65. package/lib/typescript/module/src/NativeModules/NativeWeatherOverlay.d.ts +63 -0
  66. package/lib/typescript/module/src/NativeModules/NativeWeatherOverlay.d.ts.map +1 -0
  67. package/lib/typescript/module/src/components/WeatherOverlay.d.ts +10 -0
  68. package/lib/typescript/module/src/components/WeatherOverlay.d.ts.map +1 -0
  69. package/lib/typescript/module/src/index.d.ts +11 -0
  70. package/lib/typescript/module/src/index.d.ts.map +1 -0
  71. package/lib/typescript/module/src/reanimated/index.d.ts +3 -0
  72. package/lib/typescript/module/src/reanimated/index.d.ts.map +1 -0
  73. package/lib/typescript/module/src/reanimated/useWeatherAnimation.d.ts +44 -0
  74. package/lib/typescript/module/src/reanimated/useWeatherAnimation.d.ts.map +1 -0
  75. package/lib/typescript/module/src/reportNativeError.d.ts +3 -0
  76. package/lib/typescript/module/src/reportNativeError.d.ts.map +1 -0
  77. package/lib/typescript/module/src/types.d.ts +51 -0
  78. package/lib/typescript/module/src/types.d.ts.map +1 -0
  79. package/package.json +159 -0
  80. package/react-native.config.js +13 -0
  81. package/src/NativeModules/NativeWeatherOverlay.ts +73 -0
  82. package/src/components/WeatherOverlay.tsx +173 -0
  83. package/src/index.ts +22 -0
  84. package/src/reanimated/index.ts +2 -0
  85. package/src/reanimated/useWeatherAnimation.ts +118 -0
  86. package/src/reportNativeError.ts +15 -0
  87. package/src/types.ts +56 -0
@@ -0,0 +1,343 @@
1
+ package com.jhotadhari.reactnative.mapsforge.vtm.ext.grib.modules;
2
+
3
+ import androidx.annotation.NonNull;
4
+
5
+ import com.facebook.react.bridge.Promise;
6
+ import com.facebook.react.bridge.ReactApplicationContext;
7
+ import com.facebook.react.bridge.ReactMethod;
8
+ import com.facebook.react.bridge.ReadableMap;
9
+
10
+ import com.jhotadhari.reactnative.mapsforge.vtm.ext.grib.NativeWeatherOverlaySpec;
11
+
12
+ import org.oscim.android.MapView;
13
+ import org.oscim.layers.tile.bitmap.BitmapTileLayer;
14
+
15
+ import com.jhotadhari.reactnative.mapsforge.vtm.ext.grib.tiles.WeatherGridData;
16
+ import com.jhotadhari.reactnative.mapsforge.vtm.ext.grib.tiles.WeatherTileSource;
17
+
18
+ import org.json.JSONArray;
19
+ import org.json.JSONObject;
20
+
21
+ import java.io.BufferedReader;
22
+ import java.io.InputStreamReader;
23
+ import java.net.HttpURLConnection;
24
+ import java.net.URL;
25
+ import java.util.HashMap;
26
+ import java.util.LinkedHashMap;
27
+ import java.util.Map;
28
+
29
+ // These are public classes in the parent library react-native-mapsforge-vtm
30
+ import com.jhotadhari.reactnative.mapsforge.vtm.Utils;
31
+ import com.jhotadhari.reactnative.mapsforge.vtm.LayerHelper;
32
+ import com.jhotadhari.reactnative.mapsforge.vtm.LayerZoomBoundsHelper;
33
+
34
+ public class WeatherOverlay extends NativeWeatherOverlaySpec {
35
+
36
+ private final LayerHelper layerHelper;
37
+ private final LayerZoomBoundsHelper zoomBoundsHelper;
38
+
39
+ // In-memory cache of parsed grid data, keyed by dataUrl + "|" + parameter + "|" + timeIndex.
40
+ // LRU eviction with a reasonable cap — each grid is ~128 KB+, and unbounded
41
+ // retention across URL/parameter/timestep combinations would cause OOM.
42
+ private static final int MAX_GRID_CACHE_SIZE = 32;
43
+ private final Map<String, WeatherGridData> gridCache = new LinkedHashMap<String, WeatherGridData>(
44
+ 16, 0.75f, true // access-order for LRU
45
+ ) {
46
+ @Override
47
+ protected boolean removeEldestEntry(Map.Entry<String, WeatherGridData> eldest) {
48
+ return size() > MAX_GRID_CACHE_SIZE;
49
+ }
50
+ };
51
+
52
+ public WeatherOverlay(ReactApplicationContext reactContext) {
53
+ super(reactContext);
54
+ layerHelper = new LayerHelper(this, reactContext);
55
+ zoomBoundsHelper = new LayerZoomBoundsHelper(this, reactContext);
56
+ }
57
+
58
+ @NonNull
59
+ @Override
60
+ public Map<String, Object> getTypedExportedConstants() {
61
+ final Map<String, Object> constants = new HashMap<>();
62
+ constants.put("dataUrl", "");
63
+ constants.put("parameter", "WIND");
64
+ constants.put("timeIndex", 0);
65
+ constants.put("colorMap", "wind");
66
+ constants.put("opacity", 0.7);
67
+ constants.put("zoomMin", 1);
68
+ constants.put("zoomMax", 18);
69
+ constants.put("enabledZoomMin", 1);
70
+ constants.put("enabledZoomMax", 18);
71
+ return constants;
72
+ }
73
+
74
+ // ------------------------------------------------------------------
75
+ // JS-facing methods (must match the TurboModule spec in JS)
76
+ // ------------------------------------------------------------------
77
+
78
+ @ReactMethod
79
+ public void createLayer(ReadableMap params, Promise promise) {
80
+ android.util.Log.d("WeatherOverlay", "createLayer called with params: " + params.toString());
81
+ try {
82
+ if (!Utils.rMapHasKey(params, "nativeNodeHandle")) {
83
+ Utils.promiseReject(promise, "Undefined nativeNodeHandle");
84
+ return;
85
+ }
86
+
87
+ MapView mapView = Utils.getMapView(
88
+ getReactApplicationContext(),
89
+ params.getInt("nativeNodeHandle")
90
+ );
91
+ if (null == mapView) {
92
+ Utils.promiseReject(promise, "Unable to find mapView");
93
+ return;
94
+ }
95
+
96
+ // Get params, assign defaults.
97
+ Map<String, Object> defaults = getTypedExportedConstants();
98
+ String dataUrl = Utils.rMapHasKey(params, "dataUrl")
99
+ ? params.getString("dataUrl") : (String) defaults.get("dataUrl");
100
+ String parameter = Utils.rMapHasKey(params, "parameter")
101
+ ? params.getString("parameter") : (String) defaults.get("parameter");
102
+ int timeIndex = Utils.rMapHasKey(params, "timeIndex")
103
+ ? params.getInt("timeIndex") : (int) defaults.get("timeIndex");
104
+ String colorMap = Utils.rMapHasKey(params, "colorMap")
105
+ ? params.getString("colorMap") : (String) defaults.get("colorMap");
106
+ double opacity = Utils.rMapHasKey(params, "opacity")
107
+ ? params.getDouble("opacity") : (double) defaults.get("opacity");
108
+ int zoomMin = Utils.rMapHasKey(params, "zoomMin")
109
+ ? params.getInt("zoomMin") : (int) defaults.get("zoomMin");
110
+ int zoomMax = Utils.rMapHasKey(params, "zoomMax")
111
+ ? params.getInt("zoomMax") : (int) defaults.get("zoomMax");
112
+
113
+ // Load grid data (cached in memory after first load).
114
+ // Include timeIndex in the key so different timesteps of the
115
+ // same URL+parameter are cached independently.
116
+ String cacheKey = dataUrl + "|" + parameter + "|" + timeIndex;
117
+ WeatherGridData gridData = gridCache.get(cacheKey);
118
+ if (gridData == null) {
119
+ gridData = fetchAndParseGridData(dataUrl, parameter, timeIndex);
120
+ gridCache.put(cacheKey, gridData);
121
+ }
122
+
123
+ // Create the tile source (HillshadingTileSource pattern).
124
+ WeatherTileSource tileSource = new WeatherTileSource(
125
+ zoomMin, zoomMax, gridData, parameter, timeIndex, colorMap
126
+ );
127
+
128
+ // Create the bitmap tile layer.
129
+ BitmapTileLayer layer = new BitmapTileLayer(
130
+ mapView.map(), tileSource, (float) opacity
131
+ );
132
+
133
+ // Add to map via the async helper.
134
+ layerHelper.addLayerAsync(layer, params)
135
+ .thenAccept(uid -> {
136
+ // Clear the map to trigger tile loading for the new layer.
137
+ mapView.map().clearMap();
138
+ promise.resolve(uid);
139
+ })
140
+ .exceptionally(throwable -> {
141
+ Utils.promiseReject(promise,
142
+ "Unable to add layer: " + throwable.getMessage());
143
+ return null;
144
+ });
145
+
146
+ } catch (Exception e) {
147
+ e.printStackTrace();
148
+ Utils.promiseReject(promise, e.getMessage());
149
+ }
150
+ }
151
+
152
+ @ReactMethod
153
+ public void removeLayer(ReadableMap params, Promise promise) {
154
+ layerHelper.removeLayerAsync(params)
155
+ .thenRun(() -> {
156
+ if (Utils.rMapHasKey(params, "uuid")) {
157
+ promise.resolve(params.getString("uuid"));
158
+ } else {
159
+ promise.resolve("");
160
+ }
161
+ })
162
+ .exceptionally(t -> {
163
+ Utils.promiseReject(promise, t.getMessage());
164
+ return null;
165
+ });
166
+ }
167
+
168
+ @ReactMethod
169
+ public void updateEnabledZoomMinMax(ReadableMap params, Promise promise) {
170
+ try {
171
+ if (!Utils.rMapHasKey(params, "uuid")
172
+ || !Utils.rMapHasKey(params, "nativeNodeHandle")) {
173
+ Utils.promiseReject(promise, "Undefined uuid or nativeNodeHandle");
174
+ return;
175
+ }
176
+ zoomBoundsHelper.updateEnabledZoomMinMax(params, promise);
177
+ } catch (Exception e) {
178
+ e.printStackTrace();
179
+ Utils.promiseReject(promise, e.getMessage());
180
+ }
181
+ }
182
+
183
+ @ReactMethod
184
+ public void setOpacity(ReadableMap params, Promise promise) {
185
+ try {
186
+ if (!Utils.rMapHasKey(params, "uuid")
187
+ || !Utils.rMapHasKey(params, "nativeNodeHandle")) {
188
+ Utils.promiseReject(promise, "Undefined uuid or nativeNodeHandle");
189
+ return;
190
+ }
191
+
192
+ MapView mapView = Utils.getMapView(
193
+ getReactApplicationContext(),
194
+ params.getInt("nativeNodeHandle")
195
+ );
196
+ if (null == mapView) {
197
+ Utils.promiseReject(promise, "Unable to find mapView");
198
+ return;
199
+ }
200
+
201
+ double opacity = Utils.rMapHasKey(params, "opacity")
202
+ ? params.getDouble("opacity")
203
+ : (double) getTypedExportedConstants().get("opacity");
204
+
205
+ // Route through the UI thread — BitmapTileLayer.setBitmapAlpha
206
+ // triggers a map redraw and must not be called from the bridge
207
+ // thread (consistent with the core library's threading model).
208
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread(() -> {
209
+ try {
210
+ BitmapTileLayer layer = (BitmapTileLayer) layerHelper
211
+ .getLayers(params.getInt("nativeNodeHandle"))
212
+ .get(params.getString("uuid"));
213
+
214
+ if (null == layer) {
215
+ Utils.promiseReject(promise, "Unable to find layer");
216
+ return;
217
+ }
218
+
219
+ layer.setBitmapAlpha((float) opacity, true);
220
+ promise.resolve(params.getString("uuid"));
221
+ } catch (Exception e) {
222
+ Utils.promiseReject(promise, e.getMessage());
223
+ }
224
+ });
225
+
226
+ } catch (Exception e) {
227
+ e.printStackTrace();
228
+ Utils.promiseReject(promise, e.getMessage());
229
+ }
230
+ }
231
+
232
+ @ReactMethod
233
+ public void setTimeIndex(ReadableMap params, Promise promise) {
234
+ try {
235
+ if (!Utils.rMapHasKey(params, "uuid")
236
+ || !Utils.rMapHasKey(params, "nativeNodeHandle")) {
237
+ Utils.promiseReject(promise, "Undefined uuid or nativeNodeHandle");
238
+ return;
239
+ }
240
+
241
+ // Time index changes are handled by the JS side via the
242
+ // remove+create pattern in WeatherOverlay's recreate useEffect
243
+ // (timeIndex is in its dependency array). The recreate path
244
+ // fetches fresh data with the correct cache key and creates a
245
+ // new WeatherTileSource. This method exists for API symmetry
246
+ // and future in-place interpolation support.
247
+ promise.resolve(params.getString("uuid"));
248
+
249
+ } catch (Exception e) {
250
+ e.printStackTrace();
251
+ Utils.promiseReject(promise, e.getMessage());
252
+ }
253
+ }
254
+
255
+ // ------------------------------------------------------------------
256
+ // Data loading
257
+ // ------------------------------------------------------------------
258
+
259
+ /**
260
+ * Fetches a JSON weather grid from a URL and parses it.
261
+ *
262
+ * <p>Expected JSON format:
263
+ * <pre>
264
+ * {
265
+ * "parameter": "WIND",
266
+ * "unit": "m/s",
267
+ * "timeSteps": [{
268
+ * "validTime": "2026-07-03T00:00:00Z",
269
+ * "minLat": 48.0, "maxLat": 52.0,
270
+ * "minLng": -2.0, "maxLng": 3.0,
271
+ * "ni": 20, "nj": 16,
272
+ * "values": [0.5, 1.2, ...]
273
+ * }]
274
+ * }
275
+ * </pre>
276
+ */
277
+ private WeatherGridData fetchAndParseGridData(
278
+ String dataUrl, String parameter, int timeIndex
279
+ ) {
280
+ HttpURLConnection connection = null;
281
+ try {
282
+ URL url = new URL(dataUrl);
283
+ connection = (HttpURLConnection) url.openConnection();
284
+ connection.setConnectTimeout(10000);
285
+ connection.setReadTimeout(10000);
286
+
287
+ BufferedReader reader = new BufferedReader(
288
+ new InputStreamReader(connection.getInputStream(), "UTF-8")
289
+ );
290
+ StringBuilder sb = new StringBuilder();
291
+ String line;
292
+ while ((line = reader.readLine()) != null) {
293
+ sb.append(line);
294
+ }
295
+ reader.close();
296
+
297
+ JSONObject root = new JSONObject(sb.toString());
298
+
299
+ String param = root.optString("parameter", parameter);
300
+ String unit = root.optString("unit", "");
301
+
302
+ JSONArray timeSteps = root.getJSONArray("timeSteps");
303
+ if (timeIndex < 0 || timeIndex >= timeSteps.length()) {
304
+ throw new IllegalArgumentException(
305
+ "timeIndex " + timeIndex + " out of range [0, " + (timeSteps.length() - 1) + "]"
306
+ );
307
+ }
308
+
309
+ JSONObject step = timeSteps.getJSONObject(timeIndex);
310
+
311
+ double minLat = step.getDouble("minLat");
312
+ double maxLat = step.getDouble("maxLat");
313
+ double minLng = step.getDouble("minLng");
314
+ double maxLng = step.getDouble("maxLng");
315
+ int ni = step.getInt("ni");
316
+ int nj = step.getInt("nj");
317
+
318
+ JSONArray valuesArr = step.getJSONArray("values");
319
+ float[] values = new float[valuesArr.length()];
320
+ for (int i = 0; i < valuesArr.length(); i++) {
321
+ if (valuesArr.isNull(i)) {
322
+ values[i] = Float.NaN;
323
+ } else {
324
+ values[i] = (float) valuesArr.getDouble(i);
325
+ }
326
+ }
327
+
328
+ return new WeatherGridData(
329
+ minLat, maxLat, minLng, maxLng, ni, nj, values, param, unit
330
+ );
331
+
332
+ } catch (Exception e) {
333
+ e.printStackTrace();
334
+ throw new RuntimeException(
335
+ "Failed to load weather data from " + dataUrl + ": " + e.toString()
336
+ );
337
+ } finally {
338
+ if (connection != null) {
339
+ connection.disconnect();
340
+ }
341
+ }
342
+ }
343
+ }
@@ -0,0 +1,165 @@
1
+ package com.jhotadhari.reactnative.mapsforge.vtm.ext.grib.tiles;
2
+
3
+ import android.graphics.Color;
4
+
5
+ /**
6
+ * Maps numeric weather values to display colors.
7
+ *
8
+ * <p>Built-in ramps follow meteorological conventions:
9
+ * <ul>
10
+ * <li><b>temperature</b> — blue → cyan → green → yellow → orange → red</li>
11
+ * <li><b>wind</b> — light blue → green → yellow → red → purple</li>
12
+ * <li><b>pressure</b> — blue → white → red (diverging, centered on 1013 hPa)</li>
13
+ * <li><b>rainfall</b> — transparent → light blue → blue → purple</li>
14
+ * <li><b>waves</b> — light cyan → blue → dark blue</li>
15
+ * <li><b>grayscale</b> — black → white (generic, for debugging)</li>
16
+ * </ul>
17
+ */
18
+ public class ColorRamp {
19
+
20
+ /** Color stops: [{value, r, g, b}, ...]. Sorted by value ascending. */
21
+ private final float[] values;
22
+ private final int[] colors;
23
+
24
+ private ColorRamp(float[] values, int[] colors) {
25
+ this.values = values;
26
+ this.colors = colors;
27
+ }
28
+
29
+ /**
30
+ * Returns the color for a given data value, interpolating between
31
+ * the nearest stops.
32
+ */
33
+ public int getColor(float value) {
34
+ if (value <= values[0]) return colors[0];
35
+ if (value >= values[values.length - 1]) return colors[colors.length - 1];
36
+
37
+ // Find the bracket.
38
+ int i = 0;
39
+ while (i < values.length - 1 && values[i + 1] < value) {
40
+ i++;
41
+ }
42
+
43
+ float t = (value - values[i]) / (values[i + 1] - values[i]);
44
+ t = Math.max(0, Math.min(1, t));
45
+
46
+ int c0 = colors[i];
47
+ int c1 = colors[i + 1];
48
+
49
+ int r = (int) (Color.red(c0) + (Color.red(c1) - Color.red(c0)) * t);
50
+ int g = (int) (Color.green(c0) + (Color.green(c1) - Color.green(c0)) * t);
51
+ int b = (int) (Color.blue(c0) + (Color.blue(c1) - Color.blue(c0)) * t);
52
+ int a = (int) (Color.alpha(c0) + (Color.alpha(c1) - Color.alpha(c0)) * t);
53
+
54
+ // Per-stop alpha is interpolated along with RGB. The layer-level
55
+ // opacity (set via setOpacity / BitmapTileLayer.setBitmapAlpha)
56
+ // provides the second multiplier, so users can achieve any effective
57
+ // opacity by tuning per-stop alpha and layer opacity independently.
58
+ return Color.argb(a, r, g, b);
59
+ }
60
+
61
+ /**
62
+ * Returns a built-in color ramp by name.
63
+ *
64
+ * @param name One of: "temperature", "wind", "pressure", "rainfall", "waves", "grayscale".
65
+ * @return The corresponding ColorRamp, or "wind" if the name is unrecognized.
66
+ */
67
+ public static ColorRamp forName(String name) {
68
+ switch (name != null ? name.toLowerCase() : "") {
69
+ case "temperature":
70
+ return temperature();
71
+ case "pressure":
72
+ return pressure();
73
+ case "rainfall":
74
+ return rainfall();
75
+ case "waves":
76
+ return waves();
77
+ case "grayscale":
78
+ return grayscale();
79
+ case "wind":
80
+ default:
81
+ return wind();
82
+ }
83
+ }
84
+
85
+ // -- Built-in ramps ---------------------------------------------------
86
+
87
+ /** Temperature: -20°C (blue) → 0°C (cyan) → 15°C (green) → 25°C (yellow) → 35°C (red). */
88
+ private static ColorRamp temperature() {
89
+ return new ColorRamp(
90
+ new float[]{-20, 0, 15, 25, 35},
91
+ new int[]{
92
+ Color.rgb(0, 0, 255), // -20°C: blue
93
+ Color.rgb(0, 200, 255), // 0°C: cyan
94
+ Color.rgb(0, 200, 0), // 15°C: green
95
+ Color.rgb(255, 255, 0), // 25°C: yellow
96
+ Color.rgb(255, 0, 0), // 35°C: red
97
+ }
98
+ );
99
+ }
100
+
101
+ /** Wind speed: 0 m/s (light blue) → 15 m/s (green) → 30 m/s (yellow) → 50 m/s (red) → 70 m/s (purple). */
102
+ private static ColorRamp wind() {
103
+ return new ColorRamp(
104
+ new float[]{0, 15, 30, 50, 70},
105
+ new int[]{
106
+ Color.rgb(200, 230, 255), // calm: pale blue
107
+ Color.rgb(50, 200, 50), // 15 m/s: green
108
+ Color.rgb(255, 255, 0), // 30 m/s: yellow
109
+ Color.rgb(255, 0, 0), // 50 m/s: red
110
+ Color.rgb(128, 0, 128), // 70 m/s: purple
111
+ }
112
+ );
113
+ }
114
+
115
+ /** Pressure: 960 hPa (blue) → 1013 hPa (white) → 1050 hPa (red). Diverging around standard sea-level pressure. */
116
+ private static ColorRamp pressure() {
117
+ return new ColorRamp(
118
+ new float[]{960, 990, 1013, 1036, 1050},
119
+ new int[]{
120
+ Color.rgb(0, 0, 200), // deep low: blue
121
+ Color.rgb(100, 150, 255), // low: light blue
122
+ Color.rgb(255, 255, 255), // 1013: white
123
+ Color.rgb(255, 150, 100), // high: light red
124
+ Color.rgb(200, 0, 0), // deep high: red
125
+ }
126
+ );
127
+ }
128
+
129
+ /** Rainfall rate: 0 mm/h (transparent) → 1 (light blue) → 5 (blue) → 20 (purple). */
130
+ private static ColorRamp rainfall() {
131
+ return new ColorRamp(
132
+ new float[]{0, 1, 5, 20},
133
+ new int[]{
134
+ Color.argb(40, 200, 230, 255), // trace: very pale
135
+ Color.rgb(100, 180, 255), // 1 mm/h: light blue
136
+ Color.rgb(0, 0, 255), // 5 mm/h: blue
137
+ Color.rgb(100, 0, 200), // 20 mm/h: purple
138
+ }
139
+ );
140
+ }
141
+
142
+ /** Wave height: 0m (cyan) → 2m (blue) → 5m (dark blue) → 10m (very dark). */
143
+ private static ColorRamp waves() {
144
+ return new ColorRamp(
145
+ new float[]{0, 2, 5, 10},
146
+ new int[]{
147
+ Color.rgb(180, 230, 255), // calm: light cyan
148
+ Color.rgb(30, 100, 255), // 2m: blue
149
+ Color.rgb(0, 0, 120), // 5m: dark blue
150
+ Color.rgb(0, 0, 40), // 10m: very dark
151
+ }
152
+ );
153
+ }
154
+
155
+ /** Grayscale: 0 (black) → 100 (white). Generic/debug ramp. */
156
+ private static ColorRamp grayscale() {
157
+ return new ColorRamp(
158
+ new float[]{0, 100},
159
+ new int[]{
160
+ Color.rgb(0, 0, 0),
161
+ Color.rgb(255, 255, 255),
162
+ }
163
+ );
164
+ }
165
+ }
@@ -0,0 +1,103 @@
1
+ package com.jhotadhari.reactnative.mapsforge.vtm.ext.grib.tiles;
2
+
3
+ /**
4
+ * In-memory representation of a single time step of gridded weather data.
5
+ *
6
+ * <p>The grid is a regular lat/lon grid with {@code ni} columns (longitude)
7
+ * and {@code nj} rows (latitude). Values are stored in row-major order
8
+ * (latitude varies slowest). Missing values are represented by {@link Float#NaN}.
9
+ */
10
+ public class WeatherGridData {
11
+
12
+ /** Minimum latitude (southern edge). */
13
+ public final double minLat;
14
+ /** Maximum latitude (northern edge). */
15
+ public final double maxLat;
16
+ /** Minimum longitude (western edge). */
17
+ public final double minLng;
18
+ /** Maximum longitude (eastern edge). */
19
+ public final double maxLng;
20
+ /** Number of grid points in the longitude direction. */
21
+ public final int ni;
22
+ /** Number of grid points in the latitude direction. */
23
+ public final int nj;
24
+ /** Row-major grid values, length = ni * nj. NaN = missing. */
25
+ public final float[] values;
26
+
27
+ /** The human-readable parameter name (e.g. "WIND", "TEMP"). */
28
+ public final String parameter;
29
+ /** The unit string (e.g. "m/s", "K", "hPa"). */
30
+ public final String unit;
31
+
32
+ /**
33
+ * @param minLat Minimum latitude (southern edge).
34
+ * @param maxLat Maximum latitude (northern edge).
35
+ * @param minLng Minimum longitude (western edge).
36
+ * @param maxLng Maximum longitude (eastern edge).
37
+ * @param ni Number of grid points in longitude direction.
38
+ * @param nj Number of grid points in latitude direction.
39
+ * @param values Row-major float values.
40
+ * @param parameter Parameter name.
41
+ * @param unit Unit string.
42
+ */
43
+ public WeatherGridData(
44
+ double minLat, double maxLat,
45
+ double minLng, double maxLng,
46
+ int ni, int nj,
47
+ float[] values,
48
+ String parameter,
49
+ String unit
50
+ ) {
51
+ this.minLat = minLat;
52
+ this.maxLat = maxLat;
53
+ this.minLng = minLng;
54
+ this.maxLng = maxLng;
55
+ this.ni = ni;
56
+ this.nj = nj;
57
+ this.values = values;
58
+ this.parameter = parameter;
59
+ this.unit = unit;
60
+ }
61
+
62
+ /**
63
+ * Looks up the grid value at a given geographic position using bilinear
64
+ * interpolation. Returns {@link Float#NaN} if the position is outside the
65
+ * grid bounds or if any of the four surrounding grid points are missing.
66
+ */
67
+ public float getValueAt(double lat, double lng) {
68
+ if (lat < minLat || lat > maxLat || lng < minLng || lng > maxLng) {
69
+ return Float.NaN;
70
+ }
71
+
72
+ // Fractional position within the grid [0..1].
73
+ double fx = (lng - minLng) / (maxLng - minLng);
74
+ double fy = (lat - minLat) / (maxLat - minLat);
75
+
76
+ // Grid index (floating point).
77
+ double gi = fx * (ni - 1);
78
+ double gj = (1.0 - fy) * (nj - 1); // Flip: row 0 is maxLat.
79
+
80
+ int i0 = (int) Math.floor(gi);
81
+ int i1 = Math.min(i0 + 1, ni - 1);
82
+ int j0 = (int) Math.floor(gj);
83
+ int j1 = Math.min(j0 + 1, nj - 1);
84
+
85
+ float v00 = values[j0 * ni + i0];
86
+ float v10 = values[j0 * ni + i1];
87
+ float v01 = values[j1 * ni + i0];
88
+ float v11 = values[j1 * ni + i1];
89
+
90
+ // If any surrounding point is missing, don't interpolate.
91
+ if (Float.isNaN(v00) || Float.isNaN(v10) || Float.isNaN(v01) || Float.isNaN(v11)) {
92
+ return Float.NaN;
93
+ }
94
+
95
+ double tx = gi - i0;
96
+ double ty = gj - j0;
97
+
98
+ // Bilinear interpolation.
99
+ float v0 = (float) (v00 + (v10 - v00) * tx);
100
+ float v1 = (float) (v01 + (v11 - v01) * tx);
101
+ return (float) (v0 + (v1 - v0) * ty);
102
+ }
103
+ }