-
Notifications
You must be signed in to change notification settings - Fork 318
/
Copy pathRouteController.swift
425 lines (330 loc) · 16.7 KB
/
RouteController.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
import Foundation
import CoreLocation
import MapboxNavigationNative
import MapboxMobileEvents
import MapboxDirections
import Polyline
import Turf
/**
A `RouteController` tracks the user’s progress along a route, posting notifications as the user reaches significant points along the route. On every location update, the route controller evaluates the user’s location, determining whether the user remains on the route. If not, the route controller calculates a new route.
`RouteController` is responsible for the core navigation logic whereas
`NavigationViewController` is responsible for displaying a default drop-in navigation UI.
*/
@objc(MBRouteController)
open class RouteController: NSObject {
public enum DefaultBehavior {
public static let shouldRerouteFromLocation: Bool = true
public static let shouldDiscardLocation: Bool = true
public static let didArriveAtWaypoint: Bool = true
public static let shouldPreventReroutesWhenArrivingAtWaypoint: Bool = true
public static let shouldDisableBatteryMonitoring: Bool = true
}
let navigator = MBNavigator()
public var route: Route {
get {
return routeProgress.route
}
set {
routeProgress = RouteProgress(route: newValue)
updateNavigator(with: routeProgress)
}
}
private var _routeProgress: RouteProgress {
didSet {
movementsAwayFromRoute = 0
updateNavigator(with: _routeProgress)
updateObservation(for: _routeProgress)
}
}
private var progressObservation: NSKeyValueObservation?
var movementsAwayFromRoute = 0
var routeTask: URLSessionDataTask?
var lastRerouteLocation: CLLocation?
var didFindFasterRoute = false
var isRerouting = false
var userSnapToStepDistanceFromManeuver: CLLocationDistance?
var previousArrivalWaypoint: Waypoint?
var isFirstLocation: Bool = true
@objc public var config: MBNavigatorConfig? {
get {
return navigator.getConfig()
}
set {
navigator.setConfigFor(newValue)
}
}
/**
Details about the user’s progress along the current route, leg, and step.
*/
@objc public var routeProgress: RouteProgress {
get {
return _routeProgress
}
set {
if let location = self.location {
delegate?.router?(self, willRerouteFrom: location)
}
_routeProgress = newValue
announce(reroute: routeProgress.route, at: rawLocation, proactive: didFindFasterRoute)
}
}
/**
The raw location, snapped to the current route.
- important: If the rawLocation is outside of the route snapping tolerances, this value is nil.
*/
var snappedLocation: CLLocation? {
let status = navigator.getStatusForTimestamp(Date())
guard status.routeState == .tracking || status.routeState == .complete else {
return nil
}
return CLLocation(status.location)
}
var heading: CLHeading?
/**
The most recently received user location.
- note: This is a raw location received from `locationManager`. To obtain an idealized location, use the `location` property.
*/
public var rawLocation: CLLocation? {
didSet {
if isFirstLocation == true {
isFirstLocation = false
}
}
}
@objc public var reroutesProactively: Bool = true
var lastProactiveRerouteDate: Date?
/**
The route controller’s delegate.
*/
@objc public weak var delegate: RouterDelegate?
/**
The route controller’s associated location manager.
*/
@objc public unowned var dataSource: RouterDataSource
/**
The Directions object used to create the route.
*/
@objc public var directions: Directions
/**
The idealized user location. Snapped to the route line, if applicable, otherwise raw.
- seeAlso: snappedLocation, rawLocation
*/
@objc public var location: CLLocation? {
return snappedLocation ?? rawLocation
}
required public init(along route: Route, directions: Directions = Directions.shared, dataSource source: RouterDataSource) {
self.directions = directions
self._routeProgress = RouteProgress(route: route)
self.dataSource = source
UIDevice.current.isBatteryMonitoringEnabled = true
super.init()
updateNavigator(with: _routeProgress)
updateObservation(for: _routeProgress)
}
func updateObservation(for progress: RouteProgress) {
progressObservation = progress.observe(\.legIndex, options: [.old, .new]) { [weak self] (progress, change) in
guard change.newValue != change.oldValue, let legIndex = change.newValue else {
return
}
self?.updateRouteLeg(to: legIndex)
}
}
/// updateNavigator is used to pass the new progress model onto nav-native.
private func updateNavigator(with progress: RouteProgress) {
assert(route.json != nil, "route.json missing, please verify the version of MapboxDirections.swift")
let data = try! JSONSerialization.data(withJSONObject: route.json!, options: [])
let jsonString = String(data: data, encoding: .utf8)!
// TODO: Add support for alternative route
navigator.setRouteForRouteResponse(jsonString, route: 0, leg: UInt32(routeProgress.legIndex))
}
/// updateRouteLeg is used to notify nav-native of the developer changing the active route-leg.
private func updateRouteLeg(to value: Int) {
let legIndex = UInt32(value)
navigator.changeRouteLeg(forRoute: 0, leg: legIndex)
let newStatus = navigator.changeRouteLeg(forRoute: 0, leg: legIndex)
updateIndexes(status: newStatus, progress: routeProgress)
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
guard delegate?.router?(self, shouldDiscard: location) ?? DefaultBehavior.shouldDiscardLocation else {
return
}
rawLocation = locations.last
locations.forEach { navigator.updateLocation(for: MBFixLocation($0)) }
let status = navigator.getStatusForTimestamp(location.timestamp)
// Notify observers if the step’s remaining distance has changed.
update(progress: routeProgress, with: CLLocation(status.location), rawLocation: location)
let willReroute = !userIsOnRoute(location) && delegate?.router?(self, shouldRerouteFrom: location)
?? DefaultBehavior.shouldRerouteFromLocation
updateIndexes(status: status, progress: routeProgress)
updateRouteLegProgress(status: status)
updateSpokenInstructionProgress(status: status, willReRoute: willReroute)
updateVisualInstructionProgress(status: status)
if willReroute {
reroute(from: location, along: routeProgress)
}
// Check for faster route proactively (if reroutesProactively is enabled)
checkForFasterRoute(from: location, routeProgress: routeProgress)
}
func updateIndexes(status: MBNavigationStatus, progress: RouteProgress) {
let newLegIndex = Int(status.legIndex)
let newStepIndex = Int(status.stepIndex)
let newIntersectionIndex = Int(status.intersectionIndex)
if (newLegIndex != progress.legIndex) {
progress.legIndex = newLegIndex
}
if (newStepIndex != progress.currentLegProgress.stepIndex) {
progress.currentLegProgress.stepIndex = newStepIndex
}
if (newIntersectionIndex != progress.currentLegProgress.currentStepProgress.intersectionIndex) {
progress.currentLegProgress.currentStepProgress.intersectionIndex = newIntersectionIndex
}
if let spokenIndexPrimitive = status.voiceInstruction?.index, progress.currentLegProgress.currentStepProgress.spokenInstructionIndex != Int(spokenIndexPrimitive)
{
progress.currentLegProgress.currentStepProgress.spokenInstructionIndex = Int(spokenIndexPrimitive)
}
if let visualInstructionIndex = status.bannerInstruction?.index, routeProgress.currentLegProgress.currentStepProgress.visualInstructionIndex != Int(visualInstructionIndex) {
routeProgress.currentLegProgress.currentStepProgress.visualInstructionIndex = Int(visualInstructionIndex)
}
}
func updateSpokenInstructionProgress(status: MBNavigationStatus, willReRoute: Bool) {
let didUpdate = status.voiceInstruction?.index != nil
// Announce voice instruction if it was updated and we are not going to reroute
if didUpdate && !willReRoute,
let spokenInstruction = routeProgress.currentLegProgress.currentStepProgress.currentSpokenInstruction {
announcePassage(of: spokenInstruction, routeProgress: routeProgress)
}
}
func updateVisualInstructionProgress(status: MBNavigationStatus) {
let didUpdate = status.bannerInstruction != nil
// Announce visual instruction if it was updated or it is the first location being reported
if didUpdate || isFirstLocation {
if let instruction = routeProgress.currentLegProgress.currentStepProgress.currentVisualInstruction {
announcePassage(of: instruction, routeProgress: routeProgress)
}
}
}
func updateRouteLegProgress(status: MBNavigationStatus) {
let legProgress = routeProgress.currentLegProgress
let currentDestination = routeProgress.currentLeg.destination
guard let remainingVoiceInstructions = legProgress.currentStepProgress.remainingSpokenInstructions else { return }
// We are at least at the "You will arrive" instruction
if legProgress.remainingSteps.count <= 2 && remainingVoiceInstructions.count <= 2 {
let willArrive = status.routeState == .tracking
let didArrive = status.routeState == .complete && currentDestination != previousArrivalWaypoint
if willArrive {
delegate?.router?(self, willArriveAt: currentDestination, after: legProgress.durationRemaining, distance: legProgress.distanceRemaining)
} else if didArrive {
previousArrivalWaypoint = currentDestination
legProgress.userHasArrivedAtWaypoint = true
let advancesToNextLeg = delegate?.router?(self, didArriveAt: currentDestination) ?? DefaultBehavior.didArriveAtWaypoint
guard !routeProgress.isFinalLeg && advancesToNextLeg else {
return
}
let legIndex = Int(status.legIndex + 1)
updateRouteLeg(to: legIndex)
}
}
}
private func update(progress: RouteProgress, with location: CLLocation, rawLocation: CLLocation) {
let stepProgress = progress.currentLegProgress.currentStepProgress
let step = stepProgress.step
//Increment the progress model
let polyline = Polyline(step.coordinates!)
if let closestCoordinate = polyline.closestCoordinate(to: rawLocation.coordinate) {
let remainingDistance = polyline.distance(from: closestCoordinate.coordinate)
let distanceTraveled = step.distance - remainingDistance
stepProgress.distanceTraveled = distanceTraveled
//Fire the delegate method
delegate?.router?(self, didUpdate: progress, with: location, rawLocation: rawLocation)
//Fire the notification (for now)
NotificationCenter.default.post(name: .routeControllerProgressDidChange, object: self, userInfo: [
RouteControllerNotificationUserInfoKey.routeProgressKey: progress,
RouteControllerNotificationUserInfoKey.locationKey: location, //guaranteed value
RouteControllerNotificationUserInfoKey.rawLocationKey: rawLocation //raw
])
}
}
private func announcePassage(of spokenInstructionPoint: SpokenInstruction, routeProgress: RouteProgress) {
delegate?.router?(self, didPassSpokenInstructionPoint: spokenInstructionPoint, routeProgress: routeProgress)
let info: [RouteControllerNotificationUserInfoKey: Any] = [
.routeProgressKey: routeProgress,
.spokenInstructionKey: spokenInstructionPoint
]
NotificationCenter.default.post(name: .routeControllerDidPassSpokenInstructionPoint, object: self, userInfo: info)
}
private func announcePassage(of visualInstructionPoint: VisualInstructionBanner, routeProgress: RouteProgress) {
delegate?.router?(self, didPassVisualInstructionPoint: visualInstructionPoint, routeProgress: routeProgress)
let info: [RouteControllerNotificationUserInfoKey: Any] = [
.routeProgressKey: routeProgress,
.visualInstructionKey: visualInstructionPoint
]
NotificationCenter.default.post(name: .routeControllerDidPassVisualInstructionPoint, object: self, userInfo: info)
}
/**
Returns an estimated location at a given timestamp. The timestamp must be
a future timestamp compared to the last location received by the location manager.
*/
public func projectedLocation(for timestamp: Date) -> CLLocation {
return CLLocation(navigator.getStatusForTimestamp(timestamp).location)
}
public func advanceLegIndex(location: CLLocation) {
let status = navigator.getStatusForTimestamp(location.timestamp)
routeProgress.legIndex = Int(status.legIndex)
}
public func enableLocationRecording() {
navigator.toggleHistoryFor(onOff: true)
}
public func disableLocationRecording() {
navigator.toggleHistoryFor(onOff: false)
}
public func locationHistory() -> String {
return navigator.getHistory()
}
}
extension RouteController: Router {
public func userIsOnRoute(_ location: CLLocation) -> Bool {
// If the user has arrived, do not continue monitor reroutes, step progress, etc
if routeProgress.currentLegProgress.userHasArrivedAtWaypoint &&
(delegate?.router?(self, shouldPreventReroutesWhenArrivingAt: routeProgress.currentLeg.destination) ??
DefaultBehavior.shouldPreventReroutesWhenArrivingAtWaypoint) {
return true
}
let status = navigator.getStatusForTimestamp(location.timestamp)
let offRoute = status.routeState == .offRoute
return !offRoute
}
public func reroute(from location: CLLocation, along progress: RouteProgress) {
if let lastRerouteLocation = lastRerouteLocation {
guard location.distance(from: lastRerouteLocation) >= RouteControllerMaximumDistanceBeforeRecalculating else {
return
}
}
delegate?.router?(self, willRerouteFrom: location)
NotificationCenter.default.post(name: .routeControllerWillReroute, object: self, userInfo: [
RouteControllerNotificationUserInfoKey.locationKey: location
])
self.lastRerouteLocation = location
// Avoid interrupting an ongoing reroute
if isRerouting { return }
isRerouting = true
getDirections(from: location, along: progress) { [weak self] (route, error) in
self?.isRerouting = false
guard let strongSelf: RouteController = self else {
return
}
if let error = error {
strongSelf.delegate?.router?(strongSelf, didFailToRerouteWith: error)
NotificationCenter.default.post(name: .routeControllerDidFailToReroute, object: self, userInfo: [
RouteControllerNotificationUserInfoKey.routingErrorKey: error
])
return
}
guard let route = route else { return }
strongSelf._routeProgress = RouteProgress(route: route, legIndex: 0)
strongSelf._routeProgress.currentLegProgress.stepIndex = 0
strongSelf.announce(reroute: route, at: location, proactive: false)
}
}
}
extension RouteController: InternalRouter { }