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.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/android/CMakeLists.txt +13 -0
- package/android/build.gradle +108 -0
- package/android/generated/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/NativeWeatherOverlaySpec.java +95 -0
- package/android/generated/jni/CMakeLists.txt +28 -0
- package/android/generated/jni/RNMapsforgeVtmExtGribSpec-generated.cpp +62 -0
- package/android/generated/jni/RNMapsforgeVtmExtGribSpec.h +31 -0
- package/android/generated/jni/react/renderer/components/RNMapsforgeVtmExtGribSpec/RNMapsforgeVtmExtGribSpecJSI.h +521 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/ExtGribPackage.java +62 -0
- package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/modules/WeatherOverlay.java +343 -0
- package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/tiles/ColorRamp.java +165 -0
- package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/tiles/WeatherGridData.java +103 -0
- package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/tiles/WeatherTileDataSource.java +147 -0
- package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/tiles/WeatherTileSource.java +80 -0
- package/lib/commonjs/NativeModules/NativeWeatherOverlay.js +9 -0
- package/lib/commonjs/NativeModules/NativeWeatherOverlay.js.map +1 -0
- package/lib/commonjs/components/WeatherOverlay.js +180 -0
- package/lib/commonjs/components/WeatherOverlay.js.map +1 -0
- package/lib/commonjs/index.js +21 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/reanimated/index.js +13 -0
- package/lib/commonjs/reanimated/index.js.map +1 -0
- package/lib/commonjs/reanimated/useWeatherAnimation.js +86 -0
- package/lib/commonjs/reanimated/useWeatherAnimation.js.map +1 -0
- package/lib/commonjs/reportNativeError.js +14 -0
- package/lib/commonjs/reportNativeError.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/NativeModules/NativeWeatherOverlay.js +5 -0
- package/lib/module/NativeModules/NativeWeatherOverlay.js.map +1 -0
- package/lib/module/components/WeatherOverlay.js +174 -0
- package/lib/module/components/WeatherOverlay.js.map +1 -0
- package/lib/module/index.js +16 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/reanimated/index.js +4 -0
- package/lib/module/reanimated/index.js.map +1 -0
- package/lib/module/reanimated/useWeatherAnimation.js +81 -0
- package/lib/module/reanimated/useWeatherAnimation.js.map +1 -0
- package/lib/module/reportNativeError.js +10 -0
- package/lib/module/reportNativeError.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/NativeModules/NativeWeatherOverlay.d.ts +63 -0
- package/lib/typescript/commonjs/src/NativeModules/NativeWeatherOverlay.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/components/WeatherOverlay.d.ts +10 -0
- package/lib/typescript/commonjs/src/components/WeatherOverlay.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +11 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/reanimated/index.d.ts +3 -0
- package/lib/typescript/commonjs/src/reanimated/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/reanimated/useWeatherAnimation.d.ts +44 -0
- package/lib/typescript/commonjs/src/reanimated/useWeatherAnimation.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/reportNativeError.d.ts +3 -0
- package/lib/typescript/commonjs/src/reportNativeError.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/types.d.ts +51 -0
- package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/NativeModules/NativeWeatherOverlay.d.ts +63 -0
- package/lib/typescript/module/src/NativeModules/NativeWeatherOverlay.d.ts.map +1 -0
- package/lib/typescript/module/src/components/WeatherOverlay.d.ts +10 -0
- package/lib/typescript/module/src/components/WeatherOverlay.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +11 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/reanimated/index.d.ts +3 -0
- package/lib/typescript/module/src/reanimated/index.d.ts.map +1 -0
- package/lib/typescript/module/src/reanimated/useWeatherAnimation.d.ts +44 -0
- package/lib/typescript/module/src/reanimated/useWeatherAnimation.d.ts.map +1 -0
- package/lib/typescript/module/src/reportNativeError.d.ts +3 -0
- package/lib/typescript/module/src/reportNativeError.d.ts.map +1 -0
- package/lib/typescript/module/src/types.d.ts +51 -0
- package/lib/typescript/module/src/types.d.ts.map +1 -0
- package/package.json +159 -0
- package/react-native.config.js +13 -0
- package/src/NativeModules/NativeWeatherOverlay.ts +73 -0
- package/src/components/WeatherOverlay.tsx +173 -0
- package/src/index.ts +22 -0
- package/src/reanimated/index.ts +2 -0
- package/src/reanimated/useWeatherAnimation.ts +118 -0
- package/src/reportNativeError.ts +15 -0
- 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
|
+
}
|
package/android/src/main/java/com/jhotadhari/reactnative/mapsforge/vtm/ext/grib/tiles/ColorRamp.java
ADDED
|
@@ -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
|
+
}
|