diff --git a/CHANGELOG.md b/CHANGELOG.md index b8ac44a58e2..48edbe5a24b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## master * Fixed an issue where swiping the banner down after the StepsTableViewController has already displayed could put the UI in an unstable state. ([#2197](https://github.com/mapbox/mapbox-navigation-ios/pull/2197)) +* If the user walks away from the route, they may be rerouted onto a route that initially travels in the opposite direction. This is only the case along steps that require walking on foot. ([#2215](https://github.com/mapbox/mapbox-navigation-ios/pull/2215)) +* While the user is walking, the map rotates according to the user’s heading instead of their course. ([#2215](https://github.com/mapbox/mapbox-navigation-ios/pull/2215)) +* Added the `NavigationMapView.updateCourseTracking(location:heading:camera:animated:)` method for manually forcing the map view and user course view to update according to a location or heading. ([#2215](https://github.com/mapbox/mapbox-navigation-ios/pull/2215)) +* Added `RouteControllerNotificationUserInfoKey.headingKey` to the user info dictionary of `Notification.Name.routeControllerWillReroute`, `Notification.Name.routeControllerDidReroute`, and `Notification.Name.routeControllerProgressDidChange` notifications. ([#2215](https://github.com/mapbox/mapbox-navigation-ios/pull/2215)) +* Added a `Router.heading` property that may contain a heading from the location manager. ([#2215](https://github.com/mapbox/mapbox-navigation-ios/pull/2215)) ## v0.36.0 diff --git a/Example/CustomViewController.swift b/Example/CustomViewController.swift index b45faca65d6..bfbbd0d1efc 100644 --- a/Example/CustomViewController.swift +++ b/Example/CustomViewController.swift @@ -91,6 +91,7 @@ class CustomViewController: UIViewController, MGLMapViewDelegate { let routeProgress = notification.userInfo![RouteControllerNotificationUserInfoKey.routeProgressKey] as! RouteProgress let location = notification.userInfo![RouteControllerNotificationUserInfoKey.locationKey] as! CLLocation + let heading = notification.userInfo![RouteControllerNotificationUserInfoKey.headingKey] as? CLHeading // Add maneuver arrow if routeProgress.currentLegProgress.followOnStep != nil { @@ -104,7 +105,7 @@ class CustomViewController: UIViewController, MGLMapViewDelegate { instructionsBannerView.isHidden = false // Update the user puck - mapView.updateCourseTracking(location: location, animated: true) + mapView.updateCourseTracking(location: location, heading: heading, animated: true) } @objc func updateInstructionsBanner(notification: NSNotification) { diff --git a/MapboxCoreNavigation/CoreConstants.swift b/MapboxCoreNavigation/CoreConstants.swift index 4c94d802254..94072a7d8ef 100644 --- a/MapboxCoreNavigation/CoreConstants.swift +++ b/MapboxCoreNavigation/CoreConstants.swift @@ -136,21 +136,21 @@ extension Notification.Name { /** Posted after the user diverges from the expected route, just before `RouteController` attempts to calculate a new route. - The user info dictionary contains the key `RouteControllerNotificationUserInfoKey.locationKey`. + The user info dictionary contains the keys `RouteControllerNotificationUserInfoKey.locationKey` and `RouteControllerNotificationUserInfoKey.headingKey`. */ public static let routeControllerWillReroute = MBRouteControllerWillReroute /** Posted when `RouteController` obtains a new route in response to the user diverging from a previous route. - The user info dictionary contains the keys `RouteControllerNotificationUserInfoKey.locationKey` and `RouteControllerNotificationUserInfoKey.isProactiveKey`. + The user info dictionary contains the keys `RouteControllerNotificationUserInfoKey.locationKey`, `RouteControllerNotificationUserInfoKey.headingKey`, and `RouteControllerNotificationUserInfoKey.isProactiveKey`. */ public static let routeControllerDidReroute = MBRouteControllerDidReroute /** Posted when `RouteController` receives a user location update representing movement along the expected route. - The user info dictionary contains the keys `RouteControllerNotificationUserInfoKey.routeProgressKey`, `RouteControllerNotificationUserInfoKey.locationKey`, and `RouteControllerNotificationUserInfoKey.rawLocationKey`. + The user info dictionary contains the keys `RouteControllerNotificationUserInfoKey.routeProgressKey`, `RouteControllerNotificationUserInfoKey.locationKey`, `RouteControllerNotificationUserInfoKey.headingKey`, and `RouteControllerNotificationUserInfoKey.rawLocationKey`. */ public static let routeControllerProgressDidChange = MBRouteControllerProgressDidChange diff --git a/MapboxCoreNavigation/LegacyRouteController.swift b/MapboxCoreNavigation/LegacyRouteController.swift index 089c5d6181d..35e3c5b9840 100644 --- a/MapboxCoreNavigation/LegacyRouteController.swift +++ b/MapboxCoreNavigation/LegacyRouteController.swift @@ -123,7 +123,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa return rawLocation?.snapped(to: routeProgress) } - var heading: CLHeading? + public var heading: CLHeading? /** The most recently received user location. @@ -224,6 +224,7 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa @objc public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { heading = newHeading + // TODO: Cause a map view to update its camera. } @objc public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { @@ -300,11 +301,14 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa delegate?.router?(self, didUpdate: progress, with: location, rawLocation: rawLocation) //Fire the notification (for now) - NotificationCenter.default.post(name: .routeControllerProgressDidChange, object: self, userInfo: [ - RouteControllerNotificationUserInfoKey.routeProgressKey: progress, - RouteControllerNotificationUserInfoKey.locationKey: location, //guaranteed value - RouteControllerNotificationUserInfoKey.rawLocationKey: rawLocation //raw - ]) + var userInfo: [RouteControllerNotificationUserInfoKey: Any] = [ + .routeProgressKey: progress, + .locationKey: location, //guaranteed value + .rawLocationKey: rawLocation, //raw + ] + userInfo[.headingKey] = heading + userInfo[.rawHeadingKey] = heading // heading snapping not yet implemented (an unnecessary?) + NotificationCenter.default.post(name: .routeControllerProgressDidChange, object: self, userInfo: userInfo) } } @@ -353,13 +357,15 @@ open class LegacyRouteController: NSObject, Router, InternalRouter, CLLocationMa isRerouting = true delegate?.router?(self, willRerouteFrom: location) - NotificationCenter.default.post(name: .routeControllerWillReroute, object: self, userInfo: [ - RouteControllerNotificationUserInfoKey.locationKey: location - ]) + var userInfo: [RouteControllerNotificationUserInfoKey: Any] = [ + .locationKey: location, + ] + userInfo[.headingKey] = heading + NotificationCenter.default.post(name: .routeControllerWillReroute, object: self, userInfo: userInfo) self.lastRerouteLocation = location - getDirections(from: location, along: progress) { [weak self] (route, error) in + getDirections(from: location, routeProgress: progress) { [weak self] (route, error) in guard let strongSelf = self else { return } diff --git a/MapboxCoreNavigation/MBRouteController.h b/MapboxCoreNavigation/MBRouteController.h index 517934a8c79..bfbf603f66c 100644 --- a/MapboxCoreNavigation/MBRouteController.h +++ b/MapboxCoreNavigation/MBRouteController.h @@ -79,6 +79,16 @@ extern const _Nonnull MBRouteControllerNotificationUserInfoKey MBRouteController */ extern const _Nonnull MBRouteControllerNotificationUserInfoKey MBRouteControllerRawLocationKey; +/** + A key in the user info dictionary of a `MBRouteControllerProgressDidChangeNotification` or `MBRouteControllerWillRerouteNotification` notification. The corresponding value is a `CLHeading` object representing the current idealized user heading. The value may be `nil`. + */ +extern const _Nonnull MBRouteControllerNotificationUserInfoKey MBRouteControllerHeadingKey; + +/** + A key in the user info dictionary of a `MBRouteControllerProgressDidChangeNotification` or `MBRouteControllerWillRerouteNotification` notification. The corresponding value is a `CLHeading` object representing the current raw user heading. The value may be `nil`. + */ +extern const _Nonnull MBRouteControllerNotificationUserInfoKey MBRouteControllerRawHeadingKey; + /** A key in the user info dictionary of a `MBRouteControllerDidFailToRerouteNotification` notification. The corresponding value is an `NSError` object indicating why `RouteController` was unable to calculate a new route. */ diff --git a/MapboxCoreNavigation/MBRouteController.m b/MapboxCoreNavigation/MBRouteController.m index 940d235e54f..b41dd1351f5 100644 --- a/MapboxCoreNavigation/MBRouteController.m +++ b/MapboxCoreNavigation/MBRouteController.m @@ -11,6 +11,8 @@ const MBRouteControllerNotificationUserInfoKey MBRouteControllerRouteProgressKey = @"progress"; const MBRouteControllerNotificationUserInfoKey MBRouteControllerLocationKey = @"location"; const MBRouteControllerNotificationUserInfoKey MBRouteControllerRawLocationKey = @"rawLocation"; +const MBRouteControllerNotificationUserInfoKey MBRouteControllerHeadingKey = @"heading"; +const MBRouteControllerNotificationUserInfoKey MBRouteControllerRawHeadingKey = @"rawHeading"; const MBRouteControllerNotificationUserInfoKey MBRouteControllerRoutingErrorKey = @"error"; const MBRouteControllerNotificationUserInfoKey MBRouteControllerVisualInstructionKey = @"visualInstruction"; const MBRouteControllerNotificationUserInfoKey MBRouteControllerSpokenInstructionKey = @"spokenInstruction"; diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index d009f49b835..a0145f49157 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -96,7 +96,7 @@ open class RouteController: NSObject { return CLLocation(status.location) } - var heading: CLHeading? + public var heading: CLHeading? /** The most recently received user location. @@ -158,6 +158,11 @@ open class RouteController: NSObject { navigator.setRouteForRouteResponse(jsonString, route: 0, leg: 0) } + @objc public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { + heading = newHeading + // TODO: Cause a map view to update its camera. + } + public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } @@ -168,7 +173,9 @@ open class RouteController: NSObject { rawLocation = locations.last - locations.forEach { navigator.updateLocation(for: MBFixLocation($0)) } + for location in locations { + navigator.updateLocation(for: MBFixLocation(location)) + } let status = navigator.getStatusForTimestamp(location.timestamp) @@ -276,11 +283,14 @@ open class RouteController: NSObject { delegate?.router?(self, didUpdate: progress, with: location, rawLocation: rawLocation) //Fire the notification (for now) - NotificationCenter.default.post(name: .routeControllerProgressDidChange, object: self, userInfo: [ - RouteControllerNotificationUserInfoKey.routeProgressKey: progress, - RouteControllerNotificationUserInfoKey.locationKey: location, //guaranteed value - RouteControllerNotificationUserInfoKey.rawLocationKey: rawLocation //raw - ]) + var userInfo: [RouteControllerNotificationUserInfoKey: Any] = [ + .routeProgressKey: progress, + .locationKey: location, //guaranteed value + .rawLocationKey: rawLocation, //raw + ] + userInfo[.headingKey] = heading + userInfo[.rawHeadingKey] = heading // heading snapping not yet implemented (an unnecessary?) + NotificationCenter.default.post(name: .routeControllerProgressDidChange, object: self, userInfo: userInfo) } } @@ -358,9 +368,11 @@ extension RouteController: Router { } delegate?.router?(self, willRerouteFrom: location) - NotificationCenter.default.post(name: .routeControllerWillReroute, object: self, userInfo: [ - RouteControllerNotificationUserInfoKey.locationKey: location - ]) + var userInfo: [RouteControllerNotificationUserInfoKey: Any] = [ + .locationKey: location, + ] + userInfo[.headingKey] = heading + NotificationCenter.default.post(name: .routeControllerWillReroute, object: self, userInfo: userInfo) self.lastRerouteLocation = location @@ -368,7 +380,7 @@ extension RouteController: Router { if isRerouting { return } isRerouting = true - getDirections(from: location, along: progress) { [weak self] (route, error) in + getDirections(from: location, routeProgress: progress) { [weak self] (route, error) in self?.isRerouting = false guard let strongSelf: RouteController = self else { diff --git a/MapboxCoreNavigation/RouteProgress.swift b/MapboxCoreNavigation/RouteProgress.swift index 164b812c654..41ab7c00642 100644 --- a/MapboxCoreNavigation/RouteProgress.swift +++ b/MapboxCoreNavigation/RouteProgress.swift @@ -253,12 +253,14 @@ open class RouteProgress: NSObject { } } - func reroutingOptions(with current: CLLocation) -> RouteOptions { + func reroutingOptions(from location: CLLocation) -> RouteOptions { let oldOptions = route.routeOptions - let user = Waypoint(coordinate: current.coordinate) + let user = Waypoint(coordinate: location.coordinate) - if (current.course >= 0) { - user.heading = current.course + // A pedestrian can turn on a dime; there’s no problem with a route that starts out by turning the pedestrian around. + let transportType = currentLegProgress.currentStep.transportType + if transportType != .walking && location.course >= 0 { + user.heading = location.course user.headingAccuracy = RouteProgress.reroutingAccuracy } let newWaypoints = [user] + remainingWaypointsForCalculatingRoute() diff --git a/MapboxCoreNavigation/Router.swift b/MapboxCoreNavigation/Router.swift index 9fc3d460c93..07e84d87470 100644 --- a/MapboxCoreNavigation/Router.swift +++ b/MapboxCoreNavigation/Router.swift @@ -67,6 +67,10 @@ public protocol RouterDataSource { */ @objc var rawLocation: CLLocation? { get } + /** + The most recently received user heading, if any. + */ + @objc var heading: CLHeading? { get } /** If true, the `RouteController` attempts to calculate a more optimal route for the user on an interval defined by `RouteControllerProactiveReroutingInterval`. @@ -135,7 +139,7 @@ extension InternalRouter where Self: Router { if isRerouting { return } isRerouting = true - getDirections(from: location, along: routeProgress) { [weak self] (route, error) in + getDirections(from: location, routeProgress: routeProgress) { [weak self] (route, error) in self?.isRerouting = false guard let route = route else { return } @@ -156,9 +160,9 @@ extension InternalRouter where Self: Router { } } - func getDirections(from location: CLLocation, along progress: RouteProgress, completion: @escaping (_ route: Route?, _ error: Error?)->Void) { + func getDirections(from location: CLLocation, routeProgress: RouteProgress, completion: @escaping (_ route: Route?, _ error: Error?)->Void) { routeTask?.cancel() - let options = progress.reroutingOptions(with: location) + let options = routeProgress.reroutingOptions(from: location) lastRerouteLocation = location @@ -168,7 +172,7 @@ extension InternalRouter where Self: Router { return completion(nil, error) } - let mostSimilar = routes.mostSimilar(to: progress.route) + let mostSimilar = routes.mostSimilar(to: routeProgress.route) return completion(mostSimilar ?? routes.first, error) } } @@ -188,9 +192,8 @@ extension InternalRouter where Self: Router { func announce(reroute newRoute: Route, at location: CLLocation?, proactive: Bool) { var userInfo = [RouteControllerNotificationUserInfoKey: Any]() - if let location = location { - userInfo[.locationKey] = location - } + userInfo[.locationKey] = location + userInfo[.headingKey] = heading userInfo[.isProactiveKey] = proactive NotificationCenter.default.post(name: .routeControllerDidReroute, object: self, userInfo: userInfo) delegate?.router?(self, didRerouteAlong: routeProgress.route, at: location, proactive: proactive) diff --git a/MapboxNavigation/CarPlayNavigationViewController.swift b/MapboxNavigation/CarPlayNavigationViewController.swift index adf044a9cfa..4bd74abaa1b 100644 --- a/MapboxNavigation/CarPlayNavigationViewController.swift +++ b/MapboxNavigation/CarPlayNavigationViewController.swift @@ -272,7 +272,7 @@ public class CarPlayNavigationViewController: UIViewController, NavigationMapVie // Update the user puck mapView?.updatePreferredFrameRate(for: routeProgress) let camera = MGLMapCamera(lookingAtCenter: location.coordinate, altitude: 120, pitch: 60, heading: location.course) - mapView?.updateCourseTracking(location: location, camera: camera, animated: true) + mapView?.updateCourseTracking(location: location, heading: nil, camera: camera, animated: true) let congestionLevel = routeProgress.averageCongestionLevelRemainingOnLeg ?? .unknown guard let maneuver = carSession.upcomingManeuvers.first else { return } diff --git a/MapboxNavigation/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index 2cd234324e2..7ca871b34aa 100644 --- a/MapboxNavigation/NavigationMapView.swift +++ b/MapboxNavigation/NavigationMapView.swift @@ -178,7 +178,8 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { courseTrackingDelegate?.navigationMapViewDidStopTrackingCourse?(self) } if let location = userLocationForCourseTracking { - updateCourseTracking(location: location, animated: true) + // TODO: Is it OK to use the course until the next location update? + updateCourseTracking(location: location, heading: nil, animated: true) } } } @@ -193,7 +194,8 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { oldValue?.removeFromSuperview() if let userCourseView = userCourseView { if let location = userLocationForCourseTracking { - updateCourseTracking(location: location, animated: false) + // TODO: Is it OK to use the course until the next location update? + updateCourseTracking(location: location, heading: nil, animated: false) } else { userCourseView.center = userAnchorPoint } @@ -256,8 +258,9 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { super.layoutSubviews() //If the map is in tracking mode, make sure we update the camera after the layout pass. - if (tracksUserCourse) { - updateCourseTracking(location: userLocationForCourseTracking, camera:self.camera, animated: false) + if tracksUserCourse { + // TODO: Is it OK to use course until the next location update? + updateCourseTracking(location: userLocationForCourseTracking, heading: nil, camera: camera, animated: false) } } @@ -325,7 +328,22 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { tracksUserCourse = false } + @available(*, deprecated, message: "Use updateCourseTracking(location:heading:camera:animated:) instead.") @objc public func updateCourseTracking(location: CLLocation?, camera: MGLMapCamera? = nil, animated: Bool = false) { + updateCourseTracking(location: location, heading: nil, camera: camera, animated: animated) + } + + /** + Transitions the map view’s camera and user course view to reflect the given user location. + + If `tracksUserCourse` is set to `false`, this method does not reflect the user location; it only synchronizes the user course view with the map. + + - parameter location: The location that the map view will focus on. + - parameter heading: The direction the map view will face. If this parameter is `nil`, the `location` parameter’s `CLLocation.course` property is used instead. Only set this parameter to `CLLocationManager.heading` if the user is walking or using a vehicle with a very small turning radius. + - parameter camera: The camera to transition to. If this parameter is `nil`, the default camera looks out from `location`. + - parameter animated: True to animate the transition; false to transition instantaneously. + */ + @objc public func updateCourseTracking(location: CLLocation?, heading: CLHeading?, camera: MGLMapCamera? = nil, animated: Bool = false) { // While animating to overhead mode, don't animate the puck. let duration: TimeInterval = animated && !isAnimatingToOverheadMode ? 1 : 0 animatesUserLocation = animated @@ -335,7 +353,16 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { } if tracksUserCourse { - let newCamera = camera ?? MGLMapCamera(lookingAtCenter: location.coordinate, altitude: altitude, pitch: 45, heading: location.course) + var direction: CLLocationDirection? + if let trueHeading = heading?.trueHeading, trueHeading >= 0 { + direction = trueHeading + } else if let magneticHeading = heading?.magneticHeading, magneticHeading >= 0 { + direction = magneticHeading + } else if location.course >= 0 { + direction = location.course + } + + let newCamera = camera ?? MGLMapCamera(lookingAtCenter: location.coordinate, altitude: altitude, pitch: 45, heading: direction ?? -1) let function: CAMediaTimingFunction? = animated ? CAMediaTimingFunction(name: .linear) : nil let point = userAnchorPoint let padding = UIEdgeInsets(top: point.y, left: point.x, bottom: bounds.height - point.y, right: bounds.width - point.x) diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index c2701395469..829dc49782a 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -493,7 +493,9 @@ extension NavigationViewController: NavigationServiceDelegate { if snapsUserLocationAnnotationToRoute, userHasArrivedAndShouldPreventRerouting { - mapViewController?.mapView.updateCourseTracking(location: location, animated: true) + let isWalking = progress.currentLegProgress.currentStep.transportType == .walking + let heading = isWalking ? navigationService.locationManager.heading : nil + mapViewController?.mapView.updateCourseTracking(location: location, heading: heading, animated: true) } } diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 90596b84080..03226ed9bd1 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -229,15 +229,18 @@ class RouteMapViewController: UIViewController { mapView.tracksUserCourse = true + let isWalking = navService.routeProgress.currentLegProgress.currentStep.transportType == .walking + let heading = isWalking ? navService.locationManager.heading : nil + if let camera = pendingCamera { mapView.camera = camera - } else if let location = router.location, location.course > 0 { - mapView.updateCourseTracking(location: location, animated: false) + } else if let location = router.location, location.course >= 0 { + mapView.updateCourseTracking(location: location, heading: heading, animated: false) } else if let coordinates = router.routeProgress.currentLegProgress.currentStep.coordinates, let firstCoordinate = coordinates.first, coordinates.count > 1 { let secondCoordinate = coordinates[1] let course = firstCoordinate.direction(to: secondCoordinate) let newLocation = CLLocation(coordinate: router.location?.coordinate ?? firstCoordinate, altitude: 0, horizontalAccuracy: 0, verticalAccuracy: 0, course: course, speed: 0, timestamp: Date()) - mapView.updateCourseTracking(location: newLocation, animated: false) + mapView.updateCourseTracking(location: newLocation, heading: heading, animated: false) } else { mapView.setCamera(tiltedCamera, animated: false) } @@ -338,7 +341,9 @@ class RouteMapViewController: UIViewController { } @objc func applicationWillEnterForeground(notification: NSNotification) { - mapView.updateCourseTracking(location: router.location, animated: false) + let isWalking = navService.routeProgress.currentLegProgress.currentStep.transportType == .walking + let heading = isWalking ? navService.locationManager.heading : nil + mapView.updateCourseTracking(location: router.location, heading: heading, animated: false) } func updateMapOverlays(for routeProgress: RouteProgress) {