diff --git a/CHANGELOG.md b/CHANGELOG.md index 55cc241b880..7f88918fc91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ ### Other changes * Since pure Swift protocols cannot have optional methods, various delegate protocols now provide default no-op implementations for all their methods and conform to the `UnimplementedLogging` protocol, which can inform you at runtime when a delegate method is called but has not been implemented. Messages are sent through Apple Unified Logging and can be disabled globally through [Unifed Logging](https://developer.apple.com/documentation/os/logging#2878594), or by overriding the delegate function with a no-op implementation. ([#2230](https://github.com/mapbox/mapbox-navigation-ios/pull/2230)) +* Renamed `RouteProgress.nearbyCoordinates` to `RouteProgress.nearbyShape`. ([#2275](https://github.com/mapbox/mapbox-navigation-ios/pull/2275)) +* Removed `RouteLegProgress.nearbyCoordinates`. ([#2275](https://github.com/mapbox/mapbox-navigation-ios/pull/2275)) ## v0.38.1 diff --git a/Cartfile b/Cartfile index 1adbd0c686e..2229e4e09a6 100644 --- a/Cartfile +++ b/Cartfile @@ -1,6 +1,6 @@ binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" ~> 5.2 binary "https://www.mapbox.com/ios-sdk/MapboxNavigationNative.json" ~> 6.2.1 -github "mapbox/MapboxDirections.swift" ~> 0.30.0 +github "mapbox/MapboxDirections.swift" "jerrad/delenda-duet-dependencies" #~> 0.30.0 github "mapbox/turf-swift" ~> 0.3 github "mapbox/mapbox-events-ios" ~> 0.9.5 github "ceeK/Solar" ~> 2.1.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index fee82c1017c..5100a8b13b6 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -5,7 +5,7 @@ github "CedarBDD/Cedar" "v1.0" github "Quick/Nimble" "v8.0.4" github "Quick/Quick" "v2.2.0" github "ceeK/Solar" "2.1.0" -github "mapbox/MapboxDirections.swift" "v0.30.0" +github "mapbox/MapboxDirections.swift" "472cbced199ef2d9c5c2fa49a21e8f6b249e9bf8" github "mapbox/MapboxGeocoder.swift" "v0.10.2" github "mapbox/mapbox-events-ios" "v0.9.5" github "mapbox/mapbox-speech-swift" "v0.1.1" diff --git a/Example/AppDelegate+CarPlay.swift b/Example/AppDelegate+CarPlay.swift index bbc5c8c3cb1..c6e2b35122f 100644 --- a/Example/AppDelegate+CarPlay.swift +++ b/Example/AppDelegate+CarPlay.swift @@ -90,11 +90,11 @@ extension AppDelegate: CarPlayManagerDelegate { } } - func carPlayManager(_ carPlayManager: CarPlayManager, didFailToFetchRouteBetween waypoints: [Waypoint]?, options: RouteOptions, error: NSError) -> CPNavigationAlert? { + func carPlayManager(_ carPlayManager: CarPlayManager, didFailToFetchRouteBetween waypoints: [Waypoint]?, options: RouteOptions, error: DirectionsError) -> CPNavigationAlert? { let okTitle = NSLocalizedString("CARPLAY_OK", bundle: .main, value: "OK", comment: "CPAlertTemplate OK button title") let action = CPAlertAction(title: okTitle, style: .default, handler: {_ in }) let alert = CPNavigationAlert(titleVariants: [error.localizedDescription], - subtitleVariants: [error.localizedFailureReason ?? ""], + subtitleVariants: [error.failureReason ?? ""], imageSet: nil, primaryAction: action, secondaryAction: nil, diff --git a/Example/ViewController.swift b/Example/ViewController.swift index 10a2f545522..70d3b12505f 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -6,7 +6,7 @@ import UserNotifications import AVKit private typealias RouteRequestSuccess = (([Route]) -> Void) -private typealias RouteRequestFailure = ((NSError) -> Void) +private typealias RouteRequestFailure = ((Error) -> Void) class ViewController: UIViewController { // MARK: - IBOutlets @@ -205,15 +205,13 @@ class ViewController: UIViewController { } fileprivate func requestRoute(with options: RouteOptions, success: @escaping RouteRequestSuccess, failure: RouteRequestFailure?) { - let handler: Directions.RouteCompletionHandler = { (waypoints, routes, error) in + // 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) } - - // Calculate route offline if an offline version is selected - let shouldUseOfflineRouting = Settings.selectedOfflineVersion != nil - Settings.directions.calculate(options, offline: shouldUseOfflineRouting, completionHandler: handler) } // MARK: Basic Navigation @@ -267,7 +265,7 @@ class ViewController: UIViewController { customViewController.userRoute = route let destination = MGLPointAnnotation() - destination.coordinate = route.coordinates!.last! + destination.coordinate = route.shape!.coordinates.last! customViewController.destination = destination customViewController.simulateLocation = simulationButton.isSelected @@ -363,8 +361,8 @@ extension ViewController: MGLMapViewDelegate { self.mapView?.localizeLabels() - if let routes = routes, let currentRoute = routes.first, let coords = currentRoute.coordinates { - mapView.setVisibleCoordinateBounds(MGLPolygon(coordinates: coords, count: currentRoute.coordinateCount).overlayBounds, animated: false) + if let routes = 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) } @@ -384,7 +382,7 @@ extension ViewController: NavigationMapViewDelegate { func navigationMapView(_ mapView: NavigationMapView, didSelect route: Route) { guard let routes = routes else { return } - guard let index = routes.firstIndex(where: { $0 == route }) else { return } + guard let index = routes.firstIndex(where: { $0 === route }) else { return } self.routes!.remove(at: index) self.routes!.insert(route, at: 0) } diff --git a/MapboxCoreNavigation/CLLocation.swift b/MapboxCoreNavigation/CLLocation.swift index 5d6a3f6e8f3..49996c727f2 100644 --- a/MapboxCoreNavigation/CLLocation.swift +++ b/MapboxCoreNavigation/CLLocation.swift @@ -40,7 +40,7 @@ extension CLLocation { Returns a Boolean value indicating whether the receiver is within a given distance of a route step. */ func isWithin(_ maximumDistance: CLLocationDistance, of routeStep: RouteStep) -> Bool { - guard let closestCoordinate = Polyline(routeStep.coordinates!).closestCoordinate(to: coordinate) else { + guard let shape = routeStep.shape, let closestCoordinate = shape.closestCoordinate(to: coordinate) else { return false } return closestCoordinate.distance < maximumDistance @@ -50,14 +50,14 @@ extension CLLocation { func snapped(to routeProgress: RouteProgress) -> CLLocation? { let legProgress = routeProgress.currentLegProgress - let coords = coordinates(for: routeProgress) + let polyline = snappingPolyline(for: routeProgress) - guard let closest = Polyline(coords).closestCoordinate(to: coordinate) else { return nil } - guard let calculatedCourseForLocationOnStep = interpolatedCourse(along: coords) else { return nil } + guard let closest = polyline.closestCoordinate(to: coordinate) else { return nil } + guard let calculatedCourseForLocationOnStep = interpolatedCourse(along: polyline) else { return nil } let userCourse = calculatedCourseForLocationOnStep let userCoordinate = closest.coordinate - guard let firstCoordinate = legProgress.leg.steps.first?.coordinates?.first else { return nil } + guard let firstCoordinate = legProgress.leg.steps.first?.shape?.coordinates.first else { return nil } guard shouldSnap(toRouteWith: calculatedCourseForLocationOnStep, distanceToFirstCoordinateOnLeg: self.coordinate.distance(to: firstCoordinate)) else { return nil } @@ -67,10 +67,10 @@ extension CLLocation { /** Calculates the proper coordinates to use when calculating a snapped location. */ - func coordinates(for routeProgress: RouteProgress) -> [CLLocationCoordinate2D] { + func snappingPolyline(for routeProgress: RouteProgress) -> Polyline { let legProgress = routeProgress.currentLegProgress - let nearbyCoordinates = routeProgress.nearbyCoordinates - let stepCoordinates = legProgress.currentStep.coordinates! + let nearbyPolyline = routeProgress.nearbyShape + let stepPolyline = legProgress.currentStep.shape! // If the upcoming maneuver a sharp turn, only look at the current step for snapping. // Otherwise, we may get false positives from nearby step coordinates @@ -79,37 +79,36 @@ extension CLLocation { let finalHeading = upcomingStep.finalHeading { // The max here is 180. The closer it is to 180, the sharper the turn. if initialHeading.clockwiseDifference(from: finalHeading) > 180 - RouteSnappingMaxManipulatedCourseAngle { - return stepCoordinates + return stepPolyline } if finalHeading.difference(from: course) > RouteControllerMaximumAllowedDegreeOffsetForTurnCompletion { - return stepCoordinates + return stepPolyline } } if speed <= RouteControllerMaximumSpeedForUsingCurrentStep { - return stepCoordinates + return stepPolyline } - return nearbyCoordinates + return nearbyPolyline } /** Given a location and a series of coordinates, compute what the course should be for a the location. */ - func interpolatedCourse(along coordinates: [CLLocationCoordinate2D]) -> CLLocationDirection? { - let nearByPolyline = Polyline(coordinates) + func interpolatedCourse(along polyline: Polyline) -> CLLocationDirection? { + guard let closest = polyline.closestCoordinate(to: coordinate) else { return nil } - guard let closest = nearByPolyline.closestCoordinate(to: coordinate) else { return nil } - - let slicedLineBehind = Polyline(coordinates.reversed()).sliced(from: closest.coordinate, to: coordinates.reversed().last) - let slicedLineInFront = nearByPolyline.sliced(from: closest.coordinate, to: coordinates.last) + let reversedPolyline = Polyline(polyline.coordinates.reversed()) + let slicedLineBehind = reversedPolyline.sliced(from: closest.coordinate, to: reversedPolyline.coordinates.last) + let slicedLineInFront = polyline.sliced(from: closest.coordinate, to: polyline.coordinates.last) let userDistanceBuffer: CLLocationDistance = max(speed * RouteControllerDeadReckoningTimeInterval / 2, RouteControllerUserLocationSnappingDistance / 2) guard let pointBehind = slicedLineBehind.coordinateFromStart(distance: userDistanceBuffer) else { return nil } - guard let pointBehindClosest = nearByPolyline.closestCoordinate(to: pointBehind) else { return nil } + guard let pointBehindClosest = polyline.closestCoordinate(to: pointBehind) else { return nil } guard let pointAhead = slicedLineInFront.coordinateFromStart(distance: userDistanceBuffer) else { return nil } - guard let pointAheadClosest = nearByPolyline.closestCoordinate(to: pointAhead) else { return nil } + guard let pointAheadClosest = polyline.closestCoordinate(to: pointAhead) else { return nil } // Get direction of these points let pointBehindDirection = pointBehindClosest.coordinate.direction(to: closest.coordinate) diff --git a/MapboxCoreNavigation/CoreFeedbackEvent.swift b/MapboxCoreNavigation/CoreFeedbackEvent.swift index 17d6e2143d0..acd90ba15b6 100644 --- a/MapboxCoreNavigation/CoreFeedbackEvent.swift +++ b/MapboxCoreNavigation/CoreFeedbackEvent.swift @@ -33,7 +33,7 @@ class FeedbackEvent: CoreFeedbackEvent { class RerouteEvent: CoreFeedbackEvent { func update(newRoute: Route) { - if let geometry = newRoute.coordinates { + if let geometry = newRoute.shape?.coordinates { eventDictionary["newGeometry"] = Polyline(coordinates: geometry).encodedPolyline eventDictionary["newDistanceRemaining"] = round(newRoute.distance) eventDictionary["newDurationRemaining"] = round(newRoute.expectedTravelTime) diff --git a/MapboxCoreNavigation/EventDetails.swift b/MapboxCoreNavigation/EventDetails.swift index d5c16e4ec8c..2f6e9ede45a 100644 --- a/MapboxCoreNavigation/EventDetails.swift +++ b/MapboxCoreNavigation/EventDetails.swift @@ -120,15 +120,15 @@ struct NavigationEventDetails: EventDetails { requestIdentifier = dataSource.routeProgress.route.routeIdentifier if let location = dataSource.router.rawLocation, - let coordinates = dataSource.routeProgress.route.coordinates, + let coordinates = dataSource.routeProgress.route.shape?.coordinates, let lastCoord = coordinates.last { userAbsoluteDistanceToDestination = location.distance(from: CLLocation(latitude: lastCoord.latitude, longitude: lastCoord.longitude)) } else { userAbsoluteDistanceToDestination = nil } - if let geometry = session.originalRoute.coordinates { - originalGeometry = Polyline(coordinates: geometry) + if let shape = session.originalRoute.shape { + originalGeometry = Polyline(coordinates: shape.coordinates) originalDistance = round(session.originalRoute.distance) originalEstimatedDuration = round(session.originalRoute.expectedTravelTime) originalStepCount = session.originalRoute.legs.map({$0.steps.count}).reduce(0, +) @@ -139,8 +139,8 @@ struct NavigationEventDetails: EventDetails { originalStepCount = nil } - if let geometry = session.currentRoute.coordinates { - self.geometry = Polyline(coordinates: geometry) + if let shape = session.currentRoute.shape { + self.geometry = Polyline(coordinates: shape.coordinates) distance = round(session.currentRoute.distance) estimatedDuration = round(session.currentRoute.expectedTravelTime) } else { @@ -314,12 +314,12 @@ extension RouteLegProgress: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(upcomingStep?.instructions, forKey: .upcomingInstruction) - try container.encodeIfPresent(upcomingStep?.maneuverType.description, forKey: .upcomingType) - try container.encodeIfPresent(upcomingStep?.maneuverDirection.description, forKey: .upcomingModifier) + try container.encodeIfPresent(upcomingStep?.maneuverType, forKey: .upcomingType) + try container.encodeIfPresent(upcomingStep?.maneuverDirection, forKey: .upcomingModifier) try container.encodeIfPresent(upcomingStep?.names?.joined(separator: ";"), forKey: .upcomingName) try container.encodeIfPresent(currentStep.instructions, forKey: .previousInstruction) - try container.encode(currentStep.maneuverType.description, forKey: .previousType) - try container.encode(currentStep.maneuverDirection.description, forKey: .previousModifier) + try container.encode(currentStep.maneuverType, forKey: .previousType) + try container.encode(currentStep.maneuverDirection, forKey: .previousModifier) try container.encode(currentStep.names?.joined(separator: ";"), forKey: .previousName) try container.encode(Int(currentStep.distance), forKey: .distance) try container.encode(Int(currentStep.expectedTravelTime), forKey: .duration) diff --git a/MapboxCoreNavigation/LegacyRouteController.swift b/MapboxCoreNavigation/LegacyRouteController.swift index 851b0ae91f1..cde663ab0ef 100644 --- a/MapboxCoreNavigation/LegacyRouteController.swift +++ b/MapboxCoreNavigation/LegacyRouteController.swift @@ -133,11 +133,11 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa } func updateDistanceToManeuver() { - guard let coordinates = routeProgress.currentLegProgress.currentStep.coordinates, let coordinate = rawLocation?.coordinate else { + guard let shape = routeProgress.currentLegProgress.currentStep.shape, let coordinate = rawLocation?.coordinate else { userSnapToStepDistanceFromManeuver = nil return } - userSnapToStepDistanceFromManeuver = Polyline(coordinates).distance(from: coordinate) + userSnapToStepDistanceFromManeuver = shape.distance(from: coordinate) } public var reroutingTolerance: CLLocationDistance { @@ -158,8 +158,8 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa Monitors the user's course to see if it is consistantly moving away from what we expect the course to be at a given point. */ func userCourseIsOnRoute(_ location: CLLocation) -> Bool { - let nearbyCoordinates = routeProgress.nearbyCoordinates - guard let calculatedCourseForLocationOnStep = location.interpolatedCourse(along: nearbyCoordinates) else { return true } + let nearbyPolyline = routeProgress.nearbyShape + guard let calculatedCourseForLocationOnStep = location.interpolatedCourse(along: nearbyPolyline) else { return true } let maxUpdatesAwayFromRouteGivenAccuracy = Int(location.horizontalAccuracy / Double(RouteControllerIncorrectCourseMultiplier)) @@ -175,9 +175,13 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa } public func userIsOnRoute(_ location: CLLocation) -> Bool { + + guard let destination = routeProgress.currentLeg.destination else { + preconditionFailure("Route legs used for navigation must have destinations") + } // If the user has arrived, do not continue monitor reroutes, step progress, etc if routeProgress.currentLegProgress.userHasArrivedAtWaypoint && - (delegate?.router(self, shouldPreventReroutesWhenArrivingAt: routeProgress.currentLeg.destination) ?? + (delegate?.router(self, shouldPreventReroutesWhenArrivingAt: destination) ?? RouteController.DefaultBehavior.shouldPreventReroutesWhenArrivingAtWaypoint) { return true } @@ -280,7 +284,10 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa let step = stepProgress.step //Increment the progress model - let polyline = Polyline(step.coordinates!) + guard let polyline = step.shape else { + preconditionFailure("Route steps used for navigation must have shape data") + } + if let closestCoordinate = polyline.closestCoordinate(to: rawLocation.coordinate) { let remainingDistance = polyline.distance(from: closestCoordinate.coordinate) let distanceTraveled = step.distance - remainingDistance @@ -305,9 +312,14 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa } func updateRouteLegProgress(for location: CLLocation) { - let currentDestination = routeProgress.currentLeg.destination + let legProgress = routeProgress.currentLegProgress - guard let remainingVoiceInstructions = legProgress.currentStepProgress.remainingSpokenInstructions else { return } + guard let currentDestination = legProgress.leg.destination else { + preconditionFailure("Route legs used for navigation must have destinations") + } + guard let remainingVoiceInstructions = legProgress.currentStepProgress.remainingSpokenInstructions else { + return + } // We are at least at the "You will arrive" instruction if legProgress.remainingSteps.count <= 1 && remainingVoiceInstructions.count <= 1 && currentDestination != previousArrivalWaypoint { @@ -407,8 +419,12 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa routeProgress.currentLegProgress.currentStepProgress.intersectionsIncludingUpcomingManeuverIntersection = intersections + guard let shape = currentStepProgress.step.shape else { + return + } + if let upcomingIntersection = routeProgress.currentLegProgress.currentStepProgress.upcomingIntersection { - routeProgress.currentLegProgress.currentStepProgress.userDistanceToUpcomingIntersection = Polyline(currentStepProgress.step.coordinates!).distance(from: location.coordinate, to: upcomingIntersection.location) + routeProgress.currentLegProgress.currentStepProgress.userDistanceToUpcomingIntersection = shape.distance(from: location.coordinate, to: upcomingIntersection.location) } if routeProgress.currentLegProgress.currentStepProgress.intersectionDistances == nil { @@ -508,9 +524,8 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa } func updateIntersectionDistances() { - if let coordinates = routeProgress.currentLegProgress.currentStep.coordinates, let intersections = routeProgress.currentLegProgress.currentStep.intersections { - let polyline = Polyline(coordinates) - let distances: [CLLocationDistance] = intersections.map { polyline.distance(from: coordinates.first, to: $0.location) } + if let shape = routeProgress.currentLegProgress.currentStep.shape, let intersections = routeProgress.currentLegProgress.currentStep.intersections { + let distances: [CLLocationDistance] = intersections.map { shape.distance(from: shape.coordinates.first, to: $0.location) } routeProgress.currentLegProgress.currentStepProgress.intersectionDistances = distances } } diff --git a/MapboxCoreNavigation/NavigationRouteOptions.swift b/MapboxCoreNavigation/NavigationRouteOptions.swift index 32124ceb956..a9a01b7a73b 100644 --- a/MapboxCoreNavigation/NavigationRouteOptions.swift +++ b/MapboxCoreNavigation/NavigationRouteOptions.swift @@ -13,7 +13,7 @@ open class NavigationRouteOptions: RouteOptions { - seealso: `RouteOptions` */ - public required init(waypoints: [Waypoint], profileIdentifier: MBDirectionsProfileIdentifier? = .automobileAvoidingTraffic) { + public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = .automobileAvoidingTraffic) { super.init(waypoints: waypoints.map { $0.coordinateAccuracy = -1 return $0 @@ -35,7 +35,7 @@ open class NavigationRouteOptions: RouteOptions { - seealso: `RouteOptions` */ - public convenience init(locations: [CLLocation], profileIdentifier: MBDirectionsProfileIdentifier? = .automobileAvoidingTraffic) { + public convenience init(locations: [CLLocation], profileIdentifier: DirectionsProfileIdentifier? = .automobileAvoidingTraffic) { self.init(waypoints: locations.map { Waypoint(location: $0) }, profileIdentifier: profileIdentifier) } @@ -44,12 +44,12 @@ open class NavigationRouteOptions: RouteOptions { - seealso: `RouteOptions` */ - public convenience init(coordinates: [CLLocationCoordinate2D], profileIdentifier: MBDirectionsProfileIdentifier? = .automobileAvoidingTraffic) { + public convenience init(coordinates: [CLLocationCoordinate2D], profileIdentifier: DirectionsProfileIdentifier? = .automobileAvoidingTraffic) { self.init(waypoints: coordinates.map { Waypoint(coordinate: $0) }, profileIdentifier: profileIdentifier) } - - public required init?(coder decoder: NSCoder) { - super.init(coder: decoder) + + required public init(from decoder: Decoder) throws { + try super.init(from: decoder) } } @@ -66,7 +66,7 @@ open class NavigationMatchOptions: MatchOptions { - seealso: `MatchOptions` */ - public required init(waypoints: [Waypoint], profileIdentifier: MBDirectionsProfileIdentifier? = .automobileAvoidingTraffic) { + public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = .automobileAvoidingTraffic) { super.init(waypoints: waypoints.map { $0.coordinateAccuracy = -1 return $0 @@ -86,7 +86,7 @@ open class NavigationMatchOptions: MatchOptions { - seealso: `MatchOptions` */ - public convenience init(locations: [CLLocation], profileIdentifier: MBDirectionsProfileIdentifier? = .automobileAvoidingTraffic) { + public convenience init(locations: [CLLocation], profileIdentifier: DirectionsProfileIdentifier? = .automobileAvoidingTraffic) { self.init(waypoints: locations.map { Waypoint(location: $0) }, profileIdentifier: profileIdentifier) } @@ -95,11 +95,11 @@ open class NavigationMatchOptions: MatchOptions { - seealso: `MatchOptions` */ - public convenience init(coordinates: [CLLocationCoordinate2D], profileIdentifier: MBDirectionsProfileIdentifier? = .automobileAvoidingTraffic) { + public convenience init(coordinates: [CLLocationCoordinate2D], profileIdentifier: DirectionsProfileIdentifier? = .automobileAvoidingTraffic) { self.init(waypoints: coordinates.map { Waypoint(coordinate: $0) }, profileIdentifier: profileIdentifier) } - public required init?(coder decoder: NSCoder) { - super.init(coder: decoder) + required public init(from decoder: Decoder) throws { + try super.init(from: decoder) } } diff --git a/MapboxCoreNavigation/NavigationService.swift b/MapboxCoreNavigation/NavigationService.swift index 23623f6770b..6a1e56dc745 100644 --- a/MapboxCoreNavigation/NavigationService.swift +++ b/MapboxCoreNavigation/NavigationService.swift @@ -141,7 +141,7 @@ public class MapboxNavigationService: NSObject, NavigationService { public var directions: Directions /** - The active router. By default, a `PortableRouteController`. + The active router. By default, a `RouteController`. */ public var router: Router! @@ -305,7 +305,7 @@ public class MapboxNavigationService: NSObject, NavigationService { // Jump to the first coordinate on the route if the location source does // not yet have a fixed location. if router.location == nil, - let coordinate = route.coordinates?.first { + let coordinate = route.shape?.coordinates.first { let location = CLLocation(coordinate: coordinate, altitude: -1, horizontalAccuracy: -1, verticalAccuracy: -1, course: -1, speed: 0, timestamp: Date()) router.locationManager?(nativeLocationSource, didUpdateLocations: [location]) } diff --git a/MapboxCoreNavigation/OfflineDirections.swift b/MapboxCoreNavigation/OfflineDirections.swift index 8dc30a2f8fb..6d32da4bfda 100644 --- a/MapboxCoreNavigation/OfflineDirections.swift +++ b/MapboxCoreNavigation/OfflineDirections.swift @@ -7,24 +7,62 @@ import MapboxNavigationNative */ public typealias NavigationDirectionsCompletionHandler = (_ numberOfTiles: UInt64) -> Void -enum OfflineRoutingError: Error, LocalizedError { - case unexpectedRouteResult(String) - case corruptRouteData(String) - case responseError(String) +/** + An error that occurs when calculating directions potentially offline using the `NavigationDirections.calculate(_:offline:completionHandler:)` method. +*/ +public enum OfflineRoutingError: LocalizedError { + /** + A standard Directions API error occurred. + + A Directions API error can occur whether directions are calculated online or offline. + */ + case standard(DirectionsError) + + /** + The router returned an empty response. + */ + case noData + + /** + The router returned a response that isn’t correctly formatted. + */ + case invalidResponse + + case unknown(underlying: Error) public var localizedDescription: String { switch self { - case .corruptRouteData(let value): - return value - case .unexpectedRouteResult(let value): - return value - case .responseError(let value): - return value + case .standard(let error): + return error.localizedDescription + case .noData: + return NSLocalizedString("OFFLINE_NO_RESULT", bundle: .mapboxCoreNavigation, value: "Unable to calculate the requested route while offline.", comment: "Error description when an offline route request returns no result") + case .invalidResponse: + return NSLocalizedString("OFFLINE_CORRUPT_DATA", bundle: .mapboxCoreNavigation, value: "Found an invalid route while offline.", comment: "Error message when an offline route request returns a response that can’t be deserialized") + case .unknown(let underlying): + return "Unknown Error: \(underlying.localizedDescription)" } } - var errorDescription: String? { - return localizedDescription + public var failureReason: String? { + switch self { + case .standard(let error): + return error.failureReason + case .unknown(let underlying): + return (underlying as? LocalizedError)?.failureReason + default: + return nil + } + } + + public var recoverySuggestion: String? { + switch self { + case .standard(let error): + return error.recoverySuggestion + case .unknown(let underlying): + return (underlying as? LocalizedError)?.recoverySuggestion + default: + return nil + } } } @@ -51,6 +89,19 @@ public typealias UnpackProgressHandler = (_ totalBytes: UInt64, _ remainingBytes */ public typealias UnpackCompletionHandler = (_ numberOfTiles: UInt64, _ error: Error?) -> () +/** + A closure (block) to be called when a directions request is complete. + + - parameter waypoints: An array of `Waypoint` objects. Each waypoint object corresponds to a `Waypoint` object in the original `RouteOptions` object. The locations and names of these waypoints are the result of conflating the original waypoints to known roads. The waypoints may include additional information that was not specified in the original waypoints. + + If the request was canceled or there was an error obtaining the routes, this argument may be `nil`. + - parameter routes: An array of `Route` objects. The preferred route is first; any alternative routes come next if the `RouteOptions` object’s `includesAlternativeRoutes` property was set to `true`. The preferred route depends on the route options object’s `profileIdentifier` property. + + 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 + /** 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. @@ -133,9 +184,18 @@ public class NavigationDirections: Directions { - parameter offline: Determines whether to calculate the route offline or online. - parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread. */ - public func calculate(_ options: RouteOptions, offline: Bool = true, completionHandler: @escaping Directions.RouteCompletionHandler) { - guard offline == true else { - super.calculate(options, completionHandler: completionHandler) + 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 + } + completionHandler(waypoints, routes, offlineError) + } return } @@ -143,34 +203,33 @@ public class NavigationDirections: Directions { NavigationDirectionsConstants.offlineSerialQueue.async { [weak self] in guard let result = self?.navigator.getRouteForDirectionsUri(url.absoluteString) else { - let message = NSLocalizedString("OFFLINE_NO_RESULT", bundle: .mapboxCoreNavigation, value: "Unable to calculate the requested route while offline.", comment: "Error description when an offline route request returns no result") - let error = OfflineRoutingError.unexpectedRouteResult(message) - return completionHandler(nil, nil, error as NSError) + DispatchQueue.main.async { + completionHandler(nil, nil, .noData) + } + return } guard let data = result.json.data(using: .utf8) else { - let message = NSLocalizedString("OFFLINE_CORRUPT_DATA", bundle: .mapboxCoreNavigation, value: "Found an invalid route while offline.", comment: "Error message when an offline route request returns a response that can’t be deserialized") - let error = OfflineRoutingError.corruptRouteData(message) - return completionHandler(nil, nil, error as NSError) + DispatchQueue.main.async { + completionHandler(nil, nil, .invalidResponse) + } + return } - - do { - let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] - if let errorValue = json["error"] as? String { - DispatchQueue.main.async { - let error = OfflineRoutingError.responseError(errorValue) - return completionHandler(nil, nil, error as NSError) - } - } else { - DispatchQueue.main.async { - let response = options.response(from: json) - return completionHandler(response.0, response.1, nil) + DispatchQueue.main.async { + + do { + let decoder = JSONDecoder() + decoder.userInfo[.options] = options + let response = try decoder.decode(RouteResponse.self, from: data) + guard let routes = response.routes else { + return completionHandler(response.waypoints, nil, .standard(.unableToRoute)) } + return completionHandler(response.waypoints, routes, nil) } - } catch { - DispatchQueue.main.async { - return completionHandler(nil, nil, error as NSError) + catch { + return completionHandler(nil, nil, .unknown(underlying: error)) } + } } } diff --git a/MapboxCoreNavigation/Route.swift b/MapboxCoreNavigation/Route.swift new file mode 100644 index 00000000000..480079b34cb --- /dev/null +++ b/MapboxCoreNavigation/Route.swift @@ -0,0 +1,14 @@ +import MapboxDirections + +extension Route { + + var json: String? { + let encoder = JSONEncoder() + encoder.userInfo[.options] = routeOptions + guard let encoded = try? encoder.encode(self) else { + return nil + } + let encodedString = String(data: encoded, encoding: .utf8) + return encodedString + } +} diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index 38eccadd06e..7490132f4df 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -160,13 +160,13 @@ open class RouteController: NSObject { /// updateNavigator is used to pass the new progress model onto nav-native. private func updateNavigator(with progress: RouteProgress) { - assert(route.json != nil, "route.json missing, please verify the version of MapboxDirections.swift") - - let data = try! JSONSerialization.data(withJSONObject: route.json!, options: []) - let jsonString = String(data: data, encoding: .utf8)! - + + guard let json = progress.route.json else { + return + } // TODO: Add support for alternative route - navigator.setRouteForRouteResponse(jsonString, route: 0, leg: UInt32(routeProgress.legIndex)) + navigator.setRouteForRouteResponse(json, route: 0, leg: UInt32(routeProgress.legIndex)) + } /// updateRouteLeg is used to notify nav-native of the developer changing the active route-leg. @@ -193,7 +193,7 @@ open class RouteController: NSObject { // Notify observers if the step’s remaining distance has changed. update(progress: routeProgress, with: CLLocation(status.location), rawLocation: location) - let willReroute = !userIsOnRoute(location) && delegate?.router(self, shouldRerouteFrom: location) + let willReroute = !userIsOnRoute(location, status: status) && delegate?.router(self, shouldRerouteFrom: location) ?? DefaultBehavior.shouldRerouteFromLocation updateIndexes(status: status, progress: routeProgress) @@ -258,8 +258,12 @@ open class RouteController: NSObject { func updateRouteLegProgress(status: MBNavigationStatus) { let legProgress = routeProgress.currentLegProgress - let currentDestination = routeProgress.currentLeg.destination - guard let remainingVoiceInstructions = legProgress.currentStepProgress.remainingSpokenInstructions else { return } + + guard let currentDestination = legProgress.leg.destination else { + preconditionFailure("Route legs used for navigation must have destinations") + } + guard let remainingVoiceInstructions = legProgress.currentStepProgress.remainingSpokenInstructions else { return + } // We are at least at the "You will arrive" instruction if legProgress.remainingSteps.count <= 2 && remainingVoiceInstructions.count <= 2 { @@ -287,7 +291,9 @@ open class RouteController: NSObject { let step = stepProgress.step //Increment the progress model - let polyline = Polyline(step.coordinates!) + guard let polyline = step.shape else { + preconditionFailure("Route steps used for navigation must have shape data") + } if let closestCoordinate = polyline.closestCoordinate(to: rawLocation.coordinate) { let remainingDistance = polyline.distance(from: closestCoordinate.coordinate) let distanceTraveled = step.distance - remainingDistance @@ -355,14 +361,23 @@ open class RouteController: NSObject { extension RouteController: Router { public func userIsOnRoute(_ location: CLLocation) -> Bool { + return userIsOnRoute(location, status: nil) + } + + public func userIsOnRoute(_ location: CLLocation, status: MBNavigationStatus?) -> Bool { + + guard let destination = routeProgress.currentLeg.destination else { + preconditionFailure("Route legs used for navigation must have destinations") + } + // If the user has arrived, do not continue monitor reroutes, step progress, etc if routeProgress.currentLegProgress.userHasArrivedAtWaypoint && - (delegate?.router(self, shouldPreventReroutesWhenArrivingAt: routeProgress.currentLeg.destination) ?? + (delegate?.router(self, shouldPreventReroutesWhenArrivingAt: destination) ?? DefaultBehavior.shouldPreventReroutesWhenArrivingAtWaypoint) { return true } - let status = navigator.getStatusForTimestamp(location.timestamp) + let status = status ?? navigator.getStatusForTimestamp(location.timestamp) let offRoute = status.routeState == .offRoute return !offRoute } diff --git a/MapboxCoreNavigation/RouteLeg.swift b/MapboxCoreNavigation/RouteLeg.swift index a09c89606ef..7c07d2ff314 100644 --- a/MapboxCoreNavigation/RouteLeg.swift +++ b/MapboxCoreNavigation/RouteLeg.swift @@ -2,7 +2,9 @@ import MapboxDirections import Turf extension RouteLeg { - var coordinates: [CLLocationCoordinate2D] { - return (steps.first?.coordinates ?? []) + steps.dropFirst().flatMap { ($0.coordinates ?? []).dropFirst() } + var shape: LineString { + return steps.dropFirst().reduce(into: steps.first?.shape ?? LineString([])) { (result, step) in + result.coordinates += (step.shape?.coordinates ?? []).dropFirst() + } } } diff --git a/MapboxCoreNavigation/RouteOptions.swift b/MapboxCoreNavigation/RouteOptions.swift index 6daad5c8401..254b6b180fa 100644 --- a/MapboxCoreNavigation/RouteOptions.swift +++ b/MapboxCoreNavigation/RouteOptions.swift @@ -3,27 +3,13 @@ import MapboxDirections extension RouteOptions { internal var activityType: CLActivityType { switch self.profileIdentifier { - case MBDirectionsProfileIdentifier.cycling, MBDirectionsProfileIdentifier.walking: + case .cycling, .walking: return .fitness default: return .automotiveNavigation } } - /** - Returns a copy of RouteOptions without the specified waypoint. - - - parameter waypoint: the Waypoint to exclude. - - returns: a copy of self excluding the specified waypoint. - */ - public func without(waypoint: Waypoint) -> RouteOptions { - let waypointsWithoutSpecified = waypoints.filter { $0 != waypoint } - let copy = self.copy() as! RouteOptions - copy.waypoints = waypointsWithoutSpecified - - return copy - } - /** Returns a tuple containing the waypoints along the leg at the given index and the waypoints that separate subsequent legs. @@ -42,6 +28,31 @@ extension RouteOptions { } } +extension RouteOptions: NSCopying { + public func copy(with zone: NSZone? = nil) -> Any { + do { + let encodedOptions = try JSONEncoder().encode(self) + return try JSONDecoder().decode(RouteOptions.self, from: encodedOptions) + } catch { + preconditionFailure("Unable to copy RouteOptions by round-tripping it through JSON") + } + } + + /** + Returns a copy of RouteOptions without the specified waypoint. + + - parameter waypoint: the Waypoint to exclude. + - returns: a copy of self excluding the specified waypoint. + */ + public func without(waypoint: Waypoint) -> RouteOptions { + let waypointsWithoutSpecified = waypoints.filter { $0 != waypoint } + let copy = self.copy() as! RouteOptions + copy.waypoints = waypointsWithoutSpecified + + return copy + } +} + extension Array { /** - seealso: Array.filter(_:) diff --git a/MapboxCoreNavigation/RouteProgress.swift b/MapboxCoreNavigation/RouteProgress.swift index ab2a4a70920..7082bc9a422 100644 --- a/MapboxCoreNavigation/RouteProgress.swift +++ b/MapboxCoreNavigation/RouteProgress.swift @@ -87,7 +87,7 @@ open class RouteProgress: NSObject { This property does not include waypoints whose `Waypoint.separatesLegs` property is set to `false`. */ public var remainingWaypoints: [Waypoint] { - return route.legs.suffix(from: legIndex).map { $0.destination } + return route.legs.suffix(from: legIndex).compactMap { $0.destination } } /** @@ -140,11 +140,14 @@ open class RouteProgress: NSObject { - important: The adjacent steps may be part of legs other than the current leg. */ - public var nearbyCoordinates: [CLLocationCoordinate2D] { - let priorCoordinates = priorStep?.coordinates?.dropLast() ?? [] - let currentCoordinates = currentLegProgress.currentStep.coordinates ?? [] - let upcomingCoordinates = upcomingStep?.coordinates?.dropFirst() ?? [] - return priorCoordinates + currentCoordinates + upcomingCoordinates + public var nearbyShape: LineString { + let priorCoordinates = priorStep?.shape?.coordinates.dropLast() ?? [] + let currentShape = currentLegProgress.currentStep.shape + let upcomingCoordinates = upcomingStep?.shape?.coordinates.dropFirst() ?? [] + if let currentShape = currentShape, priorCoordinates.isEmpty && upcomingCoordinates.isEmpty { + return currentShape + } + return LineString(priorCoordinates + (currentShape?.coordinates ?? []) + upcomingCoordinates) } /** @@ -184,8 +187,8 @@ open class RouteProgress: NSObject { if let segmentCongestionLevels = leg.segmentCongestionLevels, let expectedSegmentTravelTimes = leg.expectedSegmentTravelTimes { for step in leg.steps { - guard let coordinates = step.coordinates else { continue } - let stepCoordinateCount = step.maneuverType == .arrive ? Int(step.coordinateCount) : coordinates.dropLast().count + guard let coordinates = step.shape?.coordinates else { continue } + let stepCoordinateCount = step.maneuverType == .arrive ? Int(coordinates.count) : coordinates.dropLast().count let nextManeuverCoordinateIndex = maneuverCoordinateIndex + stepCoordinateCount - 1 guard nextManeuverCoordinateIndex < segmentCongestionLevels.count else { continue } @@ -211,7 +214,11 @@ open class RouteProgress: NSObject { } public var averageCongestionLevelRemainingOnLeg: CongestionLevel? { - let coordinatesLeftOnStepCount = Int(floor((Double(currentLegProgress.currentStepProgress.step.coordinateCount)) * currentLegProgress.currentStepProgress.fractionTraveled)) + guard let coordinates = currentLegProgress.currentStepProgress.step.shape?.coordinates else { + return .unknown + } + + let coordinatesLeftOnStepCount = Int(floor((Double(coordinates.count)) * currentLegProgress.currentStepProgress.fractionTraveled)) guard coordinatesLeftOnStepCount >= 0 else { return .unknown } @@ -416,19 +423,6 @@ open class RouteLegProgress: NSObject { currentStepProgress = RouteStepProgress(step: leg.steps[stepIndex], spokenInstructionIndex: spokenInstructionIndex) } - /** - Returns an array of `CLLocationCoordinate2D` of the prior, current and upcoming step geometry. - */ - @available(*, deprecated, message: "Use RouteProgress.nearbyCoordinates") - public var nearbyCoordinates: [CLLocationCoordinate2D] { - let priorCoords = priorStep?.coordinates ?? [] - let upcomingCoords = upcomingStep?.coordinates ?? [] - let currentCoords = currentStep.coordinates ?? [] - let nearby = priorCoords + currentCoords + upcomingCoords - assert(!nearby.isEmpty, "Step must have coordinates") - return nearby - } - typealias StepIndexDistance = (index: Int, distance: CLLocationDistance) func closestStep(to coordinate: CLLocationCoordinate2D) -> StepIndexDistance? { @@ -436,8 +430,8 @@ open class RouteLegProgress: NSObject { let remainingSteps = leg.steps.suffix(from: stepIndex) for (currentStepIndex, step) in remainingSteps.enumerated() { - guard let coords = step.coordinates else { continue } - guard let closestCoordOnStep = Polyline(coords).closestCoordinate(to: coordinate) else { continue } + guard let shape = step.shape else { continue } + guard let closestCoordOnStep = shape.closestCoordinate(to: coordinate) else { continue } let foundIndex = currentStepIndex + stepIndex // First time around, currentClosest will be `nil`. @@ -462,7 +456,7 @@ open class RouteLegProgress: NSObject { // The leg has only a source and no via points. Save ourselves a call to RouteLeg.coordinates, which can be expensive. return [] } - let legPolyline = Polyline(leg.coordinates) + let legPolyline = leg.shape guard let userCoordinateIndex = legPolyline.indexedCoordinateFromStart(distance: distanceTraveled)?.index else { // The leg is empty, so none of the waypoints are meaningful. return [] diff --git a/MapboxCoreNavigation/SimulatedLocationManager.swift b/MapboxCoreNavigation/SimulatedLocationManager.swift index 85234850373..8cf60ea08ec 100644 --- a/MapboxCoreNavigation/SimulatedLocationManager.swift +++ b/MapboxCoreNavigation/SimulatedLocationManager.swift @@ -35,7 +35,7 @@ open class SimulatedLocationManager: NavigationLocationManager { fileprivate var timer: DispatchTimer! fileprivate var locations: [SimulatedLocation]! - fileprivate var routeLine = [CLLocationCoordinate2D]() + fileprivate var routeShape: Polyline! /** Specify the multiplier to use when calculating speed based on the RouteLeg’s `expectedSegmentTravelTimes`. @@ -63,7 +63,7 @@ open class SimulatedLocationManager: NavigationLocationManager { copy.simulatedLocation = simulatedLocation copy.currentSpeed = currentSpeed copy.locations = locations - copy.routeLine = routeLine + copy.routeShape = routeShape copy.speedMultiplier = speedMultiplier return copy } @@ -107,9 +107,9 @@ open class SimulatedLocationManager: NavigationLocationManager { } private func reset() { - if let coordinates = route?.coordinates { - routeLine = coordinates - locations = coordinates.simulatedLocationsWithTurnPenalties() + if let shape = route?.shape { + routeShape = shape + locations = shape.coordinates.simulatedLocationsWithTurnPenalties() } } @@ -145,9 +145,7 @@ open class SimulatedLocationManager: NavigationLocationManager { } internal func tick() { - let polyline = Polyline(routeLine) - - guard let newCoordinate = polyline.coordinateFromStart(distance: currentDistance) else { + guard let polyline = routeShape, let newCoordinate = polyline.coordinateFromStart(distance: currentDistance) else { return } @@ -163,11 +161,11 @@ open class SimulatedLocationManager: NavigationLocationManager { // Simulate speed based on expected segment travel time if let expectedSegmentTravelTimes = routeProgress?.currentLeg.expectedSegmentTravelTimes, - let coordinates = routeProgress?.route.coordinates, - let closestCoordinateOnRoute = Polyline(routeProgress!.route.coordinates!).closestCoordinate(to: newCoordinate), - let nextCoordinateOnRoute = coordinates.after(element: coordinates[closestCoordinateOnRoute.index]), + let shape = routeProgress?.route.shape, + let closestCoordinateOnRoute = shape.closestCoordinate(to: newCoordinate), + let nextCoordinateOnRoute = shape.coordinates.after(element: shape.coordinates[closestCoordinateOnRoute.index]), let time = expectedSegmentTravelTimes.optional[closestCoordinateOnRoute.index] { - let distance = coordinates[closestCoordinateOnRoute.index].distance(to: nextCoordinateOnRoute) + let distance = shape.coordinates[closestCoordinateOnRoute.index].distance(to: nextCoordinateOnRoute) currentSpeed = max(distance / time, 2) } else { currentSpeed = calculateCurrentSpeed(distance: distance, coordinatesNearby: coordinatesNearby, closestLocation: closestLocation) diff --git a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/project.pbxproj b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/project.pbxproj index 863f97fb7ff..f725bbbd985 100644 --- a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/project.pbxproj +++ b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/project.pbxproj @@ -172,11 +172,13 @@ 352544DB1E66623D004C8F1C = { CreatedOnToolsVersion = 8.2.1; DevelopmentTeam = GJZR2MEM28; + LastSwiftMigration = 1130; ProvisioningStyle = Automatic; }; 352544EF1E66623D004C8F1C = { CreatedOnToolsVersion = 8.2.1; DevelopmentTeam = GJZR2MEM28; + LastSwiftMigration = 1130; ProvisioningStyle = Automatic; TestTargetID = 352544DB1E66623D004C8F1C; }; @@ -184,7 +186,7 @@ }; buildConfigurationList = 352544D71E66623D004C8F1C /* Build configuration list for PBXProject "PodInstall" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -231,8 +233,8 @@ "${PODS_ROOT}/Target Support Files/Pods-PodInstall/Pods-PodInstall-frameworks.sh", "${PODS_ROOT}/Mapbox-iOS-SDK/dynamic/Mapbox.framework", "${PODS_ROOT}/Mapbox-iOS-SDK/dynamic/Mapbox.framework.dSYM", - "${PODS_ROOT}/Mapbox-iOS-SDK/dynamic/F6FDF133-0198-394E-9C8F-5043F94B4790.bcsymbolmap", - "${PODS_ROOT}/Mapbox-iOS-SDK/dynamic/B4615DAE-86F8-35AB-B4D1-B1F1420E374A.bcsymbolmap", + "${PODS_ROOT}/Mapbox-iOS-SDK/dynamic/1C04753A-6715-3177-9FDA-8F75B4324E0C.bcsymbolmap", + "${PODS_ROOT}/Mapbox-iOS-SDK/dynamic/EE5140C5-686C-3DED-8916-3717EC675952.bcsymbolmap", "${BUILT_PRODUCTS_DIR}/MapboxCoreNavigation/MapboxCoreNavigation.framework", "${BUILT_PRODUCTS_DIR}/MapboxDirections.swift/MapboxDirections.framework", "${BUILT_PRODUCTS_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework", @@ -248,8 +250,8 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Mapbox.framework", "${DWARF_DSYM_FOLDER_PATH}/Mapbox.framework.dSYM", - "${BUILT_PRODUCTS_DIR}/F6FDF133-0198-394E-9C8F-5043F94B4790.bcsymbolmap", - "${BUILT_PRODUCTS_DIR}/B4615DAE-86F8-35AB-B4D1-B1F1420E374A.bcsymbolmap", + "${BUILT_PRODUCTS_DIR}/1C04753A-6715-3177-9FDA-8F75B4324E0C.bcsymbolmap", + "${BUILT_PRODUCTS_DIR}/EE5140C5-686C-3DED-8916-3717EC675952.bcsymbolmap", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreNavigation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxDirections.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", @@ -388,7 +390,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -439,7 +441,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; diff --git a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/xcshareddata/xcschemes/PodInstall.xcscheme b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/xcshareddata/xcschemes/PodInstall.xcscheme index 3565bd4bb08..65a137a9125 100644 --- a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/xcshareddata/xcschemes/PodInstall.xcscheme +++ b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/xcshareddata/xcschemes/PodInstall.xcscheme @@ -1,6 +1,6 @@ + + + + @@ -39,17 +48,6 @@ - - - - - - - - 'https://github.com/mapbox/MapboxDirections.swift.git', :branch => 'master' pod 'MapboxNavigation', :path => '../../../' pod 'MapboxCoreNavigation', :path => '../../../' end diff --git a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/Podfile.lock b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/Podfile.lock index 4c1ec29cc3f..ef1507c6558 100644 --- a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/Podfile.lock +++ b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/Podfile.lock @@ -1,16 +1,17 @@ PODS: - - Mapbox-iOS-SDK (5.4.0) - - MapboxCoreNavigation (0.37.0): + - Mapbox-iOS-SDK (5.5.0) + - MapboxCoreNavigation (0.38.1): - MapboxDirections.swift (~> 0.30.0) - MapboxMobileEvents (~> 0.9.5) - MapboxNavigationNative (~> 6.2.1) - Turf (~> 0.3.0) - MapboxDirections.swift (0.30.0): - Polyline (~> 4.2) + - Turf (~> 0.3) - MapboxMobileEvents (0.9.5) - - MapboxNavigation (0.37.0): + - MapboxNavigation (0.38.1): - Mapbox-iOS-SDK (~> 5.2) - - MapboxCoreNavigation (= 0.37.0) + - MapboxCoreNavigation (= 0.38.1) - MapboxSpeech (~> 0.1.0) - Solar (~> 2.1) - MapboxNavigationNative (6.2.1) @@ -21,12 +22,12 @@ PODS: DEPENDENCIES: - MapboxCoreNavigation (from `../../../`) + - MapboxDirections.swift (from `https://github.com/mapbox/MapboxDirections.swift.git`, branch `master`) - MapboxNavigation (from `../../../`) SPEC REPOS: - https://github.com/cocoapods/specs.git: + trunk: - Mapbox-iOS-SDK - - MapboxDirections.swift - MapboxMobileEvents - MapboxNavigationNative - MapboxSpeech @@ -37,21 +38,29 @@ SPEC REPOS: EXTERNAL SOURCES: MapboxCoreNavigation: :path: "../../../" + MapboxDirections.swift: + :branch: master + :git: https://github.com/mapbox/MapboxDirections.swift.git MapboxNavigation: :path: "../../../" +CHECKOUT OPTIONS: + MapboxDirections.swift: + :commit: 4d91bb2bd8332793500b72413d82de0fabe29809 + :git: https://github.com/mapbox/MapboxDirections.swift.git + SPEC CHECKSUMS: - Mapbox-iOS-SDK: 2b58f752d94e57e95b8e54f3b52569199f6efdba - MapboxCoreNavigation: fd30e78f70471462682c0f05a7708861feae30ff - MapboxDirections.swift: 1c6df988c24b753888ebd9976d7c98632501a413 + Mapbox-iOS-SDK: 7fa66a27e586acb14c7f32ec5a487343e3994451 + MapboxCoreNavigation: a0b212c232dcea1954ed27078ea5e76620a02f58 + MapboxDirections.swift: f92188002af2401e2ab97b1e74bf302243d00e43 MapboxMobileEvents: f6c21b2e59066c5c7093585de7c15adae3b63da0 - MapboxNavigation: 5f3ccb117173abcac3c8218cbc0bd3b736c57857 + MapboxNavigation: cb3ca44dabf351a9636e01f62664715c9c2e9eea MapboxNavigationNative: 11dc22140f4698d3f26989f2b6379dc81ef0d4c1 MapboxSpeech: 59b3984d3f433a443d24acf53097f918c5cc70f9 Polyline: 0e9890790292741c8186201a536b6bb6a78d02dd Solar: 2dc6e7cc39186cb0c8228fa08df76fb50c7d8f24 Turf: c6bdf62d6a70c647874f295dd1cf4eefc0c3e9e6 -PODFILE CHECKSUM: d4084fe664fbacd0cffd88ab45eaed1ad907ad1d +PODFILE CHECKSUM: 93f8b73ee661779638fb731cc44d6a0abf1b3acd -COCOAPODS: 1.7.5 +COCOAPODS: 1.8.4 diff --git a/MapboxCoreNavigationTests/LocationTests.swift b/MapboxCoreNavigationTests/LocationTests.swift index a18ab9e5efa..9315dd26bc8 100644 --- a/MapboxCoreNavigationTests/LocationTests.swift +++ b/MapboxCoreNavigationTests/LocationTests.swift @@ -6,7 +6,7 @@ import CoreLocation class LocationTests: XCTestCase { var setup: (progress: RouteProgress, firstLocation: CLLocation) { let progress = RouteProgress(route: route) - let firstCoord = progress.nearbyCoordinates.first! + let firstCoord = progress.nearbyShape.coordinates.first! let firstLocation = CLLocation(latitude: firstCoord.latitude, longitude: firstCoord.longitude) return (progress, firstLocation) @@ -54,7 +54,7 @@ class LocationTests: XCTestCase { let progress = setup.progress let firstLocation = setup.firstLocation - let calculatedCourse = firstLocation.interpolatedCourse(along: progress.currentLegProgress.currentStepProgress.step.coordinates!)! + let calculatedCourse = firstLocation.interpolatedCourse(along: progress.currentLegProgress.currentStepProgress.step.shape!)! let initialHeadingOnFirstStep = progress.currentLegProgress.currentStepProgress.step.finalHeading! XCTAssertTrue(calculatedCourse - initialHeadingOnFirstStep < 1, "At the beginning of the route, the final heading of the departure step should be very similar to the caclulated course of the first location update.") } diff --git a/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift b/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift index cbdaec678fb..4fe196212c5 100644 --- a/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift +++ b/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift @@ -5,10 +5,15 @@ import TestHelper @testable import MapboxCoreNavigation let jsonFileName = "routeWithInstructions" -let response = Fixture.JSONFromFileNamed(name: jsonFileName) +var routeOptions: NavigationRouteOptions { + let from = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.795042, longitude: -122.413165)) + let to = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.7727, longitude: -122.433378)) + return NavigationRouteOptions(waypoints: [from, to]) +} +let response = Fixture.routeResponse(from: jsonFileName, options: routeOptions) let directions = DirectionsSpy(accessToken: "pk.feedCafeDeadBeefBadeBede") let route: Route = { - return Fixture.route(from: jsonFileName) + return Fixture.route(from: jsonFileName, options: routeOptions) }() let waitForInterval: TimeInterval = 5 @@ -21,7 +26,7 @@ class MapboxCoreNavigationTests: XCTestCase { navigation = MapboxNavigationService(route: route, directions: directions, simulating: .never) let now = Date() let steps = route.legs.first!.steps - let coordinates = steps[2].coordinates! + steps[3].coordinates! + let coordinates = steps[2].shape!.coordinates + steps[3].shape!.coordinates let locations = coordinates.enumerated().map { CLLocation(coordinate: $0.element, altitude: -1, horizontalAccuracy: 10, @@ -51,7 +56,7 @@ class MapboxCoreNavigationTests: XCTestCase { navigation = MapboxNavigationService(route: route, directions: directions, simulating: .never) // Coordinates from first step - let coordinates = route.legs[0].steps[0].coordinates! + let coordinates = route.legs[0].steps[0].shape!.coordinates let now = Date() let locations = coordinates.enumerated().map { CLLocation(coordinate: $0.element, altitude: -1, horizontalAccuracy: 10, @@ -79,7 +84,7 @@ class MapboxCoreNavigationTests: XCTestCase { route.accessToken = "foo" // Coordinates from beginning of step[1] to end of step[2] - let coordinates = route.legs[0].steps[1].coordinates! + route.legs[0].steps[2].coordinates! + let coordinates = route.legs[0].steps[1].shape!.coordinates + route.legs[0].steps[2].shape!.coordinates let locations: [CLLocation] let now = Date() locations = coordinates.enumerated().map { CLLocation(coordinate: $0.element, @@ -105,7 +110,7 @@ class MapboxCoreNavigationTests: XCTestCase { func testJumpAheadToLastStep() { route.accessToken = "foo" - let coordinates = route.legs[0].steps.map { $0.coordinates! }.flatMap { $0 } + 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) } @@ -130,7 +135,7 @@ class MapboxCoreNavigationTests: XCTestCase { func testShouldReroute() { route.accessToken = "foo" - let coordinates = route.legs[0].steps[1].coordinates! + let coordinates = route.legs[0].steps[1].shape!.coordinates let now = Date() let locations = coordinates.enumerated().map { CLLocation(coordinate: $0.element, altitude: -1, horizontalAccuracy: 10, verticalAccuracy: -1, course: -1, speed: 10, timestamp: now + $0.offset) } @@ -316,9 +321,6 @@ class MapboxCoreNavigationTests: XCTestCase { } func testFailToReroute() { - enum TestError: Error { - case test - } route.accessToken = "foo" let directionsClientSpy = DirectionsSpy(accessToken: "garbage", host: nil) navigation = MapboxNavigationService(route: route, directions: directionsClientSpy, simulating: .never) @@ -332,7 +334,7 @@ class MapboxCoreNavigationTests: XCTestCase { } navigation.router.reroute(from: CLLocation(latitude: 0, longitude: 0), along: navigation.router.routeProgress) - directionsClientSpy.fireLastCalculateCompletion(with: nil, routes: nil, error: TestError.test as NSError) + directionsClientSpy.fireLastCalculateCompletion(with: nil, routes: nil, error: .profileNotFound) waitForExpectations(timeout: 2) { (error) in XCTAssertNil(error) diff --git a/MapboxCoreNavigationTests/MapboxNavigationServiceSpec.swift b/MapboxCoreNavigationTests/MapboxNavigationServiceSpec.swift index 02c501ca75d..82608bfa761 100644 --- a/MapboxCoreNavigationTests/MapboxNavigationServiceSpec.swift +++ b/MapboxCoreNavigationTests/MapboxNavigationServiceSpec.swift @@ -7,13 +7,8 @@ import TestHelper class MapboxNavigationServiceSpec: QuickSpec { lazy var initialRoute: Route = { - let jsonRoute = (response["routes"] as! [AnyObject]).first as! [String: Any] - let waypoint1 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.795042, longitude: -122.413165)) - let waypoint2 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.7727, longitude: -122.433378)) - let route = Route(json: jsonRoute, waypoints: [waypoint1, waypoint2], options: NavigationRouteOptions(waypoints: [waypoint1, waypoint2])) - + let route = response.routes!.first! route.accessToken = "foo" - return route }() diff --git a/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift b/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift index 2cea6c23dcf..f3731f28620 100644 --- a/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift +++ b/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift @@ -12,10 +12,16 @@ class NavigationEventsManagerTests: XCTestCase { } func testDepartRerouteArrive() { - let firstRoute = Fixture.route(from: "DCA-Arboretum") - let secondRoute = Fixture.route(from: "PipeFittersUnion-FourSeasonsBoston") - - let firstTrace = Array(Fixture.generateTrace(for: firstRoute).prefix(upTo: firstRoute.coordinates!.count / 2)).shiftedToPresent().qualified() + let firstRoute = Fixture.route(from: "DCA-Arboretum", options: 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 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() diff --git a/MapboxCoreNavigationTests/NavigationServiceTests.swift b/MapboxCoreNavigationTests/NavigationServiceTests.swift index 50e90d9ddc5..6ec9eb7bf15 100644 --- a/MapboxCoreNavigationTests/NavigationServiceTests.swift +++ b/MapboxCoreNavigationTests/NavigationServiceTests.swift @@ -21,14 +21,14 @@ class NavigationServiceTests: XCTestCase { let legProgress: RouteLegProgress = navigationService.router.routeProgress.currentLegProgress - let firstCoord = navigationService.router.routeProgress.nearbyCoordinates.first! + let firstCoord = navigationService.router.routeProgress.nearbyShape.coordinates.first! let firstLocation = CLLocation(coordinate: firstCoord, altitude: 5, horizontalAccuracy: 10, verticalAccuracy: 5, course: 20, speed: 4, timestamp: Date()) let remainingSteps = legProgress.remainingSteps - let penultimateCoord = legProgress.remainingSteps[4].coordinates!.first! + let penultimateCoord = legProgress.remainingSteps[4].shape!.coordinates.first! let penultimateLocation = CLLocation(coordinate: penultimateCoord, altitude: 5, horizontalAccuracy: 10, verticalAccuracy: 5, course: 20, speed: 4, timestamp: Date()) - let lastCoord = legProgress.remainingSteps.last!.coordinates!.first! + let lastCoord = legProgress.remainingSteps.last!.shape!.coordinates.first! let lastLocation = CLLocation(coordinate: lastCoord, altitude: 5, horizontalAccuracy: 10, verticalAccuracy: 5, course: 20, speed: 4, timestamp: Date()) let routeLocations = RouteLocations(firstLocation, penultimateLocation, lastLocation) @@ -36,9 +36,9 @@ class NavigationServiceTests: XCTestCase { return (navigationService: navigationService, routeLocations: routeLocations) }() - let initialRoute = Fixture.route(from: jsonFileName) + let initialRoute = Fixture.route(from: jsonFileName, options: routeOptions) - let alternateRoute = Fixture.route(from: jsonFileName) + let alternateRoute = Fixture.route(from: jsonFileName, options: routeOptions) override func setUp() { super.setUp() @@ -63,7 +63,7 @@ class NavigationServiceTests: XCTestCase { let navigation = dependencies.navigationService let route = navigation.route - let coordinates = route.coordinates!.prefix(3) + let coordinates = route.shape!.coordinates.prefix(3) let now = Date() let locations = coordinates.enumerated().map { CLLocation(coordinate: $0.element, altitude: -1, horizontalAccuracy: 10, verticalAccuracy: -1, course: -1, speed: 10, timestamp: now + $0.offset) } @@ -88,7 +88,7 @@ class NavigationServiceTests: XCTestCase { let navigation = dependencies.navigationService let route = navigation.route - let firstStepCoordinates = route.legs[0].steps[0].coordinates! + let firstStepCoordinates = route.legs[0].steps[0].shape!.coordinates let now = Date() let firstStepLocations = firstStepCoordinates.enumerated().map { CLLocation(coordinate: $0.element, altitude: -1, horizontalAccuracy: 10, verticalAccuracy: -1, course: -1, speed: 10, timestamp: now + $0.offset) @@ -98,7 +98,7 @@ class NavigationServiceTests: XCTestCase { XCTAssertTrue(navigation.router.userIsOnRoute(firstStepLocations.last!), "User should be on route") XCTAssertEqual(navigation.router.routeProgress.currentLegProgress.stepIndex, 1, "User is on first step") - let thirdStepCoordinates = route.legs[0].steps[2].coordinates! + let thirdStepCoordinates = route.legs[0].steps[2].shape!.coordinates let thirdStepLocations = thirdStepCoordinates.enumerated().map { CLLocation(coordinate: $0.element, altitude: -1, horizontalAccuracy: 10, verticalAccuracy: -1, course: -1, speed: 10, timestamp: now + firstStepCoordinates.count + $0.offset) } @@ -124,11 +124,11 @@ class NavigationServiceTests: XCTestCase { navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocation]) XCTAssertEqual(navigation.router.location!.coordinate, firstLocation.coordinate, "Check snapped location is working") - let firstCoordinateOnUpcomingStep = navigation.router.routeProgress.currentLegProgress.upcomingStep!.coordinates!.first! + let firstCoordinateOnUpcomingStep = navigation.router.routeProgress.currentLegProgress.upcomingStep!.shape!.coordinates.first! let firstLocationOnNextStepWithNoSpeed = CLLocation(coordinate: firstCoordinateOnUpcomingStep, altitude: 0, horizontalAccuracy: 10, verticalAccuracy: 10, course: 10, speed: 0, timestamp: Date()) navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocationOnNextStepWithNoSpeed]) - XCTAssertEqual(navigation.router.location!.coordinate, navigation.router.routeProgress.currentLegProgress.currentStep.coordinates!.last!, "When user is not moving, snap to current leg only") + XCTAssertEqual(navigation.router.location!.coordinate, navigation.router.routeProgress.currentLegProgress.currentStep.shape!.coordinates.last!, "When user is not moving, snap to current leg only") let firstLocationOnNextStepWithSpeed = CLLocation(coordinate: firstCoordinateOnUpcomingStep, altitude: 0, horizontalAccuracy: 10, verticalAccuracy: 10, course: 10, speed: 5, timestamp: Date()) navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocationOnNextStepWithSpeed]) @@ -144,13 +144,13 @@ class NavigationServiceTests: XCTestCase { navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocation]) XCTAssertEqual(navigation.router.location!.coordinate, firstLocation.coordinate, "Check snapped location is working") - let firstCoordinateOnUpcomingStep = navigation.router.routeProgress.currentLegProgress.upcomingStep!.coordinates!.first! + let firstCoordinateOnUpcomingStep = navigation.router.routeProgress.currentLegProgress.upcomingStep!.shape!.coordinates.first! let finalHeading = navigation.router.routeProgress.currentLegProgress.upcomingStep!.finalHeading! let firstLocationOnNextStepWithDifferentCourse = CLLocation(coordinate: firstCoordinateOnUpcomingStep, altitude: 0, horizontalAccuracy: 30, verticalAccuracy: 10, course: -finalHeading, speed: 5, timestamp: Date()) navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocationOnNextStepWithDifferentCourse]) - XCTAssertEqual(navigation.router.location!.coordinate, navigation.router.routeProgress.currentLegProgress.currentStep.coordinates!.last!, "When user's course is dissimilar from the finalHeading, they should not snap to upcoming step") + XCTAssertEqual(navigation.router.location!.coordinate, navigation.router.routeProgress.currentLegProgress.currentStep.shape!.coordinates.last!, "When user's course is dissimilar from the finalHeading, they should not snap to upcoming step") let firstLocationOnNextStepWithCorrectCourse = CLLocation(coordinate: firstCoordinateOnUpcomingStep, altitude: 0, horizontalAccuracy: 30, verticalAccuracy: 10, course: finalHeading, speed: 0, timestamp: Date()) navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocationOnNextStepWithCorrectCourse]) @@ -163,7 +163,7 @@ class NavigationServiceTests: XCTestCase { navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocation]) XCTAssertEqual(navigation.router.location!.coordinate, firstLocation.coordinate, "Check snapped location is working") - let futureCoord = Polyline(navigation.router.routeProgress.nearbyCoordinates).coordinateFromStart(distance: 100)! + let futureCoord = navigation.router.routeProgress.nearbyShape.coordinateFromStart(distance: 100)! let futureInaccurateLocation = CLLocation(coordinate: futureCoord, altitude: 0, horizontalAccuracy: 1, verticalAccuracy: 200, course: 0, speed: 5, timestamp: Date()) navigation.locationManager!(navigation.locationManager, didUpdateLocations: [futureInaccurateLocation]) @@ -175,14 +175,17 @@ 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") + let route = Fixture.route(from: "straight-line", options: NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 37.77735, longitude: -122.461465), + CLLocationCoordinate2D(latitude: 37.777016, longitude: -122.468832), + ])) route.accessToken = "foo" let navigation = MapboxNavigationService(route: route, directions: directions) let router = navigation.router! - let firstCoord = router.routeProgress.nearbyCoordinates.first! + let firstCoord = router.routeProgress.nearbyShape.coordinates.first! let firstLocation = CLLocation(latitude: firstCoord.latitude, longitude: firstCoord.longitude) - let coordNearStart = Polyline(router.routeProgress.nearbyCoordinates).coordinateFromStart(distance: 10)! + let coordNearStart = router.routeProgress.nearbyShape.coordinateFromStart(distance: 10)! navigation.locationManager(navigation.locationManager, didUpdateLocations: [firstLocation]) @@ -195,7 +198,7 @@ class NavigationServiceTests: XCTestCase { // The course should not be the interpolated course, rather the raw course. XCTAssertEqual(directionToStart, router.location!.course, "The course should be the raw course and not an interpolated course") - XCTAssertFalse(facingTowardsStartLocation.shouldSnap(toRouteWith: facingTowardsStartLocation.interpolatedCourse(along: router.routeProgress.nearbyCoordinates)!, distanceToFirstCoordinateOnLeg: facingTowardsStartLocation.distance(from: firstLocation)), "Should not snap") + XCTAssertFalse(facingTowardsStartLocation.shouldSnap(toRouteWith: facingTowardsStartLocation.interpolatedCourse(along: router.routeProgress.nearbyShape)!, distanceToFirstCoordinateOnLeg: facingTowardsStartLocation.distance(from: firstLocation)), "Should not snap") } //TODO: Broken by PortableRoutecontroller & MBNavigator -- needs team discussion. @@ -405,7 +408,11 @@ class NavigationServiceTests: XCTestCase { } func testMultiLegRoute() { - let route = Fixture.route(from: "multileg-route") + let route = Fixture.route(from: "multileg-route", 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 trace = Fixture.generateTrace(for: route).shiftedToPresent().qualified() let service = dependencies.navigationService @@ -428,7 +435,10 @@ class NavigationServiceTests: XCTestCase { func testProactiveRerouting() { typealias RouterComposition = Router & InternalRouter - let route = Fixture.route(from: "DCA-Arboretum") + 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 trace = Fixture.generateTrace(for: route).shiftedToPresent() let duration = trace.last!.timestamp.timeIntervalSince(trace.first!.timestamp) @@ -459,8 +469,12 @@ class NavigationServiceTests: XCTestCase { } let fasterRouteName = "DCA-Arboretum-dummy-faster-route" - let fasterRoute = Fixture.route(from: fasterRouteName) - let waypointsForFasterRoute = Fixture.waypoints(from: fasterRouteName) + let options = 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) directions.fireLastCalculateCompletion(with: waypointsForFasterRoute, routes: [fasterRoute], error: nil) XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:didRerouteAlong:at:proactive:)")) @@ -469,7 +483,18 @@ class NavigationServiceTests: XCTestCase { } func testNineLeggedRouteForOutOfBounds() { - let route = Fixture.route(from: "9-legged-route") + let route = Fixture.route(from: "9-legged-route", options: NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 46.423728, longitude: 13.593578), + CLLocationCoordinate2D(latitude: 46.339747, longitude: 13.574151), + CLLocationCoordinate2D(latitude: 46.34447, longitude: 13.57594), + CLLocationCoordinate2D(latitude: 46.37798, longitude: 13.58583), + CLLocationCoordinate2D(latitude: 46.408308, longitude: 13.605585), + CLLocationCoordinate2D(latitude: 46.420338, longitude: 13.602128), + CLLocationCoordinate2D(latitude: 46.429376, longitude: 13.614679), + 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 locationManager = DummyLocationManager() let trace = Fixture.generateTrace(for: route, speedMultiplier: 4).shiftedToPresent() @@ -482,10 +507,473 @@ class NavigationServiceTests: XCTestCase { } } +// func testDefaultUserInterfaceUsage() { +// XCTAssertTrue(dependencies.navigationService.eventsManager.usesDefaultUserInterface, "MapboxCoreNavigationTests should have an implicit dependency on MapboxNavigation due to running inside the Example application target.") +// } +// +// func testUserIsOnRoute() { +// let navigation = dependencies.navigationService +// let firstLocation = dependencies.routeLocations.firstLocation +// +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocation]) +// XCTAssertTrue(navigation.router.userIsOnRoute(firstLocation), "User should be on route") +// } +// +// func testUserIsOffRoute() { +// let navigation = dependencies.navigationService +// let route = navigation.route +// +// let coordinates = route.shape!.coordinates.prefix(3) +// let now = Date() +// let locations = coordinates.enumerated().map { CLLocation(coordinate: $0.element, +// altitude: -1, horizontalAccuracy: 10, verticalAccuracy: -1, course: -1, speed: 10, timestamp: now + $0.offset) } +// +// locations.forEach { navigation.router!.locationManager!(navigation.locationManager, didUpdateLocations: [$0]) } +// +// XCTAssertTrue(navigation.router.userIsOnRoute(locations.last!), "User should be on route") +// +// let coordinatesOffRoute: [CLLocationCoordinate2D] = (0...3).map { _ in locations.first!.coordinate.coordinate(at: 100, facing: 90) } +// let locationsOffRoute = coordinatesOffRoute.enumerated().map { +// CLLocation(coordinate: $0.element, altitude: -1, horizontalAccuracy: 10, +// verticalAccuracy: -1, course: -1, speed: 10, +// timestamp: now + locations.count + $0.offset) +// } +// +// locationsOffRoute.forEach { navigation.router!.locationManager!(navigation.locationManager, didUpdateLocations: [$0]) } +// +// XCTAssertFalse(navigation.router.userIsOnRoute(locationsOffRoute.last!), "User should be off route") +// } +// +// func testAdvancingToFutureStepAndNotRerouting() { +// let navigation = dependencies.navigationService +// let route = navigation.route +// +// let firstStepCoordinates = route.legs[0].steps[0].shape!.coordinates +// let now = Date() +// let firstStepLocations = firstStepCoordinates.enumerated().map { +// CLLocation(coordinate: $0.element, altitude: -1, horizontalAccuracy: 10, verticalAccuracy: -1, course: -1, speed: 10, timestamp: now + $0.offset) +// } +// +// firstStepLocations.forEach { navigation.router!.locationManager!(navigation.locationManager, didUpdateLocations: [$0]) } +// XCTAssertTrue(navigation.router.userIsOnRoute(firstStepLocations.last!), "User should be on route") +// XCTAssertEqual(navigation.router.routeProgress.currentLegProgress.stepIndex, 1, "User is on first step") +// +// let thirdStepCoordinates = route.legs[0].steps[2].shape!.coordinates +// let thirdStepLocations = thirdStepCoordinates.enumerated().map { +// CLLocation(coordinate: $0.element, altitude: -1, horizontalAccuracy: 10, verticalAccuracy: -1, course: -1, speed: 10, timestamp: now + firstStepCoordinates.count + $0.offset) +// } +// +// thirdStepLocations.forEach { navigation.router!.locationManager!(navigation.locationManager, didUpdateLocations: [$0]) } +// +// XCTAssertTrue(navigation.router.userIsOnRoute(thirdStepLocations.last!), "User should be on route") +// XCTAssertEqual(navigation.router.routeProgress.currentLegProgress.stepIndex, 3, "User should be on route and we should increment all the way to the 4th step") +// } +// +// func testSnappedLocation() { +// let navigation = dependencies.navigationService +// let firstLocation = dependencies.routeLocations.firstLocation +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocation]) +// XCTAssertEqual(navigation.router.location!.coordinate.latitude, firstLocation.coordinate.latitude, accuracy: 0.0005, "Check snapped location is working") +// XCTAssertEqual(navigation.router.location!.coordinate.longitude, firstLocation.coordinate.longitude, accuracy: 0.0005, "Check snapped location is working") +// } +// +// func testSnappedAtEndOfStepLocationWhenMovingSlowly() { +// let navigation = dependencies.navigationService +// let firstLocation = dependencies.routeLocations.firstLocation +// +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocation]) +// XCTAssertEqual(navigation.router.location!.coordinate, firstLocation.coordinate, "Check snapped location is working") +// +// let firstCoordinateOnUpcomingStep = navigation.router.routeProgress.currentLegProgress.upcomingStep!.shape!.coordinates.first! +// let firstLocationOnNextStepWithNoSpeed = CLLocation(coordinate: firstCoordinateOnUpcomingStep, altitude: 0, horizontalAccuracy: 10, verticalAccuracy: 10, course: 10, speed: 0, timestamp: Date()) +// +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocationOnNextStepWithNoSpeed]) +// XCTAssertEqual(navigation.router.location!.coordinate, navigation.router.routeProgress.currentLegProgress.currentStep.shape!.coordinates.last!, "When user is not moving, snap to current leg only") +// +// let firstLocationOnNextStepWithSpeed = CLLocation(coordinate: firstCoordinateOnUpcomingStep, altitude: 0, horizontalAccuracy: 10, verticalAccuracy: 10, course: 10, speed: 5, timestamp: Date()) +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocationOnNextStepWithSpeed]) +// +// XCTAssertEqual(navigation.router.location!.coordinate.latitude, firstCoordinateOnUpcomingStep.latitude, accuracy: 0.0005, "User is snapped to upcoming step when moving") +// XCTAssertEqual(navigation.router.location!.coordinate.longitude, firstCoordinateOnUpcomingStep.longitude, accuracy: 0.0005, "User is snapped to upcoming step when moving") +// } +// +// func testSnappedAtEndOfStepLocationWhenCourseIsSimilar() { +// let navigation = dependencies.navigationService +// let firstLocation = dependencies.routeLocations.firstLocation +// +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocation]) +// XCTAssertEqual(navigation.router.location!.coordinate, firstLocation.coordinate, "Check snapped location is working") +// +// let firstCoordinateOnUpcomingStep = navigation.router.routeProgress.currentLegProgress.upcomingStep!.shape!.coordinates.first! +// +// let finalHeading = navigation.router.routeProgress.currentLegProgress.upcomingStep!.finalHeading! +// let firstLocationOnNextStepWithDifferentCourse = CLLocation(coordinate: firstCoordinateOnUpcomingStep, altitude: 0, horizontalAccuracy: 30, verticalAccuracy: 10, course: -finalHeading, speed: 5, timestamp: Date()) +// +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocationOnNextStepWithDifferentCourse]) +// XCTAssertEqual(navigation.router.location!.coordinate, navigation.router.routeProgress.currentLegProgress.currentStep.shape!.coordinates.last!, "When user's course is dissimilar from the finalHeading, they should not snap to upcoming step") +// +// let firstLocationOnNextStepWithCorrectCourse = CLLocation(coordinate: firstCoordinateOnUpcomingStep, altitude: 0, horizontalAccuracy: 30, verticalAccuracy: 10, course: finalHeading, speed: 0, timestamp: Date()) +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocationOnNextStepWithCorrectCourse]) +// XCTAssertEqual(navigation.router.location!.coordinate, firstCoordinateOnUpcomingStep, "User is snapped to upcoming step when their course is similar to the final heading") +// } +// +// func testSnappedLocationForUnqualifiedLocation() { +// let navigation = dependencies.navigationService +// let firstLocation = dependencies.routeLocations.firstLocation +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocation]) +// XCTAssertEqual(navigation.router.location!.coordinate, firstLocation.coordinate, "Check snapped location is working") +// +// let futureCoord = navigation.router.routeProgress.nearbyShape.coordinateFromStart(distance: 100)! +// let futureInaccurateLocation = CLLocation(coordinate: futureCoord, altitude: 0, horizontalAccuracy: 1, verticalAccuracy: 200, course: 0, speed: 5, timestamp: Date()) +// +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [futureInaccurateLocation]) +// +// XCTAssertEqual(navigation.router.location!.coordinate.latitude, futureInaccurateLocation.coordinate.latitude, accuracy: 0.0005, "Inaccurate location is still snapped") +// XCTAssertEqual(navigation.router.location!.coordinate.longitude, futureInaccurateLocation.coordinate.longitude, accuracy: 0.0005, "Inaccurate location is still snapped") +// } +// +// 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: [ +// CLLocationCoordinate2D(latitude: 37.77735, longitude: -122.461465), +// CLLocationCoordinate2D(latitude: 37.777016, longitude: -122.468832), +// ])) +// +// route.accessToken = "foo" +// let navigation = MapboxNavigationService(route: route, directions: directions) +// let router = navigation.router! +// let firstCoord = router.routeProgress.nearbyShape.coordinates.first! +// let firstLocation = CLLocation(latitude: firstCoord.latitude, longitude: firstCoord.longitude) +// let coordNearStart = router.routeProgress.nearbyShape.coordinateFromStart(distance: 10)! +// +// navigation.locationManager(navigation.locationManager, didUpdateLocations: [firstLocation]) +// +// // We're now 10 meters away from the last coord, looking at the start. +// // Basically, simulating moving backwards. +// let directionToStart = coordNearStart.direction(to: firstCoord) +// let facingTowardsStartLocation = CLLocation(coordinate: coordNearStart, altitude: 0, horizontalAccuracy: 0, verticalAccuracy: 0, course: directionToStart, speed: 0, timestamp: Date()) +// +// navigation.locationManager(navigation.locationManager, didUpdateLocations: [facingTowardsStartLocation]) +// +// // The course should not be the interpolated course, rather the raw course. +// XCTAssertEqual(directionToStart, router.location!.course, "The course should be the raw course and not an interpolated course") +// XCTAssertFalse(facingTowardsStartLocation.shouldSnap(toRouteWith: facingTowardsStartLocation.interpolatedCourse(along: router.routeProgress.nearbyShape)!, distanceToFirstCoordinateOnLeg: facingTowardsStartLocation.distance(from: firstLocation)), "Should not snap") +// } +// +// //TODO: Broken by PortableRoutecontroller & MBNavigator -- needs team discussion. +// func x_testLocationShouldUseHeading() { +// let navigation = dependencies.navigationService +// let firstLocation = dependencies.routeLocations.firstLocation +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [firstLocation]) +// +// XCTAssertEqual(navigation.router.location!.course, firstLocation.course, "Course should be using course") +// +// let invalidCourseLocation = CLLocation(coordinate: firstLocation.coordinate, altitude: firstLocation.altitude, horizontalAccuracy: firstLocation.horizontalAccuracy, verticalAccuracy: firstLocation.verticalAccuracy, course: -1, speed: firstLocation.speed, timestamp: firstLocation.timestamp) +// +// let heading = CLHeading(heading: mbTestHeading, accuracy: 1)! +// +// navigation.locationManager!(navigation.locationManager, didUpdateLocations: [invalidCourseLocation]) +// navigation.locationManager!(navigation.locationManager, didUpdateHeading: heading) +// +// XCTAssertEqual(navigation.router.location!.course, mbTestHeading, "Course should be using bearing") +// } +// +// // MARK: - Events & Delegation +// +// func testTurnstileEventSentUponInitialization() { +// // MARK: it sends a turnstile event upon initialization +// +// let service = MapboxNavigationService(route: initialRoute, 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) +// navigationService.delegate = delegate +// let router = navigationService.router! +// +// navigationService.eventsManager.delaysEventFlushing = false +// navigationService.start() +// +// let eventsManagerSpy = navigationService.eventsManager as! NavigationEventsManagerSpy +// XCTAssertTrue(eventsManagerSpy.hasFlushedEvent(with: NavigationEventTypeRouteRetrieval)) +// +// router.route = alternateRoute +// +// let simulatedLocationManager = navigationService.locationManager as! SimulatedLocationManager +// +// XCTAssert(simulatedLocationManager.route == alternateRoute, "Simulated Location Manager should be updated with new route progress model") +// } +// +// func testReroutingFromALocationSendsEvents() { +// let navigationService = dependencies.navigationService +// let router = navigationService.router! +// let testLocation = dependencies.routeLocations.firstLocation +// +// navigationService.eventsManager.delaysEventFlushing = false +// +// let willRerouteNotificationExpectation = expectation(forNotification: .routeControllerWillReroute, object: router) { (notification) -> Bool in +// let fromLocation = notification.userInfo![RouteControllerNotificationUserInfoKey.locationKey] as? CLLocation +// return fromLocation == testLocation +// } +// +// let didRerouteNotificationExpectation = expectation(forNotification: .routeControllerDidReroute, object: router, handler: nil) +// +// let routeProgressDidChangeNotificationExpectation = expectation(forNotification: .routeControllerProgressDidChange, object: router) { (notification) -> Bool in +// let location = notification.userInfo![RouteControllerNotificationUserInfoKey.locationKey] as? CLLocation +// let rawLocation = notification.userInfo![RouteControllerNotificationUserInfoKey.rawLocationKey] as? CLLocation +// let _ = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress +// +// return location!.distance(from: rawLocation!) <= 0.0005 +// } +// +// // MARK: When told to re-route from location -- `reroute(from:)` +// router.reroute(from: testLocation, along: router.routeProgress) +// +// // MARK: it tells the delegate & posts a willReroute notification +// XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:willRerouteFrom:)")) +// wait(for: [willRerouteNotificationExpectation], timeout: 0.1) +// +// // MARK: Upon rerouting successfully... +// directionsClientSpy.fireLastCalculateCompletion(with: nil, routes: [alternateRoute], error: nil) +// +// // MARK: It tells the delegate & posts a didReroute notification +// XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:didRerouteAlong:at:proactive:)")) +// wait(for: [didRerouteNotificationExpectation], timeout: 0.1) +// +// // MARK: On the next call to `locationManager(_, didUpdateLocations:)` +// navigationService.locationManager!(navigationService.locationManager, didUpdateLocations: [testLocation]) +// +// // MARK: It tells the delegate & posts a routeProgressDidChange notification +// XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:didUpdate:with:rawLocation:)")) +// wait(for: [routeProgressDidChangeNotificationExpectation], timeout: 0.1) +// +// // MARK: It enqueues and flushes a NavigationRerouteEvent +// let expectedEventName = MMEEventTypeNavigationReroute +// let eventsManagerSpy = navigationService.eventsManager as! NavigationEventsManagerSpy +// XCTAssertTrue(eventsManagerSpy.hasEnqueuedEvent(with: expectedEventName)) +// XCTAssertTrue(eventsManagerSpy.hasFlushedEvent(with: expectedEventName)) +// XCTAssertEqual(eventsManagerSpy.enqueuedEventCount(with: expectedEventName), 1) +// XCTAssertEqual(eventsManagerSpy.flushedEventCount(with: expectedEventName), 1) +// } +// +// func testGeneratingAnArrivalEvent() { +// let navigation = dependencies.navigationService +// +// let now = Date() +// let trace = Fixture.generateTrace(for: route).shiftedToPresent() +// trace.forEach { navigation.router!.locationManager!(navigation.locationManager, didUpdateLocations: [$0]) } +// +// // TODO: Verify why we need a second location update when routeState == .complete to trigger `MMEEventTypeNavigationArrive` +// navigation.router!.locationManager!(navigation.locationManager, +// didUpdateLocations: [trace.last!.shifted(to: now + (trace.count + 1))]) +// +// // MARK: It queues and flushes a Depart event +// let eventsManagerSpy = navigation.eventsManager as! NavigationEventsManagerSpy +// XCTAssertTrue(eventsManagerSpy.hasFlushedEvent(with: MMEEventTypeNavigationDepart)) +// +// // MARK: When at a valid location just before the last location +// XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:willArriveAt:after:distance:)"), "Pre-arrival delegate message not fired.") +// +// // MARK: It tells the delegate that the user did arrive +// XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:didArriveAt:)")) +// +// // MARK: It enqueues and flushes an arrival event +// let expectedEventName = MMEEventTypeNavigationArrive +// XCTAssertTrue(eventsManagerSpy.hasEnqueuedEvent(with: expectedEventName)) +// XCTAssertTrue(eventsManagerSpy.hasFlushedEvent(with: expectedEventName)) +// } +// +// func testNoReroutesAfterArriving() { +// let navigation = dependencies.navigationService +// +// // MARK: When navigation begins with a location update +// let now = Date() +// let trace = Fixture.generateTrace(for: route).shiftedToPresent() +// +// trace.forEach { navigation.router.locationManager!(navigation.locationManager, didUpdateLocations: [$0]) } +// +// let eventsManagerSpy = navigation.eventsManager as! NavigationEventsManagerSpy +// XCTAssertTrue(eventsManagerSpy.hasFlushedEvent(with: MMEEventTypeNavigationDepart)) +// +// // MARK: It tells the delegate that the user did arrive +// XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:didArriveAt:)")) +// +// // MARK: Continue off route after arrival +// let offRouteCoordinate = trace.map { $0.coordinate }.last!.coordinate(at: 200, facing: 0) +// let offRouteLocations = (0...3).map { +// CLLocation(coordinate: offRouteCoordinate, altitude: -1, horizontalAccuracy: 10, verticalAccuracy: -1, course: -1, speed: 10, timestamp: now + trace.count + $0) +// } +// +// offRouteLocations.forEach { navigation.router.locationManager!(navigation.locationManager, didUpdateLocations: [$0]) } +// +// // Make sure configurable delegate is called +// XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:shouldPreventReroutesWhenArrivingAt:)")) +// +// // We should not reroute here because the user has arrived. +// XCTAssertFalse(delegate.recentMessages.contains("navigationService(_:didRerouteAlong:)")) +// +// // It enqueues and flushes an arrival event +// let expectedEventName = MMEEventTypeNavigationArrive +// XCTAssertTrue(eventsManagerSpy.hasEnqueuedEvent(with: expectedEventName)) +// XCTAssertTrue(eventsManagerSpy.hasFlushedEvent(with: expectedEventName)) +// } +// +// func testRouteControllerDoesNotHaveRetainCycle() { +// weak var subject: RouteController? = nil +// +// autoreleasepool { +// let fakeDataSource = RouteControllerDataSourceFake() +// let routeController = RouteController(along: initialRoute, directions: directionsClientSpy, dataSource: fakeDataSource) +// subject = routeController +// } +// +// XCTAssertNil(subject, "Expected RouteController not to live beyond autorelease pool") +// } +// +// func testLegacyRouteControllerDoesNotHaveRetainCycle() { +// weak var subject: LegacyRouteController? = nil +// +// autoreleasepool { +// let fakeDataSource = RouteControllerDataSourceFake() +// let routeController = LegacyRouteController(along: initialRoute, directions: directionsClientSpy, dataSource: fakeDataSource) +// subject = routeController +// } +// +// XCTAssertNil(subject, "Expected LegacyRouteController not to live beyond autorelease pool") +// } +// +// func testRouteControllerDoesNotRetainDataSource() { +// weak var subject: RouterDataSource? = nil +// +// autoreleasepool { +// let fakeDataSource = RouteControllerDataSourceFake() +// _ = RouteController(along: initialRoute, directions: directionsClientSpy, dataSource: fakeDataSource) +// subject = fakeDataSource +// } +// +// XCTAssertNil(subject, "Expected LocationManager's Delegate to be nil after RouteController Deinit") +// } +// +// func testCountdownTimerDefaultAndUpdate() { +// let directions = DirectionsSpy(accessToken: "pk.feedCafeDeadBeefBadeBede") +// let subject = MapboxNavigationService(route: initialRoute, directions: directions) +// +// XCTAssert(subject.poorGPSTimer.countdownInterval == .milliseconds(2500), "Default countdown interval should be 2500 milliseconds.") +// +// subject.poorGPSPatience = 5.0 +// XCTAssert(subject.poorGPSTimer.countdownInterval == .milliseconds(5000), "Timer should now have a countdown interval of 5000 millseconds.") +// } +// +// func testMultiLegRoute() { +// let route = Fixture.route(from: "multileg-route", 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 trace = Fixture.generateTrace(for: route).shiftedToPresent().qualified() +// let service = dependencies.navigationService +// +// let routeController = service.router as! RouteController +// routeController.route = route +// +// for (index, location) in trace.enumerated() { +// service.locationManager!(service.locationManager, didUpdateLocations: [location]) +// +// if index < 33 { +// XCTAssert(routeController.routeProgress.legIndex == 0) +// } else { +// XCTAssert(routeController.routeProgress.legIndex == 1) +// } +// } +// +// XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:didArriveAt:)")) +// } +// +// func testProactiveRerouting() { +// typealias RouterComposition = Router & InternalRouter +// +// 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 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) +// service.delegate = delegate +// let router = service.router! +// let locationManager = NavigationLocationManager() +// +// let _ = expectation(forNotification: .routeControllerDidReroute, object: router) { (notification) -> Bool in +// let isProactive = notification.userInfo![RouteControllerNotificationUserInfoKey.isProactiveKey] as? Bool +// return isProactive == true +// } +// let rerouteExpectation = expectation(description: "Proactive reroute should trigger") +// +// for location in trace { +// service.router!.locationManager!(locationManager, didUpdateLocations: [location]) +// +// let router = service.router! as! RouterComposition +// +// if router.lastRerouteLocation != nil { +// rerouteExpectation.fulfill() +// break +// } +// } +// +// let fasterRouteName = "DCA-Arboretum-dummy-faster-route" +// let options = 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) +// directions.fireLastCalculateCompletion(with: waypointsForFasterRoute, routes: [fasterRoute], error: nil) +// +// XCTAssertTrue(delegate.recentMessages.contains("navigationService(_:didRerouteAlong:at:proactive:)")) +// +// waitForExpectations(timeout: 10) +// } +// +// func testNineLeggedRouteForOutOfBounds() { +// let route = Fixture.route(from: "9-legged-route", options: NavigationRouteOptions(coordinates: [ +// CLLocationCoordinate2D(latitude: 46.423728, longitude: 13.593578), +// CLLocationCoordinate2D(latitude: 46.339747, longitude: 13.574151), +// CLLocationCoordinate2D(latitude: 46.34447, longitude: 13.57594), +// CLLocationCoordinate2D(latitude: 46.37798, longitude: 13.58583), +// CLLocationCoordinate2D(latitude: 46.408308, longitude: 13.605585), +// CLLocationCoordinate2D(latitude: 46.420338, longitude: 13.602128), +// CLLocationCoordinate2D(latitude: 46.429376, longitude: 13.614679), +// 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 locationManager = DummyLocationManager() +// let trace = Fixture.generateTrace(for: route, speedMultiplier: 4).shiftedToPresent() +// +// let service = MapboxNavigationService(route: route, directions: directions, locationSource: locationManager, eventsManagerType: nil) +// service.start() +// +// for location in trace { +// service.locationManager(locationManager, didUpdateLocations: [location]) +// } +// } + func testUnimplementedLogging() { unimplementedTestLogs = [] - let route = Fixture.route(from: "DCA-Arboretum") + 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 locationManager = DummyLocationManager() let trace = Fixture.generateTrace(for: route, speedMultiplier: 4).shiftedToPresent() diff --git a/MapboxCoreNavigationTests/OfflineRoutingTests.swift b/MapboxCoreNavigationTests/OfflineRoutingTests.swift index 38f3cea69c3..80a2e79f8a5 100644 --- a/MapboxCoreNavigationTests/OfflineRoutingTests.swift +++ b/MapboxCoreNavigationTests/OfflineRoutingTests.swift @@ -38,7 +38,7 @@ class OfflineRoutingTests: XCTestCase { wait(for: [calculateRouteExpectation], timeout: 2) XCTAssertNotNil(route) - XCTAssertEqual(route!.coordinates!.count, 47) + XCTAssertEqual(route!.shape!.coordinates.count, 47) } func testOfflineDirectionsError() { @@ -64,9 +64,11 @@ class OfflineRoutingTests: XCTestCase { directions.calculate(options, offline: true) { (waypoints, routes, error) in XCTAssertNotNil(error) - let validErrors = ["No suitable edges near location", "Unknown Routing Error"] - let validError = validErrors.contains(error!.localizedDescription) - XCTAssertTrue(validError) + if let error = error, case let .standard(directionsError) = error { + XCTAssertEqual(directionsError, .unableToRoute) + } else { + XCTFail("Error should be standard error") + } XCTAssertNil(routes) XCTAssertNil(waypoints) calculateRouteExpectation.fulfill() diff --git a/MapboxCoreNavigationTests/RouteProgressTests.swift b/MapboxCoreNavigationTests/RouteProgressTests.swift index 82bcda4024d..5b1f192a847 100644 --- a/MapboxCoreNavigationTests/RouteProgressTests.swift +++ b/MapboxCoreNavigationTests/RouteProgressTests.swift @@ -92,31 +92,12 @@ class RouteProgressTests: XCTestCase { let source = options.waypoints.first! let destination = options.waypoints.last! options.shapeFormat = .polyline - let jsonLeg = [ - "steps": [ - [ - "maneuver": [ - "type": "depart", - "location": [source.coordinate.longitude, source.coordinate.latitude], - ], - "name": "", - "mode": "", - "geometry": Polyline(coordinates: routeCoordinates, precision: 1e5).encodedPolyline, - ], - [ - "maneuver": [ - "type": "arrive", - "location": [destination.coordinate.longitude, destination.coordinate.latitude], - ], - "name": "", - "mode": "", - ], - ], - "distance": 0.0, - "duration": 0.0, - "summary": "", - ] as [String: Any] - let leg = RouteLeg(json: jsonLeg, source: source, destination: destination, options: options) + let steps = [ + RouteStep(transportType: .automobile, maneuverLocation: source.coordinate, maneuverType: .depart, maneuverDirection: nil, instructions: "", initialHeading: nil, finalHeading: nil, drivingSide: .right, exitCodes: nil, exitNames: nil, phoneticExitNames: nil, distance: 0, expectedTravelTime: 0, names: nil, phoneticNames: nil, codes: nil, destinationCodes: nil, destinations: nil, intersections: nil, instructionsSpokenAlongStep: nil, instructionsDisplayedAlongStep: nil), + RouteStep(transportType: .automobile, maneuverLocation: destination.coordinate, maneuverType: .arrive, maneuverDirection: nil, instructions: "", initialHeading: nil, finalHeading: nil, drivingSide: .right, exitCodes: nil, exitNames: nil, phoneticExitNames: nil, distance: 0, expectedTravelTime: 0, names: nil, phoneticNames: nil, codes: nil, destinationCodes: nil, destinations: nil, intersections: nil, instructionsSpokenAlongStep: nil, instructionsDisplayedAlongStep: nil), + ] + steps[0].shape = LineString(routeCoordinates) + let leg = RouteLeg(steps: steps, name: "", distance: 0, expectedTravelTime: 0, profileIdentifier: .automobile) return RouteLegProgress(leg: leg) } diff --git a/MapboxCoreNavigationTests/TunnelAuthorityTests.swift b/MapboxCoreNavigationTests/TunnelAuthorityTests.swift index 3c984e76f24..ba4b078959c 100644 --- a/MapboxCoreNavigationTests/TunnelAuthorityTests.swift +++ b/MapboxCoreNavigationTests/TunnelAuthorityTests.swift @@ -6,7 +6,11 @@ import MapboxDirections import TestHelper @testable import MapboxCoreNavigation -let tunnelRoute = Fixture.route(from: "routeWithTunnels_9thStreetDC") +let tunnelRoute = Fixture.route(from: "routeWithTunnels_9thStreetDC", options: { + 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]) +}()) class TunnelAuthorityTests: XCTestCase { lazy var locationManager = NavigationLocationManager() @@ -15,7 +19,7 @@ class TunnelAuthorityTests: XCTestCase { let routeProgress = RouteProgress(route: tunnelRoute) // Mock location move to first coordinate on tunnel route - let firstCoordinate = tunnelRoute.coordinates!.first! + let firstCoordinate = tunnelRoute.shape!.coordinates.first! let firstLocation = CLLocation(latitude: firstCoordinate.latitude, longitude: firstCoordinate.longitude) // Test outside tunnel diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index e5483cf8fe9..44e3c51c827 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -229,6 +229,8 @@ 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 */; }; + 43FB386923A202420064481E /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0557202154EF4700A1F2AA /* Route.swift */; }; + 43FB386B23A2024C0064481E /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43FB386A23A2024C0064481E /* Route.swift */; }; 492B6F84213703D10076D2C6 /* MapboxGeocoder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3507F9F92134305C0086B39E /* MapboxGeocoder.framework */; }; 492B6F85213703EE0076D2C6 /* MapboxGeocoder.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3507F9F92134305C0086B39E /* MapboxGeocoder.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6441B16A1EFC64E50076499F /* WaypointConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6441B1691EFC64E50076499F /* WaypointConfirmationViewController.swift */; }; @@ -357,7 +359,6 @@ C5F4D21920DC468B0059FABF /* CongestionLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F4D21820DC468B0059FABF /* CongestionLevel.swift */; }; C5FFAC1520D96F5C009E7F98 /* CarPlayNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5FFAC1420D96F5B009E7F98 /* CarPlayNavigationViewController.swift */; }; CFD47D9020FD85EC00BC1E49 /* MGLAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFD47D8F20FD85EC00BC1E49 /* MGLAccountManager.swift */; }; - DA0557232154FFB200A1F2AA /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0557202154EF4700A1F2AA /* Route.swift */; }; DA0557252155040700A1F2AA /* RouteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0557242155040700A1F2AA /* RouteTests.swift */; }; DA1755F82357B6BD00B06C1D /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5A60EC820A2417200C21178 /* StringTests.swift */; }; DA1755F92357B7A100B06C1D /* md5_crazy_strings.txt in Resources */ = {isa = PBXBuildFile; fileRef = C5A60ECA20A241B600C21178 /* md5_crazy_strings.txt */; }; @@ -376,6 +377,7 @@ DA303CAB21B7A93B00F921DC /* OfflineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3529FCF921A5C63A00AEA9AA /* OfflineViewController.swift */; }; DA3525702010A5210048DDFC /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DA35256E2010A5200048DDFC /* Localizable.stringsdict */; }; DA443DDE2278C90E00ED1307 /* CPTrip.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA443DDD2278C90E00ED1307 /* CPTrip.swift */; }; + DA66063023B32F99007832E5 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA66062F23B32F99007832E5 /* Array.swift */; }; DA8805002316EAED00B54D87 /* ViewController+GuidanceCards.swift in Sources */ = {isa = PBXBuildFile; fileRef = AED6285522CBE4CE00058A51 /* ViewController+GuidanceCards.swift */; }; DAA96D18215A961D00BEF703 /* route-doubling-back.json in Resources */ = {isa = PBXBuildFile; fileRef = DAA96D17215A961D00BEF703 /* route-doubling-back.json */; }; DAAE5F301EAE4C4700832871 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DAAE5F321EAE4C4700832871 /* Localizable.strings */; }; @@ -807,6 +809,7 @@ 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 = ""; }; 43E69517233D297B0019BF6E /* cover.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = cover.md; path = docs/cover.md; sourceTree = ""; }; + 43FB386A23A2024C0064481E /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; }; 6441B1691EFC64E50076499F /* WaypointConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WaypointConfirmationViewController.swift; path = Example/WaypointConfirmationViewController.swift; sourceTree = ""; }; 64847A031F04629D003F3A69 /* Feedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feedback.swift; sourceTree = ""; }; 7C12F2D7225B7C310010A931 /* DCA-Arboretum-dummy-faster-route.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "DCA-Arboretum-dummy-faster-route.json"; sourceTree = ""; }; @@ -974,6 +977,7 @@ DA625EA71F10616600FBE176 /* es */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; DA625EA91F1061DA00FBE176 /* sv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Navigation.strings; sourceTree = ""; }; DA625EAA1F10621A00FBE176 /* vi */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Navigation.strings; sourceTree = ""; }; + DA66062F23B32F99007832E5 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; DA678B7A1F6CEE6200F05913 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; DA678B7B1F6CF46600F05913 /* hu */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; DA678B7C1F6CF47200F05913 /* sv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; @@ -1540,8 +1544,10 @@ children = ( C5381F01204E03B600A5493E /* UIDevice.swift */, 352AFFA22139986E00EB3567 /* UIViewController.swift */, + DA66062F23B32F99007832E5 /* Array.swift */, 351BEC081E5BCC72006FE110 /* Bundle.swift */, 8D24A2F720409A890098CBF8 /* CGSize.swift */, + DA0557202154EF4700A1F2AA /* Route.swift */, C5F4D21820DC468B0059FABF /* CongestionLevel.swift */, C51511D020EAC89D00372A91 /* CPMapTemplate.swift */, DA443DDD2278C90E00ED1307 /* CPTrip.swift */, @@ -1553,7 +1559,6 @@ 353E3C8E20A3501C00FD1789 /* MGLStyle.swift */, C58159001EA6D02700FC6C3D /* MGLVectorTileSource.swift */, 8D5DFFF0207C04840093765A /* NSAttributedString.swift */, - DA0557202154EF4700A1F2AA /* Route.swift */, 35B7837D1F9547B300291F9A /* Transitioning.swift */, 359D283B1F9DC14F00FDE9C9 /* UICollectionView.swift */, 8D24A2F52040960C0098CBF8 /* UIEdgeInsets.swift */, @@ -1619,6 +1624,7 @@ C561735A1F182113005954F6 /* RouteStep.swift */, 351927351F0FA072003A702D /* ScreenCapture.swift */, 35A5413A1EFC052700E49846 /* RouteOptions.swift */, + 43FB386A23A2024C0064481E /* Route.swift */, C57491DE1FACC42F006F97BC /* CGPoint.swift */, C582FD5E203626E900A9086E /* CLLocationDirection.swift */, 35C98732212E037900808B82 /* MBNavigator.swift */, @@ -2398,7 +2404,6 @@ C5FFAC1520D96F5C009E7F98 /* CarPlayNavigationViewController.swift in Sources */, 8DE879661FBB9980002F06C0 /* EndOfRouteViewController.swift in Sources */, AE47A33422B1F6AE0096458C /* InstructionsCardContainerView.swift in Sources */, - DA0557232154FFB200A1F2AA /* Route.swift in Sources */, 8D24A2F62040960C0098CBF8 /* UIEdgeInsets.swift in Sources */, 353E3C8F20A3501C00FD1789 /* MGLStyle.swift in Sources */, C57491DF1FACC42F006F97BC /* CGPoint.swift in Sources */, @@ -2429,6 +2434,7 @@ 351BEC0D1E5BCC72006FE110 /* Bundle.swift in Sources */, 8DF399B21FB257B30034904C /* UIGestureRecognizer.swift in Sources */, 35B7837E1F9547B300291F9A /* Transitioning.swift in Sources */, + DA66063023B32F99007832E5 /* Array.swift in Sources */, 8DEDEF3421E3FBE80049E114 /* NavigationViewControllerDelegate.swift in Sources */, 8D5DFFF1207C04840093765A /* NSAttributedString.swift in Sources */, C565168B1FE1E23E00A0AD18 /* MapboxVoiceController.swift in Sources */, @@ -2446,6 +2452,7 @@ 351BEC021E5BCC63006FE110 /* UIView.swift in Sources */, 160D8279205996DA00D278D6 /* DataCache.swift in Sources */, 351BEBF21E5BCC63006FE110 /* Style.swift in Sources */, + 43FB386923A202420064481E /* Route.swift in Sources */, C5D1C9941FB236900067C619 /* ErrorCode.swift in Sources */, 3EA937B1F4DF73EB004BA6BE /* InstructionPresenter.swift in Sources */, 3EA93A1FEFDDB709DE84BED9 /* ImageRepository.swift in Sources */, @@ -2629,6 +2636,7 @@ C58D6BAD1DDCF2AE00387F53 /* CoreConstants.swift in Sources */, 35C77F621FE8219900338416 /* NavigationSettings.swift in Sources */, C51DF8661F38C31C006C6A15 /* Locale.swift in Sources */, + 43FB386B23A2024C0064481E /* Route.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MapboxNavigation/Array.swift b/MapboxNavigation/Array.swift new file mode 100644 index 00000000000..f35cf1713ff --- /dev/null +++ b/MapboxNavigation/Array.swift @@ -0,0 +1,34 @@ +extension Array { + /** + Conditionally remove each element depending on the elements immediately preceding and following it. + + - parameter shouldBeRemoved: A closure that is called once for each element in reverse order from last to first. The closure accepts the following arguments: the preceding element in the (unreversed) array, the element itself, and the following element in the (unreversed) array. + */ + mutating func removeSeparators(where shouldBeRemoved: (Element?, Element, Element?) throws -> Bool) rethrows { + for (index, element) in enumerated().reversed() { + let precedingElement = lazy.prefix(upTo: index).last + let followingElement = lazy.suffix(from: self.index(after: index)).first + if try shouldBeRemoved(precedingElement, element, followingElement) { + remove(at: index) + } + } + } +} + +extension Array where Element: NSAttributedString { + /** + Returns a new attributed string by concatenating the elements of the array, adding the given separator between each element. + */ + func joined(separator: NSAttributedString = .init()) -> NSAttributedString { + guard let first = first else { + return NSAttributedString() + } + + let joinedAttributedString = NSMutableAttributedString(attributedString: first) + for element in dropFirst() { + joinedAttributedString.append(separator) + joinedAttributedString.append(element) + } + return joinedAttributedString + } +} diff --git a/MapboxNavigation/CarPlayManager.swift b/MapboxNavigation/CarPlayManager.swift index 877d62e644d..ea1a0b08bc8 100644 --- a/MapboxNavigation/CarPlayManager.swift +++ b/MapboxNavigation/CarPlayManager.swift @@ -402,7 +402,7 @@ extension CarPlayManager { directions.calculate(options, completionHandler: completionHandler) } - internal func didCalculate(_ routes: [Route]?, for routeOptions: RouteOptions, between waypoints: [Waypoint]?, error: NSError?, completionHandler: CompletionHandler) { + internal func didCalculate(_ routes: [Route]?, for routeOptions: RouteOptions, between waypoints: [Waypoint]?, error: DirectionsError?, completionHandler: CompletionHandler) { defer { completionHandler() } diff --git a/MapboxNavigation/CarPlayManagerDelegate.swift b/MapboxNavigation/CarPlayManagerDelegate.swift index b62c54d0361..2aa8fd7135d 100644 --- a/MapboxNavigation/CarPlayManagerDelegate.swift +++ b/MapboxNavigation/CarPlayManagerDelegate.swift @@ -101,7 +101,7 @@ public protocol CarPlayManagerDelegate: class, UnimplementedLogging { - returns: Optionally, a `CPNavigationAlert` to present to the user. If this method returns an alert, the CarPlay manager will transition back to the map template and display the alert. If it returns `nil`, the CarPlay manager will do nothing. - note: This delegate method includes a default implementation that prints a warning to the console when this method is called. See `UnimplementedLogging` for details. */ - func carPlayManager(_ carPlayManager: CarPlayManager, didFailToFetchRouteBetween waypoints: [Waypoint]?, options: RouteOptions, error: NSError) -> CPNavigationAlert? + func carPlayManager(_ carPlayManager: CarPlayManager, didFailToFetchRouteBetween waypoints: [Waypoint]?, options: RouteOptions, error: DirectionsError) -> CPNavigationAlert? /** Offers the delegate the opportunity to customize a trip before it is presented to the user to preview. @@ -190,7 +190,7 @@ public extension CarPlayManagerDelegate { logUnimplemented(protocolType: CarPlayManagerDelegate.self, level: .debug) } - func carPlayManager(_ carPlayManager: CarPlayManager, didFailToFetchRouteBetween waypoints: [Waypoint]?, options: RouteOptions, error: NSError) -> CPNavigationAlert? { + func carPlayManager(_ carPlayManager: CarPlayManager, didFailToFetchRouteBetween waypoints: [Waypoint]?, options: RouteOptions, error: DirectionsError) -> CPNavigationAlert? { logUnimplemented(protocolType: CarPlayManagerDelegate.self, level: .debug) return nil } diff --git a/MapboxNavigation/CarPlayNavigationViewController.swift b/MapboxNavigation/CarPlayNavigationViewController.swift index 2ffd4415e58..dc49d3a0255 100644 --- a/MapboxNavigation/CarPlayNavigationViewController.swift +++ b/MapboxNavigation/CarPlayNavigationViewController.swift @@ -260,7 +260,7 @@ public class CarPlayNavigationViewController: UIViewController, NavigationMapVie } else if tracksUserCourse && !newValue { isOverviewingRoutes = !isPanningAway guard let userLocation = self.navigationService.router.location?.coordinate, - let coordinates = navigationService.route.coordinates else { + let coordinates = navigationService.route.shape?.coordinates else { return } mapView?.enableFrameByFrameCourseViewTracking(for: 1) @@ -392,7 +392,7 @@ public class CarPlayNavigationViewController: UIViewController, NavigationMapVie var maneuvers: [CPManeuver] = [primaryManeuver] // Add tertiary text if available. TODO: handle lanes. - if let tertiaryInstruction = visualInstruction.tertiaryInstruction, !tertiaryInstruction.containsLaneIndications { + if let tertiaryInstruction = visualInstruction.tertiaryInstruction, tertiaryInstruction.laneComponents.isEmpty { let tertiaryManeuver = CPManeuver() tertiaryManeuver.symbolSet = tertiaryInstruction.maneuverImageSet(side: visualInstruction.drivingSide) @@ -493,7 +493,7 @@ extension CarPlayNavigationViewController: StyleManagerDelegate { public func location(for styleManager: StyleManager) -> CLLocation? { if let location = navigationService.router.location { return location - } else if let origin = navigationService.route.coordinates?.first { + } else if let origin = navigationService.route.shape?.coordinates.first { return CLLocation(latitude: origin.latitude, longitude: origin.longitude) } else { return nil diff --git a/MapboxNavigation/ExitView.swift b/MapboxNavigation/ExitView.swift index 84002b64bab..99a83e9613b 100644 --- a/MapboxNavigation/ExitView.swift +++ b/MapboxNavigation/ExitView.swift @@ -1,7 +1,7 @@ import UIKit enum ExitSide: String{ - case left, right, other + case left, right var exitImage: UIImage { return self == .left ? ExitView.leftExitImage : ExitView.rightExitImage diff --git a/MapboxNavigation/InstructionPresenter.swift b/MapboxNavigation/InstructionPresenter.swift index 57f4ba439c5..f4b4eec5fad 100644 --- a/MapboxNavigation/InstructionPresenter.swift +++ b/MapboxNavigation/InstructionPresenter.swift @@ -10,6 +10,20 @@ protocol InstructionPresenterDataSource: class { typealias DataSource = InstructionPresenterDataSource +extension NSAttributedString.Key { + /** + A string containing an abbreviation that can be substituted for the substring when there is not enough room to display the original substring. + */ + static let abbreviation = NSAttributedString.Key(rawValue: "MBVisualInstructionComponentAbbreviation") + + /** + A number indicating the priority for which the substring should be substituted with the abbreviation specified by the `NSAttributedString.Key.abbreviation` key. + + A substring with a lower abbreviation priority value should be abbreviated before a substring with a higher abbreviation priority value. + */ + static let abbreviationPriority = NSAttributedString.Key(rawValue: "MBVisualInstructionComponentAbbreviationPriority") +} + class InstructionPresenter { private let instruction: VisualInstruction private weak var dataSource: DataSource? @@ -29,143 +43,123 @@ class InstructionPresenter { private let imageRepository: ImageRepository func attributedText() -> NSAttributedString { - let string = NSMutableAttributedString() - fittedAttributedComponents().forEach { string.append($0) } - return string - } - - func fittedAttributedComponents() -> [NSAttributedString] { - guard let source = self.dataSource else { return [] } - var attributedPairs = self.attributedPairs(for: instruction, dataSource: source, imageRepository: imageRepository, onImageDownload: completeShieldDownload) - let availableBounds = source.availableBounds() - let totalWidth: CGFloat = attributedPairs.attributedStrings.map { $0.size() }.reduce(.zero, +).width - let stringFits = totalWidth <= availableBounds.width + guard let source = self.dataSource else { + return NSAttributedString() + } - guard !stringFits else { return attributedPairs.attributedStrings } + let attributedTextRepresentation = self.attributedTextRepresentation(of: instruction, dataSource: source, imageRepository: imageRepository, onImageDownload: completeShieldDownload).mutableCopy() as! NSMutableAttributedString - let indexedComponents: [IndexedVisualInstructionComponent] = attributedPairs.components.enumerated().map { IndexedVisualInstructionComponent(component: $1, index: $0) } - let filtered = indexedComponents.filter { $0.component.abbreviation != nil } - let sorted = filtered.sorted { $0.component.abbreviationPriority < $1.component.abbreviationPriority } - for component in sorted { - let isFirst = component.index == 0 - let joinChar = isFirst ? "" : " " - guard component.component.type == .text else { continue } - guard let abbreviation = component.component.abbreviation else { continue } - - attributedPairs.attributedStrings[component.index] = NSAttributedString(string: joinChar + abbreviation, attributes: attributes(for: source)) - let newWidth: CGFloat = attributedPairs.attributedStrings.map { $0.size() }.reduce(.zero, +).width - - if newWidth <= availableBounds.width { - break + // Collect abbreviation priorities embedded in the attributed text representation. + let wholeRange = NSRange(location: 0, length: attributedTextRepresentation.length) + var priorities = IndexSet() + attributedTextRepresentation.enumerateAttribute(.abbreviationPriority, in: wholeRange, options: .longestEffectiveRangeNotRequired) { (priority, range, stop) in + if let priority = priority as? Int { + priorities.insert(priority) } } - return attributedPairs.attributedStrings - } - - typealias AttributedInstructionComponents = (components: [VisualInstructionComponent], attributedStrings: [NSAttributedString]) - - func attributedPairs(for instruction: VisualInstruction, dataSource: DataSource, imageRepository: ImageRepository, onImageDownload: @escaping ImageDownloadCompletion) -> AttributedInstructionComponents { - let components = instruction.components.compactMap { $0 as? VisualInstructionComponent } - var strings: [NSAttributedString] = [] - var processedComponents: [VisualInstructionComponent] = [] - - for (index, component) in components.enumerated() { - let isFirst = index == 0 - let joinChar = isFirst ? "" : " " - let joinString = NSAttributedString(string: joinChar, attributes: attributes(for: dataSource)) - let initial = NSAttributedString() - - //This is the closure that builds the string. - let build: (_: VisualInstructionComponent, _: [NSAttributedString]) -> Void = { (component, attributedStrings) in - processedComponents.append(component) - strings.append(attributedStrings.reduce(initial, +)) - } - let isShield: (_: VisualInstructionComponent?) -> Bool = { (component) in - guard let key = component?.cacheKey else { return false } - return imageRepository.cachedImageForKey(key) != nil + // Progressively abbreviate the attributed text representation, starting with the highest-priority abbreviations. + let availableBounds = source.availableBounds() + for currentPriority in priorities.sorted(by: <) { + // If the attributed text representation already fits, we’re done. + if attributedTextRepresentation.size().width <= availableBounds.width { + break } - let componentBefore = components.component(before: component) - let componentAfter = components.component(after: component) - switch component.type { - //Throw away exit components. We know this is safe because we know that if there is an exit component, - // there is an exit code component, and the latter contains the information we care about. - case .exit: - continue - - //If we have a exit, in the first two components, lets handle that. - case .exitCode where 0...1 ~= index: - guard let exitString = self.attributedString(forExitComponent: component, maneuverDirection: instruction.maneuverDirection, dataSource: dataSource) else { fallthrough } - build(component, [exitString]) - - //if it's a delimiter, skip it if it's between two shields. - case .delimiter where isShield(componentBefore) && isShield(componentAfter): - continue - - //If we have an icon component, lets turn it into a shield. - case .image: - if let shieldString = attributedString(forShieldComponent: component, repository: imageRepository, dataSource: dataSource, onImageDownload: onImageDownload) { - build(component, [joinString, shieldString]) - } else if let genericShieldString = attributedString(forGenericShield: component, dataSource: dataSource) { - build(component, [joinString, genericShieldString]) - } else { - fallthrough + // Look for substrings with the current abbreviation priority and replace them with the embedded abbreviations. + let wholeRange = NSRange(location: 0, length: attributedTextRepresentation.length) + attributedTextRepresentation.enumerateAttribute(.abbreviationPriority, in: wholeRange, options: []) { (priority, range, stop) in + var abbreviationRange = range + if priority as? Int == currentPriority, + let abbreviation = attributedTextRepresentation.attribute(.abbreviation, at: range.location, effectiveRange: &abbreviationRange) as? String { + assert(abbreviationRange == range, "Abbreviation and abbreviation priority should be applied to the same effective range.") + attributedTextRepresentation.replaceCharacters(in: abbreviationRange, with: abbreviation) } - - //Otherwise, process as text component. - default: - guard let componentString = attributedString(forTextComponent: component, dataSource: dataSource) else { continue } - build(component, [joinString, componentString]) } } - assert(processedComponents.count == strings.count, "The number of processed components must match the number of attributed strings") - return (components: processedComponents, attributedStrings: strings) - } - - func attributedString(forExitComponent component: VisualInstructionComponent, maneuverDirection: ManeuverDirection, dataSource: DataSource) -> NSAttributedString? { - guard component.type == .exitCode, let exitCode = component.text else { return nil } - let side: ExitSide = maneuverDirection == .left ? .left : .right - guard let exitString = exitShield(side: side, text: exitCode, component: component, dataSource: dataSource) else { return nil } - return exitString + return attributedTextRepresentation } - func attributedString(forGenericShield component: VisualInstructionComponent, dataSource: DataSource) -> NSAttributedString? { - guard component.type == .image, let text = component.text else { return nil } - return genericShield(text: text, component: component, dataSource: dataSource) + func attributedTextRepresentation(of instruction: VisualInstruction, dataSource: DataSource, imageRepository: ImageRepository, onImageDownload: @escaping ImageDownloadCompletion) -> NSAttributedString { + var components = instruction.components + + let isShield: (_ key: VisualInstruction.Component?) -> Bool = { (component) in + guard let key = component?.cacheKey else { return false } + return imageRepository.cachedImageForKey(key) != nil + } + + components.removeSeparators { (precedingComponent, component, followingComponent) -> Bool in + if case .exit(_) = component { + // Remove exit components, which appear next to exit code components. Exit code components can be styled unambiguously, making the exit component redundant. + return true + } else if isShield(precedingComponent), case .delimiter(_) = component, isShield(followingComponent) { + // Remove delimiter components flanked by image components, which the response includes only for backwards compatibility with text-only clients. + return true + } else { + return false + } + } + + let defaultAttributes: [NSAttributedString.Key: Any] = [ + .font: dataSource.font as Any, + .foregroundColor: dataSource.textColor as Any + ] + let attributedTextRepresentations = components.map { (component) -> NSAttributedString in + switch component { + case .delimiter(let text): + return NSAttributedString(string: text.text, attributes: defaultAttributes) + case .text(let text): + let attributedString = NSMutableAttributedString(string: text.text, attributes: defaultAttributes) + // Annotate the attributed text representation with an abbreviation. + if let abbreviation = text.abbreviation, let abbreviationPriority = text.abbreviationPriority { + let wholeRange = NSRange(location: 0, length: attributedString.length) + attributedString.addAttributes([ + .abbreviation: abbreviation, + .abbreviationPriority: abbreviationPriority, + ], range: wholeRange) + } + return attributedString + case .image(let image, let alternativeText): + // Ideally represent the image component as a shield image. + return self.attributedString(forShieldComponent: image, repository: imageRepository, dataSource: dataSource, cacheKey: component.cacheKey!, onImageDownload: onImageDownload) + // Fall back to a generic shield if no shield image is available. + ?? genericShield(text: alternativeText.text, dataSource: dataSource, cacheKey: component.cacheKey!) + // Finally, fall back to a plain text representation if the generic shield couldn’t be rendered. + ?? NSAttributedString(string: alternativeText.text, attributes: defaultAttributes) + case .exit(_): + preconditionFailure("Exit components should have been removed above") + case .exitCode(let text): + let exitSide: ExitSide = instruction.maneuverDirection == .left ? .left : .right + return exitShield(side: exitSide, text: text.text, dataSource: dataSource, cacheKey: component.cacheKey!) + ?? NSAttributedString(string: text.text, attributes: defaultAttributes) + case .lane(_, _): + preconditionFailure("Lane component has no attributed string representation.") + } + } + let separator = NSAttributedString(string: " ", attributes: defaultAttributes) + return attributedTextRepresentations.joined(separator: separator) } - func attributedString(forShieldComponent shield: VisualInstructionComponent, repository:ImageRepository, dataSource: DataSource, onImageDownload: @escaping ImageDownloadCompletion) -> NSAttributedString? { - guard shield.imageURL != nil, let shieldKey = shield.cacheKey else { return nil } - + func attributedString(forShieldComponent shield: VisualInstruction.Component.ImageRepresentation, repository:ImageRepository, dataSource: DataSource, cacheKey: String, onImageDownload: @escaping ImageDownloadCompletion) -> NSAttributedString? { //If we have the shield already cached, use that. - if let cachedImage = repository.cachedImageForKey(shieldKey) { + if let cachedImage = repository.cachedImageForKey(cacheKey) { return attributedString(withFont: dataSource.font, shieldImage: cachedImage) } // Let's download the shield - shieldImageForComponent(shield, in: repository, completion: onImageDownload) + shieldImageForComponent(representation: shield, in: repository, cacheKey: cacheKey, completion: onImageDownload) //Return nothing in the meantime, triggering downstream behavior (generic shield or text) return nil } - func attributedString(forTextComponent component: VisualInstructionComponent, dataSource: DataSource) -> NSAttributedString? { - guard let text = component.text else { return nil } - return NSAttributedString(string: text, attributes: attributes(for: dataSource)) - } - private func shieldImageForComponent(_ component: VisualInstructionComponent, in repository: ImageRepository, completion: @escaping ImageDownloadCompletion) { - guard let imageURL = component.imageURL, let shieldKey = component.cacheKey else { - return - } - - repository.imageWithURL(imageURL, cacheKey: shieldKey, completion: completion ) - } + private func shieldImageForComponent(representation: VisualInstruction.Component.ImageRepresentation, in repository: ImageRepository, cacheKey: String, completion: @escaping ImageDownloadCompletion) { + guard let imageURL = representation.imageURL(scale: VisualInstruction.Component.scale, format: .png) else { return } + - private func attributes(for dataSource: InstructionPresenterDataSource) -> [NSAttributedString.Key: Any] { - return [.font: dataSource.font as Any, .foregroundColor: dataSource.textColor as Any] + repository.imageWithURL(imageURL, cacheKey: cacheKey, completion: completion ) } private func attributedString(withFont font: UIFont, shieldImage: UIImage) -> NSAttributedString { @@ -175,9 +169,7 @@ class InstructionPresenter { return NSAttributedString(attachment: attachment) } - private func genericShield(text: String, component: VisualInstructionComponent, dataSource: DataSource) -> NSAttributedString? { - guard let cacheKey = component.cacheKey else { return nil } - + private func genericShield(text: String, dataSource: DataSource, cacheKey: String) -> NSAttributedString? { let additionalKey = GenericRouteShield.criticalHash(dataSource: dataSource) let attachment = GenericShieldAttachment() @@ -197,9 +189,7 @@ class InstructionPresenter { return NSAttributedString(attachment: attachment) } - private func exitShield(side: ExitSide = .right, text: String, component: VisualInstructionComponent, dataSource: DataSource) -> NSAttributedString? { - guard let cacheKey = component.cacheKey else { return nil } - + private func exitShield(side: ExitSide = .right, text: String, dataSource: DataSource, cacheKey: String) -> NSAttributedString? { let additionalKey = ExitView.criticalHash(side: side, dataSource: dataSource) let attachment = ExitAttachment() @@ -302,30 +292,3 @@ extension CGSize { return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) } } - -fileprivate struct IndexedVisualInstructionComponent { - let component: Array.Element - let index: Array.Index -} - -extension Array where Element == VisualInstructionComponent { - fileprivate func component(before component: VisualInstructionComponent) -> VisualInstructionComponent? { - guard let index = self.firstIndex(of: component) else { - return nil - } - if index > 0 { - return self[index-1] - } - return nil - } - - fileprivate func component(after component: VisualInstructionComponent) -> VisualInstructionComponent? { - guard let index = self.firstIndex(of: component) else { - return nil - } - if index+1 < self.endIndex { - return self[index+1] - } - return nil - } -} diff --git a/MapboxNavigation/InstructionsBannerView.swift b/MapboxNavigation/InstructionsBannerView.swift index a3cbb74f3da..c4357e3a05a 100644 --- a/MapboxNavigation/InstructionsBannerView.swift +++ b/MapboxNavigation/InstructionsBannerView.swift @@ -191,8 +191,8 @@ open class BaseInstructionsBannerView: UIControl { override open func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() maneuverView.isStart = true - let component = VisualInstructionComponent(type: .text, text: "Primary text label", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound) - let instruction = VisualInstruction(text: nil, maneuverType: .none, maneuverDirection: .none, components: [component]) + let component = VisualInstruction.Component.text(text: .init(text: "Primary text label", abbreviation: nil, abbreviationPriority: nil)) + let instruction = VisualInstruction(text: nil, maneuverType: .turn, maneuverDirection: .left, components: [component]) primaryLabel.instruction = instruction distance = 100 diff --git a/MapboxNavigation/LaneView.swift b/MapboxNavigation/LaneView.swift index e439a62ebf2..60279542435 100644 --- a/MapboxNavigation/LaneView.swift +++ b/MapboxNavigation/LaneView.swift @@ -5,7 +5,7 @@ import MapboxDirections open class LaneView: UIView { let invalidAlpha: CGFloat = 0.4 - var lane: Lane? { + var indications: LaneIndication? { didSet { setNeedsDisplay() } @@ -51,12 +51,12 @@ open class LaneView: UIView { static let defaultFrame: CGRect = CGRect(origin: .zero, size: 30.0) - convenience init(component: LaneIndicationComponent) { + convenience init(indications: LaneIndication, isUsable: Bool) { self.init(frame: LaneView.defaultFrame) backgroundColor = .clear - lane = Lane(indications: component.indications) - maneuverDirection = ManeuverDirection(description: component.indications.description) - isValid = component.isUsable + self.indications = indications + maneuverDirection = ManeuverDirection(rawValue: indications.description) + isValid = isUsable } override init(frame: CGRect) { @@ -80,10 +80,10 @@ open class LaneView: UIView { let resizing: LanesStyleKit.ResizingBehavior = .aspectFit - if let lane = lane { - if lane.indications.isSuperset(of: [.straightAhead, .sharpRight]) || lane.indications.isSuperset(of: [.straightAhead, .right]) || lane.indications.isSuperset(of: [.straightAhead, .slightRight]) { + if let indications = indications { + if indications.isSuperset(of: [.straightAhead, .sharpRight]) || indications.isSuperset(of: [.straightAhead, .right]) || indications.isSuperset(of: [.straightAhead, .slightRight]) { if !isValid { - if lane.indications == .slightRight { + if indications == .slightRight { LanesStyleKit.drawLaneSlightRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor) } else { LanesStyleKit.drawLaneStraightRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor) @@ -92,7 +92,7 @@ open class LaneView: UIView { } else if maneuverDirection == .straightAhead { LanesStyleKit.drawLaneStraightOnly(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, secondaryColor: secondaryColor) } else if maneuverDirection == .sharpLeft || maneuverDirection == .left || maneuverDirection == .slightLeft { - if lane.indications == .slightLeft { + if indications == .slightLeft { LanesStyleKit.drawLaneSlightRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, flipHorizontally: true) } else { LanesStyleKit.drawLaneRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, flipHorizontally: true) @@ -100,9 +100,9 @@ open class LaneView: UIView { } else { LanesStyleKit.drawLaneRightOnly(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, secondaryColor: secondaryColor) } - } else if lane.indications.isSuperset(of: [.straightAhead, .sharpLeft]) || lane.indications.isSuperset(of: [.straightAhead, .left]) || lane.indications.isSuperset(of: [.straightAhead, .slightLeft]) { + } else if indications.isSuperset(of: [.straightAhead, .sharpLeft]) || indications.isSuperset(of: [.straightAhead, .left]) || indications.isSuperset(of: [.straightAhead, .slightLeft]) { if !isValid { - if lane.indications == .slightLeft { + if indications == .slightLeft { LanesStyleKit.drawLaneSlightRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, flipHorizontally: true) } else { LanesStyleKit.drawLaneStraightRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, flipHorizontally: true) @@ -118,7 +118,7 @@ open class LaneView: UIView { } else { LanesStyleKit.drawLaneRightOnly(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, secondaryColor: secondaryColor, flipHorizontally: true) } - } else if lane.indications.description.components(separatedBy: ",").count >= 2 { + } else if indications.description.components(separatedBy: ",").count >= 2 { // Hack: // Account for a configuation where there is no straight lane // but there are at least 2 indications. @@ -131,28 +131,28 @@ open class LaneView: UIView { LanesStyleKit.drawLaneRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, flipHorizontally: true) } alpha = isValid ? 1 : invalidAlpha - } else if lane.indications.isSuperset(of: [.sharpRight]) || lane.indications.isSuperset(of: [.right]) || lane.indications.isSuperset(of: [.slightRight]) { - if lane.indications == .slightRight { + } else if indications.isSuperset(of: [.sharpRight]) || indications.isSuperset(of: [.right]) || indications.isSuperset(of: [.slightRight]) { + if indications == .slightRight { LanesStyleKit.drawLaneSlightRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor) } else { LanesStyleKit.drawLaneRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor) } alpha = isValid ? 1 : invalidAlpha - } else if lane.indications.isSuperset(of: [.sharpLeft]) || lane.indications.isSuperset(of: [.left]) || lane.indications.isSuperset(of: [.slightLeft]) { - if lane.indications == .slightLeft { + } else if indications.isSuperset(of: [.sharpLeft]) || indications.isSuperset(of: [.left]) || indications.isSuperset(of: [.slightLeft]) { + if indications == .slightLeft { LanesStyleKit.drawLaneSlightRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, flipHorizontally: true) } else { LanesStyleKit.drawLaneRight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, flipHorizontally: true) } alpha = isValid ? 1 : invalidAlpha - } else if lane.indications.isSuperset(of: [.straightAhead]) { + } else if indications.isSuperset(of: [.straightAhead]) { LanesStyleKit.drawLaneStraight(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor) alpha = isValid ? 1 : invalidAlpha - } else if lane.indications.isSuperset(of: [.uTurn]) { + } else if indications.isSuperset(of: [.uTurn]) { let flip = !(drivingSide == .left) LanesStyleKit.drawLaneUturn(frame: bounds, resizing: resizing, primaryColor: appropriatePrimaryColor, flipHorizontally: flip) alpha = isValid ? 1 : invalidAlpha - } else if lane.indications.isEmpty && isValid { + } else if indications.isEmpty && isValid { // If the lane indication is `none` and the maneuver modifier has a turn in it, // show the turn in the lane image. if maneuverDirection == .sharpRight || maneuverDirection == .right || maneuverDirection == .slightRight { diff --git a/MapboxNavigation/LanesView.swift b/MapboxNavigation/LanesView.swift index 796fb36f10a..f37428c123a 100644 --- a/MapboxNavigation/LanesView.swift +++ b/MapboxNavigation/LanesView.swift @@ -76,22 +76,24 @@ open class LanesView: UIView, NavigationComponent { public func update(for visualInstruction: VisualInstructionBanner?) { clearLaneViews() - guard let tertiaryInstruction = visualInstruction?.tertiaryInstruction, tertiaryInstruction.containsLaneIndications else { + guard let tertiaryInstruction = visualInstruction?.tertiaryInstruction else { hide() return } - let laneIndications: [LaneIndicationComponent]? = tertiaryInstruction.components.compactMap({ $0 as? LaneIndicationComponent }) + let subviews = tertiaryInstruction.components.compactMap { (component) -> LaneView? in + if case let .lane(indications: indications, isUsable: isUsable) = component { + return LaneView(indications: indications, isUsable: isUsable) + } else { + return nil + } + } - guard let lanes = laneIndications, !lanes.isEmpty else { + guard !subviews.isEmpty && subviews.contains(where: { !$0.isValid }) else { hide() return } - let subviews = lanes.map { LaneView(component: $0) } - - guard subviews.contains(where: { !$0.isValid }) else { return } - stackView.addArrangedSubviews(subviews) show() } diff --git a/MapboxNavigation/ManeuverView.swift b/MapboxNavigation/ManeuverView.swift index 841383b05cb..3503762936c 100644 --- a/MapboxNavigation/ManeuverView.swift +++ b/MapboxNavigation/ManeuverView.swift @@ -96,8 +96,8 @@ open class ManeuverView: UIView { let maneuverType = visualInstruction.maneuverType let maneuverDirection = visualInstruction.maneuverDirection - let type = maneuverType != .none ? maneuverType : .turn - let direction = maneuverDirection != .none ? maneuverDirection : .straightAhead + let type = maneuverType ?? .turn + let direction = maneuverDirection ?? .straightAhead switch type { case .merge: @@ -110,7 +110,7 @@ open class ManeuverView: UIView { ManeuversStyleKit.drawFork(frame: bounds, resizing: resizing, primaryColor: primaryColor, secondaryColor: secondaryColor) flip = [.left, .slightLeft, .sharpLeft].contains(direction) case .takeRoundabout, .turnAtRoundabout, .takeRotary: - ManeuversStyleKit.drawRoundabout(frame: bounds, resizing: resizing, primaryColor: primaryColor, secondaryColor: secondaryColor, roundabout_angle: CGFloat(visualInstruction.finalHeading)) + ManeuversStyleKit.drawRoundabout(frame: bounds, resizing: resizing, primaryColor: primaryColor, secondaryColor: secondaryColor, roundabout_angle: CGFloat(visualInstruction.finalHeading ?? 180)) flip = drivingSide == .left case .arrive: diff --git a/MapboxNavigation/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index 89bea213942..158e3c9057b 100644 --- a/MapboxNavigation/NavigationMapView.swift +++ b/MapboxNavigation/NavigationMapView.swift @@ -401,7 +401,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { public func showcase(_ routes: [Route], animated: Bool = false) { guard let active = routes.first, - let coords = active.coordinates, + let coords = active.shape?.coordinates, !coords.isEmpty else { return } //empty array removeArrow() @@ -415,7 +415,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { } func fit(to route: Route, facing direction:CLLocationDirection = 0, animated: Bool = false) { - guard let coords = route.coordinates, !coords.isEmpty else { return } + guard let coords = route.shape?.coordinates, !coords.isEmpty else { return } setUserTrackingMode(.none, animated: false, completionHandler: nil) let line = MGLPolyline(coordinates: coords, count: UInt(coords.count)) @@ -495,7 +495,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { return } - let waypoints: [Waypoint] = Array(route.legs.map { $0.destination }.dropLast()) + 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? @@ -520,10 +520,10 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { } } - if let lastLeg = route.legs.last { + if let lastLeg = route.legs.last, let destinationCoordinate = lastLeg.destination?.coordinate { removeAnnotations(annotationsToRemove() ?? []) let destination = NavigationAnnotation() - destination.coordinate = lastLeg.destination.coordinate + destination.coordinate = destinationCoordinate addAnnotation(destination) } } @@ -746,13 +746,13 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { let tapCoordinate = convert(point, toCoordinateFrom: self) //do we have routes? If so, filter routes with at least 2 coordinates. - guard let routes = routes?.filter({ $0.coordinates?.count ?? 0 > 1 }) else { return nil } + guard let routes = routes?.filter({ $0.shape?.coordinates.count ?? 0 > 1 }) else { return nil } //Sort routes by closest distance to tap gesture. let closest = routes.sorted { (left, right) -> Bool in //existance has been assured through use of filter. - let leftLine = Polyline(left.coordinates!) - let rightLine = Polyline(right.coordinates!) + let leftLine = left.shape! + let rightLine = right.shape! let leftDistance = leftLine.closestCoordinate(to: tapCoordinate)!.distance let rightDistance = rightLine.closestCoordinate(to: tapCoordinate)!.distance @@ -761,7 +761,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { //filter closest coordinates by which ones are under threshold. let candidates = closest.filter { - let closestCoordinate = Polyline($0.coordinates!).closestCoordinate(to: tapCoordinate)!.coordinate + let closestCoordinate = $0.shape!.closestCoordinate(to: tapCoordinate)!.coordinate let closestPoint = self.convert(closestCoordinate, toPointTo: self) return closestPoint.distance(to: point) < tapGestureDistanceThreshold @@ -776,7 +776,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { var altRoutes: [MGLPolylineFeature] = [] for route in routes.suffix(from: 1) { - let polyline = MGLPolylineFeature(coordinates: route.coordinates!, count: UInt(route.coordinates!.count)) + let polyline = MGLPolylineFeature(coordinates: route.shape!.coordinates, count: UInt(route.shape!.coordinates.count)) polyline.attributes["isAlternateRoute"] = true altRoutes.append(polyline) } @@ -785,7 +785,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { } func addCongestion(to route: Route, legIndex: Int?) -> [MGLPolylineFeature]? { - guard let coordinates = route.coordinates else { return nil } + guard let coordinates = route.shape?.coordinates else { return nil } var linesPerLeg: [MGLPolylineFeature] = [] @@ -797,7 +797,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { let legCoordinates: [CLLocationCoordinate2D] = leg.steps.enumerated().reduce([]) { allCoordinates, current in let index = current.offset let step = current.element - let stepCoordinates = step.coordinates! + let stepCoordinates = step.shape!.coordinates return index == 0 ? stepCoordinates : allCoordinates + stepCoordinates.suffix(from: 1) } @@ -810,7 +810,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { return polyline } } else { - lines = [MGLPolylineFeature(coordinates: route.coordinates!, count: UInt(route.coordinates!.count))] + lines = [MGLPolylineFeature(coordinates: route.shape!.coordinates, count: UInt(route.shape!.coordinates.count))] } for line in lines { @@ -850,7 +850,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { for (index, leg) in route.legs.enumerated() { let legCoordinates: [CLLocationCoordinate2D] = Array(leg.steps.compactMap { - $0.coordinates + $0.shape?.coordinates }.joined()) let polyline = MGLPolylineFeature(coordinates: legCoordinates, count: UInt(legCoordinates.count)) @@ -1010,7 +1010,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { for (stepIndex, step) in leg.steps.enumerated() { for instruction in step.instructionsSpokenAlongStep! { let feature = MGLPointFeature() - feature.coordinate = Polyline(route.legs[legIndex].steps[stepIndex].coordinates!.reversed()).coordinateFromStart(distance: instruction.distanceAlongStep)! + feature.coordinate = Polyline(route.legs[legIndex].steps[stepIndex].shape!.coordinates.reversed()).coordinateFromStart(distance: instruction.distanceAlongStep)! feature.attributes = [ "instruction": instruction.text ] features.append(feature) } diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 7fabe643e49..cd15e29851b 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -486,7 +486,9 @@ extension NavigationViewController: NavigationServiceDelegate { // In the case the user drives beyond the waypoint, // we should accurately depict this. - let destination = progress.currentLeg.destination + guard let destination = progress.currentLeg.destination else { + preconditionFailure("Current leg has no destination") + } let shouldPrevent = navigationService.delegate?.navigationService(navigationService, shouldPreventReroutesWhenArrivingAt: destination) ?? RouteController.DefaultBehavior.shouldPreventReroutesWhenArrivingAtWaypoint let userHasArrivedAndShouldPreventRerouting = shouldPrevent && !progress.currentLegProgress.userHasArrivedAtWaypoint @@ -605,7 +607,7 @@ extension NavigationViewController: StyleManagerDelegate { public func location(for styleManager: StyleManager) -> CLLocation? { if let location = navigationService.router.location { return location - } else if let firstCoord = route.coordinates?.first { + } else if let firstCoord = route.shape?.coordinates.first { return CLLocation(latitude: firstCoord.latitude, longitude: firstCoord.longitude) } else { return nil diff --git a/MapboxNavigation/NextBannerView.swift b/MapboxNavigation/NextBannerView.swift index b196e88e6b5..8e5e8ef1a59 100644 --- a/MapboxNavigation/NextBannerView.swift +++ b/MapboxNavigation/NextBannerView.swift @@ -63,8 +63,8 @@ open class NextBannerView: UIView, NavigationComponent { override open func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() maneuverView.isEnd = true - let component = VisualInstructionComponent(type: .text, text: "Next step", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound) - let instruction = VisualInstruction(text: nil, maneuverType: .none, maneuverDirection: .none, components: [component]) + let component = VisualInstruction.Component.text(text: .init(text: "Next step", abbreviation: nil, abbreviationPriority: nil)) + let instruction = VisualInstruction(text: nil, maneuverType: .turn, maneuverDirection: .right, components: [component]) instructionLabel.instruction = instruction } @@ -97,7 +97,7 @@ open class NextBannerView: UIView, NavigationComponent { Updates the instructions banner info with a given `VisualInstructionBanner`. */ public func update(for visualInstruction: VisualInstructionBanner?) { - guard let tertiaryInstruction = visualInstruction?.tertiaryInstruction, !tertiaryInstruction.containsLaneIndications else { + guard let tertiaryInstruction = visualInstruction?.tertiaryInstruction, tertiaryInstruction.laneComponents.isEmpty else { hide() return } diff --git a/MapboxNavigation/Route.swift b/MapboxNavigation/Route.swift index 7a0601cfd28..2bb8e9b1b3a 100644 --- a/MapboxNavigation/Route.swift +++ b/MapboxNavigation/Route.swift @@ -14,17 +14,17 @@ extension Route { */ func polylineAroundManeuver(legIndex: Int, stepIndex: Int, distance: CLLocationDistance) -> Polyline { let precedingLegs = legs.prefix(upTo: legIndex) - let precedingLegCoordinates = precedingLegs.flatMap { $0.steps }.flatMap { $0.coordinates ?? [] } + let precedingLegCoordinates = precedingLegs.flatMap { $0.steps }.flatMap { $0.shape?.coordinates ?? [] } let precedingSteps = legs[legIndex].steps.prefix(upTo: stepIndex) - let precedingStepCoordinates = precedingSteps.compactMap { $0.coordinates }.reduce([], +) + 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.coordinates ?? [] } + let followingLegCoordinates = followingLegs.flatMap { $0.steps }.flatMap { $0.shape?.coordinates ?? [] } let followingSteps = legs[legIndex].steps.suffix(from: stepIndex) - let followingStepCoordinates = followingSteps.compactMap { $0.coordinates }.reduce([], +) + 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. diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index d11f55f8d1e..d77752635ce 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -165,7 +165,7 @@ class RouteMapViewController: UIViewController { mapView.camera = camera } else if let location = router.location, location.course > 0 { mapView.updateCourseTracking(location: location, animated: false) - } else if let coordinates = router.routeProgress.currentLegProgress.currentStep.coordinates, let firstCoordinate = coordinates.first, coordinates.count > 1 { + } else if let coordinates = router.routeProgress.currentLegProgress.currentStep.shape?.coordinates, let firstCoordinate = coordinates.first, coordinates.count > 1 { let secondCoordinate = coordinates[1] let course = firstCoordinate.direction(to: secondCoordinate) let newLocation = CLLocation(coordinate: router.location?.coordinate ?? firstCoordinate, altitude: 0, horizontalAccuracy: 0, verticalAccuracy: 0, course: course, speed: 0, timestamp: Date()) @@ -233,7 +233,7 @@ class RouteMapViewController: UIViewController { @objc func toggleOverview(_ sender: Any) { mapView.enableFrameByFrameCourseViewTracking(for: 3) - if let coordinates = router.route.coordinates, + if let coordinates = router.route.shape?.coordinates, let userLocation = router.location?.coordinate { mapView.contentInset = contentInset(forOverviewing: true) mapView.setOverheadCameraView(from: userLocation, along: coordinates, for: contentInset(forOverviewing: true)) @@ -407,7 +407,7 @@ class RouteMapViewController: UIViewController { guard let height = navigationView.endOfRouteHeightConstraint?.constant else { return } let insets = UIEdgeInsets(top: topBannerContainerView.bounds.height, left: 20, bottom: height + 20, right: 20) - if let coordinates = route.coordinates, let userLocation = navService.router.location?.coordinate { + if let coordinates = route.shape?.coordinates, let userLocation = navService.router.location?.coordinate { let slicedLine = Polyline(coordinates).sliced(from: userLocation).coordinates let line = MGLPolyline(coordinates: slicedLine, count: UInt(slicedLine.count)) @@ -483,7 +483,7 @@ extension RouteMapViewController: NavigationComponent { } if isInOverviewMode { - if let coordinates = route.coordinates, let userLocation = router.location?.coordinate { + if let coordinates = route.shape?.coordinates, let userLocation = router.location?.coordinate { mapView.contentInset = contentInset(forOverviewing: true) mapView.setOverheadCameraView(from: userLocation, along: coordinates, for: contentInset(forOverviewing: true)) } @@ -609,7 +609,7 @@ extension RouteMapViewController: NavigationViewDelegate { } func labelCurrentRoadFeature(at location: CLLocation) { - guard let style = mapView.style, let stepCoordinates = router.routeProgress.currentLegProgress.currentStep.coordinates else { + guard let style = mapView.style, let stepShape = router.routeProgress.currentLegProgress.currentStep.shape else { return } @@ -655,7 +655,7 @@ extension RouteMapViewController: NavigationViewDelegate { for line in allLines { let featureCoordinates = Array(UnsafeBufferPointer(start: line.coordinates, count: Int(line.pointCount))) let featurePolyline = Polyline(featureCoordinates) - let slicedLine = Polyline(stepCoordinates).sliced(from: closestCoordinate) + let slicedLine = stepShape.sliced(from: closestCoordinate) let lookAheadDistance: CLLocationDistance = 10 guard let pointAheadFeature = featurePolyline.sliced(from: closestCoordinate).coordinateFromStart(distance: lookAheadDistance) else { continue } diff --git a/MapboxNavigation/StepsViewController.swift b/MapboxNavigation/StepsViewController.swift index d1469731dd8..d5408ab9404 100644 --- a/MapboxNavigation/StepsViewController.swift +++ b/MapboxNavigation/StepsViewController.swift @@ -255,8 +255,8 @@ extension StepsViewController: UITableViewDataSource { } let leg = routeProgress.route.legs[section] - let sourceName = leg.source.name - let destinationName = leg.destination.name + let sourceName = leg.source?.name + let destinationName = leg.destination?.name let majorWays = leg.name.components(separatedBy: ", ") if let destinationName = destinationName?.nonEmptyString, majorWays.count > 1 { diff --git a/MapboxNavigation/VisualInstruction.swift b/MapboxNavigation/VisualInstruction.swift index 6f337bf6e5b..8d441b8020c 100644 --- a/MapboxNavigation/VisualInstruction.swift +++ b/MapboxNavigation/VisualInstruction.swift @@ -4,9 +4,13 @@ import CarPlay #endif extension VisualInstruction { - /// Returns true if `VisualInstruction.components` contains any `LaneIndicationComponent`. - public var containsLaneIndications: Bool { - return components.contains(where: { $0 is LaneIndicationComponent }) + var laneComponents: [Component] { + return components.filter { component -> Bool in + if case VisualInstruction.Component.lane(indications: _, isUsable: _) = component { + return true + } + return false + } } func maneuverImage(side: DrivingSide, color: UIColor, size: CGSize) -> UIImage? { @@ -34,16 +38,14 @@ extension VisualInstruction { /// Returns whether the `VisualInstruction`’s maneuver image should be flipped according to the driving side. public func shouldFlipImage(side: DrivingSide) -> Bool { - let leftDirection = [.left, .slightLeft, .sharpLeft].contains(maneuverDirection) - - switch maneuverType { + switch maneuverType ?? .turn { case .takeRoundabout, .turnAtRoundabout, .takeRotary, _ where maneuverDirection == .uTurn: return side == .left default: - return leftDirection + return [.left, .slightLeft, .sharpLeft].contains(maneuverDirection ?? .straightAhead) } } diff --git a/MapboxNavigation/VisualInstructionComponent.swift b/MapboxNavigation/VisualInstructionComponent.swift index 2a69c9b3f69..ac32b70af78 100644 --- a/MapboxNavigation/VisualInstructionComponent.swift +++ b/MapboxNavigation/VisualInstructionComponent.swift @@ -1,23 +1,22 @@ import UIKit import MapboxDirections -extension VisualInstructionComponent { +extension VisualInstruction.Component { static let scale = UIScreen.main.scale var cacheKey: String? { - switch type { - case .exit, .exitCode: - guard let exitCode = self.text else { return nil } - return "exit-" + exitCode + "-\(VisualInstructionComponent.scale)" - case .image: - guard let imageURL = imageURL else { return genericCacheKey } - return "\(imageURL.absoluteString)-\(VisualInstructionComponent.scale)" - case .text, .delimiter: + switch self { + case let .exit(representation), let .exitCode(representation): + let exitCode = representation.text + return "exit-" + exitCode + "-\(VisualInstruction.Component.scale)" + case let .image(representation): + guard let imageURL = representation.image.imageBaseURL else { + return "generic-" + representation.alternativeText.text + } + + return "\(imageURL.absoluteString)-\(VisualInstruction.Component.scale)" + case .text, .delimiter, .lane: return nil } } - - var genericCacheKey: String { - return "generic-" + (text ?? "nil") - } } diff --git a/MapboxNavigationTests/CarPlayManagerTests.swift b/MapboxNavigationTests/CarPlayManagerTests.swift index ca4e2563ad1..3d0b40411de 100644 --- a/MapboxNavigationTests/CarPlayManagerTests.swift +++ b/MapboxNavigationTests/CarPlayManagerTests.swift @@ -195,7 +195,10 @@ 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") + choice.userInfo = Fixture.route(from: "route-with-banner-instructions", options: NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 37.764793, longitude: -122.463161), + CLLocationCoordinate2D(latitude: 34.054081, longitude: -118.243412), + ])) manager.mapTemplate(mapTemplate, startedTrip: CPTrip(origin: MKMapItem(), destination: MKMapItem(), routeChoices: [choice]), using: choice) @@ -216,7 +219,7 @@ class CarPlayManagerTests: XCTestCase { let manager = CarPlayManager() let spy = CarPlayManagerFailureDelegateSpy() - let testError = NSError(domain: "com.mapbox.test", code: 42, userInfo: nil) + let testError = DirectionsError.requestTooLarge let locOne = CLLocationCoordinate2D(latitude: 0, longitude: 0) let fakeOptions = RouteOptions(coordinates: [locOne]) manager.delegate = spy @@ -292,7 +295,10 @@ class CarPlayManagerSpec: QuickSpec { } let previewRoutesAction = { - let route = Fixture.route(from: "route-with-banner-instructions") + let route = Fixture.route(from: "route-with-banner-instructions", options: NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 37.764793, longitude: -122.463161), + CLLocationCoordinate2D(latitude: 34.054081, longitude: -118.243412), + ])) let waypoints = route.routeOptions.waypoints let directionsSpy = manager!.directions as! DirectionsSpy @@ -365,7 +371,10 @@ 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") + fakeRouteChoice.userInfo = Fixture.route(from: "route-with-banner-instructions", options: NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 37.764793, longitude: -122.463161), + CLLocationCoordinate2D(latitude: 34.054081, longitude: -118.243412), + ])) let fakeTrip = CPTrip(origin: MKMapItem(), destination: MKMapItem(), routeChoices: [fakeRouteChoice]) //simulate starting a fake trip @@ -447,10 +456,10 @@ func simulateCarPlayConnection(_ manager: CarPlayManager) { @available(iOS 12.0, *) class CarPlayManagerFailureDelegateSpy: CarPlayManagerDelegate { - private(set) var recievedError: NSError? + private(set) var recievedError: DirectionsError? @available(iOS 12.0, *) - func carPlayManager(_ carPlayManager: CarPlayManager, didFailToFetchRouteBetween waypoints: [Waypoint]?, options: RouteOptions, error: NSError) -> CPNavigationAlert? { + func carPlayManager(_ carPlayManager: CarPlayManager, didFailToFetchRouteBetween waypoints: [Waypoint]?, options: RouteOptions, error: DirectionsError) -> CPNavigationAlert? { recievedError = error return nil } @@ -483,17 +492,9 @@ class TestCarPlayManagerDelegate: CarPlayManagerDelegate { public var mapButtons: [CPMapButton]? func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, desiredSimulationMode: SimulationMode) -> NavigationService { - let response = Fixture.JSONFromFileNamed(name: jsonFileName) - let jsonRoute = (response["routes"] as! [AnyObject]).first as! [String: Any] - let initialRoute: Route = { - let waypoint1 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.795042, longitude: -122.413165)) - let waypoint2 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.7727, longitude: -122.433378)) - let options = NavigationRouteOptions(waypoints: [waypoint1, waypoint2]) - options.shapeFormat = .polyline - let route = Route(json: jsonRoute, waypoints: [waypoint1, waypoint2], options: options) - route.accessToken = "deadbeef" - return route - }() + 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) return service diff --git a/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift b/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift index 887909c25f1..9bca9290f49 100644 --- a/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift +++ b/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift @@ -49,7 +49,11 @@ fileprivate class CarPlayNavigationViewControllerTests: XCTestCase { //set up the litany of dependancies let directions = Directions(accessToken: "fafedeadbeef") let manager = CarPlayManager(directions: directions) - let route = Fixture.route(from: "multileg-route") + let route = Fixture.route(from: "multileg-route", 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 interface = FakeCPInterfaceController("test estimates display") let mapSpy = MapTemplateSpy() @@ -58,7 +62,7 @@ fileprivate class CarPlayNavigationViewControllerTests: XCTestCase { let fakeSession = CPNavigationSessionFake(maneuvers: [fakeManeuver]) mapSpy.fakeSession = fakeSession let progress = navService.routeProgress - let firstCoordinate = progress.currentLeg.coordinates.first! + let firstCoordinate = progress.currentLeg.shape.coordinates.first! let location = CLLocation(latitude: firstCoordinate.latitude, longitude: firstCoordinate.longitude) //create the subject and notification diff --git a/MapboxNavigationTests/Constants.swift b/MapboxNavigationTests/Constants.swift index 6a340e1a0f4..c2abad62c07 100644 --- a/MapboxNavigationTests/Constants.swift +++ b/MapboxNavigationTests/Constants.swift @@ -7,13 +7,14 @@ extension CGSize { } struct ShieldImage { + /// PNG at 3× let image: UIImage - let url: URL + let baseURL: URL } extension ShieldImage { static let i280 = ShieldImage(image: UIImage(named: "i-280", in: Bundle(for: InstructionsBannerViewIntegrationTests.self), compatibleWith: nil)!, - url: URL(string: "https://s3.amazonaws.com/mapbox/shields/v3/i-280@3x.png")!) + baseURL: URL(string: "https://s3.amazonaws.com/mapbox/shields/v3/i-280")!) static let us101 = ShieldImage(image: UIImage(named: "us-101", in: Bundle(for: InstructionsBannerViewIntegrationTests.self), compatibleWith: nil)!, - url: URL(string: "https://s3.amazonaws.com/mapbox/shields/v3/us-101@3x.png")!) + baseURL: URL(string: "https://s3.amazonaws.com/mapbox/shields/v3/us-101")!) } diff --git a/MapboxNavigationTests/GuidanceCardsSnapshotTests.swift b/MapboxNavigationTests/GuidanceCardsSnapshotTests.swift index 24c936abd3b..da8cd426684 100644 --- a/MapboxNavigationTests/GuidanceCardsSnapshotTests.swift +++ b/MapboxNavigationTests/GuidanceCardsSnapshotTests.swift @@ -8,13 +8,18 @@ import Foundation @available(iOS 11.0, *) /// :nodoc: class GuidanceCardsSnapshotTests: SnapshotTest { + let tertiaryRouteOptions = NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 39.749216, longitude: -105.008272), + CLLocationCoordinate2D(latitude: 39.694833, longitude: -104.976949), + ]) + override func setUp() { super.setUp() recordMode = false } func testRegularManeuver() { - let route = Fixture.route(from: "route-with-tertiary") + let route = Fixture.route(from: "route-with-tertiary", options: tertiaryRouteOptions) let host = UIViewController(nibName: nil, bundle: nil) let container = UIView.forAutoLayout() @@ -36,7 +41,7 @@ class GuidanceCardsSnapshotTests: SnapshotTest { } func testLanesManeuver() { - let route = Fixture.route(from: "route-with-tertiary") + let route = Fixture.route(from: "route-with-tertiary", options: tertiaryRouteOptions) let host = UIViewController(nibName: nil, bundle: nil) let container = UIView.forAutoLayout() @@ -59,7 +64,7 @@ class GuidanceCardsSnapshotTests: SnapshotTest { } func testTertiaryManeuver() { - let route = Fixture.route(from: "route-with-tertiary") + let route = Fixture.route(from: "route-with-tertiary", options: tertiaryRouteOptions) let host = UIViewController(nibName: nil, bundle: nil) let container = UIView.forAutoLayout() diff --git a/MapboxNavigationTests/InstructionPresenterTests.swift b/MapboxNavigationTests/InstructionPresenterTests.swift index 59828c42b79..8ae4032a2be 100644 --- a/MapboxNavigationTests/InstructionPresenterTests.swift +++ b/MapboxNavigationTests/InstructionPresenterTests.swift @@ -1,13 +1,14 @@ import Foundation import XCTest import MapboxDirections +import MapboxCoreNavigation import TestHelper @testable import MapboxNavigation class InstructionPresenterTests: XCTestCase { func testExitInstructionProvidesExit() { - let exitAttribute = VisualInstructionComponent(type: .exit, text: "Exit", imageURL: nil, abbreviation: nil, abbreviationPriority: 0) - let exitCodeAttribute = VisualInstructionComponent(type: .exitCode, text: "123A", imageURL: nil, abbreviation: nil, abbreviationPriority: 0) + let exitAttribute = VisualInstruction.Component.exit(text: .init(text: "Exit", abbreviation: nil, abbreviationPriority: nil)) + let exitCodeAttribute = VisualInstruction.Component.exitCode(text: .init(text: "123A", abbreviation: nil, abbreviationPriority: nil)) let exitInstruction = VisualInstruction(text: nil, maneuverType: .takeOffRamp, maneuverDirection: .right, components: [exitAttribute, exitCodeAttribute]) let label = InstructionLabel(frame: CGRect(origin: .zero, size:CGSize(width: 50, height: 50))) @@ -25,7 +26,10 @@ class InstructionPresenterTests: XCTestCase { /// NOTE: This test is disabled pending https://github.com/mapbox/mapbox-navigation-ios/issues/1468 func x_testAbbreviationPerformance() { - let route = Fixture.route(from: "route-with-banner-instructions") + let route = Fixture.route(from: "route-with-banner-instructions", options: NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 37.764793, longitude: -122.463161), + CLLocationCoordinate2D(latitude: 34.054081, longitude: -118.243412), + ])) let steps = route.legs.flatMap { $0.steps } let instructions = steps.compactMap { $0.instructionsDisplayedAlongStep?.first?.primaryInstruction } diff --git a/MapboxNavigationTests/InstructionsBannerViewIntegrationTests.swift b/MapboxNavigationTests/InstructionsBannerViewIntegrationTests.swift index de2ddbcc28c..b92a04bc39a 100644 --- a/MapboxNavigationTests/InstructionsBannerViewIntegrationTests.swift +++ b/MapboxNavigationTests/InstructionsBannerViewIntegrationTests.swift @@ -10,15 +10,15 @@ func instructionsView(size: CGSize = .iPhone6Plus) -> InstructionsBannerView { func makeVisualInstruction(_ maneuverType: ManeuverType = .arrive, _ maneuverDirection: ManeuverDirection = .left, - primaryInstruction: [VisualInstructionComponent], - secondaryInstruction: [VisualInstructionComponent]?) -> VisualInstructionBanner { + primaryInstruction: [VisualInstruction.Component], + secondaryInstruction: [VisualInstruction.Component]?) -> VisualInstructionBanner { let primary = VisualInstruction(text: "Instruction", maneuverType: maneuverType, maneuverDirection: maneuverDirection, components: primaryInstruction) var secondary: VisualInstruction? = nil if let secondaryInstruction = secondaryInstruction { secondary = VisualInstruction(text: "Instruction", maneuverType: maneuverType, maneuverDirection: maneuverDirection, components: secondaryInstruction) } - return VisualInstructionBanner(distanceAlongStep: 482.803, primaryInstruction: primary, secondaryInstruction: secondary, tertiaryInstruction: nil, drivingSide: .right) + return VisualInstructionBanner(distanceAlongStep: 482.803, primary: primary, secondary: secondary, tertiary: nil, drivingSide: .right) } class InstructionsBannerViewIntegrationTests: XCTestCase { @@ -31,21 +31,21 @@ class InstructionsBannerViewIntegrationTests: XCTestCase { return repo }() - lazy var instructions: [VisualInstructionComponent] = { - let components = [ - VisualInstructionComponent(type: .image, text: "US 101", imageURL: ShieldImage.us101.url, abbreviation: nil, abbreviationPriority: 0), - VisualInstructionComponent(type: .delimiter, text: "/", imageURL: nil, abbreviation: nil, abbreviationPriority: 0), - VisualInstructionComponent(type: .image, text: "I 280", imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: 0) + lazy var instructions: [VisualInstruction.Component] = { + let components: [VisualInstruction.Component] = [ + .image(image: .init(imageBaseURL: ShieldImage.us101.baseURL), alternativeText: .init(text: "US 101", abbreviation: nil, abbreviationPriority: 0)), + .delimiter(text: .init(text: "/", abbreviation: nil, abbreviationPriority: 0)), + .image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), ] return components }() - lazy var genericInstructions: [VisualInstructionComponent] = [ - VisualInstructionComponent(type: .image, text: "ANK 1", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .text, text: "Ankh-Morpork Highway 1", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound) + lazy var genericInstructions: [VisualInstruction.Component] = [ + .image(image: .init(imageBaseURL: nil), alternativeText: .init(text: "ANK 1", abbreviation: nil, abbreviationPriority: nil)), + .text(text: .init(text: "Ankh-Morpork Highway 1", abbreviation: nil, abbreviationPriority: nil)), ] - lazy var typicalInstruction: VisualInstructionBanner = makeVisualInstruction(primaryInstruction: [VisualInstructionComponent(type: .text, text: "Main Street", imageURL: nil, abbreviation: "Main St", abbreviationPriority: 0)], secondaryInstruction: nil) + lazy var typicalInstruction: VisualInstructionBanner = makeVisualInstruction(primaryInstruction: [.text(text: .init(text: "Main Street", abbreviation: "Main St", abbreviationPriority: 0))], secondaryInstruction: nil) private func resetImageCache() { let semaphore = DispatchSemaphore(value: 0) @@ -101,8 +101,8 @@ class InstructionsBannerViewIntegrationTests: XCTestCase { func testDelimiterIsHiddenWhenAllShieldsAreAlreadyLoaded() { //prime the cache to simulate images having already been loaded - let instruction1 = VisualInstructionComponent(type: .image, text: "I 280", imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: 0) - let instruction2 = VisualInstructionComponent(type: .image, text: "US 101", imageURL: ShieldImage.us101.url, abbreviation: nil, abbreviationPriority: 0) + let instruction1 = VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)) + let instruction2 = VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.us101.baseURL), alternativeText: .init(text: "US 101", abbreviation: nil, abbreviationPriority: 0)) imageRepository.storeImage(ShieldImage.i280.image, forKey: instruction1.cacheKey!, toDisk: false) imageRepository.storeImage(ShieldImage.us101.image, forKey: instruction2.cacheKey!, toDisk: false) @@ -136,7 +136,7 @@ class InstructionsBannerViewIntegrationTests: XCTestCase { XCTAssertNotNil(view.primaryLabel.text!.firstIndex(of: "/")) //simulate the downloads - let firstDestinationComponent: VisualInstructionComponent = instructions[0] + let firstDestinationComponent: VisualInstruction.Component = instructions[0] simulateDownloadingShieldForComponent(firstDestinationComponent) //ensure that first callback fires @@ -220,7 +220,7 @@ class InstructionsBannerViewIntegrationTests: XCTestCase { }) //simulate the downloads - let firstDestinationComponent: VisualInstructionComponent = instructions[0] + let firstDestinationComponent: VisualInstruction.Component = instructions[0] simulateDownloadingShieldForComponent(firstDestinationComponent) //ensure that first callback fires @@ -277,9 +277,9 @@ class InstructionsBannerViewIntegrationTests: XCTestCase { } func testExitBannerIntegration() { - let exitAttribute = VisualInstructionComponent(type: .exit, text: "Exit", imageURL: nil, abbreviation: nil, abbreviationPriority: 0) - let exitCodeAttribute = VisualInstructionComponent(type: .exitCode, text: "123A", imageURL: nil, abbreviation: nil, abbreviationPriority: 0) - let mainStreetString = VisualInstructionComponent(type: .text, text: "Main Street", imageURL: nil, abbreviation: "Main St", abbreviationPriority: 0) + let exitAttribute = VisualInstruction.Component.exit(text: .init(text: "Exit", abbreviation: nil, abbreviationPriority: 0)) + let exitCodeAttribute = VisualInstruction.Component.exitCode(text: .init(text: "123A", abbreviation: nil, abbreviationPriority: 0)) + let mainStreetString = VisualInstruction.Component.text(text: .init(text: "Main Street", abbreviation: "Main St", abbreviationPriority: 0)) let exitInstruction = VisualInstruction(text: nil, maneuverType: .takeOffRamp, maneuverDirection: .right, components: [exitAttribute, exitCodeAttribute, mainStreetString]) let label = InstructionLabel(frame: CGRect(origin: .zero, size:CGSize(width: 375, height: 100))) @@ -305,8 +305,12 @@ class InstructionsBannerViewIntegrationTests: XCTestCase { XCTAssert(roadName.string == "Main Street", "Banner not populating road name correctly") } - private func simulateDownloadingShieldForComponent(_ component: VisualInstructionComponent) { - let operation: ImageDownloadOperationSpy = ImageDownloadOperationSpy.operationForURL(component.imageURL!)! + private func simulateDownloadingShieldForComponent(_ component: VisualInstruction.Component) { + var imageURL: URL! + if case let VisualInstruction.Component.image(image: imageRepresentation, alternativeText: _) = component, let imageBaseURL = imageRepresentation.imageURL(format: .png) { + imageURL = imageBaseURL + } + let operation: ImageDownloadOperationSpy = ImageDownloadOperationSpy.operationForURL(imageURL)! operation.fireAllCompletions(ShieldImage.i280.image, data: ShieldImage.i280.image.pngData(), error: nil) XCTAssertNotNil(imageRepository.cachedImageForKey(component.cacheKey!)) diff --git a/MapboxNavigationTests/InstructionsBannerViewSnapshotTests.swift b/MapboxNavigationTests/InstructionsBannerViewSnapshotTests.swift index 24ff7b378e7..5dc4832e8bf 100644 --- a/MapboxNavigationTests/InstructionsBannerViewSnapshotTests.swift +++ b/MapboxNavigationTests/InstructionsBannerViewSnapshotTests.swift @@ -14,8 +14,8 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { recordMode = false agnosticOptions = [.OS, .device] - let i280Instruction = VisualInstructionComponent(type: .image, text: nil, imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: 0) - let us101Instruction = VisualInstructionComponent(type: .image, text: nil, imageURL: ShieldImage.us101.url, abbreviation: nil, abbreviationPriority: 0) + let i280Instruction = VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I-280", abbreviation: nil, abbreviationPriority: 0)) + let us101Instruction = VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.us101.baseURL), alternativeText: .init(text: "US 101", abbreviation: nil, abbreviationPriority: 0)) imageRepository.storeImage(ShieldImage.i280.image, forKey: i280Instruction.cacheKey!, toDisk: false) imageRepository.storeImage(ShieldImage.us101.image, forKey: us101Instruction.cacheKey!, toDisk: false) @@ -41,10 +41,10 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.maneuverView.isStart = true view.distance = 482 - let instructions = [ - VisualInstructionComponent(type: .text, text: "US 45", imageURL: nil, abbreviation: nil, abbreviationPriority: 0), - VisualInstructionComponent(type: .text, text: "/", imageURL: nil, abbreviation: nil, abbreviationPriority: 0), - VisualInstructionComponent(type: .text, text: "Chicago", imageURL: nil, abbreviation: nil, abbreviationPriority: 0) + let instructions: [VisualInstruction.Component] = [ + .text(text: .init(text: "US 45", abbreviation: nil, abbreviationPriority: 0)), + .text(text: .init(text: "/", abbreviation: nil, abbreviationPriority: 0)), + .text(text: .init(text: "Chicago", abbreviation: nil, abbreviationPriority: 0)), ] view.update(for: makeVisualInstruction(.turn, .right, primaryInstruction: instructions, secondaryInstruction: nil)) @@ -59,9 +59,9 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.maneuverView.isStart = true view.distance = 482 - let instructions = [ - VisualInstructionComponent(type: .image, text: "I 280", imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: 0), - VisualInstructionComponent(type: .text, text: "US 45 / Chicago / US 45 / Chicago", imageURL: nil, abbreviation: nil, abbreviationPriority: 0) + let instructions: [VisualInstruction.Component] = [ + .image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), + .text(text: .init(text: "US 45 / Chicago / US 45 / Chicago", abbreviation: nil, abbreviationPriority: 0)), ] view.update(for: makeVisualInstruction(.turn, .right, primaryInstruction: instructions, secondaryInstruction: nil)) @@ -76,11 +76,11 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.maneuverView.isStart = true view.distance = 482 - let primary = [ - VisualInstructionComponent(type: .image, text: "I 280", imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: 0), - VisualInstructionComponent(type: .text, text: "South", imageURL: nil, abbreviation: nil, abbreviationPriority: 0) + let primary: [VisualInstruction.Component] = [ + .image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), + .text(text: .init(text: "South", abbreviation: nil, abbreviationPriority: 0)), ] - let secondary = [VisualInstructionComponent(type: .text, text: "US 45 / Chicago", imageURL: nil, abbreviation: nil, abbreviationPriority: 0)] + let secondary = [VisualInstruction.Component.text(text: .init(text: "US 45 / Chicago", abbreviation: nil, abbreviationPriority: 0))] view.update(for: makeVisualInstruction(.turn, .right, primaryInstruction: primary, secondaryInstruction: secondary)) @@ -94,10 +94,10 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.maneuverView.isStart = true view.distance = 482 - let primary = [ - VisualInstructionComponent(type: .image, text: "I 280", imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: 0) + let primary: [VisualInstruction.Component] = [ + .image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), ] - let secondary = [VisualInstructionComponent(type: .text, text: "Mountain View Test", imageURL: nil, abbreviation: nil, abbreviationPriority: 0)] + let secondary = [VisualInstruction.Component.text(text: .init(text: "Mountain View Test", abbreviation: nil, abbreviationPriority: 0))] view.update(for: makeVisualInstruction(.turn, .right, primaryInstruction: primary, secondaryInstruction: secondary)) @@ -111,13 +111,15 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.maneuverView.isStart = true view.distance = 482 - let primary = [VisualInstructionComponent(type: .image, text: "I-280", imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .text, text: "Drive", imageURL: nil, abbreviation: "Dr", abbreviationPriority: 0), - VisualInstructionComponent(type: .text, text: "Avenue", imageURL: nil, abbreviation: "Ave", abbreviationPriority: 5), - VisualInstructionComponent(type: .text, text: "West", imageURL: nil, abbreviation: "W", abbreviationPriority: 4), - VisualInstructionComponent(type: .text, text: "South", imageURL: nil, abbreviation: "S", abbreviationPriority: 3), - VisualInstructionComponent(type: .text, text: "East", imageURL: nil, abbreviation: "E", abbreviationPriority: 2), - VisualInstructionComponent(type: .text, text: "North", imageURL: nil, abbreviation: "N", abbreviationPriority: 1)] + let primary: [VisualInstruction.Component] = [ + .image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), + .text(text: .init(text: "Drive", abbreviation: "Dr", abbreviationPriority: 0)), + .text(text: .init(text: "Avenue", abbreviation: "Ave", abbreviationPriority: 5)), + .text(text: .init(text: "West", abbreviation: "W", abbreviationPriority: 4)), + .text(text: .init(text: "South", abbreviation: "S", abbreviationPriority: 3)), + .text(text: .init(text: "East", abbreviation: "E", abbreviationPriority: 2)), + .text(text: .init(text: "North", abbreviation: "N", abbreviationPriority: 1)), + ] view.update(for: makeVisualInstruction(.continue, .straightAhead, primaryInstruction: primary, secondaryInstruction: nil)) @@ -131,13 +133,15 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.maneuverView.isStart = true view.distance = 482 - let primary = [VisualInstructionComponent(type: .image, text: "I 280", imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .delimiter, text: "/", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .text, text: "10", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .delimiter, text: "/", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .text, text: "15 North", imageURL: nil, abbreviation: "15 N", abbreviationPriority: 0), - VisualInstructionComponent(type: .delimiter, text: "/", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .text, text: "20 West", imageURL: nil, abbreviation: "20 W", abbreviationPriority: 1)] + let primary: [VisualInstruction.Component] = [ + .image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: nil)), + .delimiter(text: .init(text: "/", abbreviation: nil, abbreviationPriority: nil)), + .text(text: .init(text: "10", abbreviation: nil, abbreviationPriority: nil)), + .delimiter(text: .init(text: "/", abbreviation: nil, abbreviationPriority: nil)), + .text(text: .init(text: "15 North", abbreviation: "15 N", abbreviationPriority: 0)), + .delimiter(text: .init(text: "/", abbreviation: nil, abbreviationPriority: nil)), + .text(text: .init(text: "20 West", abbreviation: "20 W", abbreviationPriority: 1)), + ] imageRepository.storeImage(ShieldImage.i280.image, forKey: primary.first!.cacheKey!) view.update(for: makeVisualInstruction(.continue, .straightAhead, primaryInstruction: primary, secondaryInstruction: nil)) @@ -151,10 +155,10 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.distance = 482 - let primary = [ - VisualInstructionComponent(type: .text, text: "West", imageURL: nil, abbreviation: "W", abbreviationPriority: 0), - VisualInstructionComponent(type: .text, text: "Fremont", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .text, text: "Avenue", imageURL: nil, abbreviation: "Ave", abbreviationPriority: 1) + let primary: [VisualInstruction.Component] = [ + .text(text: .init(text: "West", abbreviation: "W", abbreviationPriority: 0)), + .text(text: .init(text: "Fremont", abbreviation: nil, abbreviationPriority: nil)), + .text(text: .init(text: "Avenue", abbreviation: "Ave", abbreviationPriority: 1)), ] view.update(for: makeVisualInstruction(.continue, .straightAhead, primaryInstruction: primary, secondaryInstruction: nil)) @@ -168,10 +172,10 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.maneuverView.isStart = true view.distance = 482 - let primary = [ - VisualInstructionComponent(type: .image, text: "I-280", imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .delimiter, text: "/", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .image, text: "US-101", imageURL: ShieldImage.us101.url, abbreviation: nil, abbreviationPriority: NSNotFound) + let primary: [VisualInstruction.Component] = [ + .image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: nil)), + .delimiter(text: .init(text: "/", abbreviation: nil, abbreviationPriority: nil)), + .image(image: .init(imageBaseURL: ShieldImage.us101.baseURL), alternativeText: .init(text: "US-101", abbreviation: nil, abbreviationPriority: nil)), ] view.update(for: makeVisualInstruction(.continue, .straightAhead, primaryInstruction: primary, secondaryInstruction: nil)) @@ -194,14 +198,14 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { instructionsBannerView.distance = 482 let primary = [ - VisualInstructionComponent(type: .image, text: "I 280", imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: 0) + VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), ] - let secondary = [VisualInstructionComponent(type: .text, text: "US 45 / Chicago", imageURL: nil, abbreviation: nil, abbreviationPriority: 0)] + let secondary = [VisualInstruction.Component.text(text: .init(text: "US 45 / Chicago", abbreviation: nil, abbreviationPriority: 0))] instructionsBannerView.update(for: makeVisualInstruction(.turn, .right, primaryInstruction: primary, secondaryInstruction: secondary)) let primaryThen = [ - VisualInstructionComponent(type: .image, text: "I 280", imageURL: ShieldImage.i280.url, abbreviation: nil, abbreviationPriority: 0) + VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), ] let primaryThenInstruction = VisualInstruction(text: nil, maneuverType: .none, maneuverDirection: .none, components: primaryThen) @@ -221,7 +225,7 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.distanceFormatter.locale = Locale(identifier: "zh-Hans") view.distance = 1000 * 999 - let primary = [VisualInstructionComponent(type: .text, text: "中国 安徽省 宣城市 郎溪县", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound)] + let primary = [VisualInstruction.Component.text(text: .init(text: "中国 安徽省 宣城市 郎溪县", abbreviation: nil, abbreviationPriority: nil))] view.update(for: makeVisualInstruction(.continue, .straightAhead, primaryInstruction: primary, secondaryInstruction: nil)) verify(view) @@ -235,7 +239,7 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.distanceFormatter.locale = Locale(identifier: "sv-se") view.distance = 1000 * 999 - let primary = [VisualInstructionComponent(type: .text, text: "Lorem Ipsum / Dolor Sit Amet", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound)] + let primary = [VisualInstruction.Component.text(text: .init(text: "Lorem Ipsum / Dolor Sit Amet", abbreviation: nil, abbreviationPriority: nil))] view.update(for: makeVisualInstruction(primaryInstruction: primary, secondaryInstruction: nil)) verify(view) @@ -249,7 +253,7 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.distanceFormatter.locale = Locale(identifier: "uk-UA") view.distance = 1000 * 999 - let primary = [VisualInstructionComponent(type: .text, text: "Lorem Ipsum / Dolor Sit Amet", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound)] + let primary = [VisualInstruction.Component.text(text: .init(text: "Lorem Ipsum / Dolor Sit Amet", abbreviation: nil, abbreviationPriority: nil))] view.update(for: makeVisualInstruction(primaryInstruction: primary, secondaryInstruction: nil)) verify(view) @@ -262,13 +266,13 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.maneuverView.isStart = true view.distance = 482 - let primary = [ - VisualInstructionComponent(type: .exit, text: "Exit", imageURL: nil, abbreviation: nil, abbreviationPriority: 0), - VisualInstructionComponent(type: .exitCode, text: "123A", imageURL: nil, abbreviation: nil, abbreviationPriority: 0), - VisualInstructionComponent(type: .text, text: "Main Street", imageURL: nil, abbreviation: "Main St", abbreviationPriority: 0) + let primary: [VisualInstruction.Component] = [ + .exit(text: .init(text: "Exit", abbreviation: nil, abbreviationPriority: 0)), + .exitCode(text: .init(text: "123A", abbreviation: nil, abbreviationPriority: 0)), + .text(text: .init(text: "Main Street", abbreviation: "Main St", abbreviationPriority: 0)), ] - let secondary = VisualInstructionComponent(type: .text, text: "Anytown Avenue", imageURL: nil, abbreviation: "Anytown Ave", abbreviationPriority: 0) + let secondary = VisualInstruction.Component.text(text: .init(text: "Anytown Avenue", abbreviation: "Anytown Ave", abbreviationPriority: 0)) DayStyle().apply() window.addSubview(view) @@ -284,12 +288,12 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.maneuverView.isStart = true view.distance = 482 - let primary = [ - VisualInstructionComponent(type: .image, text: "ANK 1", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound), - VisualInstructionComponent(type: .text, text: "Ankh-Morpork 1", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound) + let primary: [VisualInstruction.Component] = [ + .image(image: .init(imageBaseURL: nil), alternativeText: .init(text: "ANK 1", abbreviation: nil, abbreviationPriority: nil)), + .text(text: .init(text: "Ankh-Morpork 1", abbreviation: nil, abbreviationPriority: nil)), ] - let secondary = [VisualInstructionComponent(type: .text, text: "Vetinari Way", imageURL: nil, abbreviation: nil, abbreviationPriority: NSNotFound)] + let secondary = [VisualInstruction.Component.text(text: .init(text: "Vetinari Way", abbreviation: nil, abbreviationPriority: nil))] window.addSubview(view) DayStyle().apply() diff --git a/MapboxNavigationTests/InstructionsCardCollectionTests.swift b/MapboxNavigationTests/InstructionsCardCollectionTests.swift index 4da6de351c9..c1a2ef75a71 100644 --- a/MapboxNavigationTests/InstructionsCardCollectionTests.swift +++ b/MapboxNavigationTests/InstructionsCardCollectionTests.swift @@ -7,7 +7,7 @@ import MapboxDirections /// :nodoc: class InstructionsCardCollectionTests: XCTestCase { lazy var initialRoute: Route = { - return Fixture.route(from: jsonFileName) + return Fixture.route(from: jsonFileName, options: routeOptions) }() lazy var instructionsCardCollectionDataSource: (collection: InstructionsCardViewController, progress: RouteProgress, service: MapboxNavigationService, delegate: InstructionsCardCollectionDelegateSpy) = { @@ -25,7 +25,10 @@ class InstructionsCardCollectionTests: XCTestCase { return guidanceCard.view.constraintsForPinning(to: container) } - let fakeRoute = Fixture.route(from: "route-with-banner-instructions") + let fakeRoute = Fixture.route(from: "route-with-banner-instructions", options: NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 37.764793, longitude: -122.463161), + CLLocationCoordinate2D(latitude: 34.054081, longitude: -118.243412), + ])) let service = MapboxNavigationService(route: initialRoute, directions: DirectionsSpy(accessToken: "adbeknut"), simulating: .never) let routeProgress = RouteProgress(route: fakeRoute) diff --git a/MapboxNavigationTests/LaneTests.swift b/MapboxNavigationTests/LaneTests.swift index 89208ad7073..0121c8b0d30 100644 --- a/MapboxNavigationTests/LaneTests.swift +++ b/MapboxNavigationTests/LaneTests.swift @@ -31,8 +31,7 @@ class LaneTests: FBSnapshotTestCase { let groupView = UIStackView(orientation: .vertical, autoLayout: true) groupView.alignment = .center - let component = LaneIndicationComponent(indications: lane.indications, isUsable: true) - let laneView = LaneView(component: component) + let laneView = LaneView(indications: lane.indications, isUsable: true) laneView.drivingSide = lane.drivingSide laneView.backgroundColor = .white diff --git a/MapboxNavigationTests/LeaksSpec.swift b/MapboxNavigationTests/LeaksSpec.swift index afc6e887c99..6654f586cbf 100644 --- a/MapboxNavigationTests/LeaksSpec.swift +++ b/MapboxNavigationTests/LeaksSpec.swift @@ -8,11 +8,7 @@ import MapboxDirections class LeaksSpec: QuickSpec { lazy var initialRoute: Route = { - let jsonRoute = (response["routes"] as! [AnyObject]).first as! [String: Any] - let waypoint1 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.795042, longitude: -122.413165)) - let waypoint2 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.7727, longitude: -122.433378)) - let route = Route(json: jsonRoute, waypoints: [waypoint1, waypoint2], options: NavigationRouteOptions(waypoints: [waypoint1, waypoint2])) - + let route = response.routes!.first! route.accessToken = "foo" return route diff --git a/MapboxNavigationTests/ManeuverArrowTests.swift b/MapboxNavigationTests/ManeuverArrowTests.swift index dd2fd4c7f11..d2f1a6fbfad 100644 --- a/MapboxNavigationTests/ManeuverArrowTests.swift +++ b/MapboxNavigationTests/ManeuverArrowTests.swift @@ -6,7 +6,11 @@ import TestHelper @testable import MapboxCoreNavigation class ManeuverArrowTests: FBSnapshotTestCase { - let waypointRoute = Fixture.route(from: "waypoint-after-turn") + let waypointRoute = Fixture.route(from: "waypoint-after-turn", options: NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 37.787358, longitude: -122.408231), + CLLocationCoordinate2D(latitude: 37.790177, longitude: -122.408687), + CLLocationCoordinate2D(latitude: 37.788986, longitude: -122.406892), + ])) override func setUp() { super.setUp() diff --git a/MapboxNavigationTests/ManeuverViewTests.swift b/MapboxNavigationTests/ManeuverViewTests.swift index 8cc1bff19f1..baa05759c40 100644 --- a/MapboxNavigationTests/ManeuverViewTests.swift +++ b/MapboxNavigationTests/ManeuverViewTests.swift @@ -18,8 +18,8 @@ class ManeuverViewTests: FBSnapshotTestCase { window.addSubview(maneuverView) } - func maneuverInstruction(_ maneuverType: ManeuverType, _ maneuverDirection: ManeuverDirection, _ degrees: CLLocationDegrees = 180) -> VisualInstruction { - let component = VisualInstructionComponent(type: .delimiter, text: "", imageURL: nil, abbreviation: nil, abbreviationPriority: 0) + func maneuverInstruction(_ maneuverType: ManeuverType?, _ maneuverDirection: ManeuverDirection?, _ degrees: CLLocationDegrees = 180) -> VisualInstruction { + let component = VisualInstruction.Component.delimiter(text: .init(text: "", abbreviation: nil, abbreviationPriority: nil)) return VisualInstruction(text: "", maneuverType: maneuverType, maneuverDirection: maneuverDirection, components: [component], degrees: degrees) } @@ -54,7 +54,7 @@ class ManeuverViewTests: FBSnapshotTestCase { } func testArriveNone() { - maneuverView.visualInstruction = maneuverInstruction(.arrive, .none) + maneuverView.visualInstruction = maneuverInstruction(.arrive, nil) verify(maneuverView.layer) } diff --git a/MapboxNavigationTests/MapboxVoiceControllerTests.swift b/MapboxNavigationTests/MapboxVoiceControllerTests.swift index e343031c6ba..4427cb8cedc 100644 --- a/MapboxNavigationTests/MapboxVoiceControllerTests.swift +++ b/MapboxNavigationTests/MapboxVoiceControllerTests.swift @@ -11,7 +11,10 @@ class MapboxVoiceControllerTests: XCTestCase { var route: Route { get { - return Fixture.route(from: "route-with-instructions") + return Fixture.route(from: "route-with-instructions", options: NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 40.311012, longitude: -112.47926), + CLLocationCoordinate2D(latitude: 29.99908, longitude: -102.828197), + ])) } } diff --git a/MapboxNavigationTests/NavigationMapViewTests.swift b/MapboxNavigationTests/NavigationMapViewTests.swift index 4a259926bc3..96f7cc50bf8 100644 --- a/MapboxNavigationTests/NavigationMapViewTests.swift +++ b/MapboxNavigationTests/NavigationMapViewTests.swift @@ -5,15 +5,15 @@ import TestHelper @testable import MapboxCoreNavigation class NavigationMapViewTests: XCTestCase, MGLMapViewDelegate { - let response = Fixture.JSONFromFileNamed(name: "route-with-instructions") + let response = Fixture.routeResponse(from: "route-with-instructions", options: NavigationRouteOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 40.311012, longitude: -112.47926), + CLLocationCoordinate2D(latitude: 29.99908, longitude: -102.828197), + ])) var styleLoadingExpectation: XCTestExpectation? var mapView: NavigationMapView? lazy var route: Route = { - let jsonRoute = (response["routes"] as! [AnyObject]).first as! [String: Any] - let waypoint1 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.795042, longitude: -122.413165)) - let waypoint2 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.7727, longitude: -122.433378)) - let route = Route(json: jsonRoute, waypoints: [waypoint1, waypoint2], options: NavigationRouteOptions(waypoints: [waypoint1, waypoint2])) + let route = response.routes!.first! route.accessToken = "foo" return route }() diff --git a/MapboxNavigationTests/NavigationViewControllerTests.swift b/MapboxNavigationTests/NavigationViewControllerTests.swift index 0b4706e29d1..cfde418e90d 100644 --- a/MapboxNavigationTests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/NavigationViewControllerTests.swift @@ -6,7 +6,12 @@ import Turf @testable import MapboxNavigation let jsonFileName = "routeWithInstructions" -let response = Fixture.JSONFromFileNamed(name: jsonFileName) +var routeOptions: NavigationRouteOptions { + let from = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.795042, longitude: -122.413165)) + let to = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.7727, longitude: -122.433378)) + return NavigationRouteOptions(waypoints: [from, to]) +} +let response = Fixture.routeResponse(from: jsonFileName, options: routeOptions) let otherResponse = Fixture.JSONFromFileNamed(name: "route-for-lane-testing") class NavigationViewControllerTests: XCTestCase { @@ -24,7 +29,7 @@ class NavigationViewControllerTests: XCTestCase { let navigationService = navigationViewController.navigationService! let router = navigationService.router! - let firstCoord = router.routeProgress.nearbyCoordinates.first! + let firstCoord = router.routeProgress.nearbyShape.coordinates.first! let firstLocation = location(at: firstCoord) var poi = [CLLocation]() @@ -36,18 +41,18 @@ class NavigationViewControllerTests: XCTestCase { poi.append(location(at: turkStreetIntersection.location)) poi.append(location(at: fultonStreetIntersection.location)) - let lastCoord = router.routeProgress.currentLegProgress.remainingSteps.last!.coordinates!.first! + let lastCoord = router.routeProgress.currentLegProgress.remainingSteps.last!.shape!.coordinates.first! let lastLocation = location(at: lastCoord) return (navigationViewController: navigationViewController, navigationService: navigationService, startLocation: firstLocation, poi: poi, endLocation: lastLocation, voice: fakeVoice) }() lazy var initialRoute: Route = { - return Fixture.route(from: jsonFileName) + return Fixture.route(from: jsonFileName, options: routeOptions) }() lazy var newRoute: Route = { - return Fixture.route(from: jsonFileName) + return Fixture.route(from: jsonFileName, options: routeOptions) }() override func setUp() { @@ -200,11 +205,12 @@ class NavigationViewControllerTests: XCTestCase { return !navigationViewController.mapView!.annotations!.isEmpty }) - guard let annotations = navigationViewController.mapView?.annotations else { return XCTFail("Annotations not found.")} - + guard let annotations = navigationViewController.mapView?.annotations?.compactMap({ $0 as? MGLPointAnnotation }) else { + return XCTFail("No point annotations found.") + } + let firstDestination = initialRoute.routeOptions.waypoints.last!.coordinate - let destinations = annotations.filter(annotationFilter(matching: firstDestination)) - XCTAssert(!destinations.isEmpty, "Destination annotation does not exist on map") + XCTAssert(annotations.contains { $0.coordinate.distance(to: firstDestination) < 1 }, "Destination annotation does not exist on map") //lets set the second route navigationViewController.route = newRoute @@ -213,15 +219,17 @@ class NavigationViewControllerTests: XCTestCase { let secondDestination = newRoute.routeOptions.waypoints.last!.coordinate //do we have a destination on the second route? - let newDestinations = newAnnotations.filter(annotationFilter(matching: secondDestination)) - XCTAssert(!newDestinations.isEmpty, "New destination annotation does not exist on map") + XCTAssert(newAnnotations.contains { $0.coordinate.distance(to: secondDestination) < 1 }, "New destination annotation does not exist on map") } func testBlankBanner() { let window = UIApplication.shared.keyWindow! let viewController = window.rootViewController! - let route = Fixture.route(from: "DCA-Arboretum") + 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 navigationViewController = NavigationViewController(for: route) viewController.present(navigationViewController, animated: false, completion: nil) @@ -242,7 +250,10 @@ class NavigationViewControllerTests: XCTestCase { let bottom = BottomBannerFake(nibName: nil, bundle: nil) let fakeOptions = NavigationOptions(topBanner: top, bottomBanner: bottom) - let route = Fixture.route(from: "DCA-Arboretum") + 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 subject = NavigationViewController(for: route, options: fakeOptions) XCTAssert(subject.topViewController == top, "Top banner not injected properly into NVC") @@ -250,14 +261,6 @@ class NavigationViewControllerTests: XCTestCase { XCTAssert(subject.mapViewController!.children.contains(top), "Top banner not found in child VC heirarchy") XCTAssert(subject.mapViewController!.children.contains(bottom), "Bottom banner not found in child VC heirarchy") } - - private func annotationFilter(matching coordinate: CLLocationCoordinate2D) -> ((MGLAnnotation) -> Bool) { - let filter = { (annotation: MGLAnnotation) -> Bool in - guard let pointAnno = annotation as? MGLPointAnnotation else { return false } - return pointAnno.coordinate == coordinate - } - return filter - } } extension NavigationViewControllerTests: NavigationViewControllerDelegate, StyleManagerDelegate { diff --git a/MapboxNavigationTests/RouteControllerSnapshotTests.swift b/MapboxNavigationTests/RouteControllerSnapshotTests.swift index 826c997ba79..d3586ca856a 100644 --- a/MapboxNavigationTests/RouteControllerSnapshotTests.swift +++ b/MapboxNavigationTests/RouteControllerSnapshotTests.swift @@ -21,7 +21,21 @@ class RouteControllerSnapshotTests: FBSnapshotTestCase { } func testRouteSnappingOvershooting() { - let route = Fixture.routesFromMatches(at: "sthlm-double-back")![0] + let route = Fixture.routesFromMatches(at: "sthlm-double-back", options: NavigationMatchOptions(coordinates: [ + .init(latitude: 59.337928, longitude: 18.076841), + .init(latitude: 59.337661, longitude: 18.075897), + .init(latitude: 59.337129, longitude: 18.075478), + .init(latitude: 59.336866, longitude: 18.075273), + .init(latitude: 59.336623, longitude: 18.075806), + .init(latitude: 59.336391, longitude: 18.076943), + .init(latitude: 59.338731, longitude: 18.079343), + .init(latitude: 59.339058, longitude: 18.07774), + .init(latitude: 59.338901, longitude: 18.076929), + .init(latitude: 59.338333, longitude: 18.076467), + .init(latitude: 59.338156, longitude: 18.075723), + .init(latitude: 59.338311, longitude: 18.074968), + .init(latitude: 59.33865, longitude: 18.074935), + ]))![0] let bundle = Bundle(for: RouteControllerSnapshotTests.self) let filePath = bundle.path(forResource: "sthlm-double-back-replay", ofType: "json") diff --git a/MapboxNavigationTests/RouteTests.swift b/MapboxNavigationTests/RouteTests.swift index d5f9bbc2bc1..32df09dba29 100644 --- a/MapboxNavigationTests/RouteTests.swift +++ b/MapboxNavigationTests/RouteTests.swift @@ -2,25 +2,58 @@ import XCTest import MapboxDirections import TestHelper import Turf +import MapboxCoreNavigation @testable import MapboxNavigation class RouteTests: XCTestCase { func testPolylineAroundManeuver() { // Convert the match from https://github.com/mapbox/navigation-ios-examples/pull/28 into a route. // The details of the route are unimportant; what matters is the geometry. - let json = Fixture.JSONFromFileNamed(name: "route-doubling-back") - let namedWaypoints = (json["tracepoints"] as! [[String: Any]?]).compactMap { jsonTracepoint -> Waypoint? in - guard let jsonTracepoint = jsonTracepoint else { - return nil - } - let location = jsonTracepoint["location"] as! [Double] - let coordinate = CLLocationCoordinate2D(latitude: location[1], longitude: location[0]) - return Waypoint(coordinate: coordinate, name: jsonTracepoint["name"] as? String ?? "") - } - let fakeOptions = RouteOptions(coordinates: [namedWaypoints.first!.coordinate, namedWaypoints.last!.coordinate]) - let routes = (json["matchings"] as? [[String: Any]])?.map { - Route(json: $0, waypoints: namedWaypoints, options: fakeOptions) - } + let options = NavigationMatchOptions(coordinates: [ + .init(latitude: 59.3379254707993, longitude: 18.0768391763866), + .init(latitude: 59.3376613543215, longitude: 18.0758977499228), + .init(latitude: 59.3371292341531, longitude: 18.0754779388695), + .init(latitude: 59.3368658096911, longitude: 18.0752713263541), + .init(latitude: 59.3366161271274, longitude: 18.0758013323718), + .init(latitude: 59.3363847683606, longitude: 18.0769377012062), + .init(latitude: 59.3369299420601, longitude: 18.0779707637829), + .init(latitude: 59.3374784940673, longitude: 18.0789771102838), + .init(latitude: 59.3376624022706, longitude: 18.0796752015449), + .init(latitude: 59.3382345065107, longitude: 18.0801207199294), + .init(latitude: 59.338728497517, longitude: 18.0793407846583), + .init(latitude: 59.3390538588298, longitude: 18.0777368583247), + .init(latitude: 59.3389021418961, longitude: 18.0769242264769), + .init(latitude: 59.3383325439362, longitude: 18.0764655674924), + .init(latitude: 59.3381526945276, longitude: 18.0757203959448), + .init(latitude: 59.3383085323927, longitude: 18.0749662844197), + .init(latitude: 59.3386507394432, longitude: 18.0749292910378), + .init(latitude: 59.3396600470949, longitude: 18.0757133256584), + .init(latitude: 59.3402031271014, longitude: 18.0770724776848), + .init(latitude: 59.3399246668736, longitude: 18.0784376357593), + .init(latitude: 59.3393711961939, longitude: 18.0786765675365), + .init(latitude: 59.3383675368975, longitude: 18.0778982052741), + .init(latitude: 59.3379254707993, longitude: 18.0768391763866), + .init(latitude: 59.3376613543215, longitude: 18.0758977499228), + .init(latitude: 59.3371292341531, longitude: 18.0754779388695), + .init(latitude: 59.3368658096911, longitude: 18.0752713263541), + .init(latitude: 59.3366161271274, longitude: 18.0758013323718), + .init(latitude: 59.3363847683606, longitude: 18.0769377012062), + .init(latitude: 59.3369299420601, longitude: 18.0779707637829), + .init(latitude: 59.3374784940673, longitude: 18.0789771102838), + .init(latitude: 59.3376624022706, longitude: 18.0796752015449), + .init(latitude: 59.3382345065107, longitude: 18.0801207199294), + .init(latitude: 59.338728497517, longitude: 18.0793407846583), + .init(latitude: 59.3390538588298, longitude: 18.0777368583247), + .init(latitude: 59.3389021418961, longitude: 18.0769242264769), + .init(latitude: 59.3383325439362, longitude: 18.0764655674924), + .init(latitude: 59.3381526945276, longitude: 18.0757203959448), + .init(latitude: 59.3383085323927, longitude: 18.0749662844197), + .init(latitude: 59.3386507394432, longitude: 18.0749292910378), + .init(latitude: 59.3396600470949, longitude: 18.0757133256584), + ], profileIdentifier: .automobile) + options.shapeFormat = .polyline + let response = Fixture.mapMatchingResponse(from: "route-doubling-back", options: options) + let routes = response.routes let route = routes!.first! let leg = route.legs.first! @@ -28,9 +61,9 @@ class RouteTests: XCTestCase { let traversals = [1, 8, 13, 20] for stepIndex in traversals { let precedingStep = leg.steps[stepIndex - 1] - let precedingStepPolyline = Polyline(precedingStep.coordinates!) + let precedingStepPolyline = precedingStep.shape! let followingStep = leg.steps[stepIndex] - let stepPolyline = Polyline(followingStep.coordinates!) + let stepPolyline = followingStep.shape! let maneuverPolyline = route.polylineAroundManeuver(legIndex: 0, stepIndex: stepIndex, distance: 30) let firstIndexedCoordinate = precedingStepPolyline.closestCoordinate(to: maneuverPolyline.coordinates[0]) diff --git a/MapboxNavigationTests/SimulatedLocationManagerTests.swift b/MapboxNavigationTests/SimulatedLocationManagerTests.swift index 0468ef6b8c6..cb74a201921 100644 --- a/MapboxNavigationTests/SimulatedLocationManagerTests.swift +++ b/MapboxNavigationTests/SimulatedLocationManagerTests.swift @@ -13,7 +13,21 @@ class SimulatedLocationManagerTests: FBSnapshotTestCase { } func testSimulateRouteDoublesBack() { - let route = Fixture.routesFromMatches(at: "sthlm-double-back")![0] + let route = Fixture.routesFromMatches(at: "sthlm-double-back", options: NavigationMatchOptions(coordinates: [ + .init(latitude: 59.337928, longitude: 18.076841), + .init(latitude: 59.337661, longitude: 18.075897), + .init(latitude: 59.337129, longitude: 18.075478), + .init(latitude: 59.336866, longitude: 18.075273), + .init(latitude: 59.336623, longitude: 18.075806), + .init(latitude: 59.336391, longitude: 18.076943), + .init(latitude: 59.338731, longitude: 18.079343), + .init(latitude: 59.339058, longitude: 18.07774), + .init(latitude: 59.338901, longitude: 18.076929), + .init(latitude: 59.338333, longitude: 18.076467), + .init(latitude: 59.338156, longitude: 18.075723), + .init(latitude: 59.338311, longitude: 18.074968), + .init(latitude: 59.33865, longitude: 18.074935), + ]))![0] let locationManager = SimulatedLocationManager(route: route) let locationManagerSpy = SimulatedLocationManagerSpy() locationManager.delegate = locationManagerSpy diff --git a/MapboxNavigationTests/StepsViewControllerTests.swift b/MapboxNavigationTests/StepsViewControllerTests.swift index a48cb835bc5..412947b7018 100644 --- a/MapboxNavigationTests/StepsViewControllerTests.swift +++ b/MapboxNavigationTests/StepsViewControllerTests.swift @@ -6,7 +6,7 @@ import MapboxDirections class StepsViewControllerTests: XCTestCase { struct Constants { - static let jsonRoute = (response["routes"] as! [AnyObject]).first as! [String: Any] + static let route = response.routes!.first! static let accessToken = "nonsense" } @@ -19,19 +19,17 @@ class StepsViewControllerTests: XCTestCase { let stepsViewController = StepsViewController(routeProgress: routeController.routeProgress) - let firstCoord = routeController.routeProgress.nearbyCoordinates.first! + let firstCoord = routeController.routeProgress.nearbyShape.coordinates.first! let firstLocation = CLLocation(coordinate: firstCoord, altitude: 5, horizontalAccuracy: 10, verticalAccuracy: 5, course: 20, speed: 4, timestamp: Date()) - let lastCoord = routeController.routeProgress.currentLegProgress.remainingSteps.last!.coordinates!.first! + let lastCoord = routeController.routeProgress.currentLegProgress.remainingSteps.last!.shape!.coordinates.first! let lastLocation = CLLocation(coordinate: lastCoord, altitude: 5, horizontalAccuracy: 10, verticalAccuracy: 5, course: 20, speed: 4, timestamp: Date()) return (stepsViewController: stepsViewController, routeController: routeController, firstLocation: firstLocation, lastLocation: lastLocation) }() lazy var initialRoute: Route = { - let waypoint1 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 37.764793, longitude: -122.463161)) - let waypoint2 = Waypoint(coordinate: CLLocationCoordinate2D(latitude: 34.054081, longitude: -118.243412)) - let route = Route(json: Constants.jsonRoute, waypoints: [waypoint1, waypoint2], options: NavigationRouteOptions(waypoints: [waypoint1, waypoint2])) + let route = Constants.route route.accessToken = "nonsense" return route }() diff --git a/TestHelper/DirectionsSpy.swift b/TestHelper/DirectionsSpy.swift index 4eab601222c..85d8e91dc2a 100644 --- a/TestHelper/DirectionsSpy.swift +++ b/TestHelper/DirectionsSpy.swift @@ -19,7 +19,7 @@ public class DirectionsSpy: Directions { return DummyURLSessionDataTask() } - public func fireLastCalculateCompletion(with waypoints: [Waypoint]?, routes: [Route]?, error: NSError?) { + public func fireLastCalculateCompletion(with waypoints: [Waypoint]?, routes: [Route]?, error: DirectionsError?) { guard let lastCalculateOptionsCompletion = lastCalculateOptionsCompletion else { assert(false, "Can't fire a completion handler which doesn't exist!") return diff --git a/TestHelper/Fixture.swift b/TestHelper/Fixture.swift index cade0ab9b40..7ead20e2c79 100644 --- a/TestHelper/Fixture.swift +++ b/TestHelper/Fixture.swift @@ -17,21 +17,14 @@ public class Fixture: NSObject { } } - public class func JSONFromFileNamed(name: String) -> [String: Any] { + public class func JSONFromFileNamed(name: String) -> Data { guard let path = Bundle(for: Fixture.self).path(forResource: name, ofType: "json") else { - assert(false, "Fixture \(name) not found.") - return [:] + preconditionFailure("Fixture \(name) not found.") } guard let data = NSData(contentsOfFile: path) as Data? else { - assert(false, "No data found at \(path).") - return [:] - } - do { - return try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] - } catch { - assert(false, "Unable to decode JSON fixture at \(path): \(error).") - return [:] + preconditionFailure("No data found at \(path).") } + return data } public class func downloadRouteFixture(coordinates: [CLLocationCoordinate2D], fileName: String, completion: @escaping () -> Void) { @@ -70,14 +63,36 @@ public class Fixture: NSObject { return locations.map { CLLocation($0) } } - public class func route(from jsonFile: String) -> Route { - let response = JSONFromFileNamed(name: jsonFile) - let waypoints = Fixture.waypoints(from: jsonFile) - let jsonRoute = (response["routes"] as! [AnyObject]).first as! [String : Any] - let route = Route(json: jsonRoute, waypoints: waypoints, options: NavigationRouteOptions(waypoints: waypoints)) + public class func routeResponse(from jsonFile: String, options: RouteOptions) -> RouteResponse { + let responseData = JSONFromFileNamed(name: jsonFile) + do { + let decoder = JSONDecoder() + decoder.userInfo[.options] = options + return try decoder.decode(RouteResponse.self, from: responseData) + } catch { + preconditionFailure("Unable to decode JSON fixture: \(error)") + } + } + + public class func mapMatchingResponse(from jsonFile: String, options: MatchOptions) -> MapMatchingResponse { + let responseData = JSONFromFileNamed(name: jsonFile) + do { + let decoder = JSONDecoder() + decoder.userInfo[.options] = options + return try decoder.decode(MapMatchingResponse.self, from: responseData) + } catch { + preconditionFailure("Unable to decode JSON fixture: \(error)") + } + } + + public class func route(from jsonFile: String, options: RouteOptions) -> Route { + let response = routeResponse(from: jsonFile, options: options) + guard let route = response.routes?.first else { + preconditionFailure("No routes") + } // Like `Directions.postprocess(_:fetchStartDate:uuid:)` - route.routeIdentifier = response["uuid"] as? String + route.routeIdentifier = response.uuid let fetchStartDate = Date(timeIntervalSince1970: 3600) route.fetchStartDate = fetchStartDate route.responseEndDate = Date(timeInterval: 1, since: fetchStartDate) @@ -85,41 +100,21 @@ public class Fixture: NSObject { return route } - public class func waypoints(from jsonFile: String) -> [Waypoint] { - let response = JSONFromFileNamed(name: jsonFile) - let waypointsArray = response["waypoints"] as! [[String: Any]] - let waypoints = waypointsArray.map { (waypointDict) -> Waypoint in - let location = waypointDict["location"] as! [CLLocationDegrees] - let longitude = location[0] - let latitude = location[1] - return Waypoint(coordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude)) + public class func waypoints(from jsonFile: String, options: RouteOptions) -> [Waypoint] { + let response = routeResponse(from: jsonFile, options: options) + guard let waypoints = response.waypoints else { + preconditionFailure("No waypoints") } return waypoints } // Returns `Route` objects from a match response - public class func routesFromMatches(at filePath: String) -> [Route]? { - let path = Bundle(for: Fixture.self).path(forResource: filePath, ofType: "json") - let url = URL(fileURLWithPath: path!) - let data = try! Data(contentsOf: url) - let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] - let tracepoints = json["tracepoints"] as! [Any] - let coordinates = Array(repeating: CLLocationCoordinate2D(latitude: 0, longitude: 0), count: tracepoints.count) - - // Adapted from MatchOptions.response(containingRoutesFrom:) in MapboxDirections. - let jsonWaypoints = json["tracepoints"] as! [Any] - // Assume MatchOptions.waypointIndices contains the first and last indices only. - let waypoints = [jsonWaypoints.first!, jsonWaypoints.last!].map { jsonWaypoint -> Waypoint in - let jsonWaypoint = jsonWaypoint as! [String: Any] - let location = jsonWaypoint["location"] as! [Double] - let coordinate = CLLocationCoordinate2D(latitude: location[1], longitude: location[0]) - return Waypoint(coordinate: coordinate, name: jsonWaypoint["name"] as? String) - } - let opts = RouteOptions(coordinates: coordinates, profileIdentifier: .automobile) - - return (json["matchings"] as? [[String: Any]])?.map { - Route(json: $0, waypoints: waypoints, options: opts) + public class func routesFromMatches(at filePath: String, options: MatchOptions) -> [Route]? { + let response = mapMatchingResponse(from: filePath, options: options) + guard let routes = response.routes else { + preconditionFailure("No routes") } + return routes } public class func generateTrace(for route: Route, speedMultiplier: Double = 1) -> [CLLocation] { diff --git a/TestHelper/Fixtures/DCA-Arboretum-dummy-faster-route.json b/TestHelper/Fixtures/DCA-Arboretum-dummy-faster-route.json index f3fbfa3976a..e88f58b79e1 100644 --- a/TestHelper/Fixtures/DCA-Arboretum-dummy-faster-route.json +++ b/TestHelper/Fixtures/DCA-Arboretum-dummy-faster-route.json @@ -1 +1 @@ -{"routes":[{"geometry":"g~kbiAdgj}qC_fBdh@k}@|WaaAfb@qa@vMc@Je^zJsd@dNgWvFeOdAqKR_EGwIs@cOkAyQkCkMkDePuFqZaLei@{_@ea@e]}TcU}r@k|@a|@ubAmAoAq_@k`@qXoUiGgEqJyGyFsCiN_HgCoA{DgBwEiBeEcBkEwAuD_AkEgA{EkAcF}@iEe@eFq@{Fk@qEc@}DO{F_@mFI}DIaFFuENaFT_F\\wE^wEl@cFt@kEz@qE~@wEfAuFjB_EpAoD`BqFbCuDjBgDbBgF|CaDpBiElCoEvCqOpJoDrCoIjH{IxI{OvPsPtOcFvDeKfJkb@r[cF|BwDh@}DDgHmCqCsBgSkLuzAcwA{OaO_kC_gCsdAc_AkUm^aCkE_C_F}BoFsBiF_BsFeBgGoAcGgAqFcAaHs@mGc@eG]wGYcHSyGEmPBuHBwE?gIFkGAeGAoHKcHUaG[eHg@wGm@qGu@uGy@gGkAkGwAkGsAqFkBgG_BiFgB{FoBoEiBgEqCqF{BeEgDcFsj@}s@}I{K{_@ii@iDqFsCuEwCeFiDoHuFgLqC_HwAyDuBkG_BcGwAgFsBqHkA{EiAaGqAuG_AgFy@iFq@sEQcBu@iFy@mGq@cHoBcXUwF]{GU{GIqGGsPD_b@A}dAE}PQ{XuAu}@eAs|CQqdABi_@rCys@?cEA}GFyEHkDTe\\Fs_CWgQkAsRkBaOmCmMeFsP}EwMsAkCuCmDuC{EyCiEiGmJmJsGyDmCuTuHuIkB_Hm@kHe@iJBcPb@{IAyLT_F~@uEfAcExAqEdB{E|AiEdBmBjAsLzHgM`KcQxQsOhMmJrIsNzDi`@|IuUnE{xEtAozAPyvBhBiA?oEGoFHkc@CmA@gEHiGCeREmEDaF?gE?eF?uE?iF?cE?wE?aF?wE?qE?kF?iE?mF@kE?_E?eF@sECeF?{D@qF@iE?iF@{E@cE@eFEyE@w@EwaCu@sQ|@gN`D_V`JmUvN_D`BgEdCcEbCiDrBiEdCsEjCqDvB{DzBqElCgEdCsDvBcEzB{EpCyD|B}DzBkEfC}D|BkEfCmDpBkDpByDnBqBf@_ADoD?wBc@u@YoBuG_BoGkGaW}@sDw@{C_FeS{VgdAoD{NaZokA_l@eaCkLse@yEwOyD_PeFgTgAqEo`@u_BuDiOqHcZ_AeDmEePkH}[oE{PqF}U}BiJk@gCgu@o{CuZspAiSwy@eMah@}Iw^qE}QaYyiA{C}L}DaP{Qeu@mPop@u^yxAae@qkBa@iBuIe`@kDwK{GoXiJ}_@iSgz@aY_kAcy@ogDu[{qAiRqx@o^awAwByJcBwHiAgFaAmGyAuJaAiMUiCc@aRM{Yo@ij@EgCOwXKuTIeZYu]EeGe@yr@AsC]mr@MqX?{a@?{GAag@@eRI_l@GqGMqOKqNKqLYmXeAigAv@eQ~AyWvDclCnD{tAzCyx@`SnAx^vBB|A^tAv@z@dAXhAK|@q@h@kAL}ArQvAbRdAzJl@pPbA~A`CdAvAd@x@x@pAdAlBzAtCrAdAt@b@t@N|@DbA@|@Ch@Ib@MjUqHf@MlHmCfGwBfRmGrEuAvN_F~D{AvGuBzEzFrL`On@`Aj@dAf@hAfA|CtAtMP~BPbFFxCFpHDxKBdBBlEBdB@fBD|CJdDDt@Bv@Dt@Bv@JlBXzCl@jEh@tC^pAv@hDr@lDj@nDRvAl@vF\\hDxNIf@}@|@wBbAqBhAkBdAyAzAiB~AeBdB{AfBuAjBqAfDqBv@a@lDcBtB{@|Bs@~Bq@`E_AbCe@dDg@xC]jBO","legs":[{"summary":"Aviation Circle - departures, New York Avenue Northeast","weight":450,"duration":500,"steps":[{"driving_side":"right","geometry":"cv|ciAlx~|qCkUm^aCkE_C_F}BoFsBiF_BsFeBgGoAcGgAqFcAaHs@mGc@eG]wGYcHSyGEmPBuHBwE?gIFkGAeGAoHKcHUaG[eHg@wGm@qGu@uGy@gGkAkGwAkGsAqFkBgG_BiFgB{FoBoEiBgEqCqF{BeEgDcFsj@}s@}I{K{_@ii@iDqFsCuEwCeFiDoHuFgLqC_HwAyDuBkG_BcGwAgFsBqHkA{EiAaGqAuG_AgFy@iFq@sEQcBu@iFy@mGq@cHoBcXUwF]{GU{GIqGGsPD_b@A}dAE}PQ{XuAu}@eAs|CQqdABi_@","mode":"driving","maneuver":{"bearing_after":46,"bearing_before":35,"location":[-77.037463,38.878066],"modifier":"slight right","type":"fork","instruction":"Keep right onto I-395 North"},"ref":"I-395 North","weight":191.8,"duration":191.5,"name":"I-395 North","distance":1780.6},{"distance":3055.7,"name":"Center Leg Freeway (I-395 North)","ref":"I-395 North","maneuver":{"bearing_after":95,"bearing_before":90,"location":[-77.018635,38.882355],"modifier":"slight right","type":"fork","instruction":"Keep right towards I-395 North: US Senate"},"destinations":"I-395 North: US Senate","weight":206.60000000000002,"mode":"driving","geometry":"ebediAt_z{qCrCys@?cEA}GFyEHkDTe\\Fs_CWgQkAsRkBaOmCmMeFsP}EwMsAkCuCmDuC{EyCiEiGmJmJsGyDmCuTuHuIkB_Hm@kHe@iJBcPb@{IAyLT_F~@uEfAcExAqEdB{E|AiEdBmBjAsLzHgM`KcQxQsOhMmJrIsNzDi`@|IuUnE{xEtAozAPyvBhBiA?oEGoFHkc@CmA@gEHiGCeREmEDaF?gE?eF?uE?iF?cE?wE?aF?wE?qE?kF?iE?mF@kE?_E?eF@sECeF?{D@qF@iE?iF@{E@cE@eFEyE@w@EwaCu@sQ|@gN`D_V`JmUvN_D`BgEdCcEbCiDrBiEdCsEjCqDvB{DzBqElCgEdCsDvBcEzB{EpCyD|B}DzBkEfC}D|BkEfCmDpBkDpByDnBqBf@_ADoD?wBc@u@Y","driving_side":"right"}],"distance":13651.1}],"weight_name":"routability","weight":450,"duration":500,"distance":13651.1,"voiceLocale":"en-US"}],"waypoints":[{"name":"Aviation Circle - departures","location":[-77.037265,38.878206]},{"name":"Ellipse Road Northeast","location":[-76.966906,38.910736]}],"code":"Ok","uuid":"cjo4pxi1801m76msa4nwo6wzh"} \ No newline at end of file +{"routes":[{"geometry":"g~kbiAdgj}qC_fBdh@k}@|WaaAfb@qa@vMc@Je^zJsd@dNgWvFeOdAqKR_EGwIs@cOkAyQkCkMkDePuFqZaLei@{_@ea@e]}TcU}r@k|@a|@ubAmAoAq_@k`@qXoUiGgEqJyGyFsCiN_HgCoA{DgBwEiBeEcBkEwAuD_AkEgA{EkAcF}@iEe@eFq@{Fk@qEc@}DO{F_@mFI}DIaFFuENaFT_F\\wE^wEl@cFt@kEz@qE~@wEfAuFjB_EpAoD`BqFbCuDjBgDbBgF|CaDpBiElCoEvCqOpJoDrCoIjH{IxI{OvPsPtOcFvDeKfJkb@r[cF|BwDh@}DDgHmCqCsBgSkLuzAcwA{OaO_kC_gCsdAc_AkUm^aCkE_C_F}BoFsBiF_BsFeBgGoAcGgAqFcAaHs@mGc@eG]wGYcHSyGEmPBuHBwE?gIFkGAeGAoHKcHUaG[eHg@wGm@qGu@uGy@gGkAkGwAkGsAqFkBgG_BiFgB{FoBoEiBgEqCqF{BeEgDcFsj@}s@}I{K{_@ii@iDqFsCuEwCeFiDoHuFgLqC_HwAyDuBkG_BcGwAgFsBqHkA{EiAaGqAuG_AgFy@iFq@sEQcBu@iFy@mGq@cHoBcXUwF]{GU{GIqGGsPD_b@A}dAE}PQ{XuAu}@eAs|CQqdABi_@rCys@?cEA}GFyEHkDTe\\Fs_CWgQkAsRkBaOmCmMeFsP}EwMsAkCuCmDuC{EyCiEiGmJmJsGyDmCuTuHuIkB_Hm@kHe@iJBcPb@{IAyLT_F~@uEfAcExAqEdB{E|AiEdBmBjAsLzHgM`KcQxQsOhMmJrIsNzDi`@|IuUnE{xEtAozAPyvBhBiA?oEGoFHkc@CmA@gEHiGCeREmEDaF?gE?eF?uE?iF?cE?wE?aF?wE?qE?kF?iE?mF@kE?_E?eF@sECeF?{D@qF@iE?iF@{E@cE@eFEyE@w@EwaCu@sQ|@gN`D_V`JmUvN_D`BgEdCcEbCiDrBiEdCsEjCqDvB{DzBqElCgEdCsDvBcEzB{EpCyD|B}DzBkEfC}D|BkEfCmDpBkDpByDnBqBf@_ADoD?wBc@u@YoBuG_BoGkGaW}@sDw@{C_FeS{VgdAoD{NaZokA_l@eaCkLse@yEwOyD_PeFgTgAqEo`@u_BuDiOqHcZ_AeDmEePkH}[oE{PqF}U}BiJk@gCgu@o{CuZspAiSwy@eMah@}Iw^qE}QaYyiA{C}L}DaP{Qeu@mPop@u^yxAae@qkBa@iBuIe`@kDwK{GoXiJ}_@iSgz@aY_kAcy@ogDu[{qAiRqx@o^awAwByJcBwHiAgFaAmGyAuJaAiMUiCc@aRM{Yo@ij@EgCOwXKuTIeZYu]EeGe@yr@AsC]mr@MqX?{a@?{GAag@@eRI_l@GqGMqOKqNKqLYmXeAigAv@eQ~AyWvDclCnD{tAzCyx@`SnAx^vBB|A^tAv@z@dAXhAK|@q@h@kAL}ArQvAbRdAzJl@pPbA~A`CdAvAd@x@x@pAdAlBzAtCrAdAt@b@t@N|@DbA@|@Ch@Ib@MjUqHf@MlHmCfGwBfRmGrEuAvN_F~D{AvGuBzEzFrL`On@`Aj@dAf@hAfA|CtAtMP~BPbFFxCFpHDxKBdBBlEBdB@fBD|CJdDDt@Bv@Dt@Bv@JlBXzCl@jEh@tC^pAv@hDr@lDj@nDRvAl@vF\\hDxNIf@}@|@wBbAqBhAkBdAyAzAiB~AeBdB{AfBuAjBqAfDqBv@a@lDcBtB{@|Bs@~Bq@`E_AbCe@dDg@xC]jBO","legs":[{"summary":"Aviation Circle - departures, New York Avenue Northeast","weight":450,"duration":500,"steps":[{"driving_side":"right","geometry":"cv|ciAlx~|qCkUm^aCkE_C_F}BoFsBiF_BsFeBgGoAcGgAqFcAaHs@mGc@eG]wGYcHSyGEmPBuHBwE?gIFkGAeGAoHKcHUaG[eHg@wGm@qGu@uGy@gGkAkGwAkGsAqFkBgG_BiFgB{FoBoEiBgEqCqF{BeEgDcFsj@}s@}I{K{_@ii@iDqFsCuEwCeFiDoHuFgLqC_HwAyDuBkG_BcGwAgFsBqHkA{EiAaGqAuG_AgFy@iFq@sEQcBu@iFy@mGq@cHoBcXUwF]{GU{GIqGGsPD_b@A}dAE}PQ{XuAu}@eAs|CQqdABi_@","mode":"driving","maneuver":{"bearing_after":46,"bearing_before":35,"location":[-77.037463,38.878066],"modifier":"slight right","type":"fork","instruction":"Keep right onto I-395 North"},"ref":"I-395 North","weight":191.8,"duration":191.5,"name":"I-395 North","distance":1780.6},{"distance":3055.7,"name":"Center Leg Freeway (I-395 North)","ref":"I-395 North","maneuver":{"bearing_after":95,"bearing_before":90,"location":[-77.018635,38.882355],"modifier":"slight right","type":"fork","instruction":"Keep right towards I-395 North: US Senate"},"destinations":"I-395 North: US Senate","weight":206.60000000000002,"mode":"driving","geometry":"ebediAt_z{qCrCys@?cEA}GFyEHkDTe\\Fs_CWgQkAsRkBaOmCmMeFsP}EwMsAkCuCmDuC{EyCiEiGmJmJsGyDmCuTuHuIkB_Hm@kHe@iJBcPb@{IAyLT_F~@uEfAcExAqEdB{E|AiEdBmBjAsLzHgM`KcQxQsOhMmJrIsNzDi`@|IuUnE{xEtAozAPyvBhBiA?oEGoFHkc@CmA@gEHiGCeREmEDaF?gE?eF?uE?iF?cE?wE?aF?wE?qE?kF?iE?mF@kE?_E?eF@sECeF?{D@qF@iE?iF@{E@cE@eFEyE@w@EwaCu@sQ|@gN`D_V`JmUvN_D`BgEdCcEbCiDrBiEdCsEjCqDvB{DzBqElCgEdCsDvBcEzB{EpCyD|B}DzBkEfC}D|BkEfCmDpBkDpByDnBqBf@_ADoD?wBc@u@Y","duration":195.00000000000003,"driving_side":"right"}],"distance":13651.1}],"weight_name":"routability","weight":450,"duration":500,"distance":13651.1,"voiceLocale":"en-US"}],"waypoints":[{"name":"Aviation Circle - departures","location":[-77.037265,38.878206]},{"name":"Ellipse Road Northeast","location":[-76.966906,38.910736]}],"code":"Ok","uuid":"cjo4pxi1801m76msa4nwo6wzh"} \ No newline at end of file diff --git a/TestHelper/NavigationPlotter.swift b/TestHelper/NavigationPlotter.swift index 01e792575e1..de5e8eeacdb 100644 --- a/TestHelper/NavigationPlotter.swift +++ b/TestHelper/NavigationPlotter.swift @@ -98,13 +98,13 @@ public struct MatchPlotter: Plotter { extension RoutePlotter { public func draw(on plotter: NavigationPlotter) { - plotter.drawLines(between: route.coordinates!, color: color, lineWidth: lineWidth, drawDotIndicator: drawDotIndicator, drawTextIndicator: drawTextIndicator) + plotter.drawLines(between: route.shape!.coordinates, color: color, lineWidth: lineWidth, drawDotIndicator: drawDotIndicator, drawTextIndicator: drawTextIndicator) } } extension MatchPlotter { public func draw(on plotter: NavigationPlotter) { - plotter.drawLines(between: match.coordinates!, color: color, lineWidth: lineWidth, drawDotIndicator: drawDotIndicator, drawTextIndicator: drawTextIndicator) + plotter.drawLines(between: match.shape!.coordinates, color: color, lineWidth: lineWidth, drawDotIndicator: drawDotIndicator, drawTextIndicator: drawTextIndicator) } } @@ -173,11 +173,11 @@ public class NavigationPlotter: UIView { var coordinates = [CLLocationCoordinate2D]() routePlotters?.forEach({ (plotter) in - coordinates += plotter.route.coordinates! + coordinates += plotter.route.shape!.coordinates }) matchPlotters?.forEach({ (plotter) in - coordinates += plotter.match.coordinates! + coordinates += plotter.match.shape!.coordinates }) coordinatePlotters?.forEach({ (plotter) in