@nativescript-community/ui-mapbox 7.0.0-alpha.14.3191a7b → 7.0.1

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