-
Notifications
You must be signed in to change notification settings - Fork 318
/
Copy pathRouter.swift
378 lines (310 loc) · 16.7 KB
/
Router.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
import Foundation
import CoreLocation
import MapboxDirections
/**
A router data source, also known as a location manager, supplies location data to a `Router` instance. For example, a `MapboxNavigationService` supplies location data to a `RouteController` or `LegacyRouteController`.
*/
public protocol RouterDataSource: AnyObject {
/**
The location provider for the `Router.` This class is designated as the object that will provide location updates when requested.
*/
var locationManagerType: NavigationLocationManager.Type { get }
}
/**
A `RouteResponse` object that sorts routes from most optimal to least optimal and selected route index in it.
*/
public struct IndexedRouteResponse {
/**
`RouteResponse` object, containing selection of routes to follow.
*/
public let routeResponse: RouteResponse
/**
The index of the selected route within the `routeResponse`.
*/
public let routeIndex: Int
/**
Returns a route from the `routeResponse` under given `routeIndex` if possible.
*/
public var currentRoute: Route? {
guard let routes = routeResponse.routes,
routes.count > routeIndex else {
return nil
}
return routeResponse.routes?[routeIndex]
}
/**
Initializes a new `IndexedRouteResponse` object.
- parameter routeResponse: `RouteResponse` object, containing routes and other related info.
- parameter routeIndex: Selected route index in an array.
*/
public init(routeResponse: RouteResponse, routeIndex: Int) {
self.routeResponse = routeResponse
self.routeIndex = routeIndex
}
}
/**
A closure to be called when `RouteLeg` was changed.
- parameter result: Result, which in case of successfully changed leg contains the most recent
`RouteProgress` and error, in case of failure.
*/
public typealias AdvanceLegCompletionHandler = (_ result: Result<RouteProgress, Error>) -> Void
/**
A class conforming to the `Router` protocol tracks the user’s progress as they travel along a predetermined route. It calls methods on its `delegate`, which conforms to the `RouterDelegate` protocol, whenever significant events or decision points occur along the route. Despite its name, this protocol does not define the interface of a routing engine.
There are two concrete implementations of the `Router` protocol. `RouteController`, the default implementation, is capable of client-side routing and depends on the Mapbox Navigation Native framework. `LegacyRouteController` is an alternative implementation that does not have this dependency but must be used in conjunction with the Mapbox Directions API over a network connection.
*/
public protocol Router: CLLocationManagerDelegate {
/**
The route controller’s associated location manager.
*/
var dataSource: RouterDataSource { get }
/**
The route controller’s delegate.
*/
var delegate: RouterDelegate? { get set }
/**
Intializes a new `RouteController`.
- parameter routeIndex: The index of the route within the original `RouteResponse` object.
- parameter routeResponse: `RouteResponse` object, containing selection of routes to follow.
- parameter routingProvider: `RoutingProvider`, used to create route.
- parameter source: The data source for the RouteController.
*/
init(alongRouteAtIndex routeIndex: Int,
in routeResponse: RouteResponse,
options: RouteOptions,
routingProvider: RoutingProvider,
dataSource source: RouterDataSource)
/**
Details about the user’s progress along the current route, leg, and step.
*/
var routeProgress: RouteProgress { get }
/// The route along which the user is expected to travel.
///
/// You can update the route using `Router.updateRoute(with:routeOptions:completion:)`.
var route: Route { get }
/// The `RouteResponse` containing the route along which the user is expected to travel, plus its index in this `RouteResponse`, if applicable.
///
/// If you want to update the route use `Router.updateRoute(with:routeOptions:completion:)` method.
var indexedRouteResponse: IndexedRouteResponse { get }
/**
Given a users current location, returns a Boolean whether they are currently on the route.
If the user is not on the route, they should be rerouted.
*/
func userIsOnRoute(_ location: CLLocation) -> Bool
func reroute(from: CLLocation, along: RouteProgress)
/**
The idealized user location. Snapped to the route line, if applicable, otherwise raw or nil.
*/
var location: CLLocation? { get }
/**
The most recently received user location.
- note: This is a raw location received from `locationManager`. To obtain an idealized location, use the `location` property.
*/
var rawLocation: CLLocation? { get }
/**
If true, the `RouteController` attempts to calculate a more optimal route for the user on an interval defined by `RouteControllerProactiveReroutingInterval`. If `refreshesRoute` is enabled too, reroute attempt will be fired after route refreshing.
*/
var reroutesProactively: Bool { get set }
/**
If true, the `RouteController` attempts to update ETA and route congestion on an interval defined by `RouteControllerProactiveReroutingInterval`.
Refreshing will be used only if route's mode of transportation profile is set to `.automobileAvoidingTraffic`. If `reroutesProactively` is enabled too, rerouting will be checked after route is refreshed.
*/
var refreshesRoute: Bool { get set }
/**
Advances the leg index.
This is a convienence method provided to advance the leg index of any given router without having to worry about the internal data structure of the router.
*/
func advanceLegIndex(completionHandler: AdvanceLegCompletionHandler?)
/// Asynchronously replaces currently active route with the provided `IndexedRouteResponse`.
///
/// You can use this method to perform manual reroutes. `delegate` will be notified about route change via
/// `RouterDelegate.router(router:willRerouteFrom:)` and `RouterDelegate.router(_:didRerouteAlong:at:proactive:)`
/// methods.
/// - Parameters:
/// - indexedRouteResponse: A `MapboxDirections.RouteResponse` object with a new route along with its index in
/// routes array.
/// - routeOptions: Route options used to create the route. You can pass nil to reuse the `RouteOptions` from the
/// currently active route. If the new `indexedRoute` is for a different set of waypoints, `routeOptions` are
/// required.
/// - completion: A completion that will be called when when a new route is applied with a boolean indicating
/// whether the change was successful. Until completion is called `routeProgress` will represent the old route.
///
/// - Important: This method can interfere with `Route.reroute(from:along:)` method. Before updating the route
/// manually make sure that there is no reroute running by observing `RouterDelegate.router(_:willRerouteFrom:)`
/// and `router(_:didRerouteAlong:at:proactive:)` `delegate` methods.
///
/// - Important: Updating the route can have an impact on your usage costs.
/// From more info read the [Pricing Guide](https://docs.mapbox.com/ios/beta/navigation/guides/pricing/).
func updateRoute(with indexedRouteResponse: IndexedRouteResponse,
routeOptions: RouteOptions?,
completion: ((Bool) -> Void)?)
}
protocol InternalRouter: AnyObject {
var lastProactiveRerouteDate: Date? { get set }
var lastRouteRefresh: Date? { get set }
var routeTask: NavigationProviderRequest? { get set }
var lastRerouteLocation: CLLocation? { get set }
var isRerouting: Bool { get set }
var isRefreshing: Bool { get set }
var routingProvider: RoutingProvider { get }
var routeProgress: RouteProgress { get }
var indexedRouteResponse: IndexedRouteResponse { get set }
func updateRoute(with indexedRouteResponse: IndexedRouteResponse,
routeOptions: RouteOptions?,
isProactive: Bool,
completion: ((Bool) -> Void)?)
}
extension InternalRouter where Self: Router {
func refreshAndCheckForFasterRoute(from location: CLLocation, routeProgress: RouteProgress) {
if refreshesRoute {
refreshRoute(from: location, legIndex: routeProgress.legIndex) {
self.checkForFasterRoute(from: location, routeProgress: routeProgress)
}
} else {
checkForFasterRoute(from: location, routeProgress: routeProgress)
}
}
func refreshRoute(from location: CLLocation, legIndex: Int, completion: @escaping ()->()) {
guard refreshesRoute else {
completion()
return
}
guard let lastRouteRefresh = lastRouteRefresh else {
self.lastRouteRefresh = location.timestamp
completion()
return
}
guard location.timestamp.timeIntervalSince(lastRouteRefresh) >= RouteControllerProactiveReroutingInterval else {
completion()
return
}
if isRefreshing {
completion()
return
}
isRefreshing = true
routingProvider.refreshRoute(indexedRouteResponse: indexedRouteResponse,
fromLegAtIndex: UInt32(legIndex)) { [weak self] session, result in
defer {
self?.isRefreshing = false
self?.lastRouteRefresh = nil
completion()
}
guard case let .success(response) = result, let self = self else {
return
}
self.indexedRouteResponse = .init(routeResponse: response, routeIndex: self.indexedRouteResponse.routeIndex)
guard let currentRoute = self.indexedRouteResponse.currentRoute else {
assertionFailure("Refreshed `RouteResponse` did not contain required `routeIndex`!")
return
}
self.routeProgress.refreshRoute(with: currentRoute, at: location)
var userInfo = [RouteController.NotificationUserInfoKey: Any]()
userInfo[.routeProgressKey] = self.routeProgress
NotificationCenter.default.post(name: .routeControllerDidRefreshRoute, object: self, userInfo: userInfo)
self.delegate?.router(self, didRefresh: self.routeProgress)
}
}
func checkForFasterRoute(from location: CLLocation, routeProgress: RouteProgress) {
// Check for faster route given users current location
guard reroutesProactively else { return }
// Only check for faster alternatives if the user has plenty of time left on the route.
guard routeProgress.durationRemaining > RouteControllerMinimumDurationRemainingForProactiveRerouting else { return }
// If the user is approaching a maneuver, don't check for a faster alternatives
guard routeProgress.currentLegProgress.currentStepProgress.durationRemaining > RouteControllerMediumAlertInterval else { return }
guard let currentUpcomingManeuver = routeProgress.currentLegProgress.upcomingStep else {
return
}
guard let lastRouteValidationDate = lastProactiveRerouteDate else {
self.lastProactiveRerouteDate = location.timestamp
return
}
// Only check every so often for a faster route.
guard location.timestamp.timeIntervalSince(lastRouteValidationDate) >= RouteControllerProactiveReroutingInterval else {
return
}
let durationRemaining = routeProgress.durationRemaining
// Avoid interrupting an ongoing reroute
if isRerouting { return }
isRerouting = true
calculateRoutes(from: location, along: routeProgress) { [weak self] (session, result) in
guard let self = self else { return }
guard case let .success(indexedResponse) = result else {
self.isRerouting = false; return
}
let response = indexedResponse.routeResponse
guard let route = response.routes?.first else {
self.isRerouting = false; return
}
self.lastProactiveRerouteDate = nil
guard let firstLeg = route.legs.first, let firstStep = firstLeg.steps.first else {
self.isRerouting = false; return
}
let routeIsFaster = firstStep.expectedTravelTime >= RouteControllerMediumAlertInterval &&
currentUpcomingManeuver == firstLeg.steps[1] && route.expectedTravelTime <= 0.9 * durationRemaining
guard routeIsFaster else {
self.isRerouting = false; return
}
var routeOptions: RouteOptions?
if case let .route(options) = response.options {
routeOptions = options
}
// Prefer the most optimal route (the first one) over the route that matched the original choice.
let indexedRouteResponse = IndexedRouteResponse(routeResponse: response, routeIndex: 0)
self.updateRoute(with: indexedRouteResponse,
routeOptions: routeOptions ?? self.routeProgress.routeOptions,
isProactive: true) { success in
self.isRerouting = false
}
}
}
/// Like RouteCompletionHandler, but including the index of the route in the response that is most similar to the route in the route progress.
typealias IndexedRouteCompletionHandler = (_ session: Directions.Session, _ result: Result<IndexedRouteResponse, DirectionsError>) -> Void
/**
Asynchronously calculates route response from a location along an existing route tracked by the given route progress object.
- parameter origin: The origin of each resulting route.
- parameter progress: The current route progress, along which the origin is located.
- parameter completion: The closure to execute once the routes have been calculated. If successful, the result includes the index of the route that is most similar to the passed-in `RouteProgress.route`, which is not necessarily the first route. The first route is the route considered to be the most optimal, even if it differs from the original choice.
*/
func calculateRoutes(from origin: CLLocation, along progress: RouteProgress, completion: @escaping IndexedRouteCompletionHandler) {
routeTask?.cancel()
let options = progress.reroutingOptions(with: origin)
lastRerouteLocation = origin
routeTask = routingProvider.calculateRoutes(options: options) {(session, result) in
defer { self.routeTask = nil }
switch result {
case .failure(let error):
return completion(session, .failure(error))
case .success(let response):
guard let mostSimilarIndex = response.routes?.index(mostSimilarTo: progress.route) else {
return completion(session, .failure(.unableToRoute))
}
return completion(session, .success(.init(routeResponse: response, routeIndex: mostSimilarIndex)))
}
}
}
func announceImpendingReroute(at location: CLLocation) {
delegate?.router(self, willRerouteFrom: location)
NotificationCenter.default.post(name: .routeControllerWillReroute, object: self, userInfo: [
RouteController.NotificationUserInfoKey.locationKey: location,
])
}
func announce(reroute newRoute: Route, at location: CLLocation?, proactive: Bool) {
var userInfo = [RouteController.NotificationUserInfoKey: Any]()
if let location = location {
userInfo[.locationKey] = location
}
userInfo[.isProactiveKey] = proactive
NotificationCenter.default.post(name: .routeControllerDidReroute, object: self, userInfo: userInfo)
delegate?.router(self, didRerouteAlong: routeProgress.route, at: location, proactive: proactive)
}
}
extension Array where Element: MapboxDirections.Route {
func index(mostSimilarTo route: Route) -> Int? {
let target = route.description
return enumerated().min { (left, right) -> Bool in
let leftDistance = left.element.description.minimumEditDistance(to: target)
let rightDistance = right.element.description.minimumEditDistance(to: target)
return leftDistance < rightDistance
}?.offset
}
}