From 262db1a32b4485b272f4b8160f7ec86e7a37e0a9 Mon Sep 17 00:00:00 2001 From: udumft Date: Mon, 16 Aug 2021 15:13:49 +0300 Subject: [PATCH 1/4] vk-1024-hybrid-router: added NavigationRouter to provide hybrid router functionality for requesting and refreshing routes; removed Directions extension and Navigator.routerInterface; added CacheHandlerFactory to cache latest CacheHandle created. --- Example/ViewController.swift | 2 +- MapboxNavigation.xcodeproj/project.pbxproj | 12 +- .../CacheHandlerFactory.swift | 64 ++++ .../CoreNavigationNavigator.swift | 6 - Sources/MapboxCoreNavigation/Directions.swift | 73 ---- .../LegacyRouteController.swift | 12 +- .../NativeHandlersFactory.swift | 7 +- .../NavigationRouter.swift | 312 ++++++++++++++++++ .../RouteController.swift | 13 +- .../MapboxCoreNavigation/RouteProgress.swift | 6 +- Sources/MapboxCoreNavigation/Router.swift | 54 +-- .../TilesetDescriptorFactory.swift | 11 +- Sources/MapboxNavigation/CarPlayManager.swift | 14 +- Sources/MapboxNavigation/JunctionView.swift | 2 +- .../NavigationViewController.swift | 2 +- .../MapboxCoreNavigationTests.swift | 27 +- .../NavigationServiceTests.swift | 50 ++- .../RouteControllerTests.swift | 23 +- .../TilesetDescriptorFactoryTests.swift | 6 + .../CarPlayManagerTests.swift | 49 +-- 20 files changed, 540 insertions(+), 205 deletions(-) create mode 100644 Sources/MapboxCoreNavigation/CacheHandlerFactory.swift delete mode 100644 Sources/MapboxCoreNavigation/Directions.swift create mode 100644 Sources/MapboxCoreNavigation/NavigationRouter.swift diff --git a/Example/ViewController.swift b/Example/ViewController.swift index a8e7f5da406..98ea909081b 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -513,7 +513,7 @@ class ViewController: UIViewController { } func requestRoute(with options: RouteOptions, success: @escaping RouteRequestSuccess, failure: RouteRequestFailure?) { - NavigationSettings.shared.directions.calculateWithCache(options: options) { (session, result) in + NavigationRouter().requestRoutes(options: options) { (session, result) in switch result { case let .success(response): success(response) diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index f84d18ec9ae..9224ce2b197 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -35,6 +35,8 @@ 16EF6C22211BA4B300AA580B /* CarPlayMapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16EF6C21211BA4B300AA580B /* CarPlayMapViewController.swift */; }; 1FFDFD92249C1AA80091746A /* JunctionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFDFD91249C1AA70091746A /* JunctionView.swift */; }; 2B07444124B4832400615E87 /* TokenTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B07444024B4832400615E87 /* TokenTestViewController.swift */; }; + 2B2C8DBA26E75884002CCF08 /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2C8DB926E75884002CCF08 /* NavigationRouter.swift */; }; + 2B2C8DBC26E7589F002CCF08 /* CacheHandlerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2C8DBB26E7589F002CCF08 /* CacheHandlerFactory.swift */; }; 2B3ED38C2609FA7900861A84 /* ArrivalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3ED38B2609FA7900861A84 /* ArrivalController.swift */; }; 2B3ED3962609FB2300861A84 /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3ED3952609FB2300861A84 /* CameraController.swift */; }; 2B3ED3B4260A162900861A84 /* NavigationViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3ED3B3260A162900861A84 /* NavigationViewData.swift */; }; @@ -50,7 +52,6 @@ 2B81EC28241A237E00145086 /* SpeechSynthesizersControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B81EC27241A237E00145086 /* SpeechSynthesizersControllerTests.swift */; }; 2B871272263966F0001082A9 /* TileStoreConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B8712682639631C001082A9 /* TileStoreConfiguration.swift */; }; 2B91C9B12416357700E532A5 /* MapboxSpeechSynthesizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B91C9B02416357700E532A5 /* MapboxSpeechSynthesizer.swift */; }; - 2B955B1C25EFDDCB00BBFEC6 /* Directions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B955B1B25EFDDCB00BBFEC6 /* Directions.swift */; }; 2BBED92F265E2C7D00F90032 /* NativeHandlersFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BBED92E265E2C7D00F90032 /* NativeHandlersFactory.swift */; }; 2BBED93B267A3AB900F90032 /* BillingHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BBED93A267A3AB900F90032 /* BillingHandler.swift */; }; 2BBEEDA52508DB1700C8DA4A /* RouteLegProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BBEEDA42508DB1700C8DA4A /* RouteLegProgress.swift */; }; @@ -539,6 +540,8 @@ 16EF6C21211BA4B300AA580B /* CarPlayMapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayMapViewController.swift; sourceTree = ""; }; 1FFDFD91249C1AA70091746A /* JunctionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JunctionView.swift; sourceTree = ""; }; 2B07444024B4832400615E87 /* TokenTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenTestViewController.swift; sourceTree = ""; }; + 2B2C8DB926E75884002CCF08 /* NavigationRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; + 2B2C8DBB26E7589F002CCF08 /* CacheHandlerFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheHandlerFactory.swift; sourceTree = ""; }; 2B3ED38B2609FA7900861A84 /* ArrivalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrivalController.swift; sourceTree = ""; }; 2B3ED3952609FB2300861A84 /* CameraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = ""; }; 2B3ED3B3260A162900861A84 /* NavigationViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewData.swift; sourceTree = ""; }; @@ -554,7 +557,6 @@ 2B81EC27241A237E00145086 /* SpeechSynthesizersControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechSynthesizersControllerTests.swift; sourceTree = ""; }; 2B8712682639631C001082A9 /* TileStoreConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileStoreConfiguration.swift; sourceTree = ""; }; 2B91C9B02416357700E532A5 /* MapboxSpeechSynthesizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxSpeechSynthesizer.swift; sourceTree = ""; }; - 2B955B1B25EFDDCB00BBFEC6 /* Directions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Directions.swift; sourceTree = ""; }; 2BBED92E265E2C7D00F90032 /* NativeHandlersFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeHandlersFactory.swift; sourceTree = ""; }; 2BBED93A267A3AB900F90032 /* BillingHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BillingHandler.swift; sourceTree = ""; }; 2BBEEDA42508DB1700C8DA4A /* RouteLegProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteLegProgress.swift; sourceTree = ""; }; @@ -1750,7 +1752,6 @@ C582FD5E203626E900A9086E /* CLLocationDirection.swift */, 2E82B9DB26E61F4600B7837F /* CongestionLevel.swift */, C5D9800E1EFBCDAD006DBF2E /* Date.swift */, - 2B955B1B25EFDDCB00BBFEC6 /* Directions.swift */, 3A163AE2249901D000D66A0D /* FixLocation.swift */, DAF27247264E028B00C0AC37 /* Geometry.swift */, C51DF8651F38C31C006C6A15 /* Locale.swift */, @@ -1810,6 +1811,7 @@ B4843886270F8E1600E161E6 /* SimulationType.swift */, 2BBED93A267A3AB900F90032 /* BillingHandler.swift */, C58D6BAC1DDCF2AE00387F53 /* CoreConstants.swift */, + 2B2C8DBB26E7589F002CCF08 /* CacheHandlerFactory.swift */, 351BEC0B1E5BCC72006FE110 /* DistanceFormatter.swift */, B417913A2624F9EA001E0348 /* MBXInfo.plist */, C5ADFBCD1DDCC7840011824B /* Info.plist */, @@ -1818,6 +1820,7 @@ 2BBED92E265E2C7D00F90032 /* NativeHandlersFactory.swift */, 353E68FB1EF0B7F8007B2AE5 /* NavigationLocationManager.swift */, C5E7A31B1F4F6828001CB015 /* NavigationRouteOptions.swift */, + 2B2C8DB926E75884002CCF08 /* NavigationRouter.swift */, 35375EC01F31FA86004CE727 /* NavigationSettings.swift */, 351174F31EF1C0530065E248 /* ReplayLocationManager.swift */, C5ADFBF91DDCC9580011824B /* LegacyRouteController.swift */, @@ -2672,6 +2675,7 @@ 417D127726E24D0800E0AB16 /* FreeDriveEventDetails.swift in Sources */, 353E68FC1EF0B7F8007B2AE5 /* NavigationLocationManager.swift in Sources */, 2BE7013D25359C7B00F46E4E /* RouteAlert.swift in Sources */, + 2B2C8DBC26E7589F002CCF08 /* CacheHandlerFactory.swift in Sources */, 353E68FE1EF0B985007B2AE5 /* BundleAdditions.swift in Sources */, 11D1F8A22696EBD40053A93F /* Dictionary+Equality.swift in Sources */, 2EFAE005264C1F9200B618C4 /* RoadObjectLocation.swift in Sources */, @@ -2727,7 +2731,6 @@ 353E69041EF0C4E5007B2AE5 /* SimulatedLocationManager.swift in Sources */, DA5F450025F07DE200F573EC /* ElectronicHorizonOptions.swift in Sources */, DAFA92071F01735000A7FB09 /* DistanceFormatter.swift in Sources */, - 2B955B1C25EFDDCB00BBFEC6 /* Directions.swift in Sources */, DAF27252264E02D800C0AC37 /* RoadObject.swift in Sources */, 5A39B9282498F9890026DFD1 /* PassiveLocationManager.swift in Sources */, 118D883526F8CA0700B2ED7B /* EndOfRouteFeedback.swift in Sources */, @@ -2741,6 +2744,7 @@ C578DA081EFD0FFF0052079F /* ProcessInfo.swift in Sources */, 2B42586E2657BF9100B487C3 /* TileStore.swift in Sources */, 2BE7016925371E3400F46E4E /* Incident.swift in Sources */, + 2B2C8DBA26E75884002CCF08 /* NavigationRouter.swift in Sources */, E2805A5826CB994500165DB9 /* NSLock+MapboxInternal.swift in Sources */, C58D6BAD1DDCF2AE00387F53 /* CoreConstants.swift in Sources */, 2B7ACA9B25E3F84700B0ACFD /* PredictiveCacheManager.swift in Sources */, diff --git a/Sources/MapboxCoreNavigation/CacheHandlerFactory.swift b/Sources/MapboxCoreNavigation/CacheHandlerFactory.swift new file mode 100644 index 00000000000..5ca44bcd979 --- /dev/null +++ b/Sources/MapboxCoreNavigation/CacheHandlerFactory.swift @@ -0,0 +1,64 @@ +import MapboxDirections +import MapboxNavigationNative + +protocol CacheHandlerData { + var tileStorePath: String { get } + var credentials: DirectionsCredentials { get } + var tilesVersion: String? { get } + var historyDirectoryURL: URL? { get } + var targetVersion: String? { get } + var configFactoryType: ConfigFactory.Type { get } +} + +extension NativeHandlersFactory: CacheHandlerData { } + +enum CacheHandlerFactory { + + private struct CacheKey: CacheHandlerData { + let tileStorePath: String + let credentials: DirectionsCredentials + let tilesVersion: String? + let historyDirectoryURL: URL? + let targetVersion: String? + let configFactoryType: ConfigFactory.Type + + init(data: CacheHandlerData) { + self.tileStorePath = data.tileStorePath + self.credentials = data.credentials + self.tilesVersion = data.tilesVersion + self.historyDirectoryURL = data.historyDirectoryURL + self.targetVersion = data.targetVersion + self.configFactoryType = data.configFactoryType + } + + static func != (lhs: CacheKey, rhs: CacheHandlerData) -> Bool { + return lhs.tileStorePath != rhs.tileStorePath || + lhs.credentials != rhs.credentials || + lhs.tilesVersion != rhs.tilesVersion || + lhs.historyDirectoryURL != rhs.historyDirectoryURL || + lhs.targetVersion != rhs.targetVersion || + lhs.configFactoryType != rhs.configFactoryType + } + } + + private static var key: CacheKey? = nil + private static var cachedHandle: CacheHandle! + private static let lock = NSLock() + + static func getHandler(for tilesConfig: TilesConfig, + config: ConfigHandle, + historyRecorder: HistoryRecorderHandle?, + cacheData: CacheHandlerData) -> CacheHandle { + lock.lock(); defer { + lock.unlock() + } + + if key == nil || key! != cacheData { + cachedHandle = CacheFactory.build(for: tilesConfig, + config: config, + historyRecorder: historyRecorder) + key = .init(data: cacheData) + } + return cachedHandle + } +} diff --git a/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift b/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift index fb00e6878ed..abe9dff48eb 100644 --- a/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift +++ b/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift @@ -18,12 +18,6 @@ class Navigator { private(set) var cacheHandle: CacheHandle - lazy var routerInterface: MapboxNavigationNative_Private.RouterInterface = { - return MapboxNavigationNative_Private.RouterFactory.build(for: .hybrid, - cache: cacheHandle, - historyRecorder: historyRecorder) - }() - var mostRecentNavigationStatus: NavigationStatus? = nil private(set) var tileStore: TileStore diff --git a/Sources/MapboxCoreNavigation/Directions.swift b/Sources/MapboxCoreNavigation/Directions.swift deleted file mode 100644 index 2b431fd14c6..00000000000 --- a/Sources/MapboxCoreNavigation/Directions.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import Network -import MapboxDirections -import MapboxNavigationNative - -extension Directions { - - /** - Begins asynchronously calculating routes using the given options and delivers the results to a closure. - - This method retrieves the routes asynchronously from the [Mapbox Directions API](https://www.mapbox.com/api-documentation/navigation/#directions) over a network connection. If a server error occurs, details about the error are passed into the given completion handler in lieu of the routes. If network error is encountered, onboard routing engine will attempt to provide directions using existing cached tiles. - - Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). - - - parameter options: A `RouteOptions` object specifying the requirements for the resulting routes. - - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. - - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel this task. - */ - @discardableResult open func calculateWithCache(options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask? { - return calculate(options) { (session, result) in - switch result { - case .success(_): - completionHandler(session, result) - case .failure(let error): - if case DirectionsError.network(_) = error { - // we're offline - self.calculateOffline(options: options, completionHandler: completionHandler) - } else { - completionHandler(session, result) - } - } - - } - } - - /** - Begins asynchronously calculating routes using the given options and delivers the results to a closure. - - This method retrieves the routes asynchronously from onboard routing engine using existing cached tiles. - - Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). - - - parameter options: A `RouteOptions` object specifying the requirements for the resulting routes. - - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. - */ - open func calculateOffline(options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) { - let directionsUri = url(forCalculating: options) - - Navigator.shared.routerInterface.getRouteForDirectionsUri(directionsUri.absoluteString) { (result, _) in - let json = result.value as? String - let data = json?.data(using: .utf8) - let decoder = JSONDecoder() - decoder.userInfo = [.options: options, - .credentials: self.credentials] - - let session = (options: options as DirectionsOptions, credentials: self.credentials) - - if let jsonData = data, - let response = try? decoder.decode(RouteResponse.self, from: jsonData) { - DispatchQueue.main.async { - completionHandler(session, .success(response)) - } - } else { - DispatchQueue.main.async { - completionHandler(session, .failure(.unknown(response: nil, - underlying: result.error as? Error, - code: nil, - message: nil))) - } - } - } - } -} diff --git a/Sources/MapboxCoreNavigation/LegacyRouteController.swift b/Sources/MapboxCoreNavigation/LegacyRouteController.swift index 0799f5c0e9d..a83519cd13c 100644 --- a/Sources/MapboxCoreNavigation/LegacyRouteController.swift +++ b/Sources/MapboxCoreNavigation/LegacyRouteController.swift @@ -18,9 +18,9 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa public unowned var dataSource: RouterDataSource /** - The Directions object used to create the route. + Routing source type used to create the route. */ - public var directions: Directions + public var routingSource: NavigationRouter.RouterSource public var route: Route { routeProgress.route @@ -102,7 +102,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa var didFindFasterRoute = false - var routeTask: URLSessionDataTask? + var routeTask: NavigationRouter.RoutingRequest? // MARK: Navigating @@ -135,7 +135,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa preconditionFailure("`indexedRouteResponse` does not contain route for index `\(indexedRouteResponse.routeIndex)` when updating route.") } let routeOptions = routeOptions ?? routeProgress.routeOptions - routeProgress = RouteProgress(route: routes[indexedRouteResponse.routeIndex], options: routeOptions) + routeProgress = RouteProgress(route: route, options: routeOptions) self.indexedRouteResponse = indexedRouteResponse announce(reroute: route, at: location, proactive: isProactive) completion?(true) @@ -195,8 +195,8 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa return false } - required public init(alongRouteAtIndex routeIndex: Int, in routeResponse: RouteResponse, options: RouteOptions, directions: Directions = NavigationSettings.shared.directions, dataSource source: RouterDataSource) { - self.directions = directions + required public init(alongRouteAtIndex routeIndex: Int, in routeResponse: RouteResponse, options: RouteOptions, routingSource: NavigationRouter.RouterSource = .hybrid, dataSource source: RouterDataSource) { + self.routingSource = routingSource self.indexedRouteResponse = .init(routeResponse: routeResponse, routeIndex: routeIndex) self.routeProgress = RouteProgress(route: routeResponse.routes![routeIndex], options: options) self.dataSource = source diff --git a/Sources/MapboxCoreNavigation/NativeHandlersFactory.swift b/Sources/MapboxCoreNavigation/NativeHandlersFactory.swift index d946954b139..926f506e49a 100644 --- a/Sources/MapboxCoreNavigation/NativeHandlersFactory.swift +++ b/Sources/MapboxCoreNavigation/NativeHandlersFactory.swift @@ -51,9 +51,10 @@ class NativeHandlersFactory { }() lazy var cacheHandle: CacheHandle = { - CacheFactory.build(for: tilesConfig, - config: configHandle, - historyRecorder: historyRecorder) + CacheHandlerFactory.getHandler(for: tilesConfig, + config: configHandle, + historyRecorder: historyRecorder, + cacheData: self) }() lazy var roadGraph: RoadGraph = { diff --git a/Sources/MapboxCoreNavigation/NavigationRouter.swift b/Sources/MapboxCoreNavigation/NavigationRouter.swift new file mode 100644 index 00000000000..0ab19f48a08 --- /dev/null +++ b/Sources/MapboxCoreNavigation/NavigationRouter.swift @@ -0,0 +1,312 @@ +@_implementationOnly import MapboxCommon_Private +import MapboxDirections +import MapboxNavigationNative + +/** + Provides alternative access to routing API. + + Use this class instead `Directions` requests wrapper to request new routes or refresh an existing one. Depending on `RouterSource`, `NavigationRouter` will use online and/or onboard routing engines. This may be used when designing purely online or offline apps, or when you need to provide best possible service regardless of internet collection. + */ +public class NavigationRouter { + /** + Unique identifier for a giver request. + + Valid only for the same instance of `NavigationRouter` that issued it. + */ + public typealias RequestId = UInt64 + + /** + A request handler for the ongoing router action. + + You can use this instance to cancel ongoing task if needed. Retaining this handler will keep related `NavigationRouter` from deallocating. + */ + public struct RoutingRequest { + /** + Related request identifier. + */ + public let id: RequestId + + // Intended retain cycle to prevent deallocating. `RoutingRequest` will be deleted once request completes. + let router: NavigationRouter + + /** + Cancels the request if it is still active. + */ + public func cancel() { + router.finish(request: id) + } + } + + /** + Defines source of routing engine to be used for requests. + */ + public enum RouterSource { + /** + Fetch data online only + + Such `NavigationRouter` is equivalent of using bare `Directions` wrapper. + */ + case online + /** + Use online data only + + In order for such `NavigationRouter` to function properly, proper navigation data should be available onboard. `.offline` router will not be able to refresh routes. + */ + case offline + /** + Attempts to use `online` with fallback to `offline`. + + `.hybrid` router will be able to refresh routes only using internet connection. + */ + case hybrid + + var nativeSource: RouterType { + switch self { + case .online: + return .online + case .offline: + return .onboard + case .hybrid: + return .hybrid + } + } + } + + static var __testRoutesStub: ((_: RouteOptions, _: @escaping Directions.RouteCompletionHandler) -> RequestId)? = nil + + // MARK: - Properties + /** + List of ongoing tasks for the router. + + You can see if router is busy with somethin, or used related `RoutingRequest.cancel()` to cancel requests as needed. + */ + public private(set) var activeRequests: [RequestId : RoutingRequest] = [:] + /** + Configured routing engine source. + */ + public let source: RouterSource + + private let requestsLock = NSLock() + private let router: RouterInterface + private let settings: NavigationSettings + + //MARK: - Initialization + + /** + Initializes new `NavigationRouter`. + + - parameter source: routing engine source to use. + - parameter settings: settings object, used to get credentials and cache configuration. + */ + public init(_ source: RouterSource = .hybrid, settings: NavigationSettings = .shared) { + self.source = source + self.settings = settings + + let factory = NativeHandlersFactory(tileStorePath: settings.tileStoreConfiguration.navigatorLocation.tileStoreURL?.path ?? "", + credentials: settings.directions.credentials) + self.router = MapboxNavigationNative.RouterFactory.build(for: source.nativeSource, + cache: factory.cacheHandle, + historyRecorder: factory.historyRecorder) + } + + // MARK: - Public methods + + /** + Begins asynchronously calculating routes using the given options and delivers the results to a closure. + + Depending on configured `RouterSource`, this method may retrieve the routes asynchronously from the [Mapbox Directions API](https://www.mapbox.com/api-documentation/navigation/#directions) over a network connection or use onboard routing engine with available offline data. + + Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). + + - parameter options: A `RouteOptions` object specifying the requirements for the resulting routes. + - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. + - returns: Related request identifier. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using `activeRequests`. + */ + @discardableResult public func requestRoutes(options: RouteOptions, + completionHandler: @escaping Directions.RouteCompletionHandler) -> RequestId { + return Self.__testRoutesStub?(options, completionHandler) ?? + doRequest(options: options) { [weak self] (result: Result) in + guard let self = self else { return } + let session = (options: options as DirectionsOptions, + credentials: self.settings.directions.credentials) + completionHandler(session, result) + } + } + + /** + Begins asynchronously calculating matches using the given options and delivers the results to a closure. + + Depending on configured `RouterSource`, this method may retrieve the matches asynchronously from the [Mapbox Map Matching API](https://docs.mapbox.com/api/navigation/#map-matching) over a network connection or use onboard routing engine with available offline data. + + - parameter options: A `MatchOptions` object specifying the requirements for the resulting matches. + - parameter completionHandler: The closure (block) to call with the resulting matches. This closure is executed on the application’s main thread. + - returns: Related request identifier. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using `activeRequests`. + */ + @discardableResult public func requestRoutes(options: MatchOptions, + completionHandler: @escaping Directions.MatchCompletionHandler) -> RequestId { + return doRequest(options: options) { (result: Result) in + let session = (options: options as DirectionsOptions, + credentials: self.settings.directions.credentials) + completionHandler(session, result) + } + } + + /** + Begins asynchronously refreshing the selected route, optionally starting from an arbitrary leg. + + This method retrieves skeleton route data asynchronously from the Mapbox Directions Refresh API over a network connection. If a connection error or server error occurs, details about the error are passed into the given completion handler in lieu of the routes. + + - precondition: Set `RouteOptions.refreshingEnabled` to `true` when calculating the original route. + + - parameter indexedRouteResponse: The `RouteResponse` and selected `routeIndex` in it to be refreshed. + - parameter fromLegAtIndex: The index of the leg in the route at which to begin refreshing. The response will omit any leg before this index and refresh any leg from this index to the end of the route. If this argument is omitted, the entire route is refreshed. + - parameter completionHandler: The closure (block) to call with updated `RouteResponse` data. Order of `routes` remain unchanged comparing to original `indexedRouteResponse`. This closure is executed on the application’s main thread. + - returns: Related request identifier. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using `activeRequests`. + */ + @discardableResult public func refreshRoute(indexedRouteResponse: IndexedRouteResponse, + fromLegAtIndex startLegIndex: UInt32 = 0, + completionHandler: @escaping Directions.RouteCompletionHandler) -> RequestId { + guard case let .route(routeOptions) = indexedRouteResponse.routeResponse.options, + let responseIdentifier = indexedRouteResponse.routeResponse.identifier else { + preconditionFailure("Invalid route data passed for refreshing.") + } + + let encoder = JSONEncoder() + encoder.userInfo[.options] = routeOptions + + let routeIndex = UInt32(indexedRouteResponse.routeIndex) + + guard let routeData = try? encoder.encode(indexedRouteResponse.routeResponse), + let routeJSONString = String(data: routeData, encoding: .utf8) else { + preconditionFailure("Could not serialize route data for refreshing.") + } + + var requestId: RequestId! + let refreshOptions = RouteRefreshOptions(requestId: responseIdentifier, + routeIndex: routeIndex, + legIndex: startLegIndex, + routingProfile: routeOptions.profileIdentifier.nativeProfile) + requestsLock.lock() + requestId = router.getRouteRefresh(for: refreshOptions, + route: routeJSONString) { [weak self] result, _ in + guard let self = self else { return } + + self.parseResponse(requestId: requestId, + userInfo: [.options: routeOptions, + .credentials: self.settings.directions.credentials], + result: result) { (response: Result) in + let session = (options: routeOptions as DirectionsOptions, + credentials: self.settings.directions.credentials) + completionHandler(session, response) + } + } + activeRequests[requestId] = .init(id: requestId, + router: self) + requestsLock.unlock() + return requestId + } + + // MARK: - Private methods + + fileprivate func finish(request id: RequestId) { + requestsLock.lock(); defer { + requestsLock.unlock() + } + + router.cancelRequest(forToken: id) + activeRequests[id] = nil + } + + fileprivate func complete(requestId: RequestId, with result: @escaping () -> Void) { + DispatchQueue.main.async { + result() + self.finish(request: requestId) + } + } + + struct ResponseDisposition: Decodable { + var code: String? + var message: String? + var error: String? + + private enum CodingKeys: CodingKey { + case code, message, error + } + } + + fileprivate func parseResponse(requestId: RequestId, userInfo: [CodingUserInfoKey : Any], result: Expected, completion: @escaping (Result) -> Void) { + do { + let json = result.value as? String + guard let data = json?.data(using: .utf8) else { + self.complete(requestId: requestId) { + completion(.failure(.noData)) + } + return + } + + let decoder = JSONDecoder() + decoder.userInfo = userInfo + + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = DirectionsError(code: nil, + message: nil, + response: nil, + underlyingError: result.error as? Error) + + self.complete(requestId: requestId) { + completion(.failure(apiError)) + } + return + } + + guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { + let apiError = DirectionsError(code: disposition.code, + message: disposition.message, + response: nil, + underlyingError: result.error as? Error) + + self.complete(requestId: requestId) { + completion(.failure(apiError)) + } + return + } + + let result = try decoder.decode(ResponseType.self, from: data) + + self.complete(requestId: requestId) { + completion(.success(result)) + } + } catch { + self.complete(requestId: requestId) { + let bailError = DirectionsError(code: nil, message: nil, response: nil, underlyingError: error) + completion(.failure(bailError)) + } + } + } + + fileprivate func doRequest(options: DirectionsOptions, + completion: @escaping (Result) -> Void) -> RequestId { + let directionsUri = settings.directions.url(forCalculating: options).absoluteString + var requestId: RequestId! + requestsLock.lock() + requestId = router.getRouteForDirectionsUri(directionsUri) { [weak self] (result, _) in + guard let self = self else { return } + + self.parseResponse(requestId: requestId, + userInfo: [.options: options, + .credentials: self.settings.directions.credentials], + result: result, + completion: completion) + } + activeRequests[requestId] = .init(id: requestId, + router: self) + requestsLock.unlock() + return requestId + } +} + +extension DirectionsProfileIdentifier { + var nativeProfile: RoutingProfile { + return RoutingProfile(profile: rawValue) + } +} diff --git a/Sources/MapboxCoreNavigation/RouteController.swift b/Sources/MapboxCoreNavigation/RouteController.swift index b05a383f2ff..f60e005db8b 100644 --- a/Sources/MapboxCoreNavigation/RouteController.swift +++ b/Sources/MapboxCoreNavigation/RouteController.swift @@ -48,9 +48,9 @@ open class RouteController: NSObject { public unowned var dataSource: RouterDataSource /** - The Directions object used to create the route. + Routing source type used to create the route. */ - public var directions: Directions + public var routingSource: NavigationRouter.RouterSource public var route: Route { return routeProgress.route @@ -186,8 +186,7 @@ open class RouteController: NSObject { var didFindFasterRoute = false - - var routeTask: URLSessionDataTask? + var routeTask: NavigationRouter.RoutingRequest? // MARK: Navigating @@ -521,8 +520,8 @@ open class RouteController: NSObject { // MARK: Handling Lifecycle - required public init(alongRouteAtIndex routeIndex: Int, in routeResponse: RouteResponse, options: RouteOptions, directions: Directions = NavigationSettings.shared.directions, dataSource source: RouterDataSource) { - self.directions = directions + required public init(alongRouteAtIndex routeIndex: Int, in routeResponse: RouteResponse, options: RouteOptions, routingSource: NavigationRouter.RouterSource = .hybrid, dataSource source: RouterDataSource) { + self.routingSource = routingSource self.indexedRouteResponse = .init(routeResponse: routeResponse, routeIndex: routeIndex) self.routeProgress = RouteProgress(route: routeResponse.routes![routeIndex], options: options) self.dataSource = source @@ -539,6 +538,7 @@ open class RouteController: NSObject { deinit { BillingHandler.shared.stopBillingSession(with: sessionUUID) unsubscribeNotifications() + routeTask?.cancel() } private func subscribeNotifications() { @@ -683,7 +683,6 @@ extension RouteController: Router { routes.count > indexedRouteResponse.routeIndex else { preconditionFailure("`indexedRouteResponse` does not contain route for index `\(indexedRouteResponse.routeIndex)` when updating route.") } - let route = routes[indexedRouteResponse.routeIndex] if shouldStartNewBillingSession(for: route, routeOptions: routeOptions) { BillingHandler.shared.stopBillingSession(with: sessionUUID) BillingHandler.shared.beginBillingSession(for: .activeGuidance, uuid: sessionUUID) diff --git a/Sources/MapboxCoreNavigation/RouteProgress.swift b/Sources/MapboxCoreNavigation/RouteProgress.swift index ea1e7ef45ba..9ee6bd0be27 100644 --- a/Sources/MapboxCoreNavigation/RouteProgress.swift +++ b/Sources/MapboxCoreNavigation/RouteProgress.swift @@ -129,10 +129,10 @@ open class RouteProgress: Codable { public var route: Route /** - Updates the current route with attributes from the given skeletal route. + Updates the current route with a refreshed one. */ - public func refreshRoute(with refreshedRoute: RefreshedRoute, at location: CLLocation) { - route.refreshLegAttributes(from: refreshedRoute) + func refreshRoute(with refreshedRoute: Route, at location: CLLocation) { + route = refreshedRoute currentLegProgress = RouteLegProgress(leg: route.legs[legIndex], stepIndex: currentLegProgress.stepIndex, spokenInstructionIndex: currentLegProgress.currentStepProgress.spokenInstructionIndex) diff --git a/Sources/MapboxCoreNavigation/Router.swift b/Sources/MapboxCoreNavigation/Router.swift index 92d4c7622c3..eccfe974697 100644 --- a/Sources/MapboxCoreNavigation/Router.swift +++ b/Sources/MapboxCoreNavigation/Router.swift @@ -25,6 +25,16 @@ public struct IndexedRouteResponse { */ public let routeIndex: Int + /** + Returns a route from the `routeResponse` under given `routeIndex` if possible. + */ + public var selectedRoute: Route? { + guard routeResponse.routes?.count ?? 0 > routeIndex else { + return nil + } + return routeResponse.routes?[routeIndex] + } + /** Initializes a new `IndexedRouteResponse` object. @@ -66,10 +76,10 @@ public protocol Router: CLLocationManagerDelegate { - parameter routeIndex: The index of the route within the original `RouteResponse` object. - parameter routeResponse: `RouteResponse` object, containing selection of routes to follow. - - parameter directions: The Directions object that created `route`. + - parameter routingSource: `NavigationRouter` source type, used to create route. - parameter source: The data source for the RouteController. */ - init(alongRouteAtIndex routeIndex: Int, in routeResponse: RouteResponse, options: RouteOptions, directions: Directions, dataSource source: RouterDataSource) + init(alongRouteAtIndex routeIndex: Int, in routeResponse: RouteResponse, options: RouteOptions, routingSource: NavigationRouter.RouterSource, dataSource source: RouterDataSource) /** Details about the user’s progress along the current route, leg, and step. @@ -154,7 +164,7 @@ protocol InternalRouter: AnyObject { var lastRouteRefresh: Date? { get set } - var routeTask: URLSessionDataTask? { get set } + var routeTask: NavigationRouter.RoutingRequest? { get set } var lastRerouteLocation: CLLocation? { get set } @@ -162,7 +172,7 @@ protocol InternalRouter: AnyObject { var isRefreshing: Bool { get set } - var directions: Directions { get } + var routingSource: NavigationRouter.RouterSource { get } var routeProgress: RouteProgress { get } @@ -179,48 +189,49 @@ 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) + self.checkForFasterRoute(from: location, routeProgress: routeProgress, router: $0) } } else { checkForFasterRoute(from: location, routeProgress: routeProgress) } } - func refreshRoute(from location: CLLocation, legIndex: Int, completion: @escaping ()->()) { - guard refreshesRoute, let routeIdentifier = indexedRouteResponse.routeResponse.identifier else { - completion() + func refreshRoute(from location: CLLocation, legIndex: Int, completion: @escaping (NavigationRouter?)->()) { + guard refreshesRoute else { + completion(nil) return } guard let lastRouteRefresh = lastRouteRefresh else { self.lastRouteRefresh = location.timestamp - completion() + completion(nil) return } guard location.timestamp.timeIntervalSince(lastRouteRefresh) >= RouteControllerProactiveReroutingInterval else { - completion() + completion(nil) return } if isRefreshing { - completion() + completion(nil) return } isRefreshing = true - - directions.refreshRoute(responseIdentifier: routeIdentifier, routeIndex: indexedRouteResponse.routeIndex, fromLegAtIndex: legIndex) { [weak self] (session, result) in + let router = NavigationRouter(self.routingSource) + router.refreshRoute(indexedRouteResponse: indexedRouteResponse, + fromLegAtIndex: UInt32(legIndex)) { [weak self, weak router] session, result in defer { self?.isRefreshing = false self?.lastRouteRefresh = nil - completion() + completion(router) } guard case let .success(response) = result, let self = self else { return } - - self.routeProgress.refreshRoute(with: response.route, at: location) + self.indexedRouteResponse = .init(routeResponse: response, routeIndex: self.indexedRouteResponse.routeIndex) + self.routeProgress.refreshRoute(with: self.indexedRouteResponse.selectedRoute!, at: location) var userInfo = [RouteController.NotificationUserInfoKey: Any]() userInfo[.routeProgressKey] = self.routeProgress @@ -229,7 +240,7 @@ extension InternalRouter where Self: Router { } } - func checkForFasterRoute(from location: CLLocation, routeProgress: RouteProgress) { + func checkForFasterRoute(from location: CLLocation, routeProgress: RouteProgress, router: NavigationRouter? = nil) { // Check for faster route given users current location guard reroutesProactively else { return } @@ -258,7 +269,7 @@ extension InternalRouter where Self: Router { if isRerouting { return } isRerouting = true - calculateRoutes(from: location, along: routeProgress) { [weak self] (session, result) in + calculateRoutes(from: location, along: routeProgress, router: router) { [weak self] (session, result) in guard let self = self else { return } guard case let .success(indexedResponse) = result else { @@ -306,13 +317,15 @@ extension InternalRouter where Self: Router { - 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) { + func calculateRoutes(from origin: CLLocation, along progress: RouteProgress, router: NavigationRouter? = nil, completion: @escaping IndexedRouteCompletionHandler) { routeTask?.cancel() let options = progress.reroutingOptions(with: origin) lastRerouteLocation = origin - routeTask = directions.calculateWithCache(options: options) {(session, result) in + let router = router ?? NavigationRouter(self.routingSource) + let taskId = router.requestRoutes(options: options) {(session, result) in + defer { self.routeTask = nil } switch result { case .failure(let error): return completion(session, .failure(error)) @@ -324,6 +337,7 @@ extension InternalRouter where Self: Router { return completion(session, .success(.init(routeResponse: response, routeIndex: mostSimilarIndex))) } } + routeTask = router.activeRequests[taskId] } func announceImpendingReroute(at location: CLLocation) { diff --git a/Sources/MapboxCoreNavigation/TilesetDescriptorFactory.swift b/Sources/MapboxCoreNavigation/TilesetDescriptorFactory.swift index 25b5fcc98db..bd0f55a06ea 100644 --- a/Sources/MapboxCoreNavigation/TilesetDescriptorFactory.swift +++ b/Sources/MapboxCoreNavigation/TilesetDescriptorFactory.swift @@ -14,9 +14,10 @@ extension TilesetDescriptorFactory { public class func getSpecificVersion(version: String, completionQueue: DispatchQueue = .main, completion: @escaping (TilesetDescriptor) -> Void) { - let cacheHandle = Navigator.shared.cacheHandle + let factory = NativeHandlersFactory(tileStorePath: NavigationSettings.shared.tileStoreConfiguration.navigatorLocation.tileStoreURL?.path ?? "", + credentials: NavigationSettings.shared.directions.credentials) completionQueue.async { - completion(getSpecificVersion(forCache: cacheHandle, version: version)) + completion(getSpecificVersion(forCache: factory.cacheHandle, version: version)) } } @@ -31,10 +32,10 @@ extension TilesetDescriptorFactory { */ public class func getLatest(completionQueue: DispatchQueue = .main, completion: @escaping (_ latestTilesetDescriptor: TilesetDescriptor) -> Void) { - let cacheHandle = Navigator.shared.cacheHandle - + let factory = NativeHandlersFactory(tileStorePath: NavigationSettings.shared.tileStoreConfiguration.navigatorLocation.tileStoreURL?.path ?? "", + credentials: NavigationSettings.shared.directions.credentials) completionQueue.async { - completion(getLatestForCache(cacheHandle)) + completion(getLatestForCache(factory.cacheHandle)) } } } diff --git a/Sources/MapboxNavigation/CarPlayManager.swift b/Sources/MapboxNavigation/CarPlayManager.swift index 6a45be57d95..d66cbd846b3 100644 --- a/Sources/MapboxNavigation/CarPlayManager.swift +++ b/Sources/MapboxNavigation/CarPlayManager.swift @@ -58,12 +58,6 @@ public class CarPlayManager: NSObject { */ public let eventsManager: NavigationEventsManager - /** - The object that calculates routes when the user interacts with the CarPlay - interface. - */ - public let directions: Directions - /** Returns current `CarPlayActivity`, which is based on currently present `CPTemplate`. In case if `CPTemplate` was not created by `CarPlayManager` `currentActivity` will be assigned to `nil`. @@ -105,25 +99,19 @@ public class CarPlayManager: NSObject { Initializes a new CarPlay manager that manages a connection to the CarPlay interface. - parameter styles: The styles to display in the CarPlay interface. If this argument is omitted, `DayStyle` and `NightStyle` are displayed by default. - - parameter directions: The object that calculates routes when the user interacts with the CarPlay interface. If this argument is `nil` or omitted, the shared `Directions` object is used by default. - parameter eventsManager: The events manager to use during turn-by-turn navigation while connected to CarPlay. If this argument is `nil` or omitted, a standard `NavigationEventsManager` object is used by default. */ public convenience init(styles: [Style]? = nil, - directions: Directions? = nil, eventsManager: NavigationEventsManager? = nil) { self.init(styles: styles, - directions: directions, eventsManager: eventsManager, carPlayNavigationViewControllerClass: nil) } init(styles: [Style]? = nil, - directions: Directions? = nil, eventsManager: NavigationEventsManager? = nil, carPlayNavigationViewControllerClass: CarPlayNavigationViewController.Type? = nil) { self.styles = styles ?? [DayStyle(), NightStyle()] - let mapboxDirections = directions ?? NavigationSettings.shared.directions - self.directions = mapboxDirections self.eventsManager = eventsManager ?? .init(activeNavigationDataSource: nil, accessToken: NavigationSettings.shared.directions.credentials.accessToken) self.mapTemplateProvider = MapTemplateProvider() @@ -527,7 +515,7 @@ extension CarPlayManager { } func calculate(_ options: RouteOptions, completionHandler: @escaping Directions.RouteCompletionHandler) { - directions.calculateWithCache(options: options, completionHandler: completionHandler) + NavigationRouter().requestRoutes(options: options, completionHandler: completionHandler) } func didCalculate(_ result: Result, diff --git a/Sources/MapboxNavigation/JunctionView.swift b/Sources/MapboxNavigation/JunctionView.swift index 8c09c1ba367..742dd2776be 100644 --- a/Sources/MapboxNavigation/JunctionView.swift +++ b/Sources/MapboxNavigation/JunctionView.swift @@ -63,7 +63,7 @@ public class JunctionView: UIImageView { } else { guard let imageURL = guidanceViewImageRepresentation.imageURL else { return } let baseURLString = imageURL.absoluteString - guard let accessToken = service.directions.credentials.accessToken else { return } + guard let accessToken = service.credentials.accessToken else { return } let stringURL = baseURLString + "&access_token=" + accessToken guard let guidanceViewImageURL = URL(string: stringURL) else { return } diff --git a/Sources/MapboxNavigation/NavigationViewController.swift b/Sources/MapboxNavigation/NavigationViewController.swift index 6fb47f326b9..20b302d47e2 100644 --- a/Sources/MapboxNavigation/NavigationViewController.swift +++ b/Sources/MapboxNavigation/NavigationViewController.swift @@ -293,7 +293,7 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter /** Initializes a `NavigationViewController` that presents the user interface for following a predefined route based on the given options. - The route may come directly from the completion handler of the [MapboxDirections](https://docs.mapbox.com/ios/api/directions/) framework’s `Directions.calculate(_:completionHandler:)` method, or it may be unarchived or created from a JSON object. + The route may come directly from the completion handler of the [MapboxDirections](https://docs.mapbox.com/ios/api/directions/) framework’s `Directions.calculate(_:completionHandler:)` method, MapboxCoreNavigation `NavigationRouter.requestRoutes(options:completionHandler:)`, or it may be unarchived or created from a JSON object. - parameter routeResponse: `RouteResponse` object, containing selection of routes to follow. - parameter routeIndex: The index of the route within the original `RouteResponse` object. diff --git a/Tests/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift b/Tests/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift index 4951d430fe7..ebaba06ecc8 100644 --- a/Tests/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift +++ b/Tests/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift @@ -42,7 +42,7 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - directions: directions, + routingSource: .offline, simulating: .never) let now = Date() let steps = route.legs.first!.steps @@ -86,7 +86,7 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - directions: directions, + routingSource: .offline, simulating: .never) // Coordinates from first step @@ -135,7 +135,7 @@ class MapboxCoreNavigationTests: TestCase { let navigationService = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - directions: directions, + routingSource: .offline, locationSource: locationManager, simulating: .never) @@ -191,7 +191,7 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - directions: directions, + routingSource: .offline, locationSource: locationManager, simulating: .never) @@ -242,7 +242,7 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - directions: directions, + routingSource: .offline, locationSource: locationManager, simulating: .never) expectation(forNotification: .routeControllerWillReroute, object: navigation.router) { (notification) -> Bool in @@ -285,9 +285,10 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: routeResponse, routeIndex: 0, routeOptions: navOptions, - directions: .mocked, + routingSource: .offline, locationSource: locationManager, simulating: .never) + navigation.router.refreshesRoute = false expectation(forNotification: .routeControllerProgressDidChange, object: navigation.router) { (notification) -> Bool in let routeProgress = notification.userInfo![RouteController.NotificationUserInfoKey.routeProgressKey] as? RouteProgress @@ -327,15 +328,15 @@ class MapboxCoreNavigationTests: TestCase { func testOrderOfExecution() { let trace = Fixture.generateTrace(for: route).shiftedToPresent().qualified() - let directions = DirectionsSpy() let locationManager = ReplayLocationManager(locations: trace) locationManager.speedMultiplier = 100 navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - directions: directions, + routingSource: .offline, locationSource: locationManager) + navigation.router.refreshesRoute = false struct InstructionPoint { enum InstructionType { @@ -352,6 +353,9 @@ class MapboxCoreNavigationTests: TestCase { var points = [InstructionPoint]() let spokenInstructionsExpectation = expectation(forNotification: .routeControllerDidPassSpokenInstructionPoint, object: nil) { (notification) -> Bool in + guard notification.object as? Router === self.navigation.router else { + return false + } let routeProgress = notification.userInfo![RouteController.NotificationUserInfoKey.routeProgressKey] as! RouteProgress let legIndex = routeProgress.legIndex let stepIndex = routeProgress.currentLegProgress.stepIndex @@ -369,6 +373,9 @@ class MapboxCoreNavigationTests: TestCase { } let visualInstructionsExpectation = expectation(forNotification: .routeControllerDidPassVisualInstructionPoint, object: nil) { (notification) -> Bool in + guard notification.object as? Router === self.navigation.router else { + return false + } let routeProgress = notification.userInfo![RouteController.NotificationUserInfoKey.routeProgressKey] as! RouteProgress let legIndex = routeProgress.legIndex let stepIndex = routeProgress.currentLegProgress.stepIndex @@ -445,11 +452,10 @@ class MapboxCoreNavigationTests: TestCase { } func testFailToReroute() { - let directionsClientSpy = DirectionsSpy() navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - directions: directionsClientSpy, + routingSource: .online, simulating: .never) expectation(forNotification: .routeControllerWillReroute, object: navigation.router) { (notification) -> Bool in @@ -461,7 +467,6 @@ class MapboxCoreNavigationTests: TestCase { } navigation.router.reroute(from: CLLocation(latitude: 0, longitude: 0), along: navigation.router.routeProgress) - directionsClientSpy.fireLastCalculateCompletion(with: nil, routes: nil, error: .profileNotFound) waitForExpectations(timeout: 2) { (error) in XCTAssertNil(error) diff --git a/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift b/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift index d3c5edaccd4..602ff8021bf 100644 --- a/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift +++ b/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift @@ -27,7 +27,7 @@ class NavigationServiceTests: TestCase { let navigationService = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, - directions: directionsClientSpy, + routingSource: .offline, locationSource: locationSource, eventsManagerType: NavigationEventsManagerSpy.self, simulating: .never) @@ -64,6 +64,7 @@ class NavigationServiceTests: TestCase { override func tearDown() { super.tearDown() dependencies = nil + NavigationRouter.__testRoutesStub = nil } func testDefaultUserInterfaceUsage() { @@ -471,6 +472,18 @@ class NavigationServiceTests: TestCase { return true } + + // MARK: Setupping a re-route stub + NavigationRouter.__testRoutesStub = { (options, completionHandler) in + completionHandler(Directions.Session(options, DirectionsCredentials()), + .success(RouteResponse(httpResponse: nil, + identifier: nil, + routes: [self.alternateRoute], + waypoints: nil, + options: .route(options), + credentials: Fixture.credentials))) + return 0 + } dependencies.navigationService.start() @@ -483,8 +496,7 @@ class NavigationServiceTests: TestCase { XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:willRerouteFrom:)")) wait(for: [willRerouteNotificationExpectation], timeout: 0.1) - // MARK: Upon rerouting successfully... - directionsClientSpy.fireLastCalculateCompletion(with: routeOptions.waypoints, routes: [alternateRoute], error: nil) + // MARK: Upon rerouting it tells the delegate & posts a didReroute notification // MARK: It tells the delegate & posts a didReroute notification wait(for: [didRerouteNotificationExpectation], timeout: 3) @@ -510,6 +522,7 @@ class NavigationServiceTests: TestCase { let locationManager = ReplayLocationManager(locations: trace) dependencies = createDependencies(locationSource: locationManager) let navigation = dependencies.navigationService + navigation.router.refreshesRoute = false locationManager.speedMultiplier = 50 navigation.start() @@ -542,6 +555,7 @@ class NavigationServiceTests: TestCase { dependencies = createDependencies(locationSource: locationManager) let navigation = dependencies.navigationService + navigation.router.refreshesRoute = false let replayFinished = expectation(description: "Replay finished") locationManager.replayCompletionHandler = { _ in @@ -616,8 +630,7 @@ class NavigationServiceTests: TestCase { } func testCountdownTimerDefaultAndUpdate() { - let directions = DirectionsSpy() - let subject = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, directions: directions) + let subject = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, routingSource: .offline) XCTAssert(subject.poorGPSTimer.countdownInterval == .milliseconds(2500), "Default countdown interval should be 2500 milliseconds.") @@ -642,6 +655,8 @@ class NavigationServiceTests: TestCase { let navigationService = dependencies.navigationService let routeController = navigationService.router as! RouteController + routeController.refreshesRoute = false + let routeUpdated = expectation(description: "Route Updated") routeController.updateRoute(with: .init(routeResponse: routeResponse, routeIndex: 0), routeOptions: nil) { success in @@ -669,7 +684,8 @@ class NavigationServiceTests: TestCase { func testProactiveRerouting() { typealias RouterComposition = Router & InternalRouter - + dependencies = nil + let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 38.853108, longitude: -77.043331), CLLocationCoordinate2D(latitude: 38.910736, longitude: -76.966906), @@ -687,13 +703,12 @@ class NavigationServiceTests: TestCase { XCTAssert(duration > RouteControllerProactiveReroutingInterval + RouteControllerMinimumDurationRemainingForProactiveRerouting, "Duration must greater than rerouting interval and minimum duration remaining for proactive rerouting") - let directions = DirectionsSpy() let locationManager = ReplayLocationManager(locations: trace) locationManager.speedMultiplier = 100 let service = MapboxNavigationService(routeResponse: routeResponse, routeIndex: 0, routeOptions: options, - directions: directions, + routingSource: .offline, locationSource: locationManager) service.delegate = delegate let router = service.router @@ -704,6 +719,25 @@ class NavigationServiceTests: TestCase { return isProactive == true } + let fasterRouteName = "DCA-Arboretum-dummy-faster-route" + let fasterOptions = NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 38.878206, longitude: -77.037265), + CLLocationCoordinate2D(latitude: 38.910736, longitude: -76.966906), + ]) + let fasterRoute = Fixture.route(from: fasterRouteName, options: fasterOptions) + let waypointsForFasterRoute = Fixture.waypoints(from: fasterRouteName, options: fasterOptions) + let fasterResponse = RouteResponse(httpResponse: nil, + identifier: nil, + routes: [fasterRoute], + waypoints: waypointsForFasterRoute, + options: .route(options), + credentials: Fixture.credentials) + NavigationRouter.__testRoutesStub = { (options, completionHandler) in + completionHandler(Directions.Session(options, Fixture.credentials), + .success(fasterResponse)) + return 0 + } + let rerouteTriggeredExpectation = expectation(description: "Proactive reroute triggered") locationManager.onTick = { [unowned locationManager] _, _ in if (router as! RouterComposition).lastRerouteLocation != nil { diff --git a/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift b/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift index 8981effbdd8..0741bf82f13 100644 --- a/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift +++ b/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift @@ -15,6 +15,7 @@ class RouteControllerTests: TestCase { override func tearDown() { replayManager = nil + NavigationRouter.__testRoutesStub = nil super.tearDown() } @@ -70,13 +71,11 @@ class RouteControllerTests: TestCase { CLLocation(coordinate: $0) }.shiftedToPresent() - let directions = DirectionsSpy() - let navOptions = NavigationRouteOptions(coordinates: [origin, destination]) let routeController = RouteController(alongRouteAtIndex: 0, in: routeResponse, options: navOptions, - directions: directions, + routingSource: .offline, dataSource: self) let routerDelegateSpy = RouterDelegateSpy() @@ -112,7 +111,7 @@ class RouteControllerTests: TestCase { didRerouteCalled.fulfill() } - directions.onCalculateRoute = { [unowned directions] in + NavigationRouter.__testRoutesStub = { (options, completionHandler) in calculateRouteCalled.fulfill() let currentCoordinate = locationManager.location!.coordinate @@ -124,13 +123,15 @@ class RouteControllerTests: TestCase { destinationWaypoint ] - let routes = [ - Fixture.route(between: currentCoordinate, and: destination).route - ] - - directions.fireLastCalculateCompletion(with: waypoints, - routes: routes, - error: nil) + completionHandler(Directions.Session(options, DirectionsCredentials()), + .success(RouteResponse(httpResponse: nil, + identifier: nil, + routes: [Fixture.route(between: currentCoordinate, + and: destination).route], + waypoints: waypoints, + options: .route(options), + credentials: .mocked))) + return 0 } let replayFinished = expectation(description: "Replay Finished") diff --git a/Tests/MapboxCoreNavigationTests/TilesetDescriptorFactoryTests.swift b/Tests/MapboxCoreNavigationTests/TilesetDescriptorFactoryTests.swift index af08f4bb1f2..11f22567ff1 100644 --- a/Tests/MapboxCoreNavigationTests/TilesetDescriptorFactoryTests.swift +++ b/Tests/MapboxCoreNavigationTests/TilesetDescriptorFactoryTests.swift @@ -6,6 +6,12 @@ import MapboxDirections @testable import MapboxCoreNavigation final class TilesetDescriptorFactoryTests: TestCase { + + override func tearDown() { + NavigationSettings.shared.initialize(directions: .shared, tileStoreConfiguration: .default) + super.tearDown() + } + func testLatestDescriptorsAreFromGlobalNavigatorCacheHandle() { NavigationSettings.shared.initialize(directions: .mocked, tileStoreConfiguration: .custom(FileManager.default.temporaryDirectory)) diff --git a/Tests/MapboxNavigationTests/CarPlayManagerTests.swift b/Tests/MapboxNavigationTests/CarPlayManagerTests.swift index 193d5ca5608..cb937c54bb2 100644 --- a/Tests/MapboxNavigationTests/CarPlayManagerTests.swift +++ b/Tests/MapboxNavigationTests/CarPlayManagerTests.swift @@ -201,33 +201,6 @@ class CarPlayManagerTests: TestCase { XCTAssert(spy.recievedError == testError, "Delegate should have receieved error") } - func testDirectionsOverride() { - class DirectionsInvocationSpy: Directions { - typealias VoidClosure = () -> Void - var payload: VoidClosure? - - override func calculate(_ options: RouteOptions, completionHandler: @escaping Directions.RouteCompletionHandler) -> URLSessionDataTask { - payload?() - - return URLSessionDataTask() - } - } - - let expectation = XCTestExpectation(description: "Ensuring Spy is called") - let spy = DirectionsInvocationSpy(credentials: .mocked) - spy.payload = expectation.fulfill - - let subject = CarPlayManager(directions: spy) - - let waypoint1 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.795042, longitude: -122.413165)) - let waypoint2 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.7727, longitude: -122.433378)) - let options = RouteOptions(waypoints: [waypoint1, waypoint2]) - subject.calculate(options, completionHandler: { _, _ in }) - wait(for: [expectation], timeout: 1.0) - - XCTAssert(subject.directions == spy, "Directions client is not overridden properly.") - } - func testCustomStyles() { class CustomStyle: DayStyle {} @@ -260,8 +233,7 @@ class CarPlayManagerSpec: QuickSpec { Navigator._recreateNavigator() CarPlayMapViewController.swizzleMethods() - let directionsSpy = DirectionsSpy() - manager = CarPlayManager(styles: nil, directions: directionsSpy, eventsManager: nil) + manager = CarPlayManager(styles: nil, eventsManager: nil) delegate = TestCarPlayManagerDelegate() manager!.delegate = delegate @@ -279,6 +251,10 @@ class CarPlayManagerSpec: QuickSpec { beforeEach { manager!.mapTemplateProvider = MapTemplateSpyProvider() } + + afterEach { + NavigationRouter.__testRoutesStub = nil + } let previewRoutesAction = { let options = NavigationRouteOptions(coordinates: [ @@ -288,10 +264,19 @@ class CarPlayManagerSpec: QuickSpec { let route = Fixture.route(from: "route-with-banner-instructions", options: options) let waypoints = options.waypoints - let directionsSpy = manager!.directions as! DirectionsSpy - + let fasterResponse = RouteResponse(httpResponse: nil, + identifier: nil, + routes: [route], + waypoints: waypoints, + options: .route(options), + credentials: Fixture.credentials) + NavigationRouter.__testRoutesStub = { (options, completionHandler) in + completionHandler(Directions.Session(options, Fixture.credentials), + .success(fasterResponse)) + return 0 + } + manager!.previewRoutes(for: options, completionHandler: {}) - directionsSpy.fireLastCalculateCompletion(with: waypoints, routes: [route], error: nil) } context("when the trip is not customized by the developer") { From 9ccb046df768c50aedd216b17e6a3317a375cbb5 Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 14 Sep 2021 14:42:43 +0300 Subject: [PATCH 2/4] vk-1024-hybrid-router: added Navigator parameters to ensure MapboxRoutingProvider uses the same cache and history recorder handles; refactored handlers caching class to be more generic (+9 squashed commits) Squashed commits: [99e73e771] vk-1024-hybrid-router: improved code comments; added sanity check for route refreshing; re-organized MapboxRoutingProvider.swift [cd1a117d8] vk-1024-hybrid-router: CHANGELOG updated. [26467b8f0] vk-1024-hybrid-router: reverted breaking changes; update code to be backwards compatible [6a7833e42] vk-1024-hybrid-router: improved code readability; fixed Directions RoutingProvider callbacks calling thread [683b7cd72] vk-1024-hybrid-router: restored file references [a77db21a8] vk-1024-hybrid-router: fixed unit tests; updated deprecated directions symbols usage; NN imports updated [f3dd48151] vk-1024-hybrid-router: renamed new entities and it's members; added assertation messages, added code doc for CacheHandlerFactory. [838cd749f] vk-1024-hybrid-router: file references added. [2e1ff0397] vk-1024-hybrid-router: unit tests updated. CHANGELOG updated. (+5 squashed commits) Squashed commits: [1b2285acd] vk-1024-hybrid-router: introduced NavigationProvider protocol to allow usage Directions or NavigationRouter for routing tasks. Corresponding conformances added. [30c02f5b3] vk-1024-hybrid-router: eased conditions for reporting an error on offline route refreshing attempt [54e287df6] vk-1024-hybrid-router: RouteController update route fixed [8e3ef17f7] vk-1024-hybrid-router: restored NavigationService changes. Tests fixed. [c8937ff9f] vk-1024-hybrid-router: CHANGELOG updated. --- CHANGELOG.md | 4 + Example/AppDelegate.swift | 2 +- Example/CustomViewController.swift | 2 + Example/ViewController.swift | 9 +- MapboxNavigation.xcodeproj/project.pbxproj | 28 +- .../CacheHandlerFactory.swift | 64 --- .../CoreNavigationNavigator.swift | 1 - .../Directions+RoutingProvider.swift | 81 ++++ Sources/MapboxCoreNavigation/Directions.swift | 50 +++ .../MapboxCoreNavigation/HandlerFactory.swift | 87 ++++ .../LegacyRouteController.swift | 35 +- ...uter.swift => MapboxRoutingProvider.swift} | 372 ++++++++++-------- .../NativeHandlersFactory.swift | 16 +- .../NavigationService.swift | 85 +++- .../RouteController.swift | 32 +- .../MapboxCoreNavigation/RouteProgress.swift | 13 + Sources/MapboxCoreNavigation/Router.swift | 54 +-- .../RoutingProvider.swift | 59 +++ Sources/MapboxNavigation/CarPlayManager.swift | 37 +- .../NavigationViewController.swift | 11 +- .../BillingHandlerTests.swift | 5 +- .../MapboxCoreNavigationTests.swift | 24 +- .../NavigationEventsManagerTests.swift | 9 +- .../NavigationServiceTests.swift | 43 +- .../RouteControllerTests.swift | 33 +- .../CarPlayManagerTests.swift | 23 +- ...CarPlayNavigationViewControllerTests.swift | 2 +- .../MapboxNavigationTests/CarPlayUtils.swift | 9 +- .../EndOfRouteFeedbackTests.swift | 3 +- .../InstructionsCardCollectionTests.swift | 7 +- Tests/MapboxNavigationTests/LeaksSpec.swift | 7 +- .../NavigationViewControllerTests.swift | 22 +- .../SpeechSynthesizersControllerTests.swift | 14 +- .../StepsViewControllerTests.swift | 2 +- 34 files changed, 865 insertions(+), 380 deletions(-) delete mode 100644 Sources/MapboxCoreNavigation/CacheHandlerFactory.swift create mode 100644 Sources/MapboxCoreNavigation/Directions+RoutingProvider.swift create mode 100644 Sources/MapboxCoreNavigation/Directions.swift create mode 100644 Sources/MapboxCoreNavigation/HandlerFactory.swift rename Sources/MapboxCoreNavigation/{NavigationRouter.swift => MapboxRoutingProvider.swift} (63%) create mode 100644 Sources/MapboxCoreNavigation/RoutingProvider.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index bb18e584884..b728c019b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,10 @@ * Renamed the `Locale.usesMetric` property to `Locale.measuresDistancesInMetricUnits`. `Locale.usesMetric` is still available but deprecated. ([#3547](https://github.com/mapbox/mapbox-navigation-ios/pull/3547)) * Fixed an issue where the user interface did not necessarily display distances in the same units as the route by default. `NavigationRouteOptions` and `NavigationMatchOptions` now set `DirectionsOptions.distanceMeasurementSystem` to a default value matching the `NavigationSettings.distanceUnit` property. ([#3541](https://github.com/mapbox/mapbox-navigation-ios/pull/3541)) +### Other changes + +* Introduced `RoutingProvider` to parameterize routing fetching and refreshing during active guidance sessions. `Directions.calculateWithCache(options:completionHandler:)` and `Directions.calculateOffline(options:completionHandler)` functionality is deprecated by `MapboxRoutingProvider`. It is now recommended to use `MapboxRoutingProvider` to request or refresh routes instead of `Directions` object but you may also provide your own `RoutingProvider` implementation to `NavigationService`, `RouteController` or `LegacyRouteController`. Using `directions` property of listed above entities is discouraged, you should use corresponding `routingProvider` instead, albeit `Directions` also implements the protocol. ([#3261](https://github.com/mapbox/mapbox-navigation-ios/pull/3261)) + ## v2.0.1 * Added the `Notification.Name.didArriveAtWaypoint` constant for notifications posted when the user arrives at a waypoint. ([#3514](https://github.com/mapbox/mapbox-navigation-ios/pull/3514)) diff --git a/Example/AppDelegate.swift b/Example/AppDelegate.swift index e89026324f3..782fd939346 100644 --- a/Example/AppDelegate.swift +++ b/Example/AppDelegate.swift @@ -11,7 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? @available(iOS 12.0, *) - lazy var carPlayManager: CarPlayManager = CarPlayManager() + lazy var carPlayManager: CarPlayManager = CarPlayManager(routingProvider: MapboxRoutingProvider(.hybrid)) @available(iOS 12.0, *) lazy var carPlaySearchController: CarPlaySearchController = CarPlaySearchController() diff --git a/Example/CustomViewController.swift b/Example/CustomViewController.swift index 6bab20e853e..36cf18dea62 100644 --- a/Example/CustomViewController.swift +++ b/Example/CustomViewController.swift @@ -48,6 +48,8 @@ class CustomViewController: UIViewController { navigationService = MapboxNavigationService(routeResponse: indexedUserRouteResponse!.routeResponse, routeIndex: indexedUserRouteResponse!.routeIndex, routeOptions: userRouteOptions!, + routingProvider: MapboxRoutingProvider(.hybrid), + credentials: NavigationSettings.shared.directions.credentials, locationSource: locationManager, simulating: simulateLocation ? .always : .inTunnels) navigationService.delegate = self diff --git a/Example/ViewController.swift b/Example/ViewController.swift index 98ea909081b..446f5e7b290 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -513,7 +513,7 @@ class ViewController: UIViewController { } func requestRoute(with options: RouteOptions, success: @escaping RouteRequestSuccess, failure: RouteRequestFailure?) { - NavigationRouter().requestRoutes(options: options) { (session, result) in + MapboxRoutingProvider().calculateRoutes(options: options) { (session, result) in switch result { case let .success(response): success(response) @@ -561,7 +561,12 @@ class ViewController: UIViewController { func navigationService(response: RouteResponse, routeIndex: Int, options: RouteOptions) -> NavigationService { let mode: SimulationMode = simulationButton.isSelected ? .always : .inTunnels - return MapboxNavigationService(routeResponse: response, routeIndex: routeIndex, routeOptions: options, simulating: mode) + return MapboxNavigationService(routeResponse: response, + routeIndex: routeIndex, + routeOptions: options, + routingProvider: MapboxRoutingProvider(.hybrid), + credentials: NavigationSettings.shared.directions.credentials, + simulating: mode) } // MARK: - Utility methods diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index 9224ce2b197..418a434420a 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -34,9 +34,10 @@ 16EF6C1E21193A9600AA580B /* CarPlayManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16EF6C1D21193A9600AA580B /* CarPlayManagerTests.swift */; }; 16EF6C22211BA4B300AA580B /* CarPlayMapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16EF6C21211BA4B300AA580B /* CarPlayMapViewController.swift */; }; 1FFDFD92249C1AA80091746A /* JunctionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFDFD91249C1AA70091746A /* JunctionView.swift */; }; + 2B01E4B6274671550002A5F7 /* MapboxRoutingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B01E4B3274671540002A5F7 /* MapboxRoutingProvider.swift */; }; + 2B01E4B7274671550002A5F7 /* RoutingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B01E4B4274671540002A5F7 /* RoutingProvider.swift */; }; + 2B01E4B8274671550002A5F7 /* Directions+RoutingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B01E4B5274671550002A5F7 /* Directions+RoutingProvider.swift */; }; 2B07444124B4832400615E87 /* TokenTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B07444024B4832400615E87 /* TokenTestViewController.swift */; }; - 2B2C8DBA26E75884002CCF08 /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2C8DB926E75884002CCF08 /* NavigationRouter.swift */; }; - 2B2C8DBC26E7589F002CCF08 /* CacheHandlerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2C8DBB26E7589F002CCF08 /* CacheHandlerFactory.swift */; }; 2B3ED38C2609FA7900861A84 /* ArrivalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3ED38B2609FA7900861A84 /* ArrivalController.swift */; }; 2B3ED3962609FB2300861A84 /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3ED3952609FB2300861A84 /* CameraController.swift */; }; 2B3ED3B4260A162900861A84 /* NavigationViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3ED3B3260A162900861A84 /* NavigationViewData.swift */; }; @@ -65,6 +66,8 @@ 2BE7013D25359C7B00F46E4E /* RouteAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE7013C25359C7B00F46E4E /* RouteAlert.swift */; }; 2BE7016925371E3400F46E4E /* Incident.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE7016825371E3400F46E4E /* Incident.swift */; }; 2BE70189253734A000F46E4E /* TollCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE7012C2535946300F46E4E /* TollCollection.swift */; }; + 2BF398C1274BDEA8000C9A72 /* Directions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF398C0274BDEA8000C9A72 /* Directions.swift */; }; + 2BF398C3274FE99A000C9A72 /* HandlerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF398C2274FE99A000C9A72 /* HandlerFactory.swift */; }; 2E50E0C0264E35CA009D3848 /* RoadObjectMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E50E0BF264E35CA009D3848 /* RoadObjectMatcher.swift */; }; 2E50E0D2264E468B009D3848 /* RoadObjectMatcherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E50E0D1264E468B009D3848 /* RoadObjectMatcherError.swift */; }; 2E50E0DC264E49C8009D3848 /* RoadObjectMatcherDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E50E0DB264E49C8009D3848 /* RoadObjectMatcherDelegate.swift */; }; @@ -539,9 +542,10 @@ 16EF6C1D21193A9600AA580B /* CarPlayManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayManagerTests.swift; sourceTree = ""; }; 16EF6C21211BA4B300AA580B /* CarPlayMapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayMapViewController.swift; sourceTree = ""; }; 1FFDFD91249C1AA70091746A /* JunctionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JunctionView.swift; sourceTree = ""; }; + 2B01E4B3274671540002A5F7 /* MapboxRoutingProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapboxRoutingProvider.swift; sourceTree = ""; }; + 2B01E4B4274671540002A5F7 /* RoutingProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoutingProvider.swift; sourceTree = ""; }; + 2B01E4B5274671550002A5F7 /* Directions+RoutingProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Directions+RoutingProvider.swift"; sourceTree = ""; }; 2B07444024B4832400615E87 /* TokenTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenTestViewController.swift; sourceTree = ""; }; - 2B2C8DB926E75884002CCF08 /* NavigationRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; - 2B2C8DBB26E7589F002CCF08 /* CacheHandlerFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheHandlerFactory.swift; sourceTree = ""; }; 2B3ED38B2609FA7900861A84 /* ArrivalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrivalController.swift; sourceTree = ""; }; 2B3ED3952609FB2300861A84 /* CameraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = ""; }; 2B3ED3B3260A162900861A84 /* NavigationViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewData.swift; sourceTree = ""; }; @@ -570,6 +574,8 @@ 2BE701342535948100F46E4E /* RestStop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestStop.swift; sourceTree = ""; }; 2BE7013C25359C7B00F46E4E /* RouteAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteAlert.swift; sourceTree = ""; }; 2BE7016825371E3400F46E4E /* Incident.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Incident.swift; sourceTree = ""; }; + 2BF398C0274BDEA8000C9A72 /* Directions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Directions.swift; sourceTree = ""; }; + 2BF398C2274FE99A000C9A72 /* HandlerFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HandlerFactory.swift; sourceTree = ""; }; 2E50E0BF264E35CA009D3848 /* RoadObjectMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoadObjectMatcher.swift; sourceTree = ""; }; 2E50E0D1264E468B009D3848 /* RoadObjectMatcherError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoadObjectMatcherError.swift; sourceTree = ""; }; 2E50E0DB264E49C8009D3848 /* RoadObjectMatcherDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoadObjectMatcherDelegate.swift; sourceTree = ""; }; @@ -1811,16 +1817,17 @@ B4843886270F8E1600E161E6 /* SimulationType.swift */, 2BBED93A267A3AB900F90032 /* BillingHandler.swift */, C58D6BAC1DDCF2AE00387F53 /* CoreConstants.swift */, - 2B2C8DBB26E7589F002CCF08 /* CacheHandlerFactory.swift */, + 2BF398C0274BDEA8000C9A72 /* Directions.swift */, + 2B01E4B5274671550002A5F7 /* Directions+RoutingProvider.swift */, 351BEC0B1E5BCC72006FE110 /* DistanceFormatter.swift */, B417913A2624F9EA001E0348 /* MBXInfo.plist */, C5ADFBCD1DDCC7840011824B /* Info.plist */, C5ADFBCC1DDCC7840011824B /* MapboxCoreNavigation.h */, 41B901EA271048BD007F9F78 /* HistoryRecording.swift */, + 2BF398C2274FE99A000C9A72 /* HandlerFactory.swift */, 2BBED92E265E2C7D00F90032 /* NativeHandlersFactory.swift */, 353E68FB1EF0B7F8007B2AE5 /* NavigationLocationManager.swift */, C5E7A31B1F4F6828001CB015 /* NavigationRouteOptions.swift */, - 2B2C8DB926E75884002CCF08 /* NavigationRouter.swift */, 35375EC01F31FA86004CE727 /* NavigationSettings.swift */, 351174F31EF1C0530065E248 /* ReplayLocationManager.swift */, C5ADFBF91DDCC9580011824B /* LegacyRouteController.swift */, @@ -1836,6 +1843,8 @@ 2B7ACA9925E3F84600B0ACFD /* PredictiveCacheManager.swift */, 3582A25120EFA9680029C5DE /* RouterDelegate.swift */, C5ADFBFB1DDCC9AD0011824B /* RouteProgress.swift */, + 2B01E4B4274671540002A5F7 /* RoutingProvider.swift */, + 2B01E4B3274671540002A5F7 /* MapboxRoutingProvider.swift */, 2BBEEDA42508DB1700C8DA4A /* RouteLegProgress.swift */, 2BBEEDA62508E1E300C8DA4A /* RouteStepProgress.swift */, 3582A24F20EEC46B0029C5DE /* Router.swift */, @@ -2675,7 +2684,6 @@ 417D127726E24D0800E0AB16 /* FreeDriveEventDetails.swift in Sources */, 353E68FC1EF0B7F8007B2AE5 /* NavigationLocationManager.swift in Sources */, 2BE7013D25359C7B00F46E4E /* RouteAlert.swift in Sources */, - 2B2C8DBC26E7589F002CCF08 /* CacheHandlerFactory.swift in Sources */, 353E68FE1EF0B985007B2AE5 /* BundleAdditions.swift in Sources */, 11D1F8A22696EBD40053A93F /* Dictionary+Equality.swift in Sources */, 2EFAE005264C1F9200B618C4 /* RoadObjectLocation.swift in Sources */, @@ -2721,6 +2729,7 @@ 3A163AE3249901D000D66A0D /* FixLocation.swift in Sources */, 2E50E0D2264E468B009D3848 /* RoadObjectMatcherError.swift in Sources */, 8D2AA745211CDD4000EB7F72 /* NavigationService.swift in Sources */, + 2B01E4B8274671550002A5F7 /* Directions+RoutingProvider.swift in Sources */, 2B7ACA9C25E3F84700B0ACFD /* PredictiveCacheOptions.swift in Sources */, 35A5413B1EFC052700E49846 /* RouteOptions.swift in Sources */, DA5F44F525F07D3B00F573EC /* RoadObjectStoreDelegate.swift in Sources */, @@ -2728,7 +2737,9 @@ DA5F44C725F07AB700F573EC /* RoadName.swift in Sources */, DA5F44AC25F07A6800F573EC /* RoadGraphPosition.swift in Sources */, 2E50E0E6264E49EF009D3848 /* OpenLRIdentifier.swift in Sources */, + 2BF398C1274BDEA8000C9A72 /* Directions.swift in Sources */, 353E69041EF0C4E5007B2AE5 /* SimulatedLocationManager.swift in Sources */, + 2B01E4B7274671550002A5F7 /* RoutingProvider.swift in Sources */, DA5F450025F07DE200F573EC /* ElectronicHorizonOptions.swift in Sources */, DAFA92071F01735000A7FB09 /* DistanceFormatter.swift in Sources */, DAF27252264E02D800C0AC37 /* RoadObject.swift in Sources */, @@ -2737,14 +2748,15 @@ C5C94C1D1DDCD2370097296A /* RouteProgress.swift in Sources */, 2BBED93B267A3AB900F90032 /* BillingHandler.swift in Sources */, 2BE701352535948100F46E4E /* RestStop.swift in Sources */, + 2B01E4B6274671550002A5F7 /* MapboxRoutingProvider.swift in Sources */, 8D4CF9C621349FFB009C3FEE /* NavigationServiceDelegate.swift in Sources */, + 2BF398C3274FE99A000C9A72 /* HandlerFactory.swift in Sources */, 118D883626F8CA0700B2ED7B /* ActiveNavigationFeedbackType.swift in Sources */, C5CFE4881EF2FD4C006F48E8 /* MMEEventsManager.swift in Sources */, 11B3D6D626A60EBD0057C6F4 /* ActiveNavigationEventDetails.swift in Sources */, C578DA081EFD0FFF0052079F /* ProcessInfo.swift in Sources */, 2B42586E2657BF9100B487C3 /* TileStore.swift in Sources */, 2BE7016925371E3400F46E4E /* Incident.swift in Sources */, - 2B2C8DBA26E75884002CCF08 /* NavigationRouter.swift in Sources */, E2805A5826CB994500165DB9 /* NSLock+MapboxInternal.swift in Sources */, C58D6BAD1DDCF2AE00387F53 /* CoreConstants.swift in Sources */, 2B7ACA9B25E3F84700B0ACFD /* PredictiveCacheManager.swift in Sources */, diff --git a/Sources/MapboxCoreNavigation/CacheHandlerFactory.swift b/Sources/MapboxCoreNavigation/CacheHandlerFactory.swift deleted file mode 100644 index 5ca44bcd979..00000000000 --- a/Sources/MapboxCoreNavigation/CacheHandlerFactory.swift +++ /dev/null @@ -1,64 +0,0 @@ -import MapboxDirections -import MapboxNavigationNative - -protocol CacheHandlerData { - var tileStorePath: String { get } - var credentials: DirectionsCredentials { get } - var tilesVersion: String? { get } - var historyDirectoryURL: URL? { get } - var targetVersion: String? { get } - var configFactoryType: ConfigFactory.Type { get } -} - -extension NativeHandlersFactory: CacheHandlerData { } - -enum CacheHandlerFactory { - - private struct CacheKey: CacheHandlerData { - let tileStorePath: String - let credentials: DirectionsCredentials - let tilesVersion: String? - let historyDirectoryURL: URL? - let targetVersion: String? - let configFactoryType: ConfigFactory.Type - - init(data: CacheHandlerData) { - self.tileStorePath = data.tileStorePath - self.credentials = data.credentials - self.tilesVersion = data.tilesVersion - self.historyDirectoryURL = data.historyDirectoryURL - self.targetVersion = data.targetVersion - self.configFactoryType = data.configFactoryType - } - - static func != (lhs: CacheKey, rhs: CacheHandlerData) -> Bool { - return lhs.tileStorePath != rhs.tileStorePath || - lhs.credentials != rhs.credentials || - lhs.tilesVersion != rhs.tilesVersion || - lhs.historyDirectoryURL != rhs.historyDirectoryURL || - lhs.targetVersion != rhs.targetVersion || - lhs.configFactoryType != rhs.configFactoryType - } - } - - private static var key: CacheKey? = nil - private static var cachedHandle: CacheHandle! - private static let lock = NSLock() - - static func getHandler(for tilesConfig: TilesConfig, - config: ConfigHandle, - historyRecorder: HistoryRecorderHandle?, - cacheData: CacheHandlerData) -> CacheHandle { - lock.lock(); defer { - lock.unlock() - } - - if key == nil || key! != cacheData { - cachedHandle = CacheFactory.build(for: tilesConfig, - config: config, - historyRecorder: historyRecorder) - key = .init(data: cacheData) - } - return cachedHandle - } -} diff --git a/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift b/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift index abe9dff48eb..7d51d3ded4f 100644 --- a/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift +++ b/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift @@ -1,7 +1,6 @@ import MapboxNavigationNative import MapboxDirections @_implementationOnly import MapboxCommon_Private -@_implementationOnly import MapboxNavigationNative_Private class Navigator { diff --git a/Sources/MapboxCoreNavigation/Directions+RoutingProvider.swift b/Sources/MapboxCoreNavigation/Directions+RoutingProvider.swift new file mode 100644 index 00000000000..41f141a6bd0 --- /dev/null +++ b/Sources/MapboxCoreNavigation/Directions+RoutingProvider.swift @@ -0,0 +1,81 @@ +import Foundation +import MapboxDirections + +extension URLSessionDataTask: NavigationProviderRequest { + public var requestIdentifier: UInt64 { + UInt64(taskIdentifier) + } +} + +extension Directions: RoutingProvider { + + @discardableResult public func calculateRoutes(options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) -> NavigationProviderRequest? { + return calculate(options, completionHandler: completionHandler) + } + + @discardableResult public func calculateRoutes(options: MatchOptions, completionHandler: @escaping MatchCompletionHandler) -> NavigationProviderRequest? { + return calculate(options, completionHandler: completionHandler) + } + + @discardableResult public func refreshRoute(indexedRouteResponse: IndexedRouteResponse, fromLegAtIndex: UInt32, completionHandler: @escaping RouteCompletionHandler) -> NavigationProviderRequest? { + guard case let .route(routeOptions) = indexedRouteResponse.routeResponse.options else { + preconditionFailure("Invalid route data passed for refreshing. Expected `RouteResponse` containing `.route` `ResponseOptions` but got `.match`.") + } + + let session = (options: routeOptions as DirectionsOptions, + credentials: self.credentials) + + guard let responseIdentifier = indexedRouteResponse.routeResponse.identifier else { + DispatchQueue.main.async { + completionHandler(session, .failure(.noData)) + } + return nil + } + + return refreshRoute(responseIdentifier: responseIdentifier, + routeIndex: indexedRouteResponse.routeIndex, + fromLegAtIndex: Int(fromLegAtIndex), + completionHandler: { credentials, result in + switch result { + case .failure(let error): + DispatchQueue.main.async { + completionHandler(session, .failure(error)) + } + case .success(let routeRefreshResponse): + DispatchQueue.global().async { + do { + let routeResponse = try indexedRouteResponse.routeResponse.copy(with: routeOptions) + routeResponse.routes?[indexedRouteResponse.routeIndex].refreshLegAttributes(from: routeRefreshResponse.route) + DispatchQueue.main.async { + completionHandler(session, .success(routeResponse)) + } + } catch { + DispatchQueue.main.async { + completionHandler(session, .failure(.unknown(response: nil, underlying: error, code: nil, message: nil))) + } + } + } + } + }) + } +} + +extension RouteResponse { + func copy(with options: DirectionsOptions) throws -> RouteResponse { + var copy = self + copy.routes = try copy.routes?.map { try $0.copy(with: options) } + return copy + } +} + +extension Route { + func copy(with options: DirectionsOptions) throws -> Route { + let encoder = JSONEncoder() + encoder.userInfo[.options] = options + let encoded = try encoder.encode(self) + + let decoder = JSONDecoder() + decoder.userInfo[.options] = options + return try decoder.decode(Self.self, from: encoded) + } +} diff --git a/Sources/MapboxCoreNavigation/Directions.swift b/Sources/MapboxCoreNavigation/Directions.swift new file mode 100644 index 00000000000..c3eff97acf7 --- /dev/null +++ b/Sources/MapboxCoreNavigation/Directions.swift @@ -0,0 +1,50 @@ +import Foundation +import Network +import MapboxDirections +import MapboxNavigationNative + +extension Directions { + + /** + Begins asynchronously calculating routes using the given options and delivers the results to a closure. + + This method retrieves the routes asynchronously from the [Mapbox Directions API](https://www.mapbox.com/api-documentation/navigation/#directions) over a network connection. If a server error occurs, details about the error are passed into the given completion handler in lieu of the routes. If network error is encountered, onboard routing engine will attempt to provide directions using existing cached tiles. + + Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). + + - parameter options: A `RouteOptions` object specifying the requirements for the resulting routes. + - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. + - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel this task. + */ + @discardableResult open func calculateWithCache(options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) -> URLSessionDataTask? { + return calculate(options) { (session, result) in + switch result { + case .success(_): + completionHandler(session, result) + case .failure(let error): + if case DirectionsError.network(_) = error { + // we're offline + self.calculateOffline(options: options, completionHandler: completionHandler) + } else { + completionHandler(session, result) + } + } + + } + } + + /** + Begins asynchronously calculating routes using the given options and delivers the results to a closure. + + This method retrieves the routes asynchronously from onboard routing engine using existing cached tiles. + + Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). + + - parameter options: A `RouteOptions` object specifying the requirements for the resulting routes. + - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. + */ + open func calculateOffline(options: RouteOptions, completionHandler: @escaping RouteCompletionHandler) { + MapboxRoutingProvider().calculateRoutes(options: options, + completionHandler: completionHandler) + } +} diff --git a/Sources/MapboxCoreNavigation/HandlerFactory.swift b/Sources/MapboxCoreNavigation/HandlerFactory.swift new file mode 100644 index 00000000000..2e02c873e48 --- /dev/null +++ b/Sources/MapboxCoreNavigation/HandlerFactory.swift @@ -0,0 +1,87 @@ +import MapboxDirections +import MapboxNavigationNative + +protocol HandlerData { + var tileStorePath: String { get } + var credentials: Credentials { get } + var tilesVersion: String { get } + var historyDirectoryURL: URL? { get } + var targetVersion: String? { get } + var configFactoryType: ConfigFactory.Type { get } +} + +extension NativeHandlersFactory: HandlerData { } + +/** + :nodoc: + Creates new or returns existing entity of `HandlerType` constructed with `Arguments`. + + This factory is required since some of NavNative's handlers are used by multiple unrelated entities and is quite expensive to allocate. Since bindgen-generated `*Factory` classes are not an actual factory but just a wrapper around general init, `HandlerFactory` introduces basic caching of the latest allocated entity. In most of the cases there should never be multiple handlers with different attributes, so such solution is adequate at the moment. + */ +class HandlerFactory { + + private struct CacheKey: HandlerData { + let tileStorePath: String + let credentials: Credentials + let tilesVersion: String + let historyDirectoryURL: URL? + let targetVersion: String? + let configFactoryType: ConfigFactory.Type + + init(data: HandlerData) { + self.tileStorePath = data.tileStorePath + self.credentials = data.credentials + self.tilesVersion = data.tilesVersion + self.historyDirectoryURL = data.historyDirectoryURL + self.targetVersion = data.targetVersion + self.configFactoryType = data.configFactoryType + } + + static func != (lhs: CacheKey, rhs: HandlerData) -> Bool { + return lhs.tileStorePath != rhs.tileStorePath || + lhs.credentials != rhs.credentials || + lhs.tilesVersion != rhs.tilesVersion || + lhs.historyDirectoryURL?.absoluteString != rhs.historyDirectoryURL?.absoluteString || + lhs.targetVersion != rhs.targetVersion || + lhs.configFactoryType != rhs.configFactoryType + } + } + + typealias BuildHandler = (Arguments) -> HandlerType + let buildHandler: BuildHandler + + private var key: CacheKey? = nil + private var cachedHandle: HandlerType! + private let lock = NSLock() + + fileprivate init(forBuilding buildHandler: @escaping BuildHandler) { + self.buildHandler = buildHandler + } + + func getHandler(with arguments: Arguments, + cacheData: HandlerData) -> HandlerType { + lock.lock(); defer { + lock.unlock() + } + + if key == nil || key! != cacheData { + cachedHandle = buildHandler(arguments) + key = .init(data: cacheData) + } + return cachedHandle + } +} + +let historyRecorderHandlerFactory = HandlerFactory { (path: String, + configHandle: ConfigHandle) in + HistoryRecorderHandle.build(forHistoryDir: path, + config: configHandle) +} + +let cacheHandlerFactory = HandlerFactory { (tilesConfig: TilesConfig, + config: ConfigHandle, + historyRecorder: HistoryRecorderHandle?) in + CacheFactory.build(for: tilesConfig, + config: config, + historyRecorder: historyRecorder) +} diff --git a/Sources/MapboxCoreNavigation/LegacyRouteController.swift b/Sources/MapboxCoreNavigation/LegacyRouteController.swift index a83519cd13c..8304380ef4c 100644 --- a/Sources/MapboxCoreNavigation/LegacyRouteController.swift +++ b/Sources/MapboxCoreNavigation/LegacyRouteController.swift @@ -18,9 +18,15 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa public unowned var dataSource: RouterDataSource /** - Routing source type used to create the route. + A reference to a MapboxDirections service. Used for rerouting. */ - public var routingSource: NavigationRouter.RouterSource + @available(*, deprecated, message: "Use `routingProvider` instead. If route controller was not initialized using `Directions` object - this property is unused and ignored.") + public lazy var directions: Directions = routingProvider as? Directions ?? Directions.shared + + /** + Routing provider used to create the route. + */ + public var routingProvider: RoutingProvider public var route: Route { routeProgress.route @@ -102,7 +108,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa var didFindFasterRoute = false - var routeTask: NavigationRouter.RoutingRequest? + var routeTask: NavigationProviderRequest? // MARK: Navigating @@ -131,7 +137,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa routeOptions: RouteOptions?, isProactive: Bool, completion: ((Bool) -> Void)?) { - guard let routes = indexedRouteResponse.routeResponse.routes, routes.count > indexedRouteResponse.routeIndex else { + guard let route = indexedRouteResponse.currentRoute else { preconditionFailure("`indexedRouteResponse` does not contain route for index `\(indexedRouteResponse.routeIndex)` when updating route.") } let routeOptions = routeOptions ?? routeProgress.routeOptions @@ -195,8 +201,25 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa return false } - required public init(alongRouteAtIndex routeIndex: Int, in routeResponse: RouteResponse, options: RouteOptions, routingSource: NavigationRouter.RouterSource = .hybrid, dataSource source: RouterDataSource) { - self.routingSource = routingSource + @available(*, deprecated, renamed: "init(alongRouteAtIndex:routeIndex:in:options:routingProvider:dataSource:)") + public convenience init(alongRouteAtIndex routeIndex: Int, + in routeResponse: RouteResponse, + options: RouteOptions, + directions: Directions = NavigationSettings.shared.directions, + dataSource source: RouterDataSource) { + self.init(alongRouteAtIndex: routeIndex, + in: routeResponse, + options: options, + routingProvider: directions, + dataSource: source) + } + + required public init(alongRouteAtIndex routeIndex: Int, + in routeResponse: RouteResponse, + options: RouteOptions, + routingProvider: RoutingProvider = Directions.shared, + dataSource source: RouterDataSource) { + self.routingProvider = routingProvider self.indexedRouteResponse = .init(routeResponse: routeResponse, routeIndex: routeIndex) self.routeProgress = RouteProgress(route: routeResponse.routes![routeIndex], options: options) self.dataSource = source diff --git a/Sources/MapboxCoreNavigation/NavigationRouter.swift b/Sources/MapboxCoreNavigation/MapboxRoutingProvider.swift similarity index 63% rename from Sources/MapboxCoreNavigation/NavigationRouter.swift rename to Sources/MapboxCoreNavigation/MapboxRoutingProvider.swift index 0ab19f48a08..e73941ce1f1 100644 --- a/Sources/MapboxCoreNavigation/NavigationRouter.swift +++ b/Sources/MapboxCoreNavigation/MapboxRoutingProvider.swift @@ -1,62 +1,61 @@ @_implementationOnly import MapboxCommon_Private import MapboxDirections import MapboxNavigationNative +@_implementationOnly import MapboxNavigationNative_Private /** Provides alternative access to routing API. - Use this class instead `Directions` requests wrapper to request new routes or refresh an existing one. Depending on `RouterSource`, `NavigationRouter` will use online and/or onboard routing engines. This may be used when designing purely online or offline apps, or when you need to provide best possible service regardless of internet collection. + Use this class instead `Directions` requests wrapper to request new routes or refresh an existing one. Depending on `RouterSource`, `MapboxRoutingProvider` will use online and/or onboard routing engines. This may be used when designing purely online or offline apps, or when you need to provide best possible service regardless of internet collection. */ -public class NavigationRouter { - /** - Unique identifier for a giver request. - - Valid only for the same instance of `NavigationRouter` that issued it. - */ - public typealias RequestId = UInt64 +public class MapboxRoutingProvider: RoutingProvider { /** - A request handler for the ongoing router action. + Initializes new `MapboxRoutingProvider`. - You can use this instance to cancel ongoing task if needed. Retaining this handler will keep related `NavigationRouter` from deallocating. + - parameter source: routing engine source to use. + - parameter settings: settings object, used to get credentials and cache configuration. */ - public struct RoutingRequest { - /** - Related request identifier. - */ - public let id: RequestId - - // Intended retain cycle to prevent deallocating. `RoutingRequest` will be deleted once request completes. - let router: NavigationRouter + public init(_ source: Source = .hybrid, settings: NavigationSettings = .shared) { + self.source = source + self.settings = settings - /** - Cancels the request if it is still active. - */ - public func cancel() { - router.finish(request: id) - } + let factory = NativeHandlersFactory(tileStorePath: settings.tileStoreConfiguration.navigatorLocation.tileStoreURL?.path ?? "", + credentials: settings.directions.credentials, + tilesVersion: Navigator.tilesVersion, + historyDirectoryURL: Navigator.historyDirectoryURL) + self.router = RouterFactory.build(for: source.nativeSource, + cache: factory.cacheHandle, + historyRecorder: factory.historyRecorder) } + // MARK: Configuration + + /** + Configured routing engine source. + */ + public let source: Source + /** Defines source of routing engine to be used for requests. */ - public enum RouterSource { + public enum Source { /** Fetch data online only - Such `NavigationRouter` is equivalent of using bare `Directions` wrapper. + Such `MapboxRoutingProvider` is equivalent of using bare `Directions` wrapper. */ case online /** - Use online data only + Use offline data only - In order for such `NavigationRouter` to function properly, proper navigation data should be available onboard. `.offline` router will not be able to refresh routes. + In order for such `MapboxRoutingProvider` to function properly, proper navigation data should be available offline. `.offline` routing provider will not be able to refresh routes. */ case offline /** Attempts to use `online` with fallback to `offline`. - `.hybrid` router will be able to refresh routes only using internet connection. + `.hybrid` routing provider will be able to refresh routes only using internet connection. */ case hybrid @@ -72,155 +71,58 @@ public class NavigationRouter { } } - static var __testRoutesStub: ((_: RouteOptions, _: @escaping Directions.RouteCompletionHandler) -> RequestId)? = nil - - // MARK: - Properties - /** - List of ongoing tasks for the router. - - You can see if router is busy with somethin, or used related `RoutingRequest.cancel()` to cancel requests as needed. - */ - public private(set) var activeRequests: [RequestId : RoutingRequest] = [:] - /** - Configured routing engine source. - */ - public let source: RouterSource - - private let requestsLock = NSLock() - private let router: RouterInterface private let settings: NavigationSettings - //MARK: - Initialization + static var __testRoutesStub: ((_: RouteOptions, _: @escaping Directions.RouteCompletionHandler) -> Request?)? = nil - /** - Initializes new `NavigationRouter`. - - - parameter source: routing engine source to use. - - parameter settings: settings object, used to get credentials and cache configuration. - */ - public init(_ source: RouterSource = .hybrid, settings: NavigationSettings = .shared) { - self.source = source - self.settings = settings - - let factory = NativeHandlersFactory(tileStorePath: settings.tileStoreConfiguration.navigatorLocation.tileStoreURL?.path ?? "", - credentials: settings.directions.credentials) - self.router = MapboxNavigationNative.RouterFactory.build(for: source.nativeSource, - cache: factory.cacheHandle, - historyRecorder: factory.historyRecorder) - } - - // MARK: - Public methods + // MARK: Performing and Parsing Requests /** - Begins asynchronously calculating routes using the given options and delivers the results to a closure. - - Depending on configured `RouterSource`, this method may retrieve the routes asynchronously from the [Mapbox Directions API](https://www.mapbox.com/api-documentation/navigation/#directions) over a network connection or use onboard routing engine with available offline data. - - Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). + Unique identifier for a giver request. - - parameter options: A `RouteOptions` object specifying the requirements for the resulting routes. - - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. - - returns: Related request identifier. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using `activeRequests`. + Valid only for the same instance of `MapboxRoutingProvider` that issued it. */ - @discardableResult public func requestRoutes(options: RouteOptions, - completionHandler: @escaping Directions.RouteCompletionHandler) -> RequestId { - return Self.__testRoutesStub?(options, completionHandler) ?? - doRequest(options: options) { [weak self] (result: Result) in - guard let self = self else { return } - let session = (options: options as DirectionsOptions, - credentials: self.settings.directions.credentials) - completionHandler(session, result) - } - } + public typealias RequestId = UInt64 /** - Begins asynchronously calculating matches using the given options and delivers the results to a closure. + A request handler for the ongoing routing action. - Depending on configured `RouterSource`, this method may retrieve the matches asynchronously from the [Mapbox Map Matching API](https://docs.mapbox.com/api/navigation/#map-matching) over a network connection or use onboard routing engine with available offline data. - - - parameter options: A `MatchOptions` object specifying the requirements for the resulting matches. - - parameter completionHandler: The closure (block) to call with the resulting matches. This closure is executed on the application’s main thread. - - returns: Related request identifier. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using `activeRequests`. + You can use this instance to cancel ongoing task if needed. Retaining this handler will keep related `MapboxRoutingProvider` from deallocating. */ - @discardableResult public func requestRoutes(options: MatchOptions, - completionHandler: @escaping Directions.MatchCompletionHandler) -> RequestId { - return doRequest(options: options) { (result: Result) in - let session = (options: options as DirectionsOptions, - credentials: self.settings.directions.credentials) - completionHandler(session, result) + public struct Request: NavigationProviderRequest { + /** + Related request identifier. + */ + public let requestIdentifier: RequestId + + // Intended retain cycle to prevent deallocating. `Request` will be deleted once request completes. + let routingProvider: MapboxRoutingProvider + + /** + Cancels the request if it is still active. + */ + public func cancel() { + routingProvider.router.cancelRequest(forToken: requestIdentifier) } } /** - Begins asynchronously refreshing the selected route, optionally starting from an arbitrary leg. - - This method retrieves skeleton route data asynchronously from the Mapbox Directions Refresh API over a network connection. If a connection error or server error occurs, details about the error are passed into the given completion handler in lieu of the routes. + List of ongoing tasks for the routing provider. - - precondition: Set `RouteOptions.refreshingEnabled` to `true` when calculating the original route. - - - parameter indexedRouteResponse: The `RouteResponse` and selected `routeIndex` in it to be refreshed. - - parameter fromLegAtIndex: The index of the leg in the route at which to begin refreshing. The response will omit any leg before this index and refresh any leg from this index to the end of the route. If this argument is omitted, the entire route is refreshed. - - parameter completionHandler: The closure (block) to call with updated `RouteResponse` data. Order of `routes` remain unchanged comparing to original `indexedRouteResponse`. This closure is executed on the application’s main thread. - - returns: Related request identifier. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using `activeRequests`. + You can see if provider is busy with something, or use related `Request.cancel()` to cancel requests as needed. */ - @discardableResult public func refreshRoute(indexedRouteResponse: IndexedRouteResponse, - fromLegAtIndex startLegIndex: UInt32 = 0, - completionHandler: @escaping Directions.RouteCompletionHandler) -> RequestId { - guard case let .route(routeOptions) = indexedRouteResponse.routeResponse.options, - let responseIdentifier = indexedRouteResponse.routeResponse.identifier else { - preconditionFailure("Invalid route data passed for refreshing.") - } - - let encoder = JSONEncoder() - encoder.userInfo[.options] = routeOptions - - let routeIndex = UInt32(indexedRouteResponse.routeIndex) - - guard let routeData = try? encoder.encode(indexedRouteResponse.routeResponse), - let routeJSONString = String(data: routeData, encoding: .utf8) else { - preconditionFailure("Could not serialize route data for refreshing.") - } - - var requestId: RequestId! - let refreshOptions = RouteRefreshOptions(requestId: responseIdentifier, - routeIndex: routeIndex, - legIndex: startLegIndex, - routingProfile: routeOptions.profileIdentifier.nativeProfile) - requestsLock.lock() - requestId = router.getRouteRefresh(for: refreshOptions, - route: routeJSONString) { [weak self] result, _ in - guard let self = self else { return } - - self.parseResponse(requestId: requestId, - userInfo: [.options: routeOptions, - .credentials: self.settings.directions.credentials], - result: result) { (response: Result) in - let session = (options: routeOptions as DirectionsOptions, - credentials: self.settings.directions.credentials) - completionHandler(session, response) - } - } - activeRequests[requestId] = .init(id: requestId, - router: self) - requestsLock.unlock() - return requestId - } - - // MARK: - Private methods + public private(set) var activeRequests: [RequestId : Request] = [:] - fileprivate func finish(request id: RequestId) { - requestsLock.lock(); defer { - requestsLock.unlock() - } - - router.cancelRequest(forToken: id) - activeRequests[id] = nil - } + private let requestsLock = NSLock() + private let router: MapboxNavigationNative_Private.RouterInterface - fileprivate func complete(requestId: RequestId, with result: @escaping () -> Void) { - DispatchQueue.main.async { + private func complete(requestId: RequestId, with result: @escaping () -> Void) { + DispatchQueue.main.async { [self] in result() - self.finish(request: requestId) + + requestsLock { + activeRequests[requestId] = nil + } } } @@ -234,7 +136,7 @@ public class NavigationRouter { } } - fileprivate func parseResponse(requestId: RequestId, userInfo: [CodingUserInfoKey : Any], result: Expected, completion: @escaping (Result) -> Void) { + private func parseResponse(requestId: RequestId, userInfo: [CodingUserInfoKey : Any], result: Expected, completion: @escaping (Result) -> Void) { do { let json = result.value as? String guard let data = json?.data(using: .utf8) else { @@ -284,11 +186,11 @@ public class NavigationRouter { } } - fileprivate func doRequest(options: DirectionsOptions, - completion: @escaping (Result) -> Void) -> RequestId { - let directionsUri = settings.directions.url(forCalculating: options).absoluteString + private func doRequest(options: DirectionsOptions, + completion: @escaping (Result) -> Void) -> Request? { + let directionsUri = settings.directions.url(forCalculating: options).removingSKU().absoluteString var requestId: RequestId! - requestsLock.lock() + requestId = router.getRouteForDirectionsUri(directionsUri) { [weak self] (result, _) in guard let self = self else { return } @@ -298,15 +200,147 @@ public class NavigationRouter { result: result, completion: completion) } - activeRequests[requestId] = .init(id: requestId, - router: self) - requestsLock.unlock() - return requestId + let request = Request(requestIdentifier: requestId, + routingProvider: self) + requestsLock { + activeRequests[requestId] = request + } + return request + } + + // MARK: Routes Calculation + + /** + Begins asynchronously calculating routes using the given options and delivers the results to a closure. + + Depending on configured `RouterSource`, this method may retrieve the routes asynchronously from the [Mapbox Directions API](https://www.mapbox.com/api-documentation/navigation/#directions) over a network connection or use onboard routing engine with available offline data. + + Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). + + - parameter options: A `RouteOptions` object specifying the requirements for the resulting routes. + - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. + - returns: Related request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using this handle or `activeRequests`. + */ + @discardableResult public func calculateRoutes(options: RouteOptions, + completionHandler: @escaping Directions.RouteCompletionHandler) -> NavigationProviderRequest? { + return Self.__testRoutesStub?(options, completionHandler) ?? + doRequest(options: options) { [weak self] (result: Result) in + guard let self = self else { return } + let session = (options: options as DirectionsOptions, + credentials: self.settings.directions.credentials) + completionHandler(session, result) + } + } + + /** + Begins asynchronously calculating matches using the given options and delivers the results to a closure. + + Depending on configured `RouterSource`, this method may retrieve the matches asynchronously from the [Mapbox Map Matching API](https://docs.mapbox.com/api/navigation/#map-matching) over a network connection or use onboard routing engine with available offline data. + + - parameter options: A `MatchOptions` object specifying the requirements for the resulting matches. + - parameter completionHandler: The closure (block) to call with the resulting matches. This closure is executed on the application’s main thread. + - returns: Related request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using this handle or `activeRequests`. + */ + @discardableResult public func calculateRoutes(options: MatchOptions, + completionHandler: @escaping Directions.MatchCompletionHandler) -> NavigationProviderRequest? { + return doRequest(options: options) { (result: Result) in + let session = (options: options as DirectionsOptions, + credentials: self.settings.directions.credentials) + completionHandler(session, result) + } + } + + // MARK: Routes Refreshing + + /** + Begins asynchronously refreshing the selected route, optionally starting from an arbitrary leg. + + This method retrieves skeleton route data asynchronously from the Mapbox Directions Refresh API over a network connection. If a connection error or server error occurs, details about the error are passed into the given completion handler in lieu of the routes. + + - precondition: Set `RouteOptions.refreshingEnabled` to `true` when calculating the original route. + + - parameter indexedRouteResponse: The `RouteResponse` and selected `routeIndex` in it to be refreshed. + - parameter fromLegAtIndex: The index of the leg in the route at which to begin refreshing. The response will omit any leg before this index and refresh any leg from this index to the end of the route. If this argument is omitted, the entire route is refreshed. + - parameter completionHandler: The closure (block) to call with updated `RouteResponse` data. Order of `routes` remain unchanged comparing to original `indexedRouteResponse`. This closure is executed on the application’s main thread. + - returns: Related request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using this handle or `activeRequests`. + */ + @discardableResult public func refreshRoute(indexedRouteResponse: IndexedRouteResponse, + fromLegAtIndex startLegIndex: UInt32 = 0, + completionHandler: @escaping Directions.RouteCompletionHandler) -> NavigationProviderRequest? { + guard case let .route(routeOptions) = indexedRouteResponse.routeResponse.options else { + preconditionFailure("Invalid route data passed for refreshing. Expected `RouteResponse` containing `.route` `ResponseOptions` but got `.match`.") + } + + let session = (options: routeOptions as DirectionsOptions, + credentials: self.settings.directions.credentials) + + guard let responseIdentifier = indexedRouteResponse.routeResponse.identifier else { + DispatchQueue.main.async { + completionHandler(session, .failure(.noData)) + } + return nil + } + + let encoder = JSONEncoder() + encoder.userInfo[.options] = routeOptions + + let routeIndex = UInt32(indexedRouteResponse.routeIndex) + + guard let routeData = try? encoder.encode(indexedRouteResponse.routeResponse), + let routeJSONString = String(data: routeData, encoding: .utf8) else { + preconditionFailure("Could not serialize route data for refreshing.") + } + + var requestId: RequestId! + let refreshOptions = RouteRefreshOptions(requestId: responseIdentifier, + routeIndex: routeIndex, + legIndex: startLegIndex, + routingProfile: routeOptions.profileIdentifier.nativeProfile) + + requestId = router.getRouteRefresh(for: refreshOptions, + route: routeJSONString) { [weak self] result, _ in + guard let self = self else { return } + + self.parseResponse(requestId: requestId, + userInfo: [.options: routeOptions, + .credentials: self.settings.directions.credentials], + result: result) { (response: Result) in + completionHandler(session, response) + } + } + let request = Request(requestIdentifier: requestId, + routingProvider: self) + requestsLock { + activeRequests[requestId] = request + } + return request } } -extension DirectionsProfileIdentifier { +extension ProfileIdentifier { var nativeProfile: RoutingProfile { - return RoutingProfile(profile: rawValue) + var mode: RoutingMode + switch self { + case .automobile: + mode = .driving + case .automobileAvoidingTraffic: + mode = .drivingTraffic + case .cycling: + mode = .cycling + case .walking: + mode = .walking + default: + mode = .driving + } + return RoutingProfile(mode: mode, account: "mapbox") + } +} + +extension URL { + func removingSKU() -> URL { + var urlComponents = URLComponents(string: self.absoluteString)! + let filteredItems = urlComponents.queryItems?.filter { $0.name != "sku" } + urlComponents.queryItems = filteredItems + return urlComponents.url! } } diff --git a/Sources/MapboxCoreNavigation/NativeHandlersFactory.swift b/Sources/MapboxCoreNavigation/NativeHandlersFactory.swift index 926f506e49a..2428c1931e7 100644 --- a/Sources/MapboxCoreNavigation/NativeHandlersFactory.swift +++ b/Sources/MapboxCoreNavigation/NativeHandlersFactory.swift @@ -15,14 +15,14 @@ class NativeHandlersFactory { let tileStorePath: String let credentials: Credentials - let tilesVersion: String? + let tilesVersion: String let historyDirectoryURL: URL? let targetVersion: String? let configFactoryType: ConfigFactory.Type init(tileStorePath: String, credentials: Credentials, - tilesVersion: String? = nil, + tilesVersion: String = "", historyDirectoryURL: URL? = nil, targetVersion: String? = nil, configFactoryType: ConfigFactory.Type = ConfigFactory.self) { @@ -38,7 +38,9 @@ class NativeHandlersFactory { lazy var historyRecorder: HistoryRecorderHandle? = { historyDirectoryURL.flatMap { - HistoryRecorderHandle.build(forHistoryDir: $0.path, config: configHandle) + historyRecorderHandlerFactory.getHandler(with: (path: $0.path, + configHandle: configHandle), + cacheData: self) } }() @@ -51,9 +53,9 @@ class NativeHandlersFactory { }() lazy var cacheHandle: CacheHandle = { - CacheHandlerFactory.getHandler(for: tilesConfig, - config: configHandle, - historyRecorder: historyRecorder, + cacheHandlerFactory.getHandler(with: (tilesConfig: tilesConfig, + configHandle: configHandle, + historyRecorder: historyRecorder), cacheData: self) }() @@ -74,7 +76,7 @@ class NativeHandlersFactory { lazy var endpointConfig: TileEndpointConfiguration = { TileEndpointConfiguration(credentials: credentials, - tilesVersion: tilesVersion ?? "", + tilesVersion: tilesVersion, minimumDaysToPersistVersion: nil, targetVersion: targetVersion) }() diff --git a/Sources/MapboxCoreNavigation/NavigationService.swift b/Sources/MapboxCoreNavigation/NavigationService.swift index bff6f506522..44141f71a8d 100644 --- a/Sources/MapboxCoreNavigation/NavigationService.swift +++ b/Sources/MapboxCoreNavigation/NavigationService.swift @@ -5,9 +5,9 @@ import MapboxDirections /** A navigation service coordinates various nonvisual components that track the user as they navigate along a predetermined route. You use `MapboxNavigationService`, which conforms to this protocol, either as part of `NavigationViewController` or by itself as part of a custom user interface. A navigation service calls methods on its `delegate`, which conforms to the `NavigationServiceDelegate` protocol, whenever significant events or decision points occur along the route. - A navigation service controls a `NavigationLocationManager` for determining the user’s location, a `Router` that tracks the user’s progress along the route, a `Directions` service for calculating new routes (only used when rerouting), and a `NavigationEventsManager` for sending telemetry events related to navigation or user feedback. + A navigation service controls a `NavigationLocationManager` for determining the user’s location, a `Router` that tracks the user’s progress along the route, a `MapboxRoutingProvider` service for calculating new routes (only used when rerouting), and a `NavigationEventsManager` for sending telemetry events related to navigation or user feedback. - `NavigationViewController` comes with a `MapboxNavigationService` by default. You may override it to customize the `Directions` service or simulation mode. After creating the navigation service, pass it into `NavigationOptions(styles:navigationService:voiceController:topBanner:bottomBanner:)`, then pass that object into `NavigationViewController(for:options:)`. + `NavigationViewController` comes with a `MapboxNavigationService` by default. You may override it to customize the `MapboxRoutingProvider`'s source service or simulation mode. After creating the navigation service, pass it into `NavigationOptions(styles:navigationService:voiceController:topBanner:bottomBanner:)`, then pass that object into `NavigationViewController(for:options:)`. If you use a navigation service by itself, outside of `NavigationViewController`, call `start()` when the user is ready to begin navigating along the route. */ @@ -20,8 +20,19 @@ public protocol NavigationService: CLLocationManagerDelegate, RouterDataSource, /** A reference to a MapboxDirections service. Used for rerouting. */ + @available(*, deprecated, message: "Use `routingProvider` instead. If navigation service was not initialized using `Directions` object - this property is unused and ignored.") var directions: Directions { get } + /** + `RoutingProvider`, used to create route. + */ + var routingProvider: RoutingProvider { get } + + /** + Credentials data, used to authorize server requests. + */ + var credentials: Credentials { get } + /** The router object that tracks the user’s progress as they travel along a predetermined route. */ @@ -262,6 +273,38 @@ public class MapboxNavigationService: NSObject, NavigationService { stop() } + /** + Intializes a new `NavigationService`. + + - parameter routeResponse: `RouteResponse` object, containing selection of routes to follow. + - parameter routeIndex: The index of the route within the original `RouteResponse` object. + - parameter routeOptions: The route options used to get the route. + - parameter directions: The Directions object that created `route`. If this argument is omitted, the shared value of `NavigationSettings.directions` will be used. + - parameter locationSource: An optional override for the default `NaviationLocationManager`. + - parameter eventsManagerType: An optional events manager type to use while tracking the route. + - parameter simulationMode: The simulation mode desired. + - parameter routerType: An optional router type to use for traversing the route. + */ + @available(*, deprecated, renamed: "init(routeResponse:routeIndex:routeOptions:routingProvider:credentials:locationSource:eventsManagerType:simulating:routerType:)") + public convenience init(routeResponse: RouteResponse, + routeIndex: Int, + routeOptions: RouteOptions, + directions: Directions? = nil, + locationSource: NavigationLocationManager? = nil, + eventsManagerType: NavigationEventsManager.Type? = nil, + simulating simulationMode: SimulationMode? = nil, + routerType: Router.Type? = nil) { + self.init(routeResponse: routeResponse, + routeIndex: routeIndex, + routeOptions: routeOptions, + routingProvider: directions ?? Directions.shared, + credentials: directions?.credentials ?? NavigationSettings.shared.directions.credentials, + locationSource: locationSource, + eventsManagerType: eventsManagerType, + simulating: simulationMode, + routerType: routerType) + } + /** Intializes a new `NavigationService`. Useful convienence initalizer for OBJ-C users, for when you just want to set up a service without customizing anything. @@ -270,7 +313,13 @@ public class MapboxNavigationService: NSObject, NavigationService { - parameter routeOptions: The route options used to get the route. */ convenience init(routeResponse: RouteResponse, routeIndex: Int, routeOptions options: RouteOptions) { - self.init(routeResponse: routeResponse, routeIndex: routeIndex, routeOptions: options, directions: nil, locationSource: nil, eventsManagerType: nil) + self.init(routeResponse: routeResponse, + routeIndex: routeIndex, + routeOptions: options, + routingProvider: NavigationSettings.shared.directions, + credentials: NavigationSettings.shared.directions.credentials, + locationSource: nil, + eventsManagerType: nil) } /** @@ -279,7 +328,8 @@ public class MapboxNavigationService: NSObject, NavigationService { - parameter routeResponse: `RouteResponse` object, containing selection of routes to follow. - parameter routeIndex: The index of the route within the original `RouteResponse` object. - parameter routeOptions: The route options used to get the route. - - parameter directions: The Directions object that created `route`. If this argument is omitted, the shared value of `NavigationSettings.directions` will be used. + - parameter routingProvider: `RoutingProvider`, used to create route. + - parameter credentials: Credentials to authorize additional data requests throughout the route. - parameter locationSource: An optional override for the default `NaviationLocationManager`. - parameter eventsManagerType: An optional events manager type to use while tracking the route. - parameter simulationMode: The simulation mode desired. @@ -288,13 +338,15 @@ public class MapboxNavigationService: NSObject, NavigationService { required public init(routeResponse: RouteResponse, routeIndex: Int, routeOptions: RouteOptions, - directions: Directions? = nil, + routingProvider: RoutingProvider, + credentials: Credentials, locationSource: NavigationLocationManager? = nil, eventsManagerType: NavigationEventsManager.Type? = nil, simulating simulationMode: SimulationMode? = nil, routerType: Router.Type? = nil) { nativeLocationSource = locationSource ?? NavigationLocationManager() - self.directions = directions ?? NavigationSettings.shared.directions + self.routingProvider = routingProvider + self.credentials = credentials self.simulationMode = simulationMode ?? .inTunnels super.init() resumeNotifications() @@ -307,12 +359,16 @@ public class MapboxNavigationService: NSObject, NavigationService { } let routerType = routerType ?? DefaultRouter.self - _router = routerType.init(alongRouteAtIndex: routeIndex, in: routeResponse, options: routeOptions, directions: self.directions, dataSource: self) + _router = routerType.init(alongRouteAtIndex: routeIndex, + in: routeResponse, + options: routeOptions, + routingProvider: routingProvider, + dataSource: self) NavigationSettings.shared.distanceUnit = .init(routeOptions.distanceMeasurementSystem) let eventType = eventsManagerType ?? NavigationEventsManager.self _eventsManager = eventType.init(activeNavigationDataSource: self, - accessToken: self.directions.credentials.accessToken) + accessToken: self.credentials.accessToken) locationManager.activityType = routeOptions.activityType bootstrapEvents() @@ -371,7 +427,18 @@ public class MapboxNavigationService: NSObject, NavigationService { /** A reference to a MapboxDirections service. Used for rerouting. */ - public var directions: Directions + @available(*, deprecated, message: "Use `routingProvider` instead. If navigation service was not initialized using `Directions` object - this property is unused and ignored.") + public lazy var directions: Directions = self.routingProvider as? Directions ?? NavigationSettings.shared.directions + + /** + `RoutingProvider`, used to create route. + */ + public var routingProvider: RoutingProvider + + /** + Credentials data, used to authorize server requests. + */ + public var credentials: Credentials // MARK: Managing Route-Related Data diff --git a/Sources/MapboxCoreNavigation/RouteController.swift b/Sources/MapboxCoreNavigation/RouteController.swift index f60e005db8b..f077010c039 100644 --- a/Sources/MapboxCoreNavigation/RouteController.swift +++ b/Sources/MapboxCoreNavigation/RouteController.swift @@ -48,9 +48,15 @@ open class RouteController: NSObject { public unowned var dataSource: RouterDataSource /** - Routing source type used to create the route. + A reference to a MapboxDirections service. Used for rerouting. */ - public var routingSource: NavigationRouter.RouterSource + @available(*, deprecated, message: "Use `routingProvider` instead. If route controller was not initialized using `Directions` object - this property is unused and ignored.") + public lazy var directions: Directions = routingProvider as? Directions ?? Directions.shared + + /** + `RoutingProvider`, used to create route. + */ + public var routingProvider: RoutingProvider public var route: Route { return routeProgress.route @@ -186,7 +192,7 @@ open class RouteController: NSObject { var didFindFasterRoute = false - var routeTask: NavigationRouter.RoutingRequest? + var routeTask: NavigationProviderRequest? // MARK: Navigating @@ -520,8 +526,21 @@ open class RouteController: NSObject { // MARK: Handling Lifecycle - required public init(alongRouteAtIndex routeIndex: Int, in routeResponse: RouteResponse, options: RouteOptions, routingSource: NavigationRouter.RouterSource = .hybrid, dataSource source: RouterDataSource) { - self.routingSource = routingSource + @available(*, deprecated, renamed: "init(alongRouteAtIndex:in:options:routingProvider:dataSource:)") + public convenience init(alongRouteAtIndex routeIndex: Int, in routeResponse: RouteResponse, options: RouteOptions, directions: Directions = NavigationSettings.shared.directions, dataSource source: RouterDataSource) { + self.init(alongRouteAtIndex: routeIndex, + in: routeResponse, + options: options, + routingProvider: directions, + dataSource: source) + } + + required public init(alongRouteAtIndex routeIndex: Int, + in routeResponse: RouteResponse, + options: RouteOptions, + routingProvider: RoutingProvider, + dataSource source: RouterDataSource) { + self.routingProvider = routingProvider self.indexedRouteResponse = .init(routeResponse: routeResponse, routeIndex: routeIndex) self.routeProgress = RouteProgress(route: routeResponse.routes![routeIndex], options: options) self.dataSource = source @@ -679,8 +698,7 @@ extension RouteController: Router { routeOptions: RouteOptions?, isProactive: Bool, completion: ((Bool) -> Void)?) { - guard let routes = indexedRouteResponse.routeResponse.routes, - routes.count > indexedRouteResponse.routeIndex else { + guard let route = indexedRouteResponse.currentRoute else { preconditionFailure("`indexedRouteResponse` does not contain route for index `\(indexedRouteResponse.routeIndex)` when updating route.") } if shouldStartNewBillingSession(for: route, routeOptions: routeOptions) { diff --git a/Sources/MapboxCoreNavigation/RouteProgress.swift b/Sources/MapboxCoreNavigation/RouteProgress.swift index 9ee6bd0be27..e7f5982d5f7 100644 --- a/Sources/MapboxCoreNavigation/RouteProgress.swift +++ b/Sources/MapboxCoreNavigation/RouteProgress.swift @@ -133,6 +133,10 @@ open class RouteProgress: Codable { */ func refreshRoute(with refreshedRoute: Route, at location: CLLocation) { route = refreshedRoute + refreshLegProgress(at: location) + } + + private func refreshLegProgress(at location: CLLocation) { currentLegProgress = RouteLegProgress(leg: route.legs[legIndex], stepIndex: currentLegProgress.stepIndex, spokenInstructionIndex: currentLegProgress.currentStepProgress.spokenInstructionIndex) @@ -140,6 +144,15 @@ open class RouteProgress: Codable { updateDistanceTraveled(with: location) } + /** + Updates the current route with attributes from the given skeletal route. + */ + @available(*, deprecated, message: "Route refreshing logic should be handled by the SDK. There is no need to refresh the route object manually.") + public func refreshRoute(with refreshedRoute: RefreshedRoute, at location: CLLocation) { + route.refreshLegAttributes(from: refreshedRoute) + refreshLegProgress(at: location) + } + /** Increments the progress according to new location specified. - parameter location: Updated user location. diff --git a/Sources/MapboxCoreNavigation/Router.swift b/Sources/MapboxCoreNavigation/Router.swift index eccfe974697..977322805e6 100644 --- a/Sources/MapboxCoreNavigation/Router.swift +++ b/Sources/MapboxCoreNavigation/Router.swift @@ -28,8 +28,9 @@ public struct IndexedRouteResponse { /** Returns a route from the `routeResponse` under given `routeIndex` if possible. */ - public var selectedRoute: Route? { - guard routeResponse.routes?.count ?? 0 > routeIndex else { + public var currentRoute: Route? { + guard let routes = routeResponse.routes, + routes.count > routeIndex else { return nil } return routeResponse.routes?[routeIndex] @@ -76,10 +77,14 @@ public protocol Router: CLLocationManagerDelegate { - parameter routeIndex: The index of the route within the original `RouteResponse` object. - parameter routeResponse: `RouteResponse` object, containing selection of routes to follow. - - parameter routingSource: `NavigationRouter` source type, used to create route. + - parameter routingProvider: `RoutingProvider`, used to create route. - parameter source: The data source for the RouteController. */ - init(alongRouteAtIndex routeIndex: Int, in routeResponse: RouteResponse, options: RouteOptions, routingSource: NavigationRouter.RouterSource, dataSource source: RouterDataSource) + 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. @@ -164,7 +169,7 @@ protocol InternalRouter: AnyObject { var lastRouteRefresh: Date? { get set } - var routeTask: NavigationRouter.RoutingRequest? { get set } + var routeTask: NavigationProviderRequest? { get set } var lastRerouteLocation: CLLocation? { get set } @@ -172,7 +177,7 @@ protocol InternalRouter: AnyObject { var isRefreshing: Bool { get set } - var routingSource: NavigationRouter.RouterSource { get } + var routingProvider: RoutingProvider { get } var routeProgress: RouteProgress { get } @@ -189,49 +194,54 @@ 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, router: $0) + self.checkForFasterRoute(from: location, routeProgress: routeProgress) } } else { checkForFasterRoute(from: location, routeProgress: routeProgress) } } - func refreshRoute(from location: CLLocation, legIndex: Int, completion: @escaping (NavigationRouter?)->()) { + func refreshRoute(from location: CLLocation, legIndex: Int, completion: @escaping ()->()) { guard refreshesRoute else { - completion(nil) + completion() return } guard let lastRouteRefresh = lastRouteRefresh else { self.lastRouteRefresh = location.timestamp - completion(nil) + completion() return } guard location.timestamp.timeIntervalSince(lastRouteRefresh) >= RouteControllerProactiveReroutingInterval else { - completion(nil) + completion() return } if isRefreshing { - completion(nil) + completion() return } isRefreshing = true - let router = NavigationRouter(self.routingSource) - router.refreshRoute(indexedRouteResponse: indexedRouteResponse, - fromLegAtIndex: UInt32(legIndex)) { [weak self, weak router] session, result in + routingProvider.refreshRoute(indexedRouteResponse: indexedRouteResponse, + fromLegAtIndex: UInt32(legIndex)) { [weak self] session, result in defer { self?.isRefreshing = false self?.lastRouteRefresh = nil - completion(router) + completion() } guard case let .success(response) = result, let self = self else { return } self.indexedRouteResponse = .init(routeResponse: response, routeIndex: self.indexedRouteResponse.routeIndex) - self.routeProgress.refreshRoute(with: self.indexedRouteResponse.selectedRoute!, at: location) + + 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 @@ -240,7 +250,7 @@ extension InternalRouter where Self: Router { } } - func checkForFasterRoute(from location: CLLocation, routeProgress: RouteProgress, router: NavigationRouter? = nil) { + func checkForFasterRoute(from location: CLLocation, routeProgress: RouteProgress) { // Check for faster route given users current location guard reroutesProactively else { return } @@ -269,7 +279,7 @@ extension InternalRouter where Self: Router { if isRerouting { return } isRerouting = true - calculateRoutes(from: location, along: routeProgress, router: router) { [weak self] (session, result) in + calculateRoutes(from: location, along: routeProgress) { [weak self] (session, result) in guard let self = self else { return } guard case let .success(indexedResponse) = result else { @@ -317,14 +327,13 @@ extension InternalRouter where Self: Router { - 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, router: NavigationRouter? = nil, completion: @escaping IndexedRouteCompletionHandler) { + func calculateRoutes(from origin: CLLocation, along progress: RouteProgress, completion: @escaping IndexedRouteCompletionHandler) { routeTask?.cancel() let options = progress.reroutingOptions(with: origin) lastRerouteLocation = origin - let router = router ?? NavigationRouter(self.routingSource) - let taskId = router.requestRoutes(options: options) {(session, result) in + routeTask = routingProvider.calculateRoutes(options: options) {(session, result) in defer { self.routeTask = nil } switch result { case .failure(let error): @@ -337,7 +346,6 @@ extension InternalRouter where Self: Router { return completion(session, .success(.init(routeResponse: response, routeIndex: mostSimilarIndex))) } } - routeTask = router.activeRequests[taskId] } func announceImpendingReroute(at location: CLLocation) { diff --git a/Sources/MapboxCoreNavigation/RoutingProvider.swift b/Sources/MapboxCoreNavigation/RoutingProvider.swift new file mode 100644 index 00000000000..afbf0bcdca1 --- /dev/null +++ b/Sources/MapboxCoreNavigation/RoutingProvider.swift @@ -0,0 +1,59 @@ +import Foundation +import MapboxDirections + +/** + Protocol which defines a type which can be used for fetching or refreshing routes. + + SDK provides conformance to this protocol for `Directions` and `MapboxRoutingProvider`. + */ +public protocol RoutingProvider { + + /** + Routing caluclation method. + + - parameter options: A `RouteOptions` object specifying the requirements for the resulting routes. + - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. + - returns: Related request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using this handle. + */ + @discardableResult func calculateRoutes(options: RouteOptions, + completionHandler: @escaping Directions.RouteCompletionHandler) -> NavigationProviderRequest? + + /** + Map matching calculation method. + + - parameter options: A `MatchOptions` object specifying the requirements for the resulting matches. + - parameter completionHandler: The closure (block) to call with the resulting matches. This closure is executed on the application’s main thread. + - returns: Related request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using this handle. + */ + @discardableResult func calculateRoutes(options: MatchOptions, + completionHandler: @escaping Directions.MatchCompletionHandler) -> NavigationProviderRequest? + + /** + Route refreshing method. + + - parameter indexedRouteResponse: The `RouteResponse` and selected `routeIndex` in it to be refreshed. + - parameter fromLegAtIndex: The index of the leg in the route at which to begin refreshing. The response will omit any leg before this index and refresh any leg from this index to the end of the route. If this argument is omitted, the entire route is refreshed. + - parameter completionHandler: The closure (block) to call with updated `RouteResponse` data. Order of `routes` remain unchanged comparing to original `indexedRouteResponse`. This closure is executed on the application’s main thread. + - returns: Related request. If, while waiting for the completion handler to execute, you no longer want the resulting routes, cancel corresponding task using this handle. + */ + @discardableResult func refreshRoute(indexedRouteResponse: IndexedRouteResponse, + fromLegAtIndex: UInt32, + completionHandler: @escaping Directions.RouteCompletionHandler) -> NavigationProviderRequest? +} + +/** + `RoutingProvider` request type. + */ +public protocol NavigationProviderRequest { + /** + Request identifier. + + Unique within related `RoutingProvider`. + */ + var requestIdentifier: UInt64 { get } + + /** + Cancels ongoing request if it didn't finish yet. + */ + func cancel() +} diff --git a/Sources/MapboxNavigation/CarPlayManager.swift b/Sources/MapboxNavigation/CarPlayManager.swift index d66cbd846b3..22e5cc8ba0b 100644 --- a/Sources/MapboxNavigation/CarPlayManager.swift +++ b/Sources/MapboxNavigation/CarPlayManager.swift @@ -58,6 +58,17 @@ public class CarPlayManager: NSObject { */ public let eventsManager: NavigationEventsManager + /** + The object that calculates routes when the user interacts with the CarPlay interface. + */ + @available(*, deprecated, message: "Use `routingProvider` instead. If car play manager was not initialized using `Directions` object - this property is unused and ignored.") + public lazy var directions: Directions = self.routingProvider as? Directions ?? NavigationSettings.shared.directions + + /** + `RoutingProvider`, used to create route. + */ + public var routingProvider: RoutingProvider + /** Returns current `CarPlayActivity`, which is based on currently present `CPTemplate`. In case if `CPTemplate` was not created by `CarPlayManager` `currentActivity` will be assigned to `nil`. @@ -99,19 +110,41 @@ public class CarPlayManager: NSObject { Initializes a new CarPlay manager that manages a connection to the CarPlay interface. - parameter styles: The styles to display in the CarPlay interface. If this argument is omitted, `DayStyle` and `NightStyle` are displayed by default. + - parameter directions: The object that calculates routes when the user interacts with the CarPlay interface. If this argument is `nil` or omitted, the shared `Directions` object is used by default. + - parameter eventsManager: The events manager to use during turn-by-turn navigation while connected to CarPlay. If this argument is `nil` or omitted, a standard `NavigationEventsManager` object is used by default. + */ + @available(*, deprecated, renamed: "init(styles:routingProvider:eventsManager:carPlayNavigationViewControllerClass:)") + public convenience init(styles: [Style]? = nil, + directions: Directions? = nil, + eventsManager: NavigationEventsManager? = nil) { + self.init(styles: styles, + routingProvider: directions ?? NavigationSettings.shared.directions, + eventsManager: eventsManager, + carPlayNavigationViewControllerClass: nil) + } + + /** + Initializes a new CarPlay manager that manages a connection to the CarPlay interface. + + - parameter styles: The styles to display in the CarPlay interface. If this argument is omitted, `DayStyle` and `NightStyle` are displayed by default. + - parameter routingProvider: The object that calculates routes when the user interacts with the CarPlay interface. - parameter eventsManager: The events manager to use during turn-by-turn navigation while connected to CarPlay. If this argument is `nil` or omitted, a standard `NavigationEventsManager` object is used by default. */ public convenience init(styles: [Style]? = nil, + routingProvider: RoutingProvider, eventsManager: NavigationEventsManager? = nil) { self.init(styles: styles, + routingProvider: routingProvider, eventsManager: eventsManager, carPlayNavigationViewControllerClass: nil) } init(styles: [Style]? = nil, + routingProvider: RoutingProvider, eventsManager: NavigationEventsManager? = nil, carPlayNavigationViewControllerClass: CarPlayNavigationViewController.Type? = nil) { self.styles = styles ?? [DayStyle(), NightStyle()] + self.routingProvider = routingProvider self.eventsManager = eventsManager ?? .init(activeNavigationDataSource: nil, accessToken: NavigationSettings.shared.directions.credentials.accessToken) self.mapTemplateProvider = MapTemplateProvider() @@ -515,7 +548,7 @@ extension CarPlayManager { } func calculate(_ options: RouteOptions, completionHandler: @escaping Directions.RouteCompletionHandler) { - NavigationRouter().requestRoutes(options: options, completionHandler: completionHandler) + routingProvider.calculateRoutes(options: options, completionHandler: completionHandler) } func didCalculate(_ result: Result, @@ -609,6 +642,8 @@ extension CarPlayManager: CPMapTemplateDelegate { MapboxNavigationService(routeResponse: routeResponse, routeIndex: routeIndex, routeOptions: options, + routingProvider: NavigationSettings.shared.directions, + credentials: NavigationSettings.shared.directions.credentials, simulating: desiredSimulationMode) // Store newly created `MapboxNavigationService`. diff --git a/Sources/MapboxNavigation/NavigationViewController.swift b/Sources/MapboxNavigation/NavigationViewController.swift index 20b302d47e2..e84f5d6f265 100644 --- a/Sources/MapboxNavigation/NavigationViewController.swift +++ b/Sources/MapboxNavigation/NavigationViewController.swift @@ -89,7 +89,7 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter public var voiceController: RouteVoiceController! func setupVoiceController() { - let credentials = navigationService.directions.credentials + let credentials = navigationService.credentials voiceController = navigationOptions?.voiceController ?? RouteVoiceController(navigationService: navigationService, accessToken: credentials.accessToken, @@ -183,10 +183,11 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter var _routeResponse: RouteResponse? /** - An instance of `Directions` need for rerouting. See [Mapbox Directions](https://docs.mapbox.com/ios/api/directions/) for further information. + A reference to a MapboxDirections service. Used for rerouting. */ + @available(*, deprecated, message: "Use `navigationService.routingProvider` instead. If navigation service was not initialized using `Directions` object - this property is unused and ignored.") public var directions: Directions { - return navigationService.directions + navigationService?.directions ?? NavigationSettings.shared.directions } /** @@ -217,6 +218,8 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter ?? MapboxNavigationService(routeResponse: routeResponse, routeIndex: routeIndex, routeOptions: routeOptions, + routingProvider: NavigationSettings.shared.directions, + credentials: NavigationSettings.shared.directions.credentials, simulating: navigationOptions?.simulationMode) navigationService.delegate = self @@ -293,7 +296,7 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter /** Initializes a `NavigationViewController` that presents the user interface for following a predefined route based on the given options. - The route may come directly from the completion handler of the [MapboxDirections](https://docs.mapbox.com/ios/api/directions/) framework’s `Directions.calculate(_:completionHandler:)` method, MapboxCoreNavigation `NavigationRouter.requestRoutes(options:completionHandler:)`, or it may be unarchived or created from a JSON object. + The route may come directly from the completion handler of the [MapboxDirections](https://docs.mapbox.com/ios/api/directions/) framework’s `Directions.calculate(_:completionHandler:)` method, MapboxCoreNavigation `MapboxRoutingProvider.calculateRoutes(options:completionHandler:)`, or it may be unarchived or created from a JSON object. - parameter routeResponse: `RouteResponse` object, containing selection of routes to follow. - parameter routeIndex: The index of the route within the original `RouteResponse` object. diff --git a/Tests/MapboxCoreNavigationTests/BillingHandlerTests.swift b/Tests/MapboxCoreNavigationTests/BillingHandlerTests.swift index a381d8901cc..dbdf19d3ce4 100644 --- a/Tests/MapboxCoreNavigationTests/BillingHandlerTests.swift +++ b/Tests/MapboxCoreNavigationTests/BillingHandlerTests.swift @@ -539,6 +539,7 @@ final class BillingHandlerUnitTests: TestCase { let routeController = RouteController(alongRouteAtIndex: 0, in: initialRouteResponse, options: NavigationRouteOptions(coordinates: initialRouteWaypoints), + routingProvider: MapboxRoutingProvider(.offline), dataSource: dataSource) let routeUpdated = expectation(description: "Route updated") @@ -601,13 +602,11 @@ final class BillingHandlerUnitTests: TestCase { .map { CLLocation(coordinate: $0) } .shiftedToPresent() - let directions = DirectionsSpy() - let navOptions = NavigationRouteOptions(coordinates: [origin, destination]) let routeController = RouteController(alongRouteAtIndex: 0, in: routeResponse, options: navOptions, - directions: directions, + routingProvider: MapboxRoutingProvider(.offline), dataSource: self) let routerDelegateSpy = RouterDelegateSpy() diff --git a/Tests/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift b/Tests/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift index ebaba06ecc8..5ab335cd576 100644 --- a/Tests/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift +++ b/Tests/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift @@ -42,7 +42,8 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - routingSource: .offline, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, simulating: .never) let now = Date() let steps = route.legs.first!.steps @@ -86,7 +87,8 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - routingSource: .offline, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, simulating: .never) // Coordinates from first step @@ -135,7 +137,8 @@ class MapboxCoreNavigationTests: TestCase { let navigationService = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - routingSource: .offline, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: locationManager, simulating: .never) @@ -191,7 +194,8 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - routingSource: .offline, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: locationManager, simulating: .never) @@ -242,7 +246,8 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - routingSource: .offline, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: locationManager, simulating: .never) expectation(forNotification: .routeControllerWillReroute, object: navigation.router) { (notification) -> Bool in @@ -285,7 +290,8 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: routeResponse, routeIndex: 0, routeOptions: navOptions, - routingSource: .offline, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: locationManager, simulating: .never) navigation.router.refreshesRoute = false @@ -334,7 +340,8 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - routingSource: .offline, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: locationManager) navigation.router.refreshesRoute = false @@ -455,7 +462,8 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - routingSource: .online, + routingProvider: MapboxRoutingProvider(.online), + credentials: Fixture.credentials, simulating: .never) expectation(forNotification: .routeControllerWillReroute, object: navigation.router) { (notification) -> Bool in diff --git a/Tests/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift b/Tests/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift index a99aa17861a..e7d9f22eff1 100644 --- a/Tests/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift +++ b/Tests/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift @@ -35,6 +35,8 @@ class NavigationEventsManagerTests: TestCase { let locationManager = NavigationLocationManager() let service = MapboxNavigationService(routeResponse: firstRouteResponse, routeIndex: 0, routeOptions: firstRouteOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: locationManager, eventsManagerType: NavigationEventsManagerSpy.self, simulating: .always) @@ -87,7 +89,12 @@ class NavigationEventsManagerTests: TestCase { let eventTimeout = 0.3 let route = Fixture.route(from: "DCA-Arboretum", options: routeOptions) let routeResponse = Fixture.routeResponse(from: "DCA-Arboretum", options: routeOptions) - let dataSource = MapboxNavigationService(routeResponse: routeResponse, routeIndex: 0, routeOptions: routeOptions, simulating: .onPoorGPS) + let dataSource = MapboxNavigationService(routeResponse: routeResponse, + routeIndex: 0, + routeOptions: routeOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, + simulating: .onPoorGPS) let sessionState = SessionState(currentRoute: route, originalRoute: route, routeIdentifier: routeResponse.identifier) // Attempt to create NavigationEventDetails object from global queue, no errors from Main Thread Checker diff --git a/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift b/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift index 602ff8021bf..d0ba3ae0918 100644 --- a/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift +++ b/Tests/MapboxCoreNavigationTests/NavigationServiceTests.swift @@ -27,7 +27,8 @@ class NavigationServiceTests: TestCase { let navigationService = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, - routingSource: .offline, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: locationSource, eventsManagerType: NavigationEventsManagerSpy.self, simulating: .never) @@ -64,7 +65,7 @@ class NavigationServiceTests: TestCase { override func tearDown() { super.tearDown() dependencies = nil - NavigationRouter.__testRoutesStub = nil + MapboxRoutingProvider.__testRoutesStub = nil } func testDefaultUserInterfaceUsage() { @@ -410,7 +411,7 @@ class NavigationServiceTests: TestCase { func testTurnstileEventSentUponInitialization() { // MARK: it sends a turnstile event upon initialization - let service = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, locationSource: NavigationLocationManager(), eventsManagerType: NavigationEventsManagerSpy.self) + let service = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, routingProvider: MapboxRoutingProvider(.offline), credentials: Fixture.credentials, locationSource: NavigationLocationManager(), eventsManagerType: NavigationEventsManagerSpy.self) let eventsManagerSpy = service.eventsManager as! NavigationEventsManagerSpy XCTAssertTrue(eventsManagerSpy.hasFlushedEvent(with: MMEEventTypeAppUserTurnstile)) } @@ -419,6 +420,8 @@ class NavigationServiceTests: TestCase { let navigationService = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, eventsManagerType: NavigationEventsManagerSpy.self, simulating: .always) navigationService.delegate = delegate @@ -474,15 +477,15 @@ class NavigationServiceTests: TestCase { } // MARK: Setupping a re-route stub - NavigationRouter.__testRoutesStub = { (options, completionHandler) in - completionHandler(Directions.Session(options, DirectionsCredentials()), + MapboxRoutingProvider.__testRoutesStub = { (options, completionHandler) in + completionHandler(Directions.Session(options, Credentials()), .success(RouteResponse(httpResponse: nil, identifier: nil, routes: [self.alternateRoute], waypoints: nil, options: .route(options), credentials: Fixture.credentials))) - return 0 + return nil } dependencies.navigationService.start() @@ -610,7 +613,7 @@ class NavigationServiceTests: TestCase { autoreleasepool { let fakeDataSource = RouteControllerDataSourceFake() - let routeController = RouteController(alongRouteAtIndex: 0, in: initialRouteResponse, options: routeOptions, dataSource: fakeDataSource) + let routeController = RouteController(alongRouteAtIndex: 0, in: initialRouteResponse, options: routeOptions, routingProvider: MapboxRoutingProvider(.offline), dataSource: fakeDataSource) subject = routeController } @@ -622,7 +625,7 @@ class NavigationServiceTests: TestCase { autoreleasepool { let fakeDataSource = RouteControllerDataSourceFake() - _ = RouteController(alongRouteAtIndex: 0, in: initialRouteResponse, options: routeOptions, dataSource: fakeDataSource) + _ = RouteController(alongRouteAtIndex: 0, in: initialRouteResponse, options: routeOptions, routingProvider: MapboxRoutingProvider(.offline), dataSource: fakeDataSource) subject = fakeDataSource } @@ -630,7 +633,7 @@ class NavigationServiceTests: TestCase { } func testCountdownTimerDefaultAndUpdate() { - let subject = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, routingSource: .offline) + let subject = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, routingProvider: MapboxRoutingProvider(.offline), credentials: Fixture.credentials) XCTAssert(subject.poorGPSTimer.countdownInterval == .milliseconds(2500), "Default countdown interval should be 2500 milliseconds.") @@ -658,7 +661,8 @@ class NavigationServiceTests: TestCase { routeController.refreshesRoute = false let routeUpdated = expectation(description: "Route Updated") - routeController.updateRoute(with: .init(routeResponse: routeResponse, routeIndex: 0), routeOptions: nil) { + routeController.updateRoute(with: .init(routeResponse: routeResponse, routeIndex: 0), + routeOptions: routeOptions) { success in XCTAssertTrue(success) routeUpdated.fulfill() @@ -708,7 +712,8 @@ class NavigationServiceTests: TestCase { let service = MapboxNavigationService(routeResponse: routeResponse, routeIndex: 0, routeOptions: options, - routingSource: .offline, + routingProvider: MapboxRoutingProvider(.online), + credentials: Fixture.credentials, locationSource: locationManager) service.delegate = delegate let router = service.router @@ -732,10 +737,10 @@ class NavigationServiceTests: TestCase { waypoints: waypointsForFasterRoute, options: .route(options), credentials: Fixture.credentials) - NavigationRouter.__testRoutesStub = { (options, completionHandler) in + MapboxRoutingProvider.__testRoutesStub = { (options, completionHandler) in completionHandler(Directions.Session(options, Fixture.credentials), .success(fasterResponse)) - return 0 + return nil } let rerouteTriggeredExpectation = expectation(description: "Proactive reroute triggered") @@ -748,19 +753,9 @@ class NavigationServiceTests: TestCase { service.start() - wait(for: [rerouteTriggeredExpectation], timeout: locationManager.expectedReplayTime) + wait(for: [rerouteTriggeredExpectation, didRerouteExpectation], timeout: locationManager.expectedReplayTime) locationManager.stopUpdatingLocation() - - let fasterRouteName = "DCA-Arboretum-dummy-faster-route" - let fasterOptions = NavigationRouteOptions(coordinates: [ - CLLocationCoordinate2D(latitude: 38.878206, longitude: -77.037265), - CLLocationCoordinate2D(latitude: 38.910736, longitude: -76.966906), - ]) - let fasterRoute = Fixture.route(from: fasterRouteName, options: fasterOptions) - let waypointsForFasterRoute = Fixture.waypoints(from: fasterRouteName, options: fasterOptions) - directions.fireLastCalculateCompletion(with: waypointsForFasterRoute, routes: [fasterRoute], error: nil) - wait(for: [didRerouteExpectation], timeout: 10) XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:didRerouteAlong:at:proactive:)")) } diff --git a/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift b/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift index 0741bf82f13..8a223bc5734 100644 --- a/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift +++ b/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift @@ -15,7 +15,7 @@ class RouteControllerTests: TestCase { override func tearDown() { replayManager = nil - NavigationRouter.__testRoutesStub = nil + MapboxRoutingProvider.__testRoutesStub = nil super.tearDown() } @@ -29,10 +29,11 @@ class RouteControllerTests: TestCase { let locationManager = ReplayLocationManager(locations: locations) replayManager = locationManager let equivalentRouteOptions = NavigationRouteOptions(navigationMatchOptions: options) - let routeController = RouteController(alongRouteAtIndex: 0, in: routeResponse, options: equivalentRouteOptions, dataSource: self) + let routeController = RouteController(alongRouteAtIndex: 0, in: routeResponse, options: equivalentRouteOptions, routingProvider: MapboxRoutingProvider(.offline), dataSource: self) locationManager.delegate = routeController let routerDelegateSpy = RouterDelegateSpy() routeController.delegate = routerDelegateSpy + routeController.reroutesProactively = false var actualCoordinates = [CLLocationCoordinate2D]() routerDelegateSpy.onShouldDiscard = { location in @@ -75,7 +76,7 @@ class RouteControllerTests: TestCase { let routeController = RouteController(alongRouteAtIndex: 0, in: routeResponse, options: navOptions, - routingSource: .offline, + routingProvider: MapboxRoutingProvider(.offline), dataSource: self) let routerDelegateSpy = RouterDelegateSpy() @@ -111,27 +112,13 @@ class RouteControllerTests: TestCase { didRerouteCalled.fulfill() } - NavigationRouter.__testRoutesStub = { (options, completionHandler) in + MapboxRoutingProvider.__testRoutesStub = { (options, completionHandler) in + DispatchQueue.main.async { + completionHandler(Directions.Session(options, .mocked), + .success(routeResponse)) + } calculateRouteCalled.fulfill() - let currentCoordinate = locationManager.location!.coordinate - - let originWaypoint = Waypoint(coordinate: currentCoordinate) - let destinationWaypoint = Waypoint(coordinate: destination) - - let waypoints = [ - originWaypoint, - destinationWaypoint - ] - - completionHandler(Directions.Session(options, DirectionsCredentials()), - .success(RouteResponse(httpResponse: nil, - identifier: nil, - routes: [Fixture.route(between: currentCoordinate, - and: destination).route], - waypoints: waypoints, - options: .route(options), - credentials: .mocked))) - return 0 + return nil } let replayFinished = expectation(description: "Replay Finished") diff --git a/Tests/MapboxNavigationTests/CarPlayManagerTests.swift b/Tests/MapboxNavigationTests/CarPlayManagerTests.swift index cb937c54bb2..b4380591eb3 100644 --- a/Tests/MapboxNavigationTests/CarPlayManagerTests.swift +++ b/Tests/MapboxNavigationTests/CarPlayManagerTests.swift @@ -20,7 +20,9 @@ class CarPlayManagerTests: TestCase { override func setUp() { super.setUp() eventsManagerSpy = NavigationEventsManagerSpy() - manager = CarPlayManager(eventsManager: eventsManagerSpy, carPlayNavigationViewControllerClass: CarPlayNavigationViewControllerTestable.self) + manager = CarPlayManager(routingProvider: MapboxRoutingProvider(.offline), + eventsManager: eventsManagerSpy, + carPlayNavigationViewControllerClass: CarPlayNavigationViewControllerTestable.self) searchController = CarPlaySearchController() } @@ -190,7 +192,7 @@ class CarPlayManagerTests: TestCase { } func testRouteFailure() { - let manager = CarPlayManager() + let manager = CarPlayManager(routingProvider: MapboxRoutingProvider(.offline)) let spy = CarPlayManagerFailureDelegateSpy() let testError = DirectionsError.requestTooLarge @@ -209,7 +211,7 @@ class CarPlayManagerTests: TestCase { XCTAssertEqual(manager?.styles.last?.styleType, StyleType.night) let styles = [CustomStyle()] - XCTAssertEqual(CarPlayManager(styles: styles).styles, styles, "CarPlayManager should persist the initial styles given to it.") + XCTAssertEqual(CarPlayManager(styles: styles, routingProvider: MapboxRoutingProvider(.offline)).styles, styles, "CarPlayManager should persist the initial styles given to it.") } } @@ -233,7 +235,7 @@ class CarPlayManagerSpec: QuickSpec { Navigator._recreateNavigator() CarPlayMapViewController.swizzleMethods() - manager = CarPlayManager(styles: nil, eventsManager: nil) + manager = CarPlayManager(styles: nil, routingProvider: MapboxRoutingProvider(.offline), eventsManager: nil) delegate = TestCarPlayManagerDelegate() manager!.delegate = delegate @@ -253,7 +255,7 @@ class CarPlayManagerSpec: QuickSpec { } afterEach { - NavigationRouter.__testRoutesStub = nil + MapboxRoutingProvider.__testRoutesStub = nil } let previewRoutesAction = { @@ -270,10 +272,10 @@ class CarPlayManagerSpec: QuickSpec { waypoints: waypoints, options: .route(options), credentials: Fixture.credentials) - NavigationRouter.__testRoutesStub = { (options, completionHandler) in + MapboxRoutingProvider.__testRoutesStub = { (options, completionHandler) in completionHandler(Directions.Session(options, Fixture.credentials), .success(fasterResponse)) - return 0 + return nil } manager!.previewRoutes(for: options, completionHandler: {}) @@ -413,7 +415,12 @@ class CarPlayManagerSpec: QuickSpec { //TODO: ADD OPTIONS TO THIS DELEGATE METHOD func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceFor routeResponse: RouteResponse, routeIndex: Int, routeOptions: RouteOptions, desiredSimulationMode: SimulationMode) -> NavigationService? { - return MapboxNavigationService(routeResponse: routeResponse, routeIndex: routeIndex, routeOptions: routeOptions, simulating: desiredSimulationMode) + return MapboxNavigationService(routeResponse: routeResponse, + routeIndex: routeIndex, + routeOptions: routeOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, + simulating: desiredSimulationMode) } func carPlayManager(_ carPlayManager: CarPlayManager, didPresent navigationViewController: CarPlayNavigationViewController) { diff --git a/Tests/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift b/Tests/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift index 288d6c125db..b47e80b7383 100644 --- a/Tests/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift +++ b/Tests/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift @@ -35,7 +35,7 @@ fileprivate class CPManeuverFake: CPManeuver { class CarPlayNavigationViewControllerTests: TestCase { func testCarplayDisplaysCorrectEstimates() { //set up the litany of dependancies - let manager = CarPlayManager() + let manager = CarPlayManager(routingProvider: MapboxRoutingProvider(.offline)) let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 9.519172, longitude: 47.210823), CLLocationCoordinate2D(latitude: 9.52222, longitude: 47.214268), diff --git a/Tests/MapboxNavigationTests/CarPlayUtils.swift b/Tests/MapboxNavigationTests/CarPlayUtils.swift index 116bfbf61a8..48fa5a82278 100644 --- a/Tests/MapboxNavigationTests/CarPlayUtils.swift +++ b/Tests/MapboxNavigationTests/CarPlayUtils.swift @@ -65,7 +65,14 @@ class TestCarPlayManagerDelegate: CarPlayManagerDelegate { func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceFor routeResponse: RouteResponse, routeIndex: Int, routeOptions: RouteOptions, desiredSimulationMode: SimulationMode) -> NavigationService? { let response = Fixture.routeResponse(from: jsonFileName, options: routeOptions) - let service = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, locationSource: NavigationLocationManager(), eventsManagerType: NavigationEventsManagerSpy.self, simulating: desiredSimulationMode) + let service = MapboxNavigationService(routeResponse: response, + routeIndex: 0, + routeOptions: routeOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, + locationSource: NavigationLocationManager(), + eventsManagerType: NavigationEventsManagerSpy.self, + simulating: desiredSimulationMode) return service } diff --git a/Tests/MapboxNavigationTests/EndOfRouteFeedbackTests.swift b/Tests/MapboxNavigationTests/EndOfRouteFeedbackTests.swift index 2aa2619bda8..d5531162779 100644 --- a/Tests/MapboxNavigationTests/EndOfRouteFeedbackTests.swift +++ b/Tests/MapboxNavigationTests/EndOfRouteFeedbackTests.swift @@ -16,7 +16,8 @@ final class EndOfRouteFeedbackTests: TestCase { let service = MapboxNavigationService(routeResponse: route.response, routeIndex: 0, routeOptions: options, - directions: .mocked, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: nil, eventsManagerType: nil, simulating: .never, diff --git a/Tests/MapboxNavigationTests/InstructionsCardCollectionTests.swift b/Tests/MapboxNavigationTests/InstructionsCardCollectionTests.swift index 4e5341378ff..38d6a9f7daf 100644 --- a/Tests/MapboxNavigationTests/InstructionsCardCollectionTests.swift +++ b/Tests/MapboxNavigationTests/InstructionsCardCollectionTests.swift @@ -32,7 +32,12 @@ class InstructionsCardCollectionTests: TestCase { ]) let fakeRoute = Fixture.route(from: "route-with-banner-instructions", options: fakeOptions) - let service = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: fakeOptions, simulating: .never) + let service = MapboxNavigationService(routeResponse: initialRouteResponse, + routeIndex: 0, + routeOptions: fakeOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, + simulating: .never) let routeProgress = RouteProgress(route: fakeRoute, options: fakeOptions) subject.routeProgress = routeProgress diff --git a/Tests/MapboxNavigationTests/LeaksSpec.swift b/Tests/MapboxNavigationTests/LeaksSpec.swift index f6b43d79fb4..7f3ce537871 100644 --- a/Tests/MapboxNavigationTests/LeaksSpec.swift +++ b/Tests/MapboxNavigationTests/LeaksSpec.swift @@ -39,7 +39,12 @@ class LeaksSpec: QuickSpec { ResourceOptionsManager.default.resourceOptions.accessToken = .mockedAccessToken let navigationViewController = LeakTest { - let service = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: self.initialOptions, eventsManagerType: NavigationEventsManagerSpy.self) + let service = MapboxNavigationService(routeResponse: response, + routeIndex: 0, + routeOptions: self.initialOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, + eventsManagerType: NavigationEventsManagerSpy.self) let navOptions = NavigationOptions(navigationService: service, voiceController: RouteVoiceControllerStub(navigationService: self.dummySvc)) return NavigationViewController(for: response, routeIndex: 0, routeOptions: self.initialOptions, navigationOptions: navOptions) diff --git a/Tests/MapboxNavigationTests/NavigationViewControllerTests.swift b/Tests/MapboxNavigationTests/NavigationViewControllerTests.swift index 07c5b5ac743..a7f99faea72 100644 --- a/Tests/MapboxNavigationTests/NavigationViewControllerTests.swift +++ b/Tests/MapboxNavigationTests/NavigationViewControllerTests.swift @@ -72,7 +72,13 @@ class NavigationViewControllerTests: TestCase { dependencies = { UNUserNotificationCenter.replaceWithMock() - let fakeService = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, locationSource: NavigationLocationManagerStub(), simulating: .never) + let fakeService = MapboxNavigationService(routeResponse: initialRouteResponse, + routeIndex: 0, + routeOptions: routeOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, + locationSource: NavigationLocationManagerStub(), + simulating: .never) let fakeVoice: RouteVoiceController = RouteVoiceControllerStub(navigationService: fakeService) let options = NavigationOptions(navigationService: fakeService, voiceController: fakeVoice) let navigationViewController = NavigationViewController(for: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, navigationOptions: options) @@ -278,7 +284,12 @@ class NavigationViewControllerTests: TestCase { } func testDestinationAnnotationUpdatesUponReroute() { - let service = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, simulating: .never) + let service = MapboxNavigationService(routeResponse: initialRouteResponse, + routeIndex: 0, + routeOptions: routeOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, + simulating: .never) let options = NavigationOptions(styles: [TestableDayStyle()], navigationService: service) let navigationViewController = NavigationViewController(for: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, navigationOptions: options) expectation(description: "Style Loaded") { @@ -324,7 +335,12 @@ class NavigationViewControllerTests: TestCase { } func testPuck3DLayerPosition() { - let service = MapboxNavigationService(routeResponse: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, simulating: .never) + let service = MapboxNavigationService(routeResponse: initialRouteResponse, + routeIndex: 0, + routeOptions: routeOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, + simulating: .never) let options = NavigationOptions(styles: [TestableDayStyle()], navigationService: service) let navigationViewController = NavigationViewController(for: initialRouteResponse, routeIndex: 0, routeOptions: routeOptions, navigationOptions: options) diff --git a/Tests/MapboxNavigationTests/SpeechSynthesizersControllerTests.swift b/Tests/MapboxNavigationTests/SpeechSynthesizersControllerTests.swift index 90155e0f60e..e88ae6f4d5e 100644 --- a/Tests/MapboxNavigationTests/SpeechSynthesizersControllerTests.swift +++ b/Tests/MapboxNavigationTests/SpeechSynthesizersControllerTests.swift @@ -132,7 +132,12 @@ class SpeechSynthesizersControllerTests: TestCase { let expectation = XCTestExpectation(description: "Synthesizers speak should be called") let sut = SystemSpeechSynthMock() sut.speakExpectation = expectation - let dummyService = MapboxNavigationService(routeResponse: routeResponse, routeIndex: 0, routeOptions: routeOptions, simulating: .always) + let dummyService = MapboxNavigationService(routeResponse: routeResponse, + routeIndex: 0, + routeOptions: routeOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, + simulating: .always) let routeController: RouteVoiceController? = RouteVoiceController(navigationService: dummyService, speechSynthesizer: sut) XCTAssertNotNil(routeController) @@ -146,7 +151,12 @@ class SpeechSynthesizersControllerTests: TestCase { let expectation = XCTestExpectation(description: "Synthesizers speak should be called") let sut = MapboxSpeechSynthMock() sut.speakExpectation = expectation - let dummyService = MapboxNavigationService(routeResponse: routeResponse, routeIndex: 0, routeOptions: routeOptions, simulating: .always) + let dummyService = MapboxNavigationService(routeResponse: routeResponse, + routeIndex: 0, + routeOptions: routeOptions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, + simulating: .always) let routeController: RouteVoiceController? = RouteVoiceController(navigationService: dummyService, speechSynthesizer: sut) XCTAssertNotNil(routeController) diff --git a/Tests/MapboxNavigationTests/StepsViewControllerTests.swift b/Tests/MapboxNavigationTests/StepsViewControllerTests.swift index f6e9619a8cb..41ba895af98 100644 --- a/Tests/MapboxNavigationTests/StepsViewControllerTests.swift +++ b/Tests/MapboxNavigationTests/StepsViewControllerTests.swift @@ -16,7 +16,7 @@ class StepsViewControllerTests: TestCase { let bogusToken = "pk.feedCafeDeadBeefBadeBede" let dataSource = RouteControllerDataSourceFake() - let routeController = RouteController(alongRouteAtIndex: 0, in: response, options: Constants.options, dataSource: dataSource) + let routeController = RouteController(alongRouteAtIndex: 0, in: response, options: Constants.options, routingProvider: MapboxRoutingProvider(.offline), dataSource: dataSource) let stepsViewController = StepsViewController(routeProgress: routeController.routeProgress) From 6af46028d94c671be9e3ccb068631f32475e5f99 Mon Sep 17 00:00:00 2001 From: udumft Date: Fri, 26 Nov 2021 10:34:12 +0300 Subject: [PATCH 3/4] vk-1024-hybrid-router: podfile.lock updated --- Tests/CocoaPodsTest/PodInstall/Podfile.lock | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/CocoaPodsTest/PodInstall/Podfile.lock b/Tests/CocoaPodsTest/PodInstall/Podfile.lock index b7aa4b0a745..ca11eba2717 100644 --- a/Tests/CocoaPodsTest/PodInstall/Podfile.lock +++ b/Tests/CocoaPodsTest/PodInstall/Podfile.lock @@ -6,7 +6,6 @@ PODS: - MapboxDirections-pre (< 3.0.0, >= 2.1.0-rc.1) - MapboxMobileEvents (~> 1.0) - MapboxNavigationNative (~> 80.0) - - Turf (~> 2.1) - MapboxDirections-pre (2.1.0-rc.1): - Polyline (~> 5.0) - Turf (~> 2.0) @@ -55,7 +54,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: MapboxCommon: 8dd878a6c444b78bc1a819af1920dd1a0bb9f732 MapboxCoreMaps: 6ecb358c04600f6a947e52d41b78ac0c2570f930 - MapboxCoreNavigation: 192bb3b6e1d76a81fd6f644160f15c4577f163c8 + MapboxCoreNavigation: 3759f369f027b8ef1e6c1c0c576e7ea5d0a4e02d MapboxDirections-pre: 3a4102435de37f3b23022efa9afaaf9ef77307bf MapboxMaps: c19e24617b7135f87fac7dbad3a4baef657555ae MapboxMobileEvents: 14d7ac3ee95b4142c4fec2205dfd48ff453e8871 From 72e91467fc65eb64c17354c0ec5ef4af19d81cd4 Mon Sep 17 00:00:00 2001 From: udumft Date: Mon, 29 Nov 2021 13:04:16 +0300 Subject: [PATCH 4/4] vk-1024-hybrid-router: CHANGELOG updated. --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b728c019b64..d46c3d565db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,9 +66,6 @@ * Fixed an issue where `RouteStepProgress.currentIntersection` was always returning invalid value, which in turn caused inability to correctly detect whether specific location along the route is in tunnel, or not. ([#3559](https://github.com/mapbox/mapbox-navigation-ios/pull/3559)) * Renamed the `Locale.usesMetric` property to `Locale.measuresDistancesInMetricUnits`. `Locale.usesMetric` is still available but deprecated. ([#3547](https://github.com/mapbox/mapbox-navigation-ios/pull/3547)) * Fixed an issue where the user interface did not necessarily display distances in the same units as the route by default. `NavigationRouteOptions` and `NavigationMatchOptions` now set `DirectionsOptions.distanceMeasurementSystem` to a default value matching the `NavigationSettings.distanceUnit` property. ([#3541](https://github.com/mapbox/mapbox-navigation-ios/pull/3541)) - -### Other changes - * Introduced `RoutingProvider` to parameterize routing fetching and refreshing during active guidance sessions. `Directions.calculateWithCache(options:completionHandler:)` and `Directions.calculateOffline(options:completionHandler)` functionality is deprecated by `MapboxRoutingProvider`. It is now recommended to use `MapboxRoutingProvider` to request or refresh routes instead of `Directions` object but you may also provide your own `RoutingProvider` implementation to `NavigationService`, `RouteController` or `LegacyRouteController`. Using `directions` property of listed above entities is discouraged, you should use corresponding `routingProvider` instead, albeit `Directions` also implements the protocol. ([#3261](https://github.com/mapbox/mapbox-navigation-ios/pull/3261)) ## v2.0.1