@nativescript-community/ui-mapbox 6.2.31 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/common.d.ts +56 -26
  3. package/common.js +44 -28
  4. package/expression/expression-parser.android.d.ts +2 -2
  5. package/expression/expression-parser.android.js +4 -3
  6. package/expression/expression-parser.ios.d.ts +2 -2
  7. package/expression/expression-parser.ios.js +28 -13
  8. package/index.android.d.ts +59 -66
  9. package/index.android.js +1388 -1244
  10. package/index.d.ts +36 -5
  11. package/index.ios.d.ts +72 -243
  12. package/index.ios.js +1161 -1999
  13. package/layers/layer-factory.android.d.ts +7 -5
  14. package/layers/layer-factory.android.js +71 -41
  15. package/layers/layer-factory.d.ts +2 -1
  16. package/layers/layer-factory.ios.d.ts +8 -8
  17. package/layers/layer-factory.ios.js +46 -100
  18. package/layers/parser/property-parser.android.d.ts +3 -1
  19. package/layers/parser/property-parser.android.js +25 -24
  20. package/layers/parser/property-parser.d.ts +1 -1
  21. package/layers/parser/property-parser.ios.d.ts +0 -2
  22. package/layers/parser/property-parser.ios.js +0 -149
  23. package/markers/Marker.android.d.ts +28 -0
  24. package/markers/Marker.android.js +54 -0
  25. package/markers/Marker.common.d.ts +2 -0
  26. package/markers/Marker.common.js +31 -0
  27. package/markers/MarkerManager.android.d.ts +35 -0
  28. package/markers/MarkerManager.android.js +220 -0
  29. package/package.json +7 -6
  30. package/platforms/android/include.gradle +31 -27
  31. package/platforms/android/ui_mapbox.aar +0 -0
  32. package/platforms/ios/Podfile +3 -1
  33. package/platforms/ios/Resources/default_pin.png +0 -0
  34. package/platforms/ios/src/MapboxBridge.swift +1479 -0
  35. package/platforms/ios/src/NativeExpressionParser.swift +33 -0
  36. package/platforms/ios/src/NativeLayerFactory.swift +108 -0
  37. package/tsconfig.tsbuildinfo +1 -0
  38. package/typings/Mapbox.ios.d.ts +2 -3242
  39. package/typings/geojson.android.d.ts +689 -0
  40. package/typings/index.android.d.ts +46 -0
  41. package/typings/mapbox.android.d.ts +39968 -12560
  42. package/typings/mapbox.bridge.ios.d.ts +129 -0
@@ -0,0 +1,1479 @@
1
+ //
2
+ // MapboxBridge.swift
3
+ // MapboxMaps v11.x TileStore-enabled bridge for NativeScript
4
+ //
5
+ // Full bridge implementation updated to call MapboxMaps' async
6
+ // querySourceFeatures(for:options:completion:) and return the results
7
+ // synchronously to the JS bridge by waiting for completion with a timeout.
8
+ //
9
+ // Notes:
10
+ // - This attempts to build QuerySourceFeaturesOptions and to convert a
11
+ // Mapbox expression JSON (array) into a native predicate/expression
12
+ // using NativeExpressionParser when available.
13
+ // - The code uses a DispatchSemaphore to wait for the async completion
14
+ // and will return nil if the query times out or fails.
15
+ // - If the native SDK/path is unavailable for constructing an options
16
+ // filter, the bridge will fallback to returning cached GeoJSON source
17
+ // features (if the source was added via addSourceGeoJSON).
18
+ //
19
+ // If you get Xcode compile errors referencing MapboxMaps API differences
20
+ // (names/signatures), paste them here and I will adapt the bridge to
21
+ // match your installed MapboxMaps version precisely.
22
+
23
+ import Foundation
24
+ import UIKit
25
+ import MapboxMaps
26
+
27
+ public extension UIColor {
28
+ convenience init<T>(rgbaValue: T) where T: BinaryInteger {
29
+ guard rgbaValue > 0 else {
30
+ self.init(red: 0, green: 0, blue: 0, alpha: 1)
31
+ return
32
+ }
33
+
34
+ guard rgbaValue < 0xFFFFFFFF else {
35
+ self.init(red: 1, green: 1, blue: 1, alpha: 1)
36
+ return
37
+ }
38
+
39
+ let divisor = CGFloat(255)
40
+ let a = CGFloat((rgbaValue & 0xFF000000) >> 24) / divisor
41
+ let r = CGFloat((rgbaValue & 0x00FF0000) >> 16) / divisor
42
+ let g = CGFloat((rgbaValue & 0x0000FF00) >> 8) / divisor
43
+ let b = CGFloat( rgbaValue & 0x000000FF ) / divisor
44
+
45
+ self.init(red: r, green: g, blue: b, alpha: a)
46
+ }
47
+ }
48
+
49
+ extension Feature {
50
+ var asJsonObject: JSONObject? {
51
+ do {
52
+ let jsonData = try JSONEncoder().encode(self)
53
+ let jsonObject = try JSONSerialization.jsonObject(with: jsonData)
54
+ guard let jsonObject = jsonObject as? [String: Any?] else { return nil }
55
+ // if jsonObject.keys.contains("geometry") {
56
+ // // can be too long for example
57
+ // jsonObject["geometry"] = ["..."]
58
+ // }
59
+ return JSONObject(turfRawValue: jsonObject)
60
+ } catch {
61
+ return nil
62
+ }
63
+ }
64
+ }
65
+
66
+ @objcMembers
67
+ public class MapboxBridge: NSObject {
68
+
69
+ public static func parseJSONParameter(_ parameter: String?) -> Any? {
70
+ if let opt = parameter, let data = opt.data(using: .utf8), let optObj = try? JSONSerialization.jsonObject(with: data, options: []) {
71
+ return optObj
72
+ }
73
+ return nil
74
+ }
75
+ public static func encodeToJSON(_ parameter: [String: Any]?) -> String? {
76
+ do {
77
+ if parameter == nil {
78
+ return nil
79
+ }
80
+ let data = try JSONSerialization.data(withJSONObject: parameter!, options: [])
81
+ return String(data: data, encoding: .utf8) as String?
82
+ } catch {
83
+ return nil
84
+ }
85
+ }
86
+ public func postEvent(_ event: String, _ data: [String: Any]?) {
87
+ if let userInfo = MapboxBridge.encodeToJSON(data) {
88
+ NotificationCenter.default.post(name: Notification.Name(event), object: self.mapView, userInfo: ["data": userInfo])
89
+
90
+ } else {
91
+ NotificationCenter.default.post(name: Notification.Name(event), object: self.mapView)
92
+
93
+ }
94
+ }
95
+ public func postEvent(_ event: String) {
96
+ NotificationCenter.default.post(name: Notification.Name(MapboxBridge.CameraMoveCancelNotification), object: self.mapView)
97
+ }
98
+
99
+ // Notification constants
100
+ public static let MapLoadedNotification = "MapboxBridgeMapLoaded"
101
+ public static let StyleLoadedNotification = "MapboxBridgeStyleLoaded"
102
+ public static let MapClickNotification = "MapboxBridgeMapClick"
103
+ public static let MapLongPressNotification = "MapboxBridgeMapLongPress"
104
+ public static let AnnotationTapNotification = "MapboxBridgeAnnotationTap"
105
+ public static let CameraChangedNotification = "MapboxBridgeCameraChanged"
106
+ public static let CameraIdleNotification = "MapboxBridgeCameraIdle"
107
+ public static let MapScrollNotification = "MapboxBridgeMapScroll"
108
+ public static let MapMoveBeginNotification = "MapboxBridgeMapMoveBegin"
109
+ public static let MapMoveEndNotification = "MapboxBridgeMapMoveEnd"
110
+ public static let MapFlingNotification = "MapboxBridgeMapFling"
111
+ public static let CameraMoveCancelNotification = "MapboxBridgeCameraMoveCancel"
112
+ public static let OfflineProgressNotification = "MapboxBridgeOfflineProgress"
113
+ public static let OfflineCompleteNotification = "MapboxBridgeOfflineComplete"
114
+
115
+ public static let UserLocationUpdatedNotification = "MapboxBridgeUserLocationUpdated"
116
+ public static let UserTrackingStateChangedNotification = "MapboxBridgeUserTrackingStateChanged"
117
+
118
+
119
+ // Map objects & registries
120
+ public private(set) var mapView: MapView?
121
+ private var pointAnnotationManager: PointAnnotationManager?
122
+ private var polylineAnnotationManager: PolylineAnnotationManager?
123
+ private var polygonAnnotationManager: PolygonAnnotationManager?
124
+ private var polygonOutlineAnnotationManager: PolylineAnnotationManager?
125
+ private var imageRegistry: [String: UIImage] = [:]
126
+ private var viewAnnotationByMarkerId: [String: ViewAnnotation] = [:]
127
+
128
+ // Camera handling
129
+ private var cameraIdleWorkItem: DispatchWorkItem?
130
+ private var cameraChangeCallback: (([String: Any]) -> Void)? = nil
131
+
132
+ // TileStore offline
133
+ private var tileRegionLoadRequestByName: [String: Cancelable] = [:]
134
+
135
+
136
+ // last location cache
137
+ private var lastUserLocation: CLLocation?
138
+
139
+ private var offlineManager = OfflineManager()
140
+
141
+ private var cancelables = Set<AnyCancelable>()
142
+ private var locationTrackingCancellation: Cancelable?
143
+
144
+ private var userTrackingCameraMode: String = "NONE"
145
+ private var userTrackingCameraAnimated: Bool = true
146
+
147
+ // MARK: - Registry for bridge instances
148
+ // Weak-keyed map: MapView -> MapboxBridge
149
+ private static var bridgeTable: NSMapTable<AnyObject, AnyObject> = {
150
+ // weak keys (MapView), weak values (MapboxBridge)
151
+ return NSMapTable<AnyObject, AnyObject>(keyOptions: .weakMemory, valueOptions: .weakMemory)
152
+ }()
153
+
154
+ /// Register a bridge for a MapView (called when createMap creates a MapView)
155
+ private static func registerBridge(_ bridge: MapboxBridge, for mapView: MapView) {
156
+ MapboxBridge.bridgeTable.setObject(bridge, forKey: mapView)
157
+ }
158
+
159
+ /// Unregister a bridge for the given MapView (called on destroy)
160
+ private static func unregisterBridge(for mapView: MapView) {
161
+ MapboxBridge.bridgeTable.removeObject(forKey: mapView)
162
+ }
163
+
164
+ /// Find a bridge for a MapView instance if registered
165
+ @objc public static func bridge(for mapView: MapView) -> MapboxBridge? {
166
+ return MapboxBridge.bridgeTable.object(forKey: mapView) as? MapboxBridge
167
+ }
168
+
169
+ @objc public func getMapView() -> MapView? {
170
+ return mapView
171
+ }
172
+
173
+ // MARK: - Create / lifecycle
174
+ private var lastAnnotationTapConsumedAt: Date?
175
+
176
+ @objc public func createMap(_ x: Double, _ y: Double, _ width: Double, _ height: Double, _ accessToken: String, _ styleURIString: String?, _ optionsJSON: String) -> UIView {
177
+ MapboxOptions.accessToken = accessToken
178
+ let styleURI = getMapStyleURI(styleURIString)
179
+
180
+ var centerCoordinate = CLLocationCoordinate2D(latitude: 48.858093, longitude: 2.294694)
181
+ var zoom = 0.0
182
+ if let options = MapboxBridge.parseJSONParameter(optionsJSON) as? [String: Any] {
183
+ if let z = options["zoomLevel"] as? Double {
184
+ zoom = z
185
+ }
186
+ if let target = options["center"] as? [String: Any], let lat = target["lat"] as? Double, let lng = target["lng"] as? Double {
187
+ centerCoordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng)
188
+ }
189
+ }
190
+
191
+
192
+ let camera = CameraOptions(center: centerCoordinate, zoom: CGFloat(zoom))
193
+
194
+ let initOptions = MapInitOptions(cameraOptions: camera, styleURI: styleURI)
195
+
196
+
197
+ let frame = CGRect(x: x, y: y, width: width, height: height)
198
+ let mv = MapView(frame: frame, mapInitOptions: initOptions)
199
+ self.mapView = mv
200
+
201
+ // Register this bridge for the created MapView
202
+ MapboxBridge.registerBridge(self, for: mv)
203
+
204
+ addImage("default_pin", image: UIImage(named: "default_pin"))
205
+
206
+ // mapLoaded
207
+ mv.mapboxMap.onMapLoaded.observeNext { _ in
208
+ self.postEvent(MapboxBridge.MapLoadedNotification)
209
+ }.store(in: &cancelables)
210
+
211
+ // styleLoaded
212
+ mv.mapboxMap.onStyleLoaded.observeNext { _ in
213
+ self.postEvent(MapboxBridge.StyleLoadedNotification)
214
+ }.store(in: &cancelables)
215
+
216
+ // camera changed -> notify & schedule idle
217
+ mv.mapboxMap.onCameraChanged.observe { _ in
218
+ let s = mv.mapboxMap.cameraState
219
+ let userInfo: [String: Any] = [
220
+ "centerLat": s.center.latitude,
221
+ "centerLng": s.center.longitude,
222
+ "zoom": s.zoom,
223
+ "pitch": s.pitch,
224
+ "bearing": s.bearing
225
+ ]
226
+ self.postEvent(MapboxBridge.CameraChangedNotification, userInfo)
227
+ self.cameraChangeCallback?(userInfo)
228
+
229
+ self.cameraIdleWorkItem?.cancel()
230
+ let work = DispatchWorkItem { [weak self] in
231
+ guard let self = self, let mv = self.mapView else { return }
232
+ let st = mv.mapboxMap.cameraState
233
+ let info: [String: Any] = [
234
+ "centerLat": st.center.latitude,
235
+ "centerLng": st.center.longitude,
236
+ "zoom": st.zoom,
237
+ "pitch": st.pitch,
238
+ "bearing": st.bearing
239
+ ]
240
+ self.postEvent(MapboxBridge.CameraIdleNotification, info)
241
+ }
242
+ self.cameraIdleWorkItem = work
243
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: work)
244
+ }.store(in: &cancelables)
245
+
246
+ mv.mapboxMap.addInteraction(TapInteraction { [weak self] context in
247
+ guard let self else { return false }
248
+ guard self.mapView != nil else { return false }
249
+ if let t = self.lastAnnotationTapConsumedAt,
250
+ Date().timeIntervalSince(t) < 0.35 {
251
+ // ignore this map tap because an annotation already handled it
252
+ return false
253
+ }
254
+ let userInfo: [String: Any] = ["lat": context.coordinate.latitude, "lng": context.coordinate.longitude, "x": Double(context.point.x), "y": Double(context.point.y)]
255
+ self.postEvent(MapboxBridge.MapClickNotification, userInfo)
256
+ return false
257
+ })
258
+ mv.mapboxMap.addInteraction(LongPressInteraction { [weak self] context in
259
+ guard let self else { return false }
260
+ guard self.mapView != nil else { return false }
261
+ let userInfo: [String: Any] = ["lat": context.coordinate.latitude, "lng": context.coordinate.longitude, "x": Double(context.point.x), "y": Double(context.point.y)]
262
+ self.postEvent(MapboxBridge.MapLongPressNotification, userInfo)
263
+ return false
264
+ })
265
+ // mv.mapboxMap.addInteraction(Pan { [weak self] context in
266
+ // guard let self else { return false }
267
+ // guard self.mapView != nil else { return false }
268
+ // let userInfo: [String: Any] = ["lat": context.coordinate.latitude, "lng": context.coordinate.longitude, "x": Double(context.point.x), "y": Double(context.point.y)]
269
+ // self.postEvent(MapboxBridge.MapLongPressNotification, userInfo)
270
+ // return false
271
+ // })
272
+ // mv.gestures.singleTapGestureRecognizer.addTarget(self, action: #selector(handleMapTap(_:)))
273
+ // mv.gestures.longPressGestureRecognizer.addTarget(self, action: #selector(handleMapLongPress(_:)))
274
+ mv.gestures.panGestureRecognizer.addTarget(self, action: #selector(handleMapPan(_:)))
275
+
276
+ return mv
277
+ }
278
+
279
+ @objc public func show() { mapView?.isHidden = false }
280
+ @objc public func hide() { mapView?.isHidden = true }
281
+
282
+ @objc public func destroy() {
283
+ if let mv = self.mapView {
284
+ MapboxBridge.unregisterBridge(for: mv)
285
+ }
286
+ pointAnnotationManager = nil
287
+ polygonAnnotationManager = nil
288
+ polylineAnnotationManager = nil
289
+ imageRegistry.removeAll()
290
+ cameraIdleWorkItem?.cancel()
291
+ cameraIdleWorkItem = nil
292
+
293
+ for cancelable in cancelables {
294
+ cancelable.cancel()
295
+ }
296
+ cancelables.removeAll()
297
+ if mapView != nil {
298
+ for (_, view) in viewAnnotationByMarkerId {
299
+ view.remove()
300
+ }
301
+ viewAnnotationByMarkerId.removeAll()
302
+ }
303
+ for (_, request) in tileRegionLoadRequestByName {
304
+ request.cancel()
305
+ }
306
+ tileRegionLoadRequestByName.removeAll()
307
+ mapView?.removeFromSuperview()
308
+ mapView = nil
309
+
310
+ }
311
+
312
+ // MARK: - Gesture handlers
313
+
314
+ @objc private func handleMapTap(_ recognizer: UITapGestureRecognizer) {
315
+ guard let mv = mapView else { return }
316
+ let pt = recognizer.location(in: mv)
317
+ let coord = mv.mapboxMap.coordinate(for: pt)
318
+ let userInfo: [String: Any] = ["lat": coord.latitude, "lng": coord.longitude, "x": Double(pt.x), "y": Double(pt.y)]
319
+ self.postEvent(MapboxBridge.MapClickNotification, userInfo)
320
+ }
321
+
322
+ @objc private func handleMapLongPress(_ recognizer: UILongPressGestureRecognizer) {
323
+ guard let mv = mapView else { return }
324
+ if recognizer.state == .began {
325
+ let pt = recognizer.location(in: mv)
326
+ let coord = mv.mapboxMap.coordinate(for: pt)
327
+ let userInfo: [String: Any] = ["lat": coord.latitude, "lng": coord.longitude, "x": Double(pt.x), "y": Double(pt.y)]
328
+ self.postEvent(MapboxBridge.MapLongPressNotification, userInfo)
329
+ }
330
+ }
331
+
332
+ @objc private func handleMapPan(_ recognizer: UIPanGestureRecognizer) {
333
+ guard let mv = mapView else { return }
334
+ let velocity = recognizer.velocity(in: mv)
335
+ let translation = recognizer.translation(in: mv)
336
+ let pt = recognizer.location(in: mv)
337
+ let coord = mv.mapboxMap.coordinate(for: pt)
338
+ let infoBase: [String: Any] = ["lat": coord.latitude, "lng": coord.longitude, "x": Double(pt.x), "y": Double(pt.y), "vx": Double(velocity.x), "vy": Double(velocity.y), "tx": Double(translation.x), "ty": Double(translation.y)]
339
+
340
+ switch recognizer.state {
341
+ case .began:
342
+ self.postEvent(MapboxBridge.MapMoveBeginNotification, infoBase)
343
+ case .changed:
344
+ self.postEvent(MapboxBridge.MapScrollNotification, infoBase)
345
+ case .ended:
346
+ let speed = sqrt(velocity.x * velocity.x + velocity.y * velocity.y)
347
+ if speed > 1000.0 {
348
+ var fInfo = infoBase
349
+ fInfo["speed"] = Double(speed)
350
+ self.postEvent(MapboxBridge.MapFlingNotification, fInfo)
351
+ }
352
+ self.postEvent(MapboxBridge.MapMoveEndNotification, infoBase)
353
+ case .cancelled, .failed:
354
+ self.postEvent(MapboxBridge.CameraMoveCancelNotification, infoBase)
355
+ default:
356
+ break
357
+ }
358
+ }
359
+
360
+ // MARK: - Images
361
+
362
+
363
+
364
+ /// Return a UIImage for the given imageId.
365
+ /// - First looks in the local imageRegistry (images added via addImage).
366
+ /// - Then tries to fetch the image from the current style via style.image(withId:).
367
+ /// - Returns nil if not found.
368
+ @objc public func getImage(_ imageId: String) -> UIImage? {
369
+ // Local registry lookup
370
+ if let img = imageRegistry[imageId] {
371
+ return img
372
+ }
373
+
374
+ // Try to get image from style
375
+ if let mv = mapView {
376
+ // MapboxMaps 11.x style.image(withId:) returns UIImage?
377
+ if let styleImage = mv.mapboxMap.image(withId: imageId) {
378
+ return styleImage
379
+ }
380
+
381
+ }
382
+
383
+ return nil
384
+ }
385
+
386
+ @objc public func addImage(_ imageId: String, image: UIImage?) {
387
+ guard let mv = mapView else { return }
388
+ if (image != nil) {
389
+ imageRegistry[imageId] = image!
390
+ try? mv.mapboxMap.addImage(image!, id: imageId)
391
+ }
392
+ }
393
+
394
+ @objc public func removeImage(_ imageId: String) {
395
+ guard let mv = mapView else { return }
396
+ imageRegistry.removeValue(forKey: imageId)
397
+ try? mv.mapboxMap.removeImage(withId: imageId)
398
+ }
399
+
400
+ private func resolveImage(named name: String?) -> UIImage? {
401
+ guard let name = name else { return nil }
402
+ if let reg = imageRegistry[name] { return reg }
403
+ if let img = UIImage(named: name) { return img }
404
+ if let url = URL(string: name), let data = try? Data(contentsOf: url), let img = UIImage(data: data) { return img }
405
+ return nil
406
+ }
407
+
408
+ // MARK: - Markers (Point Annotations)
409
+
410
+ // markers: NSArray of NSDictionary { lat, lng, id?, title?, subtitle?, icon? }
411
+ @objc public func addMarkers(_ markersJSON: String) {
412
+ guard let mv = mapView else { return }
413
+ guard let data = markersJSON.data(using: .utf8) else { return }
414
+ guard let markers = try? JSONSerialization.jsonObject(with: data, options: []) as! [NSDictionary] else { return }
415
+
416
+ if pointAnnotationManager == nil {
417
+ pointAnnotationManager = mv.annotations.makePointAnnotationManager()
418
+ }
419
+ guard let manager = pointAnnotationManager else { return }
420
+
421
+ var current = manager.annotations
422
+ var additions: [PointAnnotation] = []
423
+
424
+ for case let dict in markers {
425
+ guard let lat = dict["lat"] as? Double, let lng = dict["lng"] as? Double else { continue }
426
+ var theId: String?
427
+ if let id = dict["id"] {
428
+ if let idS = id as? String? { theId = (idS!) }
429
+ else if let idD = id as? NSNumber? { theId = (idD!).stringValue }
430
+ else { theId = String(NSDate().timeIntervalSince1970) }
431
+ }
432
+ var pa = PointAnnotation(id:theId! , coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lng))
433
+
434
+ // userInfo
435
+ var userInfo = JSONObject()
436
+
437
+ userInfo["id"] = .string(theId!)
438
+ if let title = dict["title"] { userInfo["title"] = .string(title as! String) }
439
+ if let subtitle = dict["subtitle"] { userInfo["subtitle"] = .string(subtitle as! String) }
440
+ pa.customData = userInfo
441
+
442
+ let icon = (dict["icon"] as? String) ?? "default_pin"
443
+
444
+ if let img = imageRegistry[icon] {
445
+ pa.image = .init(image: img, name: icon)
446
+ }
447
+ _ = pa.tapHandler = { [weak self] ann in
448
+ guard let self = self else { return true }
449
+ self.lastAnnotationTapConsumedAt = Date()
450
+ self.emitAnnotationTap(pa)
451
+ return true
452
+ }
453
+ additions.append(pa)
454
+
455
+ }
456
+
457
+ current.append(contentsOf: additions)
458
+ manager.annotations = current
459
+ }
460
+
461
+ @objc public func removeMarkers(_ idsJSON: String?) {
462
+ guard let manager = pointAnnotationManager else { return }
463
+
464
+ if idsJSON == nil {
465
+ manager.annotations.removeAll()
466
+ return
467
+ }
468
+ guard let data = idsJSON!.data(using: .utf8) else { return }
469
+ guard let ids = try? JSONSerialization.jsonObject(with: data, options: []) as! [String] else { return }
470
+
471
+ var idSet = Set<String>()
472
+ for case let v in ids {
473
+ idSet.insert(v)
474
+ }
475
+
476
+ let remaining = manager.annotations.filter { ann in
477
+ return !idSet.contains(ann.id)
478
+ }
479
+ manager.annotations = remaining
480
+ }
481
+ @objc public func updateMarkerPosition(_ markerId: String, _ lat: Double, _ lng: Double) -> Bool {
482
+ guard let manager = pointAnnotationManager else { return false }
483
+
484
+ if let index = manager.annotations.firstIndex(where: { $0.id == markerId }) {
485
+ var ann = manager.annotations[index]
486
+ ann.point = Point(CLLocationCoordinate2D(latitude: lat, longitude: lng))
487
+ _ = updateViewAnnotationForMarker(markerId, lat, lng)
488
+ manager.annotations[index] = ann
489
+ return true
490
+ }
491
+ return false
492
+ }
493
+
494
+ private func emitAnnotationTap(_ annotation: PointAnnotation) {
495
+ guard mapView != nil else { return }
496
+ var info: [String: Any] = [:]
497
+ for (k, v) in annotation.customData { info[k] = v?.rawValue }
498
+ let coord = annotation.point.coordinates
499
+ info["id"] = annotation.id
500
+ info["lat"] = coord.latitude
501
+ info["lng"] = coord.longitude
502
+ self.postEvent(MapboxBridge.AnnotationTapNotification, info)
503
+ }
504
+ // MARK: - View annotations
505
+
506
+ @objc public func addViewAnnotationForMarker(_ markerId: String, _ view: UIView, _ lat: Double, _ lng: Double) -> Bool {
507
+ guard let mv = mapView else { return false }
508
+ guard let manager = pointAnnotationManager else { return false }
509
+ if let existing = viewAnnotationByMarkerId[markerId] {
510
+ existing.remove()
511
+ viewAnnotationByMarkerId.removeValue(forKey: markerId)
512
+ }
513
+
514
+ guard let an = manager.annotations.first(where: { $0.id == markerId }) else { return false }
515
+ let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng)
516
+ let annotation = ViewAnnotation(coordinate: coordinate, view: view)
517
+ // annotation.selected = false
518
+ annotation.allowOverlap = true
519
+ annotation.allowOverlapWithPuck = true
520
+ let image = an.image
521
+ let imageHeight = image?.image.size.height ?? 0
522
+ let offsetY = imageHeight - 12
523
+ // TODO: variableAnchors is broken for now if multiple
524
+ annotation.variableAnchors = [ViewAnnotationAnchorConfig(anchor: .bottom, offsetY: offsetY)
525
+ // , ViewAnnotationAnchorConfig(anchor: .bottomLeft, offsetY: offsetY), ViewAnnotationAnchorConfig(anchor: .bottomRight, offsetY: offsetY)
526
+ ]
527
+ // annotation.anchorConfig = annotation.variableAnchors.first
528
+ mv.viewAnnotations.add(annotation)
529
+ viewAnnotationByMarkerId[markerId] = annotation
530
+ return true
531
+ }
532
+
533
+ @objc public func updateViewAnnotationForMarker(_ markerId: String, _ lat: Double, _ lng: Double) -> Bool {
534
+ guard mapView != nil else { return false }
535
+ guard let view = viewAnnotationByMarkerId[markerId] else { return false }
536
+ view.annotatedFeature = .geometry(Point(CLLocationCoordinate2D(latitude: lat, longitude: lng)))
537
+ return true
538
+ }
539
+
540
+ @objc public func removeViewAnnotationForMarker(_ markerId: String) -> Bool {
541
+ guard mapView != nil else { return false }
542
+ guard let view = viewAnnotationByMarkerId[markerId] else { return false }
543
+ view.remove()
544
+ viewAnnotationByMarkerId.removeValue(forKey: markerId)
545
+ return true
546
+ }
547
+
548
+ @objc public func hasViewAnnotationForMarker(_ markerId: String) -> Bool {
549
+ return viewAnnotationByMarkerId[markerId] != nil
550
+ }
551
+ @objc public func hideAnnotationForMarker(_ markerId: String) -> Bool {
552
+ guard let view = viewAnnotationByMarkerId[markerId] else { return false }
553
+ view.visible = false
554
+ return true
555
+ }
556
+ @objc public func showAnnotationForMarker(_ markerId: String) -> Bool {
557
+ guard let view = viewAnnotationByMarkerId[markerId] else { return false }
558
+ view.visible = true
559
+ return true
560
+ }
561
+
562
+ // MARK: - Style
563
+
564
+ @objc public func setStyle(_ styleURIorURL: String, _ completion: @escaping (Bool, NSError?) -> Void) {
565
+ guard let mv = mapView else { completion(false, NSError(domain: "MapboxBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "No map available"])); return }
566
+ mv.mapboxMap.onStyleLoaded.observeNext { _ in completion(true, nil) }.store(in: &cancelables)
567
+ mv.mapboxMap.loadStyle(getMapStyleURI(styleURIorURL));
568
+ completion(false, NSError(domain: "MapboxBridge", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid style string"]))
569
+ }
570
+
571
+ // MARK: - Camera / viewport / animateCamera
572
+
573
+ @objc public func setCenter(_ lat: Double, _ lng: Double, _ animated: Bool) {
574
+ guard let mv = mapView else { return }
575
+ let cam = CameraOptions(center: CLLocationCoordinate2D(latitude: lat, longitude: lng))
576
+ if animated {
577
+ mv.camera.ease(to: cam, duration: 0.5, completion: { _ in })
578
+ } else {
579
+ mv.camera.cancelAnimations()
580
+ mv.mapboxMap.setCamera(to: cam)
581
+ }
582
+ }
583
+
584
+ @objc public func getCenter() -> String? {
585
+ guard let mv = mapView else { return nil }
586
+ let c = mv.mapboxMap.cameraState.center
587
+ return MapboxBridge.encodeToJSON(["lat": c.latitude, "lng": c.longitude])
588
+ }
589
+
590
+ @objc public func setZoom(_ zoom: Double, _ animated: Bool) {
591
+ guard let mv = mapView else { return }
592
+ let cam = CameraOptions(zoom: zoom)
593
+ if animated {
594
+ mv.camera.ease(to: cam, duration: 0.5, completion: { _ in })
595
+ } else {
596
+ mv.camera.cancelAnimations()
597
+ mv.mapboxMap.setCamera(to: cam)
598
+ }
599
+ }
600
+
601
+ @objc public func getZoom() -> NSNumber? {
602
+ guard let mv = mapView else { return nil }
603
+ return NSNumber(value: mv.mapboxMap.cameraState.zoom)
604
+ }
605
+
606
+ // animateCamera: accepts a JSON string with optional fields: bounds {south,west,north,east}, target {lat,lng}, zoom, bearing, pitch, duration
607
+ @objc public func animateCamera(_ optionsJSON: String) -> Bool {
608
+ guard let mv = mapView else { return false }
609
+ guard let obj = MapboxBridge.parseJSONParameter(optionsJSON) as? [String: Any] else { return false }
610
+
611
+ var camOptions = CameraOptions()
612
+ var duration = 0.0
613
+
614
+ if let bounds = obj["bounds"] as? [String: Any], let south = bounds["south"] as? Double, let west = bounds["west"] as? Double, let north = bounds["north"] as? Double, let east = bounds["east"] as? Double {
615
+ // Compute camera that fits bounds
616
+ let outerRing: [LocationCoordinate2D] = [
617
+ LocationCoordinate2D(latitude: north, longitude: west),
618
+ LocationCoordinate2D(latitude: north, longitude: east),
619
+ LocationCoordinate2D(latitude: south, longitude: east),
620
+ LocationCoordinate2D(latitude: south, longitude: west),
621
+ LocationCoordinate2D(latitude: north, longitude: west) // close ring
622
+ ]
623
+ if let camera = try? mv.mapboxMap.camera(for: outerRing, camera: camOptions, coordinatesPadding: .zero, maxZoom: nil, offset: nil) {
624
+ camOptions = camera
625
+ }
626
+ } else if let target = obj["target"] as? [String: Any], let lat = target["lat"] as? Double, let lng = target["lng"] as? Double {
627
+ camOptions.center = CLLocationCoordinate2D(latitude: lat, longitude: lng)
628
+ }
629
+
630
+ if let zoom = obj["zoom"] as? Double {
631
+ camOptions.zoom = zoom
632
+ }
633
+ if let bearing = obj["bearing"] as? Double {
634
+ camOptions.bearing = bearing
635
+ }
636
+ if let pitch = obj["pitch"] as? Double {
637
+ camOptions.pitch = pitch
638
+ }
639
+ if let tilt = obj["tilt"] as? Double {
640
+ camOptions.pitch = tilt
641
+ }
642
+ if let d = obj["duration"] as? Double {
643
+ duration = max(0.0, d / 1000.0)
644
+ }
645
+
646
+ mv.camera.fly(to: camOptions, duration: duration, completion: { _ in })
647
+ return true
648
+ }
649
+
650
+ @objc public func coordinateToPoint(_ lat: Double, _ lng: Double) -> String? {
651
+ guard let mv = mapView else { return nil }
652
+ let pt = mv.mapboxMap.point(for: CLLocationCoordinate2D(latitude:lat, longitude:lng))
653
+ return MapboxBridge.encodeToJSON(["x": Double(pt.x), "y": Double(pt.y)])
654
+ }
655
+ @objc public func pointToCoordinate(_ x: Double, _ y: Double) -> String? {
656
+ guard let mv = mapView else { return nil }
657
+ let coord = mv.mapboxMap.coordinate(for: CGPointMake(x, y))
658
+ return MapboxBridge.encodeToJSON(["lat": coord.latitude, "lng": coord.longitude])
659
+ }
660
+ @objc public func getViewport() -> String? {
661
+ guard let mv = mapView else { return nil }
662
+ // Return JSON string with bounds and zoom
663
+ let bounds = mv.mapboxMap.coordinateBounds(for: mv.bounds)
664
+
665
+ return MapboxBridge.encodeToJSON([
666
+ "bounds": [
667
+ "north": bounds.northeast.latitude,
668
+ "east": bounds.northeast.longitude,
669
+ "south": bounds.southwest.latitude,
670
+ "west": bounds.southwest.longitude
671
+ ],
672
+ "zoomLevel": mv.mapboxMap.cameraState.zoom
673
+ ])
674
+ }
675
+
676
+ // setViewport: parses JSON with bounds and padding and animated flag similar to TS usage
677
+ @objc public func setViewport(_ optionsJSON: String) -> Bool {
678
+ guard let mv = mapView else { return false }
679
+ guard let obj = MapboxBridge.parseJSONParameter(optionsJSON) as? [String: Any] else { return false }
680
+
681
+ guard let bounds = obj["bounds"] as? [String: Any], let south = bounds["south"] as? Double, let west = bounds["west"] as? Double, let north = bounds["north"] as? Double, let east = bounds["east"] as? Double else {
682
+ return false
683
+ }
684
+
685
+ // Turf uses LocationCoordinate2D; create the ring and repeat first point to close it
686
+ let outerRing: [LocationCoordinate2D] = [
687
+ LocationCoordinate2D(latitude: north, longitude: west),
688
+ LocationCoordinate2D(latitude: north, longitude: east),
689
+ LocationCoordinate2D(latitude: south, longitude: east),
690
+ LocationCoordinate2D(latitude: south, longitude: west),
691
+ LocationCoordinate2D(latitude: north, longitude: west) // close ring
692
+ ]
693
+
694
+ // Create a Polygon and its Geometry
695
+ // _ = Polygon([outerRing])
696
+
697
+ var padding = UIEdgeInsets(top: 25, left: 25, bottom: 25, right: 25)
698
+ if let pad = obj["padding"] as? [String: Any] {
699
+ let top = (pad["top"] as? Double).flatMap { CGFloat($0) } ?? padding.top
700
+ let left = (pad["left"] as? Double).flatMap { CGFloat($0) } ?? padding.left
701
+ let bottom = (pad["bottom"] as? Double).flatMap { CGFloat($0) } ?? padding.bottom
702
+ let right = (pad["right"] as? Double).flatMap { CGFloat($0) } ?? padding.right
703
+ padding = UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)
704
+ } else if let padSingle = obj["padding"] as? Double {
705
+ let p = CGFloat(padSingle)
706
+ padding = UIEdgeInsets(top: p, left: p, bottom: p, right: p)
707
+ }
708
+
709
+ let animated = (obj["animated"] as? Bool) ?? true
710
+ if animated {
711
+ if let camera = try? mv.mapboxMap.camera(for: outerRing, camera: CameraOptions(), coordinatesPadding: .zero, maxZoom: nil, offset: nil) {
712
+ mv.camera.ease(to: camera, duration: 0.5, completion: { _ in })
713
+ } else {
714
+ mv.camera.cancelAnimations()
715
+ mv.mapboxMap.setCamera(to: CameraOptions(center: CLLocationCoordinate2D(latitude: (south + north) / 2.0, longitude: (west + east) / 2.0)))
716
+ }
717
+ } else {
718
+ if let camera = try? mv.mapboxMap.camera(for: outerRing, camera: CameraOptions(), coordinatesPadding: .zero, maxZoom: nil, offset: nil) {
719
+ mv.camera.cancelAnimations()
720
+ mv.mapboxMap.setCamera(to: camera)
721
+ } else {
722
+ mv.camera.cancelAnimations()
723
+ mv.mapboxMap.setCamera(to: CameraOptions(center: CLLocationCoordinate2D(latitude: (south + north) / 2.0, longitude: (west + east) / 2.0)))
724
+ }
725
+ }
726
+
727
+ return true
728
+ }
729
+
730
+ // MARK: - Rendered & source queries
731
+
732
+ // Query rendered features at a screen point; returns JSON string array
733
+ // point: NSDictionary { x, y } and optional layerIds NSArray
734
+ @objc public func queryRenderedFeaturesAtPoint(_ pointJSON: String, _ layerIds: [String]?, _ completion: @escaping (String?, Error?) -> Void) -> Cancelable? {
735
+ guard let mv = mapView else { return nil }
736
+ guard let point = MapboxBridge.parseJSONParameter(pointJSON) as? [String: Double] else { return nil }
737
+ guard let x = point["x"], let y = point["y"] else { return nil }
738
+ let screenPoint = CGPoint(x: x, y: y)
739
+
740
+ let options = RenderedQueryOptions(layerIds: layerIds, filter: nil)
741
+ return mv.mapboxMap.queryRenderedFeatures(with: screenPoint, options: options) { [weak self] result in
742
+ guard self != nil else { return }
743
+
744
+ switch result {
745
+ case .success(let features):
746
+ var jsonArray: [[String: Any]] = []
747
+ for feature in features {
748
+ do {
749
+ let jsonData = try JSONEncoder().encode(feature.queriedFeature.feature)
750
+ let jsonObject = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any?]
751
+ if var json = jsonObject {
752
+ json["source"] = (feature.queriedFeature.source)
753
+ if let sourceLayer = feature.queriedFeature.sourceLayer {
754
+ json["source_layer"] = (sourceLayer)
755
+ }
756
+ jsonArray.append(json as [String : Any])
757
+ }
758
+ } catch {}
759
+ }
760
+ if let data = try? JSONSerialization.data(withJSONObject: jsonArray, options: []),
761
+ let s = String(data: data, encoding: .utf8) {
762
+ completion(s as String, nil)
763
+ } else {
764
+ completion(nil, nil)
765
+ }
766
+ case .failure(let error):
767
+ completion(nil, error)
768
+ }
769
+ }
770
+ }
771
+
772
+ @objc public func querySourceFeatures(_ sourceId: String, _ optionsJSON: String?, _ completion: @escaping (String?, Error?) -> Void) -> Cancelable? {
773
+ guard let mv = mapView else { completion(nil, nil); return nil }
774
+
775
+ var filterExpression: Any? = nil
776
+ var sourceLayers: [String] = []
777
+ if let obj = MapboxBridge.parseJSONParameter(optionsJSON) as? [String: Any] {
778
+ filterExpression = obj["filter"]
779
+ if let sl = obj["sourceLayer"] as? String { sourceLayers.append(sl)}
780
+ if let sls = obj["sourceLayers"] as? [String], sls.count > 0 { sourceLayers = sls }
781
+ }
782
+ // Try to convert expression JSON to NSPredicate or Expression via NativeExpressionParser
783
+ // var filter: Exp? = nil
784
+ // do {
785
+ // if let expr = filterExpression as? [Any] {
786
+ // let data = try JSONSerialization.data(withJSONObject: expr, options: [])
787
+ // filter = try JSONDecoder().decode(Exp.self, from: data)
788
+ // }
789
+ // } catch {
790
+ // completion(nil, error as NSError)
791
+ // return nil
792
+ // }
793
+ if (filterExpression == nil) {
794
+ completion(nil, NSError(domain: "MapboxBridge", code: 2, userInfo: [NSLocalizedDescriptionKey: "querySourceFeatures: missing filter"]))
795
+ return nil
796
+
797
+ }
798
+
799
+ let options = SourceQueryOptions(sourceLayerIds: sourceLayers, filter: filterExpression!)
800
+
801
+
802
+ // Call the native async API and return the Cancelable. Use the completion to return JSON string.
803
+ let cancelable = mv.mapboxMap.querySourceFeatures(for: sourceId, options: options) { result in
804
+ switch result {
805
+ case .success(let features):
806
+ var jsonArray: [[String: Any]] = []
807
+ for feature in features {
808
+ do {
809
+ let jsonData = try JSONEncoder().encode(feature.queriedFeature.feature)
810
+ let jsonObject = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any?]
811
+ if var json = jsonObject {
812
+ json["source"] = (feature.queriedFeature.source)
813
+ if let sourceLayer = feature.queriedFeature.sourceLayer {
814
+ json["source_layer"] = (sourceLayer)
815
+ }
816
+ jsonArray.append(json as [String : Any])
817
+ }
818
+ } catch {}
819
+ }
820
+ if let data = try? JSONSerialization.data(withJSONObject: jsonArray, options: []),
821
+ let s = String(data: data, encoding: .utf8) {
822
+ completion(s as String, nil)
823
+ } else {
824
+ completion(nil, nil)
825
+ }
826
+ case .failure(let error):
827
+ completion(nil, error as NSError)
828
+ }
829
+ }
830
+
831
+ return cancelable
832
+ }
833
+
834
+ // MARK: - Sources & Layers
835
+
836
+ // Add GeoJSON source and remember its JSON for queries
837
+ @objc public func addSourceGeoJSON(_ sourceId: String, _ geojson: String) -> Bool {
838
+ guard let mv = mapView else { return false }
839
+ // If geojson is a URL
840
+ if let url = URL(string: geojson), let scheme = url.scheme, (scheme == "http" || scheme == "https" || scheme == "file") {
841
+ var source = GeoJSONSource(id: sourceId)
842
+ source.data = .url(url)
843
+ do { try mv.mapboxMap.addSource(source); return true } catch { return false }
844
+ } else {
845
+ guard let data = geojson.data(using: .utf8) else { return false }
846
+ var source = GeoJSONSource(id: sourceId)
847
+ do {
848
+ let geoData = try JSONDecoder().decode(GeoJSONSourceData.self, from: data)
849
+ source.data = geoData
850
+ try mv.mapboxMap.addSource(source)
851
+ return true
852
+ } catch {
853
+ return false
854
+ }
855
+ }
856
+ }
857
+
858
+ @objc public func updateSourceGeoJSON(_ sourceId: String, _ geojson: String) -> Bool {
859
+ guard let mv = mapView else { return false }
860
+ guard let data = geojson.data(using: .utf8) else { return false }
861
+ do {
862
+ if let _ = try? mv.mapboxMap.source(withId: sourceId) as? GeoJSONSource {
863
+ // _ = GeoJSONSource(id: sourceId)
864
+ let geoData = try JSONDecoder().decode(GeoJSONSourceData.self, from: data)
865
+ mv.mapboxMap.updateGeoJSONSource(withId: sourceId, data: geoData)
866
+ }
867
+ return true
868
+ } catch {
869
+ return false
870
+ }
871
+ }
872
+
873
+ @objc public func removeSource(_ sourceId: String) -> Bool {
874
+ guard let mv = mapView else { return false }
875
+ do { try mv.mapboxMap.removeSource(withId: sourceId); return true } catch { return false }
876
+ }
877
+
878
+ @objc public func removeLayer(_ layerId: String) -> Bool {
879
+ guard let mv = mapView else { return false }
880
+ do {
881
+ try mv.mapboxMap.removeLayer(withId: layerId)
882
+ return true
883
+ } catch {
884
+ return false
885
+ }
886
+ }
887
+
888
+
889
+ // MARK: - Polylines / Polygons (convenience: creates source + layer)
890
+
891
+ // coordsJSON is stringified JSON array of [ [lng, lat], [lng, lat], ... ]
892
+ // optionsJSON can contain strokeColor, strokeWidth, strokeOpacity for polyline; fillColor, fillOpacity for polygon
893
+ @objc public func addPolyline(_ id: String, _ coordsJSON: String, _ optionsJSON: String?) -> Bool {
894
+ guard let mv = mapView else { return false }
895
+ guard let data = coordsJSON.data(using: .utf8), let coords = try? JSONSerialization.jsonObject(with: data, options: []) as? [[Double]] else { return false }
896
+ var ccoords = [CLLocationCoordinate2D]()
897
+ for item in coords! {
898
+ ccoords.append(CLLocationCoordinate2D(latitude: item[1], longitude: item[0]))
899
+ }
900
+ if (ccoords.isEmpty) {
901
+ return false
902
+ }
903
+
904
+
905
+ if polylineAnnotationManager == nil {
906
+ polylineAnnotationManager = mv.annotations.makePolylineAnnotationManager()
907
+ }
908
+ guard let manager = polylineAnnotationManager else { return false }
909
+ var annotation = PolylineAnnotation(id: id, lineCoordinates: ccoords)
910
+
911
+ if let opt = MapboxBridge.parseJSONParameter(optionsJSON) as? [String: Any] {
912
+ if let color = opt["strokeColor"] as? Int64 {
913
+ annotation.lineColor = (StyleColor(UIColor.init(rgbaValue: color)))
914
+ }
915
+ if let width = opt["strokeWidth"] as? Double {
916
+ annotation.lineWidth = (width)
917
+ }
918
+ if let opacity = opt["strokeOpacity"] as? Double {
919
+ annotation.lineOpacity = (opacity)
920
+ }
921
+ }
922
+
923
+ manager.annotations.removeAll { $0.id == id }
924
+ manager.annotations.append(annotation)
925
+
926
+ return true
927
+
928
+ }
929
+ @objc public func removePolygons(_ _ids: [String]?) {
930
+ guard let manager = polygonAnnotationManager else { return }
931
+ guard let ids = _ids else {
932
+ manager.annotations.removeAll()
933
+ if let outlineManager = polygonOutlineAnnotationManager {
934
+ outlineManager.annotations.removeAll()
935
+ }
936
+ return
937
+ }
938
+ // guard let data = idsJSON!.data(using: .utf8) else { return }
939
+ // guard let ids = try? JSONSerialization.jsonObject(with: data, options: []) as! [String] else { return }
940
+ //
941
+ let idSet = Set<String>(ids)
942
+ // for case let v in ids {
943
+ // idSet.insert(v)
944
+ // }
945
+ manager.annotations.removeAll { idSet.contains($0.id) }
946
+
947
+ _ = manager.annotations.filter { ann in
948
+ return !idSet.contains(ann.id)
949
+ }
950
+ if let outlineManager = polygonOutlineAnnotationManager {
951
+ outlineManager.annotations.removeAll { idSet.contains($0.id) }
952
+ }
953
+ }
954
+
955
+ @objc public func removePolylines(_ _ids: [String]?) {
956
+ guard let manager = polylineAnnotationManager else { return }
957
+ guard let ids = _ids else {
958
+ manager.annotations.removeAll()
959
+ return
960
+ }
961
+
962
+ let idSet = Set<String>(ids)
963
+
964
+ manager.annotations.removeAll { idSet.contains($0.id) }
965
+ }
966
+
967
+ enum GeoJSONSourceUpdateError: Error {
968
+ case sourceNotFound
969
+ case noGeoJSONData
970
+ case noLineStringFound
971
+ case cannotDecodeInlineString
972
+ case unsupportedDataCase
973
+ }
974
+
975
+ @objc public func addLinePoint(_ id: String, _ lnglatJSON: String, _ sourceId: String?) -> Bool {
976
+ guard let mv = mapView else { return false }
977
+ guard let data = lnglatJSON.data(using: .utf8), let coords = try? JSONSerialization.jsonObject(with: data, options: []) as? [Double] else { return false }
978
+ var actualSourceId = sourceId
979
+ if (actualSourceId == nil) {
980
+ actualSourceId = id + "_source"
981
+ }
982
+ let coordinate = CLLocationCoordinate2D(latitude: (coords![1]), longitude: (coords![0]))
983
+
984
+ guard let source = try? mv.mapboxMap.source(withId: actualSourceId!, type: GeoJSONSource.self) else { return false }
985
+
986
+ guard let data = source.data else { return false }
987
+
988
+ // Helper to convert CLLocationCoordinate2D -> Turf LocationCoordinate2D if needed
989
+ func turfCoord(from cl: CLLocationCoordinate2D) -> LocationCoordinate2D {
990
+ return LocationCoordinate2D(latitude: cl.latitude, longitude: cl.longitude)
991
+ }
992
+
993
+ // Try to update in place for supported cases
994
+ switch data {
995
+ case .featureCollection(var featureCollection):
996
+ for i in 0..<featureCollection.features.count {
997
+ guard let geom = featureCollection.features[i].geometry else { continue }
998
+
999
+ switch geom {
1000
+ case .lineString(var lineString):
1001
+ // Append coordinate and update featureCollection
1002
+ lineString.coordinates.append(turfCoord(from: coordinate))
1003
+ featureCollection.features[i].geometry = .lineString(lineString)
1004
+ mv.mapboxMap.updateGeoJSONSource(withId: actualSourceId!, data: .featureCollection(featureCollection))
1005
+ return true
1006
+ default:
1007
+ continue
1008
+ }
1009
+ }
1010
+ return false
1011
+
1012
+ case .feature(var feature):
1013
+ guard let geom = feature.geometry else {
1014
+ return false
1015
+ }
1016
+ switch geom {
1017
+ case .lineString(var lineString):
1018
+ lineString.coordinates.append(turfCoord(from: coordinate))
1019
+ feature.geometry = .lineString(lineString)
1020
+ mv.mapboxMap.updateGeoJSONSource(withId: actualSourceId!, data: .feature(feature))
1021
+ return true
1022
+ default:
1023
+ return false
1024
+ }
1025
+
1026
+ case .geometry(let geometry):
1027
+ switch geometry {
1028
+ case .lineString(var lineString):
1029
+ lineString.coordinates.append(turfCoord(from: coordinate))
1030
+ let newGeometry = Geometry.lineString(lineString)
1031
+ mv.mapboxMap.updateGeoJSONSource(withId: actualSourceId!, data: .geometry(newGeometry))
1032
+ return true
1033
+ default:
1034
+ return false
1035
+ }
1036
+
1037
+ case .string(let jsonString):
1038
+ // Try to decode the inline JSON string into Turf types and proceed
1039
+ guard let jsonData = jsonString.data(using: .utf8) else {
1040
+ return false
1041
+ }
1042
+
1043
+ let decoder = JSONDecoder()
1044
+ // Try FeatureCollection
1045
+ if var fc = try? decoder.decode(FeatureCollection.self, from: jsonData) {
1046
+ for i in 0..<fc.features.count {
1047
+ guard let geom = fc.features[i].geometry else { continue }
1048
+ switch geom {
1049
+ case .lineString(var lineString):
1050
+ lineString.coordinates.append(turfCoord(from: coordinate))
1051
+ fc.features[i].geometry = .lineString(lineString)
1052
+ mv.mapboxMap.updateGeoJSONSource(withId: actualSourceId!, data: .featureCollection(fc))
1053
+ return true
1054
+ default:
1055
+ continue
1056
+ }
1057
+ }
1058
+ return false
1059
+ }
1060
+
1061
+ // Try Feature
1062
+ if var f = try? decoder.decode(Feature.self, from: jsonData) {
1063
+ guard let geom = f.geometry else { return false }
1064
+ switch geom {
1065
+ case .lineString(var lineString):
1066
+ lineString.coordinates.append(turfCoord(from: coordinate))
1067
+ f.geometry = .lineString(lineString)
1068
+ mv.mapboxMap.updateGeoJSONSource(withId: actualSourceId!, data: .feature(f))
1069
+ return true
1070
+ default:
1071
+ return false
1072
+ }
1073
+ }
1074
+
1075
+ // Try Geometry
1076
+ if let g = try? decoder.decode(Geometry.self, from: jsonData) {
1077
+ switch g {
1078
+ case .lineString(var lineString):
1079
+ lineString.coordinates.append(turfCoord(from: coordinate))
1080
+ let newGeometry = Geometry.lineString(lineString)
1081
+ mv.mapboxMap.updateGeoJSONSource(withId: actualSourceId!, data: .geometry(newGeometry))
1082
+ return true
1083
+ default:
1084
+ return false
1085
+ }
1086
+ }
1087
+
1088
+ return false
1089
+
1090
+ default:
1091
+ return false
1092
+ }
1093
+ }
1094
+
1095
+ @objc public func addPolygon(_ id: String, _ coordsJSON: String, _ optionsJSON: String?) -> Bool {
1096
+ guard let mv = mapView else { return false }
1097
+ guard let coords = MapboxBridge.parseJSONParameter(coordsJSON) as? [[Double]] else { return false }
1098
+ var ccoords = [CLLocationCoordinate2D]()
1099
+ for item in coords {
1100
+ ccoords.append(CLLocationCoordinate2D(latitude: item[1], longitude: item[0]))
1101
+ }
1102
+ if (ccoords.isEmpty) {
1103
+ return false
1104
+ }
1105
+
1106
+
1107
+ if polygonAnnotationManager == nil {
1108
+ polygonAnnotationManager = mv.annotations.makePolygonAnnotationManager()
1109
+ }
1110
+ guard let manager = polygonAnnotationManager else { return false }
1111
+ let polygon = Polygon(outerRing: .init(coordinates: ccoords))
1112
+ var annotation = PolygonAnnotation(id: id, polygon: polygon)
1113
+
1114
+ if let opt = MapboxBridge.parseJSONParameter(optionsJSON) as? [String: Any] {
1115
+ let strokeColor = opt["strokeColor"] as? Int64
1116
+ let strokeOpacity = opt["strokeOpacity"] as? Double
1117
+ let strokeWidth = opt["strokeWidth"] as? Double
1118
+ if let color = opt["fillColor"] as? Int64 {
1119
+ annotation.fillColor = (StyleColor(UIColor.init(rgbaValue: color)))
1120
+ }
1121
+ if (strokeColor != nil) && (strokeWidth == nil) {
1122
+ annotation.fillOutlineColor = (StyleColor(UIColor.init(rgbaValue: strokeColor!)))
1123
+ }
1124
+ if let opacity = opt["fillOpacity"] as? Double {
1125
+ annotation.fillOpacity = (opacity)
1126
+ }
1127
+
1128
+
1129
+ if (strokeOpacity != nil || strokeWidth != nil){
1130
+ if polygonOutlineAnnotationManager == nil {
1131
+ polygonOutlineAnnotationManager = mv.annotations.makePolylineAnnotationManager()
1132
+ }
1133
+ var outline = PolylineAnnotation(id: id, lineCoordinates: ccoords)
1134
+ if (strokeColor != nil) {
1135
+ outline.lineColor = (StyleColor(UIColor.init(rgbaValue: strokeColor!)))
1136
+ }
1137
+ if (strokeOpacity != nil) {
1138
+ outline.lineOpacity = strokeOpacity
1139
+ }
1140
+ if (strokeWidth != nil) {
1141
+ outline.lineWidth = strokeWidth
1142
+ }
1143
+ outline.lineJoin = .round
1144
+ // Replace existing outline with same id
1145
+ polygonOutlineAnnotationManager!.annotations.removeAll { $0.id == id }
1146
+ polygonOutlineAnnotationManager!.annotations.append(outline)
1147
+ }
1148
+ }
1149
+
1150
+ manager.annotations.removeAll { $0.id == id }
1151
+ manager.annotations.append(annotation)
1152
+
1153
+ return true
1154
+ }
1155
+ // MARK: - User location, track and tilt
1156
+
1157
+
1158
+ @objc public func forceUserLocationUpdate() -> Bool {
1159
+ guard mapView != nil else { return false }
1160
+
1161
+ return false
1162
+ }
1163
+
1164
+
1165
+ @objc public func trackUser(_ optionsJSON: String) -> Bool {
1166
+ guard let mv = mapView else { return false }
1167
+
1168
+ guard let data = optionsJSON.data(using: .utf8),
1169
+ let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
1170
+ return false
1171
+ }
1172
+
1173
+ self.userTrackingCameraMode = ((obj?["cameraMode"] as? String ?? "TRACKING")).uppercased()
1174
+ let renderMode = ((obj?["renderMode"] as? String ?? "GPS")).uppercased()
1175
+ self.userTrackingCameraAnimated = (obj?["animated"] as? Bool ?? true)
1176
+ let imageName = obj?["image"] as? String
1177
+
1178
+ // If cameraMode starts with NONE -> stop tracking immediately
1179
+ // if cameraModeRaw.hasPrefix("NONE") {
1180
+ // stopTrackingUser()
1181
+ // return true
1182
+ // }
1183
+
1184
+
1185
+ // Resolve optional image
1186
+ _ = resolveImage(named: imageName)
1187
+
1188
+ // Map renderMode to showBearing: COMPASS -> true, GPS/NORMAL -> false
1189
+ let showBearing: Bool
1190
+ switch renderMode {
1191
+ case "COMPASS": showBearing = true
1192
+ case "GPS": showBearing = false
1193
+ default: showBearing = false
1194
+ }
1195
+
1196
+ let configuration = Puck2DConfiguration.makeDefault(showBearing: showBearing)
1197
+ mv.location.options.puckType = .puck2D(configuration)
1198
+ locationTrackingCancellation?.cancel()
1199
+ locationTrackingCancellation = mv.location.onLocationChange.observe { [weak self] newLocation in
1200
+ guard let location = newLocation.last else { return }
1201
+ guard let self = self else { return }
1202
+ guard let mv = self.mapView else { return }
1203
+ // Camera movement behavior based on trackingCameraMode
1204
+ // let bearing = 0.0
1205
+ var bearing = Double(mv.mapboxMap.cameraState.bearing)
1206
+ if (location.bearing != nil) {
1207
+ bearing = Double(location.bearing!)
1208
+ }
1209
+ //
1210
+ var cameraOptions: CameraOptions? = nil
1211
+ switch self.userTrackingCameraMode {
1212
+ case "TRACKING":
1213
+ cameraOptions = CameraOptions(center: location.coordinate)
1214
+ case "TRACKING_COMPASS":
1215
+ // use heading as bearing if available
1216
+ cameraOptions = CameraOptions(center: location.coordinate, bearing: bearing)
1217
+ case "TRACKING_GPS":
1218
+ // approximate as follow (no heading)
1219
+ cameraOptions = CameraOptions(center: location.coordinate)
1220
+ case "TRACKING_GPS_NORTH":
1221
+ // use course for bearing
1222
+ cameraOptions = CameraOptions(center: location.coordinate, bearing: bearing)
1223
+ default:
1224
+ break
1225
+ }
1226
+ if (cameraOptions != nil) {
1227
+ if (self.userTrackingCameraAnimated) {
1228
+ mv.camera.ease(to: CameraOptions(center: location.coordinate), duration: 0.5)
1229
+ } else {
1230
+ mv.camera.cancelAnimations()
1231
+ mv.mapboxMap.setCamera(to: CameraOptions(center: location.coordinate))
1232
+ }
1233
+ }
1234
+ // Post notification for TS listeners
1235
+ var userInfo: [String: Any] = ["lat": location.coordinate.latitude, "lng": location.coordinate.longitude]
1236
+ userInfo["accuracy"] = location.horizontalAccuracy
1237
+ if ((location.bearing?.isFinite) != nil) { userInfo["heading"] = location.bearing }
1238
+ self.postEvent(MapboxBridge.UserLocationUpdatedNotification, userInfo)
1239
+ }
1240
+
1241
+
1242
+ // Emit state change
1243
+ let stateInfo: [String: Any] = ["tracking": true, "cameraMode": self.userTrackingCameraMode, "renderMode": renderMode]
1244
+ self.postEvent(MapboxBridge.UserTrackingStateChangedNotification, stateInfo)
1245
+
1246
+ return true
1247
+ }
1248
+
1249
+ // MARK: - stopTrackingUser
1250
+ @objc public func stopTrackingUser() {
1251
+ guard let mv = mapView else { return }
1252
+
1253
+ if (locationTrackingCancellation != nil){
1254
+ locationTrackingCancellation?.cancel()
1255
+ locationTrackingCancellation = nil
1256
+
1257
+ let stateInfo: [String: Any] = ["tracking": false]
1258
+ self.postEvent(MapboxBridge.UserTrackingStateChangedNotification, stateInfo)
1259
+ }
1260
+ mv.location.options.puckType = .none
1261
+ }
1262
+
1263
+
1264
+ @objc public func getTilt() -> NSNumber? {
1265
+ guard let mv = mapView else { return nil }
1266
+ return NSNumber(value: mv.mapboxMap.cameraState.pitch)
1267
+ }
1268
+
1269
+ @objc public func setTilt(_ tilt: Double, _ animated: Bool) {
1270
+ guard let mv = mapView else { return }
1271
+ let cam = CameraOptions(pitch: tilt)
1272
+ if animated { mv.camera.ease(to: cam, duration: 0.5, completion: { _ in }) } else { mv.mapboxMap.setCamera(to: cam) }
1273
+ }
1274
+
1275
+ @objc public func getUserLocation() -> String? {
1276
+ guard let mv = mapView else { return nil }
1277
+ if let latest = mv.location.latestLocation {
1278
+ if let coordVal = (latest as AnyObject).coordinate {
1279
+ return MapboxBridge.encodeToJSON(["lat": coordVal.latitude, "lng": coordVal.longitude])
1280
+ }
1281
+ }
1282
+ return nil
1283
+ }
1284
+ let aliases: [String: StyleURI] = [
1285
+ "streets": .streets,
1286
+ "outdoors": .outdoors,
1287
+ "light": .light,
1288
+ "dark": .dark,
1289
+ "satellite": .satellite,
1290
+ "satellite-streets": .satelliteStreets,
1291
+ "satellite_streets": .satelliteStreets,
1292
+ "standard": .standard
1293
+ ]
1294
+ private func getMapStyleURI(_ str: String?) -> StyleURI {
1295
+ if (str == nil) {
1296
+ return .streets
1297
+ }
1298
+ if let mapped = aliases[str!.lowercased()] {
1299
+ return mapped
1300
+ }
1301
+ if let styleURI = StyleURI(rawValue: str!) {
1302
+ return styleURI
1303
+ }
1304
+ return .streets
1305
+ }
1306
+
1307
+ // MARK: - Offline (TileStore)
1308
+
1309
+ @objc public func downloadOfflineRegion(_ optionsJSON: String, _ progress: @escaping (String?) -> Void, _ completion: @escaping (Bool, NSError?) -> Void) {
1310
+ guard mapView != nil else { completion(false, NSError(domain: "MapboxBridge", code: 6, userInfo: [NSLocalizedDescriptionKey: "No map available"])); return }
1311
+ let ts = TileStore.default
1312
+
1313
+
1314
+ guard let obj = MapboxBridge.parseJSONParameter(optionsJSON) as? [String: Any] else {
1315
+ completion(false, NSError(domain: "MapboxBridge", code: 8, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON options"])); return
1316
+ }
1317
+
1318
+ guard let name = obj["name"] as? String else { completion(false, NSError(domain: "MapboxBridge", code: 9, userInfo: [NSLocalizedDescriptionKey: "Missing 'name' param"])); return }
1319
+ guard let styleURL = obj["style"] as? String ?? obj["styleURL"] as? String ?? obj["styleUrl"] as? String else { completion(false, NSError(domain: "MapboxBridge", code: 10, userInfo: [NSLocalizedDescriptionKey: "Missing 'styleURL' or 'styleUrl' param"])); return }
1320
+ guard let bounds = obj["bounds"] as? [String: Any],
1321
+ let north = bounds["north"] as? Double,
1322
+ let east = bounds["east"] as? Double,
1323
+ let south = bounds["south"] as? Double,
1324
+ let west = bounds["west"] as? Double else {
1325
+ completion(false, NSError(domain: "MapboxBridge", code: 11, userInfo: [NSLocalizedDescriptionKey: "Invalid or missing 'bounds' param"])); return
1326
+ }
1327
+
1328
+ let minZoom = obj["minZoom"] as? Float ?? 0.0
1329
+ let maxZoom = obj["maxZoom"] as? Float ?? 16.0
1330
+ var metadata = obj["metadata"] as? [String: Any] ?? [String: Any]()
1331
+ let regionId = obj["regionId"] as? String ?? String(Date().timeIntervalSince1970 * 1000)
1332
+ metadata["name"] = name
1333
+ metadata["styleUrl"] = styleURL
1334
+ metadata["bounds"] = bounds
1335
+ metadata["minZoom"] = minZoom
1336
+ metadata["maxZoom"] = maxZoom
1337
+ metadata["regionId"] = regionId
1338
+
1339
+ // Turf uses LocationCoordinate2D; create the ring and repeat first point to close it
1340
+ let outerRing: [LocationCoordinate2D] = [
1341
+ LocationCoordinate2D(latitude: north, longitude: west),
1342
+ LocationCoordinate2D(latitude: north, longitude: east),
1343
+ LocationCoordinate2D(latitude: south, longitude: east),
1344
+ LocationCoordinate2D(latitude: south, longitude: west),
1345
+ LocationCoordinate2D(latitude: north, longitude: west) // close ring
1346
+ ]
1347
+
1348
+ // Create a Polygon and its Geometry
1349
+ let polygon = Polygon([outerRing])
1350
+
1351
+
1352
+
1353
+ // 2. Create an offline region with tiles for the Standard or Satellite-Streets style.
1354
+ // If you are using a raster tileset you may need to set a different pixelRatio. The default is UIScreen.main.scale.
1355
+ let styleOptions = TilesetDescriptorOptions(styleURI: getMapStyleURI(styleURL),
1356
+ zoomRange: UInt8(minZoom)...UInt8(maxZoom),
1357
+ tilesets: nil)
1358
+ // Load the tile region
1359
+ let styleDescriptor = offlineManager.createTilesetDescriptor(for: styleOptions)
1360
+
1361
+ let tileRegionLoadOptions = TileRegionLoadOptions(
1362
+ geometry: polygon.geometry,
1363
+ descriptors: [styleDescriptor],
1364
+ metadata: metadata,
1365
+ acceptExpired: true)!
1366
+
1367
+ let request = ts.loadTileRegion(forId: name, loadOptions: tileRegionLoadOptions, progress: { [weak self] prog in
1368
+ guard self != nil else { return }
1369
+ var info: [String: Any] = ["name": name]
1370
+ info["expected"] = prog.requiredResourceCount
1371
+ info["completed"] = prog.completedResourceCount
1372
+ info["completedSize"] = prog.completedResourceSize
1373
+ if prog.requiredResourceCount > 0 {
1374
+ info["percentage"] = round(Double(prog.completedResourceCount) / Double(prog.requiredResourceCount) * 10000.0) / 100.0
1375
+ info["complete"] = (prog.completedResourceCount >= prog.requiredResourceCount)
1376
+ } else {
1377
+ info["percentage"] = Double(0.0)
1378
+ info["complete"] = false
1379
+ }
1380
+ progress(MapboxBridge.encodeToJSON(info))
1381
+ }, completion: { [weak self] result in
1382
+ guard let self = self else { completion(false, NSError(domain: "MapboxBridge", code: 12, userInfo: [NSLocalizedDescriptionKey: "Bridge deallocated"])) ; return }
1383
+ self.tileRegionLoadRequestByName.removeValue(forKey: name)
1384
+ switch result {
1385
+ case .success(_):
1386
+ let md: [String: Any] = ["name": name, "styleUrl": styleURL, "minZoom": minZoom, "maxZoom": maxZoom, "bounds": ["north": north, "east": east, "south": south, "west": west]]
1387
+ // if let regionMetadata = region.metadata { md["metadata"] = regionMetadata }
1388
+ self.postEvent(MapboxBridge.OfflineCompleteNotification, md)
1389
+ completion(true, nil)
1390
+ case .failure(let error):
1391
+ let nsErr = error as NSError
1392
+ completion(false, nsErr)
1393
+ @unknown default:
1394
+ completion(false, NSError(domain: "MapboxBridge", code: 13, userInfo: [NSLocalizedDescriptionKey: "Unknown offline result"]))
1395
+ }
1396
+ })
1397
+
1398
+ tileRegionLoadRequestByName[name] = request
1399
+ completion(true, nil)
1400
+ }
1401
+
1402
+ @objc public func listOfflineRegions( _ completion: @escaping (String?) -> Void) {
1403
+ let store = TileStore.default
1404
+
1405
+ store.allTileRegions { result in
1406
+ switch result {
1407
+ case .success(let regions):
1408
+ guard !regions.isEmpty else {
1409
+ DispatchQueue.main.async { completion("[]") }
1410
+ return
1411
+ }
1412
+
1413
+ var collected: [[String: Any]] = []
1414
+ let group = DispatchGroup()
1415
+ let lock = NSLock()
1416
+
1417
+ for region in regions {
1418
+ // Each region has an `id` property.
1419
+ let id = region.id
1420
+ group.enter()
1421
+ store.tileRegionMetadata(forId: id) { metaResult in
1422
+ switch metaResult {
1423
+ case .success(let m):
1424
+ lock.lock()
1425
+
1426
+ guard let metadata = m as? [String: Any] else {
1427
+ lock.unlock()
1428
+ group.leave()
1429
+ return
1430
+ }
1431
+ var data = [String: Any]()
1432
+ data["id"] = id
1433
+ data["name"] = metadata["name"]
1434
+ data["style"] = metadata["styleUrl"]
1435
+ data["minZoom"] = metadata["minZoom"]
1436
+ data["maxZoom"] = metadata["maxZoom"]
1437
+ data["metadata"] = metadata
1438
+ collected.append(data)
1439
+ lock.unlock()
1440
+ break
1441
+ case .failure:
1442
+ break
1443
+ }
1444
+ group.leave()
1445
+ }
1446
+ }
1447
+
1448
+ group.notify(queue: .global(qos: .utility)) {
1449
+ do {
1450
+ let data = try JSONSerialization.data(withJSONObject: collected, options: [])
1451
+ DispatchQueue.main.async { completion(String(data: data, encoding: .utf8) as String?) }
1452
+ } catch {
1453
+ DispatchQueue.main.async { completion("[]") }
1454
+ }
1455
+ }
1456
+ return
1457
+
1458
+ case .failure:
1459
+ // If we can't list regions, return empty array on main queue.
1460
+ DispatchQueue.main.async { completion("[]") }
1461
+ }
1462
+ }
1463
+ }
1464
+
1465
+ @objc public func deleteOfflineRegion(_ idOrName: String) {
1466
+ let ts = TileStore.default
1467
+
1468
+ ts.removeTileRegion(forId: idOrName)
1469
+ }
1470
+
1471
+
1472
+ // allow TS to set a camera-change callback directly (in addition to NotificationCenter)
1473
+ @objc public func setOnCameraChangeListener(_ callback: @escaping (NSDictionary) -> Void) {
1474
+ self.cameraChangeCallback = { dict in
1475
+ callback(dict as NSDictionary)
1476
+ }
1477
+ }
1478
+
1479
+ }