@atomiqlab/react-native-mapbox-navigation 1.1.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 +18 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +46 -0
- package/README.md +131 -0
- package/android/build.gradle +64 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/MapboxNavigationActivity.kt +421 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/MapboxNavigationEventBridge.kt +18 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/MapboxNavigationModule.kt +296 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/MapboxNavigationViewManager.kt +143 -0
- package/app.plugin.js +154 -0
- package/docs/PUBLISHING.md +97 -0
- package/docs/TROUBLESHOOTING.md +35 -0
- package/docs/USAGE.md +100 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoMapboxNavigationNative.podspec +31 -0
- package/ios/MapboxNavigationModule.swift +613 -0
- package/ios/MapboxNavigationView.swift +298 -0
- package/package.json +75 -0
- package/scripts/verify-release.mjs +115 -0
- package/src/MapboxNavigation.types.ts +136 -0
- package/src/index.tsx +204 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import MapboxNavigation
|
|
3
|
+
import MapboxDirections
|
|
4
|
+
import MapboxCoreNavigation
|
|
5
|
+
import CoreLocation
|
|
6
|
+
import UIKit
|
|
7
|
+
|
|
8
|
+
public class MapboxNavigationModule: Module {
|
|
9
|
+
private var navigationViewController: NavigationViewController?
|
|
10
|
+
private var isCurrentlyNavigating = false
|
|
11
|
+
private var currentLanguage = Locale.preferredLanguages.first ?? "en"
|
|
12
|
+
private var currentCameraMode = "following"
|
|
13
|
+
private var currentLocationResolver: CurrentLocationResolver?
|
|
14
|
+
|
|
15
|
+
public func definition() -> ModuleDefinition {
|
|
16
|
+
Name("MapboxNavigationModule")
|
|
17
|
+
|
|
18
|
+
// Events that can be sent to JS
|
|
19
|
+
Events(
|
|
20
|
+
"onLocationChange",
|
|
21
|
+
"onRouteProgressChange",
|
|
22
|
+
"onBannerInstruction",
|
|
23
|
+
"onArrive",
|
|
24
|
+
"onCancelNavigation",
|
|
25
|
+
"onError"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// Start navigation
|
|
29
|
+
AsyncFunction("startNavigation") { (options: NavigationStartOptions, promise: Promise) in
|
|
30
|
+
DispatchQueue.main.async {
|
|
31
|
+
self.startNavigation(options: options, promise: promise)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Stop navigation
|
|
36
|
+
AsyncFunction("stopNavigation") { (promise: Promise) in
|
|
37
|
+
DispatchQueue.main.async {
|
|
38
|
+
self.stopNavigation(promise: promise)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Set muted state
|
|
43
|
+
AsyncFunction("setMuted") { (muted: Bool, promise: Promise) in
|
|
44
|
+
DispatchQueue.main.async {
|
|
45
|
+
self.setMuted(muted: muted, promise: promise)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
AsyncFunction("setVoiceVolume") { (volume: Double, promise: Promise) in
|
|
50
|
+
DispatchQueue.main.async {
|
|
51
|
+
self.setVoiceVolume(volume: volume, promise: promise)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
AsyncFunction("setDistanceUnit") { (unit: String, promise: Promise) in
|
|
56
|
+
DispatchQueue.main.async {
|
|
57
|
+
self.setDistanceUnit(unit: unit, promise: promise)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
AsyncFunction("setLanguage") { (language: String, promise: Promise) in
|
|
62
|
+
DispatchQueue.main.async {
|
|
63
|
+
self.setLanguage(language: language, promise: promise)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if navigating
|
|
68
|
+
AsyncFunction("isNavigating") { (promise: Promise) in
|
|
69
|
+
promise.resolve(self.isCurrentlyNavigating)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
AsyncFunction("getNavigationSettings") { (promise: Promise) in
|
|
73
|
+
let unit: String = NavigationSettings.shared.distanceUnit == .mile ? "imperial" : "metric"
|
|
74
|
+
promise.resolve([
|
|
75
|
+
"isNavigating": self.isCurrentlyNavigating,
|
|
76
|
+
"mute": NavigationSettings.shared.voiceMuted,
|
|
77
|
+
"voiceVolume": NavigationSettings.shared.voiceVolume,
|
|
78
|
+
"distanceUnit": unit,
|
|
79
|
+
"language": self.currentLanguage
|
|
80
|
+
])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// View for embedded navigation
|
|
84
|
+
View(MapboxNavigationView.self) {
|
|
85
|
+
Events(
|
|
86
|
+
"onLocationChange",
|
|
87
|
+
"onRouteProgressChange",
|
|
88
|
+
"onBannerInstruction",
|
|
89
|
+
"onArrive",
|
|
90
|
+
"onCancelNavigation",
|
|
91
|
+
"onError"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
Prop("startOrigin") { (view: MapboxNavigationView, origin: [String: Double]) in
|
|
95
|
+
view.startOrigin = origin
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Prop("destination") { (view: MapboxNavigationView, destination: [String: Any]) in
|
|
99
|
+
view.destination = destination
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Prop("waypoints") { (view: MapboxNavigationView, waypoints: [[String: Any]]?) in
|
|
103
|
+
view.waypoints = waypoints
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Prop("shouldSimulateRoute") { (view: MapboxNavigationView, simulate: Bool) in
|
|
107
|
+
view.shouldSimulateRoute = simulate
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Prop("showCancelButton") { (view: MapboxNavigationView, show: Bool) in
|
|
111
|
+
view.showCancelButton = show
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
Prop("mute") { (view: MapboxNavigationView, mute: Bool) in
|
|
115
|
+
view.mute = mute
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Prop("voiceVolume") { (view: MapboxNavigationView, volume: Double) in
|
|
119
|
+
view.voiceVolume = volume
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
Prop("cameraPitch") { (view: MapboxNavigationView, pitch: Double) in
|
|
123
|
+
view.cameraPitch = pitch
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Prop("cameraZoom") { (view: MapboxNavigationView, zoom: Double) in
|
|
127
|
+
view.cameraZoom = zoom
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Prop("cameraMode") { (view: MapboxNavigationView, mode: String) in
|
|
131
|
+
view.cameraMode = mode
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Prop("mapStyleUri") { (view: MapboxNavigationView, mapStyleUri: String) in
|
|
135
|
+
view.mapStyleUri = mapStyleUri
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Prop("routeAlternatives") { (view: MapboxNavigationView, routeAlternatives: Bool) in
|
|
139
|
+
view.routeAlternatives = routeAlternatives
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Prop("showsSpeedLimits") { (view: MapboxNavigationView, showsSpeedLimits: Bool) in
|
|
143
|
+
view.showsSpeedLimits = showsSpeedLimits
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Prop("showsWayNameLabel") { (view: MapboxNavigationView, showsWayNameLabel: Bool) in
|
|
147
|
+
view.showsWayNameLabel = showsWayNameLabel
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Prop("distanceUnit") { (view: MapboxNavigationView, unit: String) in
|
|
151
|
+
view.distanceUnit = unit
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
Prop("language") { (view: MapboxNavigationView, language: String) in
|
|
155
|
+
view.language = language
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private func startNavigation(options: NavigationStartOptions, promise: Promise) {
|
|
161
|
+
guard let destination = options.destination.toCLLocationCoordinate2D() else {
|
|
162
|
+
promise.reject("INVALID_COORDINATES", "Invalid coordinates provided")
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if let origin = options.startOrigin?.toCLLocationCoordinate2D() {
|
|
167
|
+
startNavigation(
|
|
168
|
+
origin: origin,
|
|
169
|
+
destination: destination,
|
|
170
|
+
options: options,
|
|
171
|
+
promise: promise
|
|
172
|
+
)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
resolveCurrentOrigin { [weak self] result in
|
|
177
|
+
guard let self = self else { return }
|
|
178
|
+
switch result {
|
|
179
|
+
case .success(let origin):
|
|
180
|
+
self.startNavigation(
|
|
181
|
+
origin: origin,
|
|
182
|
+
destination: destination,
|
|
183
|
+
options: options,
|
|
184
|
+
promise: promise
|
|
185
|
+
)
|
|
186
|
+
case .failure(let error):
|
|
187
|
+
promise.reject("CURRENT_LOCATION_UNAVAILABLE", error.localizedDescription)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func startNavigation(
|
|
193
|
+
origin: CLLocationCoordinate2D,
|
|
194
|
+
destination: CLLocationCoordinate2D,
|
|
195
|
+
options: NavigationStartOptions,
|
|
196
|
+
promise: Promise
|
|
197
|
+
) {
|
|
198
|
+
|
|
199
|
+
var waypoints = [Waypoint(coordinate: origin)]
|
|
200
|
+
|
|
201
|
+
// Add intermediate waypoints if provided
|
|
202
|
+
if let intermediateWaypoints = options.waypoints {
|
|
203
|
+
for wp in intermediateWaypoints {
|
|
204
|
+
if let coord = wp.toCLLocationCoordinate2D() {
|
|
205
|
+
let waypoint = Waypoint(coordinate: coord)
|
|
206
|
+
waypoint.name = wp.name
|
|
207
|
+
waypoints.append(waypoint)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Add final destination
|
|
213
|
+
let finalWaypoint = Waypoint(coordinate: destination)
|
|
214
|
+
finalWaypoint.name = options.destination.name ?? "Destination"
|
|
215
|
+
waypoints.append(finalWaypoint)
|
|
216
|
+
|
|
217
|
+
let routeOptions = NavigationRouteOptions(waypoints: waypoints)
|
|
218
|
+
routeOptions.locale = Locale(identifier: options.language ?? currentLanguage)
|
|
219
|
+
routeOptions.distanceMeasurementSystem = options.distanceUnit == "imperial" ? .imperial : .metric
|
|
220
|
+
routeOptions.includesAlternativeRoutes = options.routeAlternatives == true
|
|
221
|
+
|
|
222
|
+
Directions.shared.calculate(routeOptions) { [weak self] (_, result) in
|
|
223
|
+
guard let self = self else { return }
|
|
224
|
+
|
|
225
|
+
switch result {
|
|
226
|
+
case .success(let response):
|
|
227
|
+
guard response.routes?.first != nil else {
|
|
228
|
+
promise.reject("NO_ROUTE", "No route found")
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let indexedRouteResponse = IndexedRouteResponse(routeResponse: response, routeIndex: 0)
|
|
233
|
+
let navigationService = MapboxNavigationService(
|
|
234
|
+
indexedRouteResponse: indexedRouteResponse,
|
|
235
|
+
credentials: Directions.shared.credentials,
|
|
236
|
+
simulating: options.shouldSimulateRoute == true ? .always : nil
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
let navigationOptions = self.buildNavigationOptions(
|
|
240
|
+
navigationService: navigationService,
|
|
241
|
+
mapStyleUri: options.mapStyleUri
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
let viewController = NavigationViewController(
|
|
245
|
+
for: indexedRouteResponse,
|
|
246
|
+
navigationOptions: navigationOptions
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
viewController.delegate = self
|
|
250
|
+
viewController.modalPresentationStyle = .fullScreen
|
|
251
|
+
|
|
252
|
+
self.currentLanguage = options.language ?? self.currentLanguage
|
|
253
|
+
NavigationSettings.shared.distanceUnit = options.distanceUnit == "imperial" ? .mile : .kilometer
|
|
254
|
+
NavigationSettings.shared.voiceMuted = options.mute == true
|
|
255
|
+
if let volume = options.voiceVolume {
|
|
256
|
+
NavigationSettings.shared.voiceVolume = Float(max(0, min(volume, 1)))
|
|
257
|
+
}
|
|
258
|
+
self.applyCameraConfiguration(
|
|
259
|
+
to: viewController,
|
|
260
|
+
mode: options.cameraMode,
|
|
261
|
+
pitch: options.cameraPitch,
|
|
262
|
+
zoom: options.cameraZoom
|
|
263
|
+
)
|
|
264
|
+
self.currentCameraMode = options.cameraMode ?? "following"
|
|
265
|
+
viewController.showsSpeedLimits = options.showsSpeedLimits ?? true
|
|
266
|
+
|
|
267
|
+
self.navigationViewController = viewController
|
|
268
|
+
self.isCurrentlyNavigating = true
|
|
269
|
+
|
|
270
|
+
if let rootVC = Self.currentTopViewController() {
|
|
271
|
+
rootVC.present(viewController, animated: true) {
|
|
272
|
+
promise.resolve(nil)
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
promise.reject("NO_ROOT_VC", "Could not find root view controller")
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case .failure(let error):
|
|
279
|
+
promise.reject("ROUTE_ERROR", error.localizedDescription)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private func resolveCurrentOrigin(
|
|
285
|
+
completion: @escaping (Result<CLLocationCoordinate2D, Error>) -> Void
|
|
286
|
+
) {
|
|
287
|
+
let resolver = CurrentLocationResolver()
|
|
288
|
+
currentLocationResolver = resolver
|
|
289
|
+
resolver.resolve { [weak self] result in
|
|
290
|
+
self?.currentLocationResolver = nil
|
|
291
|
+
completion(result)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private func stopNavigation(promise: Promise) {
|
|
296
|
+
guard let navVC = navigationViewController else {
|
|
297
|
+
promise.resolve(nil)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
navVC.dismiss(animated: true) {
|
|
302
|
+
self.navigationViewController = nil
|
|
303
|
+
self.isCurrentlyNavigating = false
|
|
304
|
+
promise.resolve(nil)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private func setMuted(muted: Bool, promise: Promise) {
|
|
309
|
+
NavigationSettings.shared.voiceMuted = muted
|
|
310
|
+
promise.resolve(nil)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private func setVoiceVolume(volume: Double, promise: Promise) {
|
|
314
|
+
NavigationSettings.shared.voiceVolume = Float(max(0, min(volume, 1)))
|
|
315
|
+
promise.resolve(nil)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private func setDistanceUnit(unit: String, promise: Promise) {
|
|
319
|
+
NavigationSettings.shared.distanceUnit = unit == "imperial" ? .mile : .kilometer
|
|
320
|
+
promise.resolve(nil)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private func setLanguage(language: String, promise: Promise) {
|
|
324
|
+
let trimmed = language.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
325
|
+
if trimmed.isEmpty {
|
|
326
|
+
promise.reject("INVALID_LANGUAGE", "Language cannot be empty")
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
currentLanguage = trimmed
|
|
330
|
+
promise.resolve(nil)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private func applyCameraConfiguration(
|
|
334
|
+
to viewController: NavigationViewController,
|
|
335
|
+
mode: String?,
|
|
336
|
+
pitch: Double?,
|
|
337
|
+
zoom: Double?
|
|
338
|
+
) {
|
|
339
|
+
guard
|
|
340
|
+
let navigationMapView = viewController.navigationMapView,
|
|
341
|
+
let viewportDataSource = navigationMapView.navigationCamera
|
|
342
|
+
.viewportDataSource as? NavigationViewportDataSource else {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let normalizedMode = (mode ?? "following").lowercased()
|
|
347
|
+
|
|
348
|
+
if normalizedMode == "overview" {
|
|
349
|
+
viewportDataSource.options.followingCameraOptions.zoomUpdatesAllowed = false
|
|
350
|
+
viewportDataSource.followingMobileCamera.zoom = CGFloat(zoom ?? 10)
|
|
351
|
+
viewportDataSource.options.followingCameraOptions.pitchUpdatesAllowed = false
|
|
352
|
+
viewportDataSource.followingMobileCamera.pitch = 0
|
|
353
|
+
} else {
|
|
354
|
+
// Keep dynamic camera updates in following mode so turn-by-turn camera behavior
|
|
355
|
+
// (zoom/pitch/bearing adaptation) remains managed by the SDK.
|
|
356
|
+
viewportDataSource.options.followingCameraOptions.pitchUpdatesAllowed = true
|
|
357
|
+
viewportDataSource.options.followingCameraOptions.zoomUpdatesAllowed = true
|
|
358
|
+
viewportDataSource.options.followingCameraOptions.bearingUpdatesAllowed = true
|
|
359
|
+
|
|
360
|
+
if let pitch = pitch {
|
|
361
|
+
viewportDataSource.followingMobileCamera.pitch = CGFloat(max(0, min(pitch, 85)))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if let zoom = zoom {
|
|
365
|
+
viewportDataSource.followingMobileCamera.zoom = CGFloat(max(1, min(zoom, 22)))
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
navigationMapView.navigationCamera.follow()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private func buildNavigationOptions(
|
|
373
|
+
navigationService: NavigationService,
|
|
374
|
+
mapStyleUri: String?
|
|
375
|
+
) -> MapboxNavigation.NavigationOptions {
|
|
376
|
+
guard
|
|
377
|
+
let styleUri = mapStyleUri?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
378
|
+
!styleUri.isEmpty,
|
|
379
|
+
let styleURL = URL(string: styleUri)
|
|
380
|
+
else {
|
|
381
|
+
return MapboxNavigation.NavigationOptions(navigationService: navigationService)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let dayStyle = DayStyle()
|
|
385
|
+
dayStyle.mapStyleURL = styleURL
|
|
386
|
+
|
|
387
|
+
let nightStyle = NightStyle()
|
|
388
|
+
nightStyle.mapStyleURL = styleURL
|
|
389
|
+
|
|
390
|
+
return MapboxNavigation.NavigationOptions(
|
|
391
|
+
styles: [dayStyle, nightStyle],
|
|
392
|
+
navigationService: navigationService
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private static func currentTopViewController() -> UIViewController? {
|
|
397
|
+
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
398
|
+
let keyWindow = scenes
|
|
399
|
+
.flatMap { $0.windows }
|
|
400
|
+
.first(where: { $0.isKeyWindow })
|
|
401
|
+
|
|
402
|
+
var top = keyWindow?.rootViewController
|
|
403
|
+
while let presented = top?.presentedViewController {
|
|
404
|
+
top = presented
|
|
405
|
+
}
|
|
406
|
+
return top
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// MARK: - NavigationViewControllerDelegate
|
|
411
|
+
extension MapboxNavigationModule: NavigationViewControllerDelegate {
|
|
412
|
+
public func navigationViewController(
|
|
413
|
+
_ navigationViewController: NavigationViewController,
|
|
414
|
+
didUpdate progress: RouteProgress,
|
|
415
|
+
with location: CLLocation,
|
|
416
|
+
rawLocation: CLLocation
|
|
417
|
+
) {
|
|
418
|
+
if currentCameraMode.lowercased() == "following" {
|
|
419
|
+
navigationViewController.navigationMapView?.navigationCamera.follow()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
sendEvent("onLocationChange", [
|
|
423
|
+
"latitude": location.coordinate.latitude,
|
|
424
|
+
"longitude": location.coordinate.longitude,
|
|
425
|
+
"bearing": location.course,
|
|
426
|
+
"speed": location.speed,
|
|
427
|
+
"altitude": location.altitude,
|
|
428
|
+
"accuracy": location.horizontalAccuracy
|
|
429
|
+
])
|
|
430
|
+
|
|
431
|
+
sendEvent("onRouteProgressChange", [
|
|
432
|
+
"distanceTraveled": progress.distanceTraveled,
|
|
433
|
+
"distanceRemaining": progress.distanceRemaining,
|
|
434
|
+
"durationRemaining": progress.durationRemaining,
|
|
435
|
+
"fractionTraveled": progress.fractionTraveled
|
|
436
|
+
])
|
|
437
|
+
|
|
438
|
+
sendEvent("onBannerInstruction", [
|
|
439
|
+
"primaryText": progress.currentLegProgress.currentStep.instructions,
|
|
440
|
+
"stepDistanceRemaining": progress.currentLegProgress.currentStepProgress.distanceRemaining
|
|
441
|
+
])
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
public func navigationViewController(
|
|
445
|
+
_ navigationViewController: NavigationViewController,
|
|
446
|
+
didArriveAt waypoint: Waypoint
|
|
447
|
+
) -> Bool {
|
|
448
|
+
sendEvent("onArrive", [
|
|
449
|
+
"name": waypoint.name ?? ""
|
|
450
|
+
])
|
|
451
|
+
return true
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
public func navigationViewControllerDidDismiss(
|
|
455
|
+
_ navigationViewController: NavigationViewController,
|
|
456
|
+
byCanceling canceled: Bool
|
|
457
|
+
) {
|
|
458
|
+
if canceled {
|
|
459
|
+
sendEvent("onCancelNavigation", [:])
|
|
460
|
+
}
|
|
461
|
+
self.navigationViewController = nil
|
|
462
|
+
self.isCurrentlyNavigating = false
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// MARK: - Helper structs
|
|
467
|
+
struct NavigationStartOptions: Record {
|
|
468
|
+
@Field var startOrigin: Coordinate?
|
|
469
|
+
@Field var destination: DestinationWaypoint
|
|
470
|
+
@Field var waypoints: [DestinationWaypoint]?
|
|
471
|
+
@Field var shouldSimulateRoute: Bool?
|
|
472
|
+
@Field var distanceUnit: String?
|
|
473
|
+
@Field var language: String?
|
|
474
|
+
@Field var mute: Bool?
|
|
475
|
+
@Field var voiceVolume: Double?
|
|
476
|
+
@Field var cameraPitch: Double?
|
|
477
|
+
@Field var cameraZoom: Double?
|
|
478
|
+
@Field var cameraMode: String?
|
|
479
|
+
@Field var mapStyleUri: String?
|
|
480
|
+
@Field var routeAlternatives: Bool?
|
|
481
|
+
@Field var showsSpeedLimits: Bool?
|
|
482
|
+
@Field var showsWayNameLabel: Bool?
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
struct Coordinate: Record {
|
|
486
|
+
@Field var latitude: Double
|
|
487
|
+
@Field var longitude: Double
|
|
488
|
+
|
|
489
|
+
func toCLLocationCoordinate2D() -> CLLocationCoordinate2D? {
|
|
490
|
+
guard latitude >= -90 && latitude <= 90,
|
|
491
|
+
longitude >= -180 && longitude <= 180 else {
|
|
492
|
+
return nil
|
|
493
|
+
}
|
|
494
|
+
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
struct DestinationWaypoint: Record {
|
|
499
|
+
@Field var latitude: Double
|
|
500
|
+
@Field var longitude: Double
|
|
501
|
+
@Field var name: String?
|
|
502
|
+
|
|
503
|
+
func toCLLocationCoordinate2D() -> CLLocationCoordinate2D? {
|
|
504
|
+
guard latitude >= -90 && latitude <= 90,
|
|
505
|
+
longitude >= -180 && longitude <= 180 else {
|
|
506
|
+
return nil
|
|
507
|
+
}
|
|
508
|
+
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private final class CurrentLocationResolver: NSObject, CLLocationManagerDelegate {
|
|
513
|
+
private let locationManager = CLLocationManager()
|
|
514
|
+
private var completion: ((Result<CLLocationCoordinate2D, Error>) -> Void)?
|
|
515
|
+
private var timeoutWorkItem: DispatchWorkItem?
|
|
516
|
+
|
|
517
|
+
enum ResolverError: LocalizedError {
|
|
518
|
+
case permissionDenied
|
|
519
|
+
case unavailable
|
|
520
|
+
case timeout
|
|
521
|
+
|
|
522
|
+
var errorDescription: String? {
|
|
523
|
+
switch self {
|
|
524
|
+
case .permissionDenied:
|
|
525
|
+
return "Location permission denied."
|
|
526
|
+
case .unavailable:
|
|
527
|
+
return "Unable to resolve current location."
|
|
528
|
+
case .timeout:
|
|
529
|
+
return "Timed out while resolving current location."
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
func resolve(
|
|
535
|
+
timeout: TimeInterval = 8.0,
|
|
536
|
+
completion: @escaping (Result<CLLocationCoordinate2D, Error>) -> Void
|
|
537
|
+
) {
|
|
538
|
+
self.completion = completion
|
|
539
|
+
locationManager.delegate = self
|
|
540
|
+
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
|
541
|
+
|
|
542
|
+
let status = locationAuthorizationStatus()
|
|
543
|
+
switch status {
|
|
544
|
+
case .notDetermined:
|
|
545
|
+
locationManager.requestWhenInUseAuthorization()
|
|
546
|
+
case .restricted, .denied:
|
|
547
|
+
finish(.failure(ResolverError.permissionDenied))
|
|
548
|
+
return
|
|
549
|
+
default:
|
|
550
|
+
requestLocation()
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
let timeoutTask = DispatchWorkItem { [weak self] in
|
|
554
|
+
self?.finish(.failure(ResolverError.timeout))
|
|
555
|
+
}
|
|
556
|
+
timeoutWorkItem = timeoutTask
|
|
557
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: timeoutTask)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private func requestLocation() {
|
|
561
|
+
if let coordinate = locationManager.location?.coordinate {
|
|
562
|
+
finish(.success(coordinate))
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
locationManager.requestLocation()
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private func locationAuthorizationStatus() -> CLAuthorizationStatus {
|
|
569
|
+
if #available(iOS 14.0, *) {
|
|
570
|
+
return locationManager.authorizationStatus
|
|
571
|
+
}
|
|
572
|
+
return CLLocationManager.authorizationStatus()
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
576
|
+
let status = locationAuthorizationStatus()
|
|
577
|
+
switch status {
|
|
578
|
+
case .authorizedAlways, .authorizedWhenInUse:
|
|
579
|
+
requestLocation()
|
|
580
|
+
case .denied, .restricted:
|
|
581
|
+
finish(.failure(ResolverError.permissionDenied))
|
|
582
|
+
default:
|
|
583
|
+
break
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
588
|
+
guard let coordinate = locations.last?.coordinate else {
|
|
589
|
+
finish(.failure(ResolverError.unavailable))
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
finish(.success(coordinate))
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
596
|
+
if let coordinate = manager.location?.coordinate {
|
|
597
|
+
finish(.success(coordinate))
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
finish(.failure(error))
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private func finish(_ result: Result<CLLocationCoordinate2D, Error>) {
|
|
604
|
+
guard let completion = completion else {
|
|
605
|
+
return
|
|
606
|
+
}
|
|
607
|
+
timeoutWorkItem?.cancel()
|
|
608
|
+
timeoutWorkItem = nil
|
|
609
|
+
self.completion = nil
|
|
610
|
+
locationManager.delegate = nil
|
|
611
|
+
completion(result)
|
|
612
|
+
}
|
|
613
|
+
}
|