@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.
- package/CHANGELOG.md +7 -0
- package/common.d.ts +56 -26
- package/common.js +44 -28
- package/expression/expression-parser.android.d.ts +2 -2
- package/expression/expression-parser.android.js +4 -3
- package/expression/expression-parser.ios.d.ts +2 -2
- package/expression/expression-parser.ios.js +28 -13
- package/index.android.d.ts +59 -66
- package/index.android.js +1388 -1244
- package/index.d.ts +36 -5
- package/index.ios.d.ts +72 -243
- package/index.ios.js +1161 -1999
- package/layers/layer-factory.android.d.ts +7 -5
- package/layers/layer-factory.android.js +71 -41
- package/layers/layer-factory.d.ts +2 -1
- package/layers/layer-factory.ios.d.ts +8 -8
- package/layers/layer-factory.ios.js +46 -100
- package/layers/parser/property-parser.android.d.ts +3 -1
- package/layers/parser/property-parser.android.js +25 -24
- package/layers/parser/property-parser.d.ts +1 -1
- package/layers/parser/property-parser.ios.d.ts +0 -2
- package/layers/parser/property-parser.ios.js +0 -149
- package/markers/Marker.android.d.ts +28 -0
- package/markers/Marker.android.js +54 -0
- package/markers/Marker.common.d.ts +2 -0
- package/markers/Marker.common.js +31 -0
- package/markers/MarkerManager.android.d.ts +35 -0
- package/markers/MarkerManager.android.js +220 -0
- package/package.json +7 -6
- package/platforms/android/include.gradle +31 -27
- package/platforms/android/ui_mapbox.aar +0 -0
- package/platforms/ios/Podfile +3 -1
- package/platforms/ios/Resources/default_pin.png +0 -0
- package/platforms/ios/src/MapboxBridge.swift +1479 -0
- package/platforms/ios/src/NativeExpressionParser.swift +33 -0
- package/platforms/ios/src/NativeLayerFactory.swift +108 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/typings/Mapbox.ios.d.ts +2 -3242
- package/typings/geojson.android.d.ts +689 -0
- package/typings/index.android.d.ts +46 -0
- package/typings/mapbox.android.d.ts +39968 -12560
- 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
|
+
}
|