diff --git a/Cartfile b/Cartfile index 34be57c476a..2db05b752b6 100644 --- a/Cartfile +++ b/Cartfile @@ -1,9 +1,9 @@ binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" ~> 3.6 -github "mapbox/MapboxDirections.swift" ~> 0.10.5 +github "mapbox/MapboxDirections.swift" ~> 0.11 github "mapbox/turf-swift" ~> 0.0.3 github "52inc/Pulley" == 1.4 github "rs/SDWebImage" ~> 4.1 -github "Project-OSRM/osrm-text-instructions.swift" ~> 0.3.5 +github "Project-OSRM/osrm-text-instructions.swift" ~> 0.4 github "frederoni/aws-sdk-ios" ~> 2.6 github "mapbox/mapbox-telemetry-ios" ~> 0.2 github "1ec5/Solar" "scheme-test-only" diff --git a/Cartfile.resolved b/Cartfile.resolved index 6d4047138bc..93abd96b2e4 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,11 +1,11 @@ binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" "3.6.4" github "1ec5/Solar" "5ce2ca7baaa82b1abb0467642ae237faf674b645" github "52inc/Pulley" "1.4" -github "Project-OSRM/osrm-text-instructions.swift" "v0.3.5" +github "Project-OSRM/osrm-text-instructions.swift" "v0.4.0" github "facebook/ios-snapshot-test-case" "2.1.4" github "frederoni/aws-sdk-ios" "2.6.0" -github "mapbox/MapboxDirections.swift" "v0.10.6" +github "mapbox/MapboxDirections.swift" "v0.11.2" github "mapbox/mapbox-telemetry-ios" "v0.2.11" github "mapbox/turf-swift" "v0.0.3" github "raphaelmor/Polyline" "v4.1.1" -github "rs/SDWebImage" "4.1.0" +github "rs/SDWebImage" "4.1.2" diff --git a/Examples/Objective-C/ViewController.m b/Examples/Objective-C/ViewController.m index 7a5a5b857be..6d62bf10145 100644 --- a/Examples/Objective-C/ViewController.m +++ b/Examples/Objective-C/ViewController.m @@ -51,36 +51,20 @@ - (IBAction)didLongPress:(UILongPressGestureRecognizer *)sender { } - (void)resumeNotifications { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(alertLevelDidChange:) name:MBRouteControllerAlertLevelDidChange object:_navigation]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(alertLevelDidChange:) name:MBRouteControllerDidPassSpokenInstructionPoint object:_navigation]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(progressDidChange:) name:MBRouteControllerNotificationProgressDidChange object:_navigation]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willReroute:) name:MBRouteControllerWillReroute object:_navigation]; } - (void)suspendNotifications { - [[NSNotificationCenter defaultCenter] removeObserver:self name:MBRouteControllerAlertLevelDidChange object:_navigation]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:MBRouteControllerDidPassSpokenInstructionPoint object:_navigation]; [[NSNotificationCenter defaultCenter] removeObserver:self name:MBRouteControllerNotificationProgressDidChange object:_navigation]; [[NSNotificationCenter defaultCenter] removeObserver:self name:MBRouteControllerWillReroute object:_navigation]; } - (void)alertLevelDidChange:(NSNotification *)notification { MBRouteProgress *routeProgress = (MBRouteProgress *)notification.userInfo[MBRouteControllerProgressDidChangeNotificationProgressKey]; - MBRouteStep *upcomingStep = routeProgress.currentLegProgress.upComingStep; - - NSString *text = nil; - if (upcomingStep) { - MBAlertLevel alertLevel = routeProgress.currentLegProgress.alertUserLevel; - if (alertLevel == MBAlertLevelHigh) { - text = upcomingStep.instructions; - } else { - text = [NSString stringWithFormat:@"In %@ %@", - [self.lengthFormatter stringFromMeters:routeProgress.currentLegProgress.currentStepProgress.distanceRemaining], - upcomingStep.instructions]; - } - } else { - text = [NSString stringWithFormat:@"In %@ %@", - [self.lengthFormatter stringFromMeters:routeProgress.currentLegProgress.currentStepProgress.distanceRemaining], - routeProgress.currentLegProgress.currentStep.instructions]; - } + NSString *text = routeProgress.currentLegProgress.currentStepProgress.currentSpokenInstruction.text; [self.speechSynth speakUtterance:[AVSpeechUtterance speechUtteranceWithString:text]]; } diff --git a/Examples/Swift/CustomViewController.swift b/Examples/Swift/CustomViewController.swift index 5dd90390199..5f14d7b16a2 100644 --- a/Examples/Swift/CustomViewController.swift +++ b/Examples/Swift/CustomViewController.swift @@ -12,12 +12,10 @@ class CustomViewController: UIViewController, MGLMapViewDelegate, AVSpeechSynthe var routeController: RouteController! let textDistanceFormatter = DistanceFormatter(approximate: true) - let voiceDistanceFormatter = SpokenDistanceFormatter(approximate: true) lazy var speechSynth = AVSpeechSynthesizer() var userRoute: Route? var simulateLocation = false let visualInstructionFormatter = VisualInstructionFormatter() - let spokenInstructionFormatter = SpokenInstructionFormatter() @IBOutlet var mapView: MGLMapView! @IBOutlet weak var arrowView: UILabel! @@ -61,13 +59,13 @@ class CustomViewController: UIViewController, MGLMapViewDelegate, AVSpeechSynthe } func resumeNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(alertLevelDidChange(_ :)), name: RouteControllerAlertLevelDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(shouldSpeak(_:)), name: RouteControllerDidPassSpokenInstructionPoint, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_ :)), name: RouteControllerProgressDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(rerouted(_:)), name: RouteControllerWillReroute, object: nil) } func suspendNotifications() { - NotificationCenter.default.removeObserver(self, name: RouteControllerAlertLevelDidChange, object: nil) + NotificationCenter.default.removeObserver(self, name: RouteControllerDidPassSpokenInstructionPoint, object: nil) NotificationCenter.default.removeObserver(self, name: RouteControllerProgressDidChange, object: nil) NotificationCenter.default.removeObserver(self, name: RouteControllerWillReroute, object: nil) } @@ -76,10 +74,11 @@ class CustomViewController: UIViewController, MGLMapViewDelegate, AVSpeechSynthe addRouteToMap() } - // When the alert level changes, this signals the user is ready for a voice announcement - func alertLevelDidChange(_ notification: NSNotification) { - let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as! RouteProgress - let text = spokenInstructionFormatter.string(routeProgress: routeProgress, userDistance: routeProgress.currentLegProgress.currentStepProgress.distanceRemaining, markUpWithSSML: false) + // When an instruction should be given + func shouldSpeak(_ notification: NSNotification) { + let routeProgress = notification.userInfo![MBRouteControllerDidPassSpokenInstructionPointRouteProgressKey] as! RouteProgress + + guard let text = routeProgress.currentLegProgress.currentStepProgress.currentSpokenInstruction?.text else { return } let utterance = AVSpeechUtterance(string: text) speechSynth.delegate = self diff --git a/MapboxCoreNavigation.podspec b/MapboxCoreNavigation.podspec index 36c25276f4d..935b15e247f 100644 --- a/MapboxCoreNavigation.podspec +++ b/MapboxCoreNavigation.podspec @@ -40,8 +40,8 @@ Pod::Spec.new do |s| s.requires_arc = true s.module_name = "MapboxCoreNavigation" - s.dependency "MapboxDirections.swift", "~> 0.10.5" - s.dependency "OSRMTextInstructions", "~> 0.3" + s.dependency "MapboxDirections.swift", "~> 0.11" + s.dependency "OSRMTextInstructions", "~> 0.4" s.dependency "MapboxMobileEvents", "~> 0.2" s.dependency "Turf", "~> 0.0.3" diff --git a/MapboxCoreNavigation/Constants.swift b/MapboxCoreNavigation/Constants.swift index 72a50068508..0ccae4f0791 100644 --- a/MapboxCoreNavigation/Constants.swift +++ b/MapboxCoreNavigation/Constants.swift @@ -17,16 +17,6 @@ public let RouteControllerProgressDidChangeNotificationLocationKey = MBRouteCont */ public let RouteControllerProgressDidChangeNotificationSecondsRemainingOnStepKey = MBRouteControllerProgressDidChangeNotificationSecondsRemainingOnStepKey -/** - Key used for accessing the `RouteProgress` object from a `RouteControllerAlertLevelDidChange` notification's `userInfo` dictionary. - */ -public let RouteControllerAlertLevelDidChangeNotificationRouteProgressKey = MBRouteControllerAlertLevelDidChangeNotificationRouteProgressKey - -/** - Key used for accessing the user's snapped distance to the end of the maneuver (CLLocationDistance) from a `RouteControllerAlertLevelDidChange` notification's `userInfo` dictionary. - */ -public let RouteControllerAlertLevelDidChangeNotificationDistanceToEndOfManeuverKey = MBRouteControllerAlertLevelDidChangeNotificationDistanceToEndOfManeuverKey - /** Key used for accessing the user's current `CLLocation` from a `RouteControllerWillReroute` notification's `userInfo` dictionary. */ @@ -53,9 +43,14 @@ public let RouteControllerDidFindFasterRouteKey = MBRouteControllerDidFindFaster public let RouteControllerProgressDidChange = Notification.Name(MBRouteControllerNotificationProgressDidChange) /** - Emitted when the alert level changes. This indicates the user should be notified about the upcoming maneuver. + Emitted when the user passes an ideal point for saying an instruction aloud. */ -public let RouteControllerAlertLevelDidChange = Notification.Name(MBRouteControllerAlertLevelDidChange) +public let RouteControllerDidPassSpokenInstructionPoint = Notification.Name(MBRouteControllerDidPassSpokenInstructionPoint) + +/** + Key for accessing the `RouteProgress` key emitted when `RouteControllerDidPassSpokenInstructionPoint` is fired. + */ +public let RouteControllerDidPassSpokenInstructionPointRouteProgressKey = MBRouteControllerDidPassSpokenInstructionPointRouteProgressKey /** Emitted when the user has gone off-route and the `RouteController` is about to reroute. @@ -104,16 +99,6 @@ public var RouteControllerHighAlertInterval: TimeInterval = 15 */ public var RouteControllerManeuverZoneRadius: CLLocationDistance = 40 -/** - Remaing distance on a motorway at which the `AlertLevel.high` `AlertLevel` will be given. This overrides `RouteControllerHighAlertInterval` only when the current step is a motorway. Default value is a half mile. - */ -public var RouteControllerMotorwayHighAlertDistance: CLLocationDistance = 0.25 * milesToMeters - -/** - Remaing distance on a motorway at which the `AlertLevel.medium` `AlertLevel` will be given. This overrides `RouteControllerMediumAlertInterval` only when the current step is a motorway. Defauly value is 2 miles. - */ -public var RouteControllerMotorwayMediumAlertDistance: CLLocationDistance = 2 * milesToMeters - /** When calculating whether or not the user is on the route, we look where the user will be given their speed and this variable. */ diff --git a/MapboxCoreNavigation/DistanceFormatter.swift b/MapboxCoreNavigation/DistanceFormatter.swift index 5281fa144fe..f0ecea4bfe6 100644 --- a/MapboxCoreNavigation/DistanceFormatter.swift +++ b/MapboxCoreNavigation/DistanceFormatter.swift @@ -28,14 +28,6 @@ public class DistanceFormatter: LengthFormatter { let nonFractionalLengthFormatter = LengthFormatter() - var usesMetric: Bool { - let locale = numberFormatter.locale as NSLocale - guard let measurementSystem = locale.object(forKey: .measurementSystem) as? String else { - return false - } - return measurementSystem == "Metric" - } - /** Intializes a new `DistanceFormatter`. @@ -57,7 +49,7 @@ public class DistanceFormatter: LengthFormatter { } func maximumFractionDigits(for distance: CLLocationDistance) -> Int { - if usesMetric { + if Locale.usesMetric { return distance < 3_000 ? 1 : 0 } else { return distance.miles < 3 ? 1 : 0 @@ -65,7 +57,7 @@ public class DistanceFormatter: LengthFormatter { } func roundingIncrement(for distance: CLLocationDistance, unit: LengthFormatter.Unit) -> Double { - if usesMetric { + if Locale.usesMetric { if distance < 25 { return 5 } else if distance < 100 { @@ -120,7 +112,7 @@ public class DistanceFormatter: LengthFormatter { func formattedDistance(_ distance: CLLocationDistance, modify unit: inout LengthFormatter.Unit) -> String { var formattedDistance: String - if usesMetric { + if Locale.usesMetric { let roundedDistance: CLLocationDistance = numberFormatter.number(from: numberFormatter.string(from: distance as NSNumber)!)?.doubleValue ?? distance numberFormatter.roundingIncrement = roundingIncrement(for: roundedDistance, unit: unit) as NSNumber diff --git a/MapboxCoreNavigation/Locale.swift b/MapboxCoreNavigation/Locale.swift index 60be4d8eb34..328dcba072d 100644 --- a/MapboxCoreNavigation/Locale.swift +++ b/MapboxCoreNavigation/Locale.swift @@ -24,4 +24,12 @@ extension Locale { Returns a `Locale` from `preferredLocalLanguageCountryCode`. */ public static var nationalizedCurrent = Locale(identifier: preferredLocalLanguageCountryCode) + + public static var usesMetric: Bool { + let locale = self.current as NSLocale + guard let measurementSystem = locale.object(forKey: .measurementSystem) as? String else { + return false + } + return measurementSystem == "Metric" + } } diff --git a/MapboxCoreNavigation/MBRouteController.h b/MapboxCoreNavigation/MBRouteController.h index 65411344f9b..f377fc8d2bd 100644 --- a/MapboxCoreNavigation/MBRouteController.h +++ b/MapboxCoreNavigation/MBRouteController.h @@ -5,15 +5,14 @@ extern NSString *const MBRouteControllerProgressDidChangeNotificationProgressKey extern NSString *const MBRouteControllerProgressDidChangeNotificationLocationKey; extern NSString *const MBRouteControllerProgressDidChangeNotificationSecondsRemainingOnStepKey; -extern NSString *const MBRouteControllerAlertLevelDidChangeNotificationRouteProgressKey; -extern NSString *const MBRouteControllerAlertLevelDidChangeNotificationDistanceToEndOfManeuverKey; +extern NSString *const MBRouteControllerDidPassSpokenInstructionPointRouteProgressKey; +extern NSString *const MBRouteControllerDidPassSpokenInstructionPoint; extern NSString *const MBRouteControllerNotificationLocationKey; extern NSString *const MBRouteControllerNotificationRouteKey; extern NSString *const MBRouteControllerNotificationErrorKey; extern NSString *const MBRouteControllerNotificationProgressDidChange; -extern NSString *const MBRouteControllerAlertLevelDidChange; extern NSString *const MBRouteControllerWillReroute; extern NSString *const MBRouteControllerDidReroute; extern NSString *const MBRouteControllerDidFailToReroute; diff --git a/MapboxCoreNavigation/MBRouteController.m b/MapboxCoreNavigation/MBRouteController.m index 2704cbfae73..0154e9869b0 100644 --- a/MapboxCoreNavigation/MBRouteController.m +++ b/MapboxCoreNavigation/MBRouteController.m @@ -5,15 +5,14 @@ NSString *const MBRouteControllerProgressDidChangeNotificationLocationKey = @"location"; NSString *const MBRouteControllerProgressDidChangeNotificationSecondsRemainingOnStepKey = @"seconds"; -NSString *const MBRouteControllerAlertLevelDidChangeNotificationRouteProgressKey = @"progress"; -NSString *const MBRouteControllerAlertLevelDidChangeNotificationDistanceToEndOfManeuverKey = @"distance"; +NSString *const MBRouteControllerDidPassSpokenInstructionPointRouteProgressKey = @"progress"; NSString *const MBRouteControllerNotificationLocationKey = @"location"; NSString *const MBRouteControllerNotificationRouteKey = @"route"; NSString *const MBRouteControllerNotificationErrorKey = @"error"; NSString *const MBRouteControllerNotificationProgressDidChange = @"RouteControllerProgressDidChange"; -NSString *const MBRouteControllerAlertLevelDidChange = @"RouteControllerAlertLevelDidChange"; +NSString *const MBRouteControllerDidPassSpokenInstructionPoint = @"RouteControllerDidPassSpokenInstructionPoint"; NSString *const MBRouteControllerWillReroute = @"RouteControllerWillReroute"; NSString *const MBRouteControllerDidReroute = @"RouteControllerDidReroute"; NSString *const MBRouteControllerDidFailToReroute = @"RouteControllerDidFailToReroute"; diff --git a/MapboxCoreNavigation/NavigationRouteOptions.swift b/MapboxCoreNavigation/NavigationRouteOptions.swift index de2498b3995..17c2783c7a5 100644 --- a/MapboxCoreNavigation/NavigationRouteOptions.swift +++ b/MapboxCoreNavigation/NavigationRouteOptions.swift @@ -3,15 +3,15 @@ import MapboxDirections /** A `NavigationRouteOptions` object specifies turn-by-turn-optimized criteria for results returned by the Mapbox Directions API. - + `NavigationRouteOptions` is a subclass of `RouteOptions` that has been optimized for navigation. Pass an instance of this class into the `Directions.calculate(_:completionHandler:)` method. */ @objc(MBNavigationRouteOptions) open class NavigationRouteOptions: RouteOptions { - + /** Initializes a navigation route options object for routes between the given waypoints and an optional profile identifier optimized for navigation. - + - SeeAlso: [RouteOptions](https://www.mapbox.com/mapbox-navigation-ios/directions/0.10.1/Classes/RouteOptions.html) */ @@ -23,31 +23,32 @@ open class NavigationRouteOptions: RouteOptions { includesSteps = true routeShapeResolution = .full attributeOptions = [.congestionLevel, .expectedTravelTime] - includesExitRoundaboutManeuver = true + includesSpokenInstructions = true + distanceMeasurementSystem = Locale.current.usesMetricSystem ? .metric : .imperial } - - + + /** Initializes a navigation route options object for routes between the given locations and an optional profile identifier optimized for navigation. - + - SeeAlso: [RouteOptions](https://www.mapbox.com/mapbox-navigation-ios/directions/0.10.1/Classes/RouteOptions.html) */ public convenience init(locations: [CLLocation], profileIdentifier: MBDirectionsProfileIdentifier? = .automobileAvoidingTraffic) { self.init(waypoints: locations.map { Waypoint(location: $0) }, profileIdentifier: profileIdentifier) } - - + + /** Initializes a route options object for routes between the given geographic coordinates and an optional profile identifier optimized for navigation. - + - SeeAlso: [RouteOptions](https://www.mapbox.com/mapbox-navigation-ios/directions/0.10.1/Classes/RouteOptions.html) */ public convenience init(coordinates: [CLLocationCoordinate2D], profileIdentifier: MBDirectionsProfileIdentifier? = .automobileAvoidingTraffic) { self.init(waypoints: coordinates.map { Waypoint(coordinate: $0) }, profileIdentifier: profileIdentifier) } - + public required init?(coder decoder: NSCoder) { super.init(coder: decoder) } diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index ac7c368a760..10ecd16e956 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -13,69 +13,69 @@ import Turf public protocol RouteControllerDelegate: class { /** Returns whether the route controller should be allowed to calculate a new route. - + If implemented, this method is called as soon as the route controller detects that the user is off the predetermined route. Implement this method to conditionally prevent rerouting. If this method returns `true`, `routeController(_:willRerouteFrom:)` will be called immediately afterwards. - + - parameter routeController: The route controller that has detected the need to calculate a new route. - parameter location: The user’s current location. - returns: True to allow the route controller to calculate a new route; false to keep tracking the current route. */ @objc(routeController:shouldRerouteFromLocation:) optional func routeController(_ routeController: RouteController, shouldRerouteFrom location: CLLocation) -> Bool - + @objc(routeController:shouldIncrementLegWhenArrivingAtWaypoint:) optional func routeController(_ routeController: RouteController, shouldIncrementLegWhenArrivingAtWaypoint waypoint: Waypoint) -> Bool - + /** Called immediately before the route controller calculates a new route. - + This method is called after `routeController(_:shouldRerouteFrom:)` is called, simultaneously with the `RouteControllerWillReroute` notification being posted, and before `routeController(_:didRerouteAlong:)` is called. - + - parameter routeController: The route controller that will calculate a new route. - parameter location: The user’s current location. */ @objc(routeController:willRerouteFromLocation:) optional func routeController(_ routeController: RouteController, willRerouteFrom location: CLLocation) - + /** Called when a location has been discarded for being inaccurate. - + See `CLLocation.isQualified` for more information about what qualifies a location. - + - parameter routeController: The route controller that discarded the location. - parameter location: The location that was discarded */ @objc(routeController:didDiscardLocation:) optional func routeController(_ routeController: RouteController, didDiscard location: CLLocation) - + /** Called immediately after the route controller receives a new route. - + This method is called after `routeController(_:willRerouteFrom:)` and simultaneously with the `RouteControllerDidReroute` notification being posted. - + - parameter routeController: The route controller that has calculated a new route. - parameter route: The new route. */ @objc(routeController:didRerouteAlongRoute:) optional func routeController(_ routeController: RouteController, didRerouteAlong route: Route) - + /** Called when the route controller fails to receive a new route. - + This method is called after `routeController(_:willRerouteFrom:)` and simultaneously with the `RouteControllerDidFailToReroute` notification being posted. - + - parameter routeController: The route controller that has calculated a new route. - parameter error: An error raised during the process of obtaining a new route. */ @objc(routeController:didFailToRerouteWithError:) optional func routeController(_ routeController: RouteController, didFailToRerouteWith error: Error) - + /** Called when the route controller’s location manager receive a location update. - + These locations can be modified due to replay or simulation but they can also derive from regular location updates from a `CLLocationManager`. - + - parameter routeController: The route controller that received the new locations. - parameter locations: The locations that were received from the associated location manager. */ @@ -85,25 +85,24 @@ public protocol RouteControllerDelegate: class { /** A `RouteController` tracks the user’s progress along a route, posting notifications as the user reaches significant points along the route. On every location update, the route controller evaluates the user’s location, determining whether the user remains on the route. If not, the route controller calculates a new route. - - `RouteController` is responsible for the core navigation logic whereas + + `RouteController` is responsible for the core navigation logic whereas `NavigationViewController` is responsible for displaying a default drop-in navigation UI. */ @objc(MBRouteController) open class RouteController: NSObject { - let events = MMEEventsManager.shared() - + /** The route controller’s delegate. */ public weak var delegate: RouteControllerDelegate? - + /** The Directions object used to create the route. */ public var directions: Directions - + /** The route controller’s associated location manager. */ @@ -113,13 +112,13 @@ open class RouteController: NSObject { locationManager.delegate = self } } - + /** If true, location updates will be simulated when driving through tunnels or other areas where there is none or bad GPS reception. */ public var isDeadReckoningEnabled = false - - + + /** If true, every 2 minutes the `RouteController` will check for a faster route for the user. */ @@ -131,9 +130,9 @@ open class RouteController: NSObject { */ public var allowRecordedAudioFeedback = false - + var didFindFasterRoute = false - + /** Details about the user’s progress along the current route, leg, and step. */ @@ -149,7 +148,7 @@ open class RouteController: NSObject { } else { sessionState.currentRoute = routeProgress.route } - + var userInfo = [String: Any]() if let location = locationManager.location { userInfo[MBRouteControllerNotificationLocationKey] = location @@ -158,26 +157,26 @@ open class RouteController: NSObject { NotificationCenter.default.post(name: RouteControllerDidReroute, object: self, userInfo: userInfo) } } - + var isRerouting = false var lastRerouteLocation: CLLocation? - + var routeTask: URLSessionDataTask? var lastLocationDate: Date? - + /// :nodoc: This is used internally when the navigation UI is being used public var usesDefaultUserInterface = false - + var sessionState:SessionState var outstandingFeedbackEvents = [CoreFeedbackEvent]() - + var hasFoundOneQualifiedLocation = false - + var recentDistancesFromManeuver: [CLLocationDistance] = [] - + /** Intializes a new `RouteController`. - + - parameter route: The route to follow. - parameter directions: The Directions object that created `route`. - parameter locationManager: The associated location manager. @@ -191,16 +190,16 @@ open class RouteController: NSObject { self.locationManager.activityType = route.routeOptions.activityType UIDevice.current.isBatteryMonitoringEnabled = true super.init() - + self.locationManager.delegate = self self.resumeNotifications() self.resetSession() - + DispatchQueue.main.async { self.startEvents(route: route) } } - + deinit { suspendLocationUpdates() checkAndSendOutstandingFeedbackEvents(forceAll: true) @@ -208,10 +207,10 @@ open class RouteController: NSObject { suspendNotifications() UIDevice.current.isBatteryMonitoringEnabled = false } - + func startEvents(route: Route) { let eventLoggingEnabled = UserDefaults.standard.bool(forKey: NavigationMetricsDebugLoggingEnabled) - + var mapboxAccessToken: String? = nil if let accessToken = route.accessToken { mapboxAccessToken = accessToken @@ -220,7 +219,7 @@ open class RouteController: NSObject { let token = dict["MGLMapboxAccessToken"] as? String { mapboxAccessToken = token } - + if let mapboxAccessToken = mapboxAccessToken { events.isDebugLoggingEnabled = eventLoggingEnabled events.isMetricsEnabledInSimulator = true @@ -233,28 +232,28 @@ open class RouteController: NSObject { assert(false, "`accessToken` must be set in the Info.plist as `MGLMapboxAccessToken` or the `Route` passed into the `RouteController` must have the `accessToken` property set.") } } - + func resumeNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(notification:)), name: RouteControllerProgressDidChange, object: self) - NotificationCenter.default.addObserver(self, selector: #selector(alertLevelDidChange(notification:)), name: RouteControllerAlertLevelDidChange, object: self) + NotificationCenter.default.addObserver(self, selector: #selector(didPassSpokenInstructionPoint(notification:)), name: RouteControllerDidPassSpokenInstructionPoint, object: self) NotificationCenter.default.addObserver(self, selector: #selector(willReroute(notification:)), name: RouteControllerWillReroute, object: self) NotificationCenter.default.addObserver(self, selector: #selector(didReroute(notification:)), name: RouteControllerDidReroute, object: self) } - + func suspendNotifications() { NotificationCenter.default.removeObserver(self) } - + /** Starts monitoring the user’s location along the route. - + Will continue monitoring until `suspendLocationUpdates()` is called. */ public func resume() { locationManager.startUpdatingLocation() locationManager.startUpdatingHeading() } - + /** Stops monitoring the user’s location along the route. */ @@ -262,41 +261,41 @@ open class RouteController: NSObject { locationManager.stopUpdatingLocation() locationManager.stopUpdatingHeading() } - + /** The most recently received user location. This is a raw location received from `locationManager`. To obtain an idealized location, use the `location` property. */ var rawLocation: CLLocation? - + public var reroutingTolerance: CLLocationDistance { guard let intersections = routeProgress.currentLegProgress.currentStepProgress.intersectionsIncludingUpcomingManeuverIntersection else { return RouteControllerMaximumDistanceBeforeRecalculating } guard let userLocation = rawLocation else { return RouteControllerMaximumDistanceBeforeRecalculating } - + for intersection in intersections { let absoluteDistanceToIntersection = userLocation.coordinate.distance(to: intersection.location) - + if absoluteDistanceToIntersection <= RouteControllerManeuverZoneRadius { return RouteControllerMaximumDistanceBeforeRecalculating / 2 } } return RouteControllerMaximumDistanceBeforeRecalculating } - + /** The most recently received user location, snapped to the route line. - + This property contains a `CLLocation` object located along the route line near the most recently received user location. This property is set to `nil` if the route controller is unable to snap the user’s location to the route line for some reason. */ public var location: CLLocation? { guard let location = rawLocation else { return nil } guard let stepCoordinates = routeProgress.currentLegProgress.currentStep.coordinates else { return nil } guard let snappedCoordinate = Polyline(stepCoordinates).closestCoordinate(to: location.coordinate) else { return location } - + var nearByCoordinates = routeProgress.currentLegProgress.nearbyCoordinates let nearByPolyline = Polyline(nearByCoordinates) - + // 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 if let upcomingStep = routeProgress.currentLegProgress.upComingStep, @@ -308,16 +307,16 @@ open class RouteController: NSObject { } guard let closest = Polyline(nearByCoordinates).closestCoordinate(to: location.coordinate) else { return nil } - + let slicedLineBehind = Polyline(nearByCoordinates.reversed()).sliced(from: closest.coordinate, to: nearByCoordinates.reversed().last) let slicedLineInFront = Polyline(nearByCoordinates).sliced(from: closest.coordinate, to: nearByCoordinates.last) let userDistanceBuffer: CLLocationDistance = max(location.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 pointAhead = slicedLineInFront.coordinateFromStart(distance: userDistanceBuffer) else { return nil } guard let pointAheadClosest = nearByPolyline.closestCoordinate(to: pointAhead) else { return nil } - + // Get direction of these points let pointBehindDirection = pointBehindClosest.coordinate.direction(to: closest.coordinate) let pointAheadDirection = closest.coordinate.direction(to: pointAheadClosest.coordinate) @@ -344,27 +343,27 @@ open class RouteController: NSObject { return CLLocation(coordinate: userCoordinate, altitude: location.altitude, horizontalAccuracy: location.horizontalAccuracy, verticalAccuracy: location.verticalAccuracy, course: userCourse, speed: location.speed, timestamp: location.timestamp) } - + /** Send feedback about the current road segment/maneuver to the Mapbox data team. - + You can pair this with a custom feedback UI in your app to flag problems during navigation such as road closures, incorrect instructions, etc. - + @param type A `FeedbackType` used to specify the type of feedback @param description A custom string used to describe the problem in detail. @return Returns a UUID string used to identify the feedback event - - If you provide a custom feedback UI that lets users elaborate on an issue, you should call this before you show the custom UI so the location and timestamp are more accurate. - + + If you provide a custom feedback UI that lets users elaborate on an issue, you should call this before you show the custom UI so the location and timestamp are more accurate. + You can then call `updateFeedback(feedbackId:)` with the returned feedback ID string to attach any additional metadata to the feedback. */ public func recordFeedback(type: FeedbackType = .general, description: String? = nil) -> String { return enqueueFeedbackEvent(type: type, description: description) } - + /** Update the feedback event with a specific feedback ID. If you implement a custom feedback UI that lets a user elaborate on an issue, you can use this to update the metadata. - + Note that feedback is sent 20 seconds after being recorded, so you should promptly update the feedback metadata after the user discards any feedback UI. */ public func updateFeedback(feedbackId: String, type: FeedbackType, description: String?, audio: Data?) { @@ -372,7 +371,7 @@ open class RouteController: NSObject { lastFeedback.update(type: type, description: description, audio: audio) } } - + /** Discard a recorded feedback event, for example if you have a custom feedback UI and the user cancelled feedback. */ @@ -391,20 +390,19 @@ extension RouteController { } checkAndSendOutstandingFeedbackEvents(forceAll: false) } - - func alertLevelDidChange(notification: NSNotification) { - let alertLevel = routeProgress.currentLegProgress.alertUserLevel - if alertLevel == .arrive && sessionState.arrivalTimestamp == nil { + + func didPassSpokenInstructionPoint(notification: NSNotification) { + if routeProgress.currentLegProgress.userHasArrivedAtWaypoint && sessionState.arrivalTimestamp == nil { sessionState.arrivalTimestamp = Date() sendArriveEvent() } recentDistancesFromManeuver.removeAll() } - + func willReroute(notification: NSNotification) { _ = enqueueRerouteEvent() } - + func didReroute(notification: NSNotification) { if let lastReroute = outstandingFeedbackEvents.map({$0 as? RerouteEvent }).last { lastReroute?.update(newRoute: routeProgress.route) @@ -414,23 +412,23 @@ extension RouteController { } extension RouteController: CLLocationManagerDelegate { - + func interpolateLocation() { guard let location = locationManager.lastKnownLocation else { return } guard let coordinates = routeProgress.route.coordinates else { return } let polyline = Polyline(coordinates) - + let distance = location.speed as CLLocationDistance - + guard let interpolatedCoordinate = polyline.coordinateFromStart(distance: routeProgress.distanceTraveled+distance) else { return } - + var course = location.course if let upcomingCoordinate = polyline.coordinateFromStart(distance: routeProgress.distanceTraveled+(distance*2)) { course = interpolatedCoordinate.direction(to: upcomingCoordinate) } - + let interpolatedLocation = CLLocation(coordinate: interpolatedCoordinate, altitude: location.altitude, horizontalAccuracy: location.horizontalAccuracy, @@ -438,22 +436,22 @@ extension RouteController: CLLocationManagerDelegate { course: course, speed: location.speed, timestamp: Date()) - + self.locationManager(self.locationManager, didUpdateLocations: [interpolatedLocation]) } - + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } self.rawLocation = location - + sessionState.pastLocations.push(location) - + if location.isQualified { hasFoundOneQualifiedLocation = true } - + if !location.isQualified && hasFoundOneQualifiedLocation { delegate?.routeController?(self, didDiscard: location) return @@ -462,16 +460,16 @@ extension RouteController: CLLocationManagerDelegate { delegate?.routeController?(self, didUpdate: [location]) NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(interpolateLocation), object: nil) - + if isDeadReckoningEnabled { perform(#selector(interpolateLocation), with: nil, afterDelay: 1.1) } - + let polyline = Polyline(routeProgress.currentLegProgress.currentStep.coordinates!) let userSnapToStepDistanceFromManeuver = polyline.distance(from: location.coordinate) let secondsToEndOfStep = userSnapToStepDistanceFromManeuver / location.speed - - guard routeProgress.currentLegProgress.alertUserLevel != .arrive, + + guard !routeProgress.currentLegProgress.userHasArrivedAtWaypoint, routeProgress.remainingWaypoints.count > 0 else { NotificationCenter.default.post(name: RouteControllerProgressDidChange, object: self, userInfo: [ RouteControllerProgressDidChangeNotificationProgressKey: routeProgress, @@ -480,7 +478,7 @@ extension RouteController: CLLocationManagerDelegate { ]) return } - + // Notify observers if the step’s remaining distance has changed. let currentStepProgress = routeProgress.currentLegProgress.currentStepProgress let currentStep = currentStepProgress.step @@ -496,15 +494,14 @@ extension RouteController: CLLocationManagerDelegate { ]) } } - guard userIsOnRoute(location) || !(delegate?.routeController?(self, shouldRerouteFrom: location) ?? true) else { reroute(from: location) return } - + updateDistanceToIntersection(from: location) monitorStepProgress(location) - + // Check for faster route given users current location guard checkForFasterRouteInBackground else { return } // Only check for faster alternatives if the user has plenty of time left on the route. @@ -516,7 +513,7 @@ extension RouteController: CLLocationManagerDelegate { /** Given a users current location, returns a Boolean whether they are currently on the route. - + If the user is not on the route, they should be rerouted. */ public func userIsOnRoute(_ location: CLLocation) -> Bool { @@ -527,8 +524,8 @@ extension RouteController: CLLocationManagerDelegate { let newLocation = CLLocation(latitude: locationInfrontOfUser.latitude, longitude: locationInfrontOfUser.longitude) let radius = max(reroutingTolerance, location.horizontalAccuracy + RouteControllerUserLocationSnappingDistance) let isCloseToCurrentStep = newLocation.isWithin(radius, of: routeProgress.currentLegProgress.currentStep) - - + + // Check to see if the user is moving away from the maneuver. // Here, we store an array of distances. If the current distance is greater than the last distance, // add it to the array. If the array grows larger than x, reroute the user. @@ -539,7 +536,7 @@ extension RouteController: CLLocationManagerDelegate { if recentDistancesFromManeuver.count > RouteControllerMinimumNumberLocationUpdatesBackwards && recentDistancesFromManeuver.last! - recentDistancesFromManeuver.first! > RouteControllerMinimumBacktrackingDistanceForRerouting { return false } - + if recentDistancesFromManeuver.isEmpty { recentDistancesFromManeuver.append(userDistanceToManeuver) } else if let lastDistance = recentDistancesFromManeuver.last, userDistanceToManeuver > lastDistance { @@ -549,7 +546,7 @@ extension RouteController: CLLocationManagerDelegate { recentDistancesFromManeuver.removeAll() } } - + // If the user is moving away from the maneuver location // and they are close to the upcoming or follow on step, // we can safely say they have completed the maneuver. @@ -558,55 +555,24 @@ extension RouteController: CLLocationManagerDelegate { if let upComingStep = routeProgress.currentLegProgress.upComingStep { let isCloseToUpComingStep = newLocation.isWithin(radius, of: upComingStep) if !isCloseToCurrentStep && isCloseToUpComingStep { - incrementRouteProgress(newAlert(from: upComingStep), location: location, updateStepIndex: true) + incrementRouteProgress() return true } } if let followOnStep = routeProgress.currentLegProgress.followOnStep { let isCloseToUpComingStep = newLocation.isWithin(radius, of: followOnStep) if !isCloseToCurrentStep && isCloseToUpComingStep { - incrementRouteProgress(newAlert(from: followOnStep), location: location, updateStepIndex: true) + incrementRouteProgress() return true } } - + return isCloseToCurrentStep } - - func incrementRouteProgress(_ newlyCalculatedAlertLevel: AlertLevel, location: CLLocation, updateStepIndex: Bool) { - if updateStepIndex { - routeProgress.currentLegProgress.stepIndex += 1 - } - - // If the step is not being updated, don't accept a lower alert level. - // A lower alert level can only occur when the user begins the next step. - guard newlyCalculatedAlertLevel.rawValue > routeProgress.currentLegProgress.alertUserLevel.rawValue || updateStepIndex else { - return - } - - if routeProgress.currentLegProgress.alertUserLevel != newlyCalculatedAlertLevel || updateStepIndex { - routeProgress.currentLegProgress.alertUserLevel = newlyCalculatedAlertLevel - - // Use fresh user location distance to end of step - // since the step could of changed - let userDistance = Polyline(routeProgress.currentLegProgress.currentStep.coordinates!).distance(from: location.coordinate) - - NotificationCenter.default.post(name: RouteControllerAlertLevelDidChange, object: self, userInfo: [ - RouteControllerAlertLevelDidChangeNotificationRouteProgressKey: routeProgress, - RouteControllerAlertLevelDidChangeNotificationDistanceToEndOfManeuverKey: userDistance - ]) - - if routeProgress.currentLegProgress.alertUserLevel == .arrive, - routeProgress.remainingWaypoints.count > 1, - (delegate?.routeController?(self, shouldIncrementLegWhenArrivingAtWaypoint: routeProgress.currentLeg.destination) ?? true) { - routeProgress.legIndex += 1 - } - } - } - + func checkForFasterRoute(from location: CLLocation) { guard let currentUpcomingManeuver = routeProgress.currentLegProgress.upComingStep else { return } - + guard let lastLocationDate = lastLocationDate else { self.lastLocationDate = location.timestamp return @@ -615,26 +581,25 @@ extension RouteController: CLLocationManagerDelegate { // Only check ever 2 minutes for faster route guard location.timestamp.timeIntervalSince(lastLocationDate) >= 120 else { return } let durationRemaining = routeProgress.durationRemaining - let currentAlertLevel = routeProgress.currentLegProgress.alertUserLevel - + getDirections(from: location) { [weak self] (route, error) in guard let strongSelf = self else { return } guard let route = route else { return } strongSelf.lastLocationDate = nil - + if let firstLeg = route.legs.first, let firstStep = firstLeg.steps.first, firstStep.expectedTravelTime >= RouteControllerMediumAlertInterval, currentUpcomingManeuver == firstLeg.steps[1], route.expectedTravelTime <= 0.9 * durationRemaining { strongSelf.didFindFasterRoute = true // If the upcoming maneuver in the new route is the same as the current upcoming maneuver, don't announce it - strongSelf.routeProgress = RouteProgress(route: route, legIndex: 0, alertLevel: currentAlertLevel) + strongSelf.routeProgress = RouteProgress(route: route, legIndex: 0, spokenInstructionIndex: strongSelf.routeProgress.currentLegProgress.currentStepProgress.spokenInstructionIndex) strongSelf.delegate?.routeController?(strongSelf, didRerouteAlong: route) strongSelf.didFindFasterRoute = false } } } - + func reroute(from location: CLLocation) { if let lastRerouteLocation = lastRerouteLocation { guard location.distance(from: lastRerouteLocation) >= RouteControllerMaximumDistanceBeforeRecalculating else { @@ -645,57 +610,52 @@ extension RouteController: CLLocationManagerDelegate { if isRerouting { return } - + isRerouting = true delegate?.routeController?(self, willRerouteFrom: location) NotificationCenter.default.post(name: RouteControllerWillReroute, object: self, userInfo: [ MBRouteControllerNotificationLocationKey: location ]) - + self.lastRerouteLocation = location - + getDirections(from: location) { [weak self] (route, error) in guard let strongSelf = self else { return } - + if let error = error { strongSelf.delegate?.routeController?(strongSelf, didFailToRerouteWith: error) NotificationCenter.default.post(name: RouteControllerDidFailToReroute, object: self, userInfo: [ MBRouteControllerNotificationErrorKey: error ]) } - + guard let route = route else { return } - - // If the first step of the new route is greater than 0.5km, let user continue without announcement. - var alertLevel: AlertLevel = .none - if let firstLeg = route.legs.first, let firstStep = firstLeg.steps.first, firstStep.distance > 500 { - alertLevel = .depart - } - strongSelf.routeProgress = RouteProgress(route: route, legIndex: 0, alertLevel: alertLevel) + + strongSelf.routeProgress = RouteProgress(route: route, legIndex: 0) strongSelf.routeProgress.currentLegProgress.stepIndex = 0 strongSelf.delegate?.routeController?(strongSelf, didRerouteAlong: route) } } - + func getDirections(from location: CLLocation, completion: @escaping (_ route: Route?, _ error: Error?)->()) { routeTask?.cancel() - + let options = routeProgress.route.routeOptions options.waypoints = [Waypoint(coordinate: location.coordinate)] + routeProgress.remainingWaypoints if let firstWaypoint = options.waypoints.first, location.course >= 0 { firstWaypoint.heading = location.course firstWaypoint.headingAccuracy = 90 } - + self.lastRerouteLocation = location - + if let accessToken = routeProgress.route.accessToken, let apiEndpoint = routeProgress.route.apiEndpoint, let host = apiEndpoint.host { directions = Directions(accessToken: accessToken, host: host) } - + routeTask = directions.calculate(options) { [weak self] (waypoints, routes, error) in defer { self?.isRerouting = false @@ -706,38 +666,32 @@ extension RouteController: CLLocationManagerDelegate { guard let route = routes?.first else { return completion(nil, nil) } - + return completion(route, error) } } - + func updateDistanceToIntersection(from location: CLLocation) { guard var intersections = routeProgress.currentLegProgress.currentStepProgress.step.intersections else { return } let currentStepProgress = routeProgress.currentLegProgress.currentStepProgress - + // The intersections array does not include the upcoming maneuver intersection. if let upcomingStep = routeProgress.currentLegProgress.upComingStep, let upcomingIntersection = upcomingStep.intersections, let firstUpcomingIntersection = upcomingIntersection.first { intersections += [firstUpcomingIntersection] } - + 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) } } - + func monitorStepProgress(_ location: CLLocation) { - // Force an announcement when the user begins a route - var alertLevel: AlertLevel = routeProgress.currentLegProgress.alertUserLevel == .none ? .depart : routeProgress.currentLegProgress.alertUserLevel - var updateStepIndex = false - + let userSnapToStepDistanceFromManeuver = Polyline(routeProgress.currentLegProgress.currentStep.coordinates!).distance(from: location.coordinate) - let secondsToEndOfStep = routeProgress.currentLegProgress.currentStepProgress.durationRemaining var courseMatchesManeuverFinalHeading = false - let outletRoadClasses = routeProgress.currentLegProgress.currentStepProgress.step.intersections?.first?.outletRoadClasses - // Bearings need to normalized so when the `finalHeading` is 359 and the user heading is 1, // we count this as within the `RouteControllerMaximumAllowedDegreeOffsetForTurnCompletion` if let upcomingStep = routeProgress.currentLegProgress.upComingStep, let finalHeading = upcomingStep.finalHeading, let initialHeading = upcomingStep.initialHeading { @@ -745,7 +699,7 @@ extension RouteController: CLLocationManagerDelegate { let finalHeadingNormalized = finalHeading.wrap(min: 0, max: 360) let userHeadingNormalized = location.course.wrap(min: 0, max: 360) let expectedTurningAngle = initialHeadingNormalized.differenceBetween(finalHeadingNormalized) - + // If the upcoming maneuver is fairly straight, // do not check if the user is within x degrees of the exit heading. // For ramps, their current heading will very close to the exit heading. @@ -759,60 +713,49 @@ extension RouteController: CLLocationManagerDelegate { } } - // When departing, `userSnapToStepDistanceFromManeuver` is most often less than `RouteControllerManeuverZoneRadius` - // since the user will most often be at the beginning of the route, in the maneuver zone - if alertLevel == .depart && userSnapToStepDistanceFromManeuver <= RouteControllerManeuverZoneRadius { - // If the user is close to the maneuver location, - // don't give a depature instruction. - // Instead, give a `.high` alert. - if secondsToEndOfStep <= RouteControllerHighAlertInterval { - alertLevel = .high - } - } else if let _ = outletRoadClasses?.contains(.motorway), - routeProgress.currentLegProgress.currentStepProgress.distanceRemaining <= RouteControllerMotorwayHighAlertDistance { - alertLevel = .high - } else if let _ = outletRoadClasses?.contains(.motorway), - routeProgress.currentLegProgress.currentStepProgress.distanceRemaining <= RouteControllerMotorwayMediumAlertDistance { - alertLevel = .medium - } else if userSnapToStepDistanceFromManeuver <= RouteControllerManeuverZoneRadius { - // Use the currentStep if there is not a next step - // This occurs when arriving - let step = routeProgress.currentLegProgress.upComingStep?.maneuverLocation ?? routeProgress.currentLegProgress.currentStep.maneuverLocation - - let userAbsoluteDistance = step.distance(to: location.coordinate) - let lastKnownUserAbsoluteDistance = routeProgress.currentLegProgress.currentStepProgress.userDistanceToManeuverLocation - + let step = routeProgress.currentLegProgress.upComingStep?.maneuverLocation ?? routeProgress.currentLegProgress.currentStep.maneuverLocation + let userAbsoluteDistance = step.distance(to: location.coordinate) + let lastKnownUserAbsoluteDistance = routeProgress.currentLegProgress.currentStepProgress.userDistanceToManeuverLocation + + if userSnapToStepDistanceFromManeuver <= RouteControllerManeuverZoneRadius { if routeProgress.currentLegProgress.upComingStep?.maneuverType == ManeuverType.arrive { - alertLevel = .arrive + routeProgress.currentLegProgress.userHasArrivedAtWaypoint = true } else if courseMatchesManeuverFinalHeading || (userAbsoluteDistance > lastKnownUserAbsoluteDistance && lastKnownUserAbsoluteDistance > RouteControllerManeuverZoneRadius) { - updateStepIndex = true + incrementRouteProgress() + } + } + + routeProgress.currentLegProgress.currentStepProgress.userDistanceToManeuverLocation = userAbsoluteDistance + + guard let spokenInstructions = routeProgress.currentLegProgress.currentStep.instructionsSpokenAlongStep else { + print("The directions request was made without `includesVoiceInstructions` enabled. This will prevent users from getting voice instructions. It's recommended to make your directions request via `NavigationRouteOptions()`.") + return + } + + for (voiceInstructionIndex, voiceInstruction) in spokenInstructions.enumerated() { + if userSnapToStepDistanceFromManeuver <= voiceInstruction.distanceAlongStep && voiceInstructionIndex >= routeProgress.currentLegProgress.currentStepProgress.spokenInstructionIndex { - // Look at the following step to determine what the new alert level should be - if let upComingStep = routeProgress.currentLegProgress.upComingStep { - alertLevel = newAlert(from: upComingStep) - } else { - assert(false, "In this case, there should always be an upcoming step") + if voiceInstructionIndex == spokenInstructions.count - 1 && routeProgress.currentLegProgress.upComingStep?.maneuverType == ManeuverType.arrive { + routeProgress.currentLegProgress.userHasArrivedAtWaypoint = true } + + NotificationCenter.default.post(name: RouteControllerDidPassSpokenInstructionPoint, object: self, userInfo: [ + MBRouteControllerDidPassSpokenInstructionPointRouteProgressKey: routeProgress + ]) + + routeProgress.currentLegProgress.currentStepProgress.spokenInstructionIndex += 1 + return } - - routeProgress.currentLegProgress.currentStepProgress.userDistanceToManeuverLocation = userAbsoluteDistance - } else if secondsToEndOfStep <= RouteControllerHighAlertInterval { - alertLevel = .high - } else if secondsToEndOfStep <= RouteControllerMediumAlertInterval { - alertLevel = .medium } - - incrementRouteProgress(alertLevel, location: location, updateStepIndex: updateStepIndex) } - - func newAlert(from upcomingStep: RouteStep) -> AlertLevel { - switch upcomingStep.expectedTravelTime { - case 0.. 1, + (delegate?.routeController?(self, shouldIncrementLegWhenArrivingAtWaypoint: routeProgress.currentLeg.destination) ?? true) { + routeProgress.legIndex += 1 } } } @@ -821,17 +764,17 @@ struct SessionState { let identifier = UUID() var departureTimestamp: Date? var arrivalTimestamp: Date? - + var totalDistanceCompleted: CLLocationDistance = 0 - + var numberOfReroutes = 0 var lastRerouteDate: Date? - + var currentRoute: Route var originalRoute: Route - + var pastLocations = FixedLengthQueue(length: 40) - + init(currentRoute: Route, originalRoute: Route) { self.currentRoute = currentRoute self.originalRoute = originalRoute @@ -850,51 +793,51 @@ extension RouteController { events.enqueueEvent(withName: MMEEventTypeNavigationArrive, attributes: events.navigationArriveEvent(routeController: self)) events.flush() } - + func sendCancelEvent() { events.enqueueEvent(withName: MMEEventTypeNavigationCancel, attributes: events.navigationCancelEvent(routeController: self)) events.flush() } - + func sendFeedbackEvent(event: CoreFeedbackEvent) { // remove from outstanding event queue if let index = outstandingFeedbackEvents.index(of: event) { outstandingFeedbackEvents.remove(at: index) } - + let eventName = event.eventDictionary["event"] as! String let eventDictionary = events.navigationFeedbackEventWithLocationsAdded(event: event, routeController: self) - + events.enqueueEvent(withName: eventName, attributes: eventDictionary) events.flush() } - + // MARK: Enqueue feedback - + func enqueueFeedbackEvent(type: FeedbackType, description: String?) -> String { let eventDictionary = events.navigationFeedbackEvent(routeController: self, type: type, description: description) let event = FeedbackEvent(timestamp: Date(), eventDictionary: eventDictionary) - + outstandingFeedbackEvents.append(event) - + return event.id.uuidString } - + func enqueueRerouteEvent() -> String { let timestamp = Date() - + let eventDictionary = events.navigationRerouteEvent(routeController: self) - + sessionState.lastRerouteDate = timestamp sessionState.numberOfReroutes += 1 - + let event = RerouteEvent(timestamp: Date(), eventDictionary: eventDictionary) outstandingFeedbackEvents.append(event) - + return event.id.uuidString } - + func checkAndSendOutstandingFeedbackEvents(forceAll: Bool) { let now = Date() let eventsToPush = forceAll ? outstandingFeedbackEvents : outstandingFeedbackEvents.filter { @@ -904,7 +847,7 @@ extension RouteController { sendFeedbackEvent(event: event) } } - + func resetSession() { sessionState = SessionState(currentRoute: routeProgress.route, originalRoute: routeProgress.route) } diff --git a/MapboxCoreNavigation/RouteProgress.swift b/MapboxCoreNavigation/RouteProgress.swift index 11a9143fcca..1e8550749b2 100644 --- a/MapboxCoreNavigation/RouteProgress.swift +++ b/MapboxCoreNavigation/RouteProgress.swift @@ -2,49 +2,6 @@ import Foundation import MapboxDirections -/** - An `AlertLevel` indicates the user’s general progress toward a step’s maneuver point. A change to the current alert level is often an opportunity to present the user with a visual or voice notification about the upcoming maneuver. - */ -@objc(MBAlertLevel) -public enum AlertLevel: Int { - - /** - Default `AlertLevel` - */ - case none - - - /** - The user has started the route. - */ - case depart - - - /** - The user has recently completed a step. - */ - case low - - - /** - The user is approaching the maneuver. - */ - case medium - - - /** - The user is at or very close to the maneuver point - */ - case high - - - /** - The user has completed the route. - */ - case arrive -} - - /** `RouteProgress` stores the user’s progress along a route. */ @@ -62,7 +19,6 @@ open class RouteProgress: NSObject { didSet { assert(legIndex >= 0 && legIndex < route.legs.endIndex) // TODO: Set stepIndex to 0 or last index based on whether leg index was incremented or decremented. - currentLegProgress.alertUserLevel = .none currentLegProgress = RouteLegProgress(leg: currentLeg) } } @@ -139,13 +95,14 @@ open class RouteProgress: NSObject { - parameter route: The route to follow. - parameter legIndex: Zero-based index indicating the current leg the user is on. - - parameter alertLevel: Optional `AlertLevel` to start the `RouteProgress` at. */ - public init(route: Route, legIndex: Int = 0, alertLevel: AlertLevel = .none) { + public init(route: Route, legIndex: Int = 0, spokenInstructionIndex: Int = 0) { self.route = route self.legIndex = legIndex super.init() - currentLegProgress = RouteLegProgress(leg: currentLeg, stepIndex: 0, alertLevel: alertLevel) + currentLegProgress = RouteLegProgress(leg: currentLeg, stepIndex: 0, spokenInstructionIndex: spokenInstructionIndex) + + for (legIndex, leg) in route.legs.enumerated() { var maneuverCoordinateIndex = 0 @@ -229,12 +186,8 @@ open class RouteLegProgress: NSObject { public var fractionTraveled: Double { return distanceTraveled / leg.distance } - - /** - `AlertLevel` for the current step. - */ - public var alertUserLevel: AlertLevel = .none - + + public var userHasArrivedAtWaypoint = false /** Returns the `RouteStep` before a given step. Returns `nil` if there is no step prior. @@ -326,13 +279,11 @@ open class RouteLegProgress: NSObject { - parameter leg: Leg on a `Route`. - parameter stepIndex: Current step the user is on. - - parameter alertLevel: Optional `AlertLevel` to start the `RouteProgress` at. */ - public init(leg: RouteLeg, stepIndex: Int = 0, alertLevel: AlertLevel = .none) { + public init(leg: RouteLeg, stepIndex: Int = 0, spokenInstructionIndex: Int = 0) { self.leg = leg self.stepIndex = stepIndex - self.alertUserLevel = alertLevel - currentStepProgress = RouteStepProgress(step: leg.steps[stepIndex]) + currentStepProgress = RouteStepProgress(step: leg.steps[stepIndex], spokenInstructionIndex: spokenInstructionIndex) } @@ -366,7 +317,6 @@ open class RouteStepProgress: NSObject { */ public var distanceTraveled: CLLocationDistance = 0 - /** Returns distance from user to end of step. */ @@ -383,6 +333,7 @@ open class RouteStepProgress: NSObject { Number between 0 and 1 representing fraction of current step traveled. */ public var fractionTraveled: Double { + guard step.distance > 0 else { return 1 } return distanceTraveled / step.distance } @@ -399,9 +350,10 @@ open class RouteStepProgress: NSObject { - parameter step: Step on a `RouteLeg`. */ - public init(step: RouteStep) { + public init(step: RouteStep, spokenInstructionIndex: Int = 0) { self.step = step self.intersectionIndex = 0 + self.spokenInstructionIndex = spokenInstructionIndex } /** @@ -434,4 +386,17 @@ open class RouteStepProgress: NSObject { The distance in meters the user is to the next intersection they will pass through. */ public var userDistanceToUpcomingIntersection: CLLocationDistance? + + /** + Index into `step.instructionsSpokenAlongStep` representing the current instruction. + */ + public var spokenInstructionIndex = 0 + + /** + Current Instruction for the user's progress along a step. + */ + public var currentSpokenInstruction: SpokenInstruction? { + guard let instructionsSpokenAlongStep = step.instructionsSpokenAlongStep else { return nil } + return instructionsSpokenAlongStep[spokenInstructionIndex] + } } diff --git a/MapboxCoreNavigation/SimulatedLocationManager.swift b/MapboxCoreNavigation/SimulatedLocationManager.swift index 760fde92c33..6e086832ada 100644 --- a/MapboxCoreNavigation/SimulatedLocationManager.swift +++ b/MapboxCoreNavigation/SimulatedLocationManager.swift @@ -78,7 +78,7 @@ public class SimulatedLocationManager: NavigationLocationManager { } @objc private func progressDidChange(_ notification: Notification) { - routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as? RouteProgress + routeProgress = notification.userInfo![MBRouteControllerDidPassSpokenInstructionPointRouteProgressKey] as? RouteProgress } @objc private func didReroute(_ notification: Notification) { diff --git a/MapboxCoreNavigation/SpokenDistanceFormatter.swift b/MapboxCoreNavigation/SpokenDistanceFormatter.swift deleted file mode 100644 index 7cd2226aaa5..00000000000 --- a/MapboxCoreNavigation/SpokenDistanceFormatter.swift +++ /dev/null @@ -1,86 +0,0 @@ -import CoreLocation - -/// Provides appropriately formatted, localized descriptions of linear distances for voice use. -@objc(MBSpokenDistanceFormatter) -public class SpokenDistanceFormatter: DistanceFormatter { - - - /** - Returns a more human readable `String` from a given `CLLocationDistance`. - - The user’s `Locale` is used here to set the units. - */ - public override func string(from distance: CLLocationDistance) -> String { - // British roads are measured in miles, yards, and feet. Simulate this idiosyncrasy using the U.S. locale. - let localeIdentifier = numberFormatter.locale.identifier - if localeIdentifier == "en-GB" || localeIdentifier == "en_GB" { - numberFormatter.locale = Locale(identifier: "en-US") - } - - var unit: LengthFormatter.Unit = .millimeter - unitString(fromMeters: distance, usedUnit: &unit) - let replacesYardsWithMiles = unit == .yard && distance.miles > 0.2 - let showsMixedFraction = (unit == .mile && distance.miles < 10) || replacesYardsWithMiles - - // An elaborate hack to use vulgar fractions with miles regardless of - // language. - if showsMixedFraction { - numberFormatter.positivePrefix = "|" - numberFormatter.positiveSuffix = "|" - numberFormatter.decimalSeparator = "!" - numberFormatter.alwaysShowsDecimalSeparator = true - } else { - numberFormatter.positivePrefix = "" - numberFormatter.positiveSuffix = "" - numberFormatter.decimalSeparator = nonFractionalLengthFormatter.numberFormatter.decimalSeparator - numberFormatter.alwaysShowsDecimalSeparator = nonFractionalLengthFormatter.numberFormatter.alwaysShowsDecimalSeparator - } - - numberFormatter.usesSignificantDigits = false - numberFormatter.maximumFractionDigits = maximumFractionDigits(for: distance) - numberFormatter.roundingIncrement = roundingIncrement(for: distance, unit: unit) as NSNumber - - var distanceString = formattedDistance(distance, modify: &unit) - - // Elaborate hack continued. - if showsMixedFraction { - var parts = distanceString.components(separatedBy: "|") - assert(parts.count == 3, "Positive format should’ve inserted two pipe characters.") - var numberParts = parts[1].components(separatedBy: "!") - assert(numberParts.count == 2, "Decimal separator should be present.") - let decimal = Int(numberParts[0]) - if let fraction = Double(".\(numberParts[1])0") { - let fourths = Int(round(fraction * 4)) - if fourths == fourths % 4 { - if decimal == 0 && fourths != 0 { - numberParts[0] = "" - } - var vulgarFractions = ["", "¼", "½", "¾"] - if Locale.current.languageCode == "en" { - vulgarFractions = ["", "a quarter", "a half", "3 quarters"] - if numberParts[0].isEmpty { - parts[2] = " \(unitString(fromValue: 1, unit: unit)) " - if fourths == 3 { - parts[2] = " of a\(parts[2])" - } - } - } - numberParts[1] = vulgarFractions[fourths] - if !numberParts[0].isEmpty && !numberParts[1].isEmpty { - numberParts[0] += " & " - } - parts[1] = numberParts.joined(separator: "") - } else { - parts[1] = numberParts.joined(separator: nonFractionalLengthFormatter.numberFormatter.decimalSeparator) - } - } else { - parts[1] = numberParts.joined(separator: nonFractionalLengthFormatter.numberFormatter.decimalSeparator) - } - distanceString = parts.joined(separator: "") - } - - return distanceString - } - - -} diff --git a/MapboxCoreNavigation/SpokenInstructionFormatter.swift b/MapboxCoreNavigation/SpokenInstructionFormatter.swift deleted file mode 100644 index 24921c4217c..00000000000 --- a/MapboxCoreNavigation/SpokenInstructionFormatter.swift +++ /dev/null @@ -1,156 +0,0 @@ -import Foundation -import CoreLocation -import OSRMTextInstructions - -/** - Formatter for creating speech strings. - */ -@objc(MBSpokenInstructionFormatter) -public class SpokenInstructionFormatter: NSObject { - - let routeStepFormatter = RouteStepFormatter() - let maneuverVoiceDistanceFormatter = SpokenDistanceFormatter(approximate: true) - - public override init() { - maneuverVoiceDistanceFormatter.unitStyle = .long - maneuverVoiceDistanceFormatter.numberFormatter.locale = .nationalizedCurrent - } - - /** - Creates a string used for announcing a step. - - If `markUpWithSSML` is true, the string will contain [SSML](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speech-synthesis-markup-language-ssml-reference). Your speech synthesizer should accept this type of string. See `PolleyVoiceController`. - */ - public func string(routeProgress: RouteProgress, userDistance: CLLocationDistance, markUpWithSSML: Bool) -> String { - let alertLevel = routeProgress.currentLegProgress.alertUserLevel - - let escapeIfNecessary = {(distance: String) -> String in - return markUpWithSSML ? distance.addingXMLEscapes : distance - } - - // If the current step arrives at a waypoint, the upcoming step is part of the next leg. - let upcomingLegIndex = routeProgress.currentLegProgress.currentStep.maneuverType == .arrive ? routeProgress.legIndex + 1 :routeProgress.legIndex - // Even if the next waypoint and the waypoint after that have the same coordinates, there will still be a step in between the two arrival steps. So the upcoming and follow-on steps are guaranteed to be part of the same leg. - let followOnLegIndex = upcomingLegIndex - - // Handle arriving at the final destination - // - let numberOfLegs = routeProgress.route.legs.count - guard let followOnInstruction = routeStepFormatter.string(for: routeProgress.currentLegProgress.followOnStep, legIndex: followOnLegIndex, numberOfLegs: numberOfLegs, markUpWithSSML: markUpWithSSML) else { - let upComingStepInstruction = routeStepFormatter.string(for: routeProgress.currentLegProgress.upComingStep, legIndex: upcomingLegIndex, numberOfLegs: numberOfLegs, markUpWithSSML: markUpWithSSML)! - var text: String - if alertLevel == .arrive { - text = upComingStepInstruction - } else { - let phrase = escapeIfNecessary(routeStepFormatter.instructions.phrase(named: .instructionWithDistance)) - text = phrase.replacingTokens { (tokenType) -> String in - switch tokenType { - case .firstInstruction: - return upComingStepInstruction - case .distance: - return escapeIfNecessary(maneuverVoiceDistanceFormatter.string(from: userDistance)) - default: - fatalError("Unexpected token \(tokenType)") - } - } - } - - return text - } - - // If there is no `upComingStep`, there definitely should not be a followOnStep. - // This should be caught above. - let upComingInstruction = routeStepFormatter.string(for: routeProgress.currentLegProgress.upComingStep, legIndex: upcomingLegIndex, numberOfLegs: numberOfLegs, markUpWithSSML: markUpWithSSML)! - let upcomingStepDuration = routeProgress.currentLegProgress.upComingStep!.expectedTravelTime - let currentInstruction = routeStepFormatter.string(for: routeProgress.currentLegProgress.currentStep, legIndex: routeProgress.legIndex, numberOfLegs: numberOfLegs, markUpWithSSML: markUpWithSSML) - let step = routeProgress.currentLegProgress.currentStep - var text: String - - // Prevent back to back instructions by adding a little more wiggle room - let linkedInstructionMultiplier = RouteControllerHighAlertInterval * RouteControllerLinkedInstructionBufferMultiplier - - // We only want to announce this special depature announcement once. - // Once it has been announced, all subsequnt announcements will not have an alert level of low - // since the user will be approaching the maneuver location. - let isStartingDeparture = routeProgress.currentLegProgress.currentStep.maneuverType == .depart && (alertLevel == .depart || alertLevel == .low) - if let currentInstruction = currentInstruction, isStartingDeparture { - if routeProgress.currentLegProgress.currentStep.distance > RouteControllerMinimumDistanceForContinueInstruction { - text = currentInstruction - } else if upcomingStepDuration > linkedInstructionMultiplier { - // If the upcoming step is an .exitRoundabout or .exitRotary, don't link the instruction - if let followOnStep = routeProgress.currentLegProgress.followOnStep, followOnStep.maneuverType == .exitRoundabout || followOnStep.maneuverType == .exitRotary { - text = upComingInstruction - } else { - let phrase = escapeIfNecessary(routeStepFormatter.instructions.phrase(named: .twoInstructionsWithDistance)) - text = phrase.replacingTokens { (tokenType) -> String in - switch tokenType { - case .firstInstruction: - return currentInstruction - case .secondInstruction: - return upComingInstruction - case .distance: - return maneuverVoiceDistanceFormatter.string(from: userDistance) - default: - fatalError("Unexpected token \(tokenType)") - } - } - } - } else { - text = upComingInstruction - } - } else if routeProgress.currentLegProgress.currentStep.distance > RouteControllerMinimumDistanceForContinueInstruction && routeProgress.currentLegProgress.alertUserLevel == .low { - if isStartingDeparture && upcomingStepDuration < linkedInstructionMultiplier { - let phrase = escapeIfNecessary(routeStepFormatter.instructions.phrase(named: .twoInstructionsWithDistance)) - text = phrase.replacingTokens { (tokenType) -> String in - switch tokenType { - case .firstInstruction: - return currentInstruction! - case .secondInstruction: - return upComingInstruction - case .distance: - return escapeIfNecessary(maneuverVoiceDistanceFormatter.string(from: userDistance)) - default: - fatalError("Unexpected token \(tokenType)") - } - } - } else if let roadDescription = step.roadDescription(markedUpWithSSML: markUpWithSSML) { - text = String.localizedStringWithFormat(NSLocalizedString("CONTINUE_ON_ROAD", bundle: .mapboxCoreNavigation, value: "Continue on %@ for %@", comment: "Format for speech string after completing a maneuver and starting a new step; 1 = way name; 2 = distance"), roadDescription, escapeIfNecessary(maneuverVoiceDistanceFormatter.string(from: userDistance))) - } else { - text = String.localizedStringWithFormat(NSLocalizedString("CONTINUE", bundle: .mapboxCoreNavigation, value: "Continue for %@", comment: "Format for speech string after completing a maneuver and starting a new step; 1 = distance"), escapeIfNecessary(maneuverVoiceDistanceFormatter.string(from: userDistance))) - } - } else if alertLevel == .high && upcomingStepDuration < linkedInstructionMultiplier { - // If the upcoming step is an .exitRoundabout or .exitRotary, don't link the instruction - if let followOnStep = routeProgress.currentLegProgress.followOnStep, followOnStep.maneuverType == .exitRoundabout || followOnStep.maneuverType == .exitRotary { - text = upComingInstruction - } else { - let phrase = escapeIfNecessary(routeStepFormatter.instructions.phrase(named: .twoInstructions)) - text = phrase.replacingTokens { (tokenType) -> String in - switch tokenType { - case .firstInstruction: - return upComingInstruction - case .secondInstruction: - return followOnInstruction - default: - fatalError("Unexpected token \(tokenType)") - } - } - } - } else if alertLevel != .high { - let phrase = escapeIfNecessary(routeStepFormatter.instructions.phrase(named: .instructionWithDistance)) - text = phrase.replacingTokens { (tokenType) -> String in - switch tokenType { - case .firstInstruction: - return upComingInstruction - case .distance: - return escapeIfNecessary(maneuverVoiceDistanceFormatter.string(from: userDistance)) - default: - fatalError("Unexpected token \(tokenType)") - } - } - } else { - text = upComingInstruction - } - - return text.removingPunctuation - } -} diff --git a/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift b/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift index 48a6883a3d1..9e717e5732b 100644 --- a/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift +++ b/MapboxCoreNavigationTests/MapboxCoreNavigationTests.swift @@ -3,12 +3,12 @@ import MapboxDirections import Turf @testable import MapboxCoreNavigation -let response = Fixture.JSONFromFileNamed(name: "route") +let response = Fixture.JSONFromFileNamed(name: "routeWithInstructions") 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 directions = Directions(accessToken: "pk.feedCafeDeadBeefBadeBede") -let route = Route(json: jsonRoute, waypoints: [waypoint1, waypoint2], routeOptions: RouteOptions(waypoints: [waypoint1, waypoint2])) +let route = Route(json: jsonRoute, waypoints: [waypoint1, waypoint2], routeOptions: NavigationRouteOptions(waypoints: [waypoint1, waypoint2])) let waitForInterval: TimeInterval = 5 @@ -21,13 +21,12 @@ class MapboxCoreNavigationTests: XCTestCase { navigation.resume() let depart = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 37.795042, longitude: -122.413165), altitude: 1, horizontalAccuracy: 1, verticalAccuracy: 1, course: 0, speed: 10, timestamp: Date()) - self.expectation(forNotification: RouteControllerAlertLevelDidChange.rawValue, object: navigation) { (notification) -> Bool in - XCTAssertEqual(notification.userInfo?.count, 2) + self.expectation(forNotification: RouteControllerDidPassSpokenInstructionPoint.rawValue, object: navigation) { (notification) -> Bool in + XCTAssertEqual(notification.userInfo?.count, 1) - let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as? RouteProgress - let userDistance = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationDistanceToEndOfManeuverKey] as! CLLocationDistance + let routeProgress = notification.userInfo![RouteControllerDidPassSpokenInstructionPointRouteProgressKey] as? RouteProgress - return routeProgress != nil && routeProgress?.currentLegProgress.alertUserLevel == .depart && round(userDistance) == 384 + return routeProgress != nil && routeProgress?.currentLegProgress.userHasArrivedAtWaypoint == false } navigation.locationManager(navigation.locationManager, didUpdateLocations: [depart]) @@ -37,20 +36,19 @@ class MapboxCoreNavigationTests: XCTestCase { } } - func testLowAlert() { + func testNewStep() { route.accessToken = "foo" - let locations = [CLLocation(coordinate: CLLocationCoordinate2D(latitude: 37.789118, longitude: -122.432209), - altitude: 1, horizontalAccuracy: 1, verticalAccuracy: 1, course: 171, speed: 10, timestamp: Date())] - let locationManager = ReplayLocationManager(locations: locations) + let location = CLLocation(coordinate: CLLocationCoordinate2D(latitude: 37.79132445827303, longitude: -122.42229044437408), + altitude: 1, horizontalAccuracy: 1, verticalAccuracy: 1, course: 171, speed: 10, timestamp: Date()) + let locationManager = ReplayLocationManager(locations: [location, location]) let navigation = RouteController(along: route, directions: directions, locationManager: locationManager) - self.expectation(forNotification: RouteControllerAlertLevelDidChange.rawValue, object: navigation) { (notification) -> Bool in - XCTAssertEqual(notification.userInfo?.count, 2) + self.expectation(forNotification: RouteControllerDidPassSpokenInstructionPoint.rawValue, object: navigation) { (notification) -> Bool in + XCTAssertEqual(notification.userInfo?.count, 1) - let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as? RouteProgress - let userDistance = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationDistanceToEndOfManeuverKey] as! CLLocationDistance + let routeProgress = notification.userInfo![RouteControllerDidPassSpokenInstructionPointRouteProgressKey] as? RouteProgress - return routeProgress?.currentLegProgress.alertUserLevel == .low && routeProgress?.currentLegProgress.stepIndex == 2 && round(userDistance) == 1786 + return routeProgress?.currentLegProgress.stepIndex == 1 } navigation.resume() @@ -102,12 +100,8 @@ class MapboxCoreNavigationTests: XCTestCase { let navigation = RouteController(along: route, directions: directions, locationManager: locationManager) self.expectation(forNotification: RouteControllerProgressDidChange.rawValue, object: navigation) { (notification) -> Bool in - let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as? RouteProgress - guard let alertLevel = routeProgress?.currentLegProgress.alertUserLevel else { - return false - } - - return alertLevel == .arrive + let routeProgress = notification.userInfo![RouteControllerDidPassSpokenInstructionPointRouteProgressKey] as? RouteProgress + return routeProgress != nil } navigation.resume() diff --git a/MapboxCoreNavigationTests/RouteProgressTests.swift b/MapboxCoreNavigationTests/RouteProgressTests.swift index b14d976d5d3..58589b6a02e 100644 --- a/MapboxCoreNavigationTests/RouteProgressTests.swift +++ b/MapboxCoreNavigationTests/RouteProgressTests.swift @@ -4,40 +4,30 @@ import MapboxDirections @testable import MapboxCoreNavigation class RouteProgressTests: XCTestCase { - func testAlertLevels() { - XCTAssertNotNil(AlertLevel.none) - XCTAssertNotNil(AlertLevel.depart) - XCTAssertNotNil(AlertLevel.low) - XCTAssertNotNil(AlertLevel.medium) - XCTAssertNotNil(AlertLevel.high) - XCTAssertNotNil(AlertLevel.arrive) - } - func testRouteProgress() { let routeProgress = RouteProgress(route: route) XCTAssertEqual(routeProgress.fractionTraveled, 0) - XCTAssertEqual(routeProgress.distanceRemaining, 4317.7) + XCTAssertEqual(routeProgress.distanceRemaining, 4316.9) XCTAssertEqual(routeProgress.distanceTraveled, 0) - XCTAssertEqual(round(routeProgress.durationRemaining), 790) + XCTAssertEqual(round(routeProgress.durationRemaining), 764) } func testRouteLegProgress() { let routeProgress = RouteProgress(route: route) - XCTAssertEqual(routeProgress.currentLeg.description, "California Street, Webster Street") - XCTAssertEqual(routeProgress.currentLegProgress.alertUserLevel, .none) + XCTAssertEqual(routeProgress.currentLeg.description, "Sacramento Street, Gough Street") XCTAssertEqual(routeProgress.currentLegProgress.distanceTraveled, 0) - XCTAssertEqual(round(routeProgress.currentLegProgress.durationRemaining), 790) + XCTAssertEqual(round(routeProgress.currentLegProgress.durationRemaining), 764) XCTAssertEqual(routeProgress.currentLegProgress.fractionTraveled, 0) XCTAssertEqual(routeProgress.currentLegProgress.stepIndex, 0) - XCTAssertEqual(routeProgress.currentLegProgress.followOnStep?.description, "Turn left onto Webster Street") - XCTAssertEqual(routeProgress.currentLegProgress.upComingStep?.description, "Turn right onto California Street") + XCTAssertEqual(routeProgress.currentLegProgress.followOnStep?.description, "Turn left onto Gough Street") + XCTAssertEqual(routeProgress.currentLegProgress.upComingStep?.description, "Turn right onto Sacramento Street") } func testRouteStepProgress() { let routeProgress = RouteProgress(route: route) - XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.distanceRemaining, 384.3) + XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.distanceRemaining, 279.8) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.distanceTraveled, 0) - XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.durationRemaining, 101.7) + XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.durationRemaining, 79.2) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.fractionTraveled, 0) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.userDistanceToManeuverLocation, Double.infinity) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.step.description, "Head south on Taylor Street") @@ -46,11 +36,12 @@ class RouteProgressTests: XCTestCase { func testNextRouteStepProgress() { let routeProgress = RouteProgress(route: route) routeProgress.currentLegProgress.stepIndex = 1 - XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.distanceRemaining, 1757.6) + XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.spokenInstructionIndex, 0) + XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.distanceRemaining, 1171) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.distanceTraveled, 0) - XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.durationRemaining, 288.9) + XCTAssertEqual(round(routeProgress.currentLegProgress.currentStepProgress.durationRemaining), 194) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.fractionTraveled, 0) XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.userDistanceToManeuverLocation, Double.infinity) - XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.step.description, "Turn right onto California Street") + XCTAssertEqual(routeProgress.currentLegProgress.currentStepProgress.step.description, "Turn right onto Sacramento Street") } } diff --git a/MapboxCoreNavigationTests/routeWithInstructions.json b/MapboxCoreNavigationTests/routeWithInstructions.json new file mode 100644 index 00000000000..82ed55396e1 --- /dev/null +++ b/MapboxCoreNavigationTests/routeWithInstructions.json @@ -0,0 +1 @@ +{"waypoints":[{"name":"Taylor Street","location":[-122.413165,37.795042]},{"name":"Page Street","location":[-122.433378,37.772701]}],"routes":[{"legs":[{"steps":[{"intersections":[{"out":0,"entry":[true],"location":[-122.413165,37.795042],"bearings":[171]},{"out":1,"in":3,"entry":[true,true,false,false],"location":[-122.413021,37.79432],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[true,true,false,false],"location":[-122.412845,37.793439],"bearings":[75,165,255,345]}],"geometry":"_zteFhycjVnC]nDa@vAQvAQ","duration":79.2,"distance":279.8,"name":"Taylor Street","weight":83.4,"mode":"driving","maneuver":{"bearing_after":171,"bearing_before":0,"type":"depart","location":[-122.413165,37.795042],"instruction":"Head south on Taylor Street"},"voiceInstructions":[{"distanceAlongGeometry":279.8,"announcement":"Head south on Taylor Street","ssmlAnnouncement":"Head south on Taylor Street"},{"distanceAlongGeometry":247.3,"announcement":"In 900 feet, turn right onto Sacramento Street","ssmlAnnouncement":"In 900 feet, turn right onto Sacramento Street"},{"distanceAlongGeometry":53,"announcement":"Turn right onto Sacramento Street","ssmlAnnouncement":"Turn right onto Sacramento Street"}]},{"intersections":[{"out":2,"in":3,"entry":[false,true,true,false],"location":[-122.412667,37.792557],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,true,true,true],"location":[-122.414309,37.792348],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,true,true,true],"location":[-122.415955,37.792139],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,true,true,true],"location":[-122.4176,37.79193],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,true,true,true],"location":[-122.419245,37.791722],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,true,true,true],"location":[-122.420889,37.791513],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,false,true,true],"location":[-122.422456,37.791314],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,true,true,false],"location":[-122.422609,37.791294],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,false,true,true],"location":[-122.424179,37.791095],"bearings":[75,165,255,345]}],"geometry":"ojteFdvcjVh@fITbDHnAHtAHdA^|F?B?Bh@dIh@fIPpCDt@JzABTB\\@T`@nG@RBRJfBVxD@P","duration":194.00000000000003,"distance":1171,"name":"Sacramento Street","weight":228.80000000000004,"mode":"driving","maneuver":{"bearing_after":260,"location":[-122.412667,37.792557],"type":"turn","bearing_before":170,"modifier":"right","instruction":"Turn right onto Sacramento Street"},"voiceInstructions":[{"distanceAlongGeometry":1171,"announcement":"In half a mile, turn left onto Gough Street","ssmlAnnouncement":"In half a mile, turn left onto Gough Street"},{"distanceAlongGeometry":422.5,"announcement":"In a quarter of a mile, turn left onto Gough Street","ssmlAnnouncement":"In a quarter of a mile, turn left onto Gough Street"},{"distanceAlongGeometry":90.5,"announcement":"Turn left onto Gough Street","ssmlAnnouncement":"Turn left onto Gough Street"}]},{"intersections":[{"out":1,"in":0,"entry":[false,true,true,true],"location":[-122.425821,37.790887],"bearings":[75,165,255,345]},{"out":1,"in":3,"lanes":[{"valid":true,"indications":["straight","left"]},{"valid":true,"indications":["straight","right"]}],"entry":[true,true,true,false],"location":[-122.425636,37.789956],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[false,true,true,false],"location":[-122.425441,37.789003],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[true,true,false,false],"location":[-122.425253,37.788071],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[false,true,true,false],"location":[-122.425065,37.78714],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[true,true,true,false],"location":[-122.424876,37.786204],"bearings":[75,165,255,345]},{"out":2,"in":4,"entry":[false,false,true,true,false],"location":[-122.424719,37.785429],"bearings":[60,90,165,270,345]},{"out":1,"in":3,"entry":[false,true,false,false],"location":[-122.42469,37.785283],"bearings":[90,165,270,345]},{"out":1,"in":3,"entry":[false,true,true,false],"location":[-122.424312,37.783409],"bearings":[75,165,255,345]},{"out":1,"in":2,"entry":[true,true,false],"location":[-122.424219,37.782947],"bearings":[75,165,345]},{"out":1,"in":3,"entry":[true,true,true,false],"location":[-122.424125,37.782479],"bearings":[75,165,255,345]}],"geometry":"a`teFjhfjVLAvC]RCRCxC_@PCLAnAOjAONCNAjAOlAONALCzC_@PALCnBUZE\\ENCdBS`AMr@IhC[zAQ|AQhDc@NA","duration":190.39999999999998,"distance":1051.8,"name":"Gough Street","weight":238.6,"mode":"driving","maneuver":{"bearing_after":170,"location":[-122.425821,37.790887],"type":"turn","bearing_before":260,"modifier":"left","instruction":"Turn left onto Gough Street"},"voiceInstructions":[{"distanceAlongGeometry":1051.8,"announcement":"In half a mile, turn right onto Turk Street","ssmlAnnouncement":"In half a mile, turn right onto Turk Street"},{"distanceAlongGeometry":386.7,"announcement":"In a quarter of a mile, turn right onto Turk Street","ssmlAnnouncement":"In a quarter of a mile, turn right onto Turk Street"},{"distanceAlongGeometry":82.9,"announcement":"Turn right onto Turk Street","ssmlAnnouncement":"Turn right onto Turk Street"}]},{"intersections":[{"out":2,"in":3,"entry":[false,true,true,false],"location":[-122.423938,37.781548],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,true,true,true],"location":[-122.427225,37.78113],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,false,true,true],"location":[-122.430368,37.780729],"bearings":[75,165,255,345]}],"geometry":"uereFr|ejVf@zH@Hh@jIh@fIb@vG@RBZ","duration":74.1,"distance":585.4,"name":"Turk Street","weight":100.9,"mode":"driving","maneuver":{"bearing_after":260,"location":[-122.423938,37.781548],"type":"turn","bearing_before":170,"modifier":"right","instruction":"Turn right onto Turk Street"},"voiceInstructions":[{"distanceAlongGeometry":585.4,"announcement":"In a quarter of a mile, turn left onto Webster Street","ssmlAnnouncement":"In a quarter of a mile, turn left onto Webster Street"},{"distanceAlongGeometry":118.5,"announcement":"Turn left onto Webster Street","ssmlAnnouncement":"Turn left onto Webster Street"}]},{"intersections":[{"out":1,"in":0,"entry":[false,true,true,false],"location":[-122.430513,37.780711],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[true,true,false,false],"location":[-122.430324,37.779778],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[true,true,true,false],"location":[-122.430136,37.778845],"bearings":[75,165,255,345]}],"geometry":"m`reFtegjVLA`BSx@KNCNAxC_@NANCdBSr@KNA","duration":41.3,"distance":314.7,"name":"Webster Street","weight":57.5,"mode":"driving","maneuver":{"bearing_after":170,"location":[-122.430513,37.780711],"type":"turn","bearing_before":260,"modifier":"left","instruction":"Turn left onto Webster Street"},"voiceInstructions":[{"distanceAlongGeometry":314.7,"announcement":"In a quarter of a mile, turn right onto Fulton Street","ssmlAnnouncement":"In a quarter of a mile, turn right onto Fulton Street"},{"distanceAlongGeometry":114.3,"announcement":"Turn right onto Fulton Street","ssmlAnnouncement":"Turn right onto Fulton Street"}]},{"intersections":[{"out":2,"in":3,"entry":[true,true,true,false],"location":[-122.429948,37.777917],"bearings":[75,165,255,345]},{"out":1,"in":0,"entry":[false,true,false],"location":[-122.430724,37.777818],"bearings":[75,255,345]}],"geometry":"_oqeFdbgjVBRNdCTlD","duration":21.5,"distance":146.4,"name":"Fulton Street","weight":32.3,"mode":"driving","maneuver":{"bearing_after":260,"location":[-122.429948,37.777917],"type":"turn","bearing_before":170,"modifier":"right","instruction":"Turn right onto Fulton Street"},"voiceInstructions":[{"distanceAlongGeometry":146.4,"announcement":"In 500 feet, turn left onto Fillmore Street","ssmlAnnouncement":"In 500 feet, turn left onto Fillmore Street"},{"distanceAlongGeometry":102.1,"announcement":"Turn left onto Fillmore Street","ssmlAnnouncement":"Turn left onto Fillmore Street"}]},{"intersections":[{"out":1,"in":0,"entry":[false,true,true,true],"location":[-122.431592,37.777707],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[true,true,true,false],"location":[-122.431404,37.776776],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[true,true,true,false],"location":[-122.431216,37.775843],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[false,true,true,false],"location":[-122.431028,37.774912],"bearings":[75,165,255,345]},{"out":1,"in":3,"entry":[true,true,false,false],"location":[-122.430839,37.773978],"bearings":[75,165,255,345]}],"geometry":"umqeFllgjVxDe@zDc@xDe@xDe@xDe@","duration":125.4,"distance":524.8,"name":"Fillmore Street","weight":137.6,"mode":"driving","maneuver":{"bearing_after":170,"location":[-122.431592,37.777707],"type":"turn","bearing_before":260,"modifier":"left","instruction":"Turn left onto Fillmore Street"},"voiceInstructions":[{"distanceAlongGeometry":524.8,"announcement":"In a quarter of a mile, turn right onto Page Street","ssmlAnnouncement":"In a quarter of a mile, turn right onto Page Street"},{"distanceAlongGeometry":293,"announcement":"In 1000 feet, turn right onto Page Street","ssmlAnnouncement":"In 1000 feet, turn right onto Page Street"},{"distanceAlongGeometry":62.8,"announcement":"Turn right onto Page Street","ssmlAnnouncement":"Turn right onto Page Street"}]},{"intersections":[{"out":2,"in":3,"entry":[true,true,true,false],"location":[-122.430651,37.773048],"bearings":[75,165,255,345]},{"out":2,"in":0,"entry":[false,true,true,true],"location":[-122.432298,37.772838],"bearings":[75,165,255,345]}],"geometry":"qppeFpfgjVh@hIZvE","duration":38,"distance":242.8,"name":"Page Street","weight":38,"mode":"driving","maneuver":{"bearing_after":260,"location":[-122.430651,37.773048],"type":"turn","bearing_before":170,"modifier":"right","instruction":"Turn right onto Page Street"},"voiceInstructions":[{"distanceAlongGeometry":242.8,"announcement":"In 800 feet, you have arrived at your destination","ssmlAnnouncement":"In 800 feet, you have arrived at your destination"},{"distanceAlongGeometry":0,"announcement":"You have arrived at your destination","ssmlAnnouncement":"You have arrived at your destination"}]},{"intersections":[{"in":0,"entry":[true],"location":[-122.433378,37.772701],"bearings":[81]}],"geometry":"knpeFrwgjV","duration":0,"distance":0,"name":"Page Street","weight":0,"mode":"driving","maneuver":{"bearing_after":0,"bearing_before":261,"type":"arrive","location":[-122.433378,37.772701],"instruction":"You have arrived at your destination"},"voiceInstructions":[]}],"weight":917.1,"distance":4316.9,"summary":"Sacramento Street, Gough Street","duration":763.9}],"weight_name":"routability","weight":917.1,"distance":4316.9,"duration":763.9}],"code":"Ok","uuid":"cj8oqa44y00xn8up90soa1eej"} diff --git a/MapboxNavigation-Documentation.podspec b/MapboxNavigation-Documentation.podspec index cf82b42bddb..28f76f2f9e0 100644 --- a/MapboxNavigation-Documentation.podspec +++ b/MapboxNavigation-Documentation.podspec @@ -43,9 +43,9 @@ Pod::Spec.new do |s| s.requires_arc = true s.module_name = "MapboxNavigation" - s.dependency "MapboxDirections.swift", "~> 0.10.5" + s.dependency "MapboxDirections.swift", "~> 0.11" s.dependency "Mapbox-iOS-SDK", "~> 3.6" - s.dependency "OSRMTextInstructions", "~> 0.3" + s.dependency "OSRMTextInstructions", "~> 0.4" s.dependency "Pulley", "1.4" s.dependency "SDWebImage", "~> 4.1" s.dependency "AWSPolly", "~> 2.6" diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index 2288ffac5eb..e290ad44cad 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -95,8 +95,6 @@ 35718BE71EF3194200AFA3D1 /* tunnel.json in Resources */ = {isa = PBXBuildFile; fileRef = 35718BE41EF316BA00AFA3D1 /* tunnel.json */; }; 35718BE81EF3194500AFA3D1 /* tunnel.route in Resources */ = {isa = PBXBuildFile; fileRef = 35718BE31EF316BA00AFA3D1 /* tunnel.route */; }; 35726EE81F0856E900AFA1B6 /* DayStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35726EE71F0856E900AFA1B6 /* DayStyle.swift */; }; - 357826C31F1B7CCE005C54FB /* SpokenDistanceFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357826C21F1B7CCE005C54FB /* SpokenDistanceFormatter.swift */; }; - 357826C71F1B84F2005C54FB /* DistanceFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357826C61F1B84F2005C54FB /* DistanceFormatterTests.swift */; }; 358D14661E5E3B7700ADE590 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 358D14651E5E3B7700ADE590 /* AppDelegate.swift */; }; 358D14681E5E3B7700ADE590 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 358D14671E5E3B7700ADE590 /* ViewController.swift */; }; 359574A81F28CC5A00838209 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359574A71F28CC3800838209 /* CLLocation.swift */; }; @@ -139,6 +137,7 @@ C52D09CE1DEF5E5100BE3C5C /* route.json in Resources */ = {isa = PBXBuildFile; fileRef = C52D09CD1DEF5E5100BE3C5C /* route.json */; }; C52D09D31DEF636C00BE3C5C /* Fixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52D09D21DEF636C00BE3C5C /* Fixture.swift */; }; C53208AB1E81FFB900910266 /* NavigationMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53208AA1E81FFB900910266 /* NavigationMapView.swift */; }; + C5387A9D1F8FDB13000D2E93 /* routeWithInstructions.json in Resources */ = {isa = PBXBuildFile; fileRef = C5387A9C1F8FDB13000D2E93 /* routeWithInstructions.json */; }; C53C193D1F38E0D5008DB406 /* VisualInstructionFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53C193C1F38E0D5008DB406 /* VisualInstructionFormatter.swift */; }; C53C196D1F38EA25008DB406 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C53C196F1F38EA25008DB406 /* Localizable.strings */; }; C549F8321F17F2C5001A0A2D /* MapboxMobileEvents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C549F8311F17F2C5001A0A2D /* MapboxMobileEvents.framework */; }; @@ -149,7 +148,6 @@ C578DA081EFD0FFF0052079F /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C578DA071EFD0FFF0052079F /* ProcessInfo.swift */; }; C58159011EA6D02700FC6C3D /* MGLVectorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58159001EA6D02700FC6C3D /* MGLVectorSource.swift */; }; C58454161F4B9FD30057D8B3 /* Shields.plist in Resources */ = {isa = PBXBuildFile; fileRef = 351BEC2A1E5BD538006FE110 /* Shields.plist */; }; - C588C3C11F3387C700520EF2 /* SpokenInstructionFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C588C3C01F3387C700520EF2 /* SpokenInstructionFormatter.swift */; }; C588C3C21F33882100520EF2 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35BF8CA31F28EBD8003F6125 /* String.swift */; }; C588C3C31F3388A100520EF2 /* RouteStepFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C997401E732C5400544D1C /* RouteStepFormatter.swift */; }; C58D6BAD1DDCF2AE00387F53 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58D6BAC1DDCF2AE00387F53 /* Constants.swift */; }; @@ -331,7 +329,7 @@ 351BEBE71E5BCC63006FE110 /* RouteTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouteTableViewCell.swift; sourceTree = ""; }; 351BEBE81E5BCC63006FE110 /* RouteTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouteTableViewController.swift; sourceTree = ""; }; 351BEBE91E5BCC63006FE110 /* RouteTableViewHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouteTableViewHeaderView.swift; sourceTree = ""; }; - 351BEBEA1E5BCC63006FE110 /* NavigationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = NavigationViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 351BEBEA1E5BCC63006FE110 /* NavigationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = NavigationViewController.swift; sourceTree = ""; }; 351BEBED1E5BCC63006FE110 /* StyleKitArrows.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyleKitArrows.swift; sourceTree = ""; }; 351BEBEF1E5BCC63006FE110 /* TurnArrowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TurnArrowView.swift; sourceTree = ""; }; 351BEBF01E5BCC63006FE110 /* UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; @@ -373,8 +371,6 @@ 35718BE31EF316BA00AFA3D1 /* tunnel.route */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = tunnel.route; sourceTree = ""; }; 35718BE41EF316BA00AFA3D1 /* tunnel.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = tunnel.json; sourceTree = ""; }; 35726EE71F0856E900AFA1B6 /* DayStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayStyle.swift; sourceTree = ""; }; - 357826C21F1B7CCE005C54FB /* SpokenDistanceFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpokenDistanceFormatter.swift; sourceTree = ""; }; - 357826C61F1B84F2005C54FB /* DistanceFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistanceFormatterTests.swift; sourceTree = ""; }; 357F0DF01EB9D99F00A0B53C /* sv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; 357F0DF11EB9DAB400A0B53C /* vi */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; 358D14631E5E3B7700ADE590 /* Example-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -430,6 +426,7 @@ C52D09CD1DEF5E5100BE3C5C /* route.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = route.json; sourceTree = ""; }; C52D09D21DEF636C00BE3C5C /* Fixture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fixture.swift; sourceTree = ""; }; C53208AA1E81FFB900910266 /* NavigationMapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = NavigationMapView.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + C5387A9C1F8FDB13000D2E93 /* routeWithInstructions.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = routeWithInstructions.json; sourceTree = ""; }; C53C193C1F38E0D5008DB406 /* VisualInstructionFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisualInstructionFormatter.swift; sourceTree = ""; }; C53C19701F38EACD008DB406 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; C53C19711F38EADB008DB406 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; @@ -443,7 +440,6 @@ C57607B01F4CC97D00C27423 /* Solar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Solar.framework; path = Carthage/Build/iOS/Solar.framework; sourceTree = ""; }; C578DA071EFD0FFF0052079F /* ProcessInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = ""; }; C58159001EA6D02700FC6C3D /* MGLVectorSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MGLVectorSource.swift; sourceTree = ""; }; - C588C3C01F3387C700520EF2 /* SpokenInstructionFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpokenInstructionFormatter.swift; sourceTree = ""; }; C58D6BAC1DDCF2AE00387F53 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; C5A6B2DC1F4CE8E8004260EA /* StyleType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyleType.swift; sourceTree = ""; }; C5ADFBC91DDCC7840011824B /* MapboxCoreNavigation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MapboxCoreNavigation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -826,8 +822,6 @@ C51DF8681F38C57A006C6A15 /* Instructions */ = { isa = PBXGroup; children = ( - 357826C21F1B7CCE005C54FB /* SpokenDistanceFormatter.swift */, - C588C3C01F3387C700520EF2 /* SpokenInstructionFormatter.swift */, 35C997401E732C5400544D1C /* RouteStepFormatter.swift */, C53C193C1F38E0D5008DB406 /* VisualInstructionFormatter.swift */, ); @@ -841,6 +835,7 @@ 35718BE31EF316BA00AFA3D1 /* tunnel.route */, C52D09CD1DEF5E5100BE3C5C /* route.json */, DAB2CCE61DF7AFDE001B2FE1 /* dc-line.geojson */, + C5387A9C1F8FDB13000D2E93 /* routeWithInstructions.json */, ); name = Fixtures; sourceTree = ""; @@ -931,7 +926,6 @@ C5ADFBD71DDCC7840011824B /* MapboxCoreNavigationTests.swift */, C52AC1251DF0E48600396B9F /* RouteProgressTests.swift */, C5ADFBD91DDCC7840011824B /* Info.plist */, - 357826C61F1B84F2005C54FB /* DistanceFormatterTests.swift */, 359574A91F28CCBB00838209 /* LocationTests.swift */, ); path = MapboxCoreNavigationTests; @@ -1289,6 +1283,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C5387A9D1F8FDB13000D2E93 /* routeWithInstructions.json in Resources */, DAB2CCE71DF7AFDF001B2FE1 /* dc-line.geojson in Resources */, 35718BE71EF3194200AFA3D1 /* tunnel.json in Resources */, C52D09CE1DEF5E5100BE3C5C /* route.json in Resources */, @@ -1461,7 +1456,6 @@ buildActionMask = 2147483647; files = ( C561735B1F182113005954F6 /* RouteStep.swift in Sources */, - C588C3C11F3387C700520EF2 /* SpokenInstructionFormatter.swift in Sources */, C53C193D1F38E0D5008DB406 /* VisualInstructionFormatter.swift in Sources */, 35BF8CA21F28EB60003F6125 /* Array.swift in Sources */, 353E68FC1EF0B7F8007B2AE5 /* NavigationLocationManager.swift in Sources */, @@ -1470,7 +1464,6 @@ 351174F41EF1C0530065E248 /* ReplayLocationManager.swift in Sources */, C5C94C1C1DDCD2340097296A /* RouteController.swift in Sources */, 359574A81F28CC5A00838209 /* CLLocation.swift in Sources */, - 357826C31F1B7CCE005C54FB /* SpokenDistanceFormatter.swift in Sources */, 351927361F0FA072003A702D /* ScreenCapture.swift in Sources */, C5D9800F1EFBCDAD006DBF2E /* Date.swift in Sources */, 35B839491E2E3D5D0045A868 /* MBRouteController.m in Sources */, @@ -1493,7 +1486,6 @@ buildActionMask = 2147483647; files = ( C5ADFBD81DDCC7840011824B /* MapboxCoreNavigationTests.swift in Sources */, - 357826C71F1B84F2005C54FB /* DistanceFormatterTests.swift in Sources */, C52AC1261DF0E48600396B9F /* RouteProgressTests.swift in Sources */, 359574AA1F28CCBB00838209 /* LocationTests.swift in Sources */, C52D09D31DEF636C00BE3C5C /* Fixture.swift in Sources */, diff --git a/MapboxNavigation/LanesContainerView.swift b/MapboxNavigation/LanesContainerView.swift index c18f1e59296..e3aed7827eb 100644 --- a/MapboxNavigation/LanesContainerView.swift +++ b/MapboxNavigation/LanesContainerView.swift @@ -34,12 +34,12 @@ class LanesContainerView: LanesView { addConstraint(NSLayoutConstraint(item: self, attribute: .centerX, relatedBy: .equal, toItem: stackView, attribute: .centerX, multiplier: 1, constant: 0)) } - func updateLaneViews(step: RouteStep, alertLevel: AlertLevel) { + func updateLaneViews(step: RouteStep, durationRemaining: TimeInterval) { clearLaneViews() if let allLanes = step.intersections?.first?.approachLanes, let usableLanes = step.intersections?.first?.usableApproachLanes, - (alertLevel == .high || alertLevel == .medium) { + durationRemaining < RouteControllerMediumAlertInterval { for (i, lane) in allLanes.enumerated() { let laneView = laneArrowView() diff --git a/MapboxNavigation/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index d1817fc5da8..6bc804800c7 100644 --- a/MapboxNavigation/NavigationMapView.swift +++ b/MapboxNavigation/NavigationMapView.swift @@ -109,7 +109,7 @@ open class NavigationMapView: MGLMapView { func progressDidChange(_ notification: Notification) { guard tracksUserCourse else { return } - let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as! RouteProgress + let routeProgress = notification.userInfo![RouteControllerProgressDidChangeNotificationProgressKey] as! RouteProgress let stepProgress = routeProgress.currentLegProgress.currentStepProgress let expectedTravelTime = stepProgress.step.expectedTravelTime diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 2368b2058db..4a0ffcc932b 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -418,42 +418,39 @@ public class NavigationViewController: NavigationPulleyViewController, RouteMapV func resumeNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(notification:)), name: RouteControllerProgressDidChange, object: routeController) - NotificationCenter.default.addObserver(self, selector: #selector(alertLevelDidChange(notification:)), name: RouteControllerAlertLevelDidChange, object: routeController) + NotificationCenter.default.addObserver(self, selector: #selector(didPassInstructionPoint(notification:)), name: RouteControllerDidPassSpokenInstructionPoint, object: routeController) } func suspendNotifications() { NotificationCenter.default.removeObserver(self, name: RouteControllerProgressDidChange, object: routeController) - NotificationCenter.default.removeObserver(self, name: RouteControllerAlertLevelDidChange, object: routeController) + NotificationCenter.default.removeObserver(self, name: RouteControllerDidPassSpokenInstructionPoint, object: routeController) } func progressDidChange(notification: NSNotification) { resetETATimer() - let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as! RouteProgress + let routeProgress = notification.userInfo![MBRouteControllerDidPassSpokenInstructionPointRouteProgressKey] as! RouteProgress let location = notification.userInfo![RouteControllerProgressDidChangeNotificationLocationKey] as! CLLocation let secondsRemaining = notification.userInfo![RouteControllerProgressDidChangeNotificationSecondsRemainingOnStepKey] as! TimeInterval mapViewController?.notifyDidChange(routeProgress: routeProgress, location: location, secondsRemaining: secondsRemaining) tableViewController?.updateETA(routeProgress: routeProgress) - progressBar.progress = routeProgress.currentLegProgress.alertUserLevel == .arrive ? 1 : CGFloat(routeProgress.fractionTraveled) + progressBar.progress = routeProgress.currentLegProgress.userHasArrivedAtWaypoint ? 1 : CGFloat(routeProgress.fractionTraveled) } - func alertLevelDidChange(notification: NSNotification) { - let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as! RouteProgress - let alertLevel = routeProgress.currentLegProgress.alertUserLevel + func didPassInstructionPoint(notification: NSNotification) { + let routeProgress = notification.userInfo![MBRouteControllerDidPassSpokenInstructionPointRouteProgressKey] as! RouteProgress - mapViewController?.notifyAlertLevelDidChange(routeProgress: routeProgress) - tableViewController?.notifyAlertLevelDidChange() + mapViewController?.updateMapOverlays(for: routeProgress) + tableViewController?.reload() - // Any time the alert level changes, clear out previous notifications. - // When we give a high alert notification, we want to clear out this notification when completing that step. clearStaleNotifications() - if let upComingStep = routeProgress.currentLegProgress.upComingStep, alertLevel == .high { + if let upComingStep = routeProgress.currentLegProgress.upComingStep, routeProgress.currentLegProgress.currentStepProgress.durationRemaining < RouteControllerHighAlertInterval { scheduleLocalNotification(about: upComingStep, legIndex: routeProgress.legIndex, numberOfLegs: routeProgress.route.legs.count) } - if routeProgress.currentLegProgress.alertUserLevel == .arrive { + if routeProgress.currentLegProgress.userHasArrivedAtWaypoint { navigationDelegate?.navigationViewController?(self, didArriveAt: routeProgress.currentLegProgress.leg.destination) } diff --git a/MapboxNavigation/PollyVoiceController.swift b/MapboxNavigation/PollyVoiceController.swift index ca76e8306a0..37c8e67c7c4 100644 --- a/MapboxNavigation/PollyVoiceController.swift +++ b/MapboxNavigation/PollyVoiceController.swift @@ -49,12 +49,11 @@ public class PollyVoiceController: RouteVoiceController { super.init() } - public override func alertLevelDidChange(notification: NSNotification) { + public override func didPassSpokenInstructionPoint(notification: NSNotification) { guard shouldSpeak(for: notification) == true else { return } - let routeProgresss = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as! RouteProgress - let userDistances = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationDistanceToEndOfManeuverKey] as! CLLocationDistance - let instruction = spokenInstructionFormatter.string(routeProgress: routeProgresss, userDistance: userDistances, markUpWithSSML: true) + let routeProgresss = notification.userInfo![MBRouteControllerDidPassSpokenInstructionPointRouteProgressKey] as! RouteProgress + guard let instruction = routeProgresss.currentLegProgress.currentStepProgress.currentSpokenInstruction?.ssmlText else { return } pollyTask?.cancel() audioPlayer?.stop() @@ -117,7 +116,7 @@ public class PollyVoiceController: RouteVoiceController { input.voiceId = voiceId } - input.text = "\(text)" + input.text = text let builder = AWSPollySynthesizeSpeechURLBuilder.default().getPreSignedURL(input) builder.continueWith { [weak self] (awsTask: AWSTask) -> Any? in diff --git a/MapboxNavigation/RouteManeuverViewController.swift b/MapboxNavigation/RouteManeuverViewController.swift index a55b5a12235..e45c4ff3538 100644 --- a/MapboxNavigation/RouteManeuverViewController.swift +++ b/MapboxNavigation/RouteManeuverViewController.swift @@ -112,7 +112,7 @@ class RouteManeuverViewController: UIViewController { distance = distanceRemaining > 5 ? distanceRemaining : 0 - if routeProgress.currentLegProgress.alertUserLevel == .arrive { + if routeProgress.currentLegProgress.userHasArrivedAtWaypoint { distance = nil destinationLabel.unabridgedText = routeProgress.currentLeg.destination.name ?? routeStepFormatter.string(for: routeStepFormatter.string(for: routeProgress.currentLegProgress.upComingStep, legIndex: routeProgress.legIndex, numberOfLegs: routeProgress.route.legs.count, markUpWithSSML: false)) } else { diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index c8c878e39e0..6c7db8d8b37 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -252,19 +252,12 @@ class RouteMapViewController: UIViewController { } } - func notifyAlertLevelDidChange(routeProgress: RouteProgress) { + func updateMapOverlays(for routeProgress: RouteProgress) { if routeProgress.currentLegProgress.followOnStep != nil { mapView.addArrow(route: routeController.routeProgress.route, legIndex: routeController.routeProgress.legIndex, stepIndex: routeController.routeProgress.currentLegProgress.stepIndex + 1) } else { mapView.removeArrow() } - - if currentLegIndexMapped != routeProgress.legIndex { - mapView.showWaypoints(routeProgress.route, legIndex: routeProgress.legIndex) - mapView.showRoute(routeProgress.route, legIndex: routeProgress.legIndex) - - currentLegIndexMapped = routeProgress.legIndex - } } func mapView(_ mapView: MGLMapView, imageFor annotation: MGLAnnotation) -> MGLAnnotationImage? { @@ -289,9 +282,9 @@ class RouteMapViewController: UIViewController { routePageViewController(routePageViewController, willTransitionTo: controller, didSwipe: false) } - if let upComingStep = routeProgress.currentLegProgress?.upComingStep, routeProgress.currentLegProgress.alertUserLevel != .arrive { + if let upComingStep = routeProgress.currentLegProgress?.upComingStep, !routeProgress.currentLegProgress.userHasArrivedAtWaypoint { if routePageViewController.currentManeuverPage.step == upComingStep { - updateLaneViews(step: upComingStep, alertLevel: routeProgress.currentLegProgress.alertUserLevel) + updateLaneViews(step: upComingStep, durationRemaining: routeProgress.currentLegProgress.currentStepProgress.durationRemaining) } } @@ -302,6 +295,13 @@ class RouteMapViewController: UIViewController { controller.notifyDidChange(routeProgress: routeProgress, secondsRemaining: secondsRemaining) controller.roadCode = step.codes?.first ?? step.destinationCodes?.first ?? step.destinations?.first + + if currentLegIndexMapped != routeProgress.legIndex { + mapView.showWaypoints(routeProgress.route, legIndex: routeProgress.legIndex) + mapView.showRoute(routeProgress.route, legIndex: routeProgress.legIndex) + + currentLegIndexMapped = routeProgress.legIndex + } guard isInOverviewMode else { return @@ -320,8 +320,8 @@ class RouteMapViewController: UIViewController { right: 0) } - func updateLaneViews(step: RouteStep, alertLevel: AlertLevel) { - laneViewsContainerView.updateLaneViews(step: step, alertLevel: alertLevel) + func updateLaneViews(step: RouteStep, durationRemaining: TimeInterval) { + laneViewsContainerView.updateLaneViews(step: step, durationRemaining: durationRemaining) if laneViewsContainerView.stackView.arrangedSubviews.count > 0 { showLaneViews() @@ -542,7 +542,7 @@ extension RouteMapViewController: RoutePageViewControllerDelegate { maneuverViewController.roadCode = step.codes?.first ?? step.destinationCodes?.first ?? step.destinations?.first maneuverViewController.updateStreetNameForStep() - updateLaneViews(step: step, alertLevel: .high) + updateLaneViews(step: step, durationRemaining: 0) if !isInOverviewMode { if didSwipe, step != routeController.routeProgress.currentLegProgress.upComingStep { diff --git a/MapboxNavigation/RouteTableViewController.swift b/MapboxNavigation/RouteTableViewController.swift index dced070c836..5a32d0f4099 100644 --- a/MapboxNavigation/RouteTableViewController.swift +++ b/MapboxNavigation/RouteTableViewController.swift @@ -85,7 +85,7 @@ class RouteTableViewController: UIViewController { } } - func notifyAlertLevelDidChange() { + func reload() { if let visibleIndexPaths = tableView.indexPathsForVisibleRows { tableView.reloadRows(at: visibleIndexPaths, with: .fade) } diff --git a/MapboxNavigation/RouteVoiceController.swift b/MapboxNavigation/RouteVoiceController.swift index 5e0758775db..75bef8c049e 100644 --- a/MapboxNavigation/RouteVoiceController.swift +++ b/MapboxNavigation/RouteVoiceController.swift @@ -11,8 +11,6 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP lazy var speechSynth = AVSpeechSynthesizer() var audioPlayer: AVAudioPlayer? - let maneuverVoiceDistanceFormatter = SpokenDistanceFormatter(approximate: true) - let spokenInstructionFormatter = SpokenInstructionFormatter() let routeStepFormatter = RouteStepFormatter() var recentlyAnnouncedRouteStep: RouteStep? var fallbackText: String! @@ -72,8 +70,6 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP speechSynth.delegate = self rerouteSoundPlayer.delegate = self - maneuverVoiceDistanceFormatter.unitStyle = .long - maneuverVoiceDistanceFormatter.numberFormatter.locale = .nationalizedCurrent resumeNotifications() } @@ -84,13 +80,13 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP } func resumeNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(alertLevelDidChange(notification:)), name: RouteControllerAlertLevelDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didPassSpokenInstructionPoint(notification:)), name: RouteControllerDidPassSpokenInstructionPoint, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(pauseSpeechAndPlayReroutingDing(notification:)), name: RouteControllerWillReroute, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(didReroute(notification:)), name: RouteControllerDidReroute, object: nil) } func suspendNotifications() { - NotificationCenter.default.removeObserver(self, name: RouteControllerAlertLevelDidChange, object: nil) + NotificationCenter.default.removeObserver(self, name: RouteControllerDidPassSpokenInstructionPoint, object: nil) NotificationCenter.default.removeObserver(self, name: RouteControllerWillReroute, object: nil) NotificationCenter.default.removeObserver(self, name: RouteControllerDidReroute, object: nil) } @@ -158,7 +154,7 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP recentlyAnnouncedRouteStep = nil } - open func alertLevelDidChange(notification: NSNotification) { + open func didPassSpokenInstructionPoint(notification: NSNotification) { guard shouldSpeak(for: notification) == true else { return } speak(fallbackText, error: nil) @@ -168,8 +164,7 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP func shouldSpeak(for notification: NSNotification) -> Bool { guard isEnabled, volume > 0, !NavigationSettings.shared.muted else { return false } - let routeProgress = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationRouteProgressKey] as! RouteProgress - let userDistance = notification.userInfo![RouteControllerAlertLevelDidChangeNotificationDistanceToEndOfManeuverKey] as! CLLocationDistance + let routeProgress = notification.userInfo![RouteControllerDidPassSpokenInstructionPointRouteProgressKey] as! RouteProgress // We're guarding against two things here: // 1. `recentlyAnnouncedRouteStep` being nil. @@ -182,12 +177,7 @@ open class RouteVoiceController: NSObject, AVSpeechSynthesizerDelegate, AVAudioP // Set recentlyAnnouncedRouteStep to the current step recentlyAnnouncedRouteStep = routeProgress.currentLegProgress.currentStep - fallbackText = spokenInstructionFormatter.string(routeProgress: routeProgress, userDistance: userDistance, markUpWithSSML: false) - - // If the user is merging onto a highway, an announcement to merge is a bit excessive - if let upComingStep = routeProgress.currentLegProgress.upComingStep, routeProgress.currentLegProgress.currentStep.maneuverType == .takeOnRamp && upComingStep.maneuverType == .merge && routeProgress.currentLegProgress.alertUserLevel == .high { - return false - } + fallbackText = routeProgress.currentLegProgress.currentStepProgress.currentSpokenInstruction?.text return true } diff --git a/MapboxNavigationTests/LaneTests.swift b/MapboxNavigationTests/LaneTests.swift index 4c82ed364d7..02665b873b4 100644 --- a/MapboxNavigationTests/LaneTests.swift +++ b/MapboxNavigationTests/LaneTests.swift @@ -25,7 +25,7 @@ class LaneTests: FBSnapshotTestCase { let controller = storyboard().instantiateViewController(withIdentifier: "RouteMapViewController") as! RouteMapViewController XCTAssert(controller.view != nil) - controller.updateLaneViews(step: step, alertLevel: .high) + controller.updateLaneViews(step: step, durationRemaining: 20) controller.showLaneViews(animated: false) FBSnapshotVerifyView(controller.laneViewsContainerView) diff --git a/MapboxNavigationTests/MapboxNavigationTests.swift b/MapboxNavigationTests/MapboxNavigationTests.swift index 88245299bbe..cea0f07a9dc 100644 --- a/MapboxNavigationTests/MapboxNavigationTests.swift +++ b/MapboxNavigationTests/MapboxNavigationTests.swift @@ -152,7 +152,7 @@ class MapboxNavigationTests: FBSnapshotTestCase { let routeController = RouteController(along: route, directions: directions) let steps = routeController.routeProgress.currentLeg.steps let stepWithLanes = steps[8] - controller.updateLaneViews(step: stepWithLanes, alertLevel: .high) + controller.updateLaneViews(step: stepWithLanes, durationRemaining: 20) controller.showLaneViews(animated: false) FBSnapshotVerifyView(controller.laneViewsContainerView)