diff --git a/Cartfile b/Cartfile index 315c682381..ce3bd045f5 100644 --- a/Cartfile +++ b/Cartfile @@ -1,6 +1,6 @@ binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" ~> 5.6 binary "https://www.mapbox.com/ios-sdk/MapboxNavigationNative.json" ~> 6.2.1 -github "mapbox/mapbox-directions-swift" == 1.0.0-alpha.1 +github "mapbox/mapbox-directions-swift" "c91d3ba051fa67759b905299dad85a163697efcf" github "mapbox/turf-swift" ~> 0.3 github "mapbox/mapbox-events-ios" ~> 0.10 github "ceeK/Solar" ~> 2.1.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index 657360f246..b8a0bee4e6 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,12 +1,12 @@ -binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" "5.7.0" +binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" "5.8.0" binary "https://www.mapbox.com/ios-sdk/MapboxNavigationNative.json" "6.2.1" github "AndriiDoroshko/SnappyShrimp" "1.6.4" github "CedarBDD/Cedar" "v1.0" -github "Quick/Nimble" "v8.0.5" +github "Quick/Nimble" "v8.0.7" github "Quick/Quick" "v2.2.0" github "ceeK/Solar" "2.1.0" github "mapbox/MapboxGeocoder.swift" "v0.10.2" -github "mapbox/mapbox-directions-swift" "v1.0.0-alpha.1" +github "mapbox/mapbox-directions-swift" "c91d3ba051fa67759b905299dad85a163697efcf" github "mapbox/mapbox-events-ios" "v0.10.2" github "mapbox/mapbox-speech-swift" "v0.3.0" github "mapbox/turf-swift" "v0.3.0" diff --git a/Example/AppDelegate+CarPlay.swift b/Example/AppDelegate+CarPlay.swift index c6e2b35122..3ad0e8aa4c 100644 --- a/Example/AppDelegate+CarPlay.swift +++ b/Example/AppDelegate+CarPlay.swift @@ -44,12 +44,13 @@ extension AppDelegate: CPApplicationDelegate { @available(iOS 12.0, *) extension AppDelegate: CarPlayManagerDelegate { - func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, desiredSimulationMode: SimulationMode) -> NavigationService { + func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, routeOptions: RouteOptions, desiredSimulationMode: SimulationMode) -> NavigationService { + if let nvc = self.window?.rootViewController?.presentedViewController as? NavigationViewController, let service = nvc.navigationService { //Do not set simulation mode if we already have an active navigation session. return service } - return MapboxNavigationService(route: route, simulating: desiredSimulationMode) + return MapboxNavigationService(route: route, routeOptions: routeOptions, simulating: desiredSimulationMode) } // MARK: CarPlayManagerDelegate diff --git a/Example/CustomViewController.swift b/Example/CustomViewController.swift index 5bc8e9f3f6..22bc9a674e 100644 --- a/Example/CustomViewController.swift +++ b/Example/CustomViewController.swift @@ -14,6 +14,8 @@ class CustomViewController: UIViewController, MGLMapViewDelegate { var simulateLocation = false var userRoute: Route? + + var userRouteOptions: RouteOptions? // Start voice instructions var voiceController: MapboxVoiceController! @@ -38,7 +40,7 @@ class CustomViewController: UIViewController, MGLMapViewDelegate { super.viewDidLoad() let locationManager = simulateLocation ? SimulatedLocationManager(route: userRoute!) : NavigationLocationManager() - navigationService = MapboxNavigationService(route: userRoute!, locationSource: locationManager, simulating: simulateLocation ? .always : .onPoorGPS) + navigationService = MapboxNavigationService(route: userRoute!, routeOptions: userRouteOptions!, locationSource: locationManager, simulating: simulateLocation ? .always : .onPoorGPS) voiceController = MapboxVoiceController(navigationService: navigationService) mapView.delegate = self diff --git a/Example/ViewController+GuidanceCards.swift b/Example/ViewController+GuidanceCards.swift index baaff866d3..96246405a5 100644 --- a/Example/ViewController+GuidanceCards.swift +++ b/Example/ViewController+GuidanceCards.swift @@ -5,7 +5,7 @@ import MapboxDirections /// :nodoc: extension ViewController: InstructionsCardCollectionDelegate { public func instructionsCardCollection(_ instructionsCardCollection: InstructionsCardViewController, didPreview step: RouteStep) { - guard let route = routes?.first else { return } + guard let route = response?.routes?.first else { return } // find the leg that contains the step, legIndex, and stepIndex guard let leg = route.legs.first(where: { $0.steps.contains(step) }), diff --git a/Example/ViewController.swift b/Example/ViewController.swift index 70d3b12505..3c11f6ce85 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -5,7 +5,7 @@ import MapboxDirections import UserNotifications import AVKit -private typealias RouteRequestSuccess = (([Route]) -> Void) +private typealias RouteRequestSuccess = ((RouteResponse) -> Void) private typealias RouteRequestFailure = ((Error) -> Void) class ViewController: UIViewController { @@ -35,14 +35,15 @@ class ViewController: UIViewController { } } - var routes: [Route]? { + var response: RouteResponse? { didSet { - startButton.isEnabled = (routes?.count ?? 0 > 0) - guard let routes = routes, let current = routes.first else { + guard let routes = response?.routes, let current = routes.first else { + startButton.isEnabled = false mapView?.removeRoutes() return + } - + startButton.isEnabled = true mapView?.show(routes) mapView?.showWaypoints(on: current) } @@ -52,17 +53,17 @@ class ViewController: UIViewController { // MARK: Directions Request Handlers - fileprivate lazy var defaultSuccess: RouteRequestSuccess = { [weak self] (routes) in - guard let current = routes.first else { return } + fileprivate lazy var defaultSuccess: RouteRequestSuccess = { [weak self] (response) in + guard let routes = response.routes, !routes.isEmpty, case let .route(options) = response.options else { return } self?.mapView?.removeWaypoints() - self?.routes = routes - self?.waypoints = current.routeOptions.waypoints + self?.response = response + self?.waypoints = options.waypoints self?.clearMap.isHidden = false self?.longPressHintView.isHidden = true } fileprivate lazy var defaultFailure: RouteRequestFailure = { [weak self] (error) in - self?.routes = nil //clear routes from the map + self?.response = nil //clear routes from the map print(error.localizedDescription) self?.presentAlert(message: error.localizedDescription) } @@ -207,62 +208,63 @@ class ViewController: UIViewController { fileprivate func requestRoute(with options: RouteOptions, success: @escaping RouteRequestSuccess, failure: RouteRequestFailure?) { // Calculate route offline if an offline version is selected let shouldUseOfflineRouting = Settings.selectedOfflineVersion != nil - Settings.directions.calculate(options, offline: shouldUseOfflineRouting) { (waypoints, routes, error) in - if let error = error { failure?(error) } - guard let routes = routes else { return } - return success(routes) + Settings.directions.calculate(options, offline: shouldUseOfflineRouting) { (session, result) in + switch result { + case let .success(response): + success(response) + case let .failure(error): + failure?(error) + } } } // MARK: Basic Navigation func startBasicNavigation() { - guard let route = routes?.first else { return } + guard let response = response, let route = response.routes?.first, case let .route(routeOptions) = response.options else { return } - let service = navigationService(route: route) + let service = navigationService(route: route, options: routeOptions) let navigationViewController = self.navigationViewController(navigationService: service) presentAndRemoveMapview(navigationViewController, completion: beginCarPlayNavigation) } func startNavigation(styles: [Style]) { - guard let route = routes?.first else { return } + guard let response = response, let route = response.routes?.first, case let .route(routeOptions) = response.options else { return } - let options = NavigationOptions(styles: styles, navigationService: navigationService(route: route)) - let navigationViewController = NavigationViewController(for: route, options: options) + let options = NavigationOptions(styles: styles, navigationService: navigationService(route: route, options: routeOptions)) + let navigationViewController = NavigationViewController(for: route, routeOptions: routeOptions, navigationOptions: options) navigationViewController.delegate = self presentAndRemoveMapview(navigationViewController, completion: beginCarPlayNavigation) } func navigationViewController(navigationService: NavigationService) -> NavigationViewController { - let route = navigationService.route - let options = NavigationOptions( navigationService: navigationService) + let options = NavigationOptions(navigationService: navigationService) - let navigationViewController = NavigationViewController(for: route, options: options) + let navigationViewController = NavigationViewController(for: navigationService.route, routeOptions: navigationService.routeProgress.routeOptions, navigationOptions: options) navigationViewController.delegate = self navigationViewController.mapView?.delegate = self return navigationViewController } public func beginNavigationWithCarplay(navigationService: NavigationService) { - self.routes = [navigationService.route] - let navigationViewController = activeNavigationViewController ?? self.navigationViewController(navigationService: navigationService) navigationViewController.didConnectToCarPlay() - + guard activeNavigationViewController == nil else { return } - + presentAndRemoveMapview(navigationViewController, completion: nil) } // MARK: Custom Navigation UI func startCustomNavigation() { - guard let route = routes?.first else { return } + guard let route = response?.routes?.first, let responseOptions = response?.options, case let .route(routeOptions) = responseOptions else { return } guard let customViewController = storyboard?.instantiateViewController(withIdentifier: "custom") as? CustomViewController else { return } customViewController.userRoute = route + customViewController.userRouteOptions = routeOptions let destination = MGLPointAnnotation() destination.coordinate = route.shape!.coordinates.last! @@ -275,11 +277,11 @@ class ViewController: UIViewController { // MARK: Styling the default UI func startStyledNavigation() { - guard let route = routes?.first else { return } + guard let response = response, let route = response.routes?.first, case let .route(routeOptions) = response.options else { return } let styles = [CustomDayStyle(), CustomNightStyle()] - let options = NavigationOptions(styles:styles, navigationService: navigationService(route: route)) - let navigationViewController = NavigationViewController(for: route, options: options) + let options = NavigationOptions(styles:styles, navigationService: navigationService(route: route, options: routeOptions)) + let navigationViewController = NavigationViewController(for: route, routeOptions: routeOptions, navigationOptions: options) navigationViewController.delegate = self presentAndRemoveMapview(navigationViewController, completion: beginCarPlayNavigation) @@ -287,22 +289,22 @@ class ViewController: UIViewController { // MARK: Guidance Cards func startGuidanceCardsNavigation() { - guard let route = routes?.first else { return } + guard let response = response, let route = response.routes?.first, case let .route(routeOptions) = response.options else { return } let instructionsCardCollection = InstructionsCardViewController() instructionsCardCollection.cardCollectionDelegate = self - let options = NavigationOptions(navigationService: navigationService(route: route), topBanner: instructionsCardCollection) - let navigationViewController = NavigationViewController(for: route, options: options) + let options = NavigationOptions(navigationService: navigationService(route: route, options: routeOptions), topBanner: instructionsCardCollection) + let navigationViewController = NavigationViewController(for: route, routeOptions: routeOptions, navigationOptions: options) navigationViewController.delegate = self presentAndRemoveMapview(navigationViewController, completion: beginCarPlayNavigation) } - func navigationService(route: Route) -> NavigationService { + func navigationService(route: Route, options: RouteOptions) -> NavigationService { let simulate = simulationButton.isSelected let mode: SimulationMode = simulate ? .always : .onPoorGPS - return MapboxNavigationService(route: route, directions: Settings.directions, simulating: mode) + return MapboxNavigationService(route: route, routeOptions: options, directions: Settings.directions, simulating: mode) } func presentAndRemoveMapview(_ navigationViewController: NavigationViewController, completion: CompletionHandler?) { @@ -361,7 +363,7 @@ extension ViewController: MGLMapViewDelegate { self.mapView?.localizeLabels() - if let routes = routes, let currentRoute = routes.first, let coords = currentRoute.shape?.coordinates { + if let routes = response?.routes, let currentRoute = routes.first, let coords = currentRoute.shape?.coordinates { mapView.setVisibleCoordinateBounds(MGLPolygon(coordinates: coords, count: UInt(coords.count)).overlayBounds, animated: false) self.mapView?.show(routes) self.mapView?.showWaypoints(on: currentRoute) @@ -372,7 +374,7 @@ extension ViewController: MGLMapViewDelegate { // MARK: - NavigationMapViewDelegate extension ViewController: NavigationMapViewDelegate { func navigationMapView(_ mapView: NavigationMapView, didSelect waypoint: Waypoint) { - guard let routeOptions = routes?.first?.routeOptions else { return } + guard let responseOptions = response?.options, case let .route(routeOptions) = responseOptions else { return } let modifiedOptions = routeOptions.without(waypoint: waypoint) presentWaypointRemovalActionSheet { _ in @@ -381,10 +383,10 @@ extension ViewController: NavigationMapViewDelegate { } func navigationMapView(_ mapView: NavigationMapView, didSelect route: Route) { - guard let routes = routes else { return } + guard let routes = response?.routes else { return } guard let index = routes.firstIndex(where: { $0 === route }) else { return } - self.routes!.remove(at: index) - self.routes!.insert(route, at: 0) + self.response!.routes!.remove(at: index) + self.response!.routes!.insert(route, at: 0) } private func presentWaypointRemovalActionSheet(completionHandler approve: @escaping ((UIAlertAction) -> Void)) { @@ -435,22 +437,22 @@ extension ViewController: VoiceControllerDelegate { func navigationViewController(_ navigationViewController: NavigationViewController, shouldRerouteFrom location: CLLocation) -> Bool { let shouldUseOfflineRouting = Settings.selectedOfflineVersion != nil - guard shouldUseOfflineRouting == true else { + guard shouldUseOfflineRouting == true, let responseOptions = response?.options, case let .route(routeOptions) = responseOptions else { return true } - let currentRoute = navigationViewController.route - let profileIdentifier = currentRoute.routeOptions.profileIdentifier + let profileIdentifier = routeOptions.profileIdentifier var waypoints: [Waypoint] = [Waypoint(location: location)] - var remainingWaypoints = currentRoute.routeOptions.waypoints + var remainingWaypoints = navigationViewController.navigationService.routeProgress.remainingWaypoints remainingWaypoints.removeFirst() waypoints.append(contentsOf: remainingWaypoints) let options = NavigationRouteOptions(waypoints: waypoints, profileIdentifier: profileIdentifier) - Settings.directions.calculate(options, offline: true) { (waypoints, routes, error) in - guard let route = routes?.first else { return } + Settings.directions.calculate(options, offline: true) { (session, result) in + guard case let .success(response) = result, let routes = response.routes, let route = routes.first else { return } + navigationViewController.navigationService.route = route } diff --git a/MapboxCoreNavigation/EventDetails.swift b/MapboxCoreNavigation/EventDetails.swift index 2f6e9ede45..6ac473eeec 100644 --- a/MapboxCoreNavigation/EventDetails.swift +++ b/MapboxCoreNavigation/EventDetails.swift @@ -112,7 +112,7 @@ struct NavigationEventDetails: EventDetails { coordinate = dataSource.router.rawLocation?.coordinate startTimestamp = session.departureTimestamp ?? nil sdkIdentifier = defaultInterface ? "mapbox-navigation-ui-ios" : "mapbox-navigation-ios" - profile = dataSource.routeProgress.route.routeOptions.profileIdentifier.rawValue + profile = dataSource.routeProgress.routeOptions.profileIdentifier.rawValue simulation = dataSource.locationProvider is SimulatedLocationManager.Type sessionIdentifier = session.identifier.uuidString diff --git a/MapboxCoreNavigation/LegacyRouteController.swift b/MapboxCoreNavigation/LegacyRouteController.swift index 476c2b0909..590ed08769 100644 --- a/MapboxCoreNavigation/LegacyRouteController.swift +++ b/MapboxCoreNavigation/LegacyRouteController.swift @@ -11,7 +11,7 @@ protocol RouteControllerDataSource: class { } @available(*, deprecated, renamed: "RouteController") -open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationManagerDelegate { +open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationManagerDelegate { public weak var delegate: RouterDelegate? @@ -58,7 +58,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa return routeProgress.route } set { - routeProgress = RouteProgress(route: newValue) + routeProgress = RouteProgress(route: newValue, options: routeProgress.routeOptions) } } @@ -78,9 +78,9 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa var userSnapToStepDistanceFromManeuver: CLLocationDistance? - required public init(along route: Route, directions: Directions = Directions.shared, dataSource source: RouterDataSource) { + required public init(along route: Route, options: RouteOptions, directions: Directions = Directions.shared, dataSource source: RouterDataSource) { self.directions = directions - self._routeProgress = RouteProgress(route: route) + self._routeProgress = RouteProgress(route: route, options: options) self.dataSource = source UIDevice.current.isBatteryMonitoringEnabled = true @@ -359,24 +359,28 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa self.lastRerouteLocation = location - getDirections(from: location, along: progress) { [weak self] (route, error) in + getDirections(from: location, along: progress) { [weak self] (session, result) in guard let strongSelf = self else { return } - - strongSelf.isRerouting = false - if let error = error { + + strongSelf.isRerouting = false + switch result { + case let .failure(error): strongSelf.delegate?.router(strongSelf, didFailToRerouteWith: error) - NotificationCenter.default.post(name: .routeControllerDidFailToReroute, object: self, userInfo: [ - RouteController.NotificationUserInfoKey.routingErrorKey: error, - ]) - return + NotificationCenter.default.post(name: .routeControllerDidFailToReroute, object: self, userInfo: [ + RouteController.NotificationUserInfoKey.routingErrorKey: error, + ]) + return + case let .success(response): + guard case let .route(options) = response.options, let route = response.routes?.first else { + return + } + strongSelf.route = route + strongSelf._routeProgress = RouteProgress(route: route, options: options, legIndex: 0) + strongSelf._routeProgress.currentLegProgress.stepIndex = 0 + strongSelf.announce(reroute: route, at: location, proactive: false) } - - guard let route = route else { return } - strongSelf._routeProgress = RouteProgress(route: route, legIndex: 0) - strongSelf._routeProgress.currentLegProgress.stepIndex = 0 - strongSelf.announce(reroute: route, at: location, proactive: false) } } diff --git a/MapboxCoreNavigation/NavigationRouteOptions.swift b/MapboxCoreNavigation/NavigationRouteOptions.swift index 05bca77d20..beba1944c5 100644 --- a/MapboxCoreNavigation/NavigationRouteOptions.swift +++ b/MapboxCoreNavigation/NavigationRouteOptions.swift @@ -7,7 +7,7 @@ import MapboxDirections `NavigationRouteOptions` is a subclass of `RouteOptions` that has been optimized for navigation. Pass an instance of this class into the `Directions.calculate(_:completionHandler:)` method. - note: `NavigationRouteOptions` is designed to be used with the `Directions` and `NavigationDirections` classes for specifying routing criteria. To customize the user experience in a `NavigationViewController`, use the `NavigationOptions` class. */ -open class NavigationRouteOptions: RouteOptions { +open class NavigationRouteOptions: RouteOptions, OptimizedForNavigation { /** Initializes a navigation route options object for routes between the given waypoints and an optional profile identifier optimized for navigation. @@ -19,21 +19,24 @@ open class NavigationRouteOptions: RouteOptions { return $0 }, profileIdentifier: profileIdentifier) includesAlternativeRoutes = true - shapeFormat = .polyline6 - includesSteps = true - routeShapeResolution = .full if profileIdentifier == .walking { attributeOptions = [.congestionLevel, .expectedTravelTime] } else { attributeOptions = [.congestionLevel, .expectedTravelTime, .maximumSpeedLimit] } - includesSpokenInstructions = true - locale = Locale.nationalizedCurrent - distanceMeasurementSystem = Locale.current.usesMetricSystem ? .metric : .imperial - includesVisualInstructions = true includesExitRoundaboutManeuver = true + + optimizeForNavigation() } + /** + Initializes an equivalent `RouteOptions` object from a `NavigationMapOptions` + + - seealso: `NavigationMatchOptions` + */ + public convenience init(navigationMatchOptions options: NavigationMatchOptions) { + self.init(waypoints: options.waypoints, profileIdentifier: options.profileIdentifier) + } /** Initializes a navigation route options object for routes between the given locations and an optional profile identifier optimized for navigation. @@ -64,30 +67,26 @@ open class NavigationRouteOptions: RouteOptions { Note: it is very important you specify the `waypoints` for the route. Usually the only two values for this `IndexSet` will be 0 and the length of the coordinates. Otherwise, all coordinates passed through will be considered waypoints. */ -open class NavigationMatchOptions: MatchOptions { +open class NavigationMatchOptions: MatchOptions, OptimizedForNavigation { /** Initializes a navigation route options object for routes between the given waypoints and an optional profile identifier optimized for navigation. - - seealso: `MatchOptions` + - seealso: `RouteOptions` */ public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = .automobileAvoidingTraffic) { super.init(waypoints: waypoints.map { $0.coordinateAccuracy = -1 return $0 }, profileIdentifier: profileIdentifier) - includesSteps = true - routeShapeResolution = .full - shapeFormat = .polyline6 attributeOptions = [.congestionLevel, .expectedTravelTime] if profileIdentifier == .automobile || profileIdentifier == .automobileAvoidingTraffic { attributeOptions.insert(.maximumSpeedLimit) } - includesSpokenInstructions = true - locale = Locale.nationalizedCurrent - distanceMeasurementSystem = Locale.current.usesMetricSystem ? .metric : .imperial - includesVisualInstructions = true + + optimizeForNavigation() } + /** Initializes a navigation match options object for routes between the given locations and an optional profile identifier optimized for navigation. @@ -110,3 +109,28 @@ open class NavigationMatchOptions: MatchOptions { try super.init(from: decoder) } } + +protocol OptimizedForNavigation: class { + var includesSteps: Bool { get set } + var routeShapeResolution: RouteShapeResolution { get set } + var shapeFormat: RouteShapeFormat { get set } + var attributeOptions: AttributeOptions { get set } + var locale: Locale { get set } + var distanceMeasurementSystem: MeasurementSystem { get set } + var includesSpokenInstructions: Bool { get set } + var includesVisualInstructions: Bool { get set } + + func optimizeForNavigation() +} + +extension OptimizedForNavigation { + func optimizeForNavigation() { + shapeFormat = .polyline6 + includesSteps = true + routeShapeResolution = .full + includesSpokenInstructions = true + locale = Locale.nationalizedCurrent + distanceMeasurementSystem = Locale.current.usesMetricSystem ? .metric : .imperial + includesVisualInstructions = true + } +} diff --git a/MapboxCoreNavigation/NavigationService.swift b/MapboxCoreNavigation/NavigationService.swift index 6a1e56dc74..1c979c1649 100644 --- a/MapboxCoreNavigation/NavigationService.swift +++ b/MapboxCoreNavigation/NavigationService.swift @@ -206,8 +206,8 @@ public class MapboxNavigationService: NSObject, NavigationService { - parameter route: The route to follow. */ - convenience init(route: Route) { - self.init(route: route, directions: nil, locationSource: nil, eventsManagerType: nil) + convenience init(route: Route, routeOptions options: RouteOptions) { + self.init(route: route, routeOptions: options, directions: nil, locationSource: nil, eventsManagerType: nil) } /** Intializes a new `NavigationService`. @@ -220,6 +220,7 @@ public class MapboxNavigationService: NSObject, NavigationService { - parameter routerType: An optional router type to use for traversing the route. */ required public init(route: Route, + routeOptions: RouteOptions, directions: Directions? = nil, locationSource: NavigationLocationManager? = nil, eventsManagerType: NavigationEventsManager.Type? = nil, @@ -237,11 +238,12 @@ public class MapboxNavigationService: NSObject, NavigationService { } let routerType = routerType ?? DefaultRouter.self - router = routerType.init(along: route, directions: self.directions, dataSource: self) + router = routerType.init(along: route, options: routeOptions, directions: self.directions, dataSource: self) + NavigationSettings.shared.distanceUnit = routeOptions.locale.usesMetric ? .kilometer : .mile let eventType = eventsManagerType ?? NavigationEventsManager.self - eventsManager = eventType.init(dataSource: self, accessToken: route.accessToken) - locationManager.activityType = route.routeOptions.activityType + eventsManager = eventType.init(dataSource: self, accessToken: self.directions.credentials.accessToken) + locationManager.activityType = routeOptions.activityType bootstrapEvents() router.delegate = self diff --git a/MapboxCoreNavigation/OfflineDirections.swift b/MapboxCoreNavigation/OfflineDirections.swift index 73d76b6231..3f8e3f1741 100644 --- a/MapboxCoreNavigation/OfflineDirections.swift +++ b/MapboxCoreNavigation/OfflineDirections.swift @@ -100,7 +100,7 @@ public typealias UnpackCompletionHandler = (_ numberOfTiles: UInt64, _ error: Er If the request was canceled or there was an error obtaining the routes, this argument is `nil`. This is not to be confused with the situation in which no results were found, in which case the array is present but empty. - parameter error: The error that occurred, or `nil` if the placemarks were obtained successfully. */ -public typealias OfflineRouteCompletionHandler = ([MapboxDirections.Waypoint]?, [MapboxDirections.Route]?, OfflineRoutingError?) -> Void +public typealias OfflineRouteCompletionHandler = (_ session: Directions.Session, _ result: Result) -> Void /** A `NavigationDirections` object provides you with optimal directions between different locations, or waypoints. The directions object passes your request to a built-in routing engine and returns the requested information to a closure (block) that you provide. A directions object can handle multiple simultaneous requests. A `RouteOptions` object specifies criteria for the results, such as intermediate waypoints, a mode of transportation, or the level of detail to be returned. In addition to `Directions`, `NavigationDirections` provides support for offline routing. @@ -108,10 +108,6 @@ public typealias OfflineRouteCompletionHandler = ([MapboxDirections.Waypoint]?, Each result produced by the directions object is stored in a `Route` object. Depending on the `RouteOptions` object you provide, each route may include detailed information suitable for turn-by-turn directions, or it may include only high-level information such as the distance, estimated travel time, and name of each leg of the trip. The waypoints that form the request may be conflated with nearby locations, as appropriate; the resulting waypoints are provided to the closure. */ public class NavigationDirections: Directions { - public override init(accessToken: String? = nil, host: String? = nil) { - super.init(accessToken: accessToken, host: host) - } - /** Configures the router with the given set of tiles. @@ -183,31 +179,32 @@ public class NavigationDirections: Directions { public func calculate(_ options: RouteOptions, offline: Bool = true, completionHandler: @escaping OfflineRouteCompletionHandler) { guard offline else { - super.calculate(options) { (waypoints, routes, error) in - let offlineError: OfflineRoutingError? - if let error = error { - offlineError = .standard(error) - } else { - offlineError = nil + super.calculate(options) { (session, result) in + + switch result { + case let .failure(directionsError): + completionHandler(session, .failure(.standard(directionsError))) + case let .success(response): + completionHandler(session, .success(response)) } - completionHandler(waypoints, routes, offlineError) } return } let url = self.url(forCalculating: options) + let session: Directions.Session = (options: options, credentials: self.credentials) NavigationDirectionsConstants.offlineSerialQueue.async { [weak self] in guard let result = self?.navigator.getRouteForDirectionsUri(url.absoluteString) else { DispatchQueue.main.async { - completionHandler(nil, nil, .noData) + completionHandler(session, .failure(.noData)) } return } guard let data = result.json.data(using: .utf8) else { DispatchQueue.main.async { - completionHandler(nil, nil, .invalidResponse) + completionHandler(session, .failure(.invalidResponse)) } return } @@ -216,14 +213,15 @@ public class NavigationDirections: Directions { do { let decoder = JSONDecoder() decoder.userInfo[.options] = options + decoder.userInfo[.credentials] = session.credentials let response = try decoder.decode(RouteResponse.self, from: data) - guard let routes = response.routes else { - return completionHandler(response.waypoints, nil, .standard(.unableToRoute)) + guard let routes = response.routes, !routes.isEmpty else { + return completionHandler(session, .failure(.standard(.unableToRoute))) } - return completionHandler(response.waypoints, routes, nil) + return completionHandler(session, .success(response)) } catch { - return completionHandler(nil, nil, .unknown(underlying: error)) + return completionHandler(session, .failure(.unknown(underlying: error))) } } diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index d84dcb6fc5..6573221d90 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -28,7 +28,7 @@ open class RouteController: NSObject { return routeProgress.route } set { - routeProgress = RouteProgress(route: newValue) + routeProgress = RouteProgress(route: newValue, options: routeProgress.routeOptions) updateNavigator(with: routeProgress) } } @@ -137,9 +137,9 @@ open class RouteController: NSObject { return snappedLocation ?? rawLocation } - required public init(along route: Route, directions: Directions = Directions.shared, dataSource source: RouterDataSource) { + required public init(along route: Route, options: RouteOptions, directions: Directions = Directions.shared, dataSource source: RouterDataSource) { self.directions = directions - self._routeProgress = RouteProgress(route: route) + self._routeProgress = RouteProgress(route: route, options: options) self.dataSource = source UIDevice.current.isBatteryMonitoringEnabled = true @@ -161,7 +161,7 @@ open class RouteController: NSObject { /// updateNavigator is used to pass the new progress model onto nav-native. private func updateNavigator(with progress: RouteProgress) { let encoder = JSONEncoder() - encoder.userInfo[.options] = progress.route.routeOptions + encoder.userInfo[.options] = progress.routeOptions guard let routeData = try? encoder.encode(progress.route), let routeJSONString = String(data: routeData, encoding: .utf8) else { return @@ -401,25 +401,31 @@ extension RouteController: Router { if isRerouting { return } isRerouting = true - getDirections(from: location, along: progress) { [weak self] (route, error) in + getDirections(from: location, along: progress) { [weak self] (session, result) in self?.isRerouting = false guard let strongSelf: RouteController = self else { return } - if let error = error { + switch result { + case let .success(response): + guard let route = response.routes?.first else { return } + guard case let .route(routeOptions) = response.options else { return } //TODO: Can a match hit this codepoint? + strongSelf._routeProgress = RouteProgress(route: route, options: routeOptions, legIndex: 0) + strongSelf._routeProgress.currentLegProgress.stepIndex = 0 + strongSelf.announce(reroute: route, at: location, proactive: false) + + case let .failure(error): strongSelf.delegate?.router(strongSelf, didFailToRerouteWith: error) NotificationCenter.default.post(name: .routeControllerDidFailToReroute, object: self, userInfo: [ NotificationUserInfoKey.routingErrorKey: error, ]) return } + - guard let route = route else { return } - strongSelf._routeProgress = RouteProgress(route: route, legIndex: 0) - strongSelf._routeProgress.currentLegProgress.stepIndex = 0 - strongSelf.announce(reroute: route, at: location, proactive: false) + } } } diff --git a/MapboxCoreNavigation/RouteProgress.swift b/MapboxCoreNavigation/RouteProgress.swift index cd9088352d..2ec7efb8b9 100644 --- a/MapboxCoreNavigation/RouteProgress.swift +++ b/MapboxCoreNavigation/RouteProgress.swift @@ -12,6 +12,8 @@ open class RouteProgress: NSObject { Returns the current `Route`. */ public let route: Route + + public let routeOptions: RouteOptions /** Index representing current `RouteLeg`. @@ -94,7 +96,7 @@ open class RouteProgress: NSObject { The waypoints remaining on the current route, including any waypoints that do not separate legs. */ func remainingWaypointsForCalculatingRoute() -> [Waypoint] { - let (currentLegViaPoints, remainingWaypoints) = route.routeOptions.waypoints(fromLegAt: legIndex) + let (currentLegViaPoints, remainingWaypoints) = routeOptions.waypoints(fromLegAt: legIndex) let currentLegRemainingViaPoints = currentLegProgress.remainingWaypoints(among: currentLegViaPoints) return currentLegRemainingViaPoints + remainingWaypoints } @@ -171,8 +173,9 @@ open class RouteProgress: NSObject { - parameter route: The route to follow. - parameter legIndex: Zero-based index indicating the current leg the user is on. */ - public init(route: Route, legIndex: Int = 0, spokenInstructionIndex: Int = 0) { + public init(route: Route, options: RouteOptions, legIndex: Int = 0, spokenInstructionIndex: Int = 0) { self.route = route + self.routeOptions = options self.legIndex = legIndex self.currentLegProgress = RouteLegProgress(leg: route.legs[legIndex], stepIndex: 0, spokenInstructionIndex: spokenInstructionIndex) super.init() @@ -254,7 +257,7 @@ open class RouteProgress: NSObject { } func reroutingOptions(with current: CLLocation) -> RouteOptions { - let oldOptions = route.routeOptions + let oldOptions = routeOptions let user = Waypoint(coordinate: current.coordinate) if (current.course >= 0) { diff --git a/MapboxCoreNavigation/Router.swift b/MapboxCoreNavigation/Router.swift index d433c276fb..cfe76e210e 100644 --- a/MapboxCoreNavigation/Router.swift +++ b/MapboxCoreNavigation/Router.swift @@ -35,7 +35,7 @@ public protocol Router: class, CLLocationManagerDelegate { - parameter directions: The Directions object that created `route`. - parameter source: The data source for the RouteController. */ - init(along route: Route, directions: Directions, dataSource source: RouterDataSource) + init(along route: Route, options: RouteOptions, directions: Directions, dataSource source: RouterDataSource) /** Details about the user’s progress along the current route, leg, and step. @@ -128,10 +128,13 @@ extension InternalRouter where Self: Router { if isRerouting { return } isRerouting = true - getDirections(from: location, along: routeProgress) { [weak self] (route, error) in + getDirections(from: location, along: routeProgress) { [weak self] (session, result) in self?.isRerouting = false - guard let route = route else { return } + guard case let .success(response) = result else { + return + } + guard let route = response.routes?.first else { return } self?.lastProactiveRerouteDate = nil @@ -148,19 +151,28 @@ extension InternalRouter where Self: Router { } } - func getDirections(from location: CLLocation, along progress: RouteProgress, completion: @escaping (_ route: Route?, _ error: Error?)->Void) { + func getDirections(from location: CLLocation, along progress: RouteProgress, completion: @escaping Directions.RouteCompletionHandler) { routeTask?.cancel() let options = progress.reroutingOptions(with: location) lastRerouteLocation = location - routeTask = directions.calculate(options) {(waypoints, routes, error) in - guard let routes = routes else { - return completion(nil, error) + routeTask = directions.calculate(options) {(session, result) in + + guard case let .success(response) = result else { + return completion(session, result) } + + + guard let mostSimilar = response.routes?.mostSimilar(to: progress.route) else { + return completion(session, result) + } + + var modifiedResponse = response + modifiedResponse.routes?.removeAll { $0 == mostSimilar } + modifiedResponse.routes?.insert(mostSimilar, at: 0) - let mostSimilar = routes.mostSimilar(to: progress.route) - return completion(mostSimilar ?? routes.first, error) + return completion(session, .success(modifiedResponse)) } } @@ -174,7 +186,7 @@ extension InternalRouter where Self: Router { didFindFasterRoute = false } - routeProgress = RouteProgress(route: route, legIndex: 0, spokenInstructionIndex: spokenInstructionIndex) + routeProgress = RouteProgress(route: route, options: routeProgress.routeOptions, legIndex: 0, spokenInstructionIndex: spokenInstructionIndex) } func announce(reroute newRoute: Route, at location: CLLocation?, proactive: Bool) { diff --git a/MapboxCoreNavigationTests/LocationTests.swift b/MapboxCoreNavigationTests/LocationTests.swift index 9315dd26bc..a139e8387c 100644 --- a/MapboxCoreNavigationTests/LocationTests.swift +++ b/MapboxCoreNavigationTests/LocationTests.swift @@ -5,7 +5,7 @@ import CoreLocation class LocationTests: XCTestCase { var setup: (progress: RouteProgress, firstLocation: CLLocation) { - let progress = RouteProgress(route: route) + let progress = RouteProgress(route: route, options: routeOptions) let firstCoord = progress.nearbyShape.coordinates.first! let firstLocation = CLLocation(latitude: firstCoord.latitude, longitude: firstCoord.longitude) diff --git a/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift b/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift index a11f9cc031..2273327e60 100644 --- a/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift +++ b/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift @@ -11,7 +11,7 @@ var routeOptions: NavigationRouteOptions { return NavigationRouteOptions(waypoints: [from, to]) } let response = Fixture.routeResponse(from: jsonFileName, options: routeOptions) -let directions = DirectionsSpy(accessToken: "pk.feedCafeDeadBeefBadeBede") +let directions = DirectionsSpy() let route: Route = { return Fixture.route(from: jsonFileName, options: routeOptions) }() @@ -22,8 +22,7 @@ class MapboxCoreNavigationTests: XCTestCase { var navigation: MapboxNavigationService! func testNavigationNotificationsInfoDict() { - route.accessToken = "foo" - navigation = MapboxNavigationService(route: route, directions: directions, simulating: .never) + navigation = MapboxNavigationService(route: route, routeOptions: routeOptions, directions: directions, simulating: .never) let now = Date() let steps = route.legs.first!.steps let coordinates = steps[2].shape!.coordinates + steps[3].shape!.coordinates @@ -52,8 +51,7 @@ class MapboxCoreNavigationTests: XCTestCase { } func testDepart() { - route.accessToken = "foo" - navigation = MapboxNavigationService(route: route, directions: directions, simulating: .never) + navigation = MapboxNavigationService(route: route, routeOptions: routeOptions, directions: directions, simulating: .never) // Coordinates from first step let coordinates = route.legs[0].steps[0].shape!.coordinates @@ -81,8 +79,6 @@ class MapboxCoreNavigationTests: XCTestCase { } func testNewStep() { - route.accessToken = "foo" - // Coordinates from beginning of step[1] to end of step[2] let coordinates = route.legs[0].steps[1].shape!.coordinates + route.legs[0].steps[2].shape!.coordinates let locations: [CLLocation] @@ -91,7 +87,7 @@ class MapboxCoreNavigationTests: XCTestCase { altitude: -1, horizontalAccuracy: -1, verticalAccuracy: -1, course: -1, speed: 10, timestamp: now + $0.offset) } - navigation = MapboxNavigationService(route: route, directions: directions, simulating: .never) + navigation = MapboxNavigationService(route: route, routeOptions: routeOptions, directions: directions, simulating: .never) expectation(forNotification: .routeControllerDidPassSpokenInstructionPoint, object: navigation.router) { (notification) -> Bool in let routeProgress = notification.userInfo![RouteController.NotificationUserInfoKey.routeProgressKey] as? RouteProgress @@ -108,15 +104,13 @@ class MapboxCoreNavigationTests: XCTestCase { } func testJumpAheadToLastStep() { - route.accessToken = "foo" - let coordinates = route.legs[0].steps.map { $0.shape!.coordinates }.flatMap { $0 } let now = Date() let locations = coordinates.enumerated().map { CLLocation(coordinate: $0.element, altitude: -1, horizontalAccuracy: -1, verticalAccuracy: -1, timestamp: now + $0.offset) } let locationManager = ReplayLocationManager(locations: locations) - navigation = MapboxNavigationService(route: route, directions: directions, locationSource: locationManager, simulating: .never) + navigation = MapboxNavigationService(route: route, routeOptions: routeOptions, directions: directions, locationSource: locationManager, simulating: .never) expectation(forNotification: .routeControllerDidPassSpokenInstructionPoint, object: navigation.router) { (notification) -> Bool in let routeProgress = notification.userInfo![RouteController.NotificationUserInfoKey.routeProgressKey] as? RouteProgress @@ -133,8 +127,6 @@ class MapboxCoreNavigationTests: XCTestCase { } func testShouldReroute() { - route.accessToken = "foo" - let coordinates = route.legs[0].steps[1].shape!.coordinates let now = Date() let locations = coordinates.enumerated().map { CLLocation(coordinate: $0.element, @@ -150,7 +142,7 @@ class MapboxCoreNavigationTests: XCTestCase { } let locationManager = ReplayLocationManager(locations: locations + offRouteLocations) - navigation = MapboxNavigationService(route: route, directions: directions, locationSource: locationManager, simulating: .never) + navigation = MapboxNavigationService(route: route, routeOptions: routeOptions, directions: directions, locationSource: locationManager, simulating: .never) expectation(forNotification: .routeControllerWillReroute, object: navigation.router) { (notification) -> Bool in XCTAssertEqual(notification.userInfo?.count, 1) @@ -170,15 +162,13 @@ class MapboxCoreNavigationTests: XCTestCase { } func testArrive() { - route.accessToken = "foo" - let now = Date() let locations = Fixture.generateTrace(for: route).enumerated().map { $0.element.shifted(to: now + $0.offset) } let locationManager = DummyLocationManager() - navigation = MapboxNavigationService(route: route, directions: directions, locationSource: locationManager, simulating: .never) + navigation = MapboxNavigationService(route: route, routeOptions: routeOptions, directions: directions, locationSource: locationManager, simulating: .never) expectation(forNotification: .routeControllerProgressDidChange, object: navigation.router) { (notification) -> Bool in let routeProgress = notification.userInfo![RouteController.NotificationUserInfoKey.routeProgressKey] as? RouteProgress @@ -224,10 +214,9 @@ class MapboxCoreNavigationTests: XCTestCase { } func testOrderOfExecution() { - route.accessToken = "foo" - let trace = Fixture.generateTrace(for: route).shiftedToPresent().qualified() - navigation = MapboxNavigationService(route: route) + let directions = DirectionsSpy() + navigation = MapboxNavigationService(route: route, routeOptions: routeOptions, directions: directions) struct InstructionPoint { enum InstructionType { @@ -321,9 +310,8 @@ class MapboxCoreNavigationTests: XCTestCase { } func testFailToReroute() { - route.accessToken = "foo" - let directionsClientSpy = DirectionsSpy(accessToken: "garbage", host: nil) - navigation = MapboxNavigationService(route: route, directions: directionsClientSpy, simulating: .never) + let directionsClientSpy = DirectionsSpy() + navigation = MapboxNavigationService(route: route, routeOptions: routeOptions, directions: directionsClientSpy, simulating: .never) expectation(forNotification: .routeControllerWillReroute, object: navigation.router) { (notification) -> Bool in return true diff --git a/MapboxCoreNavigationTests/MapboxNavigationServiceSpec.swift b/MapboxCoreNavigationTests/MapboxNavigationServiceSpec.swift index 82608bfa76..f62e180a32 100644 --- a/MapboxCoreNavigationTests/MapboxNavigationServiceSpec.swift +++ b/MapboxCoreNavigationTests/MapboxNavigationServiceSpec.swift @@ -8,7 +8,6 @@ import TestHelper class MapboxNavigationServiceSpec: QuickSpec { lazy var initialRoute: Route = { let route = response.routes!.first! - route.accessToken = "foo" return route }() @@ -17,7 +16,7 @@ class MapboxNavigationServiceSpec: QuickSpec { let route = initialRoute let subject = LeakTest { - let service = MapboxNavigationService(route: route, directions: DirectionsSpy(accessToken: "deadbeef")) + let service = MapboxNavigationService(route: route, routeOptions: routeOptions, directions: DirectionsSpy()) return service } it("Must not leak.") { diff --git a/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift b/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift index acfbfc8976..9c86aba056 100644 --- a/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift +++ b/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift @@ -14,20 +14,24 @@ class NavigationEventsManagerTests: XCTestCase { } func testDepartRerouteArrive() { - let firstRoute = Fixture.route(from: "DCA-Arboretum", options: NavigationRouteOptions(coordinates: [ + let firstRouteOptions = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 38.853108, longitude: -77.043331), CLLocationCoordinate2D(latitude: 38.910736, longitude: -76.966906), - ])) - let secondRoute = Fixture.route(from: "PipeFittersUnion-FourSeasonsBoston", options: NavigationRouteOptions(coordinates: [ - CLLocationCoordinate2D(latitude: 42.361634, longitude: -71.12852), - CLLocationCoordinate2D(latitude: 42.352396, longitude: -71.068719), - ])) + ]) + let firstRoute = Fixture.route(from: "DCA-Arboretum", options: firstRouteOptions) + + let secondRouteOptions = NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 42.361634, longitude: -71.12852), + CLLocationCoordinate2D(latitude: 42.352396, longitude: -71.068719), + ]) + let secondRoute = Fixture.route(from: "PipeFittersUnion-FourSeasonsBoston", options: secondRouteOptions) let firstTrace = Array(Fixture.generateTrace(for: firstRoute).prefix(upTo: firstRoute.shape!.coordinates.count / 2)).shiftedToPresent().qualified() let secondTrace = Fixture.generateTrace(for: secondRoute).shifted(to: firstTrace.last!.timestamp + 1).qualified() let locationManager = NavigationLocationManager() let service = MapboxNavigationService(route: firstRoute, + routeOptions: firstRouteOptions, directions: nil, locationSource: locationManager, eventsManagerType: NavigationEventsManagerSpy.self, diff --git a/MapboxCoreNavigationTests/NavigationServiceTests.swift b/MapboxCoreNavigationTests/NavigationServiceTests.swift index 4234eb7208..f0f6af5451 100644 --- a/MapboxCoreNavigationTests/NavigationServiceTests.swift +++ b/MapboxCoreNavigationTests/NavigationServiceTests.swift @@ -10,13 +10,13 @@ fileprivate let mbTestHeading: CLLocationDirection = 50 class NavigationServiceTests: XCTestCase { var eventsManagerSpy: NavigationEventsManagerSpy! - let directionsClientSpy = DirectionsSpy(accessToken: "garbage", host: nil) + let directionsClientSpy = DirectionsSpy() let delegate = NavigationServiceDelegateSpy() typealias RouteLocations = (firstLocation: CLLocation, penultimateLocation: CLLocation, lastLocation: CLLocation) lazy var dependencies: (navigationService: NavigationService, routeLocations: RouteLocations) = { - let navigationService = MapboxNavigationService(route: initialRoute, directions: directionsClientSpy, eventsManagerType: NavigationEventsManagerSpy.self, simulating: .never) + let navigationService = MapboxNavigationService(route: initialRoute, routeOptions: routeOptions, directions: directionsClientSpy, eventsManagerType: NavigationEventsManagerSpy.self, simulating: .never) navigationService.delegate = delegate let legProgress: RouteLegProgress = navigationService.router.routeProgress.currentLegProgress @@ -174,14 +174,14 @@ class NavigationServiceTests: XCTestCase { func testUserPuckShouldFaceBackwards() { // This route is a simple straight line: http://geojson.io/#id=gist:anonymous/64cfb27881afba26e3969d06bacc707c&map=17/37.77717/-122.46484 - let directions = DirectionsSpy(accessToken: "pk.feedCafeDeadBeefBadeBede") - let route = Fixture.route(from: "straight-line", options: NavigationRouteOptions(coordinates: [ + let directions = DirectionsSpy() + let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 37.77735, longitude: -122.461465), CLLocationCoordinate2D(latitude: 37.777016, longitude: -122.468832), - ])) + ]) + let route = Fixture.route(from: "straight-line", options: options) - route.accessToken = "foo" - let navigation = MapboxNavigationService(route: route, directions: directions) + let navigation = MapboxNavigationService(route: route, routeOptions: options, directions: directions) let router = navigation.router! let firstCoord = router.routeProgress.nearbyShape.coordinates.first! let firstLocation = CLLocation(latitude: firstCoord.latitude, longitude: firstCoord.longitude) @@ -224,13 +224,13 @@ class NavigationServiceTests: XCTestCase { func testTurnstileEventSentUponInitialization() { // MARK: it sends a turnstile event upon initialization - let service = MapboxNavigationService(route: initialRoute, directions: directionsClientSpy, locationSource: NavigationLocationManager(), eventsManagerType: NavigationEventsManagerSpy.self) + let service = MapboxNavigationService(route: initialRoute, routeOptions: routeOptions, directions: directionsClientSpy, locationSource: NavigationLocationManager(), eventsManagerType: NavigationEventsManagerSpy.self) let eventsManagerSpy = service.eventsManager as! NavigationEventsManagerSpy XCTAssertTrue(eventsManagerSpy.hasFlushedEvent(with: MMEEventTypeAppUserTurnstile)) } func testReroutingFromLocationUpdatesSimulatedLocationSource() { - let navigationService = MapboxNavigationService(route: initialRoute, directions: directionsClientSpy, eventsManagerType: NavigationEventsManagerSpy.self, simulating: .always) + let navigationService = MapboxNavigationService(route: initialRoute, routeOptions: routeOptions, directions: directionsClientSpy, eventsManagerType: NavigationEventsManagerSpy.self, simulating: .always) navigationService.delegate = delegate let router = navigationService.router! @@ -366,7 +366,7 @@ class NavigationServiceTests: XCTestCase { autoreleasepool { let fakeDataSource = RouteControllerDataSourceFake() - let routeController = RouteController(along: initialRoute, directions: directionsClientSpy, dataSource: fakeDataSource) + let routeController = RouteController(along: initialRoute, options: routeOptions, directions: directionsClientSpy, dataSource: fakeDataSource) subject = routeController } @@ -378,7 +378,7 @@ class NavigationServiceTests: XCTestCase { autoreleasepool { let fakeDataSource = RouteControllerDataSourceFake() - let routeController = LegacyRouteController(along: initialRoute, directions: directionsClientSpy, dataSource: fakeDataSource) + let routeController = LegacyRouteController(along: initialRoute, options: routeOptions, directions: directionsClientSpy, dataSource: fakeDataSource) subject = routeController } @@ -390,7 +390,7 @@ class NavigationServiceTests: XCTestCase { autoreleasepool { let fakeDataSource = RouteControllerDataSourceFake() - _ = RouteController(along: initialRoute, directions: directionsClientSpy, dataSource: fakeDataSource) + _ = RouteController(along: initialRoute, options: routeOptions, directions: directionsClientSpy, dataSource: fakeDataSource) subject = fakeDataSource } @@ -398,8 +398,8 @@ class NavigationServiceTests: XCTestCase { } func testCountdownTimerDefaultAndUpdate() { - let directions = DirectionsSpy(accessToken: "pk.feedCafeDeadBeefBadeBede") - let subject = MapboxNavigationService(route: initialRoute, directions: directions) + let directions = DirectionsSpy() + let subject = MapboxNavigationService(route: initialRoute, routeOptions: routeOptions, directions: directions) XCTAssert(subject.poorGPSTimer.countdownInterval == .milliseconds(2500), "Default countdown interval should be 2500 milliseconds.") @@ -435,18 +435,19 @@ class NavigationServiceTests: XCTestCase { func testProactiveRerouting() { typealias RouterComposition = Router & InternalRouter - let route = Fixture.route(from: "DCA-Arboretum", options: NavigationRouteOptions(coordinates: [ + let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 38.853108, longitude: -77.043331), CLLocationCoordinate2D(latitude: 38.910736, longitude: -76.966906), - ])) + ]) + let route = Fixture.route(from: "DCA-Arboretum", options: options) let trace = Fixture.generateTrace(for: route).shiftedToPresent() let duration = trace.last!.timestamp.timeIntervalSince(trace.first!.timestamp) XCTAssert(duration > RouteControllerProactiveReroutingInterval + RouteControllerMinimumDurationRemainingForProactiveRerouting, "Duration must greater than rerouting interval and minimum duration remaining for proactive rerouting") - let directions = DirectionsSpy(accessToken: "pk.feedCafeDeadBeefBadeBede") - let service = MapboxNavigationService(route: route, directions: directions) + let directions = DirectionsSpy() + let service = MapboxNavigationService(route: route, routeOptions: options, directions: directions) service.delegate = delegate let router = service.router! let locationManager = NavigationLocationManager() @@ -469,12 +470,12 @@ class NavigationServiceTests: XCTestCase { } let fasterRouteName = "DCA-Arboretum-dummy-faster-route" - let options = NavigationRouteOptions(coordinates: [ + 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: options) - let waypointsForFasterRoute = Fixture.waypoints(from: fasterRouteName, options: options) + let fasterRoute = Fixture.route(from: fasterRouteName, options: fasterOptions) + let waypointsForFasterRoute = Fixture.waypoints(from: fasterRouteName, options: fasterOptions) directions.fireLastCalculateCompletion(with: waypointsForFasterRoute, routes: [fasterRoute], error: nil) XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:didRerouteAlong:at:proactive:)")) @@ -483,7 +484,7 @@ class NavigationServiceTests: XCTestCase { } func testNineLeggedRouteForOutOfBounds() { - let route = Fixture.route(from: "9-legged-route", options: NavigationRouteOptions(coordinates: [ + let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 46.423728, longitude: 13.593578), CLLocationCoordinate2D(latitude: 46.339747, longitude: 13.574151), CLLocationCoordinate2D(latitude: 46.34447, longitude: 13.57594), @@ -494,12 +495,13 @@ class NavigationServiceTests: XCTestCase { CLLocationCoordinate2D(latitude: 46.435762, longitude: 13.626714), CLLocationCoordinate2D(latitude: 46.436658, longitude: 13.639499), CLLocationCoordinate2D(latitude: 46.43878, longitude: 13.64052), - ])) - let directions = Directions(accessToken: "foo") + ]) + let route = Fixture.route(from: "9-legged-route", options: options) + let directions = Directions(credentials: Fixture.credentials) let locationManager = DummyLocationManager() let trace = Fixture.generateTrace(for: route, speedMultiplier: 4).shiftedToPresent() - let service = MapboxNavigationService(route: route, directions: directions, locationSource: locationManager, eventsManagerType: nil) + let service = MapboxNavigationService(route: route, routeOptions: options, directions: directions, locationSource: locationManager, eventsManagerType: nil) service.start() for location in trace { @@ -970,15 +972,16 @@ class NavigationServiceTests: XCTestCase { func testUnimplementedLogging() { unimplementedTestLogs = [] - let route = Fixture.route(from: "DCA-Arboretum", options: NavigationRouteOptions(coordinates: [ - CLLocationCoordinate2D(latitude: 38.853108, longitude: -77.043331), - CLLocationCoordinate2D(latitude: 38.910736, longitude: -76.966906), - ])) - let directions = Directions(accessToken: "foo") + let options = NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 38.853108, longitude: -77.043331), + CLLocationCoordinate2D(latitude: 38.910736, longitude: -76.966906), + ]) + let route = Fixture.route(from: "DCA-Arboretum", options: options) + let directions = Directions(credentials: Fixture.credentials) let locationManager = DummyLocationManager() let trace = Fixture.generateTrace(for: route, speedMultiplier: 4).shiftedToPresent() - let service = MapboxNavigationService(route: route, directions: directions, locationSource: locationManager, eventsManagerType: nil) + let service = MapboxNavigationService(route: route, routeOptions: options, directions: directions, locationSource: locationManager, eventsManagerType: nil) let spy = EmptyNavigationServiceDelegate() service.delegate = spy diff --git a/MapboxCoreNavigationTests/OfflineRoutingTests.swift b/MapboxCoreNavigationTests/OfflineRoutingTests.swift index 80a2e79f8a..314ef523b1 100644 --- a/MapboxCoreNavigationTests/OfflineRoutingTests.swift +++ b/MapboxCoreNavigationTests/OfflineRoutingTests.swift @@ -10,7 +10,7 @@ class OfflineRoutingTests: XCTestCase { let setupExpectation = expectation(description: "Set up offline routing") - let directions = NavigationDirections(accessToken: "foo") + let directions = NavigationDirections(credentials: Fixture.credentials) directions.configureRouter(tilesURL: tilesURL) { (numberOfTiles) in XCTAssertEqual(numberOfTiles, 5) @@ -25,20 +25,29 @@ class OfflineRoutingTests: XCTestCase { let options = NavigationRouteOptions(coordinates: coordinates, profileIdentifier: .automobile) let calculateRouteExpectation = expectation(description: "Calculate route offline") - var route: Route? + var possibleRoute: Route? - directions.calculate(options, offline: true) { (waypoints, routes, error) in - XCTAssertNil(error) - XCTAssertNotNil(waypoints) - XCTAssertNotNil(routes) - route = routes!.first! - calculateRouteExpectation.fulfill() + directions.calculate(options, offline: true) { (session, result) in + switch result { + case let .failure(error): + XCTFail("Unexpected Failure: \(error)") + + case let .success(response): + XCTAssertNotNil(response.routes) + XCTAssertNotNil(response.waypoints) + possibleRoute = response.routes!.first! + calculateRouteExpectation.fulfill() + } } wait(for: [calculateRouteExpectation], timeout: 2) - XCTAssertNotNil(route) - XCTAssertEqual(route!.shape!.coordinates.count, 47) + guard let route = possibleRoute else { + XCTFail("No route returned") + return + } + + XCTAssertEqual(route.shape!.coordinates.count, 47) } func testOfflineDirectionsError() { @@ -47,7 +56,7 @@ class OfflineRoutingTests: XCTestCase { let setupExpectation = expectation(description: "Set up offline routing") - let directions = NavigationDirections(accessToken: "foo") + let directions = NavigationDirections(credentials: Fixture.credentials) directions.configureRouter(tilesURL: tilesURL) { (numberOfTiles) in XCTAssertEqual(numberOfTiles, 5) setupExpectation.fulfill() @@ -62,15 +71,18 @@ class OfflineRoutingTests: XCTestCase { let options = NavigationRouteOptions(coordinates: coordinates, profileIdentifier: .automobile) let calculateRouteExpectation = expectation(description: "Calculate route offline") - directions.calculate(options, offline: true) { (waypoints, routes, error) in - XCTAssertNotNil(error) - if let error = error, case let .standard(directionsError) = error { - XCTAssertEqual(directionsError, .unableToRoute) - } else { - XCTFail("Error should be standard error") + directions.calculate(options, offline: true) { (session, response) in + guard case let .failure(error) = response else { + XCTFail("Unexpected Success") + return + } + + guard case let .standard(directionsError) = error else { + XCTFail("Wrong error type.") + return } - XCTAssertNil(routes) - XCTAssertNil(waypoints) + + XCTAssertEqual(directionsError, .unableToRoute) calculateRouteExpectation.fulfill() } @@ -107,7 +119,7 @@ class OfflineRoutingTests: XCTestCase { let configureExpectation = self.expectation(description: "Configure router with unpacked tar") - let directions = NavigationDirections(accessToken: "foo") + let directions = NavigationDirections(credentials: Fixture.credentials) directions.configureRouter(tilesURL: outputDirectoryURL) { (numberOfTiles) in XCTAssertEqual(numberOfTiles, 5) configureExpectation.fulfill() diff --git a/MapboxCoreNavigationTests/RouteProgressTests.swift b/MapboxCoreNavigationTests/RouteProgressTests.swift index ef77fc4b9b..ba088076b4 100644 --- a/MapboxCoreNavigationTests/RouteProgressTests.swift +++ b/MapboxCoreNavigationTests/RouteProgressTests.swift @@ -7,7 +7,7 @@ import Turf class RouteProgressTests: XCTestCase { func testRouteProgress() { - let routeProgress = RouteProgress(route: route) + let routeProgress = RouteProgress(route: route, options: routeOptions) XCTAssertEqual(routeProgress.fractionTraveled, 0) XCTAssertEqual(routeProgress.distanceRemaining, 4054.2) XCTAssertEqual(routeProgress.distanceTraveled, 0) @@ -15,7 +15,7 @@ class RouteProgressTests: XCTestCase { } func testRouteLegProgress() { - let routeProgress = RouteProgress(route: route) + let routeProgress = RouteProgress(route: route, options: routeOptions) XCTAssertEqual(routeProgress.currentLeg.description, "Hyde Street, Page Street") XCTAssertEqual(routeProgress.currentLegProgress.distanceTraveled, 0) XCTAssertEqual(round(routeProgress.currentLegProgress.durationRemaining), 858) @@ -26,7 +26,7 @@ class RouteProgressTests: XCTestCase { } func testRouteStepProgress() { - let routeProgress = RouteProgress(route: route) + let routeProgress = RouteProgress(route: route, options: routeOptions) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.distanceRemaining, 384.1) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.distanceTraveled, 0) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.durationRemaining, 86.6, accuracy: 0.001) @@ -36,7 +36,7 @@ class RouteProgressTests: XCTestCase { } func testNextRouteStepProgress() { - let routeProgress = RouteProgress(route: route) + let routeProgress = RouteProgress(route: route, options: routeOptions) routeProgress.currentLegProgress.stepIndex = 1 XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.spokenInstructionIndex, 0) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.distanceRemaining, 439.1) diff --git a/MapboxCoreNavigationTests/TunnelAuthorityTests.swift b/MapboxCoreNavigationTests/TunnelAuthorityTests.swift index ba4b078959..c05deb96c5 100644 --- a/MapboxCoreNavigationTests/TunnelAuthorityTests.swift +++ b/MapboxCoreNavigationTests/TunnelAuthorityTests.swift @@ -6,17 +6,24 @@ import MapboxDirections import TestHelper @testable import MapboxCoreNavigation -let tunnelRoute = Fixture.route(from: "routeWithTunnels_9thStreetDC", options: { + +let tunnelOptions: RouteOptions = { let from = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.892134, longitude: -77.023975)) let to = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 38.880594, longitude: -77.024705)) return NavigationRouteOptions(waypoints: [from, to]) -}()) +}() + +let tunnelResponse = Fixture.routeResponse(from: "routeWithTunnels_9thStreetDC", options: tunnelOptions) + +var tunnelRoute: Route { + return tunnelResponse.routes!.first! +} class TunnelAuthorityTests: XCTestCase { lazy var locationManager = NavigationLocationManager() func testUserWithinTunnelEntranceRadius() { - let routeProgress = RouteProgress(route: tunnelRoute) + let routeProgress = RouteProgress(route: tunnelRoute, options: tunnelOptions) // Mock location move to first coordinate on tunnel route let firstCoordinate = tunnelRoute.shape!.coordinates.first! diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index 7122f94ee1..29fb1ca87f 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -226,6 +226,7 @@ 3EA937B1F4DF73EB004BA6BE /* InstructionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA93230997B8D59E3B76C8C /* InstructionPresenter.swift */; }; 3EA93A1FEFDDB709DE84BED9 /* ImageRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA938BE5468824787100228 /* ImageRepository.swift */; }; 4303A3992332CD6200B5737D /* UnimplementedLogging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4303A3982332CD6200B5737D /* UnimplementedLogging.swift */; }; + 4316D95C24340555000DD8F8 /* Match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4316D95B24340555000DD8F8 /* Match.swift */; }; 4341758223060A17004264A9 /* route-with-tertiary.json in Resources */ = {isa = PBXBuildFile; fileRef = 439FFC222304BC23004C20AA /* route-with-tertiary.json */; }; 4341758423061666004264A9 /* SnapshotTest+Mapbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4341758323061666004264A9 /* SnapshotTest+Mapbox.swift */; }; 439FFC252304BF54004C20AA /* GuidanceCardsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439FFC242304BF54004C20AA /* GuidanceCardsSnapshotTests.swift */; }; @@ -807,6 +808,7 @@ 3EA93A10227A7DAF1861D9F5 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; 3EA93EBD6E6BEC966BBE51D6 /* NavigationServiceTestDoubles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationServiceTestDoubles.swift; sourceTree = ""; }; 4303A3982332CD6200B5737D /* UnimplementedLogging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnimplementedLogging.swift; sourceTree = ""; }; + 4316D95B24340555000DD8F8 /* Match.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Match.swift; sourceTree = ""; }; 4341758323061666004264A9 /* SnapshotTest+Mapbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SnapshotTest+Mapbox.swift"; sourceTree = ""; }; 439FFC222304BC23004C20AA /* route-with-tertiary.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "route-with-tertiary.json"; sourceTree = ""; }; 439FFC242304BF54004C20AA /* GuidanceCardsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuidanceCardsSnapshotTests.swift; sourceTree = ""; }; @@ -1551,6 +1553,7 @@ 351BEC081E5BCC72006FE110 /* Bundle.swift */, 8D24A2F720409A890098CBF8 /* CGSize.swift */, DA0557202154EF4700A1F2AA /* Route.swift */, + 4316D95B24340555000DD8F8 /* Match.swift */, C5F4D21820DC468B0059FABF /* CongestionLevel.swift */, C51511D020EAC89D00372A91 /* CPMapTemplate.swift */, DA443DDD2278C90E00ED1307 /* CPTrip.swift */, @@ -2447,6 +2450,7 @@ 8D5DFFF1207C04840093765A /* NSAttributedString.swift in Sources */, C565168B1FE1E23E00A0AD18 /* MapboxVoiceController.swift in Sources */, 35CF34B11F0A733200C2692E /* UIFont.swift in Sources */, + 4316D95C24340555000DD8F8 /* Match.swift in Sources */, 8DEB4066220CE596008BAAB4 /* NavigationMapViewDelegate.swift in Sources */, 359D283C1F9DC14F00FDE9C9 /* UICollectionView.swift in Sources */, AE47A32F22B1F6AE0096458C /* DayInstructionsCardStyle.swift in Sources */, diff --git a/MapboxNavigation/CPTrip.swift b/MapboxNavigation/CPTrip.swift index 407acdde00..3f18db1fdf 100644 --- a/MapboxNavigation/CPTrip.swift +++ b/MapboxNavigation/CPTrip.swift @@ -36,7 +36,8 @@ extension CPTrip { let routeChoice = CPRouteChoice(summaryVariants: summaryVariants, additionalInformationVariants: [route.description], selectionSummaryVariants: [route.description]) - routeChoice.userInfo = route + let info: (Route, RouteOptions) = (route: route, options: routeOptions) + routeChoice.userInfo = info return routeChoice } diff --git a/MapboxNavigation/CarPlayManager.swift b/MapboxNavigation/CarPlayManager.swift index ea1a0b08bc..47d30cafa8 100644 --- a/MapboxNavigation/CarPlayManager.swift +++ b/MapboxNavigation/CarPlayManager.swift @@ -227,8 +227,9 @@ public class CarPlayManager: NSObject { */ public func beginNavigationWithCarPlay(using currentLocation: CLLocationCoordinate2D, navigationService: NavigationService) { let route = navigationService.route + let routeOptions = navigationService.routeProgress.routeOptions - var trip = CPTrip(routes: [route], routeOptions: route.routeOptions, waypoints: route.routeOptions.waypoints) + var trip = CPTrip(routes: [route], routeOptions: routeOptions, waypoints: routeOptions.waypoints) trip = delegate?.carPlayManager(self, willPreview: trip) ?? trip self.navigationService = navigationService @@ -389,11 +390,11 @@ extension CarPlayManager { } public func previewRoutes(for options: RouteOptions, completionHandler: @escaping CompletionHandler) { - calculate(options) { [weak self] (waypoints, routes, error) in - self?.didCalculate(routes, + calculate(options) { [weak self] (session, result) in + + self?.didCalculate(result, + in: session, for: options, - between: waypoints, - error: error, completionHandler: completionHandler) } } @@ -402,14 +403,15 @@ extension CarPlayManager { directions.calculate(options, completionHandler: completionHandler) } - internal func didCalculate(_ routes: [Route]?, for routeOptions: RouteOptions, between waypoints: [Waypoint]?, error: DirectionsError?, completionHandler: CompletionHandler) { + internal func didCalculate(_ result: Result,in session: Directions.Session, for routeOptions: RouteOptions, completionHandler: CompletionHandler) { defer { completionHandler() } - if let error = error { + switch result { + case let .failure(error): guard let delegate = delegate, - let alert = delegate.carPlayManager(self, didFailToFetchRouteBetween: waypoints, options: routeOptions, error: error) else { + let alert = delegate.carPlayManager(self, didFailToFetchRouteBetween: routeOptions.waypoints, options: routeOptions, error: error) else { return } @@ -417,30 +419,28 @@ extension CarPlayManager { interfaceController?.popToRootTemplate(animated: true) mapTemplate?.present(navigationAlert: alert, animated: true) return - } - - guard let waypoints = waypoints, let routes = routes else { - return - } - - var trip = CPTrip(routes: routes, routeOptions: routeOptions, waypoints: waypoints) - trip = delegate?.carPlayManager(self, willPreview: trip) ?? trip + case let .success(response): + guard let routes = response.routes, case let .route(responseOptions) = response.options else { return } + let waypoints = responseOptions.waypoints + var trip = CPTrip(routes: routes, routeOptions: routeOptions, waypoints: waypoints) + trip = delegate?.carPlayManager(self, willPreview: trip) ?? trip - var previewText = defaultTripPreviewTextConfiguration() + var previewText = defaultTripPreviewTextConfiguration() - if let customPreviewText = delegate?.carPlayManager(self, willPreview: trip, with: previewText) { - previewText = customPreviewText - } + if let customPreviewText = delegate?.carPlayManager(self, willPreview: trip, with: previewText) { + previewText = customPreviewText + } - let traitCollection = (self.carWindow?.rootViewController as! CarPlayMapViewController).traitCollection - let previewMapTemplate = mapTemplateProvider.mapTemplate(forPreviewing: trip, traitCollection: traitCollection, mapDelegate: self) + let traitCollection = (self.carWindow?.rootViewController as! CarPlayMapViewController).traitCollection + let previewMapTemplate = mapTemplateProvider.mapTemplate(forPreviewing: trip, traitCollection: traitCollection, mapDelegate: self) - previewMapTemplate.showTripPreviews([trip], textConfiguration: previewText) - - guard let interfaceController = interfaceController else { - return + previewMapTemplate.showTripPreviews([trip], textConfiguration: previewText) + + guard let interfaceController = interfaceController else { + return + } + interfaceController.pushTemplate(previewMapTemplate, animated: true) } - interfaceController.pushTemplate(previewMapTemplate, animated: true) } private func defaultTripPreviewTextConfiguration() -> CPTripPreviewTextConfiguration { @@ -464,13 +464,13 @@ extension CarPlayManager: CPMapTemplateDelegate { mapTemplate.hideTripPreviews() - let route = routeChoice.userInfo as! Route + let (route, options) = routeChoice.userInfo as! (Route, RouteOptions) let desiredSimulationMode: SimulationMode = simulatesLocations ? .always : .onPoorGPS let service = navigationService ?? - delegate?.carPlayManager(self, navigationServiceAlong: route, desiredSimulationMode: desiredSimulationMode) ?? - MapboxNavigationService(route: route, simulating: desiredSimulationMode) + delegate?.carPlayManager(self, navigationServiceAlong: route, routeOptions: options, desiredSimulationMode: desiredSimulationMode) ?? + MapboxNavigationService(route: route, routeOptions: options, simulating: desiredSimulationMode) navigationService = service //store the service it was newly created/fetched @@ -535,7 +535,7 @@ extension CarPlayManager: CPMapTemplateDelegate { } carPlayMapViewController.isOverviewingRoutes = true let mapView = carPlayMapViewController.mapView - let route = routeChoice.userInfo as! Route + let (route, _) = routeChoice.userInfo as! (Route, RouteOptions) let estimates = CPTravelEstimates(distanceRemaining: Measurement(distance: route.distance).localized(), timeRemaining: route.expectedTravelTime) diff --git a/MapboxNavigation/CarPlayManagerDelegate.swift b/MapboxNavigation/CarPlayManagerDelegate.swift index ef961f1e19..7611c0bea9 100644 --- a/MapboxNavigation/CarPlayManagerDelegate.swift +++ b/MapboxNavigation/CarPlayManagerDelegate.swift @@ -58,10 +58,11 @@ public protocol CarPlayManagerDelegate: class, UnimplementedLogging { - parameter carPlayManager: The CarPlay manager instance. - parameter route: The route for which the returned route controller will manage location updates. + - parameter routeOptions: the options that were specified for the route request. - parameter desiredSimulationMode: The desired simulation mode to use. - returns: A navigation service that manages location updates along `route`. */ - func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, desiredSimulationMode: SimulationMode) -> NavigationService + func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, routeOptions: RouteOptions, desiredSimulationMode: SimulationMode) -> NavigationService /** Offers the delegate an opportunity to react to updates in the search text. diff --git a/MapboxNavigation/MapboxVoiceController.swift b/MapboxNavigation/MapboxVoiceController.swift index de843d3518..75e4e35426 100644 --- a/MapboxNavigation/MapboxVoiceController.swift +++ b/MapboxNavigation/MapboxVoiceController.swift @@ -83,7 +83,7 @@ open class MapboxVoiceController: RouteVoiceController, AVAudioPlayerDelegate { open override func didPassSpokenInstructionPoint(notification: NSNotification) { let routeProgresss = notification.userInfo![RouteController.NotificationUserInfoKey.routeProgressKey] as! RouteProgress - locale = routeProgresss.route.routeOptions.locale + locale = routeProgresss.routeOptions.locale let currentLegProgress: RouteLegProgress = routeProgresss.currentLegProgress let instructionSets = currentLegProgress.remainingSteps.prefix(stepsAheadToCache).compactMap { $0.instructionsSpokenAlongStep } diff --git a/MapboxNavigation/Match.swift b/MapboxNavigation/Match.swift new file mode 100644 index 0000000000..982a763f3e --- /dev/null +++ b/MapboxNavigation/Match.swift @@ -0,0 +1,41 @@ +import MapboxDirections +import Turf + +extension Match { + /** + Returns a polyline extending a given distance in either direction from a given maneuver along the route. + + The maneuver is identified by a leg index and step index, in case the route doubles back on itself. + + - parameter legIndex: Zero-based index of the leg containing the maneuver. + - parameter stepIndex: Zero-based index of the step containing the maneuver. + - parameter distance: Distance by which the resulting polyline extends in either direction from the maneuver. + - returns: A polyline whose length is twice `distance` and whose centroid is located at the maneuver. + */ + func polylineAroundManeuver(legIndex: Int, stepIndex: Int, distance: CLLocationDistance) -> Polyline { + let precedingLegs = legs.prefix(upTo: legIndex) + let precedingLegCoordinates = precedingLegs.flatMap { $0.steps }.flatMap { $0.shape?.coordinates ?? [] } + + let precedingSteps = legs[legIndex].steps.prefix(upTo: stepIndex) + let precedingStepCoordinates = precedingSteps.compactMap { $0.shape?.coordinates }.reduce([], +) + let precedingPolyline = Polyline((precedingLegCoordinates + precedingStepCoordinates).reversed()) + + let followingLegs = legs.suffix(from: legIndex).dropFirst() + let followingLegCoordinates = followingLegs.flatMap { $0.steps }.flatMap { $0.shape?.coordinates ?? [] } + + let followingSteps = legs[legIndex].steps.suffix(from: stepIndex) + let followingStepCoordinates = followingSteps.compactMap { $0.shape?.coordinates }.reduce([], +) + let followingPolyline = Polyline(followingStepCoordinates + followingLegCoordinates) + + // After trimming, reverse the array so that the resulting polyline proceeds in a forward direction throughout. + let trimmedPrecedingCoordinates: [CLLocationCoordinate2D] + if precedingPolyline.coordinates.isEmpty { + trimmedPrecedingCoordinates = [] + } else { + trimmedPrecedingCoordinates = precedingPolyline.trimmed(from: precedingPolyline.coordinates[0], distance: distance).coordinates.reversed() + } + // Omit the first coordinate, which is already contained in trimmedPrecedingCoordinates. + let trimmedFollowingCoordinates = followingPolyline.trimmed(from: followingPolyline.coordinates[0], distance: distance).coordinates.suffix(from: 1) + return Polyline(trimmedPrecedingCoordinates + trimmedFollowingCoordinates) + } +} diff --git a/MapboxNavigation/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index 33c7e08e47..c122aedd07 100644 --- a/MapboxNavigation/NavigationMapView.swift +++ b/MapboxNavigation/NavigationMapView.swift @@ -507,7 +507,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { let waypoints: [Waypoint] = Array(route.legs.dropLast().compactMap { $0.destination }) let source = navigationMapViewDelegate?.navigationMapView(self, shapeFor: waypoints, legIndex: legIndex) ?? shape(for: waypoints, legIndex: legIndex) - if route.routeOptions.waypoints.count > 2 { //are we on a multipoint route? + if route.legs.count > 1 { //are we on a multipoint route? routes = [route] //update the model if let waypointSource = style.source(withIdentifier: SourceIdentifier.waypoint) as? MGLShapeSource { @@ -709,9 +709,11 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { //TODO: Change to point-based distance calculation private func waypoints(on routes: [Route], closeTo point: CGPoint) -> [Waypoint]? { let tapCoordinate = convert(point, toCoordinateFrom: self) - let multipointRoutes = routes.filter { $0.routeOptions.waypoints.count >= 3} + let multipointRoutes = routes.filter { $0.legs.count > 1} guard multipointRoutes.count > 0 else { return nil } - let waypoints = multipointRoutes.flatMap({$0.routeOptions.waypoints}) + let waypoints = multipointRoutes.compactMap { route in + route.legs.dropLast().compactMap { $0.destination } + }.flatMap {$0} //lets sort the array in order of closest to tap let closest = waypoints.sorted { (left, right) -> Bool in diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index d3fd51b813..929a99b5b8 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -33,12 +33,17 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter } set { navigationService.route = newValue - NavigationSettings.shared.distanceUnit = route.routeOptions.locale.usesMetric ? .kilometer : .mile navigationComponents.forEach { $0.navigationService(navigationService, didRerouteAlong: newValue, at: nil, proactive: false) } } } + public var routeOptions: RouteOptions { + get { + return navigationService.routeProgress.routeOptions + } + } + /** An instance of `Directions` need for rerouting. See [Mapbox Directions](https://mapbox.github.io/mapbox-navigation-ios/directions/) for further information. */ @@ -201,28 +206,29 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter - parameter route: The route to navigate along. - parameter options: The navigation options to use for the navigation session. */ - required public init(for route: Route, - options: NavigationOptions? = nil) { + required public init(for route: Route, routeOptions: RouteOptions, + navigationOptions: NavigationOptions? = nil) { super.init(nibName: nil, bundle: nil) - self.navigationService = options?.navigationService ?? MapboxNavigationService(route: route) + self.navigationService = navigationOptions?.navigationService ?? MapboxNavigationService(route: route, routeOptions: routeOptions) self.navigationService.delegate = self - self.voiceController = options?.voiceController ?? MapboxVoiceController(navigationService: navigationService, speechClient: SpeechSynthesizer(accessToken: navigationService?.directions.accessToken, host: navigationService?.directions.apiEndpoint.host)) + let credentials = navigationService.directions.credentials + self.voiceController = navigationOptions?.voiceController ?? MapboxVoiceController(navigationService: navigationService, speechClient: SpeechSynthesizer(accessToken: credentials.accessToken, host: credentials.host.absoluteString)) - NavigationSettings.shared.distanceUnit = route.routeOptions.locale.usesMetric ? .kilometer : .mile + NavigationSettings.shared.distanceUnit = routeOptions.locale.usesMetric ? .kilometer : .mile styleManager = StyleManager() styleManager.delegate = self - styleManager.styles = options?.styles ?? [DayStyle(), NightStyle()] + styleManager.styles = navigationOptions?.styles ?? [DayStyle(), NightStyle()] - let bottomBanner = options?.bottomBanner ?? { + let bottomBanner = navigationOptions?.bottomBanner ?? { let viewController: BottomBannerViewController = .init() viewController.delegate = self return viewController }() bottomViewController = bottomBanner - if let customBanner = options?.topBanner { + if let customBanner = navigationOptions?.topBanner { topViewController = customBanner } else { let defaultBanner = TopBannerViewController(nibName: nil, bundle: nil) @@ -249,7 +255,7 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter mapViewController.view.pinInSuperview() mapViewController.reportButton.isHidden = !showsReportFeedback - if !(route.routeOptions is NavigationRouteOptions) { + if !(routeOptions is NavigationRouteOptions) { print("`Route` was created using `RouteOptions` and not `NavigationRouteOptions`. Although not required, this may lead to a suboptimal navigation experience. Without `NavigationRouteOptions`, it is not guaranteed you will get congestion along the route line, better ETAs and ETA label color dependent on congestion.") } } @@ -258,11 +264,12 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter Initializes a navigation view controller with the given route and navigation service. - parameter route: The route to navigate along. + - parameter routeOptions: the options object used to generate the route. - parameter navigationService: The navigation service that manages navigation along the route. */ - convenience init(route: Route, navigationService service: NavigationService) { + convenience init(route: Route, routeOptions: RouteOptions, navigationService service: NavigationService) { let options = NavigationOptions(navigationService: service) - self.init(for: route, options: options) + self.init(for: route, routeOptions: routeOptions, navigationOptions: options) } deinit { diff --git a/MapboxNavigationTests/CarPlayManagerTests.swift b/MapboxNavigationTests/CarPlayManagerTests.swift index 3d0b40411d..2cce34cbdb 100644 --- a/MapboxNavigationTests/CarPlayManagerTests.swift +++ b/MapboxNavigationTests/CarPlayManagerTests.swift @@ -195,10 +195,11 @@ class CarPlayManagerTests: XCTestCase { // given the user is previewing route choices // when a trip is started using one of the route choices let choice = CPRouteChoice(summaryVariants: ["summary1"], additionalInformationVariants: ["addl1"], selectionSummaryVariants: ["selection1"]) - choice.userInfo = Fixture.route(from: "route-with-banner-instructions", options: NavigationRouteOptions(coordinates: [ + let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 37.764793, longitude: -122.463161), CLLocationCoordinate2D(latitude: 34.054081, longitude: -118.243412), - ])) + ]) + choice.userInfo = (Fixture.route(from: "route-with-banner-instructions", options: options), options) manager.mapTemplate(mapTemplate, startedTrip: CPTrip(origin: MKMapItem(), destination: MKMapItem(), routeChoices: [choice]), using: choice) @@ -223,7 +224,7 @@ class CarPlayManagerTests: XCTestCase { let locOne = CLLocationCoordinate2D(latitude: 0, longitude: 0) let fakeOptions = RouteOptions(coordinates: [locOne]) manager.delegate = spy - manager.didCalculate(nil, for: fakeOptions, between: nil, error: testError, completionHandler: { }) + manager.didCalculate(.failure(testError), in: (options: fakeOptions, credentials: Fixture.credentials), for: fakeOptions, completionHandler: { }) XCTAssert(spy.recievedError == testError, "Delegate should have receieved error") } @@ -240,7 +241,7 @@ class CarPlayManagerTests: XCTestCase { } let expectation = XCTestExpectation(description: "Ensuring Spy is called") - let spy = DirectionsInvocationSpy(accessToken: "DeadBeefCafe", host: nil) + let spy = DirectionsInvocationSpy() spy.payload = expectation.fulfill let subject = CarPlayManager(directions: spy) @@ -248,7 +249,7 @@ class CarPlayManagerTests: XCTestCase { 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 }) + subject.calculate(options, completionHandler: { _, _ in }) wait(for: [expectation], timeout: 1.0) XCTAssert(subject.directions == spy, "Directions client is not overridden properly.") @@ -280,7 +281,7 @@ class CarPlayManagerSpec: QuickSpec { var delegate: TestCarPlayManagerDelegate? beforeEach { - let directionsSpy = DirectionsSpy(accessToken: "asdf") + let directionsSpy = DirectionsSpy() manager = CarPlayManager(styles: nil, directions: directionsSpy, eventsManager: nil) delegate = TestCarPlayManagerDelegate() manager!.delegate = delegate @@ -295,15 +296,16 @@ class CarPlayManagerSpec: QuickSpec { } let previewRoutesAction = { - let route = Fixture.route(from: "route-with-banner-instructions", options: NavigationRouteOptions(coordinates: [ + let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 37.764793, longitude: -122.463161), CLLocationCoordinate2D(latitude: 34.054081, longitude: -118.243412), - ])) - let waypoints = route.routeOptions.waypoints + ]) + let route = Fixture.route(from: "route-with-banner-instructions", options: options) + let waypoints = options.waypoints let directionsSpy = manager!.directions as! DirectionsSpy - manager!.previewRoutes(for: route.routeOptions, completionHandler: {}) + manager!.previewRoutes(for: options, completionHandler: {}) directionsSpy.fireLastCalculateCompletion(with: waypoints, routes: [route], error: nil) } @@ -371,10 +373,11 @@ class CarPlayManagerSpec: QuickSpec { let action = { let fakeTemplate = CPMapTemplate() let fakeRouteChoice = CPRouteChoice(summaryVariants: ["summary1"], additionalInformationVariants: ["addl1"], selectionSummaryVariants: ["selection1"]) - fakeRouteChoice.userInfo = Fixture.route(from: "route-with-banner-instructions", options: NavigationRouteOptions(coordinates: [ + let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 37.764793, longitude: -122.463161), CLLocationCoordinate2D(latitude: 34.054081, longitude: -118.243412), - ])) + ]) + fakeRouteChoice.userInfo = (Fixture.route(from: "route-with-banner-instructions", options: options), options) let fakeTrip = CPTrip(origin: MKMapItem(), destination: MKMapItem(), routeChoices: [fakeRouteChoice]) //simulate starting a fake trip @@ -436,9 +439,10 @@ class CarPlayManagerSpec: QuickSpec { //no-op } - func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, desiredSimulationMode: SimulationMode) -> NavigationService { - let directionsFake = Directions(accessToken: "foo") - return MapboxNavigationService(route: route, directions: directionsFake, simulating: desiredSimulationMode) + //TODO: ADD OPTIONS TO THIS DELEGATE METHOD + func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, routeOptions: RouteOptions, desiredSimulationMode: SimulationMode) -> NavigationService { + let directionsFake = Directions(credentials: Fixture.credentials) + return MapboxNavigationService(route: route, routeOptions: routeOptions, directions: directionsFake, simulating: desiredSimulationMode) } } } @@ -464,7 +468,7 @@ class CarPlayManagerFailureDelegateSpy: CarPlayManagerDelegate { return nil } - func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, desiredSimulationMode: SimulationMode) -> NavigationService { + func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, routeOptions: RouteOptions, desiredSimulationMode: SimulationMode) -> NavigationService { fatalError("This is an empty stub.") } @@ -491,12 +495,11 @@ class TestCarPlayManagerDelegate: CarPlayManagerDelegate { public var trailingBarButtons: [CPBarButton]? public var mapButtons: [CPMapButton]? - func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, desiredSimulationMode: SimulationMode) -> NavigationService { + func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, routeOptions: RouteOptions, desiredSimulationMode: SimulationMode) -> NavigationService { let response = Fixture.routeResponse(from: jsonFileName, options: routeOptions) let initialRoute = response.routes!.first! - initialRoute.accessToken = "deadbeef" - let directionsClientSpy = DirectionsSpy(accessToken: "garbage", host: nil) - let service = MapboxNavigationService(route: initialRoute, directions: directionsClientSpy, locationSource: NavigationLocationManager(), eventsManagerType: NavigationEventsManagerSpy.self, simulating: desiredSimulationMode) + let directionsClientSpy = DirectionsSpy() + let service = MapboxNavigationService(route: initialRoute, routeOptions: routeOptions, directions: directionsClientSpy, locationSource: NavigationLocationManager(), eventsManagerType: NavigationEventsManagerSpy.self, simulating: desiredSimulationMode) return service } diff --git a/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift b/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift index 6b9fe7243e..e0a75f2043 100644 --- a/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift +++ b/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift @@ -47,14 +47,15 @@ fileprivate class CPNavigationSessionFake: CPNavigationSession { fileprivate class CarPlayNavigationViewControllerTests: XCTestCase { func testCarplayDisplaysCorrectEstimates() { //set up the litany of dependancies - let directions = Directions(accessToken: "fafedeadbeef") + let directions = Directions(credentials: Fixture.credentials) let manager = CarPlayManager(directions: directions) - let route = Fixture.route(from: "multileg-route", options: NavigationRouteOptions(coordinates: [ + let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 9.519172, longitude: 47.210823), CLLocationCoordinate2D(latitude: 9.52222, longitude: 47.214268), CLLocationCoordinate2D(latitude: 47.212326, longitude: 9.512569), - ])) - let navService = MapboxNavigationService(route: route) + ]) + let route = Fixture.route(from: "multileg-route", options: options) + let navService = MapboxNavigationService(route: route, routeOptions: options) let interface = FakeCPInterfaceController("test estimates display") let mapSpy = MapTemplateSpy() let trip = CPTrip(origin: MKMapItem(), destination: MKMapItem(), routeChoices: []) diff --git a/MapboxNavigationTests/GuidanceCardsSnapshotTests.swift b/MapboxNavigationTests/GuidanceCardsSnapshotTests.swift index da8cd42668..37a005ff7c 100644 --- a/MapboxNavigationTests/GuidanceCardsSnapshotTests.swift +++ b/MapboxNavigationTests/GuidanceCardsSnapshotTests.swift @@ -33,7 +33,7 @@ class GuidanceCardsSnapshotTests: SnapshotTest { return cards.view.constraintsForPinning(to: container) } - let progress = RouteProgress(route: route, legIndex: 0, spokenInstructionIndex: 0) + let progress = RouteProgress(route: route, options: tertiaryRouteOptions, legIndex: 0, spokenInstructionIndex: 0) subject.routeProgress = progress @@ -55,7 +55,7 @@ class GuidanceCardsSnapshotTests: SnapshotTest { return cards.view.constraintsForPinning(to: container) } - let progress = RouteProgress(route: route, legIndex: 0, spokenInstructionIndex: 0) + let progress = RouteProgress(route: route, options: tertiaryRouteOptions, legIndex: 0, spokenInstructionIndex: 0) progress.currentLegProgress.stepIndex = 1 subject.routeProgress = progress @@ -78,7 +78,7 @@ class GuidanceCardsSnapshotTests: SnapshotTest { return cards.view.constraintsForPinning(to: container) } - let progress = RouteProgress(route: route, legIndex: 0, spokenInstructionIndex: 0) + let progress = RouteProgress(route: route, options: tertiaryRouteOptions, legIndex: 0, spokenInstructionIndex: 0) progress.currentLegProgress.stepIndex = 5 subject.routeProgress = progress diff --git a/MapboxNavigationTests/InstructionsCardCollectionTests.swift b/MapboxNavigationTests/InstructionsCardCollectionTests.swift index ade00f119a..b6444decd7 100644 --- a/MapboxNavigationTests/InstructionsCardCollectionTests.swift +++ b/MapboxNavigationTests/InstructionsCardCollectionTests.swift @@ -25,13 +25,14 @@ class InstructionsCardCollectionTests: XCTestCase { return guidanceCard.view.constraintsForPinning(to: container) } - let fakeRoute = Fixture.route(from: "route-with-banner-instructions", options: NavigationRouteOptions(coordinates: [ + let fakeOptions = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 37.764793, longitude: -122.463161), CLLocationCoordinate2D(latitude: 34.054081, longitude: -118.243412), - ])) + ]) + let fakeRoute = Fixture.route(from: "route-with-banner-instructions", options: fakeOptions) - let service = MapboxNavigationService(route: initialRoute, directions: DirectionsSpy(accessToken: "adbeknut"), simulating: .never) - let routeProgress = RouteProgress(route: fakeRoute) + let service = MapboxNavigationService(route: initialRoute, routeOptions: fakeOptions, directions: DirectionsSpy(), simulating: .never) + let routeProgress = RouteProgress(route: fakeRoute, options: fakeOptions) subject.routeProgress = routeProgress return (collection: subject, progress: routeProgress, service: service, delegate: delegate) diff --git a/MapboxNavigationTests/LeaksSpec.swift b/MapboxNavigationTests/LeaksSpec.swift index 6654f586cb..037a09fae4 100644 --- a/MapboxNavigationTests/LeaksSpec.swift +++ b/MapboxNavigationTests/LeaksSpec.swift @@ -9,12 +9,18 @@ import MapboxDirections class LeaksSpec: QuickSpec { lazy var initialRoute: Route = { let route = response.routes!.first! - route.accessToken = "foo" return route }() - lazy var dummySvc: NavigationService = MapboxNavigationService(route: self.initialRoute) + lazy var initialOptions: RouteOptions = { + guard case let .route(options) = response.options else { + preconditionFailure("expecting route options") + } + return options + }() + + lazy var dummySvc: NavigationService = MapboxNavigationService(route: self.initialRoute, routeOptions: initialOptions) override func spec() { describe("RouteVoiceController") { @@ -35,10 +41,12 @@ class LeaksSpec: QuickSpec { let route = initialRoute let navigationViewController = LeakTest { - let directions = DirectionsSpy(accessToken: "deadbeef") - let service = MapboxNavigationService(route: route, directions: directions, eventsManagerType: NavigationEventsManagerSpy.self) - let options = NavigationOptions(navigationService: service, voiceController: RouteVoiceControllerStub(navigationService: self.dummySvc)) - return NavigationViewController(for: route, options: options) + let directions = DirectionsSpy(credentials: Fixture.credentials) + let service = MapboxNavigationService(route: route, routeOptions: self.initialOptions, directions: directions, eventsManagerType: NavigationEventsManagerSpy.self) + let navOptions = NavigationOptions(navigationService: service, voiceController: RouteVoiceControllerStub(navigationService: self.dummySvc)) + + + return NavigationViewController(for: route, routeOptions: self.initialOptions, navigationOptions: navOptions) } it("must not leak") { diff --git a/MapboxNavigationTests/MapboxVoiceControllerTests.swift b/MapboxNavigationTests/MapboxVoiceControllerTests.swift index 344c869ebe..5618490d3c 100644 --- a/MapboxNavigationTests/MapboxVoiceControllerTests.swift +++ b/MapboxNavigationTests/MapboxVoiceControllerTests.swift @@ -11,12 +11,14 @@ class MapboxVoiceControllerTests: XCTestCase { var route: Route { get { - return Fixture.route(from: "route-with-instructions", options: NavigationRouteOptions(coordinates: [ - CLLocationCoordinate2D(latitude: 40.311012, longitude: -112.47926), - CLLocationCoordinate2D(latitude: 29.99908, longitude: -102.828197), - ])) + return Fixture.route(from: "route-with-instructions", options: routeOptions) } } + + let routeOptions: RouteOptions = NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 40.311012, longitude: -112.47926), + CLLocationCoordinate2D(latitude: 29.99908, longitude: -102.828197), + ]) override func setUp() { super.setUp() @@ -35,7 +37,7 @@ class MapboxVoiceControllerTests: XCTestCase { } func testControllerDownloadsAndCachesInstructionDataWhenNotified() { - let service = MapboxNavigationService(route: route) + let service = MapboxNavigationService(route: route, routeOptions: routeOptions) let subject = MapboxVoiceController(navigationService: service, speechClient: speechAPISpy, audioPlayerType: AudioPlayerDummy.self) let userInfo = [ RouteController.NotificationUserInfoKey.routeProgressKey: service.routeProgress, @@ -57,7 +59,7 @@ class MapboxVoiceControllerTests: XCTestCase { } func testVoiceDeinit() { - let dummyService = MapboxNavigationService(route: route) + let dummyService = MapboxNavigationService(route: route, routeOptions: routeOptions) var voiceController: MockMapboxVoiceController? = MockMapboxVoiceController(navigationService: dummyService) let deinitExpectation = expectation(description: "Voice Controller should deinitialize") voiceController!.deinitExpectation = deinitExpectation @@ -67,7 +69,7 @@ class MapboxVoiceControllerTests: XCTestCase { func testAudioCalls() { typealias Note = Notification.Name.MapboxVoiceTests - let service = MapboxNavigationService(route: route) + let service = MapboxNavigationService(route: route, routeOptions: routeOptions) service.routeProgress.currentLegProgress.currentStepProgress.spokenInstructionIndex = 1 let routeProgress = service.routeProgress @@ -92,13 +94,14 @@ class MapboxVoiceControllerTests: XCTestCase { } func testAccessTokenPropagatesFromNavigationViewController() { - let directions = DirectionsSpy(accessToken: "foo") - let service = MapboxNavigationService(route: route, directions: directions) + let directions = DirectionsSpy() + let service = MapboxNavigationService(route: route, routeOptions: routeOptions, directions: directions) + let options = NavigationOptions(navigationService: service) - let nvc = NavigationViewController(for: route, options: options) + let nvc = NavigationViewController(for: route, routeOptions: routeOptions, navigationOptions: options) let voiceController = nvc.voiceController as! MapboxVoiceController - XCTAssertEqual(voiceController.speech.accessToken, "foo", + XCTAssertEqual(voiceController.speech.accessToken, "deadbeef", "Access token should propagate from NavigationViewController to SpeechSynthesizer") } } diff --git a/MapboxNavigationTests/NavigationMapViewTests.swift b/MapboxNavigationTests/NavigationMapViewTests.swift index 96f7cc50bf..622d780bf5 100644 --- a/MapboxNavigationTests/NavigationMapViewTests.swift +++ b/MapboxNavigationTests/NavigationMapViewTests.swift @@ -14,10 +14,11 @@ class NavigationMapViewTests: XCTestCase, MGLMapViewDelegate { lazy var route: Route = { let route = response.routes!.first! - route.accessToken = "foo" return route }() + + override func setUp() { super.setUp() diff --git a/MapboxNavigationTests/NavigationViewControllerTests.swift b/MapboxNavigationTests/NavigationViewControllerTests.swift index cfde418e90..cc11218276 100644 --- a/MapboxNavigationTests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/NavigationViewControllerTests.swift @@ -19,11 +19,11 @@ class NavigationViewControllerTests: XCTestCase { var updatedStyleNumberOfTimes = 0 lazy var dependencies: (navigationViewController: NavigationViewController, navigationService: NavigationService, startLocation: CLLocation, poi: [CLLocation], endLocation: CLLocation, voice: RouteVoiceController) = { - let fakeDirections = DirectionsSpy(accessToken: "garbage", host: nil) - let fakeService = MapboxNavigationService(route: initialRoute, directions: fakeDirections, locationSource: NavigationLocationManagerStub(), simulating: .never) + let fakeDirections = DirectionsSpy() + let fakeService = MapboxNavigationService(route: initialRoute, routeOptions: routeOptions, directions: fakeDirections, locationSource: NavigationLocationManagerStub(), simulating: .never) let fakeVoice: RouteVoiceController = RouteVoiceControllerStub(navigationService: fakeService) let options = NavigationOptions(navigationService: fakeService, voiceController: fakeVoice) - let navigationViewController = NavigationViewController(for: initialRoute, options: options) + let navigationViewController = NavigationViewController(for: initialRoute, routeOptions: routeOptions, navigationOptions: options) navigationViewController.delegate = self @@ -81,7 +81,7 @@ class NavigationViewControllerTests: XCTestCase { func testNavigationShouldNotCallStyleManagerDidRefreshAppearanceMoreThanOnceWithOneStyle() { let options = NavigationOptions(styles: [DayStyle()], navigationService: dependencies.navigationService, voiceController: dependencies.voice) - let navigationViewController = NavigationViewController(for: initialRoute, options: options) + let navigationViewController = NavigationViewController(for: initialRoute, routeOptions: routeOptions, navigationOptions: options) let service = dependencies.navigationService navigationViewController.styleManager.delegate = self @@ -123,7 +123,7 @@ class NavigationViewControllerTests: XCTestCase { // If tunnel flags are enabled and we need to switch styles, we should not force refresh the map style because we have only 1 style. func testNavigationShouldNotCallStyleManagerDidRefreshAppearanceWhenOnlyOneStyle() { let options = NavigationOptions(styles:[NightStyle()], navigationService: dependencies.navigationService, voiceController: dependencies.voice) - let navigationViewController = NavigationViewController(for: initialRoute, options: options) + let navigationViewController = NavigationViewController(for: initialRoute, routeOptions: routeOptions, navigationOptions: options) let service = dependencies.navigationService navigationViewController.styleManager.delegate = self @@ -139,7 +139,7 @@ class NavigationViewControllerTests: XCTestCase { func testNavigationShouldNotCallStyleManagerDidRefreshAppearanceMoreThanOnceWithTwoStyles() { let options = NavigationOptions(styles: [DayStyle(), NightStyle()], navigationService: dependencies.navigationService, voiceController: dependencies.voice) - let navigationViewController = NavigationViewController(for: initialRoute, options: options) + let navigationViewController = NavigationViewController(for: initialRoute, routeOptions: routeOptions, navigationOptions: options) let service = dependencies.navigationService navigationViewController.styleManager.delegate = self @@ -192,9 +192,9 @@ class NavigationViewControllerTests: XCTestCase { } func testDestinationAnnotationUpdatesUponReroute() { - let service = MapboxNavigationService(route: initialRoute, directions: DirectionsSpy(accessToken: "beef"), simulating: .never) + let service = MapboxNavigationService(route: initialRoute, routeOptions: routeOptions, directions: DirectionsSpy(), simulating: .never) let options = NavigationOptions(styles: [TestableDayStyle()], navigationService: service) - let navigationViewController = NavigationViewController(for: initialRoute, options: options) + let navigationViewController = NavigationViewController(for: initialRoute, routeOptions: routeOptions, navigationOptions: options) let styleLoaded = keyValueObservingExpectation(for: navigationViewController, keyPath: "mapView.style", expectedValue: nil) //wait for the style to load -- routes won't show without it. @@ -209,14 +209,14 @@ class NavigationViewControllerTests: XCTestCase { return XCTFail("No point annotations found.") } - let firstDestination = initialRoute.routeOptions.waypoints.last!.coordinate + let firstDestination = initialRoute.legs.last!.destination!.coordinate XCTAssert(annotations.contains { $0.coordinate.distance(to: firstDestination) < 1 }, "Destination annotation does not exist on map") //lets set the second route navigationViewController.route = newRoute guard let newAnnotations = navigationViewController.mapView?.annotations else { return XCTFail("New annotations not found.")} - let secondDestination = newRoute.routeOptions.waypoints.last!.coordinate + let secondDestination = newRoute.legs.last!.destination!.coordinate //do we have a destination on the second route? XCTAssert(newAnnotations.contains { $0.coordinate.distance(to: secondDestination) < 1 }, "New destination annotation does not exist on map") @@ -226,11 +226,13 @@ class NavigationViewControllerTests: XCTestCase { let window = UIApplication.shared.keyWindow! let viewController = window.rootViewController! - let route = Fixture.route(from: "DCA-Arboretum", options: NavigationRouteOptions(coordinates: [ + let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 38.853108, longitude: -77.043331), CLLocationCoordinate2D(latitude: 38.910736, longitude: -76.966906), - ])) - let navigationViewController = NavigationViewController(for: route) + ]) + + let route = Fixture.route(from: "DCA-Arboretum", options: options) + let navigationViewController = NavigationViewController(for: route, routeOptions: options) viewController.present(navigationViewController, animated: false, completion: nil) @@ -249,13 +251,15 @@ class NavigationViewControllerTests: XCTestCase { let top = TopBannerFake(nibName: nil, bundle: nil) let bottom = BottomBannerFake(nibName: nil, bundle: nil) - let fakeOptions = NavigationOptions(topBanner: top, bottomBanner: bottom) - let route = Fixture.route(from: "DCA-Arboretum", options: NavigationRouteOptions(coordinates: [ + let navOptions = NavigationOptions(topBanner: top, bottomBanner: bottom) + + let options = NavigationRouteOptions(coordinates: [ CLLocationCoordinate2D(latitude: 38.853108, longitude: -77.043331), CLLocationCoordinate2D(latitude: 38.910736, longitude: -76.966906), - ])) + ]) + let route = Fixture.route(from: "DCA-Arboretum", options: options) - let subject = NavigationViewController(for: route, options: fakeOptions) + let subject = NavigationViewController(for: route, routeOptions: options, navigationOptions: navOptions) XCTAssert(subject.topViewController == top, "Top banner not injected properly into NVC") XCTAssert(subject.bottomViewController == bottom, "Bottom banner not injected properly into NVC") XCTAssert(subject.mapViewController!.children.contains(top), "Top banner not found in child VC heirarchy") diff --git a/MapboxNavigationTests/RouteControllerSnapshotTests.swift b/MapboxNavigationTests/RouteControllerSnapshotTests.swift index d3586ca856..274793ad3c 100644 --- a/MapboxNavigationTests/RouteControllerSnapshotTests.swift +++ b/MapboxNavigationTests/RouteControllerSnapshotTests.swift @@ -21,7 +21,7 @@ class RouteControllerSnapshotTests: FBSnapshotTestCase { } func testRouteSnappingOvershooting() { - let route = Fixture.routesFromMatches(at: "sthlm-double-back", options: NavigationMatchOptions(coordinates: [ + let options = NavigationMatchOptions(coordinates: [ .init(latitude: 59.337928, longitude: 18.076841), .init(latitude: 59.337661, longitude: 18.075897), .init(latitude: 59.337129, longitude: 18.075478), @@ -35,7 +35,8 @@ class RouteControllerSnapshotTests: FBSnapshotTestCase { .init(latitude: 59.338156, longitude: 18.075723), .init(latitude: 59.338311, longitude: 18.074968), .init(latitude: 59.33865, longitude: 18.074935), - ]))![0] + ]) + let route = Fixture.routesFromMatches(at: "sthlm-double-back", options: options)![0] let bundle = Bundle(for: RouteControllerSnapshotTests.self) let filePath = bundle.path(forResource: "sthlm-double-back-replay", ofType: "json") @@ -44,7 +45,8 @@ class RouteControllerSnapshotTests: FBSnapshotTestCase { let locationManager = ReplayLocationManager(locations: locations) replayManager = locationManager locationManager.startDate = Date() - let routeController = RouteController(along: route, dataSource: self) + let equivalentRouteOptions = NavigationRouteOptions(navigationMatchOptions: options) + let routeController = RouteController(along: route, options: equivalentRouteOptions, dataSource: self) locationManager.delegate = routeController var snappedLocations = [CLLocation]() diff --git a/MapboxNavigationTests/RouteTests.swift b/MapboxNavigationTests/RouteTests.swift index 32df09dba2..88cfb69af3 100644 --- a/MapboxNavigationTests/RouteTests.swift +++ b/MapboxNavigationTests/RouteTests.swift @@ -53,7 +53,7 @@ class RouteTests: XCTestCase { ], profileIdentifier: .automobile) options.shapeFormat = .polyline let response = Fixture.mapMatchingResponse(from: "route-doubling-back", options: options) - let routes = response.routes + let routes = response.matches let route = routes!.first! let leg = route.legs.first! diff --git a/MapboxNavigationTests/StepsViewControllerTests.swift b/MapboxNavigationTests/StepsViewControllerTests.swift index 412947b701..bdf5fac743 100644 --- a/MapboxNavigationTests/StepsViewControllerTests.swift +++ b/MapboxNavigationTests/StepsViewControllerTests.swift @@ -7,15 +7,16 @@ import MapboxDirections class StepsViewControllerTests: XCTestCase { struct Constants { static let route = response.routes!.first! - static let accessToken = "nonsense" + static let options = routeOptions + static let credentials = Fixture.credentials } lazy var dependencies: (stepsViewController: StepsViewController, routeController: RouteController, firstLocation: CLLocation, lastLocation: CLLocation) = { let bogusToken = "pk.feedCafeDeadBeefBadeBede" - let directions = Directions(accessToken: bogusToken) + let directions = Directions(credentials: Fixture.credentials) let dataSource = RouteControllerDataSourceFake() - let routeController = RouteController(along: initialRoute, directions: directions, dataSource: dataSource) + let routeController = RouteController(along: Constants.route, options: Constants.options, directions: directions, dataSource: dataSource) let stepsViewController = StepsViewController(routeProgress: routeController.routeProgress) @@ -28,11 +29,6 @@ class StepsViewControllerTests: XCTestCase { return (stepsViewController: stepsViewController, routeController: routeController, firstLocation: firstLocation, lastLocation: lastLocation) }() - lazy var initialRoute: Route = { - let route = Constants.route - route.accessToken = "nonsense" - return route - }() func testRebuildStepsInstructionsViewDataSource() { let stepsViewController = dependencies.stepsViewController diff --git a/TestHelper/DirectionsSpy.swift b/TestHelper/DirectionsSpy.swift index 85d8e91dc2..ba440a7fb4 100644 --- a/TestHelper/DirectionsSpy.swift +++ b/TestHelper/DirectionsSpy.swift @@ -20,12 +20,26 @@ public class DirectionsSpy: Directions { } public func fireLastCalculateCompletion(with waypoints: [Waypoint]?, routes: [Route]?, error: DirectionsError?) { + let wpts = waypoints ?? [] + let options = RouteOptions(waypoints: wpts) + + let session: Directions.Session = (options: options, credentials: credentials) guard let lastCalculateOptionsCompletion = lastCalculateOptionsCompletion else { assert(false, "Can't fire a completion handler which doesn't exist!") return } - lastCalculateOptionsCompletion(waypoints, routes, error) + if let error = error { + lastCalculateOptionsCompletion(session, .failure(error)) + } else { + let response = RouteResponse(httpResponse: nil, routes: routes, waypoints: waypoints, options: .route(options), credentials: credentials) + + lastCalculateOptionsCompletion(session, .success(response)) + } +} + + public convenience init() { + self.init(credentials: Fixture.credentials) } public func reset() { diff --git a/TestHelper/Fixture.swift b/TestHelper/Fixture.swift index 7ead20e2c7..c598ac2c9b 100644 --- a/TestHelper/Fixture.swift +++ b/TestHelper/Fixture.swift @@ -29,15 +29,18 @@ public class Fixture: NSObject { public class func downloadRouteFixture(coordinates: [CLLocationCoordinate2D], fileName: String, completion: @escaping () -> Void) { let accessToken = "<# Mapbox Access Token #>" - let directions = Directions(accessToken: accessToken) + let credentials = DirectionsCredentials(accessToken: accessToken) + let directions = Directions(credentials: credentials) let options = RouteOptions(coordinates: coordinates, profileIdentifier: .automobileAvoidingTraffic) options.includesSteps = true options.routeShapeResolution = .full let filePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(fileName) - _ = directions.calculate(options, completionHandler: { (waypoints, routes, error) in - guard let _ = routes?.first else { return } + _ = directions.calculate(options, completionHandler: { (session, result) in + guard case let .success(response) = result else { return } + + guard let routes = response.routes, !routes.isEmpty else { return } print("Route downloaded to \(filePath)") completion() }) @@ -68,6 +71,7 @@ public class Fixture: NSObject { do { let decoder = JSONDecoder() decoder.userInfo[.options] = options + decoder.userInfo[.credentials] = Fixture.credentials return try decoder.decode(RouteResponse.self, from: responseData) } catch { preconditionFailure("Unable to decode JSON fixture: \(error)") @@ -79,6 +83,7 @@ public class Fixture: NSObject { do { let decoder = JSONDecoder() decoder.userInfo[.options] = options + decoder.userInfo[.credentials] = Fixture.credentials return try decoder.decode(MapMatchingResponse.self, from: responseData) } catch { preconditionFailure("Unable to decode JSON fixture: \(error)") @@ -92,7 +97,7 @@ public class Fixture: NSObject { } // Like `Directions.postprocess(_:fetchStartDate:uuid:)` - route.routeIdentifier = response.uuid + route.routeIdentifier = response.identifier let fetchStartDate = Date(timeIntervalSince1970: 3600) route.fetchStartDate = fetchStartDate route.responseEndDate = Date(timeInterval: 1, since: fetchStartDate) @@ -111,7 +116,8 @@ public class Fixture: NSObject { // Returns `Route` objects from a match response public class func routesFromMatches(at filePath: String, options: MatchOptions) -> [Route]? { let response = mapMatchingResponse(from: filePath, options: options) - guard let routes = response.routes else { + let routeResponse = try! RouteResponse(matching: response, options: options, credentials: Fixture.credentials) + guard let routes = routeResponse.routes else { preconditionFailure("No routes") } return routes @@ -129,6 +135,9 @@ public class Fixture: NSObject { return traceCollector.locations } + + public static let credentials: DirectionsCredentials = DirectionsCredentials(accessToken: "deadbeef", host: URL(string: "https://example.com")!) + } class TraceCollector: NSObject, CLLocationManagerDelegate {