From 539ff15f1ce502d782cdaba11785f8c3ee277072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Tue, 24 Dec 2019 17:34:08 -0800 Subject: [PATCH 01/39] Updated MapboxDirections.swift --- Cartfile.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 8fa1138646090f83901ddd7498b679000dda4587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Tue, 24 Dec 2019 17:47:09 -0800 Subject: [PATCH 02/39] Updated CocoaPods installation tests --- .../PodInstall.xcodeproj/project.pbxproj | 8 ++--- .../CocoaPodsTest/PodInstall/Podfile | 1 + .../CocoaPodsTest/PodInstall/Podfile.lock | 33 ++++++++++++------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/project.pbxproj b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/project.pbxproj index 863f97fb7ff..1ea019d6caf 100644 --- a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/project.pbxproj +++ b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/project.pbxproj @@ -231,8 +231,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 +248,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", diff --git a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/Podfile b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/Podfile index c2b58a2426c..553f1bbbe59 100644 --- a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/Podfile +++ b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/Podfile @@ -3,6 +3,7 @@ use_frameworks! target 'PodInstall' do # The branch of MapboxNavigation and MapboxNavigation will be replaced by Bitrise to use the current branch. + pod 'MapboxDirections.swift', :git => '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 From 254e4c9fc1f57fa186c1028c4a56d29dd757288e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Thu, 26 Dec 2019 11:21:04 -0800 Subject: [PATCH 03/39] Upgraded CocoaPods installation test project --- .../PodInstall.xcodeproj/project.pbxproj | 8 ++++--- .../xcschemes/PodInstall.xcscheme | 24 ++++++++----------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/project.pbxproj b/MapboxCoreNavigationTests/CocoaPodsTest/PodInstall/PodInstall.xcodeproj/project.pbxproj index 1ea019d6caf..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, @@ -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 @@ - - - - - - - - Date: Mon, 25 Nov 2019 15:55:10 -0700 Subject: [PATCH 04/39] Fixing integration issues --- Cartfile | 2 +- MapboxCoreNavigation/CLLocation.swift | 6 +-- MapboxCoreNavigation/CoreFeedbackEvent.swift | 2 +- MapboxCoreNavigation/EventDetails.swift | 14 +++---- .../LegacyRouteController.swift | 20 +++++---- MapboxCoreNavigation/NavigationService.swift | 2 +- MapboxCoreNavigation/OfflineDirections.swift | 42 ++++++++++++++----- MapboxCoreNavigation/RouteController.swift | 14 +++++-- MapboxCoreNavigation/RouteLeg.swift | 2 +- MapboxCoreNavigation/RouteOptions.swift | 5 +++ MapboxCoreNavigation/RouteProgress.swift | 26 +++++++----- .../SimulatedLocationManager.swift | 6 +-- 12 files changed, 92 insertions(+), 49 deletions(-) diff --git a/Cartfile b/Cartfile index 1adbd0c686e..8da34ca97f2 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/objc-delenda-est" #~> 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/MapboxCoreNavigation/CLLocation.swift b/MapboxCoreNavigation/CLLocation.swift index 5d6a3f6e8f3..dfd88dfe49c 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 coords = routeStep.shape?.coordinates, let closestCoordinate = Polyline(coords).closestCoordinate(to: coordinate) else { return false } return closestCoordinate.distance < maximumDistance @@ -57,7 +57,7 @@ extension CLLocation { 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 } @@ -70,7 +70,7 @@ extension CLLocation { func coordinates(for routeProgress: RouteProgress) -> [CLLocationCoordinate2D] { let legProgress = routeProgress.currentLegProgress let nearbyCoordinates = routeProgress.nearbyCoordinates - let stepCoordinates = legProgress.currentStep.coordinates! + let stepCoordinates = legProgress.currentStep.shape!.coordinates // 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 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..3bc8ec0d134 100644 --- a/MapboxCoreNavigation/EventDetails.swift +++ b/MapboxCoreNavigation/EventDetails.swift @@ -120,14 +120,14 @@ 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 { + if let geometry = session.originalRoute.shape?.coordinates { originalGeometry = Polyline(coordinates: geometry) originalDistance = round(session.originalRoute.distance) originalEstimatedDuration = round(session.originalRoute.expectedTravelTime) @@ -139,7 +139,7 @@ struct NavigationEventDetails: EventDetails { originalStepCount = nil } - if let geometry = session.currentRoute.coordinates { + if let geometry = session.currentRoute.shape?.coordinates { self.geometry = Polyline(coordinates: geometry) distance = round(session.currentRoute.distance) estimatedDuration = round(session.currentRoute.expectedTravelTime) @@ -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..1754228094a 100644 --- a/MapboxCoreNavigation/LegacyRouteController.swift +++ b/MapboxCoreNavigation/LegacyRouteController.swift @@ -133,7 +133,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa } func updateDistanceToManeuver() { - guard let coordinates = routeProgress.currentLegProgress.currentStep.coordinates, let coordinate = rawLocation?.coordinate else { + guard let coordinates = routeProgress.currentLegProgress.currentStep.shape?.coordinates, let coordinate = rawLocation?.coordinate else { userSnapToStepDistanceFromManeuver = nil return } @@ -175,9 +175,13 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa } public func userIsOnRoute(_ location: CLLocation) -> Bool { + + guard let destination = routeProgress.currentLeg.destination else { + return true + } // 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,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa let step = stepProgress.step //Increment the progress model - let polyline = Polyline(step.coordinates!) + let polyline = Polyline(step.shape!.coordinates) if let closestCoordinate = polyline.closestCoordinate(to: rawLocation.coordinate) { let remainingDistance = polyline.distance(from: closestCoordinate.coordinate) let distanceTraveled = step.distance - remainingDistance @@ -305,9 +309,11 @@ 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, 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 { @@ -408,7 +414,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa routeProgress.currentLegProgress.currentStepProgress.intersectionsIncludingUpcomingManeuverIntersection = intersections 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 = Polyline(currentStepProgress.step.shape!.coordinates).distance(from: location.coordinate, to: upcomingIntersection.location) } if routeProgress.currentLegProgress.currentStepProgress.intersectionDistances == nil { @@ -508,7 +514,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa } func updateIntersectionDistances() { - if let coordinates = routeProgress.currentLegProgress.currentStep.coordinates, let intersections = routeProgress.currentLegProgress.currentStep.intersections { + if let coordinates = routeProgress.currentLegProgress.currentStep.shape?.coordinates, let intersections = routeProgress.currentLegProgress.currentStep.intersections { let polyline = Polyline(coordinates) let distances: [CLLocationDistance] = intersections.map { polyline.distance(from: coordinates.first, to: $0.location) } routeProgress.currentLegProgress.currentStepProgress.intersectionDistances = distances diff --git a/MapboxCoreNavigation/NavigationService.swift b/MapboxCoreNavigation/NavigationService.swift index 23623f6770b..308c92ece21 100644 --- a/MapboxCoreNavigation/NavigationService.swift +++ b/MapboxCoreNavigation/NavigationService.swift @@ -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..d7740afe3d0 100644 --- a/MapboxCoreNavigation/OfflineDirections.swift +++ b/MapboxCoreNavigation/OfflineDirections.swift @@ -7,10 +7,18 @@ import MapboxNavigationNative */ public typealias NavigationDirectionsCompletionHandler = (_ numberOfTiles: UInt64) -> Void -enum OfflineRoutingError: Error, LocalizedError { +public enum OfflineRoutingError: DirectionsError, LocalizedError { + public typealias RawValue = String + + var failureReason: String { + #warning("unimplemented") + return "Unimplemented" + } + case unexpectedRouteResult(String) case corruptRouteData(String) case responseError(String) + case unknown(underlying: Error) public var localizedDescription: String { switch self { @@ -20,12 +28,10 @@ enum OfflineRoutingError: Error, LocalizedError { return value case .responseError(let value): return value + case .unknown(let underlying): + return "Unknown Error: \(underlying.localizedDescription)" } } - - var errorDescription: String? { - return localizedDescription - } } struct NavigationDirectionsConstants { @@ -51,6 +57,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. @@ -134,6 +153,9 @@ public class NavigationDirections: Directions { - 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) { + + let complete = completionHandler as OfflineRouteCompletionHandler + guard offline == true else { super.calculate(options, completionHandler: completionHandler) return @@ -145,13 +167,13 @@ public class NavigationDirections: Directions { 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) + return complete(nil, nil, error) } 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) + return complete(nil, nil, error) } do { @@ -159,17 +181,17 @@ public class NavigationDirections: Directions { if let errorValue = json["error"] as? String { DispatchQueue.main.async { let error = OfflineRoutingError.responseError(errorValue) - return completionHandler(nil, nil, error as NSError) + return complete(nil, nil, error) } } else { DispatchQueue.main.async { let response = options.response(from: json) - return completionHandler(response.0, response.1, nil) + return complete(response.0, response.1, nil) } } } catch { DispatchQueue.main.async { - return completionHandler(nil, nil, error as NSError) + return complete(nil, nil, .unknown(underlying: error)) } } } diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index 38eccadd06e..4fcc6378256 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -258,8 +258,9 @@ 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, 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 +288,7 @@ open class RouteController: NSObject { let step = stepProgress.step //Increment the progress model - let polyline = Polyline(step.coordinates!) + let polyline = Polyline(step.shape!.coordinates) if let closestCoordinate = polyline.closestCoordinate(to: rawLocation.coordinate) { let remainingDistance = polyline.distance(from: closestCoordinate.coordinate) let distanceTraveled = step.distance - remainingDistance @@ -355,9 +356,14 @@ open class RouteController: NSObject { extension RouteController: Router { public func userIsOnRoute(_ location: CLLocation) -> Bool { + + guard let destination = routeProgress.currentLeg.destination else { + return true + } + // 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 } diff --git a/MapboxCoreNavigation/RouteLeg.swift b/MapboxCoreNavigation/RouteLeg.swift index a09c89606ef..5d1340bcd5e 100644 --- a/MapboxCoreNavigation/RouteLeg.swift +++ b/MapboxCoreNavigation/RouteLeg.swift @@ -3,6 +3,6 @@ import Turf extension RouteLeg { var coordinates: [CLLocationCoordinate2D] { - return (steps.first?.coordinates ?? []) + steps.dropFirst().flatMap { ($0.coordinates ?? []).dropFirst() } + return (steps.first?.shape?.coordinates ?? []) + steps.dropFirst().flatMap { ($0.shape?.coordinates ?? []).dropFirst() } } } diff --git a/MapboxCoreNavigation/RouteOptions.swift b/MapboxCoreNavigation/RouteOptions.swift index 6daad5c8401..0a5ccdb18a5 100644 --- a/MapboxCoreNavigation/RouteOptions.swift +++ b/MapboxCoreNavigation/RouteOptions.swift @@ -24,6 +24,11 @@ extension RouteOptions { return copy } +// init(options: RouteOptions) { +// +// } + + /** Returns a tuple containing the waypoints along the leg at the given index and the waypoints that separate subsequent legs. diff --git a/MapboxCoreNavigation/RouteProgress.swift b/MapboxCoreNavigation/RouteProgress.swift index ab2a4a70920..8517f1acbdf 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 } } /** @@ -141,9 +141,9 @@ 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() ?? [] + let priorCoordinates = priorStep?.shape?.coordinates.dropLast() ?? [] + let currentCoordinates = currentLegProgress.currentStep.shape?.coordinates ?? [] + let upcomingCoordinates = upcomingStep?.shape?.coordinates.dropFirst() ?? [] return priorCoordinates + currentCoordinates + upcomingCoordinates } @@ -184,8 +184,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 +211,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 } @@ -421,9 +425,9 @@ open class RouteLegProgress: NSObject { */ @available(*, deprecated, message: "Use RouteProgress.nearbyCoordinates") public var nearbyCoordinates: [CLLocationCoordinate2D] { - let priorCoords = priorStep?.coordinates ?? [] - let upcomingCoords = upcomingStep?.coordinates ?? [] - let currentCoords = currentStep.coordinates ?? [] + let priorCoords = priorStep?.shape?.coordinates ?? [] + let upcomingCoords = upcomingStep?.shape?.coordinates ?? [] + let currentCoords = currentStep.shape?.coordinates ?? [] let nearby = priorCoords + currentCoords + upcomingCoords assert(!nearby.isEmpty, "Step must have coordinates") return nearby @@ -436,7 +440,7 @@ 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 coords = step.shape?.coordinates else { continue } guard let closestCoordOnStep = Polyline(coords).closestCoordinate(to: coordinate) else { continue } let foundIndex = currentStepIndex + stepIndex diff --git a/MapboxCoreNavigation/SimulatedLocationManager.swift b/MapboxCoreNavigation/SimulatedLocationManager.swift index 85234850373..8d021a4da8e 100644 --- a/MapboxCoreNavigation/SimulatedLocationManager.swift +++ b/MapboxCoreNavigation/SimulatedLocationManager.swift @@ -107,7 +107,7 @@ open class SimulatedLocationManager: NavigationLocationManager { } private func reset() { - if let coordinates = route?.coordinates { + if let coordinates = route?.shape?.coordinates { routeLine = coordinates locations = coordinates.simulatedLocationsWithTurnPenalties() } @@ -163,8 +163,8 @@ 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 coordinates = routeProgress?.route.shape?.coordinates, + let closestCoordinateOnRoute = Polyline(coordinates).closestCoordinate(to: newCoordinate), let nextCoordinateOnRoute = coordinates.after(element: coordinates[closestCoordinateOnRoute.index]), let time = expectedSegmentTravelTimes.optional[closestCoordinateOnRoute.index] { let distance = coordinates[closestCoordinateOnRoute.index].distance(to: nextCoordinateOnRoute) From 59d0ef03b672413fb6012bc28c5bd819c6d275ab Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Tue, 26 Nov 2019 18:15:21 -0700 Subject: [PATCH 05/39] Whittling down compiler errors --- .../NavigationRouteOptions.swift | 22 +++++++++---------- MapboxCoreNavigation/RouteOptions.swift | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) 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/RouteOptions.swift b/MapboxCoreNavigation/RouteOptions.swift index 0a5ccdb18a5..842a2b0f47a 100644 --- a/MapboxCoreNavigation/RouteOptions.swift +++ b/MapboxCoreNavigation/RouteOptions.swift @@ -3,7 +3,7 @@ import MapboxDirections extension RouteOptions { internal var activityType: CLActivityType { switch self.profileIdentifier { - case MBDirectionsProfileIdentifier.cycling, MBDirectionsProfileIdentifier.walking: + case DirectionsProfileIdentifier.cycling, DirectionsProfileIdentifier.walking: return .fitness default: return .automotiveNavigation From 8407927f21af7b779bc85d582d881126ad5d6ee8 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Tue, 10 Dec 2019 15:31:59 -0700 Subject: [PATCH 06/39] Removing extraneous `Polyline` initializations since shape is now baked into model --- MapboxCoreNavigation/CLLocation.swift | 2 +- MapboxCoreNavigation/EventDetails.swift | 8 ++++---- .../LegacyRouteController.swift | 20 ++++++++++++------- MapboxCoreNavigation/RouteController.swift | 4 ++-- MapboxCoreNavigation/RouteProgress.swift | 4 ++-- .../SimulatedLocationManager.swift | 20 +++++++++---------- MapboxNavigation/NavigationMapView.swift | 6 +++--- MapboxNavigation/RouteMapViewController.swift | 4 ++-- 8 files changed, 37 insertions(+), 31 deletions(-) diff --git a/MapboxCoreNavigation/CLLocation.swift b/MapboxCoreNavigation/CLLocation.swift index dfd88dfe49c..8800f996b8b 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 coords = routeStep.shape?.coordinates, let closestCoordinate = Polyline(coords).closestCoordinate(to: coordinate) else { + guard let shape = routeStep.shape, let closestCoordinate = shape.closestCoordinate(to: coordinate) else { return false } return closestCoordinate.distance < maximumDistance diff --git a/MapboxCoreNavigation/EventDetails.swift b/MapboxCoreNavigation/EventDetails.swift index 3bc8ec0d134..2f6e9ede45a 100644 --- a/MapboxCoreNavigation/EventDetails.swift +++ b/MapboxCoreNavigation/EventDetails.swift @@ -127,8 +127,8 @@ struct NavigationEventDetails: EventDetails { userAbsoluteDistanceToDestination = nil } - if let geometry = session.originalRoute.shape?.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.shape?.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 { diff --git a/MapboxCoreNavigation/LegacyRouteController.swift b/MapboxCoreNavigation/LegacyRouteController.swift index 1754228094a..48a2457daab 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.shape?.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 { @@ -284,7 +284,10 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa let step = stepProgress.step //Increment the progress model - let polyline = Polyline(step.shape!.coordinates) + guard let polyline = step.shape else { + return + } + if let closestCoordinate = polyline.closestCoordinate(to: rawLocation.coordinate) { let remainingDistance = polyline.distance(from: closestCoordinate.coordinate) let distanceTraveled = step.distance - remainingDistance @@ -413,8 +416,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.shape!.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 { @@ -514,9 +521,8 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa } func updateIntersectionDistances() { - if let coordinates = routeProgress.currentLegProgress.currentStep.shape?.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/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index 4fcc6378256..ce5b71206f0 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -288,8 +288,8 @@ open class RouteController: NSObject { let step = stepProgress.step //Increment the progress model - let polyline = Polyline(step.shape!.coordinates) - if let closestCoordinate = polyline.closestCoordinate(to: rawLocation.coordinate) { + if let polyline = step.shape, + let closestCoordinate = polyline.closestCoordinate(to: rawLocation.coordinate) { let remainingDistance = polyline.distance(from: closestCoordinate.coordinate) let distanceTraveled = step.distance - remainingDistance stepProgress.distanceTraveled = distanceTraveled diff --git a/MapboxCoreNavigation/RouteProgress.swift b/MapboxCoreNavigation/RouteProgress.swift index 8517f1acbdf..19259734f7c 100644 --- a/MapboxCoreNavigation/RouteProgress.swift +++ b/MapboxCoreNavigation/RouteProgress.swift @@ -440,8 +440,8 @@ open class RouteLegProgress: NSObject { let remainingSteps = leg.steps.suffix(from: stepIndex) for (currentStepIndex, step) in remainingSteps.enumerated() { - guard let coords = step.shape?.coordinates else { continue } - guard let closestCoordOnStep = Polyline(coords).closestCoordinate(to: coordinate) else { continue } + guard let coords = step.shape else { continue } + guard let closestCoordOnStep = shape.closestCoordinate(to: coordinate) else { continue } let foundIndex = currentStepIndex + stepIndex // First time around, currentClosest will be `nil`. diff --git a/MapboxCoreNavigation/SimulatedLocationManager.swift b/MapboxCoreNavigation/SimulatedLocationManager.swift index 8d021a4da8e..0368f1179af 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?.shape?.coordinates { - routeLine = coordinates - locations = coordinates.simulatedLocationsWithTurnPenalties() + if let shape = route?.shape { + routeShape = shape + locations = shape.coordinates.simulatedLocationsWithTurnPenalties() } } @@ -145,7 +145,7 @@ open class SimulatedLocationManager: NavigationLocationManager { } internal func tick() { - let polyline = Polyline(routeLine) + let polyline = routeShape guard let newCoordinate = polyline.coordinateFromStart(distance: currentDistance) else { return @@ -163,11 +163,11 @@ open class SimulatedLocationManager: NavigationLocationManager { // Simulate speed based on expected segment travel time if let expectedSegmentTravelTimes = routeProgress?.currentLeg.expectedSegmentTravelTimes, - let coordinates = routeProgress?.route.shape?.coordinates, - let closestCoordinateOnRoute = Polyline(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/MapboxNavigation/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index 89bea213942..712c5b77194 100644 --- a/MapboxNavigation/NavigationMapView.swift +++ b/MapboxNavigation/NavigationMapView.swift @@ -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) } @@ -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/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index d11f55f8d1e..4351f031246 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -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 } From 67b37dc40759669f6ce5fa7ce896aa1ef84b3fc7 Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 13 Dec 2019 15:39:03 -0700 Subject: [PATCH 07/39] Fixing errors, updating for new `shape` property and getting `OfflineDirectionsError` to work --- Cartfile | 2 +- MapboxCoreNavigation/OfflineDirections.swift | 33 ++++++++++--------- MapboxCoreNavigation/Route.swift | 13 ++++++++ MapboxCoreNavigation/RouteController.swift | 12 +++---- MapboxCoreNavigation/RouteOptions.swift | 16 ++++++--- MapboxCoreNavigation/RouteProgress.swift | 4 +-- .../SimulatedLocationManager.swift | 6 ++-- MapboxNavigation.xcodeproj/project.pbxproj | 10 ++++-- .../CarPlayNavigationViewController.swift | 4 +-- MapboxNavigation/NavigationMapView.swift | 22 ++++++------- .../NavigationViewController.swift | 2 +- MapboxNavigation/Route.swift | 8 ++--- MapboxNavigation/RouteMapViewController.swift | 8 ++--- 13 files changed, 83 insertions(+), 57 deletions(-) create mode 100644 MapboxCoreNavigation/Route.swift diff --git a/Cartfile b/Cartfile index 8da34ca97f2..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" "jerrad/objc-delenda-est" #~> 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/MapboxCoreNavigation/OfflineDirections.swift b/MapboxCoreNavigation/OfflineDirections.swift index d7740afe3d0..d98400cdbba 100644 --- a/MapboxCoreNavigation/OfflineDirections.swift +++ b/MapboxCoreNavigation/OfflineDirections.swift @@ -15,6 +15,12 @@ public enum OfflineRoutingError: DirectionsError, LocalizedError { return "Unimplemented" } + /** + No route could be found between the specified locations. + + Make sure it is possible to travel between the locations with the mode of transportation implied by the profileIdentifier option. For example, it is impossible to travel by car from one continent to another without either a land bridge or a ferry connection. + */ + case unableToRoute case unexpectedRouteResult(String) case corruptRouteData(String) case responseError(String) @@ -22,6 +28,8 @@ public enum OfflineRoutingError: DirectionsError, LocalizedError { public var localizedDescription: String { switch self { + case .unableToRoute: + return "Unable to route between speficied waypoints." case .corruptRouteData(let value): return value case .unexpectedRouteResult(let value): @@ -170,29 +178,24 @@ public class NavigationDirections: Directions { return complete(nil, nil, error) } - guard let data = result.json.data(using: .utf8) else { + 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 complete(nil, nil, error) } - - 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 complete(nil, nil, error) - } - } else { - DispatchQueue.main.async { - let response = options.response(from: json) - return complete(response.0, response.1, nil) + DispatchQueue.main.async { + + do { + let response = try JSONDecoder().decode(RouteResponse.self, from: data) + guard let routes = response.routes else { + return complete(response.waypoints, nil, .unableToRoute) } + return complete(response.waypoints, routes, nil) } - } catch { - DispatchQueue.main.async { + catch { return complete(nil, nil, .unknown(underlying: error)) } + } } } diff --git a/MapboxCoreNavigation/Route.swift b/MapboxCoreNavigation/Route.swift new file mode 100644 index 00000000000..3485ce15da0 --- /dev/null +++ b/MapboxCoreNavigation/Route.swift @@ -0,0 +1,13 @@ +import MapboxDirections + +extension Route { + + var json: String? { + let encoder = JSONEncoder() + 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 ce5b71206f0..b35101bf6d2 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. diff --git a/MapboxCoreNavigation/RouteOptions.swift b/MapboxCoreNavigation/RouteOptions.swift index 842a2b0f47a..0ec64b3bac0 100644 --- a/MapboxCoreNavigation/RouteOptions.swift +++ b/MapboxCoreNavigation/RouteOptions.swift @@ -10,6 +10,17 @@ extension RouteOptions { } } + convenience init(options: RouteOptions) { + self.init(waypoints: options.waypoints, profileIdentifier: options.profileIdentifier) + allowsUTurnAtWaypoint = options.allowsUTurnAtWaypoint + roadClassesToAvoid = options.roadClassesToAvoid + alleyPriority = options.alleyPriority + walkwayPriority = options.walkwayPriority + speed = options.speed + includesAlternativeRoutes = options.includesAlternativeRoutes + includesExitRoundaboutManeuver = options.includesExitRoundaboutManeuver + } + /** Returns a copy of RouteOptions without the specified waypoint. @@ -18,15 +29,12 @@ extension RouteOptions { */ public func without(waypoint: Waypoint) -> RouteOptions { let waypointsWithoutSpecified = waypoints.filter { $0 != waypoint } - let copy = self.copy() as! RouteOptions + let copy = RouteOptions(options: self) copy.waypoints = waypointsWithoutSpecified return copy } -// init(options: RouteOptions) { -// -// } /** diff --git a/MapboxCoreNavigation/RouteProgress.swift b/MapboxCoreNavigation/RouteProgress.swift index 19259734f7c..18c682049f4 100644 --- a/MapboxCoreNavigation/RouteProgress.swift +++ b/MapboxCoreNavigation/RouteProgress.swift @@ -259,7 +259,7 @@ open class RouteProgress: NSObject { user.headingAccuracy = RouteProgress.reroutingAccuracy } let newWaypoints = [user] + remainingWaypointsForCalculatingRoute() - let newOptions = oldOptions.copy() as! RouteOptions + let newOptions = RouteOptions(options: oldOptions) newOptions.waypoints = newWaypoints return newOptions @@ -440,7 +440,7 @@ open class RouteLegProgress: NSObject { let remainingSteps = leg.steps.suffix(from: stepIndex) for (currentStepIndex, step) in remainingSteps.enumerated() { - guard let coords = step.shape else { continue } + guard let shape = step.shape else { continue } guard let closestCoordOnStep = shape.closestCoordinate(to: coordinate) else { continue } let foundIndex = currentStepIndex + stepIndex diff --git a/MapboxCoreNavigation/SimulatedLocationManager.swift b/MapboxCoreNavigation/SimulatedLocationManager.swift index 0368f1179af..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 routeShape: Polyline + fileprivate var routeShape: Polyline! /** Specify the multiplier to use when calculating speed based on the RouteLeg’s `expectedSegmentTravelTimes`. @@ -145,9 +145,7 @@ open class SimulatedLocationManager: NavigationLocationManager { } internal func tick() { - let polyline = routeShape - - guard let newCoordinate = polyline.coordinateFromStart(distance: currentDistance) else { + guard let polyline = routeShape, let newCoordinate = polyline.coordinateFromStart(distance: currentDistance) else { return } diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index e5483cf8fe9..690d12540f4 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 */; }; @@ -807,6 +808,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 = ""; }; @@ -1542,6 +1544,7 @@ 352AFFA22139986E00EB3567 /* UIViewController.swift */, 351BEC081E5BCC72006FE110 /* Bundle.swift */, 8D24A2F720409A890098CBF8 /* CGSize.swift */, + DA0557202154EF4700A1F2AA /* Route.swift */, C5F4D21820DC468B0059FABF /* CongestionLevel.swift */, C51511D020EAC89D00372A91 /* CPMapTemplate.swift */, DA443DDD2278C90E00ED1307 /* CPTrip.swift */, @@ -1553,7 +1556,6 @@ 353E3C8E20A3501C00FD1789 /* MGLStyle.swift */, C58159001EA6D02700FC6C3D /* MGLVectorTileSource.swift */, 8D5DFFF0207C04840093765A /* NSAttributedString.swift */, - DA0557202154EF4700A1F2AA /* Route.swift */, 35B7837D1F9547B300291F9A /* Transitioning.swift */, 359D283B1F9DC14F00FDE9C9 /* UICollectionView.swift */, 8D24A2F52040960C0098CBF8 /* UIEdgeInsets.swift */, @@ -1619,6 +1621,7 @@ C561735A1F182113005954F6 /* RouteStep.swift */, 351927351F0FA072003A702D /* ScreenCapture.swift */, 35A5413A1EFC052700E49846 /* RouteOptions.swift */, + 43FB386A23A2024C0064481E /* Route.swift */, C57491DE1FACC42F006F97BC /* CGPoint.swift */, C582FD5E203626E900A9086E /* CLLocationDirection.swift */, 35C98732212E037900808B82 /* MBNavigator.swift */, @@ -2398,7 +2401,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 */, @@ -2446,6 +2448,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 +2632,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/CarPlayNavigationViewController.swift b/MapboxNavigation/CarPlayNavigationViewController.swift index 2ffd4415e58..b7c0e1c6c89 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) @@ -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/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index 712c5b77194..cea0a88a5c7 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)) @@ -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 @@ -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)) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 7fabe643e49..2a6d2981e00 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -605,7 +605,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/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 4351f031246..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)) } From f6dd0c5a0e733adadfae0313ddb33cfa19bad51e Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Mon, 16 Dec 2019 16:53:37 -0700 Subject: [PATCH 08/39] Working on getting InstructionPresenter updated with new model. WIP. --- MapboxNavigation/InstructionPresenter.swift | 73 ++++++++----------- MapboxNavigation/LaneView.swift | 8 +- MapboxNavigation/StepsViewController.swift | 4 +- .../VisualInstructionComponent.swift | 25 +++---- 4 files changed, 50 insertions(+), 60 deletions(-) diff --git a/MapboxNavigation/InstructionPresenter.swift b/MapboxNavigation/InstructionPresenter.swift index 57f4ba439c5..2adbe15fcd9 100644 --- a/MapboxNavigation/InstructionPresenter.swift +++ b/MapboxNavigation/InstructionPresenter.swift @@ -43,7 +43,7 @@ class InstructionPresenter { guard !stringFits else { return attributedPairs.attributedStrings } - let indexedComponents: [IndexedVisualInstructionComponent] = attributedPairs.components.enumerated().map { IndexedVisualInstructionComponent(component: $1, index: $0) } + let indexedComponents: [IndexedVisualInstruction.Component] = 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 { @@ -63,12 +63,12 @@ class InstructionPresenter { return attributedPairs.attributedStrings } - typealias AttributedInstructionComponents = (components: [VisualInstructionComponent], attributedStrings: [NSAttributedString]) + typealias AttributedInstructionComponents = (components: [VisualInstruction.Component], attributedStrings: [NSAttributedString]) func attributedPairs(for instruction: VisualInstruction, dataSource: DataSource, imageRepository: ImageRepository, onImageDownload: @escaping ImageDownloadCompletion) -> AttributedInstructionComponents { - let components = instruction.components.compactMap { $0 as? VisualInstructionComponent } + let components = instruction.components var strings: [NSAttributedString] = [] - var processedComponents: [VisualInstructionComponent] = [] + var processedComponents: [VisualInstruction.Component] = [] for (index, component) in components.enumerated() { let isFirst = index == 0 @@ -76,27 +76,29 @@ class InstructionPresenter { 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 + let build: (_: VisualInstruction.Component, _: [NSAttributedString]) -> Void = { (component, attributedStrings) in processedComponents.append(component) strings.append(attributedStrings.reduce(initial, +)) } - let isShield: (_: VisualInstructionComponent?) -> Bool = { (component) in + let isShield: (_ key: VisualInstruction.Component?) -> Bool = { (component) in guard let key = component?.cacheKey else { return false } return imageRepository.cachedImageForKey(key) != nil } let componentBefore = components.component(before: component) let componentAfter = components.component(after: component) - switch component.type { + switch component { //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 } + case let .exitCode(representation) where 0...1 ~= index: + guard let exitString = self.attributedString(forExitRepresentation: representation, maneuverDirection: instruction.maneuverDirection!, dataSource: dataSource, cacheKey: component.cacheKey!) else { fallthrough } build(component, [exitString]) //if it's a delimiter, skip it if it's between two shields. @@ -104,19 +106,19 @@ class InstructionPresenter { 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) { + case let .image(imageRepresentation, textRepresentation): + if let shieldString = attributedString(forShieldComponent: imageRepresentation, repository: imageRepository, dataSource: dataSource, cacheKey: component.cacheKey!, onImageDownload: onImageDownload) { build(component, [joinString, shieldString]) - } else if let genericShieldString = attributedString(forGenericShield: component, dataSource: dataSource) { + } else if let genericShieldString = attributedString(forGenericShield: textRepresentation, dataSource: dataSource, cacheKey: component.cacheKey!) { build(component, [joinString, genericShieldString]) } else { fallthrough } - //Otherwise, process as text component. - default: - guard let componentString = attributedString(forTextComponent: component, dataSource: dataSource) else { continue } + case let .text(textRepresentation), let .delimiter(textRepresentation): + let componentString = NSAttributedString(string: textRepresentation.text, attributes: attributes(for: dataSource)) build(component, [joinString, componentString]) + } } @@ -124,44 +126,37 @@ class InstructionPresenter { 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 } + func attributedString(forExitRepresentation representation: VisualInstruction.Component.TextRepresentation, maneuverDirection: ManeuverDirection, dataSource: DataSource, cacheKey: String) -> NSAttributedString? { + let exitCode = representation.text let side: ExitSide = maneuverDirection == .left ? .left : .right - guard let exitString = exitShield(side: side, text: exitCode, component: component, dataSource: dataSource) else { return nil } + guard let exitString = exitShield(side: side, text: exitCode, dataSource: dataSource, cacheKey: cacheKey) else { return nil } return exitString } - 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 attributedString(forGenericShield representation: VisualInstruction.Component.TextRepresentation, dataSource: DataSource, cacheKey: String) -> NSAttributedString? { + let text = representation.text + return genericShield(text: text, dataSource: dataSource, cacheKey: cacheKey) } - 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 - } + 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 } + - repository.imageWithURL(imageURL, cacheKey: shieldKey, completion: completion ) + repository.imageWithURL(imageURL, cacheKey: cacheKey, completion: completion ) } private func attributes(for dataSource: InstructionPresenterDataSource) -> [NSAttributedString.Key: Any] { @@ -175,9 +170,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 +190,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() diff --git a/MapboxNavigation/LaneView.swift b/MapboxNavigation/LaneView.swift index e439a62ebf2..4139e94ad44 100644 --- a/MapboxNavigation/LaneView.swift +++ b/MapboxNavigation/LaneView.swift @@ -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 + lane = Lane(indications: indications) + maneuverDirection = ManeuverDirection(rawValue: indications.description) + isValid = isUsable } override init(frame: CGRect) { 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/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") - } } From e86a47c709562bb9918347370076365e43b19a7d Mon Sep 17 00:00:00 2001 From: Jerrad Thramer Date: Fri, 20 Dec 2019 14:23:48 -0700 Subject: [PATCH 09/39] WIP: Continuing to hack away at InstructionsPresenter. Slowly making progress. --- MapboxNavigation/InstructionPresenter.swift | 44 ++++++++++++------- MapboxNavigation/InstructionsBannerView.swift | 3 +- MapboxNavigation/ManeuverView.swift | 2 +- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/MapboxNavigation/InstructionPresenter.swift b/MapboxNavigation/InstructionPresenter.swift index 2adbe15fcd9..09df49c67aa 100644 --- a/MapboxNavigation/InstructionPresenter.swift +++ b/MapboxNavigation/InstructionPresenter.swift @@ -43,16 +43,27 @@ class InstructionPresenter { guard !stringFits else { return attributedPairs.attributedStrings } - let indexedComponents: [IndexedVisualInstruction.Component] = 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 + typealias IndexedTextRepresentation = (Array.Index, VisualInstruction.Component.TextRepresentation) + let textRepresentations: [IndexedTextRepresentation] = attributedPairs.components.enumerated().compactMap { (idx, elem) in + if case let VisualInstruction.Component.text(representation) = elem { + return (idx, representation) + } + return nil + } + + let sorted = textRepresentations.sorted { first, second in + let firstPriority = first.1.abbreviationPriority ?? Int.max + let secondPriority = second.1.abbreviationPriority ?? Int.max + + return firstPriority < secondPriority + } + + for (index, representation) in sorted { + let isFirst = index == 0 let joinChar = isFirst ? "" : " " - guard component.component.type == .text else { continue } - guard let abbreviation = component.component.abbreviation else { continue } + guard let abbreviation = representation.abbreviation else { continue } - attributedPairs.attributedStrings[component.index] = NSAttributedString(string: joinChar + abbreviation, attributes: attributes(for: source)) + attributedPairs.attributedStrings[index] = NSAttributedString(string: joinChar + abbreviation, attributes: attributes(for: source)) let newWidth: CGFloat = attributedPairs.attributedStrings.map { $0.size() }.reduce(.zero, +).width if newWidth <= availableBounds.width { @@ -87,6 +98,7 @@ class InstructionPresenter { guard let key = component?.cacheKey else { return false } return imageRepository.cachedImageForKey(key) != nil } + let componentBefore = components.component(before: component) let componentAfter = components.component(after: component) @@ -119,7 +131,8 @@ class InstructionPresenter { let componentString = NSAttributedString(string: textRepresentation.text, attributes: attributes(for: dataSource)) build(component, [joinString, componentString]) - } + default: + continue } assert(processedComponents.count == strings.count, "The number of processed components must match the number of attributed strings") @@ -294,13 +307,12 @@ extension CGSize { } } -fileprivate struct IndexedVisualInstructionComponent { - let component: Array.Element - let index: Array.Index -} +typealias IndexedVisualInstructionComponent = (Array.Element, + Array.Index) + -extension Array where Element == VisualInstructionComponent { - fileprivate func component(before component: VisualInstructionComponent) -> VisualInstructionComponent? { +extension Array where Element == VisualInstruction.Component { + fileprivate func component(before component: VisualInstruction.Component) -> VisualInstruction.Component? { guard let index = self.firstIndex(of: component) else { return nil } @@ -310,7 +322,7 @@ extension Array where Element == VisualInstructionComponent { return nil } - fileprivate func component(after component: VisualInstructionComponent) -> VisualInstructionComponent? { + fileprivate func component(after component: VisualInstruction.Component) -> VisualInstruction.Component? { guard let index = self.firstIndex(of: component) else { return nil } diff --git a/MapboxNavigation/InstructionsBannerView.swift b/MapboxNavigation/InstructionsBannerView.swift index a3cbb74f3da..4c53dd13f2b 100644 --- a/MapboxNavigation/InstructionsBannerView.swift +++ b/MapboxNavigation/InstructionsBannerView.swift @@ -191,7 +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 representation = VisualInstruction.Component.TextRepresentation(text: "Primary text label", abbreviation: nil, abbreviationPriority: nil) + let component = VisualInstruction.Component.text(text: representation) let instruction = VisualInstruction(text: nil, maneuverType: .none, maneuverDirection: .none, components: [component]) primaryLabel.instruction = instruction diff --git a/MapboxNavigation/ManeuverView.swift b/MapboxNavigation/ManeuverView.swift index 841383b05cb..5a6abbd3cdb 100644 --- a/MapboxNavigation/ManeuverView.swift +++ b/MapboxNavigation/ManeuverView.swift @@ -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: From f0c5a4903533737b177acb9b5ca96a56492c3496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 20 Dec 2019 23:07:17 -0800 Subject: [PATCH 10/39] Fixed syntax error --- MapboxNavigation/InstructionPresenter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MapboxNavigation/InstructionPresenter.swift b/MapboxNavigation/InstructionPresenter.swift index 09df49c67aa..8d56d206836 100644 --- a/MapboxNavigation/InstructionPresenter.swift +++ b/MapboxNavigation/InstructionPresenter.swift @@ -132,7 +132,8 @@ class InstructionPresenter { build(component, [joinString, componentString]) default: - continue + continue + } } assert(processedComponents.count == strings.count, "The number of processed components must match the number of attributed strings") From 226ac5596cfa47529a32c15fab5cbeb4d5f1e050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 20 Dec 2019 23:06:54 -0800 Subject: [PATCH 11/39] Fixed error handling --- Example/AppDelegate+CarPlay.swift | 4 +- Example/ViewController.swift | 10 ++-- MapboxCoreNavigation/OfflineDirections.swift | 48 ++++++++++++------- MapboxNavigation/CarPlayManager.swift | 2 +- MapboxNavigation/CarPlayManagerDelegate.swift | 4 +- .../CarPlayManagerTests.swift | 2 +- 6 files changed, 41 insertions(+), 29 deletions(-) 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..55a91d2bc80 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 diff --git a/MapboxCoreNavigation/OfflineDirections.swift b/MapboxCoreNavigation/OfflineDirections.swift index d98400cdbba..69717441bf9 100644 --- a/MapboxCoreNavigation/OfflineDirections.swift +++ b/MapboxCoreNavigation/OfflineDirections.swift @@ -7,14 +7,10 @@ import MapboxNavigationNative */ public typealias NavigationDirectionsCompletionHandler = (_ numberOfTiles: UInt64) -> Void -public enum OfflineRoutingError: DirectionsError, LocalizedError { +public enum OfflineRoutingError: LocalizedError { public typealias RawValue = String - var failureReason: String { - #warning("unimplemented") - return "Unimplemented" - } - + case onlineError(DirectionsError) /** No route could be found between the specified locations. @@ -28,8 +24,10 @@ public enum OfflineRoutingError: DirectionsError, LocalizedError { public var localizedDescription: String { switch self { + case .onlineError(let error): + return error.localizedDescription case .unableToRoute: - return "Unable to route between speficied waypoints." + return "Unable to route between specified waypoints." case .corruptRouteData(let value): return value case .unexpectedRouteResult(let value): @@ -40,6 +38,16 @@ public enum OfflineRoutingError: DirectionsError, LocalizedError { return "Unknown Error: \(underlying.localizedDescription)" } } + + var failureReason: String { + #warning("unimplemented") + return "Unimplemented" + } + + public var recoverySuggestion: String? { + #warning("unimplemented") + return nil + } } struct NavigationDirectionsConstants { @@ -160,12 +168,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) { - - let complete = completionHandler as OfflineRouteCompletionHandler + public func calculate(_ options: RouteOptions, offline: Bool = true, completionHandler: @escaping OfflineRouteCompletionHandler) { - guard offline == true else { - super.calculate(options, completionHandler: completionHandler) + guard offline else { + super.calculate(options) { (waypoints, routes, error) in + let offlineError: OfflineRoutingError? + if let error = error { + offlineError = .onlineError(error) + } else { + offlineError = nil + } + completionHandler(waypoints, routes, offlineError) + } return } @@ -175,25 +189,25 @@ public class NavigationDirections: Directions { 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 complete(nil, nil, error) + return completionHandler(nil, nil, error) } 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 complete(nil, nil, error) + return completionHandler(nil, nil, error) } DispatchQueue.main.async { do { let response = try JSONDecoder().decode(RouteResponse.self, from: data) guard let routes = response.routes else { - return complete(response.waypoints, nil, .unableToRoute) + return completionHandler(response.waypoints, nil, .unableToRoute) } - return complete(response.waypoints, routes, nil) + return completionHandler(response.waypoints, routes, nil) } catch { - return complete(nil, nil, .unknown(underlying: error)) + return completionHandler(nil, nil, .unknown(underlying: error)) } } 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/MapboxNavigationTests/CarPlayManagerTests.swift b/MapboxNavigationTests/CarPlayManagerTests.swift index ca4e2563ad1..f03bcf3c0cf 100644 --- a/MapboxNavigationTests/CarPlayManagerTests.swift +++ b/MapboxNavigationTests/CarPlayManagerTests.swift @@ -450,7 +450,7 @@ class CarPlayManagerFailureDelegateSpy: CarPlayManagerDelegate { private(set) var recievedError: NSError? @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 } From 6864b43fac94d99e9b2b9fd9c80696a44fca4914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 20 Dec 2019 23:08:32 -0800 Subject: [PATCH 12/39] Fixed route geometry errors --- Example/ViewController.swift | 8 ++++---- MapboxNavigation/NavigationMapView.swift | 2 +- MapboxNavigation/NavigationViewController.swift | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Example/ViewController.swift b/Example/ViewController.swift index 55a91d2bc80..70d3b12505f 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -265,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 @@ -361,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) } @@ -382,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/MapboxNavigation/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index cea0a88a5c7..158e3c9057b 100644 --- a/MapboxNavigation/NavigationMapView.swift +++ b/MapboxNavigation/NavigationMapView.swift @@ -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? diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 2a6d2981e00..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 From d1fec947aa6701d6223ac26010c10bef0411c275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 20 Dec 2019 23:09:48 -0800 Subject: [PATCH 13/39] Fixed lane-related errors --- .../CarPlayNavigationViewController.swift | 2 +- MapboxNavigation/InstructionsBannerView.swift | 5 ++- MapboxNavigation/LaneView.swift | 32 +++++++++---------- MapboxNavigation/LanesView.swift | 16 ++++++---- MapboxNavigation/NextBannerView.swift | 6 ++-- MapboxNavigation/VisualInstruction.swift | 10 ++++-- 6 files changed, 38 insertions(+), 33 deletions(-) diff --git a/MapboxNavigation/CarPlayNavigationViewController.swift b/MapboxNavigation/CarPlayNavigationViewController.swift index b7c0e1c6c89..dc49d3a0255 100644 --- a/MapboxNavigation/CarPlayNavigationViewController.swift +++ b/MapboxNavigation/CarPlayNavigationViewController.swift @@ -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) diff --git a/MapboxNavigation/InstructionsBannerView.swift b/MapboxNavigation/InstructionsBannerView.swift index 4c53dd13f2b..c4357e3a05a 100644 --- a/MapboxNavigation/InstructionsBannerView.swift +++ b/MapboxNavigation/InstructionsBannerView.swift @@ -191,9 +191,8 @@ open class BaseInstructionsBannerView: UIControl { override open func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() maneuverView.isStart = true - let representation = VisualInstruction.Component.TextRepresentation(text: "Primary text label", abbreviation: nil, abbreviationPriority: nil) - let component = VisualInstruction.Component.text(text: representation) - 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 4139e94ad44..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() } @@ -54,7 +54,7 @@ open class LaneView: UIView { convenience init(indications: LaneIndication, isUsable: Bool) { self.init(frame: LaneView.defaultFrame) backgroundColor = .clear - lane = Lane(indications: indications) + self.indications = indications maneuverDirection = ManeuverDirection(rawValue: indications.description) isValid = isUsable } @@ -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/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/VisualInstruction.swift b/MapboxNavigation/VisualInstruction.swift index 6f337bf6e5b..45d7979d975 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? { From 52ddc9cfe2d29241af719547d1b80d8191284e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Fri, 20 Dec 2019 23:35:16 -0800 Subject: [PATCH 14/39] Fixed TestHelper compiler errors --- TestHelper/DirectionsSpy.swift | 2 +- TestHelper/Fixture.swift | 75 +++++++++++++----------------- TestHelper/NavigationPlotter.swift | 8 ++-- 3 files changed, 38 insertions(+), 47 deletions(-) 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..cd1c64f0c3e 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) { @@ -71,13 +64,19 @@ public class Fixture: NSObject { } 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)) + let responseData = JSONFromFileNamed(name: jsonFile) + let response: RouteResponse! + do { + response = try JSONDecoder().decode(RouteResponse.self, from: responseData) + } catch { + preconditionFailure("Unable to decode JSON fixture: \(error)") + } + 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) @@ -86,40 +85,32 @@ public class Fixture: NSObject { } 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)) + let responseData = JSONFromFileNamed(name: jsonFile) + let response: RouteResponse! + do { + response = try JSONDecoder().decode(RouteResponse.self, from: responseData) + } catch { + preconditionFailure("Unable to decode JSON fixture: \(error)") + } + 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 responseData = JSONFromFileNamed(name: filePath) + let response: MapMatchingResponse! + do { + response = try JSONDecoder().decode(MapMatchingResponse.self, from: responseData) + } catch { + preconditionFailure("Unable to decode JSON fixture: \(error)") } - let opts = RouteOptions(coordinates: coordinates, profileIdentifier: .automobile) - - return (json["matchings"] as? [[String: Any]])?.map { - Route(json: $0, waypoints: waypoints, options: opts) + 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/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 From b17adaff34e02a93518e591ae8779969bcc11345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 21 Dec 2019 00:29:45 -0800 Subject: [PATCH 15/39] Fixed MapboxNavigationTests compiler errors --- .../CarPlayManagerTests.swift | 18 +-- .../InstructionPresenterTests.swift | 4 +- ...structionsBannerViewIntegrationTests.swift | 46 ++++---- .../InstructionsBannerViewSnapshotTests.swift | 110 +++++++++--------- MapboxNavigationTests/LaneTests.swift | 3 +- MapboxNavigationTests/LeaksSpec.swift | 6 +- MapboxNavigationTests/ManeuverViewTests.swift | 6 +- .../NavigationMapViewTests.swift | 7 +- .../NavigationViewControllerTests.swift | 4 +- MapboxNavigationTests/RouteTests.swift | 19 +-- .../StepsViewControllerTests.swift | 8 +- TestHelper/Fixture.swift | 26 +++-- 12 files changed, 121 insertions(+), 136 deletions(-) diff --git a/MapboxNavigationTests/CarPlayManagerTests.swift b/MapboxNavigationTests/CarPlayManagerTests.swift index f03bcf3c0cf..a233cf441ba 100644 --- a/MapboxNavigationTests/CarPlayManagerTests.swift +++ b/MapboxNavigationTests/CarPlayManagerTests.swift @@ -216,7 +216,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 @@ -447,7 +447,7 @@ 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: DirectionsError) -> CPNavigationAlert? { @@ -483,17 +483,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) + 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/InstructionPresenterTests.swift b/MapboxNavigationTests/InstructionPresenterTests.swift index 59828c42b79..9d782d54c3a 100644 --- a/MapboxNavigationTests/InstructionPresenterTests.swift +++ b/MapboxNavigationTests/InstructionPresenterTests.swift @@ -6,8 +6,8 @@ import TestHelper 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))) diff --git a/MapboxNavigationTests/InstructionsBannerViewIntegrationTests.swift b/MapboxNavigationTests/InstructionsBannerViewIntegrationTests.swift index de2ddbcc28c..7710f38e048 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.url), alternativeText: .init(text: "US 101", abbreviation: nil, abbreviationPriority: 0)), + .delimiter(text: .init(text: "/", abbreviation: nil, abbreviationPriority: 0)), + .image(image: .init(imageBaseURL: ShieldImage.i280.url), 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.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)) + let instruction2 = VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.us101.url), 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.imageBaseURL { + 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..a12cdac9dde 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.url), alternativeText: .init(text: "I-280", abbreviation: nil, abbreviationPriority: 0)) + let us101Instruction = VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.us101.url), 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.url), 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.url), 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.url), 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.url), 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.url), 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.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: nil)), + .delimiter(text: .init(text: "/", abbreviation: nil, abbreviationPriority: nil)), + .image(image: .init(imageBaseURL: ShieldImage.us101.url), 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.url), 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.url), 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/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/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/NavigationMapViewTests.swift b/MapboxNavigationTests/NavigationMapViewTests.swift index 4a259926bc3..1553858e345 100644 --- a/MapboxNavigationTests/NavigationMapViewTests.swift +++ b/MapboxNavigationTests/NavigationMapViewTests.swift @@ -5,15 +5,12 @@ 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") 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..ed884c70026 100644 --- a/MapboxNavigationTests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/NavigationViewControllerTests.swift @@ -6,7 +6,7 @@ import Turf @testable import MapboxNavigation let jsonFileName = "routeWithInstructions" -let response = Fixture.JSONFromFileNamed(name: jsonFileName) +let response = Fixture.routeResponse(from: jsonFileName) let otherResponse = Fixture.JSONFromFileNamed(name: "route-for-lane-testing") class NavigationViewControllerTests: XCTestCase { @@ -36,7 +36,7 @@ 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) diff --git a/MapboxNavigationTests/RouteTests.swift b/MapboxNavigationTests/RouteTests.swift index d5f9bbc2bc1..3eb556edd07 100644 --- a/MapboxNavigationTests/RouteTests.swift +++ b/MapboxNavigationTests/RouteTests.swift @@ -8,19 +8,8 @@ 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 response = Fixture.mapMatchingResponse(from: "route-doubling-back") + let routes = response.routes let route = routes!.first! let leg = route.legs.first! @@ -28,9 +17,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/StepsViewControllerTests.swift b/MapboxNavigationTests/StepsViewControllerTests.swift index a48cb835bc5..b441635c130 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" } @@ -22,16 +22,14 @@ class StepsViewControllerTests: XCTestCase { let firstCoord = routeController.routeProgress.nearbyCoordinates.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/Fixture.swift b/TestHelper/Fixture.swift index cd1c64f0c3e..2cbfa308edf 100644 --- a/TestHelper/Fixture.swift +++ b/TestHelper/Fixture.swift @@ -63,14 +63,26 @@ public class Fixture: NSObject { return locations.map { CLLocation($0) } } - public class func route(from jsonFile: String) -> Route { + public class func routeResponse(from jsonFile: String) -> RouteResponse { + let responseData = JSONFromFileNamed(name: jsonFile) + do { + return try JSONDecoder().decode(RouteResponse.self, from: responseData) + } catch { + preconditionFailure("Unable to decode JSON fixture: \(error)") + } + } + + public class func mapMatchingResponse(from jsonFile: String) -> MapMatchingResponse { let responseData = JSONFromFileNamed(name: jsonFile) - let response: RouteResponse! do { - response = try JSONDecoder().decode(RouteResponse.self, from: responseData) + return try JSONDecoder().decode(MapMatchingResponse.self, from: responseData) } catch { preconditionFailure("Unable to decode JSON fixture: \(error)") } + } + + public class func route(from jsonFile: String) -> Route { + let response = routeResponse(from: jsonFile) guard let route = response.routes?.first else { preconditionFailure("No routes") } @@ -85,13 +97,7 @@ public class Fixture: NSObject { } public class func waypoints(from jsonFile: String) -> [Waypoint] { - let responseData = JSONFromFileNamed(name: jsonFile) - let response: RouteResponse! - do { - response = try JSONDecoder().decode(RouteResponse.self, from: responseData) - } catch { - preconditionFailure("Unable to decode JSON fixture: \(error)") - } + let response = routeResponse(from: jsonFile) guard let waypoints = response.waypoints else { preconditionFailure("No waypoints") } From 58a7db30513397e18a2548d2689fce7e19cb002b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 21 Dec 2019 11:56:46 -0800 Subject: [PATCH 16/39] Streamlined polyline conversions; fixed MapboxCoreNavigationTests compiler errors --- CHANGELOG.md | 2 ++ MapboxCoreNavigation/CLLocation.swift | 35 +++++++++---------- .../LegacyRouteController.swift | 4 +-- MapboxCoreNavigation/RouteProgress.swift | 22 ++++-------- MapboxCoreNavigationTests/LocationTests.swift | 4 +-- .../MapboxCoreNavigationTests.swift | 17 ++++----- .../MapboxNavigationServiceSpec.swift | 7 +--- .../NavigationEventsManagerTests.swift | 2 +- .../NavigationServiceTests.swift | 28 +++++++-------- .../OfflineRoutingTests.swift | 2 +- .../RouteProgressTests.swift | 30 +++------------- .../TunnelAuthorityTests.swift | 2 +- 12 files changed, 59 insertions(+), 96 deletions(-) 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/MapboxCoreNavigation/CLLocation.swift b/MapboxCoreNavigation/CLLocation.swift index 8800f996b8b..49996c727f2 100644 --- a/MapboxCoreNavigation/CLLocation.swift +++ b/MapboxCoreNavigation/CLLocation.swift @@ -50,10 +50,10 @@ 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 @@ -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.shape!.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/LegacyRouteController.swift b/MapboxCoreNavigation/LegacyRouteController.swift index 48a2457daab..47188a51562 100644 --- a/MapboxCoreNavigation/LegacyRouteController.swift +++ b/MapboxCoreNavigation/LegacyRouteController.swift @@ -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)) diff --git a/MapboxCoreNavigation/RouteProgress.swift b/MapboxCoreNavigation/RouteProgress.swift index 18c682049f4..97478286805 100644 --- a/MapboxCoreNavigation/RouteProgress.swift +++ b/MapboxCoreNavigation/RouteProgress.swift @@ -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] { + public var nearbyShape: LineString { let priorCoordinates = priorStep?.shape?.coordinates.dropLast() ?? [] - let currentCoordinates = currentLegProgress.currentStep.shape?.coordinates ?? [] + let currentShape = currentLegProgress.currentStep.shape let upcomingCoordinates = upcomingStep?.shape?.coordinates.dropFirst() ?? [] - return priorCoordinates + currentCoordinates + upcomingCoordinates + if let currentShape = currentShape, priorCoordinates.isEmpty && upcomingCoordinates.isEmpty { + return currentShape + } + return LineString(priorCoordinates + (currentShape?.coordinates ?? []) + upcomingCoordinates) } /** @@ -420,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?.shape?.coordinates ?? [] - let upcomingCoords = upcomingStep?.shape?.coordinates ?? [] - let currentCoords = currentStep.shape?.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? { 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..7b1011cd221 100644 --- a/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift +++ b/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift @@ -5,7 +5,7 @@ import TestHelper @testable import MapboxCoreNavigation let jsonFileName = "routeWithInstructions" -let response = Fixture.JSONFromFileNamed(name: jsonFileName) +let response = Fixture.routeResponse(from: jsonFileName) let directions = DirectionsSpy(accessToken: "pk.feedCafeDeadBeefBadeBede") let route: Route = { return Fixture.route(from: jsonFileName) @@ -21,7 +21,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 +51,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 +79,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 +105,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 +130,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 +316,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 +329,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..f4a4c9f9039 100644 --- a/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift +++ b/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift @@ -15,7 +15,7 @@ class NavigationEventsManagerTests: XCTestCase { 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 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..2b20f393d66 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) @@ -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]) @@ -180,9 +180,9 @@ class NavigationServiceTests: XCTestCase { 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 +195,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. diff --git a/MapboxCoreNavigationTests/OfflineRoutingTests.swift b/MapboxCoreNavigationTests/OfflineRoutingTests.swift index 38f3cea69c3..fce3f196838 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() { diff --git a/MapboxCoreNavigationTests/RouteProgressTests.swift b/MapboxCoreNavigationTests/RouteProgressTests.swift index 82bcda4024d..ae017668c2d 100644 --- a/MapboxCoreNavigationTests/RouteProgressTests.swift +++ b/MapboxCoreNavigationTests/RouteProgressTests.swift @@ -92,31 +92,11 @@ 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), + ] + 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..40f9413a6ba 100644 --- a/MapboxCoreNavigationTests/TunnelAuthorityTests.swift +++ b/MapboxCoreNavigationTests/TunnelAuthorityTests.swift @@ -15,7 +15,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 From 52e06398fe03f3a6228c0d6f4b5ab006dfa92587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 21 Dec 2019 12:01:20 -0800 Subject: [PATCH 17/39] Require legs to have destinations --- MapboxCoreNavigation/LegacyRouteController.swift | 7 +++++-- MapboxCoreNavigation/RouteController.swift | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/MapboxCoreNavigation/LegacyRouteController.swift b/MapboxCoreNavigation/LegacyRouteController.swift index 47188a51562..d1f5b4f9734 100644 --- a/MapboxCoreNavigation/LegacyRouteController.swift +++ b/MapboxCoreNavigation/LegacyRouteController.swift @@ -177,7 +177,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa public func userIsOnRoute(_ location: CLLocation) -> Bool { guard let destination = routeProgress.currentLeg.destination else { - return true + 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 && @@ -314,7 +314,10 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa func updateRouteLegProgress(for location: CLLocation) { let legProgress = routeProgress.currentLegProgress - guard let currentDestination = legProgress.leg.destination, let remainingVoiceInstructions = legProgress.currentStepProgress.remainingSpokenInstructions else { + guard let currentDestination = legProgress.leg.destination else { + preconditionFailure("Route legs used for navigation must have destinations") + } + guard let remainingVoiceInstructions = legProgress.currentStepProgress.remainingSpokenInstructions else { return } diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index b35101bf6d2..d3ff3181f45 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -259,7 +259,10 @@ open class RouteController: NSObject { func updateRouteLegProgress(status: MBNavigationStatus) { let legProgress = routeProgress.currentLegProgress - guard let currentDestination = legProgress.leg.destination, 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 @@ -358,7 +361,7 @@ extension RouteController: Router { public func userIsOnRoute(_ location: CLLocation) -> Bool { guard let destination = routeProgress.currentLeg.destination else { - return true + preconditionFailure("Route legs used for navigation must have destinations") } // If the user has arrived, do not continue monitor reroutes, step progress, etc From b1bee1baeede411192442e95e6616faa76318558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 21 Dec 2019 12:03:11 -0800 Subject: [PATCH 18/39] Require steps to have shapes --- MapboxCoreNavigation/LegacyRouteController.swift | 2 +- MapboxCoreNavigation/RouteController.swift | 6 ++++-- MapboxCoreNavigation/RouteOptions.swift | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/MapboxCoreNavigation/LegacyRouteController.swift b/MapboxCoreNavigation/LegacyRouteController.swift index d1f5b4f9734..cde663ab0ef 100644 --- a/MapboxCoreNavigation/LegacyRouteController.swift +++ b/MapboxCoreNavigation/LegacyRouteController.swift @@ -285,7 +285,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa //Increment the progress model guard let polyline = step.shape else { - return + preconditionFailure("Route steps used for navigation must have shape data") } if let closestCoordinate = polyline.closestCoordinate(to: rawLocation.coordinate) { diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index d3ff3181f45..565a120a99f 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -291,8 +291,10 @@ open class RouteController: NSObject { let step = stepProgress.step //Increment the progress model - if let polyline = step.shape, - let closestCoordinate = polyline.closestCoordinate(to: rawLocation.coordinate) { + 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 stepProgress.distanceTraveled = distanceTraveled diff --git a/MapboxCoreNavigation/RouteOptions.swift b/MapboxCoreNavigation/RouteOptions.swift index 0ec64b3bac0..b4c59fe05d9 100644 --- a/MapboxCoreNavigation/RouteOptions.swift +++ b/MapboxCoreNavigation/RouteOptions.swift @@ -3,7 +3,7 @@ import MapboxDirections extension RouteOptions { internal var activityType: CLActivityType { switch self.profileIdentifier { - case DirectionsProfileIdentifier.cycling, DirectionsProfileIdentifier.walking: + case .cycling, .walking: return .fitness default: return .automotiveNavigation From 0ac34b003dc7b02c7f9b30f7ab3fc527f009f5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 21 Dec 2019 12:06:56 -0800 Subject: [PATCH 19/39] Call completion handlers on main thread --- MapboxCoreNavigation/OfflineDirections.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/MapboxCoreNavigation/OfflineDirections.swift b/MapboxCoreNavigation/OfflineDirections.swift index 69717441bf9..f888b5c60ae 100644 --- a/MapboxCoreNavigation/OfflineDirections.swift +++ b/MapboxCoreNavigation/OfflineDirections.swift @@ -189,13 +189,19 @@ public class NavigationDirections: Directions { 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) + DispatchQueue.main.async { + completionHandler(nil, nil, error) + } + 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) + DispatchQueue.main.async { + completionHandler(nil, nil, error) + } + return } DispatchQueue.main.async { From 41d2f0782b578b627db5bc862354ee8e1df367c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 21 Dec 2019 12:25:19 -0800 Subject: [PATCH 20/39] Bend over backwards for Xcode 10 --- MapboxNavigation/ManeuverView.swift | 4 ++-- MapboxNavigation/VisualInstruction.swift | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/MapboxNavigation/ManeuverView.swift b/MapboxNavigation/ManeuverView.swift index 5a6abbd3cdb..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: diff --git a/MapboxNavigation/VisualInstruction.swift b/MapboxNavigation/VisualInstruction.swift index 45d7979d975..8d441b8020c 100644 --- a/MapboxNavigation/VisualInstruction.swift +++ b/MapboxNavigation/VisualInstruction.swift @@ -38,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) } } From 0486778621f774fa88289617dd0c18b12d227ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sat, 21 Dec 2019 12:32:53 -0800 Subject: [PATCH 21/39] Implemented offline errors --- MapboxCoreNavigation/OfflineDirections.swift | 74 +++++++++++--------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/MapboxCoreNavigation/OfflineDirections.swift b/MapboxCoreNavigation/OfflineDirections.swift index f888b5c60ae..a42ac05ecaa 100644 --- a/MapboxCoreNavigation/OfflineDirections.swift +++ b/MapboxCoreNavigation/OfflineDirections.swift @@ -7,46 +7,60 @@ import MapboxNavigationNative */ public typealias NavigationDirectionsCompletionHandler = (_ numberOfTiles: UInt64) -> Void +/** + An error that occurs when calculating directions potentially offline using the `NavigationDirections.calculate(_:offline:completionHandler:)` method. +*/ public enum OfflineRoutingError: LocalizedError { - public typealias RawValue = String + /** + The enclosed error occurred when attempting to calculate directions online. + */ + case online(DirectionsError) - case onlineError(DirectionsError) /** - No route could be found between the specified locations. - - Make sure it is possible to travel between the locations with the mode of transportation implied by the profileIdentifier option. For example, it is impossible to travel by car from one continent to another without either a land bridge or a ferry connection. + The router returned an empty response. */ - case unableToRoute - case unexpectedRouteResult(String) - case corruptRouteData(String) - case responseError(String) + 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 .onlineError(let error): + case .online(let error): return error.localizedDescription - case .unableToRoute: - return "Unable to route between specified waypoints." - case .corruptRouteData(let value): - return value - case .unexpectedRouteResult(let value): - return value - case .responseError(let value): - return value + 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 failureReason: String { - #warning("unimplemented") - return "Unimplemented" + public var failureReason: String? { + switch self { + case .online(let error): + return error.failureReason + case .unknown(let underlying): + return (underlying as? LocalizedError)?.failureReason + default: + return nil + } } public var recoverySuggestion: String? { - #warning("unimplemented") - return nil + switch self { + case .online(let error): + return error.recoverySuggestion + case .unknown(let underlying): + return (underlying as? LocalizedError)?.recoverySuggestion + default: + return nil + } } } @@ -174,7 +188,7 @@ public class NavigationDirections: Directions { super.calculate(options) { (waypoints, routes, error) in let offlineError: OfflineRoutingError? if let error = error { - offlineError = .onlineError(error) + offlineError = .online(error) } else { offlineError = nil } @@ -187,19 +201,15 @@ 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) DispatchQueue.main.async { - completionHandler(nil, nil, error) + 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) + guard let data = result.json.data(using: .utf8) else { DispatchQueue.main.async { - completionHandler(nil, nil, error) + completionHandler(nil, nil, .invalidResponse) } return } @@ -208,7 +218,7 @@ public class NavigationDirections: Directions { do { let response = try JSONDecoder().decode(RouteResponse.self, from: data) guard let routes = response.routes else { - return completionHandler(response.waypoints, nil, .unableToRoute) + return completionHandler(response.waypoints, nil, .online(.unableToRoute)) } return completionHandler(response.waypoints, routes, nil) } From 4f7f20e0d6d18829d2f4b0b3fedc30e779cf9eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 21 Dec 2019 15:47:35 -0800 Subject: [PATCH 22/39] Specify options when decoding fixtures --- .../MapboxCoreNavigationTests.swift | 9 +++- .../NavigationEventsManagerTests.swift | 10 +++- .../NavigationServiceTests.swift | 46 +++++++++++++++---- .../TunnelAuthorityTests.swift | 6 ++- .../CarPlayManagerTests.swift | 17 +++++-- ...CarPlayNavigationViewControllerTests.swift | 6 ++- .../GuidanceCardsSnapshotTests.swift | 11 +++-- .../InstructionPresenterTests.swift | 6 ++- .../InstructionsCardCollectionTests.swift | 7 ++- .../ManeuverArrowTests.swift | 6 ++- .../MapboxVoiceControllerTests.swift | 5 +- .../NavigationMapViewTests.swift | 5 +- .../NavigationViewControllerTests.swift | 23 +++++++--- MapboxNavigationTests/RouteTests.swift | 8 +++- .../StepsViewControllerTests.swift | 2 +- TestHelper/Fixture.swift | 20 ++++---- 16 files changed, 143 insertions(+), 44 deletions(-) diff --git a/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift b/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift index 7b1011cd221..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.routeResponse(from: 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 diff --git a/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift b/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift index f4a4c9f9039..f3731f28620 100644 --- a/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift +++ b/MapboxCoreNavigationTests/NavigationEventsManagerTests.swift @@ -12,8 +12,14 @@ class NavigationEventsManagerTests: XCTestCase { } func testDepartRerouteArrive() { - let firstRoute = Fixture.route(from: "DCA-Arboretum") - let secondRoute = Fixture.route(from: "PipeFittersUnion-FourSeasonsBoston") + 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() diff --git a/MapboxCoreNavigationTests/NavigationServiceTests.swift b/MapboxCoreNavigationTests/NavigationServiceTests.swift index 2b20f393d66..4026738bb1c 100644 --- a/MapboxCoreNavigationTests/NavigationServiceTests.swift +++ b/MapboxCoreNavigationTests/NavigationServiceTests.swift @@ -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() @@ -175,7 +175,10 @@ 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) @@ -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() @@ -485,7 +510,10 @@ class NavigationServiceTests: XCTestCase { 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/TunnelAuthorityTests.swift b/MapboxCoreNavigationTests/TunnelAuthorityTests.swift index 40f9413a6ba..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() diff --git a/MapboxNavigationTests/CarPlayManagerTests.swift b/MapboxNavigationTests/CarPlayManagerTests.swift index a233cf441ba..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) @@ -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 @@ -483,7 +492,7 @@ class TestCarPlayManagerDelegate: CarPlayManagerDelegate { public var mapButtons: [CPMapButton]? func carPlayManager(_ carPlayManager: CarPlayManager, navigationServiceAlong route: Route, desiredSimulationMode: SimulationMode) -> NavigationService { - let response = Fixture.routeResponse(from: jsonFileName) + let response = Fixture.routeResponse(from: jsonFileName, options: routeOptions) let initialRoute = response.routes!.first! initialRoute.accessToken = "deadbeef" let directionsClientSpy = DirectionsSpy(accessToken: "garbage", host: nil) diff --git a/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift b/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift index 887909c25f1..4d038b123ce 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() 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 9d782d54c3a..8ae4032a2be 100644 --- a/MapboxNavigationTests/InstructionPresenterTests.swift +++ b/MapboxNavigationTests/InstructionPresenterTests.swift @@ -1,6 +1,7 @@ import Foundation import XCTest import MapboxDirections +import MapboxCoreNavigation import TestHelper @testable import MapboxNavigation @@ -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/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/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/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 1553858e345..96f7cc50bf8 100644 --- a/MapboxNavigationTests/NavigationMapViewTests.swift +++ b/MapboxNavigationTests/NavigationMapViewTests.swift @@ -5,7 +5,10 @@ import TestHelper @testable import MapboxCoreNavigation class NavigationMapViewTests: XCTestCase, MGLMapViewDelegate { - let response = Fixture.routeResponse(from: "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? diff --git a/MapboxNavigationTests/NavigationViewControllerTests.swift b/MapboxNavigationTests/NavigationViewControllerTests.swift index ed884c70026..50eb21f643a 100644 --- a/MapboxNavigationTests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/NavigationViewControllerTests.swift @@ -6,7 +6,12 @@ import Turf @testable import MapboxNavigation let jsonFileName = "routeWithInstructions" -let response = Fixture.routeResponse(from: 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]() @@ -43,11 +48,11 @@ class NavigationViewControllerTests: XCTestCase { }() 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() { @@ -221,7 +226,10 @@ class NavigationViewControllerTests: XCTestCase { 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") diff --git a/MapboxNavigationTests/RouteTests.swift b/MapboxNavigationTests/RouteTests.swift index 3eb556edd07..b72e166386b 100644 --- a/MapboxNavigationTests/RouteTests.swift +++ b/MapboxNavigationTests/RouteTests.swift @@ -2,13 +2,19 @@ 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 response = Fixture.mapMatchingResponse(from: "route-doubling-back") + let response = Fixture.mapMatchingResponse(from: "route-doubling-back", options: NavigationMatchOptions(coordinates: [ + CLLocationCoordinate2D(latitude: 59.337928, longitude: 18.076841), + CLLocationCoordinate2D(latitude: 59.337661, longitude: 18.075897), + CLLocationCoordinate2D(latitude: 59.337129, longitude: 18.075478), + // … + ])) let routes = response.routes let route = routes!.first! let leg = route.legs.first! diff --git a/MapboxNavigationTests/StepsViewControllerTests.swift b/MapboxNavigationTests/StepsViewControllerTests.swift index b441635c130..412947b7018 100644 --- a/MapboxNavigationTests/StepsViewControllerTests.swift +++ b/MapboxNavigationTests/StepsViewControllerTests.swift @@ -19,7 +19,7 @@ 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!.shape!.coordinates.first! diff --git a/TestHelper/Fixture.swift b/TestHelper/Fixture.swift index 2cbfa308edf..fe92cd63a34 100644 --- a/TestHelper/Fixture.swift +++ b/TestHelper/Fixture.swift @@ -63,26 +63,30 @@ public class Fixture: NSObject { return locations.map { CLLocation($0) } } - public class func routeResponse(from jsonFile: String) -> RouteResponse { + public class func routeResponse(from jsonFile: String, options: RouteOptions) -> RouteResponse { let responseData = JSONFromFileNamed(name: jsonFile) do { - return try JSONDecoder().decode(RouteResponse.self, from: responseData) + 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) -> MapMatchingResponse { + public class func mapMatchingResponse(from jsonFile: String, options: MatchOptions) -> MapMatchingResponse { let responseData = JSONFromFileNamed(name: jsonFile) do { - return try JSONDecoder().decode(MapMatchingResponse.self, from: responseData) + 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) -> Route { - let response = routeResponse(from: jsonFile) + 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") } @@ -96,8 +100,8 @@ public class Fixture: NSObject { return route } - public class func waypoints(from jsonFile: String) -> [Waypoint] { - let response = routeResponse(from: jsonFile) + 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") } From 7357da5ca5c75ecfe823ce16dad6000d9db64e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 22 Dec 2019 13:33:07 -0800 Subject: [PATCH 23/39] Avoid unnecessary shape copying --- MapboxCoreNavigation/RouteLeg.swift | 4 ++-- MapboxCoreNavigation/RouteProgress.swift | 2 +- .../CarPlayNavigationViewControllerTests.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MapboxCoreNavigation/RouteLeg.swift b/MapboxCoreNavigation/RouteLeg.swift index 5d1340bcd5e..0fe82e88ab0 100644 --- a/MapboxCoreNavigation/RouteLeg.swift +++ b/MapboxCoreNavigation/RouteLeg.swift @@ -2,7 +2,7 @@ import MapboxDirections import Turf extension RouteLeg { - var coordinates: [CLLocationCoordinate2D] { - return (steps.first?.shape?.coordinates ?? []) + steps.dropFirst().flatMap { ($0.shape?.coordinates ?? []).dropFirst() } + var shape: LineString { + return LineString((steps.first?.shape?.coordinates ?? []) + steps.dropFirst().flatMap { ($0.shape?.coordinates ?? []).dropFirst() }) } } diff --git a/MapboxCoreNavigation/RouteProgress.swift b/MapboxCoreNavigation/RouteProgress.swift index 97478286805..dfd17e2647d 100644 --- a/MapboxCoreNavigation/RouteProgress.swift +++ b/MapboxCoreNavigation/RouteProgress.swift @@ -456,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/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift b/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift index 4d038b123ce..9bca9290f49 100644 --- a/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift +++ b/MapboxNavigationTests/CarPlayNavigationViewControllerTests.swift @@ -62,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 From 70bd68f38f3c1765715cf1c56139056d67a7366b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 22 Dec 2019 13:34:33 -0800 Subject: [PATCH 24/39] Rewrote RouteOptions copying Rely on encoding and decoding to copy the object instead of manually copying each individual property, which is error-prone (and incomplete). --- MapboxCoreNavigation/RouteOptions.swift | 52 ++++++++++++------------ MapboxCoreNavigation/RouteProgress.swift | 2 +- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/MapboxCoreNavigation/RouteOptions.swift b/MapboxCoreNavigation/RouteOptions.swift index b4c59fe05d9..254b6b180fa 100644 --- a/MapboxCoreNavigation/RouteOptions.swift +++ b/MapboxCoreNavigation/RouteOptions.swift @@ -10,33 +10,6 @@ extension RouteOptions { } } - convenience init(options: RouteOptions) { - self.init(waypoints: options.waypoints, profileIdentifier: options.profileIdentifier) - allowsUTurnAtWaypoint = options.allowsUTurnAtWaypoint - roadClassesToAvoid = options.roadClassesToAvoid - alleyPriority = options.alleyPriority - walkwayPriority = options.walkwayPriority - speed = options.speed - includesAlternativeRoutes = options.includesAlternativeRoutes - includesExitRoundaboutManeuver = options.includesExitRoundaboutManeuver - } - - /** - 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 = RouteOptions(options: self) - 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. @@ -55,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 dfd17e2647d..7082bc9a422 100644 --- a/MapboxCoreNavigation/RouteProgress.swift +++ b/MapboxCoreNavigation/RouteProgress.swift @@ -262,7 +262,7 @@ open class RouteProgress: NSObject { user.headingAccuracy = RouteProgress.reroutingAccuracy } let newWaypoints = [user] + remainingWaypointsForCalculatingRoute() - let newOptions = RouteOptions(options: oldOptions) + let newOptions = oldOptions.copy() as! RouteOptions newOptions.waypoints = newWaypoints return newOptions From ad1e09c65b6e1382872a0eb8a3aed5e6c0f42f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 22 Dec 2019 15:41:08 -0800 Subject: [PATCH 25/39] Eliminated extra navigation status access --- MapboxCoreNavigation/RouteController.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index 565a120a99f..7490132f4df 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -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) @@ -361,6 +361,10 @@ 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") @@ -373,7 +377,7 @@ extension RouteController: Router { return true } - let status = navigator.getStatusForTimestamp(location.timestamp) + let status = status ?? navigator.getStatusForTimestamp(location.timestamp) let offRoute = status.routeState == .offRoute return !offRoute } From d88e8bf33e168d98cbb3e2c984037825e8d2d3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Sun, 22 Dec 2019 22:47:03 -0800 Subject: [PATCH 26/39] Fixed encoding route to JSON for navigator --- MapboxCoreNavigation/Route.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MapboxCoreNavigation/Route.swift b/MapboxCoreNavigation/Route.swift index 3485ce15da0..480079b34cb 100644 --- a/MapboxCoreNavigation/Route.swift +++ b/MapboxCoreNavigation/Route.swift @@ -4,6 +4,7 @@ extension Route { var json: String? { let encoder = JSONEncoder() + encoder.userInfo[.options] = routeOptions guard let encoded = try? encoder.encode(self) else { return nil } From c3dd04af7e95ced52ff2ce059af3b485acff7d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 23 Dec 2019 01:16:47 -0800 Subject: [PATCH 27/39] Fixed anachronism in documentation comment --- MapboxCoreNavigation/NavigationService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MapboxCoreNavigation/NavigationService.swift b/MapboxCoreNavigation/NavigationService.swift index 308c92ece21..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! From dede35fa8d76d4a89c5891a295216810089e0ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 23 Dec 2019 01:17:11 -0800 Subject: [PATCH 28/39] Fixed decoding response offline --- MapboxCoreNavigation/OfflineDirections.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MapboxCoreNavigation/OfflineDirections.swift b/MapboxCoreNavigation/OfflineDirections.swift index a42ac05ecaa..cb4414b3961 100644 --- a/MapboxCoreNavigation/OfflineDirections.swift +++ b/MapboxCoreNavigation/OfflineDirections.swift @@ -216,7 +216,9 @@ public class NavigationDirections: Directions { DispatchQueue.main.async { do { - let response = try JSONDecoder().decode(RouteResponse.self, from: data) + 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, .online(.unableToRoute)) } From dcdb1097be40786e903341a47d83e1433ad9c53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 23 Dec 2019 01:29:08 -0800 Subject: [PATCH 29/39] Fixed route progress synthesis --- MapboxCoreNavigationTests/RouteProgressTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MapboxCoreNavigationTests/RouteProgressTests.swift b/MapboxCoreNavigationTests/RouteProgressTests.swift index ae017668c2d..5b1f192a847 100644 --- a/MapboxCoreNavigationTests/RouteProgressTests.swift +++ b/MapboxCoreNavigationTests/RouteProgressTests.swift @@ -96,6 +96,7 @@ class RouteProgressTests: XCTestCase { 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) } From 5156dc6cd0db588eb935142fe2c2669400f0db4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 23 Dec 2019 09:14:00 -0800 Subject: [PATCH 30/39] Added missing required key to fixture --- TestHelper/Fixtures/DCA-Arboretum-dummy-faster-route.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 495ec3fb90d3fb729f1b2b98c16937ac2772ef48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Mon, 23 Dec 2019 09:34:10 -0800 Subject: [PATCH 31/39] Directions API errors can occur offline too --- MapboxCoreNavigation/OfflineDirections.swift | 16 +++++++++------- .../OfflineRoutingTests.swift | 8 +++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/MapboxCoreNavigation/OfflineDirections.swift b/MapboxCoreNavigation/OfflineDirections.swift index cb4414b3961..6d32da4bfda 100644 --- a/MapboxCoreNavigation/OfflineDirections.swift +++ b/MapboxCoreNavigation/OfflineDirections.swift @@ -12,9 +12,11 @@ public typealias NavigationDirectionsCompletionHandler = (_ numberOfTiles: UInt6 */ public enum OfflineRoutingError: LocalizedError { /** - The enclosed error occurred when attempting to calculate directions online. + A standard Directions API error occurred. + + A Directions API error can occur whether directions are calculated online or offline. */ - case online(DirectionsError) + case standard(DirectionsError) /** The router returned an empty response. @@ -30,7 +32,7 @@ public enum OfflineRoutingError: LocalizedError { public var localizedDescription: String { switch self { - case .online(let error): + 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") @@ -43,7 +45,7 @@ public enum OfflineRoutingError: LocalizedError { public var failureReason: String? { switch self { - case .online(let error): + case .standard(let error): return error.failureReason case .unknown(let underlying): return (underlying as? LocalizedError)?.failureReason @@ -54,7 +56,7 @@ public enum OfflineRoutingError: LocalizedError { public var recoverySuggestion: String? { switch self { - case .online(let error): + case .standard(let error): return error.recoverySuggestion case .unknown(let underlying): return (underlying as? LocalizedError)?.recoverySuggestion @@ -188,7 +190,7 @@ public class NavigationDirections: Directions { super.calculate(options) { (waypoints, routes, error) in let offlineError: OfflineRoutingError? if let error = error { - offlineError = .online(error) + offlineError = .standard(error) } else { offlineError = nil } @@ -220,7 +222,7 @@ public class NavigationDirections: Directions { decoder.userInfo[.options] = options let response = try decoder.decode(RouteResponse.self, from: data) guard let routes = response.routes else { - return completionHandler(response.waypoints, nil, .online(.unableToRoute)) + return completionHandler(response.waypoints, nil, .standard(.unableToRoute)) } return completionHandler(response.waypoints, routes, nil) } diff --git a/MapboxCoreNavigationTests/OfflineRoutingTests.swift b/MapboxCoreNavigationTests/OfflineRoutingTests.swift index fce3f196838..80a2e79f8a5 100644 --- a/MapboxCoreNavigationTests/OfflineRoutingTests.swift +++ b/MapboxCoreNavigationTests/OfflineRoutingTests.swift @@ -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() From 90a908df77075fd1d9504167dc88a1f155533913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 24 Dec 2019 12:09:31 -0800 Subject: [PATCH 32/39] Fixed UnimplementedLogging test --- .../NavigationServiceTests.swift | 460 ++++++++++++++++++ 1 file changed, 460 insertions(+) diff --git a/MapboxCoreNavigationTests/NavigationServiceTests.swift b/MapboxCoreNavigationTests/NavigationServiceTests.swift index 4026738bb1c..6ec9eb7bf15 100644 --- a/MapboxCoreNavigationTests/NavigationServiceTests.swift +++ b/MapboxCoreNavigationTests/NavigationServiceTests.swift @@ -507,6 +507,466 @@ 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 = [] From 0067b017684eee8eecdd0256f5cc9902eb13cb2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 24 Dec 2019 13:40:36 -0800 Subject: [PATCH 33/39] Removed unused code --- MapboxNavigation/InstructionPresenter.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/MapboxNavigation/InstructionPresenter.swift b/MapboxNavigation/InstructionPresenter.swift index 8d56d206836..f33231830bd 100644 --- a/MapboxNavigation/InstructionPresenter.swift +++ b/MapboxNavigation/InstructionPresenter.swift @@ -308,10 +308,6 @@ extension CGSize { } } -typealias IndexedVisualInstructionComponent = (Array.Element, - Array.Index) - - extension Array where Element == VisualInstruction.Component { fileprivate func component(before component: VisualInstruction.Component) -> VisualInstruction.Component? { guard let index = self.firstIndex(of: component) else { From 0a1cce45e54dab5d009785ddcad0a739fcfac647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 24 Dec 2019 15:34:40 -0800 Subject: [PATCH 34/39] Fixed shield image mocking --- MapboxNavigationTests/Constants.swift | 7 +++--- ...structionsBannerViewIntegrationTests.swift | 10 ++++----- .../InstructionsBannerViewSnapshotTests.swift | 22 +++++++++---------- 3 files changed, 20 insertions(+), 19 deletions(-) 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/InstructionsBannerViewIntegrationTests.swift b/MapboxNavigationTests/InstructionsBannerViewIntegrationTests.swift index 7710f38e048..b92a04bc39a 100644 --- a/MapboxNavigationTests/InstructionsBannerViewIntegrationTests.swift +++ b/MapboxNavigationTests/InstructionsBannerViewIntegrationTests.swift @@ -33,9 +33,9 @@ class InstructionsBannerViewIntegrationTests: XCTestCase { lazy var instructions: [VisualInstruction.Component] = { let components: [VisualInstruction.Component] = [ - .image(image: .init(imageBaseURL: ShieldImage.us101.url), alternativeText: .init(text: "US 101", abbreviation: nil, abbreviationPriority: 0)), + .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.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), + .image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), ] return components }() @@ -101,8 +101,8 @@ class InstructionsBannerViewIntegrationTests: XCTestCase { func testDelimiterIsHiddenWhenAllShieldsAreAlreadyLoaded() { //prime the cache to simulate images having already been loaded - let instruction1 = VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.i280.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)) - let instruction2 = VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.us101.url), alternativeText: .init(text: "US 101", 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) @@ -307,7 +307,7 @@ class InstructionsBannerViewIntegrationTests: XCTestCase { private func simulateDownloadingShieldForComponent(_ component: VisualInstruction.Component) { var imageURL: URL! - if case let VisualInstruction.Component.image(image: imageRepresentation, alternativeText: _) = component, let imageBaseURL = imageRepresentation.imageBaseURL { + if case let VisualInstruction.Component.image(image: imageRepresentation, alternativeText: _) = component, let imageBaseURL = imageRepresentation.imageURL(format: .png) { imageURL = imageBaseURL } let operation: ImageDownloadOperationSpy = ImageDownloadOperationSpy.operationForURL(imageURL)! diff --git a/MapboxNavigationTests/InstructionsBannerViewSnapshotTests.swift b/MapboxNavigationTests/InstructionsBannerViewSnapshotTests.swift index a12cdac9dde..5dc4832e8bf 100644 --- a/MapboxNavigationTests/InstructionsBannerViewSnapshotTests.swift +++ b/MapboxNavigationTests/InstructionsBannerViewSnapshotTests.swift @@ -14,8 +14,8 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { recordMode = false agnosticOptions = [.OS, .device] - let i280Instruction = VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.i280.url), alternativeText: .init(text: "I-280", abbreviation: nil, abbreviationPriority: 0)) - let us101Instruction = VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.us101.url), alternativeText: .init(text: "US 101", 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) @@ -60,7 +60,7 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.distance = 482 let instructions: [VisualInstruction.Component] = [ - .image(image: .init(imageBaseURL: ShieldImage.i280.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), + .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)), ] @@ -77,7 +77,7 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.distance = 482 let primary: [VisualInstruction.Component] = [ - .image(image: .init(imageBaseURL: ShieldImage.i280.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), + .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 = [VisualInstruction.Component.text(text: .init(text: "US 45 / Chicago", abbreviation: nil, abbreviationPriority: 0))] @@ -95,7 +95,7 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.distance = 482 let primary: [VisualInstruction.Component] = [ - .image(image: .init(imageBaseURL: ShieldImage.i280.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), + .image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), ] let secondary = [VisualInstruction.Component.text(text: .init(text: "Mountain View Test", abbreviation: nil, abbreviationPriority: 0))] @@ -112,7 +112,7 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.distance = 482 let primary: [VisualInstruction.Component] = [ - .image(image: .init(imageBaseURL: ShieldImage.i280.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), + .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)), @@ -134,7 +134,7 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.distance = 482 let primary: [VisualInstruction.Component] = [ - .image(image: .init(imageBaseURL: ShieldImage.i280.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: nil)), + .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)), @@ -173,9 +173,9 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { view.distance = 482 let primary: [VisualInstruction.Component] = [ - .image(image: .init(imageBaseURL: ShieldImage.i280.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: nil)), + .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.url), alternativeText: .init(text: "US-101", 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)) @@ -198,14 +198,14 @@ class InstructionsBannerViewSnapshotTests: FBSnapshotTestCase { instructionsBannerView.distance = 482 let primary = [ - VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.i280.url), alternativeText: .init(text: "I 280", abbreviation: nil, abbreviationPriority: 0)), + VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.i280.baseURL), alternativeText: .init(text: "I 280", 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 = [ - VisualInstruction.Component.image(image: .init(imageBaseURL: ShieldImage.i280.url), alternativeText: .init(text: "I 280", 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) From 89108718e3c3b602cf55e8e07dc2ebba86def558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 24 Dec 2019 16:24:52 -0800 Subject: [PATCH 35/39] =?UTF-8?q?Fixed=20map=20matching=E2=80=93based=20te?= =?UTF-8?q?sts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit route-doubling-back.json was apparently generated with 5 digits of precision. --- .../RouteControllerSnapshotTests.swift | 16 +++++- MapboxNavigationTests/RouteTests.swift | 50 ++++++++++++++++--- .../SimulatedLocationManagerTests.swift | 16 +++++- TestHelper/Fixture.swift | 10 +--- 4 files changed, 76 insertions(+), 16 deletions(-) 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 b72e166386b..32df09dba29 100644 --- a/MapboxNavigationTests/RouteTests.swift +++ b/MapboxNavigationTests/RouteTests.swift @@ -9,12 +9,50 @@ 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 response = Fixture.mapMatchingResponse(from: "route-doubling-back", options: NavigationMatchOptions(coordinates: [ - CLLocationCoordinate2D(latitude: 59.337928, longitude: 18.076841), - CLLocationCoordinate2D(latitude: 59.337661, longitude: 18.075897), - CLLocationCoordinate2D(latitude: 59.337129, longitude: 18.075478), - // … - ])) + 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! 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/TestHelper/Fixture.swift b/TestHelper/Fixture.swift index fe92cd63a34..7ead20e2c79 100644 --- a/TestHelper/Fixture.swift +++ b/TestHelper/Fixture.swift @@ -109,14 +109,8 @@ public class Fixture: NSObject { } // Returns `Route` objects from a match response - public class func routesFromMatches(at filePath: String) -> [Route]? { - let responseData = JSONFromFileNamed(name: filePath) - let response: MapMatchingResponse! - do { - response = try JSONDecoder().decode(MapMatchingResponse.self, from: responseData) - } catch { - preconditionFailure("Unable to decode JSON fixture: \(error)") - } + 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") } From 4527f0955ec2086f80f1a6f720e3e8f499693dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 24 Dec 2019 17:21:52 -0800 Subject: [PATCH 36/39] Loosened annotation test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coordinates are now round-tripped through Polyline encoding, so they get rounded to 5 or 6 places. What matters is that they’re still really close to the mark in terms of physical distances. --- .../NavigationViewControllerTests.swift | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/MapboxNavigationTests/NavigationViewControllerTests.swift b/MapboxNavigationTests/NavigationViewControllerTests.swift index 50eb21f643a..cfde418e90d 100644 --- a/MapboxNavigationTests/NavigationViewControllerTests.swift +++ b/MapboxNavigationTests/NavigationViewControllerTests.swift @@ -205,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 @@ -218,8 +219,7 @@ 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() { @@ -261,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 { From c85c4fa664c730ebf3d1af216bfdf58832f27440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Tue, 24 Dec 2019 23:06:42 -0800 Subject: [PATCH 37/39] Simplified stringification of instruction components --- MapboxNavigation.xcodeproj/project.pbxproj | 4 + MapboxNavigation/Array.swift | 34 +++++ MapboxNavigation/ExitView.swift | 2 +- MapboxNavigation/InstructionPresenter.swift | 150 ++++++-------------- 4 files changed, 85 insertions(+), 105 deletions(-) create mode 100644 MapboxNavigation/Array.swift diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index 690d12540f4..44e3c51c827 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -377,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 */; }; @@ -976,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 = ""; }; @@ -1542,6 +1544,7 @@ children = ( C5381F01204E03B600A5493E /* UIDevice.swift */, 352AFFA22139986E00EB3567 /* UIViewController.swift */, + DA66062F23B32F99007832E5 /* Array.swift */, 351BEC081E5BCC72006FE110 /* Bundle.swift */, 8D24A2F720409A890098CBF8 /* CGSize.swift */, DA0557202154EF4700A1F2AA /* Route.swift */, @@ -2431,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 */, 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/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 f33231830bd..9de043a5abb 100644 --- a/MapboxNavigation/InstructionPresenter.swift +++ b/MapboxNavigation/InstructionPresenter.swift @@ -29,19 +29,20 @@ 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 [] } + guard let source = self.dataSource else { + return NSAttributedString() + } var attributedPairs = self.attributedPairs(for: instruction, dataSource: source, imageRepository: imageRepository, onImageDownload: completeShieldDownload) + let defaultAttributes = attributes(for: source) + let separator = NSAttributedString(string: " ", attributes: defaultAttributes) + let totalWidth = attributedPairs.attributedStrings.joined(separator: separator).size().width + let availableBounds = source.availableBounds() - let totalWidth: CGFloat = attributedPairs.attributedStrings.map { $0.size() }.reduce(.zero, +).width let stringFits = totalWidth <= availableBounds.width - guard !stringFits else { return attributedPairs.attributedStrings } + guard !stringFits else { + return attributedPairs.attributedStrings.joined(separator: separator) + } typealias IndexedTextRepresentation = (Array.Index, VisualInstruction.Component.TextRepresentation) let textRepresentations: [IndexedTextRepresentation] = attributedPairs.components.enumerated().compactMap { (idx, elem) in @@ -59,97 +60,60 @@ class InstructionPresenter { } for (index, representation) in sorted { - let isFirst = index == 0 - let joinChar = isFirst ? "" : " " guard let abbreviation = representation.abbreviation else { continue } - attributedPairs.attributedStrings[index] = NSAttributedString(string: joinChar + abbreviation, attributes: attributes(for: source)) - let newWidth: CGFloat = attributedPairs.attributedStrings.map { $0.size() }.reduce(.zero, +).width + attributedPairs.attributedStrings[index] = NSAttributedString(string: abbreviation, attributes: defaultAttributes) + let newWidth = attributedPairs.attributedStrings.joined(separator: separator).size().width if newWidth <= availableBounds.width { break } } - return attributedPairs.attributedStrings + return attributedPairs.attributedStrings.joined(separator: separator) } typealias AttributedInstructionComponents = (components: [VisualInstruction.Component], attributedStrings: [NSAttributedString]) func attributedPairs(for instruction: VisualInstruction, dataSource: DataSource, imageRepository: ImageRepository, onImageDownload: @escaping ImageDownloadCompletion) -> AttributedInstructionComponents { - let components = instruction.components - var strings: [NSAttributedString] = [] - var processedComponents: [VisualInstruction.Component] = [] + var components = instruction.components - 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: (_: VisualInstruction.Component, _: [NSAttributedString]) -> Void = { (component, attributedStrings) in - processedComponents.append(component) - strings.append(attributedStrings.reduce(initial, +)) - } - let isShield: (_ key: VisualInstruction.Component?) -> Bool = { (component) in - guard let key = component?.cacheKey else { return false } - return imageRepository.cachedImageForKey(key) != nil + 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 componentBefore = components.component(before: component) - let componentAfter = components.component(after: component) - + } + + let attributedTextRepresentations = components.map { (component) -> NSAttributedString in switch component { - //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 let .exitCode(representation) where 0...1 ~= index: - guard let exitString = self.attributedString(forExitRepresentation: representation, maneuverDirection: instruction.maneuverDirection!, dataSource: dataSource, cacheKey: component.cacheKey!) 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 let .image(imageRepresentation, textRepresentation): - if let shieldString = attributedString(forShieldComponent: imageRepresentation, repository: imageRepository, dataSource: dataSource, cacheKey: component.cacheKey!, onImageDownload: onImageDownload) { - build(component, [joinString, shieldString]) - } else if let genericShieldString = attributedString(forGenericShield: textRepresentation, dataSource: dataSource, cacheKey: component.cacheKey!) { - build(component, [joinString, genericShieldString]) - } else { - fallthrough - } - - case let .text(textRepresentation), let .delimiter(textRepresentation): - let componentString = NSAttributedString(string: textRepresentation.text, attributes: attributes(for: dataSource)) - build(component, [joinString, componentString]) - - default: - continue + case .delimiter(let text), .text(let text): + return NSAttributedString(string: text.text, attributes: attributes(for: dataSource)) + case .image(let image, let alternativeText): + return attributedString(forShieldComponent: image, repository: imageRepository, dataSource: dataSource, cacheKey: component.cacheKey!, onImageDownload: onImageDownload) + ?? genericShield(text: alternativeText.text, dataSource: dataSource, cacheKey: component.cacheKey!) + ?? NSAttributedString(string: alternativeText.text, attributes: attributes(for: dataSource)) + 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: attributes(for: dataSource)) + case .lane(_, _): + preconditionFailure("Lane component has no attributed string representation.") } } - - assert(processedComponents.count == strings.count, "The number of processed components must match the number of attributed strings") - return (components: processedComponents, attributedStrings: strings) - } - - func attributedString(forExitRepresentation representation: VisualInstruction.Component.TextRepresentation, maneuverDirection: ManeuverDirection, dataSource: DataSource, cacheKey: String) -> NSAttributedString? { - let exitCode = representation.text - let side: ExitSide = maneuverDirection == .left ? .left : .right - guard let exitString = exitShield(side: side, text: exitCode, dataSource: dataSource, cacheKey: cacheKey) else { return nil } - return exitString - } - - func attributedString(forGenericShield representation: VisualInstruction.Component.TextRepresentation, dataSource: DataSource, cacheKey: String) -> NSAttributedString? { - let text = representation.text - return genericShield(text: text, dataSource: dataSource, cacheKey: cacheKey) + return (components, attributedTextRepresentations) } func attributedString(forShieldComponent shield: VisualInstruction.Component.ImageRepresentation, repository:ImageRepository, dataSource: DataSource, cacheKey: String, onImageDownload: @escaping ImageDownloadCompletion) -> NSAttributedString? { @@ -307,25 +271,3 @@ extension CGSize { return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) } } - -extension Array where Element == VisualInstruction.Component { - fileprivate func component(before component: VisualInstruction.Component) -> VisualInstruction.Component? { - guard let index = self.firstIndex(of: component) else { - return nil - } - if index > 0 { - return self[index-1] - } - return nil - } - - fileprivate func component(after component: VisualInstruction.Component) -> VisualInstruction.Component? { - guard let index = self.firstIndex(of: component) else { - return nil - } - if index+1 < self.endIndex { - return self[index+1] - } - return nil - } -} From 1ac44252afca881a1675801e4f502890ba1d2d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Wed, 25 Dec 2019 00:32:26 -0800 Subject: [PATCH 38/39] Streamlined stringification of instruction components Build up a single attributed string as a container, embedding abbreviations in attributes. --- MapboxNavigation/InstructionPresenter.swift | 107 ++++++++++++-------- 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/MapboxNavigation/InstructionPresenter.swift b/MapboxNavigation/InstructionPresenter.swift index 9de043a5abb..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? @@ -32,50 +46,42 @@ class InstructionPresenter { guard let source = self.dataSource else { return NSAttributedString() } - var attributedPairs = self.attributedPairs(for: instruction, dataSource: source, imageRepository: imageRepository, onImageDownload: completeShieldDownload) - let defaultAttributes = attributes(for: source) - let separator = NSAttributedString(string: " ", attributes: defaultAttributes) - let totalWidth = attributedPairs.attributedStrings.joined(separator: separator).size().width - - let availableBounds = source.availableBounds() - let stringFits = totalWidth <= availableBounds.width - guard !stringFits else { - return attributedPairs.attributedStrings.joined(separator: separator) - } + let attributedTextRepresentation = self.attributedTextRepresentation(of: instruction, dataSource: source, imageRepository: imageRepository, onImageDownload: completeShieldDownload).mutableCopy() as! NSMutableAttributedString - typealias IndexedTextRepresentation = (Array.Index, VisualInstruction.Component.TextRepresentation) - let textRepresentations: [IndexedTextRepresentation] = attributedPairs.components.enumerated().compactMap { (idx, elem) in - if case let VisualInstruction.Component.text(representation) = elem { - return (idx, representation) + // 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 nil } - let sorted = textRepresentations.sorted { first, second in - let firstPriority = first.1.abbreviationPriority ?? Int.max - let secondPriority = second.1.abbreviationPriority ?? Int.max - - return firstPriority < secondPriority - } - - for (index, representation) in sorted { - guard let abbreviation = representation.abbreviation else { continue } - - attributedPairs.attributedStrings[index] = NSAttributedString(string: abbreviation, attributes: defaultAttributes) - let newWidth = attributedPairs.attributedStrings.joined(separator: separator).size().width - - if newWidth <= availableBounds.width { + // 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 } + + // 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) + } + } } - return attributedPairs.attributedStrings.joined(separator: separator) + return attributedTextRepresentation } - typealias AttributedInstructionComponents = (components: [VisualInstruction.Component], attributedStrings: [NSAttributedString]) - - func attributedPairs(for instruction: VisualInstruction, dataSource: DataSource, imageRepository: ImageRepository, onImageDownload: @escaping ImageDownloadCompletion) -> AttributedInstructionComponents { + 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 @@ -95,25 +101,44 @@ class InstructionPresenter { } } + 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), .text(let text): - return NSAttributedString(string: text.text, attributes: attributes(for: dataSource)) + 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): - return attributedString(forShieldComponent: image, repository: imageRepository, dataSource: dataSource, cacheKey: component.cacheKey!, onImageDownload: onImageDownload) + // 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!) - ?? NSAttributedString(string: alternativeText.text, attributes: attributes(for: dataSource)) + // 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: attributes(for: dataSource)) + ?? NSAttributedString(string: text.text, attributes: defaultAttributes) case .lane(_, _): preconditionFailure("Lane component has no attributed string representation.") } } - return (components, attributedTextRepresentations) + let separator = NSAttributedString(string: " ", attributes: defaultAttributes) + return attributedTextRepresentations.joined(separator: separator) } func attributedString(forShieldComponent shield: VisualInstruction.Component.ImageRepresentation, repository:ImageRepository, dataSource: DataSource, cacheKey: String, onImageDownload: @escaping ImageDownloadCompletion) -> NSAttributedString? { @@ -137,10 +162,6 @@ class InstructionPresenter { repository.imageWithURL(imageURL, cacheKey: cacheKey, completion: completion ) } - private func attributes(for dataSource: InstructionPresenterDataSource) -> [NSAttributedString.Key: Any] { - return [.font: dataSource.font as Any, .foregroundColor: dataSource.textColor as Any] - } - private func attributedString(withFont font: UIFont, shieldImage: UIImage) -> NSAttributedString { let attachment = ShieldAttachment() attachment.font = font From a5942cb343d93096b3d810561054cd5492edc51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguye=CC=82=CC=83n?= Date: Thu, 26 Dec 2019 11:07:58 -0800 Subject: [PATCH 39/39] Eliminated extra array copy --- MapboxCoreNavigation/RouteLeg.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MapboxCoreNavigation/RouteLeg.swift b/MapboxCoreNavigation/RouteLeg.swift index 0fe82e88ab0..7c07d2ff314 100644 --- a/MapboxCoreNavigation/RouteLeg.swift +++ b/MapboxCoreNavigation/RouteLeg.swift @@ -3,6 +3,8 @@ import Turf extension RouteLeg { var shape: LineString { - return LineString((steps.first?.shape?.coordinates ?? []) + steps.dropFirst().flatMap { ($0.shape?.coordinates ?? []).dropFirst() }) + return steps.dropFirst().reduce(into: steps.first?.shape ?? LineString([])) { (result, step) in + result.coordinates += (step.shape?.coordinates ?? []).dropFirst() + } } }