@aguacerowx/react-native 0.0.29 → 0.0.30

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 (39) hide show
  1. package/ios/GridRenderLayer.swift +60 -117
  2. package/ios/GridRenderLayerBridge.swift +0 -1
  3. package/ios/GridRenderLayerView.m +1 -41
  4. package/ios/InspectorModule.swift +2 -2
  5. package/lib/commonjs/ios/GridRenderLayer.swift +60 -117
  6. package/lib/commonjs/ios/GridRenderLayerBridge.swift +0 -1
  7. package/lib/commonjs/ios/GridRenderLayerView.m +1 -41
  8. package/lib/commonjs/ios/InspectorModule.swift +2 -2
  9. package/lib/commonjs/package.json +1 -1
  10. package/lib/commonjs/src/GridRenderLayer.js +0 -2
  11. package/lib/commonjs/src/GridRenderLayer.js.map +1 -1
  12. package/lib/commonjs/src/WeatherLayerManager.js +125 -77
  13. package/lib/commonjs/src/WeatherLayerManager.js.map +1 -1
  14. package/lib/module/ios/GridRenderLayer.swift +60 -117
  15. package/lib/module/ios/GridRenderLayerBridge.swift +0 -1
  16. package/lib/module/ios/GridRenderLayerView.m +1 -41
  17. package/lib/module/ios/InspectorModule.swift +2 -2
  18. package/lib/module/lib/commonjs/android/build.gradle +108 -0
  19. package/lib/module/lib/commonjs/ios/AguaceroPackage.m +19 -0
  20. package/lib/module/lib/commonjs/ios/FragmentUniforms.swift +16 -0
  21. package/lib/module/lib/commonjs/ios/GridRenderLayer.swift +986 -0
  22. package/lib/module/lib/commonjs/ios/GridRenderLayerManager.mm +158 -0
  23. package/lib/module/lib/commonjs/ios/GridRenderLayerView.m +217 -0
  24. package/lib/module/lib/commonjs/ios/compiled-shaders/Shaders-device.metallib +0 -0
  25. package/lib/module/lib/commonjs/package.json +1 -1
  26. package/lib/module/lib/commonjs/src/GridRenderLayer.js +0 -2
  27. package/lib/module/lib/commonjs/src/GridRenderLayer.js.map +1 -1
  28. package/lib/module/lib/commonjs/src/WeatherLayerManager.js +125 -77
  29. package/lib/module/lib/commonjs/src/WeatherLayerManager.js.map +1 -1
  30. package/lib/module/package.json +1 -1
  31. package/lib/module/src/GridRenderLayer.js +0 -2
  32. package/lib/module/src/GridRenderLayer.js.map +1 -1
  33. package/lib/module/src/WeatherLayerManager.js +125 -77
  34. package/lib/module/src/WeatherLayerManager.js.map +1 -1
  35. package/lib/typescript/src/GridRenderLayer.d.ts.map +1 -1
  36. package/lib/typescript/src/WeatherLayerManager.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/GridRenderLayer.js +0 -2
  39. package/src/WeatherLayerManager.js +135 -87
@@ -0,0 +1,986 @@
1
+ import Foundation
2
+ import MapboxMaps
3
+ import Metal
4
+ import libzstd
5
+ import simd
6
+
7
+ // MARK: - Swift-only wrapper for CustomLayerHost conformance
8
+ @objc internal final class GridRenderLayerHost: NSObject, CustomLayerHost {
9
+ weak var layer: GridRenderLayer?
10
+
11
+ init(layer: GridRenderLayer) {
12
+ self.layer = layer
13
+ super.init()
14
+ }
15
+
16
+ func renderingWillStart(_ metalDevice: MTLDevice, colorPixelFormat: UInt, depthStencilPixelFormat: UInt) {
17
+ layer?.internalRenderingWillStart(metalDevice, colorPixelFormat: colorPixelFormat, depthStencilPixelFormat: depthStencilPixelFormat)
18
+ }
19
+
20
+ func render(_ parameters: CustomLayerRenderParameters, mtlCommandBuffer: MTLCommandBuffer, mtlRenderPassDescriptor: MTLRenderPassDescriptor) {
21
+ layer?.internalRender(parameters, mtlCommandBuffer: mtlCommandBuffer, mtlRenderPassDescriptor: mtlRenderPassDescriptor)
22
+ }
23
+
24
+ func renderingWillEnd() {
25
+ layer?.internalRenderingWillEnd()
26
+ }
27
+ }
28
+
29
+ @objc(GridRenderLayer)
30
+ public class GridRenderLayer: NSObject {
31
+
32
+ // MARK: - Properties
33
+
34
+ public var id: String
35
+ @objc internal var hostWrapper: GridRenderLayerHost!
36
+
37
+ // Metal objects
38
+ private var device: MTLDevice!
39
+ private var commandQueue: MTLCommandQueue!
40
+ private var pipelineState: MTLRenderPipelineState!
41
+ private var vertexBuffer: MTLBuffer?
42
+ private var indexBuffer: MTLBuffer?
43
+ private var dataTexture: MTLTexture?
44
+ private var colormapTexture: MTLTexture?
45
+ private var dataSamplerState: MTLSamplerState!
46
+ private var colormapSamplerState: MTLSamplerState!
47
+ private var isDataSamplerLinear: Bool = false
48
+
49
+ private var pendingColormapUpdate: String?
50
+ private var pendingDataUpdate: (data: String, nx: NSNumber, ny: NSNumber, scale: NSNumber, offset: NSNumber, missing: NSNumber, scaleType: String)?
51
+ private var pendingGeometryUpdate: (corners: [String: Any], gridDef: [String: Any])?
52
+
53
+ // Layer state
54
+ private var indexCount: Int = 0
55
+ private var uniforms = FragmentUniforms(
56
+ opacity: 1.0,
57
+ dataRange: SIMD2<Float>(0.0, 1.0),
58
+ scale: 1.0,
59
+ offset: 0.0,
60
+ missingQuantized: 127.0,
61
+ textureSize: SIMD2<Float>(0.0, 0.0),
62
+ smoothing: 1,
63
+ scaleType: 0,
64
+ isPtype: 0,
65
+ isMRMS: 0
66
+ )
67
+ private var isVisible = false
68
+ private var pendingActiveFrameKey: String?
69
+ private let inspectorCache = InspectorDataCache.shared
70
+ private struct FrameMetadata {
71
+ let scale: Float
72
+ let offset: Float
73
+ let missing: Float
74
+ let scaleType: Int
75
+ let nx: Float
76
+ let ny: Float
77
+ let filePath: String
78
+ let originalScale: Float
79
+ let originalOffset: Float
80
+ }
81
+ private var frameCache: [String: FrameMetadata] = [:]
82
+ private let frameProcessingQueue = DispatchQueue(label: "com.aguacero.frame-processing", qos: .userInitiated, attributes: .concurrent)
83
+ private let semaphore = DispatchSemaphore(value: 8)
84
+
85
+ private struct VertexInfo {
86
+ var mercX: Float
87
+ var mercY: Float
88
+ var texU: Float
89
+ var texV: Float
90
+ var index: UInt16
91
+ }
92
+ @objc
93
+ public init(id: String) {
94
+ self.id = id
95
+ super.init()
96
+ self.hostWrapper = GridRenderLayerHost(layer: self)
97
+ }
98
+
99
+ @objc public func getHostWrapper() -> Any {
100
+ return self.hostWrapper as Any
101
+ }
102
+
103
+ // MARK: - Public Methods (called from Manager)
104
+
105
+ @objc public func setOpacity(value: Float) {
106
+ self.uniforms.opacity = value
107
+ NotificationCenter.default.post(name: NSNotification.Name("TriggerMapRepaint"), object: nil)
108
+ }
109
+
110
+ @objc public func setDataRange(value: [NSNumber]) {
111
+ self.uniforms.dataRange = SIMD2<Float>(value[0].floatValue, value[1].floatValue)
112
+ NotificationCenter.default.post(name: NSNotification.Name("TriggerMapRepaint"), object: nil)
113
+ }
114
+
115
+ @objc public func setSmoothing(value: Bool) {
116
+ self.uniforms.smoothing = value ? 1 : 0
117
+ NotificationCenter.default.post(name: NSNotification.Name("TriggerMapRepaint"), object: nil)
118
+ }
119
+
120
+ @objc(setIsMRMSWithIsMRMS:)
121
+ public func setIsMRMS(isMRMS: Bool) {
122
+ self.uniforms.isMRMS = isMRMS ? 1 : 0
123
+ NotificationCenter.default.post(name: NSNotification.Name("TriggerMapRepaint"), object: nil)
124
+ }
125
+
126
+ // ADD THIS METHOD
127
+ @objc(setVariableWithVariable:)
128
+ public func setVariable(variable: String) {
129
+ let isPtypeVar = (variable == "ptypeRefl" || variable == "ptypeRate")
130
+ self.uniforms.isPtype = isPtypeVar ? 1 : 0
131
+ NotificationCenter.default.post(name: NSNotification.Name("TriggerMapRepaint"), object: nil)
132
+ }
133
+
134
+ @objc public func clear() {
135
+ self.isVisible = false
136
+ self.inspectorCache.clear()
137
+ }
138
+
139
+ @objc public func updateDataParameters(scale: NSNumber, offset: NSNumber, missing: NSNumber, scaleType: NSNumber) { // ADD scaleType parameter
140
+ // Update both the inspector cache AND the rendering uniforms
141
+ self.uniforms.scale = scale.floatValue
142
+ self.uniforms.offset = offset.floatValue
143
+ self.uniforms.missingQuantized = missing.floatValue
144
+ self.uniforms.scaleType = Int32(scaleType.intValue) // ADD THIS
145
+
146
+ inspectorCache.update(
147
+ data: inspectorCache.lastDecompressedData,
148
+ nx: inspectorCache.nx,
149
+ ny: inspectorCache.ny,
150
+ scale: scale.floatValue,
151
+ offset: offset.floatValue,
152
+ missing: missing.floatValue,
153
+ scaleType: scaleType.intValue // ADD THIS
154
+ )
155
+ }
156
+
157
+ @objc public func updateColormapTexture(colormapAsBase64: String) {
158
+ guard let device = self.device else {
159
+ pendingColormapUpdate = colormapAsBase64
160
+ return
161
+ }
162
+ guard let data = Data(base64Encoded: colormapAsBase64) else { return }
163
+ let textureDescriptor = MTLTextureDescriptor()
164
+ textureDescriptor.pixelFormat = .rgba8Unorm
165
+ textureDescriptor.width = 256
166
+ textureDescriptor.height = 1
167
+ textureDescriptor.usage = .shaderRead
168
+ guard let texture = device.makeTexture(descriptor: textureDescriptor) else { return }
169
+ texture.replace(
170
+ region: MTLRegionMake2D(0, 0, 256, 1),
171
+ mipmapLevel: 0,
172
+ withBytes: (data as NSData).bytes,
173
+ bytesPerRow: 256 * 4
174
+ )
175
+ self.colormapTexture = texture
176
+ }
177
+
178
+ @objc(clearGpuCache)
179
+ public func clearGpuCache() {
180
+ frameProcessingQueue.async { [weak self] in
181
+ self?.frameCache.removeAll()
182
+ }
183
+ }
184
+
185
+ private func processRawData(fileData: Data) -> Data? {
186
+ guard let decompressedDeltas = self.decompressZstd(data: fileData) else {
187
+ print("❌ [GridRenderLayer] Failed to decompress zstd data")
188
+ return nil
189
+ }
190
+ let reconstructedData = self.reconstructData(decompressedDeltas: decompressedDeltas)
191
+ let finalTextureBytes = self.transformData(finalData: reconstructedData)
192
+ return finalTextureBytes
193
+ }
194
+
195
+ private func createTextureFromBytes(bytes: Data, nx: Int, ny: Int) -> MTLTexture? {
196
+ guard let device = self.device else { return nil }
197
+ let textureDescriptor = MTLTextureDescriptor()
198
+ textureDescriptor.pixelFormat = .r8Unorm
199
+ textureDescriptor.width = nx
200
+ textureDescriptor.height = ny
201
+ textureDescriptor.usage = .shaderRead
202
+
203
+ guard let texture = device.makeTexture(descriptor: textureDescriptor) else { return nil }
204
+ texture.replace(
205
+ region: MTLRegionMake2D(0, 0, nx, ny),
206
+ mipmapLevel: 0,
207
+ withBytes: (bytes as NSData).bytes,
208
+ bytesPerRow: nx
209
+ )
210
+ return texture
211
+ }
212
+
213
+ private func updateInspectorCache(filePath: String, nx: Int, ny: Int, scale: Float, offset: Float, missing: Float, scaleType: Int) {
214
+ // This is now called from frameProcessingQueue, so we can do the work synchronously
215
+ guard let fileData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
216
+ print("❌ [Inspector] FATAL: Failed to read file data from path.")
217
+ self.inspectorCache.clear()
218
+ return
219
+ }
220
+
221
+ guard let decompressedDeltas = self.decompressZstd(data: fileData) else {
222
+ print("❌ [Inspector] FATAL: Failed to decompress Zstd data.")
223
+ self.inspectorCache.clear()
224
+ return
225
+ }
226
+
227
+ let reconstructedData = self.reconstructData(decompressedDeltas: decompressedDeltas)
228
+
229
+ self.inspectorCache.update(
230
+ data: reconstructedData,
231
+ nx: nx,
232
+ ny: ny,
233
+ scale: scale,
234
+ offset: offset,
235
+ missing: missing,
236
+ scaleType: scaleType
237
+ )
238
+ }
239
+
240
+ @objc(setActiveFrameWithCacheKey:)
241
+ public func setActiveFrame(cacheKey: String) {
242
+ if let frame = frameCache[cacheKey] {
243
+ frameProcessingQueue.async { [weak self] in
244
+ guard let self = self else { return }
245
+
246
+ guard let fileData = try? Data(contentsOf: URL(fileURLWithPath: frame.filePath)),
247
+ let finalTextureBytes = self.processRawData(fileData: fileData)
248
+ else {
249
+ print("❌ [GridRenderLayer] Failed to load texture data for active frame")
250
+ return
251
+ }
252
+
253
+ DispatchQueue.main.async { [weak self] in
254
+ guard let self = self,
255
+ let texture = self.createTextureFromBytes(bytes: finalTextureBytes, nx: Int(frame.nx), ny: Int(frame.ny))
256
+ else { return }
257
+
258
+ self.dataTexture = texture
259
+ self.uniforms.scale = frame.scale
260
+ self.uniforms.offset = frame.offset
261
+ self.uniforms.missingQuantized = frame.missing
262
+ self.uniforms.textureSize = SIMD2<Float>(frame.nx, frame.ny)
263
+ self.uniforms.scaleType = Int32(frame.scaleType)
264
+
265
+ self.isVisible = true
266
+ self.pendingActiveFrameKey = nil
267
+ NotificationCenter.default.post(name: NSNotification.Name("TriggerMapRepaint"), object: nil)
268
+
269
+ // Update inspector cache
270
+ self.frameProcessingQueue.async { [weak self] in
271
+ self?.updateInspectorCache(
272
+ filePath: frame.filePath,
273
+ nx: Int(frame.nx),
274
+ ny: Int(frame.ny),
275
+ scale: frame.scale,
276
+ offset: frame.offset,
277
+ missing: frame.missing,
278
+ scaleType: frame.scaleType
279
+ )
280
+ }
281
+ }
282
+ }
283
+
284
+ } else {
285
+ print("⚠️ [GridRenderLayer] setActiveFrame cache MISS for key: \(cacheKey). Will apply when primed.")
286
+ self.pendingActiveFrameKey = cacheKey
287
+ self.isVisible = false
288
+ }
289
+ }
290
+
291
+ @objc(primeGpuCacheWithFrameInfo:)
292
+ public func primeGpuCache(frameInfo: [String: [String: Any]]) {
293
+ let group = DispatchGroup()
294
+
295
+ for (cacheKey, info) in frameInfo {
296
+ group.enter()
297
+
298
+ frameProcessingQueue.async { [weak self] in
299
+ guard let self = self else {
300
+ group.leave()
301
+ return
302
+ }
303
+
304
+ self.semaphore.wait()
305
+ defer {
306
+ self.semaphore.signal()
307
+ group.leave()
308
+ }
309
+
310
+ if self.frameCache[cacheKey] != nil { return }
311
+
312
+ guard let filePath = info["filePath"] as? String,
313
+ let nx = info["nx"] as? NSNumber,
314
+ let ny = info["ny"] as? NSNumber,
315
+ let scale = info["scale"] as? NSNumber,
316
+ let offset = info["offset"] as? NSNumber,
317
+ let missing = info["missing"] as? NSNumber,
318
+ let scaleTypeStr = info["scaleType"] as? String,
319
+ let originalScale = info["originalScale"] as? NSNumber,
320
+ let originalOffset = info["originalOffset"] as? NSNumber
321
+ else {
322
+ print("❌ [GridRenderLayer] Skipping prime for \(cacheKey), missing data.")
323
+ return
324
+ }
325
+
326
+ let metadata = FrameMetadata(
327
+ scale: scale.floatValue,
328
+ offset: offset.floatValue,
329
+ missing: missing.floatValue,
330
+ scaleType: (scaleTypeStr == "sqrt") ? 1 : 0,
331
+ nx: nx.floatValue,
332
+ ny: ny.floatValue,
333
+ filePath: filePath,
334
+ originalScale: originalScale.floatValue,
335
+ originalOffset: originalOffset.floatValue
336
+ )
337
+ self.frameCache[cacheKey] = metadata
338
+
339
+ if let pendingKey = self.pendingActiveFrameKey, pendingKey == cacheKey {
340
+ DispatchQueue.main.async {
341
+ self.setActiveFrame(cacheKey: pendingKey)
342
+ }
343
+ }
344
+ }
345
+ }
346
+ }
347
+
348
+ @objc public func updateDataTexture(data: String, nx: NSNumber, ny: NSNumber, scale: NSNumber, offset: NSNumber, missing: NSNumber, scaleType: String) {
349
+ let filePath = data
350
+
351
+ frameProcessingQueue.async { [weak self] in
352
+ guard let self = self else { return }
353
+
354
+ guard let fileData = try? Data(contentsOf: URL(fileURLWithPath: filePath)),
355
+ let finalTextureBytes = self.processRawData(fileData: fileData)
356
+ else {
357
+ print("❌ [GridRenderLayer] FAST LANE: Failed to process initial frame data.")
358
+ return
359
+ }
360
+
361
+ DispatchQueue.main.async { [weak self] in
362
+ guard let self = self, let texture = self.createTextureFromBytes(bytes: finalTextureBytes, nx: nx.intValue, ny: ny.intValue) else {
363
+ print("❌ [GridRenderLayer] FAST LANE: Failed to create texture")
364
+ return
365
+ }
366
+
367
+ self.dataTexture = texture
368
+ self.uniforms.scale = scale.floatValue
369
+ self.uniforms.offset = offset.floatValue
370
+ self.uniforms.missingQuantized = missing.floatValue
371
+ self.uniforms.scaleType = Int32((scaleType == "sqrt") ? 1 : 0)
372
+ self.uniforms.textureSize = SIMD2<Float>(nx.floatValue, ny.floatValue)
373
+
374
+ self.isVisible = true
375
+ NotificationCenter.default.post(name: NSNotification.Name("TriggerMapRepaint"), object: nil)
376
+
377
+ self.updateInspectorCache(
378
+ filePath: filePath,
379
+ nx: nx.intValue,
380
+ ny: ny.intValue,
381
+ scale: scale.floatValue,
382
+ offset: offset.floatValue,
383
+ missing: missing.floatValue,
384
+ scaleType: (scaleType == "sqrt") ? 1 : 0
385
+ )
386
+ }
387
+ }
388
+ }
389
+
390
+ @objc public func updateGeometry(corners: [String: Any], gridDef: [String: Any]) {
391
+ guard self.device != nil else {
392
+ print("⚠️ [GridRenderLayer] Device not ready yet, storing geometry for later")
393
+ pendingGeometryUpdate = (corners: corners, gridDef: gridDef)
394
+ return
395
+ }
396
+
397
+ DispatchQueue.global(qos: .userInitiated).async {
398
+
399
+ var vertices: [Float] = []
400
+ var indices: [UInt16] = []
401
+
402
+ self.generateGeometryData(gridDef: gridDef, vertices: &vertices, indices: &indices)
403
+
404
+ if vertices.isEmpty || indices.isEmpty {
405
+ print("❌ [GridRenderLayer] No geometry generated")
406
+ return
407
+ }
408
+
409
+ self.indexCount = indices.count
410
+
411
+ DispatchQueue.main.async {
412
+ self.vertexBuffer = self.device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Float>.size, options: [])
413
+ self.indexBuffer = self.device.makeBuffer(bytes: indices, length: indices.count * MemoryLayout<UInt16>.size, options: [])
414
+
415
+ NotificationCenter.default.post(name: NSNotification.Name("TriggerMapRepaint"), object: nil)
416
+ }
417
+ }
418
+ }
419
+
420
+ private func decompressZstd(data: Data) -> Data? {
421
+ let decompressedSize = data.withUnsafeBytes { ptr in
422
+ ZSTD_getFrameContentSize(ptr.baseAddress, data.count)
423
+ }
424
+
425
+ guard Int64(bitPattern: decompressedSize) > 0 else {
426
+ print("❌ [GridRenderLayer] Could not determine decompressed size")
427
+ return nil
428
+ }
429
+
430
+ var decompressedData = Data(count: Int(decompressedSize))
431
+
432
+ let result = decompressedData.withUnsafeMutableBytes { decompressedPtr -> Int in
433
+ data.withUnsafeBytes { compressedPtr -> Int in
434
+ let returnValue = ZSTD_decompress(
435
+ decompressedPtr.baseAddress,
436
+ Int(decompressedSize),
437
+ compressedPtr.baseAddress,
438
+ data.count
439
+ )
440
+ return Int(returnValue)
441
+ }
442
+ }
443
+
444
+ guard result > 0 && result == decompressedData.count else {
445
+ if result > 0 {
446
+ let size_t_result = size_t(result)
447
+ if let errorName = ZSTD_getErrorName(size_t_result) {
448
+ let errorString = String(cString: errorName)
449
+ print("❌ [GridRenderLayer] Zstd decompression failed: \(errorString)")
450
+ }
451
+ }
452
+ return nil
453
+ }
454
+ return decompressedData
455
+ }
456
+
457
+ // MARK: - Private Helper Methods
458
+
459
+ private func reconstructData(decompressedDeltas: Data) -> Data {
460
+ guard !decompressedDeltas.isEmpty else { return Data() }
461
+ var reconstructedData = Data(count: decompressedDeltas.count)
462
+ reconstructedData[0] = decompressedDeltas[0]
463
+ for i in 1..<decompressedDeltas.count {
464
+ reconstructedData[i] = UInt8(Int8(bitPattern: reconstructedData[i-1]) &+ Int8(bitPattern: decompressedDeltas[i]))
465
+ }
466
+ return reconstructedData
467
+ }
468
+
469
+ private func transformData(finalData: Data) -> Data {
470
+ var transformedData = Data(count: finalData.count)
471
+ for i in 0..<finalData.count {
472
+ transformedData[i] = UInt8(Int16(Int8(bitPattern: finalData[i])) + 128)
473
+ }
474
+ return transformedData
475
+ }
476
+
477
+ private func isLCCType(gridDef: [String: Any]) -> Bool {
478
+ if let type = gridDef["type"] as? String {
479
+ return type == "lambert_conformal_conic"
480
+ }
481
+ return false
482
+ }
483
+
484
+ private func generateLCCGeometry(gridDef: [String: Any], vertices: inout [Float], indices: inout [UInt16]) {
485
+ guard let gridParams = gridDef["grid_params"] as? [String: Any],
486
+ let projParams = gridDef["proj_params"] as? [String: Any],
487
+ let nx = gridParams["nx"] as? Int,
488
+ let ny = gridParams["ny"] as? Int,
489
+ let dx = gridParams["dx"] as? Double,
490
+ let dy = gridParams["dy"] as? Double,
491
+ let x_origin = gridParams["x_origin"] as? Double,
492
+ let y_origin = gridParams["y_origin"] as? Double else {
493
+ return
494
+ }
495
+
496
+ let subdivisions = 60
497
+ let TILE_SIZE: Double = 512.0
498
+
499
+ let x_min = x_origin
500
+ let y_max = y_origin
501
+ let x_max = x_origin + Double(nx - 1) * dx
502
+ let y_min = y_origin + Double(ny - 1) * dy
503
+
504
+ var vertexGrid: [[VertexInfo?]] = Array(repeating: Array(repeating: nil, count: subdivisions + 1), count: subdivisions + 1)
505
+ var validVertexCount: UInt16 = 0
506
+
507
+ // Generate vertices
508
+ for row in 0...subdivisions {
509
+ for col in 0...subdivisions {
510
+ let t_x = Double(col) / Double(subdivisions)
511
+ let t_y = Double(row) / Double(subdivisions)
512
+
513
+ let proj_x = x_min + t_x * (x_max - x_min)
514
+ let proj_y = y_max + t_y * (y_min - y_max)
515
+
516
+ // Convert LCC projection coordinates to lat/lon
517
+ if let (lon, lat) = lccToLonLat(i: proj_x, j: proj_y, gridDef: gridDef) {
518
+ // Convert lat/lon to Mercator
519
+ let mercX_normalized = (lon + 180.0) / 360.0
520
+ let clampedLat = max(-85.05112878, min(85.05112878, lat))
521
+ let sinLatitude = sin(clampedLat * .pi / 180.0)
522
+ let mercY_normalized = 0.5 - log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * .pi)
523
+
524
+ let mercX = mercX_normalized * TILE_SIZE
525
+ let mercY = mercY_normalized * TILE_SIZE
526
+
527
+ // Check for invalid values
528
+ if !mercX.isFinite || !mercY.isFinite {
529
+ vertexGrid[row][col] = nil
530
+ continue
531
+ }
532
+
533
+ let tex_u = Float(t_x)
534
+ let tex_v = Float(t_y)
535
+
536
+ let vInfo = VertexInfo(
537
+ mercX: Float(mercX),
538
+ mercY: Float(mercY),
539
+ texU: tex_u,
540
+ texV: tex_v,
541
+ index: validVertexCount
542
+ )
543
+
544
+ vertexGrid[row][col] = vInfo
545
+
546
+ vertices.append(Float(mercX))
547
+ vertices.append(Float(mercY))
548
+ vertices.append(tex_u)
549
+ vertices.append(tex_v)
550
+
551
+ validVertexCount += 1
552
+ } else {
553
+ vertexGrid[row][col] = nil
554
+ }
555
+ }
556
+ }
557
+
558
+ if vertices.isEmpty {
559
+ print("❌ [LCC Geometry] No valid vertices generated")
560
+ return
561
+ }
562
+
563
+ // Generate indices
564
+ for row in 0..<subdivisions {
565
+ for col in 0..<subdivisions {
566
+ guard let topLeft = vertexGrid[row][col],
567
+ let topRight = vertexGrid[row][col + 1],
568
+ let bottomLeft = vertexGrid[row + 1][col],
569
+ let bottomRight = vertexGrid[row + 1][col + 1] else {
570
+ continue
571
+ }
572
+
573
+ indices.append(topLeft.index)
574
+ indices.append(bottomLeft.index)
575
+ indices.append(topRight.index)
576
+
577
+ indices.append(topRight.index)
578
+ indices.append(bottomLeft.index)
579
+ indices.append(bottomRight.index)
580
+ }
581
+ }
582
+ }
583
+
584
+ private func lccToLonLat(i: Double, j: Double, gridDef: [String: Any]) -> (lon: Double, lat: Double)? {
585
+ guard let projParams = gridDef["proj_params"] as? [String: Any],
586
+ let lat_0 = (projParams["lat_0"] as? NSNumber)?.doubleValue,
587
+ let lon_0 = (projParams["lon_0"] as? NSNumber)?.doubleValue,
588
+ let lat_1 = (projParams["lat_1"] as? NSNumber)?.doubleValue,
589
+ let lat_2 = (projParams["lat_2"] as? NSNumber)?.doubleValue,
590
+ let r_earth = (projParams["R"] as? NSNumber)?.doubleValue else {
591
+ print("❌ [LCC Geometry] Failed to extract LCC parameters.")
592
+ return nil
593
+ }
594
+
595
+ let π = Double.pi
596
+ let toRad = π / 180.0
597
+ let toDeg = 180.0 / π
598
+
599
+ let lat1_rad = lat_1 * toRad
600
+ let lat2_rad = lat_2 * toRad
601
+ let lat0_rad = lat_0 * toRad
602
+ let lon0_rad = lon_0 * toRad
603
+
604
+ let n: Double
605
+ if abs(lat_1 - lat_2) < 1e-10 {
606
+ n = sin(lat1_rad)
607
+ } else {
608
+ n = log(cos(lat1_rad) / cos(lat2_rad)) / log(tan(π/4.0 + lat2_rad/2.0) / tan(π/4.0 + lat1_rad/2.0))
609
+ }
610
+
611
+ let F = cos(lat1_rad) * pow(tan(π/4.0 + lat1_rad/2.0), n) / n
612
+ let rho_0 = r_earth * F * pow(tan(π/4.0 + lat0_rad/2.0), -n)
613
+
614
+ let x = i
615
+ let y = j
616
+
617
+ let rho = sqrt(x * x + (rho_0 - y) * (rho_0 - y))
618
+ if rho < 1e-10 {
619
+ return (lon: lon_0, lat: lat_0)
620
+ }
621
+
622
+ let theta = atan2(x, rho_0 - y)
623
+
624
+ let lon = lon0_rad + theta / n
625
+ let lat = 2.0 * atan(pow(r_earth * F / rho, 1.0 / n)) - π/2.0
626
+
627
+ let lonDeg = lon * toDeg
628
+ let latDeg = lat * toDeg
629
+
630
+ if !lonDeg.isFinite || !latDeg.isFinite {
631
+ return nil
632
+ }
633
+
634
+ return (lon: lonDeg, lat: latDeg)
635
+ }
636
+
637
+ private func generateGeometryData(gridDef: [String: Any], vertices: inout [Float], indices: inout [UInt16]) {
638
+ guard let gridParams = gridDef["grid_params"] as? [String: Any] else {
639
+ print("❌ [generateGeometryData] No grid_params found")
640
+ return
641
+ }
642
+
643
+ // Check grid type
644
+ let isGFS = isGFSType(gridParams: gridParams)
645
+ let isLCC = isLCCType(gridDef: gridDef)
646
+
647
+ if isGFS {
648
+ // GFS path remains unchanged
649
+ let subdivisions = 120
650
+ let verticesPerRow = (subdivisions * 3) + 1
651
+ let TILE_SIZE: Double = 512.0
652
+
653
+ for row in 0...subdivisions {
654
+ for col in 0...(subdivisions * 3) {
655
+ let v_interp = Float(row) / Float(subdivisions)
656
+ let u_interp = Float(col) / Float(subdivisions)
657
+ let lon = -540.0 + Double(u_interp) * 1080.0
658
+ let lat = -90.0 + Double(v_interp) * 180.0
659
+
660
+ let merc = lonLatToMercator(lon: lon, lat: lat, tileSize: TILE_SIZE)
661
+ vertices.append(contentsOf: [merc.x, merc.y])
662
+
663
+ let tex_u = Float((lon + 180.0) / 360.0)
664
+ let tex_v = 1.0 - v_interp
665
+ vertices.append(contentsOf: [tex_u, tex_v])
666
+ }
667
+ }
668
+
669
+ for row in 0..<subdivisions {
670
+ for col in 0..<(subdivisions * 3) {
671
+ let tl = UInt16(row * verticesPerRow + col)
672
+ let tr = tl + 1
673
+ let bl = UInt16((row + 1) * verticesPerRow + col)
674
+ let br = bl + 1
675
+ indices.append(contentsOf: [tl, bl, tr, tr, bl, br])
676
+ }
677
+ }
678
+ return
679
+ }
680
+
681
+ if isLCC {
682
+ generateLCCGeometry(gridDef: gridDef, vertices: &vertices, indices: &indices)
683
+ return
684
+ }
685
+
686
+ let nx = gridParams["nx"] as? Int ?? 0
687
+ let ny = gridParams["ny"] as? Int ?? 0
688
+ let lon_first = gridParams["lon_first"] as? Double ?? 0.0
689
+ let lat_first = gridParams["lat_first"] as? Double ?? 90.0
690
+ let dx = gridParams["dx_degrees"] as? Double ?? 0.0
691
+ let dy = gridParams["dy_degrees"] as? Double ?? 0.0
692
+ let lon_last = gridParams["lon_last"] as? Double ?? (lon_first + Double(nx - 1) * dx)
693
+ let lat_last = gridParams["lat_last"] as? Double ?? (lat_first + Double(ny - 1) * dy)
694
+
695
+ let lat_span = lat_last - lat_first
696
+ let isSouthToNorth = lat_span > 0
697
+
698
+ let data_lon_first_180 = lon_first > 180 ? lon_first - 360 : lon_first
699
+ let data_lon_last_180 = lon_last > 180 ? lon_last - 360 : lon_last
700
+ let data_lon_range = data_lon_last_180 - data_lon_first_180
701
+
702
+ let subdivisions_x = 120
703
+ let subdivisions_y = 60
704
+ let verticesPerRow = subdivisions_x + 1
705
+ let TILE_SIZE: Double = 512.0
706
+
707
+ let worldCopies = (data_lon_range > 300) ? [-1, 0, 1] : [0]
708
+
709
+ for world_copy in worldCopies {
710
+ let vertexStartIndex = UInt16(vertices.count / 4)
711
+ let lon_offset = Double(world_copy) * 360.0
712
+
713
+ for row in 0...subdivisions_y {
714
+ for col in 0...subdivisions_x {
715
+ let v_interp = Float(row) / Float(subdivisions_y)
716
+ let u_interp = Float(col) / Float(subdivisions_x)
717
+
718
+ let vertex_lon = data_lon_first_180 + (Double(u_interp) * data_lon_range)
719
+ let vertex_lat = lat_first + (Double(v_interp) * lat_span)
720
+
721
+ let mercX_normalized = ((vertex_lon + lon_offset) + 180.0) / 360.0
722
+ let clampedLat = max(-85.05112878, min(85.05112878, vertex_lat))
723
+ let sinLatitude = sin(clampedLat * .pi / 180.0)
724
+ let mercY_normalized = 0.5 - log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * .pi)
725
+ let mercX = mercX_normalized * TILE_SIZE
726
+ let mercY = mercY_normalized * TILE_SIZE
727
+
728
+ vertices.append(contentsOf: [Float(mercX), Float(mercY)])
729
+
730
+ let tex_u = u_interp
731
+ let tex_v = isSouthToNorth ? (1.0 - v_interp) : v_interp
732
+
733
+ vertices.append(contentsOf: [tex_u, tex_v])
734
+ }
735
+ }
736
+
737
+ for row in 0..<UInt16(subdivisions_y) {
738
+ for col in 0..<UInt16(subdivisions_x) {
739
+ let tl = vertexStartIndex + row * UInt16(verticesPerRow) + col
740
+ let tr = tl + 1
741
+ let bl = vertexStartIndex + (row + 1) * UInt16(verticesPerRow) + col
742
+ let br = bl + 1
743
+
744
+ if isSouthToNorth {
745
+ indices.append(contentsOf: [tl, bl, tr, tr, bl, br])
746
+ } else {
747
+ indices.append(contentsOf: [tl, tr, bl, bl, tr, br])
748
+ }
749
+ }
750
+ }
751
+ }
752
+ }
753
+
754
+ private func isGFSType(gridParams: [String: Any]) -> Bool {
755
+ return (gridParams["lon_first"] as? Double) == 0.0 &&
756
+ abs((gridParams["lat_first"] as? Double) ?? -1) == 90.0
757
+ }
758
+
759
+ private func lonLatToMercator(lon: Double, lat: Double, tileSize: Double) -> (x: Float, y: Float) {
760
+ let mercX_normalized = (lon + 180.0) / 360.0
761
+ let clampedLat = max(-85.05112878, min(85.05112878, lat))
762
+ let sinLatitude = sin(clampedLat * .pi / 180.0)
763
+ let mercY_normalized = 0.5 - log((1.0 + sinLatitude) / (1.0 - sinLatitude)) / (4.0 * .pi)
764
+
765
+ return (x: Float(mercX_normalized * tileSize), y: Float(mercY_normalized * tileSize))
766
+ }
767
+
768
+ // MARK: - Internal methods for CustomLayerHost (called by wrapper)
769
+
770
+ internal func internalRenderingWillStart(_ metalDevice: MTLDevice, colorPixelFormat: UInt, depthStencilPixelFormat: UInt) {
771
+ self.device = metalDevice
772
+ self.commandQueue = metalDevice.makeCommandQueue()
773
+
774
+ let bundle = Bundle(for: GridRenderLayer.self)
775
+
776
+ // Determine if we're running on simulator or device
777
+ #if targetEnvironment(simulator)
778
+ let metallibName = "Shaders-simulator"
779
+ #else
780
+ let metallibName = "Shaders-device"
781
+ #endif
782
+
783
+ // Try to load pre-compiled metallib for the current platform
784
+ let defaultLibrary: MTLLibrary?
785
+ if let metallibUrl = bundle.url(forResource: metallibName, withExtension: "metallib"),
786
+ let library = try? metalDevice.makeLibrary(URL: metallibUrl) {
787
+ defaultLibrary = library
788
+ }
789
+ // Fall back to compiling from .metal source (for development with npm link)
790
+ else if let metalUrl = bundle.url(forResource: "Shaders", withExtension: "metal"),
791
+ let source = try? String(contentsOf: metalUrl),
792
+ let library = try? metalDevice.makeLibrary(source: source, options: nil) {
793
+ defaultLibrary = library
794
+ }
795
+ // Neither worked
796
+ else {
797
+ print("❌ [GridRenderLayer] Could not find or compile Metal shaders")
798
+ print(" Bundle path: \(bundle.bundlePath)")
799
+ return
800
+ }
801
+
802
+ guard let library = defaultLibrary else {
803
+ print("❌ [GridRenderLayer] Failed to create Metal library")
804
+ return
805
+ }
806
+
807
+ let vertexFunction = library.makeFunction(name: "vertex_main")
808
+ let fragmentFunction = library.makeFunction(name: "fragment_main")
809
+
810
+ // Set up vertex descriptor
811
+ let vertexDescriptor = MTLVertexDescriptor()
812
+ vertexDescriptor.attributes[0].format = .float2 // position (x, y)
813
+ vertexDescriptor.attributes[0].offset = 0
814
+ vertexDescriptor.attributes[0].bufferIndex = 0
815
+
816
+ vertexDescriptor.attributes[1].format = .float2 // texCoord (u, v)
817
+ vertexDescriptor.attributes[1].offset = MemoryLayout<Float>.size * 2
818
+ vertexDescriptor.attributes[1].bufferIndex = 0
819
+
820
+ vertexDescriptor.layouts[0].stride = MemoryLayout<Float>.size * 4 // 2 floats for pos + 2 for texCoord
821
+ vertexDescriptor.layouts[0].stepFunction = .perVertex
822
+
823
+ let pipelineDescriptor = MTLRenderPipelineDescriptor()
824
+ pipelineDescriptor.vertexDescriptor = vertexDescriptor
825
+ pipelineDescriptor.vertexFunction = vertexFunction
826
+ pipelineDescriptor.fragmentFunction = fragmentFunction
827
+
828
+ pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormat(rawValue: colorPixelFormat) ?? .bgra8Unorm
829
+ pipelineDescriptor.depthAttachmentPixelFormat = MTLPixelFormat(rawValue: depthStencilPixelFormat) ?? .depth32Float_stencil8
830
+ pipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormat(rawValue: depthStencilPixelFormat) ?? .depth32Float_stencil8
831
+
832
+ pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
833
+ pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
834
+ pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
835
+ pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
836
+ pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
837
+ pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
838
+ pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
839
+
840
+ do {
841
+ self.pipelineState = try metalDevice.makeRenderPipelineState(descriptor: pipelineDescriptor)
842
+ } catch {
843
+ print("❌ [GridRenderLayer] Failed to create pipeline state: \(error)")
844
+ return
845
+ }
846
+
847
+ let dataSamplerDesc = MTLSamplerDescriptor()
848
+ dataSamplerDesc.minFilter = .nearest
849
+ dataSamplerDesc.magFilter = .nearest
850
+ self.dataSamplerState = metalDevice.makeSamplerState(descriptor: dataSamplerDesc)
851
+
852
+ let colormapSamplerDesc = MTLSamplerDescriptor()
853
+ colormapSamplerDesc.minFilter = .nearest
854
+ colormapSamplerDesc.magFilter = .nearest
855
+ colormapSamplerDesc.sAddressMode = .clampToEdge
856
+ self.colormapSamplerState = metalDevice.makeSamplerState(descriptor: colormapSamplerDesc)
857
+
858
+ // Process any pending updates that came in before Metal was ready
859
+ if let pendingGeometry = pendingGeometryUpdate {
860
+ updateGeometry(corners: pendingGeometry.corners, gridDef: pendingGeometry.gridDef)
861
+ pendingGeometryUpdate = nil
862
+ }
863
+
864
+ if let pendingColormap = pendingColormapUpdate {
865
+ updateColormapTexture(colormapAsBase64: pendingColormap)
866
+ pendingColormapUpdate = nil
867
+ }
868
+
869
+ if let pendingData = pendingDataUpdate {
870
+ updateDataTexture(data: pendingData.data, nx: pendingData.nx, ny: pendingData.ny,
871
+ scale: pendingData.scale, offset: pendingData.offset,
872
+ missing: pendingData.missing, scaleType: pendingData.scaleType)
873
+ pendingDataUpdate = nil
874
+ }
875
+ }
876
+ internal func internalRender(_ parameters: CustomLayerRenderParameters, mtlCommandBuffer: MTLCommandBuffer, mtlRenderPassDescriptor: MTLRenderPassDescriptor) {
877
+ guard isVisible,
878
+ let pipeline = pipelineState,
879
+ let vertices = vertexBuffer,
880
+ let indices = indexBuffer,
881
+ let dataTex = dataTexture,
882
+ let colormapTex = colormapTexture,
883
+ indexCount > 0,
884
+ let encoder = mtlCommandBuffer.makeRenderCommandEncoder(descriptor: mtlRenderPassDescriptor) else {
885
+
886
+ return
887
+ }
888
+
889
+ let needsLinear = (uniforms.smoothing != 0)
890
+ if isDataSamplerLinear != needsLinear {
891
+ let samplerDesc = MTLSamplerDescriptor()
892
+ samplerDesc.minFilter = needsLinear ? .linear : .nearest
893
+ samplerDesc.magFilter = needsLinear ? .linear : .nearest
894
+ dataSamplerState = device.makeSamplerState(descriptor: samplerDesc)
895
+ isDataSamplerLinear = needsLinear
896
+ }
897
+
898
+ // Apply zoom scale transformation
899
+ let zoom = parameters.zoom
900
+ let scale = Float(pow(2.0, zoom))
901
+
902
+ let matrixArray = parameters.projectionMatrix
903
+ var floatArray = matrixArray.map { $0.floatValue }
904
+
905
+ floatArray[0] *= scale // X scale
906
+ floatArray[1] *= scale // X rotation
907
+ floatArray[2] *= scale // X Z-component
908
+ floatArray[3] *= scale // X translation
909
+ floatArray[4] *= scale // Y rotation
910
+ floatArray[5] *= scale // Y scale
911
+ floatArray[6] *= scale // Y Z-component
912
+ floatArray[7] *= scale // Y translation
913
+ floatArray[8] *= scale // Z X-component
914
+ floatArray[9] *= scale // Z Y-component
915
+ floatArray[10] *= scale // Z scale
916
+ floatArray[11] *= scale // Z translation
917
+
918
+ let mvp = matrix_float4x4(
919
+ SIMD4<Float>(floatArray[0], floatArray[1], floatArray[2], floatArray[3]),
920
+ SIMD4<Float>(floatArray[4], floatArray[5], floatArray[6], floatArray[7]),
921
+ SIMD4<Float>(floatArray[8], floatArray[9], floatArray[10], floatArray[11]),
922
+ SIMD4<Float>(floatArray[12], floatArray[13], floatArray[14], floatArray[15])
923
+ )
924
+
925
+ // Add depth state
926
+ let depthStencilDescriptor = MTLDepthStencilDescriptor()
927
+ depthStencilDescriptor.depthCompareFunction = .always
928
+ depthStencilDescriptor.isDepthWriteEnabled = false
929
+ let depthStencilState = device.makeDepthStencilState(descriptor: depthStencilDescriptor)
930
+
931
+ encoder.setRenderPipelineState(pipeline)
932
+ encoder.setDepthStencilState(depthStencilState!)
933
+ encoder.setVertexBuffer(vertices, offset: 0, index: 0)
934
+ encoder.setVertexBytes([mvp], length: MemoryLayout<matrix_float4x4>.size, index: 1)
935
+
936
+ var mutableUniforms = uniforms
937
+ encoder.setFragmentBytes(&mutableUniforms, length: MemoryLayout<FragmentUniforms>.stride, index: 0)
938
+ encoder.setFragmentTexture(dataTex, index: 0)
939
+ encoder.setFragmentTexture(colormapTex, index: 1)
940
+ encoder.setFragmentSamplerState(dataSamplerState, index: 0)
941
+ encoder.setFragmentSamplerState(colormapSamplerState, index: 1)
942
+
943
+ encoder.drawIndexedPrimitives(type: .triangle, indexCount: indexCount, indexType: .uint16, indexBuffer: indices, indexBufferOffset: 0)
944
+
945
+ encoder.endEncoding()
946
+ }
947
+
948
+ deinit {
949
+ print("🔴 [GridRenderLayer] deinit called for layer: \(id)")
950
+
951
+ // Clean up all resources
952
+ frameCache.removeAll()
953
+ vertexBuffer = nil
954
+ indexBuffer = nil
955
+ dataTexture = nil
956
+ colormapTexture = nil
957
+ device = nil
958
+ commandQueue = nil
959
+ pipelineState = nil
960
+ dataSamplerState = nil
961
+ colormapSamplerState = nil
962
+
963
+ // Break the reference cycle
964
+ hostWrapper = nil
965
+ }
966
+
967
+ internal func internalRenderingWillEnd() {
968
+ vertexBuffer = nil
969
+ indexBuffer = nil
970
+ dataTexture = nil
971
+ colormapTexture = nil
972
+ }
973
+ }
974
+
975
+ extension GridRenderLayer {
976
+ // Override to prevent KVC crashes during cleanup
977
+ open override func value(forUndefinedKey key: String) -> Any? {
978
+ print("⚠️ [GridRenderLayer] Attempted to access undefined key: \(key)")
979
+ return nil
980
+ }
981
+
982
+ open override func setValue(_ value: Any?, forUndefinedKey key: String) {
983
+ print("⚠️ [GridRenderLayer] Attempted to set undefined key: \(key)")
984
+ // Silently ignore instead of crashing
985
+ }
986
+ }