@aguacerowx/react-native 0.0.52 → 0.0.53

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 (36) hide show
  1. package/android/src/main/cpp/satellite_ktx_jni.cpp +6 -1
  2. package/android/src/main/java/com/aguacerowx/reactnative/SatelliteLayer.java +121 -1
  3. package/android/src/main/java/com/aguacerowx/reactnative/SatelliteLayerView.java +556 -392
  4. package/ios/SatelliteLayerView.swift +517 -517
  5. package/lib/commonjs/WeatherLayerManager.js +21 -1
  6. package/lib/commonjs/WeatherLayerManager.js.map +1 -1
  7. package/lib/commonjs/aguaceroRnDebug.js +1 -0
  8. package/lib/commonjs/aguaceroRnDebug.js.map +1 -1
  9. package/lib/commonjs/index.js +37 -0
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/satellite/satelliteAndroidController.js +17 -9
  12. package/lib/commonjs/satellite/satelliteAndroidController.js.map +1 -1
  13. package/lib/commonjs/satelliteRnDebug.js +261 -0
  14. package/lib/commonjs/satelliteRnDebug.js.map +1 -0
  15. package/lib/module/WeatherLayerManager.js +21 -1
  16. package/lib/module/WeatherLayerManager.js.map +1 -1
  17. package/lib/module/aguaceroRnDebug.js +1 -0
  18. package/lib/module/aguaceroRnDebug.js.map +1 -1
  19. package/lib/module/index.js +1 -0
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/satellite/satelliteAndroidController.js +17 -9
  22. package/lib/module/satellite/satelliteAndroidController.js.map +1 -1
  23. package/lib/module/satelliteRnDebug.js +248 -0
  24. package/lib/module/satelliteRnDebug.js.map +1 -0
  25. package/lib/typescript/WeatherLayerManager.d.ts.map +1 -1
  26. package/lib/typescript/aguaceroRnDebug.d.ts.map +1 -1
  27. package/lib/typescript/index.d.ts +1 -0
  28. package/lib/typescript/satellite/satelliteAndroidController.d.ts.map +1 -1
  29. package/lib/typescript/satelliteRnDebug.d.ts +81 -0
  30. package/lib/typescript/satelliteRnDebug.d.ts.map +1 -0
  31. package/package.json +2 -2
  32. package/src/WeatherLayerManager.js +20 -0
  33. package/src/aguaceroRnDebug.js +1 -0
  34. package/src/index.js +8 -0
  35. package/src/satellite/satelliteAndroidController.js +20 -8
  36. package/src/satelliteRnDebug.js +269 -0
@@ -1,517 +1,517 @@
1
- import Foundation
2
- import MapboxMaps
3
- import React
4
- import UIKit
5
-
6
- @objc(SatelliteLayerView)
7
- public final class SatelliteLayerView: UIView {
8
-
9
- /// Xcode / device console — grep `[AguaceroWX][SatelliteLayerView]` or `SatelliteLayerView`
10
- private static func satLog(_ message: String) {
11
- NSLog("[AguaceroWX][SatelliteLayerView] %@", message)
12
- }
13
-
14
- public static let satelliteLayerId = "aguacero-satellite-layer"
15
- private static let weatherGridLayerId = "aguacero-weather-grid-custom"
16
-
17
- @objc public var layerInstance: AguaceroSatelliteRenderer!
18
- @objc public weak var mapView: MapView?
19
- @objc public var layerId: String = SatelliteLayerView.satelliteLayerId
20
- @objc public var belowID: String?
21
-
22
- @objc public weak var bridge: RCTBridge?
23
-
24
- private var isLayerAdded = false
25
- private var satReparentScheduled = false
26
-
27
- @objc public var satelliteLayerAdded: Bool { isLayerAdded }
28
-
29
- private let decodeQueue = DispatchQueue(label: "com.aguacero.satellite-decode", qos: .utility)
30
- private var decodeGeneration = 0
31
- private var lastRunKey: String?
32
-
33
- private var fetchInFlight = Set<Int64>()
34
- private let fetchLock = NSLock()
35
-
36
- private lazy var urlSession: URLSession = {
37
- let c = URLSessionConfiguration.ephemeral
38
- c.timeoutIntervalForRequest = 120
39
- return URLSession(configuration: c)
40
- }()
41
-
42
- public override init(frame: CGRect) {
43
- layerInstance = AguaceroSatelliteRenderer(id: SatelliteLayerView.satelliteLayerId)
44
- super.init(frame: frame)
45
- isOpaque = false
46
- Self.satLog("init(frame:) SatelliteLayerView=\(Unmanaged.passUnretained(self).toOpaque())")
47
- }
48
-
49
- required init?(coder: NSCoder) {
50
- layerInstance = AguaceroSatelliteRenderer(id: SatelliteLayerView.satelliteLayerId)
51
- super.init(coder: coder)
52
- isOpaque = false
53
- Self.satLog("init(coder:) SatelliteLayerView=\(Unmanaged.passUnretained(self).toOpaque())")
54
- }
55
-
56
- public override func didMoveToWindow() {
57
- super.didMoveToWindow()
58
- if window != nil && !isLayerAdded {
59
- findMapViewAndAddLayer(in: window!)
60
- }
61
- }
62
-
63
- public override func layoutSubviews() {
64
- super.layoutSubviews()
65
- if !isLayerAdded {
66
- let root = window ?? superview
67
- if let root {
68
- findMapViewAndAddLayer(in: root)
69
- }
70
- }
71
- }
72
-
73
- private func findMapViewAndAddLayer(in view: UIView) {
74
- if isLayerAdded { return }
75
- if let map = findMapViewRecursive(view) {
76
- Self.satLog("findMapView: OK mapView attached, belowID=\(belowID ?? "nil")")
77
- self.mapView = map
78
- waitForStyleLoadAndAddLayer()
79
- } else {
80
- Self.satLog("findMapView: MapView not found under RNMBX hierarchy yet (will retry from layout/window)")
81
- }
82
- }
83
-
84
- private func findMapViewRecursive(_ view: UIView) -> MapView? {
85
- let name = NSStringFromClass(type(of: view))
86
- if name == "RNMBXMapView", view.subviews.count > 0 {
87
- let first = view.subviews[0]
88
- let subName = NSStringFromClass(type(of: first))
89
- if subName.contains("MapboxMaps.MapView") || subName == "MapView" {
90
- return first as? MapView
91
- }
92
- }
93
- for sub in view.subviews {
94
- if let found = findMapViewRecursive(sub) {
95
- return found
96
- }
97
- }
98
- return nil
99
- }
100
-
101
- private func waitForStyleLoadAndAddLayer() {
102
- guard let mapView else { return }
103
- let anchor = belowID ?? "AML_-_terrain"
104
- let anchorExists = GridRenderLayerBridge.layerExists(in: mapView, layerId: anchor)
105
- Self.satLog("waitForStyle: anchor=\(anchor) exists=\(anchorExists) gridLayer=\(GridRenderLayerBridge.layerExists(in: mapView, layerId: Self.weatherGridLayerId))")
106
- if anchorExists {
107
- addLayerToMap()
108
- return
109
- }
110
- pollForLayerExistence(anchor: anchor, attempts: 0)
111
- }
112
-
113
- private func pollForLayerExistence(anchor: String, attempts: Int) {
114
- guard let mapView, !isLayerAdded else { return }
115
- if attempts > 80 {
116
- Self.satLog("pollAnchor: GAVE UP after ~\(attempts * 100)ms — layer id \"\(anchor)\" never appeared. Satellite custom layer was NOT added. Check Mapbox style / belowID prop.")
117
- return
118
- }
119
- if attempts % 20 == 0, attempts > 0 {
120
- Self.satLog("pollAnchor: still waiting for \"\(anchor)\" attempt=\(attempts)")
121
- }
122
- if GridRenderLayerBridge.layerExists(in: mapView, layerId: anchor) {
123
- Self.satLog("pollAnchor: anchor \"\(anchor)\" appeared after \(attempts) attempts")
124
- addLayerToMap()
125
- return
126
- }
127
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
128
- self?.pollForLayerExistence(anchor: anchor, attempts: attempts + 1)
129
- }
130
- }
131
-
132
- private func addLayerToMap() {
133
- guard let mapView, !isLayerAdded else { return }
134
- guard let host = layerInstance.getHostWrapper() as? CustomLayerHost else {
135
- Self.satLog("addLayerToMap: ABORT — CustomLayer host missing")
136
- return
137
- }
138
-
139
- let success = GridRenderLayerBridge.addSatelliteCustomLayer(
140
- to: mapView,
141
- layerHost: host,
142
- layerId: layerId,
143
- weatherGridLayerId: Self.weatherGridLayerId,
144
- belowLayerId: belowID
145
- )
146
-
147
- if success {
148
- isLayerAdded = true
149
- let gridThere = GridRenderLayerBridge.layerExists(in: mapView, layerId: Self.weatherGridLayerId)
150
- Self.satLog("addLayerToMap: OK layerId=\(layerId) aboveGrid=\(gridThere) belowID=\(belowID ?? "nil")")
151
- GridRenderLayerBridge.triggerRepaint(on: mapView)
152
- scheduleReparentAboveGridOnce()
153
- } else {
154
- Self.satLog("addLayerToMap: FAILED Mapbox addLayer for layerId=\(layerId) — see GridRenderLayerBridge logs")
155
- }
156
- }
157
-
158
- private func scheduleReparentAboveGridOnce() {
159
- if satReparentScheduled { return }
160
- satReparentScheduled = true
161
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
162
- self?.satReparentScheduled = false
163
- self?.reparentAboveGridIfNeeded()
164
- }
165
- }
166
-
167
- private func reparentAboveGridIfNeeded() {
168
- guard let mapView else { return }
169
- let gridOk = GridRenderLayerBridge.layerExists(in: mapView, layerId: Self.weatherGridLayerId)
170
- let satOk = GridRenderLayerBridge.layerExists(in: mapView, layerId: layerId)
171
- guard gridOk, satOk else {
172
- Self.satLog("reparentAboveGrid: skip grid=\(gridOk) satellite=\(satOk)")
173
- return
174
- }
175
- guard let host = layerInstance.getHostWrapper() as? CustomLayerHost else { return }
176
-
177
- GridRenderLayerBridge.removeCustomLayer(from: mapView, layerId: layerId)
178
- _ = GridRenderLayerBridge.addSatelliteCustomLayer(
179
- to: mapView,
180
- layerHost: host,
181
- layerId: layerId,
182
- weatherGridLayerId: Self.weatherGridLayerId,
183
- belowLayerId: belowID
184
- )
185
- GridRenderLayerBridge.triggerRepaint(on: mapView)
186
- }
187
-
188
- private struct FrameClass {
189
- let trueColor: Bool
190
- let colormapKind: String
191
- let useColormap: Bool
192
- }
193
-
194
- private static func classifyShaderFileName(_ shaderFileName: String?) -> FrameClass {
195
- let fn = shaderFileName ?? ""
196
- let isTrueColor =
197
- fn.contains("_truecolor_")
198
- || fn.contains("_geocolor_")
199
- || fn.contains("_firetemperature_")
200
- || fn.contains("_dust_")
201
- || fn.contains("_simplewatervapor_")
202
- || fn.contains("_ntmicro_")
203
- || fn.contains("_daycloudphase_")
204
- || fn.contains("_daylandcloudfire_")
205
- || fn.contains("_airmass_")
206
- || fn.contains("_sandwich_")
207
-
208
- var colormapType = "none"
209
- if fn.contains("C13") || fn.contains("C14") || fn.contains("C15") || fn.contains("C16") {
210
- colormapType = "ir"
211
- } else if fn.contains("C08") || fn.contains("C09") || fn.contains("C10") {
212
- colormapType = "wv"
213
- }
214
-
215
- let useColormap = !isTrueColor && colormapType != "none"
216
- return FrameClass(trueColor: isTrueColor, colormapKind: colormapType, useColormap: useColormap)
217
- }
218
-
219
- /// Match `WeatherLayerManager` grid `buildGridFrameProcessOptions`: authoritative `apiKey` in the query string
220
- /// plus `x-api-key` on the request (same pattern as `AguaceroCore` grid / NEXRAD CDN).
221
- private static func satelliteFetchURL(from url: URL, apiKey: String) -> URL {
222
- guard var c = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }
223
- var q = c.queryItems ?? []
224
- q.removeAll { $0.name == "apiKey" }
225
- q.append(URLQueryItem(name: "apiKey", value: apiKey))
226
- if !q.contains(where: { $0.name == "userId" }) {
227
- q.append(URLQueryItem(name: "userId", value: "sdk-user"))
228
- }
229
- c.queryItems = q
230
- return c.url ?? url
231
- }
232
-
233
- @objc public func clearSatellite() {
234
- Self.satLog("clearSatellite: bump decodeGeneration (was \(decodeGeneration)) → \(decodeGeneration + 1), cancel in-flight fetches")
235
- decodeGeneration += 1
236
- fetchLock.lock()
237
- fetchInFlight.removeAll()
238
- fetchLock.unlock()
239
- layerInstance.clearAll()
240
- if let mapView {
241
- GridRenderLayerBridge.triggerRepaint(on: mapView)
242
- }
243
- }
244
-
245
- @objc public func syncFromJson(_ json: String?) {
246
- guard let json, !json.isEmpty,
247
- let data = json.data(using: .utf8),
248
- let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
249
- else {
250
- Self.satLog("syncFromJson: SKIP empty or invalid JSON (nil=\(json == nil) len=\(json?.count ?? 0))")
251
- return
252
- }
253
-
254
- let runKey = (obj["runKey"] as? String) ?? ""
255
- if !runKey.isEmpty, runKey != lastRunKey {
256
- Self.satLog("syncFromJson: new runKey=\(runKey) (invalidate caches + bump generation)")
257
- lastRunKey = runKey
258
- decodeGeneration += 1
259
- fetchLock.lock()
260
- fetchInFlight.removeAll()
261
- fetchLock.unlock()
262
- layerInstance.clearAll()
263
- }
264
-
265
- layerInstance.setVisible((obj["visible"] as? Bool) ?? true)
266
- layerInstance.setOpacity(Float((obj["opacity"] as? Double) ?? 1.0))
267
- layerInstance.setFillSmoothing(obj["fillSmoothing"] as? Int ?? 0)
268
-
269
- if let tu = obj["targetUnix"] as? Int64 {
270
- layerInstance.setActiveUnix(NSNumber(value: tu))
271
- } else if let tu = obj["targetUnix"] as? Int {
272
- layerInstance.setActiveUnix(NSNumber(value: Int64(tu)))
273
- } else if let tu = obj["targetUnix"] as? Double {
274
- layerInstance.setActiveUnix(NSNumber(value: Int64(tu)))
275
- }
276
-
277
- let apiKey = (obj["apiKey"] as? String) ?? ""
278
- let bundleId = (obj["bundleId"] as? String) ?? ""
279
- var gridSiteOrigin = (obj["gridRequestSiteOrigin"] as? String) ?? ""
280
- gridSiteOrigin = gridSiteOrigin.trimmingCharacters(in: .whitespacesAndNewlines)
281
- while gridSiteOrigin.hasSuffix("/") {
282
- gridSiteOrigin.removeLast()
283
- }
284
- let frames = obj["frames"] as? [[String: Any]]
285
- let frameCount = frames?.count ?? 0
286
- let tgt = obj["targetUnix"]
287
- Self.satLog(
288
- "syncFromJson: runKey=\(runKey.isEmpty ? "(none)" : runKey) frames=\(frameCount) apiKey.len=\(apiKey.count) bundleId.len=\(bundleId.count) gridOrigin.len=\(gridSiteOrigin.count) visible=\((obj["visible"] as? Bool) ?? true) targetUnix=\(String(describing: tgt)) layerOnMap=\(isLayerAdded)"
289
- )
290
-
291
- guard let frames, !frames.isEmpty, !apiKey.isEmpty else {
292
- if frames == nil {
293
- Self.satLog("syncFromJson: EARLY EXIT — \"frames\" missing or not an array (no KTX URLs to fetch)")
294
- } else if frames?.isEmpty == true {
295
- Self.satLog("syncFromJson: EARLY EXIT — frames[] empty (JS timeline map likely not loaded yet; nothing to fetch)")
296
- } else {
297
- Self.satLog("syncFromJson: EARLY EXIT — apiKey empty (cannot authorize satellite CDN)")
298
- }
299
- if let mapView {
300
- GridRenderLayerBridge.triggerRepaint(on: mapView)
301
- }
302
- return
303
- }
304
-
305
- let gen = decodeGeneration
306
-
307
- for (idx, item) in frames.enumerated() {
308
- guard let unixNum = item["unix"],
309
- let urlStr = item["url"] as? String,
310
- let url = URL(string: urlStr)
311
- else { continue }
312
- let unix: Int64
313
- if let i = unixNum as? Int64 {
314
- unix = i
315
- } else if let i = unixNum as? Int {
316
- unix = Int64(i)
317
- } else if let d = unixNum as? Double {
318
- unix = Int64(d)
319
- } else {
320
- continue
321
- }
322
- let shaderFileName = item["shaderFileName"] as? String ?? ""
323
- if idx < 5 {
324
- Self.satLog("scheduleFetch[\(idx)]: unix=\(unix) host=\(url.host ?? "?") shader=\(shaderFileName.prefix(48))…")
325
- }
326
- scheduleFetchIfNeeded(
327
- gen: gen,
328
- unix: unix,
329
- url: url,
330
- shaderFileName: shaderFileName,
331
- apiKey: apiKey,
332
- bundleId: bundleId,
333
- gridRequestSiteOrigin: gridSiteOrigin
334
- )
335
- }
336
- if frames.count > 5 {
337
- Self.satLog("scheduleFetch: … and \(frames.count - 5) more frame(s) not listed")
338
- }
339
-
340
- if let mapView {
341
- GridRenderLayerBridge.triggerRepaint(on: mapView)
342
- }
343
- }
344
-
345
- private func scheduleFetchIfNeeded(
346
- gen: Int,
347
- unix: Int64,
348
- url: URL,
349
- shaderFileName: String,
350
- apiKey: String,
351
- bundleId: String,
352
- gridRequestSiteOrigin: String
353
- ) {
354
- if layerInstance.isFrameCached(unix) {
355
- Self.satLog("fetch: unix=\(unix) already cached — skip download")
356
- return
357
- }
358
-
359
- fetchLock.lock()
360
- if fetchInFlight.contains(unix) {
361
- fetchLock.unlock()
362
- Self.satLog("fetch: unix=\(unix) already in flight — skip duplicate")
363
- return
364
- }
365
- fetchInFlight.insert(unix)
366
- fetchLock.unlock()
367
-
368
- decodeQueue.async { [weak self] in
369
- guard let self else { return }
370
- if gen != self.decodeGeneration {
371
- Self.satLog("fetch: unix=\(unix) aborted — decodeGeneration changed (stale run)")
372
- self.fetchLock.lock()
373
- self.fetchInFlight.remove(unix)
374
- self.fetchLock.unlock()
375
- return
376
- }
377
-
378
- let fetchURL = Self.satelliteFetchURL(from: url, apiKey: apiKey)
379
- var req = URLRequest(url: fetchURL)
380
- req.setValue(apiKey, forHTTPHeaderField: "x-api-key")
381
- if !bundleId.isEmpty {
382
- req.setValue(bundleId, forHTTPHeaderField: "x-app-identifier")
383
- }
384
- // Match WeatherFrameProcessorModule + AguaceroCore grid fetch (CloudFront returns 403 without Origin on RN).
385
- var gridOrigin = gridRequestSiteOrigin.trimmingCharacters(in: .whitespacesAndNewlines)
386
- if gridOrigin.isEmpty {
387
- gridOrigin = "https://localhost"
388
- }
389
- while gridOrigin.hasSuffix("/") {
390
- gridOrigin.removeLast()
391
- }
392
- if !gridOrigin.isEmpty {
393
- req.setValue(gridOrigin, forHTTPHeaderField: "Origin")
394
- req.setValue("\(gridOrigin)/", forHTTPHeaderField: "Referer")
395
- }
396
-
397
- let sem = DispatchSemaphore(value: 0)
398
- var outData: Data?
399
- var httpStatus = -1
400
- let task = self.urlSession.dataTask(with: req) { data, response, _ in
401
- if let http = response as? HTTPURLResponse {
402
- httpStatus = http.statusCode
403
- }
404
- outData = data
405
- sem.signal()
406
- }
407
- task.resume()
408
- sem.wait()
409
-
410
- if gen != self.decodeGeneration {
411
- Self.satLog("fetch: unix=\(unix) post-network abort — generation bumped")
412
- DispatchQueue.main.async {
413
- self.fetchLock.lock()
414
- self.fetchInFlight.remove(unix)
415
- self.fetchLock.unlock()
416
- }
417
- return
418
- }
419
-
420
- guard httpStatus == 200, let ktxBytes = outData, !ktxBytes.isEmpty else {
421
- let bodyLen = outData?.count ?? 0
422
- Self.satLog("fetch: unix=\(unix) HTTP \(httpStatus) bodyBytes=\(bodyLen) — expected 200 + non-empty KTX2 body")
423
- DispatchQueue.main.async {
424
- self.fetchLock.lock()
425
- self.fetchInFlight.remove(unix)
426
- self.fetchLock.unlock()
427
- }
428
- return
429
- }
430
-
431
- if gen != self.decodeGeneration {
432
- DispatchQueue.main.async {
433
- self.fetchLock.lock()
434
- self.fetchInFlight.remove(unix)
435
- self.fetchLock.unlock()
436
- }
437
- return
438
- }
439
-
440
- let packedOpt = AguaceroSatelliteKtxBridge.transcodeKtx2(ktxBytes)
441
- let parsedOpt = packedOpt.flatMap { AguaceroSatelliteRenderer.parsePacked($0) }
442
- let meshOpt: SatelliteMeshBuilder.MeshResult? =
443
- parsedOpt.flatMap { p in
444
- SatelliteMeshBuilder.buildMesh(shaderFileName: shaderFileName, width: p.width, height: p.height)
445
- }
446
- guard packedOpt != nil,
447
- let parsed = parsedOpt,
448
- let mesh = meshOpt
449
- else {
450
- Self.satLog(
451
- "fetch: unix=\(unix) DECODE FAIL — ktxIn=\(ktxBytes.count)B packed=\(packedOpt?.count ?? -1) parsed=\(parsedOpt != nil) mesh=\(meshOpt != nil) shader=\(shaderFileName.prefix(40))"
452
- )
453
- DispatchQueue.main.async {
454
- self.fetchLock.lock()
455
- self.fetchInFlight.remove(unix)
456
- self.fetchLock.unlock()
457
- }
458
- return
459
- }
460
-
461
- let fc = Self.classifyShaderFileName(shaderFileName)
462
- let pending = AguaceroSatelliteRenderer.PendingGpuUpload(
463
- unix: unix,
464
- mesh: mesh,
465
- decoded: parsed,
466
- trueColor: fc.trueColor,
467
- colormapKind: fc.colormapKind,
468
- useColormap: fc.useColormap
469
- )
470
-
471
- DispatchQueue.main.async {
472
- self.fetchLock.lock()
473
- self.fetchInFlight.remove(unix)
474
- self.fetchLock.unlock()
475
-
476
- if gen != self.decodeGeneration {
477
- return
478
- }
479
- if !self.isLayerAdded {
480
- if let w = self.window {
481
- self.findMapViewAndAddLayer(in: w)
482
- } else if let sv = self.superview {
483
- self.findMapViewAndAddLayer(in: sv)
484
- }
485
- }
486
- Self.satLog(
487
- "GPU queue: unix=\(unix) \(parsed.width)x\(parsed.height) tc=\(fc.trueColor) cmap=\(fc.colormapKind) layerOnMap=\(self.isLayerAdded)"
488
- )
489
- self.layerInstance.queueGpuUpload(pending)
490
- if let mv = self.mapView {
491
- GridRenderLayerBridge.triggerRepaint(on: mv)
492
- }
493
- }
494
- }
495
- }
496
-
497
- @objc public func activateSatelliteCachedUnix(_ unix: Int64) {
498
- Self.satLog("activateSatelliteCachedUnix: unix=\(unix) cached=\(layerInstance.isFrameCached(unix))")
499
- layerInstance.activateCachedUnix(unix)
500
- if let mapView {
501
- GridRenderLayerBridge.triggerRepaint(on: mapView)
502
- }
503
- }
504
-
505
- @objc public func updateStyleFromJson(_ json: String?) {
506
- guard let json, !json.isEmpty,
507
- let data = json.data(using: .utf8),
508
- let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
509
- else { return }
510
- layerInstance.setVisible((obj["visible"] as? Bool) ?? true)
511
- layerInstance.setOpacity(Float((obj["opacity"] as? Double) ?? 1.0))
512
- layerInstance.setFillSmoothing(obj["fillSmoothing"] as? Int ?? 0)
513
- if let mapView {
514
- GridRenderLayerBridge.triggerRepaint(on: mapView)
515
- }
516
- }
517
- }
1
+ import Foundation
2
+ import MapboxMaps
3
+ import React
4
+ import UIKit
5
+
6
+ @objc(SatelliteLayerView)
7
+ public final class SatelliteLayerView: UIView {
8
+
9
+ /// Xcode / device console — grep `[AguaceroWX][SatelliteLayerView]` or `SatelliteLayerView`
10
+ private static func satLog(_ message: String) {
11
+ NSLog("[AguaceroWX][SatelliteLayerView] %@", message)
12
+ }
13
+
14
+ public static let satelliteLayerId = "aguacero-satellite-layer"
15
+ private static let weatherGridLayerId = "aguacero-weather-grid-custom"
16
+
17
+ @objc public var layerInstance: AguaceroSatelliteRenderer!
18
+ @objc public weak var mapView: MapView?
19
+ @objc public var layerId: String = SatelliteLayerView.satelliteLayerId
20
+ @objc public var belowID: String?
21
+
22
+ @objc public weak var bridge: RCTBridge?
23
+
24
+ private var isLayerAdded = false
25
+ private var satReparentScheduled = false
26
+
27
+ @objc public var satelliteLayerAdded: Bool { isLayerAdded }
28
+
29
+ private let decodeQueue = DispatchQueue(label: "com.aguacero.satellite-decode", qos: .utility)
30
+ private var decodeGeneration = 0
31
+ private var lastRunKey: String?
32
+
33
+ private var fetchInFlight = Set<Int64>()
34
+ private let fetchLock = NSLock()
35
+
36
+ private lazy var urlSession: URLSession = {
37
+ let c = URLSessionConfiguration.ephemeral
38
+ c.timeoutIntervalForRequest = 120
39
+ return URLSession(configuration: c)
40
+ }()
41
+
42
+ public override init(frame: CGRect) {
43
+ layerInstance = AguaceroSatelliteRenderer(id: SatelliteLayerView.satelliteLayerId)
44
+ super.init(frame: frame)
45
+ isOpaque = false
46
+ Self.satLog("init(frame:) SatelliteLayerView=\(Unmanaged.passUnretained(self).toOpaque())")
47
+ }
48
+
49
+ required init?(coder: NSCoder) {
50
+ layerInstance = AguaceroSatelliteRenderer(id: SatelliteLayerView.satelliteLayerId)
51
+ super.init(coder: coder)
52
+ isOpaque = false
53
+ Self.satLog("init(coder:) SatelliteLayerView=\(Unmanaged.passUnretained(self).toOpaque())")
54
+ }
55
+
56
+ public override func didMoveToWindow() {
57
+ super.didMoveToWindow()
58
+ if window != nil && !isLayerAdded {
59
+ findMapViewAndAddLayer(in: window!)
60
+ }
61
+ }
62
+
63
+ public override func layoutSubviews() {
64
+ super.layoutSubviews()
65
+ if !isLayerAdded {
66
+ let root = window ?? superview
67
+ if let root {
68
+ findMapViewAndAddLayer(in: root)
69
+ }
70
+ }
71
+ }
72
+
73
+ private func findMapViewAndAddLayer(in view: UIView) {
74
+ if isLayerAdded { return }
75
+ if let map = findMapViewRecursive(view) {
76
+ Self.satLog("findMapView: OK mapView attached, belowID=\(belowID ?? "nil")")
77
+ self.mapView = map
78
+ waitForStyleLoadAndAddLayer()
79
+ } else {
80
+ Self.satLog("findMapView: MapView not found under RNMBX hierarchy yet (will retry from layout/window)")
81
+ }
82
+ }
83
+
84
+ private func findMapViewRecursive(_ view: UIView) -> MapView? {
85
+ let name = NSStringFromClass(type(of: view))
86
+ if name == "RNMBXMapView", view.subviews.count > 0 {
87
+ let first = view.subviews[0]
88
+ let subName = NSStringFromClass(type(of: first))
89
+ if subName.contains("MapboxMaps.MapView") || subName == "MapView" {
90
+ return first as? MapView
91
+ }
92
+ }
93
+ for sub in view.subviews {
94
+ if let found = findMapViewRecursive(sub) {
95
+ return found
96
+ }
97
+ }
98
+ return nil
99
+ }
100
+
101
+ private func waitForStyleLoadAndAddLayer() {
102
+ guard let mapView else { return }
103
+ let anchor = belowID ?? "AML_-_terrain"
104
+ let anchorExists = GridRenderLayerBridge.layerExists(in: mapView, layerId: anchor)
105
+ Self.satLog("waitForStyle: anchor=\(anchor) exists=\(anchorExists) gridLayer=\(GridRenderLayerBridge.layerExists(in: mapView, layerId: Self.weatherGridLayerId))")
106
+ if anchorExists {
107
+ addLayerToMap()
108
+ return
109
+ }
110
+ pollForLayerExistence(anchor: anchor, attempts: 0)
111
+ }
112
+
113
+ private func pollForLayerExistence(anchor: String, attempts: Int) {
114
+ guard let mapView, !isLayerAdded else { return }
115
+ if attempts > 80 {
116
+ Self.satLog("pollAnchor: GAVE UP after ~\(attempts * 100)ms — layer id \"\(anchor)\" never appeared. Satellite custom layer was NOT added. Check Mapbox style / belowID prop.")
117
+ return
118
+ }
119
+ if attempts % 20 == 0, attempts > 0 {
120
+ Self.satLog("pollAnchor: still waiting for \"\(anchor)\" attempt=\(attempts)")
121
+ }
122
+ if GridRenderLayerBridge.layerExists(in: mapView, layerId: anchor) {
123
+ Self.satLog("pollAnchor: anchor \"\(anchor)\" appeared after \(attempts) attempts")
124
+ addLayerToMap()
125
+ return
126
+ }
127
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
128
+ self?.pollForLayerExistence(anchor: anchor, attempts: attempts + 1)
129
+ }
130
+ }
131
+
132
+ private func addLayerToMap() {
133
+ guard let mapView, !isLayerAdded else { return }
134
+ guard let host = layerInstance.getHostWrapper() as? CustomLayerHost else {
135
+ Self.satLog("addLayerToMap: ABORT — CustomLayer host missing")
136
+ return
137
+ }
138
+
139
+ let success = GridRenderLayerBridge.addSatelliteCustomLayer(
140
+ to: mapView,
141
+ layerHost: host,
142
+ layerId: layerId,
143
+ weatherGridLayerId: Self.weatherGridLayerId,
144
+ belowLayerId: belowID
145
+ )
146
+
147
+ if success {
148
+ isLayerAdded = true
149
+ let gridThere = GridRenderLayerBridge.layerExists(in: mapView, layerId: Self.weatherGridLayerId)
150
+ Self.satLog("addLayerToMap: OK layerId=\(layerId) aboveGrid=\(gridThere) belowID=\(belowID ?? "nil")")
151
+ GridRenderLayerBridge.triggerRepaint(on: mapView)
152
+ scheduleReparentAboveGridOnce()
153
+ } else {
154
+ Self.satLog("addLayerToMap: FAILED Mapbox addLayer for layerId=\(layerId) — see GridRenderLayerBridge logs")
155
+ }
156
+ }
157
+
158
+ private func scheduleReparentAboveGridOnce() {
159
+ if satReparentScheduled { return }
160
+ satReparentScheduled = true
161
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
162
+ self?.satReparentScheduled = false
163
+ self?.reparentAboveGridIfNeeded()
164
+ }
165
+ }
166
+
167
+ private func reparentAboveGridIfNeeded() {
168
+ guard let mapView else { return }
169
+ let gridOk = GridRenderLayerBridge.layerExists(in: mapView, layerId: Self.weatherGridLayerId)
170
+ let satOk = GridRenderLayerBridge.layerExists(in: mapView, layerId: layerId)
171
+ guard gridOk, satOk else {
172
+ Self.satLog("reparentAboveGrid: skip grid=\(gridOk) satellite=\(satOk)")
173
+ return
174
+ }
175
+ guard let host = layerInstance.getHostWrapper() as? CustomLayerHost else { return }
176
+
177
+ GridRenderLayerBridge.removeCustomLayer(from: mapView, layerId: layerId)
178
+ _ = GridRenderLayerBridge.addSatelliteCustomLayer(
179
+ to: mapView,
180
+ layerHost: host,
181
+ layerId: layerId,
182
+ weatherGridLayerId: Self.weatherGridLayerId,
183
+ belowLayerId: belowID
184
+ )
185
+ GridRenderLayerBridge.triggerRepaint(on: mapView)
186
+ }
187
+
188
+ private struct FrameClass {
189
+ let trueColor: Bool
190
+ let colormapKind: String
191
+ let useColormap: Bool
192
+ }
193
+
194
+ private static func classifyShaderFileName(_ shaderFileName: String?) -> FrameClass {
195
+ let fn = shaderFileName ?? ""
196
+ let isTrueColor =
197
+ fn.contains("_truecolor_")
198
+ || fn.contains("_geocolor_")
199
+ || fn.contains("_firetemperature_")
200
+ || fn.contains("_dust_")
201
+ || fn.contains("_simplewatervapor_")
202
+ || fn.contains("_ntmicro_")
203
+ || fn.contains("_daycloudphase_")
204
+ || fn.contains("_daylandcloudfire_")
205
+ || fn.contains("_airmass_")
206
+ || fn.contains("_sandwich_")
207
+
208
+ var colormapType = "none"
209
+ if fn.contains("C13") || fn.contains("C14") || fn.contains("C15") || fn.contains("C16") {
210
+ colormapType = "ir"
211
+ } else if fn.contains("C08") || fn.contains("C09") || fn.contains("C10") {
212
+ colormapType = "wv"
213
+ }
214
+
215
+ let useColormap = !isTrueColor && colormapType != "none"
216
+ return FrameClass(trueColor: isTrueColor, colormapKind: colormapType, useColormap: useColormap)
217
+ }
218
+
219
+ /// Match `WeatherLayerManager` grid `buildGridFrameProcessOptions`: authoritative `apiKey` in the query string
220
+ /// plus `x-api-key` on the request (same pattern as `AguaceroCore` grid / NEXRAD CDN).
221
+ private static func satelliteFetchURL(from url: URL, apiKey: String) -> URL {
222
+ guard var c = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }
223
+ var q = c.queryItems ?? []
224
+ q.removeAll { $0.name == "apiKey" }
225
+ q.append(URLQueryItem(name: "apiKey", value: apiKey))
226
+ if !q.contains(where: { $0.name == "userId" }) {
227
+ q.append(URLQueryItem(name: "userId", value: "sdk-user"))
228
+ }
229
+ c.queryItems = q
230
+ return c.url ?? url
231
+ }
232
+
233
+ @objc public func clearSatellite() {
234
+ Self.satLog("clearSatellite: bump decodeGeneration (was \(decodeGeneration)) → \(decodeGeneration + 1), cancel in-flight fetches")
235
+ decodeGeneration += 1
236
+ fetchLock.lock()
237
+ fetchInFlight.removeAll()
238
+ fetchLock.unlock()
239
+ layerInstance.clearAll()
240
+ if let mapView {
241
+ GridRenderLayerBridge.triggerRepaint(on: mapView)
242
+ }
243
+ }
244
+
245
+ @objc public func syncFromJson(_ json: String?) {
246
+ guard let json, !json.isEmpty,
247
+ let data = json.data(using: .utf8),
248
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
249
+ else {
250
+ Self.satLog("syncFromJson: SKIP empty or invalid JSON (nil=\(json == nil) len=\(json?.count ?? 0))")
251
+ return
252
+ }
253
+
254
+ let runKey = (obj["runKey"] as? String) ?? ""
255
+ if !runKey.isEmpty, runKey != lastRunKey {
256
+ Self.satLog("syncFromJson: new runKey=\(runKey) (invalidate caches + bump generation)")
257
+ lastRunKey = runKey
258
+ decodeGeneration += 1
259
+ fetchLock.lock()
260
+ fetchInFlight.removeAll()
261
+ fetchLock.unlock()
262
+ layerInstance.clearAll()
263
+ }
264
+
265
+ layerInstance.setVisible((obj["visible"] as? Bool) ?? true)
266
+ layerInstance.setOpacity(Float((obj["opacity"] as? Double) ?? 1.0))
267
+ layerInstance.setFillSmoothing(obj["fillSmoothing"] as? Int ?? 0)
268
+
269
+ if let tu = obj["targetUnix"] as? Int64 {
270
+ layerInstance.setActiveUnix(NSNumber(value: tu))
271
+ } else if let tu = obj["targetUnix"] as? Int {
272
+ layerInstance.setActiveUnix(NSNumber(value: Int64(tu)))
273
+ } else if let tu = obj["targetUnix"] as? Double {
274
+ layerInstance.setActiveUnix(NSNumber(value: Int64(tu)))
275
+ }
276
+
277
+ let apiKey = (obj["apiKey"] as? String) ?? ""
278
+ let bundleId = (obj["bundleId"] as? String) ?? ""
279
+ var gridSiteOrigin = (obj["gridRequestSiteOrigin"] as? String) ?? ""
280
+ gridSiteOrigin = gridSiteOrigin.trimmingCharacters(in: .whitespacesAndNewlines)
281
+ while gridSiteOrigin.hasSuffix("/") {
282
+ gridSiteOrigin.removeLast()
283
+ }
284
+ let frames = obj["frames"] as? [[String: Any]]
285
+ let frameCount = frames?.count ?? 0
286
+ let tgt = obj["targetUnix"]
287
+ Self.satLog(
288
+ "syncFromJson: runKey=\(runKey.isEmpty ? "(none)" : runKey) frames=\(frameCount) apiKey.len=\(apiKey.count) bundleId.len=\(bundleId.count) gridOrigin.len=\(gridSiteOrigin.count) visible=\((obj["visible"] as? Bool) ?? true) targetUnix=\(String(describing: tgt)) layerOnMap=\(isLayerAdded)"
289
+ )
290
+
291
+ guard let frames, !frames.isEmpty, !apiKey.isEmpty else {
292
+ if frames == nil {
293
+ Self.satLog("syncFromJson: EARLY EXIT — \"frames\" missing or not an array (no KTX URLs to fetch)")
294
+ } else if frames?.isEmpty == true {
295
+ Self.satLog("syncFromJson: EARLY EXIT — frames[] empty (JS timeline map likely not loaded yet; nothing to fetch)")
296
+ } else {
297
+ Self.satLog("syncFromJson: EARLY EXIT — apiKey empty (cannot authorize satellite CDN)")
298
+ }
299
+ if let mapView {
300
+ GridRenderLayerBridge.triggerRepaint(on: mapView)
301
+ }
302
+ return
303
+ }
304
+
305
+ let gen = decodeGeneration
306
+
307
+ for (idx, item) in frames.enumerated() {
308
+ guard let unixNum = item["unix"],
309
+ let urlStr = item["url"] as? String,
310
+ let url = URL(string: urlStr)
311
+ else { continue }
312
+ let unix: Int64
313
+ if let i = unixNum as? Int64 {
314
+ unix = i
315
+ } else if let i = unixNum as? Int {
316
+ unix = Int64(i)
317
+ } else if let d = unixNum as? Double {
318
+ unix = Int64(d)
319
+ } else {
320
+ continue
321
+ }
322
+ let shaderFileName = item["shaderFileName"] as? String ?? ""
323
+ if idx < 5 {
324
+ Self.satLog("scheduleFetch[\(idx)]: unix=\(unix) host=\(url.host ?? "?") shader=\(shaderFileName.prefix(48))…")
325
+ }
326
+ scheduleFetchIfNeeded(
327
+ gen: gen,
328
+ unix: unix,
329
+ url: url,
330
+ shaderFileName: shaderFileName,
331
+ apiKey: apiKey,
332
+ bundleId: bundleId,
333
+ gridRequestSiteOrigin: gridSiteOrigin
334
+ )
335
+ }
336
+ if frames.count > 5 {
337
+ Self.satLog("scheduleFetch: … and \(frames.count - 5) more frame(s) not listed")
338
+ }
339
+
340
+ if let mapView {
341
+ GridRenderLayerBridge.triggerRepaint(on: mapView)
342
+ }
343
+ }
344
+
345
+ private func scheduleFetchIfNeeded(
346
+ gen: Int,
347
+ unix: Int64,
348
+ url: URL,
349
+ shaderFileName: String,
350
+ apiKey: String,
351
+ bundleId: String,
352
+ gridRequestSiteOrigin: String
353
+ ) {
354
+ if layerInstance.isFrameCached(unix) {
355
+ Self.satLog("fetch: unix=\(unix) already cached — skip download")
356
+ return
357
+ }
358
+
359
+ fetchLock.lock()
360
+ if fetchInFlight.contains(unix) {
361
+ fetchLock.unlock()
362
+ Self.satLog("fetch: unix=\(unix) already in flight — skip duplicate")
363
+ return
364
+ }
365
+ fetchInFlight.insert(unix)
366
+ fetchLock.unlock()
367
+
368
+ decodeQueue.async { [weak self] in
369
+ guard let self else { return }
370
+ if gen != self.decodeGeneration {
371
+ Self.satLog("fetch: unix=\(unix) aborted — decodeGeneration changed (stale run)")
372
+ self.fetchLock.lock()
373
+ self.fetchInFlight.remove(unix)
374
+ self.fetchLock.unlock()
375
+ return
376
+ }
377
+
378
+ let fetchURL = Self.satelliteFetchURL(from: url, apiKey: apiKey)
379
+ var req = URLRequest(url: fetchURL)
380
+ req.setValue(apiKey, forHTTPHeaderField: "x-api-key")
381
+ if !bundleId.isEmpty {
382
+ req.setValue(bundleId, forHTTPHeaderField: "x-app-identifier")
383
+ }
384
+ // Match WeatherFrameProcessorModule + AguaceroCore grid fetch (CloudFront returns 403 without Origin on RN).
385
+ var gridOrigin = gridRequestSiteOrigin.trimmingCharacters(in: .whitespacesAndNewlines)
386
+ if gridOrigin.isEmpty {
387
+ gridOrigin = "https://localhost"
388
+ }
389
+ while gridOrigin.hasSuffix("/") {
390
+ gridOrigin.removeLast()
391
+ }
392
+ if !gridOrigin.isEmpty {
393
+ req.setValue(gridOrigin, forHTTPHeaderField: "Origin")
394
+ req.setValue("\(gridOrigin)/", forHTTPHeaderField: "Referer")
395
+ }
396
+
397
+ let sem = DispatchSemaphore(value: 0)
398
+ var outData: Data?
399
+ var httpStatus = -1
400
+ let task = self.urlSession.dataTask(with: req) { data, response, _ in
401
+ if let http = response as? HTTPURLResponse {
402
+ httpStatus = http.statusCode
403
+ }
404
+ outData = data
405
+ sem.signal()
406
+ }
407
+ task.resume()
408
+ sem.wait()
409
+
410
+ if gen != self.decodeGeneration {
411
+ Self.satLog("fetch: unix=\(unix) post-network abort — generation bumped")
412
+ DispatchQueue.main.async {
413
+ self.fetchLock.lock()
414
+ self.fetchInFlight.remove(unix)
415
+ self.fetchLock.unlock()
416
+ }
417
+ return
418
+ }
419
+
420
+ guard httpStatus == 200, let ktxBytes = outData, !ktxBytes.isEmpty else {
421
+ let bodyLen = outData?.count ?? 0
422
+ Self.satLog("fetch: unix=\(unix) HTTP \(httpStatus) bodyBytes=\(bodyLen) — expected 200 + non-empty KTX2 body")
423
+ DispatchQueue.main.async {
424
+ self.fetchLock.lock()
425
+ self.fetchInFlight.remove(unix)
426
+ self.fetchLock.unlock()
427
+ }
428
+ return
429
+ }
430
+
431
+ if gen != self.decodeGeneration {
432
+ DispatchQueue.main.async {
433
+ self.fetchLock.lock()
434
+ self.fetchInFlight.remove(unix)
435
+ self.fetchLock.unlock()
436
+ }
437
+ return
438
+ }
439
+
440
+ let packedOpt = AguaceroSatelliteKtxBridge.transcodeKtx2(ktxBytes)
441
+ let parsedOpt = packedOpt.flatMap { AguaceroSatelliteRenderer.parsePacked($0) }
442
+ let meshOpt: SatelliteMeshBuilder.MeshResult? =
443
+ parsedOpt.flatMap { p in
444
+ SatelliteMeshBuilder.buildMesh(shaderFileName: shaderFileName, width: p.width, height: p.height)
445
+ }
446
+ guard packedOpt != nil,
447
+ let parsed = parsedOpt,
448
+ let mesh = meshOpt
449
+ else {
450
+ Self.satLog(
451
+ "fetch: unix=\(unix) DECODE FAIL — ktxIn=\(ktxBytes.count)B packed=\(packedOpt?.count ?? -1) parsed=\(parsedOpt != nil) mesh=\(meshOpt != nil) shader=\(shaderFileName.prefix(40))"
452
+ )
453
+ DispatchQueue.main.async {
454
+ self.fetchLock.lock()
455
+ self.fetchInFlight.remove(unix)
456
+ self.fetchLock.unlock()
457
+ }
458
+ return
459
+ }
460
+
461
+ let fc = Self.classifyShaderFileName(shaderFileName)
462
+ let pending = AguaceroSatelliteRenderer.PendingGpuUpload(
463
+ unix: unix,
464
+ mesh: mesh,
465
+ decoded: parsed,
466
+ trueColor: fc.trueColor,
467
+ colormapKind: fc.colormapKind,
468
+ useColormap: fc.useColormap
469
+ )
470
+
471
+ DispatchQueue.main.async {
472
+ self.fetchLock.lock()
473
+ self.fetchInFlight.remove(unix)
474
+ self.fetchLock.unlock()
475
+
476
+ if gen != self.decodeGeneration {
477
+ return
478
+ }
479
+ if !self.isLayerAdded {
480
+ if let w = self.window {
481
+ self.findMapViewAndAddLayer(in: w)
482
+ } else if let sv = self.superview {
483
+ self.findMapViewAndAddLayer(in: sv)
484
+ }
485
+ }
486
+ Self.satLog(
487
+ "GPU queue: unix=\(unix) \(parsed.width)x\(parsed.height) tc=\(fc.trueColor) cmap=\(fc.colormapKind) layerOnMap=\(self.isLayerAdded)"
488
+ )
489
+ self.layerInstance.queueGpuUpload(pending)
490
+ if let mv = self.mapView {
491
+ GridRenderLayerBridge.triggerRepaint(on: mv)
492
+ }
493
+ }
494
+ }
495
+ }
496
+
497
+ @objc public func activateSatelliteCachedUnix(_ unix: Int64) {
498
+ Self.satLog("activateSatelliteCachedUnix: unix=\(unix) cached=\(layerInstance.isFrameCached(unix))")
499
+ layerInstance.activateCachedUnix(unix)
500
+ if let mapView {
501
+ GridRenderLayerBridge.triggerRepaint(on: mapView)
502
+ }
503
+ }
504
+
505
+ @objc public func updateStyleFromJson(_ json: String?) {
506
+ guard let json, !json.isEmpty,
507
+ let data = json.data(using: .utf8),
508
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
509
+ else { return }
510
+ layerInstance.setVisible((obj["visible"] as? Bool) ?? true)
511
+ layerInstance.setOpacity(Float((obj["opacity"] as? Double) ?? 1.0))
512
+ layerInstance.setFillSmoothing(obj["fillSmoothing"] as? Int ?? 0)
513
+ if let mapView {
514
+ GridRenderLayerBridge.triggerRepaint(on: mapView)
515
+ }
516
+ }
517
+ }