From 06f790ba0ae4250ba455c36835d53f99b3b3b8df Mon Sep 17 00:00:00 2001 From: Victor Kononov Date: Mon, 29 Nov 2021 13:26:05 +0300 Subject: [PATCH] Hybrid Router (#3261) vk-1024-hybrid-router: added MapboxNavigationRouter to provide hybrid router functionality for requesting and refreshing routes; introduced NavigationProvider protocol to allow usage Directions or MapboxNavigationRouter for routing tasks. Corresponding conformances added; CHANGELOG updated; refactored handlers caching class to be more generic; unit tests updated --- CHANGELOG.md | 1 + Example/AppDelegate.swift | 2 +- Example/CustomViewController.swift | 2 + Example/ViewController.swift | 9 +- MapboxNavigation.xcodeproj/project.pbxproj | 24 +- .../CoreNavigationNavigator.swift | 7 - .../Directions+RoutingProvider.swift | 81 ++++ Sources/MapboxCoreNavigation/Directions.swift | 27 +- .../MapboxCoreNavigation/HandlerFactory.swift | 87 +++++ .../LegacyRouteController.swift | 37 +- .../MapboxRoutingProvider.swift | 346 ++++++++++++++++++ .../NativeHandlersFactory.swift | 17 +- .../NavigationService.swift | 85 ++++- .../RouteController.swift | 35 +- .../MapboxCoreNavigation/RouteProgress.swift | 19 +- Sources/MapboxCoreNavigation/Router.swift | 40 +- .../RoutingProvider.swift | 59 +++ .../TilesetDescriptorFactory.swift | 11 +- Sources/MapboxNavigation/CarPlayManager.swift | 39 +- Sources/MapboxNavigation/JunctionView.swift | 2 +- .../NavigationViewController.swift | 11 +- Tests/CocoaPodsTest/PodInstall/Podfile.lock | 3 +- .../BillingHandlerTests.swift | 5 +- .../MapboxCoreNavigationTests.swift | 35 +- .../NavigationEventsManagerTests.swift | 9 +- .../NavigationServiceTests.swift | 75 ++-- .../RouteControllerTests.swift | 32 +- .../TilesetDescriptorFactoryTests.swift | 6 + .../CarPlayManagerTests.swift | 64 ++-- ...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 +- 37 files changed, 1026 insertions(+), 210 deletions(-) create mode 100644 Sources/MapboxCoreNavigation/Directions+RoutingProvider.swift create mode 100644 Sources/MapboxCoreNavigation/HandlerFactory.swift create mode 100644 Sources/MapboxCoreNavigation/MapboxRoutingProvider.swift create mode 100644 Sources/MapboxCoreNavigation/RoutingProvider.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index bb18e584884..d46c3d565db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ * 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)) +* 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 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 a8e7f5da406..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?) { - NavigationSettings.shared.directions.calculateWithCache(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 f84d18ec9ae..418a434420a 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -34,6 +34,9 @@ 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 */; }; 2B3ED38C2609FA7900861A84 /* ArrivalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3ED38B2609FA7900861A84 /* ArrivalController.swift */; }; 2B3ED3962609FB2300861A84 /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3ED3952609FB2300861A84 /* CameraController.swift */; }; @@ -50,7 +53,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 */; }; @@ -64,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 */; }; @@ -538,6 +542,9 @@ 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 = ""; }; 2B3ED38B2609FA7900861A84 /* ArrivalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrivalController.swift; sourceTree = ""; }; 2B3ED3952609FB2300861A84 /* CameraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = ""; }; @@ -554,7 +561,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 = ""; }; @@ -568,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 = ""; }; @@ -1750,7 +1758,6 @@ C582FD5E203626E900A9086E /* CLLocationDirection.swift */, 2E82B9DB26E61F4600B7837F /* CongestionLevel.swift */, C5D9800E1EFBCDAD006DBF2E /* Date.swift */, - 2B955B1B25EFDDCB00BBFEC6 /* Directions.swift */, 3A163AE2249901D000D66A0D /* FixLocation.swift */, DAF27247264E028B00C0AC37 /* Geometry.swift */, C51DF8651F38C31C006C6A15 /* Locale.swift */, @@ -1810,11 +1817,14 @@ B4843886270F8E1600E161E6 /* SimulationType.swift */, 2BBED93A267A3AB900F90032 /* BillingHandler.swift */, C58D6BAC1DDCF2AE00387F53 /* CoreConstants.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 */, @@ -1833,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 */, @@ -2717,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 */, @@ -2724,17 +2737,20 @@ 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 */, - 2B955B1C25EFDDCB00BBFEC6 /* Directions.swift in Sources */, DAF27252264E02D800C0AC37 /* RoadObject.swift in Sources */, 5A39B9282498F9890026DFD1 /* PassiveLocationManager.swift in Sources */, 118D883526F8CA0700B2ED7B /* EndOfRouteFeedback.swift in Sources */, 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 */, diff --git a/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift b/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift index fb00e6878ed..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 { @@ -18,12 +17,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+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 index 2b431fd14c6..c3eff97acf7 100644 --- a/Sources/MapboxCoreNavigation/Directions.swift +++ b/Sources/MapboxCoreNavigation/Directions.swift @@ -44,30 +44,7 @@ extension Directions { - 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))) - } - } - } + 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 0799f5c0e9d..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 /** - The Directions object used to create the route. + A reference to a MapboxDirections service. Used for rerouting. */ - public var directions: Directions + @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: URLSessionDataTask? + var routeTask: NavigationProviderRequest? // MARK: Navigating @@ -131,11 +137,11 @@ 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 - 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 +201,25 @@ 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 + @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/MapboxRoutingProvider.swift b/Sources/MapboxCoreNavigation/MapboxRoutingProvider.swift new file mode 100644 index 00000000000..e73941ce1f1 --- /dev/null +++ b/Sources/MapboxCoreNavigation/MapboxRoutingProvider.swift @@ -0,0 +1,346 @@ +@_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`, `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 MapboxRoutingProvider: RoutingProvider { + + /** + Initializes new `MapboxRoutingProvider`. + + - parameter source: routing engine source to use. + - parameter settings: settings object, used to get credentials and cache configuration. + */ + public init(_ source: Source = .hybrid, settings: NavigationSettings = .shared) { + self.source = source + self.settings = settings + + 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 Source { + /** + Fetch data online only + + Such `MapboxRoutingProvider` is equivalent of using bare `Directions` wrapper. + */ + case online + /** + Use offline data only + + 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` routing provider 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 + } + } + } + + private let settings: NavigationSettings + + static var __testRoutesStub: ((_: RouteOptions, _: @escaping Directions.RouteCompletionHandler) -> Request?)? = nil + + // MARK: Performing and Parsing Requests + + /** + Unique identifier for a giver request. + + Valid only for the same instance of `MapboxRoutingProvider` that issued it. + */ + public typealias RequestId = UInt64 + + /** + A request handler for the ongoing routing action. + + You can use this instance to cancel ongoing task if needed. Retaining this handler will keep related `MapboxRoutingProvider` from deallocating. + */ + 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) + } + } + + /** + List of ongoing tasks for the routing provider. + + You can see if provider is busy with something, or use related `Request.cancel()` to cancel requests as needed. + */ + public private(set) var activeRequests: [RequestId : Request] = [:] + + private let requestsLock = NSLock() + private let router: MapboxNavigationNative_Private.RouterInterface + + private func complete(requestId: RequestId, with result: @escaping () -> Void) { + DispatchQueue.main.async { [self] in + result() + + requestsLock { + activeRequests[requestId] = nil + } + } + } + + struct ResponseDisposition: Decodable { + var code: String? + var message: String? + var error: String? + + private enum CodingKeys: CodingKey { + case code, message, error + } + } + + 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 { + 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)) + } + } + } + + private func doRequest(options: DirectionsOptions, + completion: @escaping (Result) -> Void) -> Request? { + let directionsUri = settings.directions.url(forCalculating: options).removingSKU().absoluteString + var requestId: RequestId! + + 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) + } + 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 ProfileIdentifier { + var nativeProfile: RoutingProfile { + 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 d946954b139..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,10 @@ class NativeHandlersFactory { }() lazy var cacheHandle: CacheHandle = { - CacheFactory.build(for: tilesConfig, - config: configHandle, - historyRecorder: historyRecorder) + cacheHandlerFactory.getHandler(with: (tilesConfig: tilesConfig, + configHandle: configHandle, + historyRecorder: historyRecorder), + cacheData: self) }() lazy var roadGraph: RoadGraph = { @@ -73,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 b05a383f2ff..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 /** - The Directions object used to create the route. + A reference to a MapboxDirections service. Used for rerouting. */ - public var directions: Directions + @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,8 +192,7 @@ open class RouteController: NSObject { var didFindFasterRoute = false - - var routeTask: URLSessionDataTask? + var routeTask: NavigationProviderRequest? // MARK: Navigating @@ -521,8 +526,21 @@ 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 + @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 @@ -539,6 +557,7 @@ open class RouteController: NSObject { deinit { BillingHandler.shared.stopBillingSession(with: sessionUUID) unsubscribeNotifications() + routeTask?.cancel() } private func subscribeNotifications() { @@ -679,11 +698,9 @@ 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.") } - 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..e7f5982d5f7 100644 --- a/Sources/MapboxCoreNavigation/RouteProgress.swift +++ b/Sources/MapboxCoreNavigation/RouteProgress.swift @@ -129,10 +129,14 @@ 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 + 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 92d4c7622c3..977322805e6 100644 --- a/Sources/MapboxCoreNavigation/Router.swift +++ b/Sources/MapboxCoreNavigation/Router.swift @@ -25,6 +25,17 @@ public struct IndexedRouteResponse { */ public let routeIndex: Int + /** + Returns a route from the `routeResponse` under given `routeIndex` if possible. + */ + public var currentRoute: Route? { + guard let routes = routeResponse.routes, + routes.count > routeIndex else { + return nil + } + return routeResponse.routes?[routeIndex] + } + /** Initializes a new `IndexedRouteResponse` object. @@ -66,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 directions: The Directions object that created `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, directions: Directions, 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. @@ -154,7 +169,7 @@ protocol InternalRouter: AnyObject { var lastRouteRefresh: Date? { get set } - var routeTask: URLSessionDataTask? { get set } + var routeTask: NavigationProviderRequest? { get set } var lastRerouteLocation: CLLocation? { get set } @@ -162,7 +177,7 @@ protocol InternalRouter: AnyObject { var isRefreshing: Bool { get set } - var directions: Directions { get } + var routingProvider: RoutingProvider { get } var routeProgress: RouteProgress { get } @@ -187,7 +202,7 @@ extension InternalRouter where Self: Router { } func refreshRoute(from location: CLLocation, legIndex: Int, completion: @escaping ()->()) { - guard refreshesRoute, let routeIdentifier = indexedRouteResponse.routeResponse.identifier else { + guard refreshesRoute else { completion() return } @@ -208,8 +223,8 @@ extension InternalRouter where Self: Router { return } isRefreshing = true - - directions.refreshRoute(responseIdentifier: routeIdentifier, routeIndex: indexedRouteResponse.routeIndex, fromLegAtIndex: legIndex) { [weak self] (session, result) in + routingProvider.refreshRoute(indexedRouteResponse: indexedRouteResponse, + fromLegAtIndex: UInt32(legIndex)) { [weak self] session, result in defer { self?.isRefreshing = false self?.lastRouteRefresh = nil @@ -219,8 +234,14 @@ extension InternalRouter where Self: Router { guard case let .success(response) = result, let self = self else { return } + self.indexedRouteResponse = .init(routeResponse: response, routeIndex: self.indexedRouteResponse.routeIndex) + + guard let currentRoute = self.indexedRouteResponse.currentRoute else { + assertionFailure("Refreshed `RouteResponse` did not contain required `routeIndex`!") + return + } - self.routeProgress.refreshRoute(with: response.route, at: location) + self.routeProgress.refreshRoute(with: currentRoute, at: location) var userInfo = [RouteController.NotificationUserInfoKey: Any]() userInfo[.routeProgressKey] = self.routeProgress @@ -312,7 +333,8 @@ extension InternalRouter where Self: Router { lastRerouteLocation = origin - routeTask = directions.calculateWithCache(options: options) {(session, result) in + routeTask = routingProvider.calculateRoutes(options: options) {(session, result) in + defer { self.routeTask = nil } switch result { case .failure(let error): return completion(session, .failure(error)) 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/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..22e5cc8ba0b 100644 --- a/Sources/MapboxNavigation/CarPlayManager.swift +++ b/Sources/MapboxNavigation/CarPlayManager.swift @@ -59,10 +59,15 @@ public class CarPlayManager: NSObject { public let eventsManager: NavigationEventsManager /** - The object that calculates routes when the user interacts with the CarPlay - interface. + The object that calculates routes when the user interacts with the CarPlay interface. */ - public let directions: Directions + @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 @@ -108,22 +113,38 @@ public class CarPlayManager: NSObject { - 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, - directions: directions, + 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, - directions: Directions? = nil, + routingProvider: RoutingProvider, eventsManager: NavigationEventsManager? = nil, carPlayNavigationViewControllerClass: CarPlayNavigationViewController.Type? = nil) { self.styles = styles ?? [DayStyle(), NightStyle()] - let mapboxDirections = directions ?? NavigationSettings.shared.directions - self.directions = mapboxDirections + self.routingProvider = routingProvider self.eventsManager = eventsManager ?? .init(activeNavigationDataSource: nil, accessToken: NavigationSettings.shared.directions.credentials.accessToken) self.mapTemplateProvider = MapTemplateProvider() @@ -527,7 +548,7 @@ extension CarPlayManager { } func calculate(_ options: RouteOptions, completionHandler: @escaping Directions.RouteCompletionHandler) { - directions.calculateWithCache(options: options, completionHandler: completionHandler) + routingProvider.calculateRoutes(options: options, completionHandler: completionHandler) } func didCalculate(_ result: Result, @@ -621,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/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..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, 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/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 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 4951d430fe7..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, - directions: directions, + 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, - directions: directions, + 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, - directions: directions, + 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, - directions: directions, + 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, - directions: directions, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: locationManager, simulating: .never) expectation(forNotification: .routeControllerWillReroute, object: navigation.router) { (notification) -> Bool in @@ -285,9 +290,11 @@ class MapboxCoreNavigationTests: TestCase { navigation = MapboxNavigationService(routeResponse: routeResponse, routeIndex: 0, routeOptions: navOptions, - directions: .mocked, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, 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 +334,16 @@ 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, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: locationManager) + navigation.router.refreshesRoute = false struct InstructionPoint { enum InstructionType { @@ -352,6 +360,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 +380,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 +459,11 @@ class MapboxCoreNavigationTests: TestCase { } func testFailToReroute() { - let directionsClientSpy = DirectionsSpy() navigation = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: routeOptions, - directions: directionsClientSpy, + routingProvider: MapboxRoutingProvider(.online), + credentials: Fixture.credentials, simulating: .never) expectation(forNotification: .routeControllerWillReroute, object: navigation.router) { (notification) -> Bool in @@ -461,7 +475,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/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 d3c5edaccd4..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, - directions: directionsClientSpy, + routingProvider: MapboxRoutingProvider(.offline), + credentials: Fixture.credentials, locationSource: locationSource, eventsManagerType: NavigationEventsManagerSpy.self, simulating: .never) @@ -64,6 +65,7 @@ class NavigationServiceTests: TestCase { override func tearDown() { super.tearDown() dependencies = nil + MapboxRoutingProvider.__testRoutesStub = nil } func testDefaultUserInterfaceUsage() { @@ -409,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)) } @@ -418,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 @@ -471,6 +475,18 @@ class NavigationServiceTests: TestCase { return true } + + // MARK: Setupping a re-route stub + 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 nil + } dependencies.navigationService.start() @@ -483,8 +499,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 +525,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 +558,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 @@ -596,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 } @@ -608,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 } @@ -616,8 +633,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, routingProvider: MapboxRoutingProvider(.offline), credentials: Fixture.credentials) XCTAssert(subject.poorGPSTimer.countdownInterval == .milliseconds(2500), "Default countdown interval should be 2500 milliseconds.") @@ -642,8 +658,11 @@ 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) { + routeController.updateRoute(with: .init(routeResponse: routeResponse, routeIndex: 0), + routeOptions: routeOptions) { success in XCTAssertTrue(success) routeUpdated.fulfill() @@ -669,7 +688,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 +707,13 @@ 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, + routingProvider: MapboxRoutingProvider(.online), + credentials: Fixture.credentials, locationSource: locationManager) service.delegate = delegate let router = service.router @@ -704,6 +724,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) + MapboxRoutingProvider.__testRoutesStub = { (options, completionHandler) in + completionHandler(Directions.Session(options, Fixture.credentials), + .success(fasterResponse)) + return nil + } + let rerouteTriggeredExpectation = expectation(description: "Proactive reroute triggered") locationManager.onTick = { [unowned locationManager] _, _ in if (router as! RouterComposition).lastRerouteLocation != nil { @@ -714,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 8981effbdd8..8a223bc5734 100644 --- a/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift +++ b/Tests/MapboxCoreNavigationTests/RouteControllerTests.swift @@ -15,6 +15,7 @@ class RouteControllerTests: TestCase { override func tearDown() { replayManager = nil + MapboxRoutingProvider.__testRoutesStub = nil super.tearDown() } @@ -28,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 @@ -70,13 +72,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, + routingProvider: MapboxRoutingProvider(.offline), dataSource: self) let routerDelegateSpy = RouterDelegateSpy() @@ -112,25 +112,13 @@ class RouteControllerTests: TestCase { didRerouteCalled.fulfill() } - directions.onCalculateRoute = { [unowned directions] 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 - ] - - let routes = [ - Fixture.route(between: currentCoordinate, and: destination).route - ] - - directions.fireLastCalculateCompletion(with: waypoints, - routes: routes, - error: nil) + return nil } 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..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 @@ -201,33 +203,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 {} @@ -236,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.") } } @@ -260,8 +235,7 @@ class CarPlayManagerSpec: QuickSpec { Navigator._recreateNavigator() CarPlayMapViewController.swizzleMethods() - let directionsSpy = DirectionsSpy() - manager = CarPlayManager(styles: nil, directions: directionsSpy, eventsManager: nil) + manager = CarPlayManager(styles: nil, routingProvider: MapboxRoutingProvider(.offline), eventsManager: nil) delegate = TestCarPlayManagerDelegate() manager!.delegate = delegate @@ -279,6 +253,10 @@ class CarPlayManagerSpec: QuickSpec { beforeEach { manager!.mapTemplateProvider = MapTemplateSpyProvider() } + + afterEach { + MapboxRoutingProvider.__testRoutesStub = nil + } let previewRoutesAction = { let options = NavigationRouteOptions(coordinates: [ @@ -288,10 +266,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) + MapboxRoutingProvider.__testRoutesStub = { (options, completionHandler) in + completionHandler(Directions.Session(options, Fixture.credentials), + .success(fasterResponse)) + return nil + } + manager!.previewRoutes(for: options, completionHandler: {}) - directionsSpy.fireLastCalculateCompletion(with: waypoints, routes: [route], error: nil) } context("when the trip is not customized by the developer") { @@ -428,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)