From c28e1e7d8fc9981a06b9b46e29d75a33bb8f74e9 Mon Sep 17 00:00:00 2001 From: Maxim Makhun Date: Tue, 13 Apr 2021 15:34:57 -0400 Subject: [PATCH] Navigation camera viewport API. (#2826) --- CHANGELOG.md | 8 + Example/AppDelegate+CarPlay.swift | 2 +- Example/CustomViewController.swift | 26 +- Example/ViewController+FreeDrive.swift | 11 +- Example/ViewController+GuidanceCards.swift | 3 +- Example/ViewController.swift | 119 +++-- MapboxNavigation.xcodeproj/project.pbxproj | 74 ++- .../xcshareddata/xcschemes/Bench.xcscheme | 2 +- .../xcschemes/Example-CarPlay.xcscheme | 2 +- .../xcshareddata/xcschemes/Example.xcscheme | 2 +- .../xcschemes/MapboxCoreNavigation.xcscheme | 2 +- .../xcschemes/MapboxNavigation.xcscheme | 2 +- Sources/MapboxNavigation/Array.swift | 63 +++ Sources/MapboxNavigation/BoundingBox.swift | 18 + .../CLLocationDirection.swift | 12 + Sources/MapboxNavigation/CameraOptions.swift | 26 + .../CameraStateTransition.swift | 55 +++ Sources/MapboxNavigation/CarPlayManager.swift | 91 ++-- .../CarPlayMapViewController.swift | 94 ++-- .../CarPlayNavigationViewController.swift | 115 +---- Sources/MapboxNavigation/Collection.swift | 11 + Sources/MapboxNavigation/MapView.swift | 6 - .../MapboxNavigation/NavigationCamera.swift | 216 +++++++++ .../NavigationCameraConstants.swift | 70 +++ .../NavigationCameraDebugView.swift | 195 ++++++++ .../NavigationCameraState.swift | 36 ++ .../NavigationCameraStateTransition.swift | 418 ++++++++++++++++ .../NavigationCameraType.swift | 16 + .../MapboxNavigation/NavigationMapView.swift | 457 +++++------------- .../NavigationMapViewDelegate.swift | 42 +- Sources/MapboxNavigation/NavigationView.swift | 6 +- .../NavigationViewController.swift | 124 ++--- .../NavigationViewportDataSource.swift | 424 ++++++++++++++++ Sources/MapboxNavigation/Route.swift | 4 + .../RouteMapViewController.swift | 355 ++++---------- Sources/MapboxNavigation/UIEdgeInsets.swift | 7 + Sources/MapboxNavigation/UserCourseView.swift | 38 +- .../MapboxNavigation/UserHaloCourseView.swift | 15 - .../MapboxNavigation/ViewportDataSource.swift | 56 +++ .../ViewportDataSourceType.swift | 27 ++ docs/jazzy.yml | 34 +- 41 files changed, 2227 insertions(+), 1057 deletions(-) create mode 100644 Sources/MapboxNavigation/BoundingBox.swift create mode 100644 Sources/MapboxNavigation/CLLocationDirection.swift create mode 100644 Sources/MapboxNavigation/CameraOptions.swift create mode 100644 Sources/MapboxNavigation/CameraStateTransition.swift create mode 100644 Sources/MapboxNavigation/Collection.swift create mode 100644 Sources/MapboxNavigation/NavigationCamera.swift create mode 100644 Sources/MapboxNavigation/NavigationCameraConstants.swift create mode 100644 Sources/MapboxNavigation/NavigationCameraDebugView.swift create mode 100644 Sources/MapboxNavigation/NavigationCameraState.swift create mode 100644 Sources/MapboxNavigation/NavigationCameraStateTransition.swift create mode 100644 Sources/MapboxNavigation/NavigationCameraType.swift create mode 100644 Sources/MapboxNavigation/NavigationViewportDataSource.swift create mode 100644 Sources/MapboxNavigation/ViewportDataSource.swift create mode 100644 Sources/MapboxNavigation/ViewportDataSourceType.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a5b1a0b5630..0ae10b9c63f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,14 @@ * Improved performance and decreased memory usage when downloading routing tiles. ([#2808](https://github.com/mapbox/mapbox-navigation-ios/pull/2808)) * Renamed `PassiveLocationManager.startUpdatingLocation(completionHandler:)` to `PassiveLocationManager.startUpdatingLocation()`. This method now runs synchronously like `CLLocationManager.startUpdatingLocation()`. ([#2823](https://github.com/mapbox/mapbox-navigation-ios/pull/2823)) +### Camera + +* Added Navigation Viewport Camera APIs, which allow to control camera viewport system frames based on various properties, such as: current location, some or all of the remaining route line coordinates, upcoming maneuvers etc. This allows to provide a camera viewport system, which is optimal for visualization and animation in navigation applications. ([#2826](https://github.com/mapbox/mapbox-navigation-ios/pull/2826)) +* Removed `CarPlayNavigationViewController.tracksUserCourse`, `NavigationMapView.defaultAltitude`, `NavigationMapView.zoomedOutMotorwayAltitude`, `NavigationMapView.longManeuverDistance`, `NavigationMapView.showsUserLocation`, `NavigationMapView.tracksUserCourse`, `NavigationMapView.enableFrameByFrameCourseViewTracking(for:)`, `NavigationMapView.updateCourseTracking(location:camera:animated:)` `NavigationMapView.defaultPadding`, `NavigationMapView.setOverheadCameraView(from:along:for:)`, `NavigationMapView.recenterMap()`, `NavigationMapViewDelegate.navigationMapViewUserAnchorPoint(_:)`, `NavigationMapViewCourseTrackingDelegate`, `NavigationViewController.pendingCamera` in favor of new Navigation Viewport Camera APIs. ([#2826](https://github.com/mapbox/mapbox-navigation-ios/pull/2826)) +* Replaced `CourseUpdatable.update(location:pitch:direction:animated:tracksUserCourse:)` with `CourseUpdatable.update(location:pitch:direction:animated:navigationCameraState:)` to provide more agile way of handling `NavigationCameraState`. ([#2826](https://github.com/mapbox/mapbox-navigation-ios/pull/2826)) +* Added `NavigationMapView.init(frame:navigationCameraType:)` to be able to provide type of `NavigationCamera`, which should be used for that specific instance of `NavigationMapView` (either iOS or CarPlay). ([#2826](https://github.com/mapbox/mapbox-navigation-ios/pull/2826)) +* Added `NavigationCamera`, `ViewportDataSourceType`, `ViewportDataSourceDelegate`, `NavigationCameraState` Navigation Viewport Camera APIs. By default Navigation SDK for iOS provides default camera behavior via `NavigationViewportDataSource` and `NavigationCameraStateTransition` classes. If you'd like to override current behavior use `ViewportDataSource` and `CameraStateTransition` protocols for custom behavior. ([#2826](https://github.com/mapbox/mapbox-navigation-ios/pull/2826)) + ### CarPlay * Removed deprecated `CarPlayNavigationDelegate.carPlayNavigationViewControllerDidArrive(_:)`. ([#2808](https://github.com/mapbox/mapbox-navigation-ios/pull/2808)) diff --git a/Example/AppDelegate+CarPlay.swift b/Example/AppDelegate+CarPlay.swift index 338d380638f..dfda6180348 100644 --- a/Example/AppDelegate+CarPlay.swift +++ b/Example/AppDelegate+CarPlay.swift @@ -57,7 +57,7 @@ extension AppDelegate: CarPlayManagerDelegate { // MARK: CarPlayManagerDelegate func carPlayManager(_ carPlayManager: CarPlayManager, didBeginNavigationWith service: NavigationService) { - currentAppRootViewController?.beginNavigationWithCarplay(navigationService: service) + currentAppRootViewController?.beginNavigationWithCarPlay(navigationService: service) carPlayManager.currentNavigator?.compassView.isHidden = false // Render part of the route that has been traversed with full transparency, to give the illusion of a disappearing route. diff --git a/Example/CustomViewController.swift b/Example/CustomViewController.swift index e30e95e2af0..cee70550959 100644 --- a/Example/CustomViewController.swift +++ b/Example/CustomViewController.swift @@ -4,10 +4,16 @@ import MapboxNavigation import MapboxDirections import MapboxMaps -// FIXME: Currently if `MapView` is created using storyboard crash occurs. class CustomViewController: UIViewController { + var destinationAnnotation: PointAnnotation! { + didSet { + navigationMapView.mapView.annotationManager.addAnnotation(destinationAnnotation) + } + } + var navigationService: NavigationService! + var simulateLocation = false var userIndexedRoute: IndexedRoute? @@ -34,6 +40,7 @@ class CustomViewController: UIViewController { super.viewDidLoad() navigationMapView.mapView.style.styleURL = .custom(url: URL(string: "mapbox://styles/mapbox-map-design/ckd6dqf981hi71iqlyn3e896y")!) + navigationMapView.userCourseView.isHidden = false let locationManager = simulateLocation ? SimulatedLocationManager(route: userIndexedRoute!.0) : NavigationLocationManager() navigationService = MapboxNavigationService(route: userIndexedRoute!.0, routeIndex: userIndexedRoute!.1, routeOptions: userRouteOptions!, locationSource: locationManager, simulating: simulateLocation ? .always : .onPoorGPS) @@ -51,13 +58,15 @@ class CustomViewController: UIViewController { // Start navigation navigationService.start() - // Center map on user - navigationMapView.recenterMap() - navigationMapView.mapView.on(.styleLoaded, handler: { [weak self] _ in guard let route = self?.navigationService.route else { return } self?.navigationMapView.show([route]) }) + + // By default `NavigationViewportDataSource` tracks location changes from `PassiveLocationDataSource`, to consume + // locations in active guidance navigation `ViewportDataSourceType` should be set to `.active`. + let navigationViewportDataSource = NavigationViewportDataSource(navigationMapView.mapView, viewportDataSourceType: .active) + navigationMapView.navigationCamera.viewportDataSource = navigationViewportDataSource } override func viewWillAppear(_ animated: Bool) { @@ -101,8 +110,8 @@ class CustomViewController: UIViewController { instructionsBannerView.updateDistance(for: routeProgress.currentLegProgress.currentStepProgress) instructionsBannerView.isHidden = false - // Update the user puck - navigationMapView.updateCourseTracking(location: location, animated: true) + // Update `UserCourseView` to be placed on the most recent location. + navigationMapView.updateUserCourseView(location, animated: true) } @objc func updateInstructionsBanner(notification: NSNotification) { @@ -122,7 +131,7 @@ class CustomViewController: UIViewController { } @IBAction func recenterMap(_ sender: Any) { - navigationMapView.recenterMap() + navigationMapView.navigationCamera.follow() } @IBAction func showFeedback(_ sender: Any) { @@ -172,8 +181,7 @@ class CustomViewController: UIViewController { updatePreviewBannerWith(step: step, maneuverStep: maneuverStep) // stop tracking user, and move camera to step location - navigationMapView.tracksUserCourse = false - navigationMapView.enableFrameByFrameCourseViewTracking(for: 1) + navigationMapView.navigationCamera.stop() navigationMapView.mapView.cameraManager.setCamera(centerCoordinate: maneuverStep.maneuverLocation, bearing: maneuverStep.initialHeading!, animated: true) diff --git a/Example/ViewController+FreeDrive.swift b/Example/ViewController+FreeDrive.swift index 3a0f6d17bd0..798d0bbb86d 100644 --- a/Example/ViewController+FreeDrive.swift +++ b/Example/ViewController+FreeDrive.swift @@ -10,9 +10,9 @@ import MapboxMaps extension ViewController { - func setupPassiveLocationManager(_ navigationMapView: NavigationMapView) { + func setupPassiveLocationManager() { setupFreeDriveStyledFeatures() - + let passiveLocationDataSource = PassiveLocationDataSource() let passiveLocationManager = PassiveLocationManager(dataSource: passiveLocationDataSource) navigationMapView.mapView.locationManager.overrideLocationProvider(with: passiveLocationManager) @@ -67,6 +67,12 @@ extension ViewController { color: .lightGray, lineWidth: 3.0, lineString: LineString([])) + + navigationMapView.mapView.on(.styleLoaded, handler: { [weak self] _ in + guard let self = self else { return } + self.addStyledFeature(self.trackStyledFeature) + self.addStyledFeature(self.rawTrackStyledFeature) + }) } func updateFreeDriveStyledFeatures() { @@ -116,7 +122,6 @@ extension ViewController { let branchNames = branchEdgeIdentifiers.flatMap { edgeNames(identifier: $0) } statusString += " at \(branchNames.joined(separator: ", "))" } - print(statusString) } func edgeNames(identifier: ElectronicHorizon.Edge.Identifier) -> [String] { diff --git a/Example/ViewController+GuidanceCards.swift b/Example/ViewController+GuidanceCards.swift index 61edbf61823..79b3dfb3da3 100644 --- a/Example/ViewController+GuidanceCards.swift +++ b/Example/ViewController+GuidanceCards.swift @@ -19,8 +19,7 @@ extension ViewController: InstructionsCardCollectionDelegate { let maneuverStep = leg.steps[stepIndex + 1] // stop tracking user, and move camera to step location - navigationMapView.tracksUserCourse = false - navigationMapView.enableFrameByFrameCourseViewTracking(for: 1) + navigationMapView.navigationCamera.stop() let camera = CameraOptions(center: maneuverStep.maneuverLocation, zoom: navigationMapView.mapView.zoom, diff --git a/Example/ViewController.swift b/Example/ViewController.swift index de79a98c416..0f98dbf99af 100755 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -25,6 +25,7 @@ class ViewController: UIViewController { typealias RouteRequestSuccess = ((RouteResponse) -> Void) typealias RouteRequestFailure = ((Error) -> Void) + typealias ActionHandler = (UIAlertAction) -> Void private var foundAllBuildings = false @@ -52,7 +53,7 @@ class ViewController: UIViewController { var response: RouteResponse? { didSet { guard let routes = response?.routes, let currentRoute = routes.first else { - clearMapView() + clearNavigationMapView() return } @@ -66,6 +67,30 @@ class ViewController: UIViewController { weak var activeNavigationViewController: NavigationViewController? + // MARK: - Initializer methods + + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + private func commonInit() { + if let appDelegate = UIApplication.shared.delegate as? AppDelegate { + appDelegate.currentAppRootViewController = self + } + } + + deinit { + if let navigationMapView = navigationMapView { + uninstall(navigationMapView) + } + } + // MARK: - UIViewController lifecycle methods override func viewDidLoad() { @@ -101,30 +126,16 @@ class ViewController: UIViewController { } private func configure(_ navigationMapView: NavigationMapView) { - setupPassiveLocationManager(navigationMapView) + setupPassiveLocationManager() navigationMapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(navigationMapView) navigationMapView.delegate = self - navigationMapView.mapView.on(.styleLoaded, handler: { [weak self] _ in - guard let self = self else { return } - self.addStyledFeature(self.trackStyledFeature) - self.addStyledFeature(self.rawTrackStyledFeature) - }) navigationMapView.mapView.update { $0.location.puckType = .puck2D() } - // TODO: Provide a reliable way of setting camera to current coordinate. - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - if let coordinate = navigationMapView.mapView.locationManager.latestLocation?.coordinate { - navigationMapView.mapView.cameraManager.setCamera(to: CameraOptions(center: coordinate, zoom: 13), - animated: true, - completion: nil) - } - } - setupGestureRecognizers() setupPerformActionBarButtonItem() } @@ -134,7 +145,7 @@ class ViewController: UIViewController { navigationMapView.removeFromSuperview() } - private func clearMapView() { + private func clearNavigationMapView() { startButton.isEnabled = false clearMap.isHidden = true longPressHintView.isHidden = false @@ -158,7 +169,7 @@ class ViewController: UIViewController { } @IBAction func clearMapPressed(_ sender: Any) { - clearMapView() + clearNavigationMapView() } @IBAction func startButtonPressed(_ sender: Any) { @@ -167,13 +178,13 @@ class ViewController: UIViewController { // MARK: - CarPlay navigation methods - public func beginNavigationWithCarplay(navigationService: NavigationService) { + public func beginNavigationWithCarPlay(navigationService: NavigationService) { let navigationViewController = activeNavigationViewController ?? self.navigationViewController(navigationService: navigationService) navigationViewController.didConnectToCarPlay() guard activeNavigationViewController == nil else { return } - presentAndRemoveMapview(navigationViewController, completion: nil) + present(navigationViewController) } func beginCarPlayNavigation() { @@ -189,8 +200,6 @@ class ViewController: UIViewController { private func presentActionsAlertController() { let alertController = UIAlertController(title: "Start Navigation", message: "Select the navigation type", preferredStyle: .actionSheet) - typealias ActionHandler = (UIAlertAction) -> Void - let basic: ActionHandler = { _ in self.startBasicNavigation() } let day: ActionHandler = { _ in self.startNavigation(styles: [DayStyle()]) } let night: ActionHandler = { _ in self.startNavigation(styles: [NightStyle()]) } @@ -232,7 +241,7 @@ class ViewController: UIViewController { // Example of building highlighting in 2D. navigationViewController.waypointStyle = .building - presentAndRemoveMapview(navigationViewController, completion: beginCarPlayNavigation) + present(navigationViewController, completion: beginCarPlayNavigation) } func startBasicNavigation() { @@ -253,21 +262,25 @@ class ViewController: UIViewController { // Control floating buttons position in a navigation view. navigationViewController.floatingButtonsPosition = .topTrailing - present(navigationViewController, completion: nil) + present(navigationViewController) } func startCustomNavigation() { - guard let route = response?.routes?.first, let responseOptions = response?.options, case let .route(routeOptions) = responseOptions else { return } - - guard let customViewController = storyboard?.instantiateViewController(withIdentifier: "custom") as? CustomViewController else { return } + guard let route = response?.routes?.first, + let responseOptions = response?.options, + case let .route(routeOptions) = responseOptions, + let customViewController = storyboard?.instantiateViewController(withIdentifier: "custom") as? CustomViewController else { return } customViewController.userIndexedRoute = (route, 0) customViewController.userRouteOptions = routeOptions - - // TODO: Add the ability to show destination annotation. customViewController.simulateLocation = simulationButton.isSelected - - present(customViewController, animated: true, completion: nil) + + present(customViewController, animated: true) { + if let destinationCoordinate = route.shape?.coordinates.last { + let destinationAnnotation = PointAnnotation(coordinate: destinationCoordinate) + customViewController.destinationAnnotation = destinationAnnotation + } + } } func startStyledNavigation() { @@ -278,7 +291,7 @@ class ViewController: UIViewController { let navigationViewController = NavigationViewController(for: route, routeIndex: 0, routeOptions: routeOptions, navigationOptions: options) navigationViewController.delegate = self - presentAndRemoveMapview(navigationViewController, completion: beginCarPlayNavigation) + present(navigationViewController, completion: beginCarPlayNavigation) } func startGuidanceCardsNavigation() { @@ -291,7 +304,7 @@ class ViewController: UIViewController { let navigationViewController = NavigationViewController(for: route, routeIndex: 0, routeOptions: routeOptions, navigationOptions: options) navigationViewController.delegate = self - presentAndRemoveMapview(navigationViewController, completion: beginCarPlayNavigation) + present(navigationViewController, completion: beginCarPlayNavigation) } // MARK: - UIGestureRecognizer methods @@ -315,11 +328,6 @@ class ViewController: UIViewController { let destinationCoordinate = navigationMapView.mapView.coordinate(for: gestureLocation, in: navigationMapView) - // TODO: Implement ability to get last annotation. - // if let annotation = navigationMapView.annotations?.last, waypoints.count > 2 { - // mapView.removeAnnotation(annotation) - // } - if waypoints.count > 1 { waypoints = Array(waypoints.dropFirst()) } @@ -342,12 +350,14 @@ class ViewController: UIViewController { let alertController = UIAlertController(title: "Perform action", message: "Select specific action to perform it", preferredStyle: .actionSheet) - typealias ActionHandler = (UIAlertAction) -> Void - let toggleDayNightStyle: ActionHandler = { _ in self.toggleDayNightStyle() } + let requestFollowCamera: ActionHandler = { _ in self.requestFollowCamera() } + let requestIdleCamera: ActionHandler = { _ in self.requestIdleCamera() } let actions: [(String, UIAlertAction.Style, ActionHandler?)] = [ ("Toggle Day/Night Style", .default, toggleDayNightStyle), + ("Request Following Camera", .default, requestFollowCamera), + ("Request Idle Camera", .default, requestIdleCamera), ("Cancel", .cancel, nil) ] @@ -370,6 +380,14 @@ class ViewController: UIViewController { } } + func requestFollowCamera() { + navigationMapView.navigationCamera.follow() + } + + func requestIdleCamera() { + navigationMapView.navigationCamera.stop() + } + func requestRoute() { guard waypoints.count > 0 else { return } guard let currentLocation = navigationMapView.mapView.locationManager.latestLocation?.internalLocation else { @@ -432,7 +450,7 @@ class ViewController: UIViewController { return navigationViewController } - func present(_ navigationViewController: NavigationViewController, completion: CompletionHandler?) { + func present(_ navigationViewController: NavigationViewController, completion: CompletionHandler? = nil) { navigationViewController.modalPresentationStyle = .fullScreen activeNavigationViewController = navigationViewController @@ -441,6 +459,12 @@ class ViewController: UIViewController { } } + func endCarPlayNavigation(canceled: Bool) { + if #available(iOS 12.0, *), let delegate = UIApplication.shared.delegate as? AppDelegate { + delegate.carPlayManager.currentNavigator?.exitNavigation(byCanceling: canceled) + } + } + func dismissActiveNavigationViewController() { activeNavigationViewController?.dismiss(animated: true) { self.activeNavigationViewController = nil @@ -453,17 +477,6 @@ class ViewController: UIViewController { return MapboxNavigationService(route: route, routeIndex: routeIndex, routeOptions: options, simulating: mode) } - func presentAndRemoveMapview(_ navigationViewController: NavigationViewController, completion: CompletionHandler?) { - navigationViewController.modalPresentationStyle = .fullScreen - activeNavigationViewController = navigationViewController - - present(navigationViewController, animated: true) { [weak self] in - completion?() - - self?.navigationMapView = nil - } - } - // MARK: - Utility methods func presentAlert(_ title: String? = nil, message: String? = nil) { @@ -525,7 +538,9 @@ extension ViewController: NavigationViewControllerDelegate { } func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) { + endCarPlayNavigation(canceled: canceled) dismissActiveNavigationViewController() + clearNavigationMapView() } } diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index 0dae5d90c45..28c53847770 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -226,17 +226,31 @@ 8A0E0A52257AD9C300C2E924 /* NightStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A0E0A51257AD9C300C2E924 /* NightStyle.swift */; }; 8A17635B25CC89D800737520 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A17635A25CC89D800737520 /* Expression.swift */; }; 8A1763D025CCC38C00737520 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A1763CF25CCC38C00737520 /* Double.swift */; }; + 8A2DFA8626168A300034A87E /* NavigationCameraDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A2DFA8526168A300034A87E /* NavigationCameraDebugView.swift */; }; + 8A30113C25DDCC8A00CE192A /* NavigationCameraConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A30113B25DDCC8A00CE192A /* NavigationCameraConstants.swift */; }; 8A3A219025EEC00200EDA999 /* CoreNavigationNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A3A218F25EEC00200EDA999 /* CoreNavigationNavigator.swift */; }; 8A41F5B225BF61AE00BD6FCF /* CarPlayActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A41F5B125BF61AE00BD6FCF /* CarPlayActivity.swift */; }; 8A41F5EC25BF624900BD6FCF /* MapOrnamentPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A41F5EB25BF624900BD6FCF /* MapOrnamentPosition.swift */; }; 8A41F63C25BF631500BD6FCF /* NavigationMapView+VanishingRouteLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A41F63A25BF631500BD6FCF /* NavigationMapView+VanishingRouteLine.swift */; }; 8A41F63D25BF631500BD6FCF /* NavigationMapView+BuildingHighlighting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A41F63B25BF631500BD6FCF /* NavigationMapView+BuildingHighlighting.swift */; }; + 8A44662B260A6C51008BA55E /* ViewportDataSourceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A44662A260A6C51008BA55E /* ViewportDataSourceType.swift */; }; + 8A446645260A7B24008BA55E /* BoundingBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A446644260A7B24008BA55E /* BoundingBox.swift */; }; + 8A8C3D98260175D20071D274 /* CLLocationDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A8C3D97260175D20071D274 /* CLLocationDirection.swift */; }; 8AA849E924E722410008EE59 /* WaypointStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AA849E824E722410008EE59 /* WaypointStyle.swift */; }; + 8AC3965325DC66570027A035 /* NavigationCameraType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC3965225DC66570027A035 /* NavigationCameraType.swift */; }; 8AC6191025881F8D00430AA8 /* route-with-mixed-road-classes.json in Resources */ = {isa = PBXBuildFile; fileRef = 8AC6190B25881F8D00430AA8 /* route-with-mixed-road-classes.json */; }; 8AC6191125881F8D00430AA8 /* route-with-missing-road-classes.json in Resources */ = {isa = PBXBuildFile; fileRef = 8AC6190C25881F8D00430AA8 /* route-with-missing-road-classes.json */; }; 8AC6191225881F8D00430AA8 /* route-with-not-present-road-classes.json in Resources */ = {isa = PBXBuildFile; fileRef = 8AC6190D25881F8D00430AA8 /* route-with-not-present-road-classes.json */; }; 8AC6191325881F8D00430AA8 /* route-with-road-classes-single-congestion.json in Resources */ = {isa = PBXBuildFile; fileRef = 8AC6190E25881F8D00430AA8 /* route-with-road-classes-single-congestion.json */; }; 8AC6191425881F8D00430AA8 /* route-with-same-congestion-different-road-classes.json in Resources */ = {isa = PBXBuildFile; fileRef = 8AC6190F25881F8D00430AA8 /* route-with-same-congestion-different-road-classes.json */; }; + 8AD866F625CA1BF10019A638 /* NavigationCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AD866EB25CA1BF00019A638 /* NavigationCamera.swift */; }; + 8AD866F725CA1BF10019A638 /* NavigationCameraStateTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AD866EC25CA1BF00019A638 /* NavigationCameraStateTransition.swift */; }; + 8AD866F925CA1BF10019A638 /* ViewportDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AD866EE25CA1BF00019A638 /* ViewportDataSource.swift */; }; + 8AD866FB25CA1BF10019A638 /* NavigationCameraState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AD866F025CA1BF10019A638 /* NavigationCameraState.swift */; }; + 8AD866FD25CA1BF10019A638 /* CameraStateTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AD866F225CA1BF10019A638 /* CameraStateTransition.swift */; }; + 8AD866FF25CA1BF10019A638 /* NavigationViewportDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AD866F425CA1BF10019A638 /* NavigationViewportDataSource.swift */; }; + 8AE9081225FAA53300F37077 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE9081125FAA53300F37077 /* Collection.swift */; }; + 8AFF437125F847340053CBB1 /* CameraOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFF437025F847340053CBB1 /* CameraOptions.swift */; }; 8D07C5A820B612310093D779 /* EmptyStyle.json in Resources */ = {isa = PBXBuildFile; fileRef = 8D07C5A720B612310093D779 /* EmptyStyle.json */; }; 8D1A5CD2212DDFCD0059BA4A /* DispatchTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D1A5CD1212DDFCD0059BA4A /* DispatchTimer.swift */; }; 8D24A2F62040960C0098CBF8 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D24A2F52040960C0098CBF8 /* UIEdgeInsets.swift */; }; @@ -733,6 +747,8 @@ 8A0E0A51257AD9C300C2E924 /* NightStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightStyle.swift; sourceTree = ""; }; 8A17635A25CC89D800737520 /* Expression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Expression.swift; sourceTree = ""; }; 8A1763CF25CCC38C00737520 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; + 8A2DFA8526168A300034A87E /* NavigationCameraDebugView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationCameraDebugView.swift; sourceTree = ""; }; + 8A30113B25DDCC8A00CE192A /* NavigationCameraConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCameraConstants.swift; sourceTree = ""; }; 8A3A218F25EEC00200EDA999 /* CoreNavigationNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreNavigationNavigator.swift; sourceTree = ""; }; 8A41F5B125BF61AE00BD6FCF /* CarPlayActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarPlayActivity.swift; sourceTree = ""; }; 8A41F5EB25BF624900BD6FCF /* MapOrnamentPosition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapOrnamentPosition.swift; sourceTree = ""; }; @@ -743,14 +759,26 @@ 8A41F65C25BF642200BD6FCF /* MapboxMaps.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MapboxMaps.xcframework; path = MapboxNavigation/MapboxMaps/MapboxMaps.xcframework; sourceTree = ""; }; 8A41F65D25BF642200BD6FCF /* MapboxCommon.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MapboxCommon.xcframework; path = MapboxNavigation/MapboxMaps/MapboxCommon.xcframework; sourceTree = ""; }; 8A41F65E25BF642200BD6FCF /* MapboxCoreMaps.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MapboxCoreMaps.xcframework; path = MapboxNavigation/MapboxMaps/MapboxCoreMaps.xcframework; sourceTree = ""; }; + 8A44662A260A6C51008BA55E /* ViewportDataSourceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewportDataSourceType.swift; sourceTree = ""; }; + 8A446644260A7B24008BA55E /* BoundingBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoundingBox.swift; sourceTree = ""; }; + 8A8C3D97260175D20071D274 /* CLLocationDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationDirection.swift; sourceTree = ""; }; 8A997F2A2581A9E4005D50D6 /* MapboxMapsAnnotations.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MapboxMapsAnnotations.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8A997F2D2581A9EA005D50D6 /* MapboxMaps.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MapboxMaps.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8AA849E824E722410008EE59 /* WaypointStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointStyle.swift; sourceTree = ""; }; + 8AC3965225DC66570027A035 /* NavigationCameraType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCameraType.swift; sourceTree = ""; }; 8AC6190B25881F8D00430AA8 /* route-with-mixed-road-classes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "route-with-mixed-road-classes.json"; sourceTree = ""; }; 8AC6190C25881F8D00430AA8 /* route-with-missing-road-classes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "route-with-missing-road-classes.json"; sourceTree = ""; }; 8AC6190D25881F8D00430AA8 /* route-with-not-present-road-classes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "route-with-not-present-road-classes.json"; sourceTree = ""; }; 8AC6190E25881F8D00430AA8 /* route-with-road-classes-single-congestion.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "route-with-road-classes-single-congestion.json"; sourceTree = ""; }; 8AC6190F25881F8D00430AA8 /* route-with-same-congestion-different-road-classes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "route-with-same-congestion-different-road-classes.json"; sourceTree = ""; }; + 8AD866EB25CA1BF00019A638 /* NavigationCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationCamera.swift; sourceTree = ""; }; + 8AD866EC25CA1BF00019A638 /* NavigationCameraStateTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationCameraStateTransition.swift; sourceTree = ""; }; + 8AD866EE25CA1BF00019A638 /* ViewportDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewportDataSource.swift; sourceTree = ""; }; + 8AD866F025CA1BF10019A638 /* NavigationCameraState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationCameraState.swift; sourceTree = ""; }; + 8AD866F225CA1BF10019A638 /* CameraStateTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraStateTransition.swift; sourceTree = ""; }; + 8AD866F425CA1BF10019A638 /* NavigationViewportDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationViewportDataSource.swift; sourceTree = ""; }; + 8AE9081125FAA53300F37077 /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; + 8AFF437025F847340053CBB1 /* CameraOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraOptions.swift; sourceTree = ""; }; 8B808F852487CFEC00EEE453 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Main.strings; sourceTree = ""; }; 8B808F862487CFEC00EEE453 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Navigation.strings; sourceTree = ""; }; 8B808F892487CFEC00EEE453 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; @@ -1201,6 +1229,7 @@ 351BEBD81E5BCC28006FE110 /* MapboxNavigation */ = { isa = PBXGroup; children = ( + 8AD866EA25CA1BC90019A638 /* Camera */, 8A0E0A66257ADD3D00C2E924 /* Navigation */, 8A0E0A6E257AE59100C2E924 /* Instructions */, 8A0E0A6D257AE4B300C2E924 /* Cache */, @@ -1493,6 +1522,23 @@ name = Instructions; sourceTree = ""; }; + 8AD866EA25CA1BC90019A638 /* Camera */ = { + isa = PBXGroup; + children = ( + 8AD866EB25CA1BF00019A638 /* NavigationCamera.swift */, + 8AC3965225DC66570027A035 /* NavigationCameraType.swift */, + 8AD866F025CA1BF10019A638 /* NavigationCameraState.swift */, + 8AD866F225CA1BF10019A638 /* CameraStateTransition.swift */, + 8AD866EC25CA1BF00019A638 /* NavigationCameraStateTransition.swift */, + 8AD866EE25CA1BF00019A638 /* ViewportDataSource.swift */, + 8AD866F425CA1BF10019A638 /* NavigationViewportDataSource.swift */, + 8A44662A260A6C51008BA55E /* ViewportDataSourceType.swift */, + 8A30113B25DDCC8A00CE192A /* NavigationCameraConstants.swift */, + 8A2DFA8526168A300034A87E /* NavigationCameraDebugView.swift */, + ); + name = Camera; + sourceTree = ""; + }; 8DF8E4DD2202694100B29FEF /* Documents */ = { isa = PBXGroup; children = ( @@ -1608,6 +1654,10 @@ 8A17635A25CC89D800737520 /* Expression.swift */, 8A1763CF25CCC38C00737520 /* Double.swift */, B419BFF125F00A9C0086639B /* Feature.swift */, + 8AFF437025F847340053CBB1 /* CameraOptions.swift */, + 8AE9081125FAA53300F37077 /* Collection.swift */, + 8A8C3D97260175D20071D274 /* CLLocationDirection.swift */, + 8A446644260A7B24008BA55E /* BoundingBox.swift */, ); name = Extensions; sourceTree = ""; @@ -2029,7 +2079,7 @@ New, ); LastSwiftUpdateCheck = 1220; - LastUpgradeCheck = 1230; + LastUpgradeCheck = 1240; ORGANIZATIONNAME = Mapbox; TargetAttributes = { 351BEBD61E5BCC28006FE110 = { @@ -2436,6 +2486,7 @@ 350E2C5F22707EB80014CEB3 /* UIScreen.swift in Sources */, 8DB45E90201698EB001EA6A3 /* UIStackView.swift in Sources */, B419BFF225F00A9C0086639B /* Feature.swift in Sources */, + 8A44662B260A6C51008BA55E /* ViewportDataSourceType.swift in Sources */, 351BEBFC1E5BCC63006FE110 /* NavigationViewController.swift in Sources */, AE7DE6C621A47A23002653D1 /* CarPlaySearchController+CPSearchTemplateDelegate.swift in Sources */, 8D8EA9BC20575CD80077F478 /* FeedbackCollectionViewCell.swift in Sources */, @@ -2443,13 +2494,16 @@ 35E407681F5625FF00EFC814 /* StyleKitMarker.swift in Sources */, C588C3C21F33882100520EF2 /* String.swift in Sources */, C53208AB1E81FFB900910266 /* NavigationMapView.swift in Sources */, + 8A30113C25DDCC8A00CE192A /* NavigationCameraConstants.swift in Sources */, 16EF6C22211BA4B300AA580B /* CarPlayMapViewController.swift in Sources */, 8A41F63C25BF631500BD6FCF /* NavigationMapView+VanishingRouteLine.swift in Sources */, + 8AD866F725CA1BF10019A638 /* NavigationCameraStateTransition.swift in Sources */, 351BEBF61E5BCC63006FE110 /* RouteMapViewController.swift in Sources */, 16A509D7202BC0CA0011D788 /* ImageDownload.swift in Sources */, 8A1763D025CCC38C00737520 /* Double.swift in Sources */, 8DB63A3A1FBBCA2200928389 /* RatingControl.swift in Sources */, 353EC9D71FB09708002EB0AB /* StepsViewController.swift in Sources */, + 8AE9081225FAA53300F37077 /* Collection.swift in Sources */, 35726EE81F0856E900AFA1B6 /* DayStyle.swift in Sources */, 2B91C9B12416357700E532A5 /* MapboxSpeechSynthesizer.swift in Sources */, AE47A32C22B1F6AE0096458C /* InstructionsCardViewController.swift in Sources */, @@ -2462,6 +2516,7 @@ 8AA849E924E722410008EE59 /* WaypointStyle.swift in Sources */, 8A41F5EC25BF624900BD6FCF /* MapOrnamentPosition.swift in Sources */, 359D1B281FFE70D30052FA42 /* NavigationView.swift in Sources */, + 8AD866FF25CA1BF10019A638 /* NavigationViewportDataSource.swift in Sources */, C5FFAC1520D96F5C009E7F98 /* CarPlayNavigationViewController.swift in Sources */, 8DE879661FBB9980002F06C0 /* EndOfRouteViewController.swift in Sources */, AE47A33422B1F6AE0096458C /* InstructionsCardContainerView.swift in Sources */, @@ -2487,6 +2542,7 @@ AE87207E22CF97B900D7DAB7 /* InstructionsCardCollectionDelegate.swift in Sources */, AE47A33022B1F6AE0096458C /* InstructionsCardView.swift in Sources */, 8DCB4248218A540A00D6FCAD /* NavigationComponent.swift in Sources */, + 8AD866FB25CA1BF10019A638 /* NavigationCameraState.swift in Sources */, 351BEC051E5BCC6C006FE110 /* LaneView.swift in Sources */, C5A7EC5C1FD610A80008B9BA /* VisualInstructionComponent.swift in Sources */, CFD47D9020FD85EC00BC1E49 /* AccountManager.swift in Sources */, @@ -2497,8 +2553,11 @@ 8A41F5B225BF61AE00BD6FCF /* CarPlayActivity.swift in Sources */, DA66063023B32F99007832E5 /* Array.swift in Sources */, 8DEDEF3421E3FBE80049E114 /* NavigationViewControllerDelegate.swift in Sources */, + 8A446645260A7B24008BA55E /* BoundingBox.swift in Sources */, + 8AD866F625CA1BF10019A638 /* NavigationCamera.swift in Sources */, 8D5DFFF1207C04840093765A /* NSAttributedString.swift in Sources */, 35CF34B11F0A733200C2692E /* UIFont.swift in Sources */, + 8AD866F925CA1BF10019A638 /* ViewportDataSource.swift in Sources */, 4316D95C24340555000DD8F8 /* Match.swift in Sources */, 8DEB4066220CE596008BAAB4 /* NavigationMapViewDelegate.swift in Sources */, 359D283C1F9DC14F00FDE9C9 /* UICollectionView.swift in Sources */, @@ -2509,12 +2568,16 @@ 2B5407EB24470B0A006C820B /* AVAudioSession.swift in Sources */, 35F611C41F1E1C0500C43249 /* FeedbackViewController.swift in Sources */, 353AA5601FCEF583009F0384 /* StyleManager.swift in Sources */, + 8AC3965325DC66570027A035 /* NavigationCameraType.swift in Sources */, 35F520C01FB482A200FC9C37 /* NextBannerView.swift in Sources */, + 8AD866FD25CA1BF10019A638 /* CameraStateTransition.swift in Sources */, C5A6B2DD1F4CE8E8004260EA /* StyleType.swift in Sources */, C5381F03204E052A00A5493E /* UIDevice.swift in Sources */, + 8A2DFA8626168A300034A87E /* NavigationCameraDebugView.swift in Sources */, 351BEC021E5BCC63006FE110 /* UIView.swift in Sources */, 2EBF20AE25D6F89000DB7BF2 /* Utils.swift in Sources */, 160D8279205996DA00D278D6 /* DataCache.swift in Sources */, + 8AFF437125F847340053CBB1 /* CameraOptions.swift in Sources */, 351BEBF21E5BCC63006FE110 /* Style.swift in Sources */, 43FB386923A202420064481E /* Route.swift in Sources */, 3EA937B1F4DF73EB004BA6BE /* InstructionPresenter.swift in Sources */, @@ -2522,6 +2585,7 @@ 3EA93A1FEFDDB709DE84BED9 /* ImageRepository.swift in Sources */, 2B8098412411375700FED452 /* SpeechSynthesizing.swift in Sources */, 3EA9371104016CD402547F1A /* ImageCache.swift in Sources */, + 8A8C3D98260175D20071D274 /* CLLocationDirection.swift in Sources */, 8D4B60E7219CBEB300C41906 /* CarPlayManagerDelegate.swift in Sources */, 3EA9369C33A8F10DAE9043AA /* ImageDownloader.swift in Sources */, 3EA9301B03F8679BEDD4795F /* Cache.swift in Sources */, @@ -3366,10 +3430,10 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "com.mapbox.Example-CarPlay"; + PRODUCT_BUNDLE_IDENTIFIER = "com.Mapbox.CarPlay-Example"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "69c90fd8-c53b-41a4-ac73-5bc11068a49a"; - PROVISIONING_PROFILE_SPECIFIER = "Navigation Example"; + PROVISIONING_PROFILE_SPECIFIER = "CarPlay Provisioning Profile"; SWIFT_OBJC_BRIDGING_HEADER = "Example/Example-Swift-BridgingHeader.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -3392,10 +3456,10 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "com.mapbox.Example-CarPlay"; + PRODUCT_BUNDLE_IDENTIFIER = "com.Mapbox.CarPlay-Example"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "69c90fd8-c53b-41a4-ac73-5bc11068a49a"; - PROVISIONING_PROFILE_SPECIFIER = "Navigation Example"; + PROVISIONING_PROFILE_SPECIFIER = "CarPlay Provisioning Profile"; SWIFT_OBJC_BRIDGING_HEADER = "Example/Example-Swift-BridgingHeader.h"; }; name = Release; diff --git a/MapboxNavigation.xcodeproj/xcshareddata/xcschemes/Bench.xcscheme b/MapboxNavigation.xcodeproj/xcshareddata/xcschemes/Bench.xcscheme index 28eb4c1d79d..dfeebef78d2 100644 --- a/MapboxNavigation.xcodeproj/xcshareddata/xcschemes/Bench.xcscheme +++ b/MapboxNavigation.xcodeproj/xcshareddata/xcschemes/Bench.xcscheme @@ -1,6 +1,6 @@ [CLLocationCoordinate2D] { + return self.map({ coords -> [CLLocationCoordinate2D] in + if let coords = coords { + return coords + } else { + return [kCLLocationCoordinate2DInvalid] + } + }).reduce([], +) + } +} + +extension Array where Iterator.Element == CLLocationCoordinate2D { + + func sliced(from: CLLocationCoordinate2D? = nil, to: CLLocationCoordinate2D? = nil) -> [CLLocationCoordinate2D] { + return LineString(self).sliced(from: from, to: to)?.coordinates ?? [] + } + + func distance(from: CLLocationCoordinate2D? = nil, to: CLLocationCoordinate2D? = nil) -> CLLocationDistance? { + return LineString(self).distance(from: from, to: to) + } + + func trimmed(from: CLLocationCoordinate2D? = nil, distance: CLLocationDistance) -> [CLLocationCoordinate2D] { + if let fromCoord = from ?? self.first { + return LineString(self).trimmed(from: fromCoord, distance: distance)?.coordinates ?? [] + } else { + return [] + } + } + + var centerCoordinate: CLLocationCoordinate2D { + let avgLat = self.map({ $0.latitude }).reduce(0.0, +) / Double(self.count) + let avgLng = self.map({ $0.longitude }).reduce(0.0, +) / Double(self.count) + + return CLLocationCoordinate2D(latitude: avgLat, longitude: avgLng) + } +} + +extension Array where Iterator.Element == CGPoint { + + var boundingBoxPoints: [CGPoint] { + let yCoordinates = self.map({ $0.y }) + let xCoordinates = self.map({ $0.x }) + if let yMax = yCoordinates.max(), + let xMin = xCoordinates.min(), + let yMin = yCoordinates.min(), + let xMax = xCoordinates.max() { + let topLeftPoint = CGPoint(x: xMin, y: yMin) + let topRightPoint = CGPoint(x: xMax, y: yMin) + let bottomRightPoint = CGPoint(x: xMax, y: yMax) + let bottomLeftPoint = CGPoint(x: xMin, y: yMax) + + return [topLeftPoint, topRightPoint, bottomRightPoint, bottomLeftPoint] + } + + return [] + } +} diff --git a/Sources/MapboxNavigation/BoundingBox.swift b/Sources/MapboxNavigation/BoundingBox.swift new file mode 100644 index 00000000000..027606af03b --- /dev/null +++ b/Sources/MapboxNavigation/BoundingBox.swift @@ -0,0 +1,18 @@ +import Turf +import CoreGraphics + +extension BoundingBox { + + /** + Returns zoom level inside of specific `CGSize`, in which `BoundingBox` was fit to. + */ + func zoomLevel(fitTo size: CGSize) -> Double { + let latitudeFraction = (self.northEast.latitude.toRadians() - self.southWest.latitude.toRadians()) / .pi + let longitudeDiff = self.northEast.longitude - self.southWest.longitude + let longitudeFraction = ((longitudeDiff < 0) ? (longitudeDiff + 360) : longitudeDiff) / 360 + let latitudeZoom = log(Double(size.height) / 512.0 / latitudeFraction) / M_LN2 + let longitudeZoom = log(Double(size.width) / 512.0 / longitudeFraction) / M_LN2 + + return min(latitudeZoom, longitudeZoom, 21.0) + } +} diff --git a/Sources/MapboxNavigation/CLLocationDirection.swift b/Sources/MapboxNavigation/CLLocationDirection.swift new file mode 100644 index 00000000000..b9b452473cf --- /dev/null +++ b/Sources/MapboxNavigation/CLLocationDirection.swift @@ -0,0 +1,12 @@ +import CoreLocation + +extension CLLocationDirection { + + /** + Returns shortest rotation between two angles. + */ + func shortestRotation(angle: CLLocationDirection) -> CLLocationDirection { + guard !self.isNaN && !angle.isNaN else { return 0.0 } + return (self - angle).wrap(min: -180.0, max: 180.0) + } +} diff --git a/Sources/MapboxNavigation/CameraOptions.swift b/Sources/MapboxNavigation/CameraOptions.swift new file mode 100644 index 00000000000..9352f6e0db2 --- /dev/null +++ b/Sources/MapboxNavigation/CameraOptions.swift @@ -0,0 +1,26 @@ +import MapboxMaps + +extension CameraOptions { + + /** + Returns description of all properties in `CameraOptions`. + */ + public override var debugDescription: String { + var propertiesCount: UInt32 = 0 + let properties = class_copyPropertyList(CameraOptions.self, &propertiesCount) + + var description = [String]() + for i in 0.. Void)) + + /** + Method, which performs camera transition to the `NavigationCameraState.overview` state. + + - parameter cameraOptions: Instance of `CameraOptions`, which describes viewpoint of the `MapView`. + - parameter completion: Completion handler, which is called after performing transition. + */ + func transitionToOverview(_ cameraOptions: CameraOptions, completion: @escaping (() -> Void)) + + /** + Method, which performs camera update, when already in the `NavigationCameraState.following` state. + + - parameter cameraOptions: Instance of `CameraOptions`, which describes viewpoint of the `MapView`. + */ + func updateForFollowing(_ cameraOptions: CameraOptions) + + /** + Method, which performs camera update, when already in the `NavigationCameraState.overview` state. + + - parameter cameraOptions: Instance of `CameraOptions`, which describes viewpoint of the `MapView`. + */ + func updateForOverview(_ cameraOptions: CameraOptions) + + /** + Method, which cancels current transition. + */ + func cancelPendingTransition() +} diff --git a/Sources/MapboxNavigation/CarPlayManager.swift b/Sources/MapboxNavigation/CarPlayManager.swift index b70042ab351..28b5cffa164 100644 --- a/Sources/MapboxNavigation/CarPlayManager.swift +++ b/Sources/MapboxNavigation/CarPlayManager.swift @@ -35,24 +35,9 @@ public class CarPlayManager: NSObject { navigationService?.simulationSpeedMultiplier = simulatedSpeedMultiplier } } - - var trackingStateObservation: NSKeyValueObservation? - - deinit { - trackingStateObservation = nil - } public fileprivate(set) var mainMapTemplate: CPMapTemplate? - public fileprivate(set) weak var currentNavigator: CarPlayNavigationViewController? { - didSet { - if let controller = currentNavigator { - trackingStateObservation = controller.observe(\.tracksUserCourse) { [weak self] (controller, change) in - let imageName = controller.tracksUserCourse ? "carplay_overview" : "carplay_locate" - self?.userTrackingButton.image = UIImage(named: imageName, in: .mapboxNavigation, compatibleWith: nil) - } - } - } - } + public fileprivate(set) weak var currentNavigator: CarPlayNavigationViewController? internal var mapTemplateProvider: MapTemplateProvider @@ -140,10 +125,13 @@ public class CarPlayManager: NSObject { */ public lazy var userTrackingButton: CPMapButton = { let userTrackingButton = CPMapButton { [weak self] button in - guard let navigationViewController = self?.currentNavigator else { - return + guard let navigationMapView = self?.currentNavigator?.navigationMapView else { return } + + if navigationMapView.navigationCamera.state == .following { + navigationMapView.navigationCamera.moveToOverview() + } else { + navigationMapView.navigationCamera.follow() } - navigationViewController.tracksUserCourse = !navigationViewController.tracksUserCourse } userTrackingButton.image = UIImage(named: "carplay_overview", in: .mapboxNavigation, compatibleWith: nil) @@ -211,6 +199,33 @@ public class CarPlayManager: NSObject { self.mapTemplate(mapTemplate, startedTrip: trip, using: routeChoice) } } + + func subscribeForNotifications() { + NotificationCenter.default.addObserver(self, + selector: #selector(navigationCameraStateDidChange(_:)), + name: .navigationCameraStateDidChange, + object: currentNavigator?.navigationMapView?.navigationCamera) + } + + func unsubscribeFromNotifications() { + NotificationCenter.default.removeObserver(self, + name: .navigationCameraStateDidChange, + object: currentNavigator?.navigationMapView?.navigationCamera) + } + + @objc func navigationCameraStateDidChange(_ notification: Notification) { + guard let state = notification.userInfo?[NavigationCamera.NotificationUserInfoKey.state] as? NavigationCameraState else { return } + switch state { + case .idle: + break + case .transitionToFollowing, .following: + userTrackingButton.image = UIImage(named: "carplay_overview", in: .mapboxNavigation, compatibleWith: nil) + break + case .transitionToOverview, .overview: + userTrackingButton.image = UIImage(named: "carplay_locate", in: .mapboxNavigation, compatibleWith: nil) + break + } + } } // MARK: - CPApplicationDelegate methods @@ -237,6 +252,8 @@ extension CarPlayManager: CPApplicationDelegate { interfaceController.setRootTemplate(mapTemplate, animated: false) eventsManager.sendCarPlayConnectEvent() + + subscribeForNotifications() } public func application(_ application: UIApplication, didDisconnectCarInterfaceController interfaceController: CPInterfaceController, from window: CPWindow) { @@ -333,30 +350,18 @@ extension CarPlayManager: CPInterfaceControllerDelegate { let navigationMapView = mapViewController.navigationMapView navigationMapView.removeRoutes() navigationMapView.removeWaypoints() - - // Since tracking mode is no longer part of `MapView` functionality camera is used directly to - // zoom-in to most recent location. - let latestLocation = navigationMapView.mapView.locationManager.latestLocation - navigationMapView.mapView.cameraManager.setCamera(centerCoordinate: latestLocation?.coordinate, - zoom: 12.0, - bearing: latestLocation?.course, - pitch: 0, - animated: true) } } + public func templateWillDisappear(_ template: CPTemplate, animated: Bool) { guard let interface = interfaceController else { return } let onFreedriveMapOrNavigating = interface.templates.count == 1 guard let top = interface.topTemplate, - type(of: top) == CPSearchTemplate.self || onFreedriveMapOrNavigating else { return } - - if onFreedriveMapOrNavigating { - carPlayMapViewController?.isOverviewingRoutes = false - } + type(of: top) == CPSearchTemplate.self || onFreedriveMapOrNavigating else { return } - carPlayMapViewController?.resetCamera(animated: false) + navigationMapView?.navigationCamera.follow() } } @@ -483,8 +488,7 @@ extension CarPlayManager: CPMapTemplateDelegate { navigationViewController.carPlayNavigationDelegate = self currentNavigator = navigationViewController - carPlayMapViewController.isOverviewingRoutes = false - carPlayMapViewController.present(navigationViewController, animated: true, completion: nil) + carPlayMapViewController.present(navigationViewController, animated: true) let navigationMapView = carPlayMapViewController.navigationMapView navigationMapView.removeRoutes() @@ -525,7 +529,6 @@ extension CarPlayManager: CPMapTemplateDelegate { guard let carPlayMapViewController = carPlayMapViewController else { return } - carPlayMapViewController.isOverviewingRoutes = true let navigationMapView = carPlayMapViewController.navigationMapView let (route, _, _) = routeChoice.userInfo as! (Route, Int, RouteOptions) @@ -533,10 +536,6 @@ extension CarPlayManager: CPMapTemplateDelegate { timeRemaining: route.expectedTravelTime) mapTemplate.updateEstimates(estimates, for: trip) - // FIXME: Unable to tilt map during route selection -- https://github.com/mapbox/mapbox-gl-native/issues/2259 - let topDownCamera = navigationMapView.mapView.camera - topDownCamera.pitch = 0 - navigationMapView.mapView.cameraManager.setCamera(to: topDownCamera, completion: nil) navigationMapView.showcase([route]) delegate?.carPlayManager(self, selectedPreviewFor: trip, using: routeChoice) @@ -551,12 +550,6 @@ extension CarPlayManager: CPMapTemplateDelegate { navigationMapView.removeWaypoints() delegate?.carPlayManagerDidEndNavigation(self) } - - public func mapTemplateDidBeginPanGesture(_ mapTemplate: CPMapTemplate) { - if let navigationViewController = currentNavigator, mapTemplate == navigationViewController.mapTemplate { - navigationViewController.beginPanGesture() - } - } public func mapTemplate(_ mapTemplate: CPMapTemplate, didEndPanGestureWithVelocity velocity: CGPoint) { // TODO: Find a way to control `recenterButton` visibility. @@ -617,6 +610,9 @@ extension CarPlayManager: CPMapTemplateDelegate { public func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) { guard let carPlayMapViewController = carPlayMapViewController else { return } + + // After `MapView` panning `NavigationCamera` should be moved to idle state to prevent any further changes. + navigationMapView?.navigationCamera.stop() // Determine the screen distance to pan by based on the distance from the visual center to the closest side. let navigationMapView = carPlayMapViewController.navigationMapView @@ -717,7 +713,6 @@ internal class MapTemplateProvider: NSObject { return CPMapTemplate() } } - #else /** CarPlay support requires iOS 12.0 or above and the CarPlay framework. diff --git a/Sources/MapboxNavigation/CarPlayMapViewController.swift b/Sources/MapboxNavigation/CarPlayMapViewController.swift index b4371cfb72d..36767a6e272 100644 --- a/Sources/MapboxNavigation/CarPlayMapViewController.swift +++ b/Sources/MapboxNavigation/CarPlayMapViewController.swift @@ -1,5 +1,6 @@ import Foundation import MapboxMaps +import MapboxCoreNavigation #if canImport(CarPlay) import CarPlay @@ -9,7 +10,6 @@ import CarPlay */ @available(iOS 12.0, *) public class CarPlayMapViewController: UIViewController { - static let defaultAltitude: CLLocationDistance = 850 var styleManager: StyleManager? @@ -31,13 +31,6 @@ public class CarPlayMapViewController: UIViewController { return coarseLocationManager }() - var isOverviewingRoutes: Bool = false { - didSet { - // Fix content insets in overview mode. - automaticallyAdjustsScrollViewInsets = !isOverviewingRoutes - } - } - var navigationMapView: NavigationMapView { get { return self.view as! NavigationMapView @@ -49,15 +42,7 @@ public class CarPlayMapViewController: UIViewController { */ public lazy var recenterButton: CPMapButton = { let recenter = CPMapButton { [weak self] button in - // Since tracking mode is no longer part of `MapView` functionality camera is used directly to - // zoom-in to most recent location. - guard let self = self else { return } - let latestLocation = self.navigationMapView.mapView.locationManager.latestLocation - self.navigationMapView.mapView.cameraManager.setCamera(centerCoordinate: latestLocation?.coordinate, - zoom: 12.0, - bearing: latestLocation?.course, - pitch: 0, - animated: true) + self?.navigationMapView.navigationCamera.follow() button.isHidden = true } @@ -72,11 +57,13 @@ public class CarPlayMapViewController: UIViewController { */ public lazy var zoomInButton: CPMapButton = { let zoomInButton = CPMapButton { [weak self] (button) in - guard let self = self else { return } + guard let self = self, let mapView = self.navigationMapView.mapView else { return } + + self.navigationMapView.navigationCamera.stop() - let cameraOptions = self.navigationMapView.mapView.camera - cameraOptions.zoom = self.navigationMapView.mapView.zoom + 1.0 - self.navigationMapView.mapView.cameraManager.setCamera(to: cameraOptions, completion: nil) + let cameraOptions = mapView.camera + cameraOptions.zoom = mapView.zoom + 1.0 + mapView.cameraManager.setCamera(to: cameraOptions) } let bundle = Bundle.mapboxNavigation @@ -90,11 +77,13 @@ public class CarPlayMapViewController: UIViewController { */ public lazy var zoomOutButton: CPMapButton = { let zoomOutButton = CPMapButton { [weak self] button in - guard let self = self else { return } - - let cameraOptions = self.navigationMapView.mapView.camera - cameraOptions.zoom = self.navigationMapView.mapView.zoom - 1.0 - self.navigationMapView.mapView.cameraManager.setCamera(to: cameraOptions, completion: nil) + guard let self = self, let mapView = self.navigationMapView.mapView else { return } + + self.navigationMapView.navigationCamera.stop() + + let cameraOptions = mapView.camera + cameraOptions.zoom = mapView.zoom - 1.0 + mapView.cameraManager.setCamera(to: cameraOptions) } let bundle = Bundle.mapboxNavigation @@ -140,19 +129,24 @@ public class CarPlayMapViewController: UIViewController { } override public func loadView() { - let navigationMapView = NavigationMapView(frame: UIScreen.main.bounds) + let navigationMapView = NavigationMapView(frame: UIScreen.main.bounds, navigationCameraType: .carPlay) navigationMapView.mapView.on(.styleLoaded) { _ in navigationMapView.localizeLabels() } self.view = navigationMapView + + setupPassiveLocationManager() } override public func viewDidLoad() { super.viewDidLoad() setupStyleManager() - resetCamera(animated: false, altitude: CarPlayMapViewController.defaultAltitude) + navigationMapView.navigationCamera.follow() + navigationMapView.mapView.update { + $0.location.puckType = .puck2D() + } } func setupStyleManager() { @@ -161,6 +155,12 @@ public class CarPlayMapViewController: UIViewController { styleManager?.styles = styles } + func setupPassiveLocationManager() { + let passiveLocationDataSource = PassiveLocationDataSource() + let passiveLocationManager = PassiveLocationManager(dataSource: passiveLocationDataSource) + navigationMapView.mapView.locationManager.overrideLocationProvider(with: passiveLocationManager) + } + /** Creates a new pan map button for the CarPlay map view controller. @@ -198,40 +198,20 @@ public class CarPlayMapViewController: UIViewController { return closeButton } - func resetCamera(animated: Bool = false, altitude: CLLocationDistance? = nil) { - let camera = navigationMapView.mapView.camera - let pitch: CGFloat = 60 - if let altitude = altitude, - let latitude = navigationMapView.mapView.locationManager.latestLocation?.internalLocation.coordinate.latitude { - camera.zoom = CGFloat(ZoomLevelForAltitude(altitude, pitch, latitude, navigationMapView.mapView.bounds.size)) - } - - camera.pitch = pitch - - navigationMapView.mapView.cameraManager.setCamera(to: camera, animated: animated, completion: nil) - } - override public func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() - // Since tracking mode is no longer part of `MapView` functionality camera is used directly to - // zoom-in to most recent location. + guard let activeRoute = navigationMapView.routes?.first else { - let latestLocation = navigationMapView.mapView.locationManager.latestLocation - navigationMapView.mapView.cameraManager.setCamera(centerCoordinate: latestLocation?.coordinate, - zoom: 12.0, - bearing: latestLocation?.course, - pitch: 0, - animated: true) - + navigationMapView.navigationCamera.follow() return } - if isOverviewingRoutes { - // FIXME: Unable to tilt map during route selection -- https://github.com/mapbox/mapbox-gl-native/issues/2259 - let topDownCamera = navigationMapView.mapView.camera - topDownCamera.pitch = 0 - navigationMapView.mapView.cameraManager.setCamera(to: topDownCamera, completion: nil) - navigationMapView.fit(to: activeRoute, animated: false) + if navigationMapView.navigationCamera.state == .idle { + let cameraOptions = navigationMapView.mapView.camera + cameraOptions.pitch = 0 + navigationMapView.mapView.cameraManager.setCamera(to: cameraOptions) + + navigationMapView.fitCamera(to: activeRoute) } } } @@ -239,7 +219,7 @@ public class CarPlayMapViewController: UIViewController { @available(iOS 12.0, *) extension CarPlayMapViewController: StyleManagerDelegate { public func location(for styleManager: StyleManager) -> CLLocation? { - return navigationMapView.userLocationForCourseTracking ?? navigationMapView.mapView.locationManager.latestLocation?.internalLocation ?? coarseLocationManager.location + return navigationMapView.mostRecentUserCourseViewLocation ?? navigationMapView.mapView.locationManager.latestLocation?.internalLocation ?? coarseLocationManager.location } public func styleManager(_ styleManager: StyleManager, didApply style: Style) { diff --git a/Sources/MapboxNavigation/CarPlayNavigationViewController.swift b/Sources/MapboxNavigation/CarPlayNavigationViewController.swift index e239abc1fad..264c44852d2 100644 --- a/Sources/MapboxNavigation/CarPlayNavigationViewController.swift +++ b/Sources/MapboxNavigation/CarPlayNavigationViewController.swift @@ -133,29 +133,33 @@ public class CarPlayNavigationViewController: UIViewController { // MARK: - Setting-up methods func setupNavigationMapView() { - let navigationMapView = NavigationMapView(frame: view.bounds) + let navigationMapView = NavigationMapView(frame: view.bounds, navigationCameraType: .carPlay) + navigationMapView.navigationCamera.viewportDataSource = NavigationViewportDataSource(navigationMapView.mapView, + viewportDataSourceType: .active) navigationMapView.translatesAutoresizingMaskIntoConstraints = false - navigationMapView.defaultAltitude = 500 - navigationMapView.zoomedOutMotorwayAltitude = 1000 - navigationMapView.longManeuverDistance = 500 - navigationMapView.recenterMap() navigationMapView.mapView.on(.styleLoaded) { [weak self] _ in self?.navigationMapView?.localizeLabels() self?.updateRouteOnMap() - self?.navigationMapView?.recenterMap() self?.navigationMapView?.mapView.showsTraffic = false } navigationMapView.mapView.update { $0.ornaments.compassVisiblity = .hidden - $0.location.puckType = .puck2D() + $0.location.puckType = .none } + navigationMapView.userCourseView.isHidden = false + navigationMapView.navigationCamera.follow() + view.addSubview(navigationMapView) navigationMapView.pinInSuperview() self.navigationMapView = navigationMapView + + if let coordinate = navigationService.routeProgress.route.shape?.coordinates.first { + navigationMapView.setInitialCamera(coordinate) + } } func setupOrnaments() { @@ -197,43 +201,6 @@ public class CarPlayNavigationViewController: UIViewController { NotificationCenter.default.removeObserver(self, name: .routeControllerDidPassVisualInstructionPoint, object: nil) } - // MARK: - Overridden methods - - public override func viewSafeAreaInsetsDidChange() { - super.viewSafeAreaInsetsDidChange() - - navigationMapView?.enableFrameByFrameCourseViewTracking(for: 1) - } - - public override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - if isOverviewingRoutes { return } // Don't move content when overlays change. - - navigationMapView?.mapView.padding = contentInset(forOverviewing: false) - } - - func contentInset(forOverviewing overviewing: Bool) -> UIEdgeInsets { - guard let navigationMapView = navigationMapView else { return .zero } - var insets = navigationMapView.safeArea - if !overviewing { - // Puck position calculation - position it just above the bottom of the content area. - var contentFrame = navigationMapView.bounds.inset(by: insets) - - // Avoid letting the puck go partially off-screen, and add a comfortable padding beyond that. - let courseViewBounds = navigationMapView.userCourseView.bounds - // If it is not possible to position it right above the content area, center it at the remaining space. - contentFrame = contentFrame.insetBy(dx: min(NavigationMapView.courseViewMinimumInsets.left + courseViewBounds.width / 2.0, contentFrame.width / 2.0), - dy: min(NavigationMapView.courseViewMinimumInsets.top + courseViewBounds.height / 2.0, contentFrame.height / 2.0)) - assert(!contentFrame.isInfinite) - - let y = contentFrame.maxY - let height = navigationMapView.bounds.height - insets.top = height - insets.bottom - 2 * (height - insets.bottom - y) - } - - return insets - } - /** Begins a navigation session along the given trip. @@ -262,51 +229,6 @@ public class CarPlayNavigationViewController: UIViewController { carInterfaceController.pushTemplate(self.carFeedbackTemplate, animated: true) } - /** - A Boolean value indicating whether the map should follow the user’s location and rotate when the course changes. - - When this property is true, the map follows the user’s location and rotates when their course changes. Otherwise, the map shows an overview of the route. - */ - @objc public dynamic var tracksUserCourse: Bool { - get { - return navigationMapView?.tracksUserCourse ?? false - } - set { - let progress = navigationService.routeProgress - if !tracksUserCourse && newValue { - isOverviewingRoutes = false - navigationMapView?.recenterMap() - navigationMapView?.addArrow(route: progress.route, - legIndex: progress.legIndex, - stepIndex: progress.currentLegProgress.stepIndex + 1) - navigationMapView?.mapView.padding = contentInset(forOverviewing: false) - } else if tracksUserCourse && !newValue { - isOverviewingRoutes = !isPanningAway - guard let userLocation = self.navigationService.router.location, - let shape = navigationService.route.shape else { - return - } - navigationMapView?.enableFrameByFrameCourseViewTracking(for: 1) - navigationMapView?.mapView.padding = contentInset(forOverviewing: isOverviewingRoutes) - if (isOverviewingRoutes) { - navigationMapView?.setOverheadCameraView(from: userLocation, along: shape, for: contentInset(forOverviewing: true)) - } - } - } - } - - // Tracks if tracksUserCourse was set to false from overview button or panned away. - var isPanningAway = false - var isOverviewingRoutes = false - - public func beginPanGesture() { - isPanningAway = true - tracksUserCourse = false - navigationMapView?.tracksUserCourse = false - navigationMapView?.enableFrameByFrameCourseViewTracking(for: 1) - isPanningAway = false - } - @objc func visualInstructionDidChange(_ notification: NSNotification) { let routeProgress = notification.userInfo![RouteController.NotificationUserInfoKey.routeProgressKey] as! RouteProgress updateManeuvers(routeProgress) @@ -320,17 +242,7 @@ public class CarPlayNavigationViewController: UIViewController { // Update the user puck navigationMapView?.updatePreferredFrameRate(for: routeProgress) - let pitch: CGFloat = 60 - let zoom = CGFloat(ZoomLevelForAltitude(120, - pitch, - location.coordinate.latitude, - navigationMapView?.mapView.bounds.size ?? .zero)) - - let camera = CameraOptions(center: location.coordinate, - zoom: zoom, - bearing: location.course, - pitch: pitch) - navigationMapView?.updateCourseTracking(location: location, camera: camera, animated: true) + navigationMapView?.updateUserCourseView(location, animated: true) let congestionLevel = routeProgress.averageCongestionLevelRemainingOnLeg ?? .unknown guard let maneuver = carSession.upcomingManeuvers.first else { return } @@ -377,7 +289,6 @@ public class CarPlayNavigationViewController: UIViewController { @objc func rerouted(_ notification: NSNotification) { updateRouteOnMap() - self.navigationMapView?.recenterMap() } func updateRouteOnMap() { @@ -528,7 +439,7 @@ public class CarPlayNavigationViewController: UIViewController { let exitTitle = NSLocalizedString("CARPLAY_EXIT_NAVIGATION", bundle: .mapboxNavigation, value: "Exit navigation", comment: "Title on the exit button in the arrival form") let exitAction = CPAlertAction(title: exitTitle, style: .cancel) { (action) in self.exitNavigation() - self.dismiss(animated: true, completion: nil) + self.dismiss(animated: true) } let rateTitle = NSLocalizedString("CARPLAY_RATE_TRIP", bundle: .mapboxNavigation, value: "Rate your trip", comment: "Title on rate button in CarPlay") let rateAction = CPAlertAction(title: rateTitle, style: .default) { (action) in diff --git a/Sources/MapboxNavigation/Collection.swift b/Sources/MapboxNavigation/Collection.swift new file mode 100644 index 00000000000..a341225e083 --- /dev/null +++ b/Sources/MapboxNavigation/Collection.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Collection { + + /** + Returns the element at the specified index if it is within bounds, otherwise nil. + */ + subscript(safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Sources/MapboxNavigation/MapView.swift b/Sources/MapboxNavigation/MapView.swift index cdc6be24015..7ed6e18170a 100644 --- a/Sources/MapboxNavigation/MapView.swift +++ b/Sources/MapboxNavigation/MapView.swift @@ -104,12 +104,6 @@ extension MapView { } } - // TODO: Consider replacing after Maps SDK provides the ability to convert zoom to altitude and vice versa. - var altitude: CLLocationDistance? { - guard let latitude = locationManager.latestLocation?.coordinate.latitude else { return nil } - return AltitudeForZoomLevel(Double(zoom), pitch, latitude, bounds.size) - } - /** Returns a list of style source datasets (e.g. `mapbox.mapbox-streets-v8`), based on provided selected style source types. diff --git a/Sources/MapboxNavigation/NavigationCamera.swift b/Sources/MapboxNavigation/NavigationCamera.swift new file mode 100644 index 00000000000..96384222d28 --- /dev/null +++ b/Sources/MapboxNavigation/NavigationCamera.swift @@ -0,0 +1,216 @@ +import Foundation +import MapboxMaps + +/** + `NavigationCamera` class provides functionality, which allows to manage camera related states + and transitions in a typical navigation scenarios. It's fed with `CameraOptions` via the `ViewportDataSource` + protocol and executes transitions using `CameraStateTransition` protocol. + */ +public class NavigationCamera: NSObject, ViewportDataSourceDelegate { + + /** + Current state of `NavigationCamera`. Defaults to `NavigationCameraState.idle`. + */ + public private(set) var state: NavigationCameraState = .idle { + didSet { + NotificationCenter.default.post(name: .navigationCameraStateDidChange, + object: self, + userInfo: [ + NavigationCamera.NotificationUserInfoKey.state: state + ]) + } + } + + /** + Protocol, which is used to provide location related data to continuously perform camera related updates. + By default `NavigationMapView` uses `NavigationViewportDataSource`. + */ + public var viewportDataSource: ViewportDataSource { + didSet { + viewportDataSource.delegate = self + + debugView?.navigationViewportDataSource = viewportDataSource as? NavigationViewportDataSource + } + } + + /** + Protocol, which is used to execute camera transitions. By default `NavigationMapView` uses + `NavigationCameraStateTransition`. + */ + public var cameraStateTransition: CameraStateTransition + + /** + `MapView` instance, which will be used for performing camera related transitions. + */ + weak var mapView: MapView? + + /** + Type of `NavigationCamera`. Used to decide on which platform (iOS or CarPlay) transitions and updates should be executed. + */ + var type: NavigationCameraType = .mobile + + /** + Instance of `NavigationCameraDebugView`, which is drawn on `MapView` surface for debugging purposes. + */ + var debugView: NavigationCameraDebugView? = nil + + /** + Initializer of `NavigationCamera` object. + + - parameter mapView: Instance of `MapView`, on which camera related transitions will be executed. + - parameter navigationCameraType: Type of camera, which is used to perform camera transition (either iOS or CarPlay). + */ + public required init(_ mapView: MapView, navigationCameraType: NavigationCameraType = .mobile) { + self.mapView = mapView + self.viewportDataSource = NavigationViewportDataSource(mapView) + self.cameraStateTransition = NavigationCameraStateTransition(mapView) + self.type = navigationCameraType + + super.init() + + self.viewportDataSource.delegate = self + + setupGestureRegonizers() + + // Uncomment to be able to see `NavigationCameraDebugView`. + // setupDebugView(mapView, + // navigationCameraType: navigationCameraType, + // navigationViewportDataSource: self.viewportDataSource as? NavigationViewportDataSource) + } + + // MARK: - Setting-up methods + + func setupGestureRegonizers() { + makeGestureRecognizersDisableCameraFollowing() + } + + // MARK: - ViewportDataSourceDelegate methods + + public func viewportDataSource(_ dataSource: ViewportDataSource, didUpdate cameraOptions: [String: CameraOptions]) { + switch state { + case .following: + switch type { + case .carPlay: + if let followingCarPlayCamera = cameraOptions[CameraOptions.followingCarPlayCamera] { + cameraStateTransition.updateForFollowing(followingCarPlayCamera) + } + case .mobile: + if let followingMobileCamera = cameraOptions[CameraOptions.followingMobileCamera] { + cameraStateTransition.updateForFollowing(followingMobileCamera) + } + } + break + + case .overview: + switch type { + case .carPlay: + if let overviewCarPlayCamera = cameraOptions[CameraOptions.overviewCarPlayCamera] { + cameraStateTransition.updateForOverview(overviewCarPlayCamera) + } + case .mobile: + if let overviewMobileCamera = cameraOptions[CameraOptions.overviewMobileCamera] { + cameraStateTransition.updateForOverview(overviewMobileCamera) + } + } + break + + case .idle, .transitionToFollowing, .transitionToOverview: + break + } + } + + // MARK: - NavigationCamera state related methods + + /** + Call to this method executes a transition to `NavigationCameraState.following` state. + When started, state will first change to `NavigationCameraState.transitionToFollowing` and then + to the final `NavigationCameraState.following` when ended. + */ + public func follow() { + switch state { + case .transitionToFollowing, .following: + return + + case .idle, .transitionToOverview, .overview: + state = .transitionToFollowing + + var cameraOptions: CameraOptions + switch type { + case .mobile: + cameraOptions = viewportDataSource.followingMobileCamera + case .carPlay: + cameraOptions = viewportDataSource.followingCarPlayCamera + } + + cameraStateTransition.transitionToFollowing(cameraOptions) { + self.state = .following + } + + break + } + } + + /** + Call to this method executes a transition to `NavigationCameraState.overview` state. + When started, state will first change to `NavigationCameraState.transitionToOverview` and then + to the final `NavigationCameraState.overview` when ended. + */ + public func moveToOverview() { + switch state { + case .transitionToOverview, .overview: + return + + case .idle, .transitionToFollowing, .following: + state = .transitionToOverview + + var cameraOptions: CameraOptions + switch type { + case .mobile: + cameraOptions = viewportDataSource.overviewMobileCamera + case .carPlay: + cameraOptions = viewportDataSource.overviewCarPlayCamera + } + + cameraStateTransition.transitionToOverview(cameraOptions) { + self.state = .overview + } + + break + } + } + + /** + Call to this method immediately moves `NavigationCamera` to `NavigationCameraState.idle` state + and stops all pending transitions. + */ + @objc public func stop() { + if state == .idle { return } + + cameraStateTransition.cancelPendingTransition() + + state = .idle + } + + /** + Modifies `MapView` gesture recognizers to disable follow mode and move `NavigationCamera` to + `NavigationCameraState.idle` state. + */ + func makeGestureRecognizersDisableCameraFollowing() { + for gestureRecognizer in mapView?.gestureRecognizers ?? [] + where gestureRecognizer is UIPanGestureRecognizer + || gestureRecognizer is UIRotationGestureRecognizer + || gestureRecognizer is UIPinchGestureRecognizer { + gestureRecognizer.addTarget(self, action: #selector(stop)) + } + } + + func setupDebugView(_ mapView: MapView, + navigationCameraType: NavigationCameraType, + navigationViewportDataSource: NavigationViewportDataSource?) { + debugView = NavigationCameraDebugView(mapView, + frame: mapView.frame, + navigationCameraType: navigationCameraType, + navigationViewportDataSource: navigationViewportDataSource) + mapView.addSubview(debugView!) + } +} diff --git a/Sources/MapboxNavigation/NavigationCameraConstants.swift b/Sources/MapboxNavigation/NavigationCameraConstants.swift new file mode 100644 index 00000000000..6d6e76e86ac --- /dev/null +++ b/Sources/MapboxNavigation/NavigationCameraConstants.swift @@ -0,0 +1,70 @@ +import Foundation +import MapboxMaps + +extension CameraOptions { + + /** + Key, which is used to access `CameraOptions` provided via `ViewportDataSourceDelegate` + so that it can be consumed by `NavigationCamera` in `.transitionToFollowing` or `.following` states on iOS. + */ + public static let followingMobileCamera = "FollowingMobileCamera" + + /** + Key, which is used to access `CameraOptions` provided via `ViewportDataSourceDelegate` + so that it can be consumed by `NavigationCamera` in `.transitionToOverview` or `.overview` states on iOS. + */ + public static let overviewMobileCamera = "OverviewMobileCamera" + + /** + Key, which is used to access `CameraOptions` provided via `ViewportDataSourceDelegate` + so that it can be consumed by `NavigationCamera` in `.transitionToFollowing` or `.following` states on CarPlay. + */ + public static let followingCarPlayCamera = "FollowingCarPlayCamera" + + /** + Key, which is used to access `CameraOptions` provided via `ViewportDataSourceDelegate` + so that it can be consumed by `NavigationCamera` in `.transitionToOverview` or `.overview` states on CarPlay. + */ + public static let overviewCarPlayCamera = "OverviewCarPlayCamera" +} + +extension Notification.Name { + + /** + Posted when value of `NavigationCamera.state` property changes. + + The user info dictionary contains `NavigationCamera.NotificationUserInfoKey.state` key. + */ + public static let navigationCameraStateDidChange: Notification.Name = .init(rawValue: "NavigationCameraStateDidChange") + + /** + Posted when `NavigationViewportDataSource` changes underlying `CameraOptions`, which will be used by + `NavigationCameraStateTransition` when running camera related transitions on `iOS` and `CarPlay`. + + The user info dictionary contains `NavigationCamera.NotificationUserInfoKey.cameraOptions` key. + */ + public static let navigationCameraViewportDidChange: Notification.Name = .init(rawValue: "NavigationViewportDidChange") +} + +extension NavigationCamera { + + public struct NotificationUserInfoKey: Hashable, Equatable, RawRepresentable { + public typealias RawValue = String + + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + /** + A key in the user info dictionary of a `Notification.Name.navigationCameraStateDidChange` notification. The corresponding value is a `NavigationCameraState` object. + */ + public static let state: NotificationUserInfoKey = .init(rawValue: "state") + + /** + A key in the user info dictionary of a `Notification.Name.navigationCameraViewportDidChange` notification. The corresponding value is a `[String: CameraOptions]` dictionary object. + */ + public static let cameraOptions: NotificationUserInfoKey = .init(rawValue: "cameraOptions") + } +} diff --git a/Sources/MapboxNavigation/NavigationCameraDebugView.swift b/Sources/MapboxNavigation/NavigationCameraDebugView.swift new file mode 100644 index 00000000000..b59cd5f17ef --- /dev/null +++ b/Sources/MapboxNavigation/NavigationCameraDebugView.swift @@ -0,0 +1,195 @@ +import UIKit +import MapboxMaps + +/** + `UIView`, which is drawn on top of `MapView` and shows `CameraOptions` when `NavigationCamera` is in `.following` mode. + Such `UIView` is useful for debugging purposes (especially when debugging camera behavior on CarPlay). + */ +class NavigationCameraDebugView: UIView { + + weak var mapView: MapView? + + let navigationCameraType: NavigationCameraType + + weak var navigationViewportDataSource: NavigationViewportDataSource? { + didSet { + subscribeForNotifications(navigationViewportDataSource) + } + } + + var viewportLayer = CALayer() + var viewportTextLayer = CATextLayer() + var anchorLayer = CALayer() + var anchorTextLayer = CATextLayer() + var centerLayer = CALayer() + var centerTextLayer = CATextLayer() + var pitchTextLayer = CATextLayer() + var zoomTextLayer = CATextLayer() + var bearingTextLayer = CATextLayer() + var centerCoordinateTextLayer = CATextLayer() + + required init(_ mapView: MapView, + frame: CGRect, + navigationCameraType: NavigationCameraType, + navigationViewportDataSource: NavigationViewportDataSource?) { + self.mapView = mapView + self.navigationCameraType = navigationCameraType + self.navigationViewportDataSource = navigationViewportDataSource + + super.init(frame: frame) + + isUserInteractionEnabled = false + backgroundColor = .clear + subscribeForNotifications(navigationViewportDataSource) + + viewportLayer.borderWidth = 3.0 + viewportLayer.borderColor = UIColor.green.cgColor + layer.addSublayer(viewportLayer) + + anchorLayer.backgroundColor = UIColor.red.cgColor + anchorLayer.frame = .init(x: 0.0, y: 0.0, width: 6.0, height: 6.0) + anchorLayer.cornerRadius = 3.0 + layer.addSublayer(anchorLayer) + + anchorTextLayer = CATextLayer() + anchorTextLayer.string = "Anchor" + anchorTextLayer.fontSize = UIFont.systemFontSize + anchorTextLayer.backgroundColor = UIColor.clear.cgColor + anchorTextLayer.foregroundColor = UIColor.red.cgColor + anchorTextLayer.frame = .zero + layer.addSublayer(anchorTextLayer) + + centerLayer.backgroundColor = UIColor.blue.cgColor + centerLayer.frame = .init(x: 0.0, y: 0.0, width: 6.0, height: 6.0) + centerLayer.cornerRadius = 3.0 + layer.addSublayer(centerLayer) + + centerTextLayer = CATextLayer() + centerTextLayer.string = "Center" + centerTextLayer.fontSize = UIFont.systemFontSize + centerTextLayer.backgroundColor = UIColor.clear.cgColor + centerTextLayer.foregroundColor = UIColor.blue.cgColor + centerTextLayer.frame = .zero + layer.addSublayer(centerTextLayer) + + pitchTextLayer = createDefaultTextLayer() + layer.addSublayer(pitchTextLayer) + + zoomTextLayer = createDefaultTextLayer() + layer.addSublayer(zoomTextLayer) + + bearingTextLayer = createDefaultTextLayer() + layer.addSublayer(bearingTextLayer) + + viewportTextLayer = createDefaultTextLayer() + layer.addSublayer(viewportTextLayer) + + centerCoordinateTextLayer = createDefaultTextLayer() + layer.addSublayer(centerCoordinateTextLayer) + } + + func createDefaultTextLayer() -> CATextLayer { + let textLayer = CATextLayer() + textLayer.string = "" + textLayer.fontSize = UIFont.systemFontSize + textLayer.backgroundColor = UIColor.clear.cgColor + textLayer.foregroundColor = UIColor.black.cgColor + textLayer.frame = .zero + + return textLayer + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + unsubscribeFromNotifications(navigationViewportDataSource) + } + + func subscribeForNotifications(_ object: Any?) { + NotificationCenter.default.addObserver(self, + selector: #selector(navigationCameraViewportDidChange(_:)), + name: .navigationCameraViewportDidChange, + object: object) + } + + func unsubscribeFromNotifications(_ object: Any?) { + NotificationCenter.default.removeObserver(self, + name: .navigationCameraViewportDidChange, + object: object) + } + + @objc func navigationCameraViewportDidChange(_ notification: NSNotification) { + guard let mapView = mapView, + let cameraOptions = notification.userInfo?[NavigationCamera.NotificationUserInfoKey.cameraOptions] as? Dictionary else { return } + + var camera: CameraOptions? = nil + + switch navigationCameraType { + case .carPlay: + camera = cameraOptions[CameraOptions.followingCarPlayCamera] + case .mobile: + camera = cameraOptions[CameraOptions.followingMobileCamera] + } + + if let anchorPosition = camera?.anchor { + anchorLayer.position = anchorPosition + anchorTextLayer.frame = .init(x: anchorLayer.frame.origin.x + 5.0, + y: anchorLayer.frame.origin.y + 5.0, + width: 80.0, + height: 20.0) + } + + if let pitch = camera?.pitch { + pitchTextLayer.frame = .init(x: viewportLayer.frame.origin.x + 5.0, + y: viewportLayer.frame.origin.y + 5.0, + width: viewportLayer.frame.size.width - 10.0, + height: 20.0) + pitchTextLayer.string = "Pitch: \(pitch)º" + } + + if let zoom = camera?.zoom { + zoomTextLayer.frame = .init(x: viewportLayer.frame.origin.x + 5.0, + y: viewportLayer.frame.origin.y + 30.0, + width: viewportLayer.frame.size.width - 10.0, + height: 20.0) + zoomTextLayer.string = "Zoom: \(zoom)" + } + + if let bearing = camera?.bearing { + bearingTextLayer.frame = .init(x: viewportLayer.frame.origin.x + 5.0, + y: viewportLayer.frame.origin.y + 55.0, + width: viewportLayer.frame.size.width - 10.0, + height: 20.0) + bearingTextLayer.string = "Bearing: \(bearing)º" + } + + if let edgeInsets = camera?.padding { + viewportLayer.frame = CGRect(x: edgeInsets.left, + y: edgeInsets.top, + width: mapView.frame.width - edgeInsets.left - edgeInsets.right, + height: mapView.frame.height - edgeInsets.top - edgeInsets.bottom) + + viewportTextLayer.frame = .init(x: viewportLayer.frame.origin.x + 5.0, + y: viewportLayer.frame.origin.y + 80.0, + width: viewportLayer.frame.size.width - 10.0, + height: 20.0) + viewportTextLayer.string = "Padding: (top: \(edgeInsets.top), left: \(edgeInsets.left), bottom: \(edgeInsets.bottom), right: \(edgeInsets.right))" + } + + if let centerCoordinate = camera?.center { + centerLayer.position = mapView.point(for: centerCoordinate) + centerTextLayer.frame = .init(x: centerLayer.frame.origin.x + 5.0, + y: centerLayer.frame.origin.y + 5.0, + width: 80.0, + height: 20.0) + + centerCoordinateTextLayer.frame = .init(x: viewportLayer.frame.origin.x + 5.0, + y: viewportLayer.frame.origin.y + 105.0, + width: viewportLayer.frame.size.width - 10.0, + height: 20.0) + centerCoordinateTextLayer.string = "Center coordinate: (lat: \(centerCoordinate.latitude), lng: \(centerCoordinate.longitude))" + } + } +} diff --git a/Sources/MapboxNavigation/NavigationCameraState.swift b/Sources/MapboxNavigation/NavigationCameraState.swift new file mode 100644 index 00000000000..743f7c6b140 --- /dev/null +++ b/Sources/MapboxNavigation/NavigationCameraState.swift @@ -0,0 +1,36 @@ +/** + Possible states which `NavigationCamera` can have. + */ +public enum NavigationCameraState { + + /** + State when `NavigationCamera` does not execute any transitions. + + Such state is set after invoking `NavigationCamera.stop()`. + */ + case idle + + /** + State when `NavigationCamera` transitions to the `NavigationCameraState.following` state. + + Such state is set after invoking `NavigationCamera.follow()`. + */ + case transitionToFollowing + + /** + State when `NavigationCamera` finished transition to the following state. + */ + case following + + /** + State when `NavigationCamera` is transitioning to the `NavigationCameraState.overview` state. + + Such state is set after invoking `NavigationCamera.moveToOverview()`. + */ + case transitionToOverview + + /** + State when `NavigationCamera` finished transition to the overview state. + */ + case overview +} diff --git a/Sources/MapboxNavigation/NavigationCameraStateTransition.swift b/Sources/MapboxNavigation/NavigationCameraStateTransition.swift new file mode 100644 index 00000000000..43962f80c3e --- /dev/null +++ b/Sources/MapboxNavigation/NavigationCameraStateTransition.swift @@ -0,0 +1,418 @@ +import MapboxMaps +import Turf + +/** + Class, which conforms to `CameraStateTransition` protocol and provides default implementation of + camera related transitions by using `CameraAnimator` functionality provided by Mapbox Maps SDK. + */ +public class NavigationCameraStateTransition: CameraStateTransition { + + public weak var mapView: MapView? + + let centerTimingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.0, y: 0.0), controlPoint2: CGPoint(x: 1.0, y: 1.0)) + let zoomTimingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.0, y: 0.0), controlPoint2: CGPoint(x: 1.0, y: 1.0)) + let bearingTimingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.0, y: 0.0), controlPoint2: CGPoint(x: 1.0, y: 1.0)) + let pitchTimingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.0, y: 0.0), controlPoint2: CGPoint(x: 1.0, y: 1.0)) + + var animatorCenter: CameraAnimator! + var animatorZoom: CameraAnimator! + var animatorBearing: CameraAnimator! + var animatorPitch: CameraAnimator! + + typealias TransitionParameters = ( + cameraOptions: CameraOptions, + centerAnimationDuration: TimeInterval, + centerAnimationDelay: TimeInterval, + zoomAnimationDuration: TimeInterval, + zoomAnimationDelay: TimeInterval, + bearingAnimationDuration: TimeInterval, + bearingAnimationDelay: TimeInterval, + pitchAndAnchorAnimationDuration: TimeInterval, + pitchAndAnchorAnimationDelay: TimeInterval + ) + + required public init(_ mapView: MapView) { + self.mapView = mapView + + resetAnimators() + } + + public func transitionToFollowing(_ cameraOptions: CameraOptions, completion: @escaping (() -> Void)) { + guard let mapView = mapView, + let zoom = cameraOptions.zoom, + let location = cameraOptions.center, + let bearing = cameraOptions.bearing, + let pitch = cameraOptions.pitch, + let padding = cameraOptions.padding, + let anchor = cameraOptions.anchor else { + completion() + return + } + + if transitionDestinationIsOffScreen(location, edgeInsets: padding) { + let screenCenterPoint = self.screenCenterPoint(0.0, bounds: mapView.bounds, edgeInsets: padding) + let lineString = LineString([mapView.centerCoordinate, location]) + let camera = mapView.cameraManager.camera(fitting: .lineString(lineString)) + camera.bearing = CLLocationDirection(mapView.bearing) + camera.pitch = 0.0 + if let midPointZoom = camera.zoom { + let cameraOptions = CameraOptions(center: location, + anchor: screenCenterPoint, + zoom: CGFloat(midPointZoom), + bearing: bearing, + pitch: 0.0) + + transitionFromHighZoomToMidpoint(cameraOptions) { + let cameraOptions = CameraOptions(center: location, + anchor: anchor, + zoom: CGFloat(zoom), + bearing: bearing, + pitch: CGFloat(pitch)) + + self.transitionFromLowZoomToHighZoom(cameraOptions) { + completion() + } + } + } + } else { + if mapView.zoom < zoom { + transitionFromLowZoomToHighZoom(cameraOptions) { + self.resetAnimators() + completion() + } + } else { + transitionFromHighZoomToLowZoom(cameraOptions) { + self.resetAnimators() + completion() + } + } + } + } + + public func transitionToOverview(_ cameraOptions: CameraOptions, completion: @escaping (() -> Void)) { + guard let mapView = mapView, + let zoom = cameraOptions.zoom else { + completion() + return + } + + cameraOptions.pitch = 0.0 + + if mapView.zoom < zoom { + transitionFromLowZoomToHighZoom(cameraOptions) { + completion() + } + } else { + transitionFromHighZoomToLowZoom(cameraOptions) { + completion() + } + } + } + + public func updateForFollowing(_ cameraOptions: CameraOptions) { + update(cameraOptions) + } + + public func updateForOverview(_ cameraOptions: CameraOptions) { + cameraOptions.bearing = 0.0 + update(cameraOptions) + } + + public func cancelPendingTransition() { + stopAnimators() + } + + func update(_ cameraOptions: CameraOptions) { + guard let mapView = mapView, + let zoom = cameraOptions.zoom, + let location = cameraOptions.center, + let bearing = cameraOptions.bearing, + let pitch = cameraOptions.pitch, + let anchor = cameraOptions.anchor, + let padding = cameraOptions.padding else { return } + + animatorCenter.stopAnimation() + animatorCenter.addAnimations { + mapView.centerCoordinate = location + } + + animatorZoom.stopAnimation() + animatorZoom.addAnimations { + mapView.zoom = zoom + } + + animatorBearing.stopAnimation() + animatorBearing.addAnimations { + mapView.bearing = bearing + } + + animatorPitch.stopAnimation() + animatorPitch.addAnimations { + mapView.pitch = pitch + mapView.anchor = anchor + } + + mapView.padding = padding + + animatorCenter.startAnimation() + animatorZoom.startAnimation() + animatorBearing.startAnimation() + animatorPitch.startAnimation() + } + + func resetAnimators() { + let duration = 1.0 + animatorCenter = mapView?.cameraManager.makeCameraAnimator(duration: duration, timingParameters: centerTimingParameters) + animatorZoom = mapView?.cameraManager.makeCameraAnimator(duration: duration, timingParameters: zoomTimingParameters) + animatorBearing = mapView?.cameraManager.makeCameraAnimator(duration: duration, timingParameters: bearingTimingParameters) + animatorPitch = mapView?.cameraManager.makeCameraAnimator(duration: duration, timingParameters: pitchTimingParameters) + } + + func stopAnimators() { + let animators = [ + animatorCenter, + animatorZoom, + animatorBearing, + animatorPitch + ] + + animators.forEach { + $0?.stopAnimation() + } + } + + func transitionFromLowZoomToHighZoom(_ cameraOptions: CameraOptions, completion: @escaping (() -> Void)) { + guard let mapView = mapView, + let zoom = cameraOptions.zoom, + let location = cameraOptions.center, + let bearing = cameraOptions.bearing else { + completion() + return + } + + let centerTranslationDistance = mapView.centerCoordinate.distance(to: location) + let metersPerSecondMaxCenterAnimation: Double = 1500.0 + let centerAnimationDuration: TimeInterval = max(min(centerTranslationDistance / metersPerSecondMaxCenterAnimation, 1.6), 0.6) + let centerAnimationDelay: TimeInterval = 0.0 + + let zoomLevelDistance = CLLocationDistance(abs(mapView.zoom - zoom)) + let levelsPerSecondMaxZoomAnimation: Double = 3.0 + let zoomAnimationDuration: TimeInterval = max(min(zoomLevelDistance / levelsPerSecondMaxZoomAnimation, 1.6), 0.6) + let zoomAnimationDelay: TimeInterval = centerAnimationDuration * 0.5 + let endZoomAnimation: TimeInterval = zoomAnimationDuration + zoomAnimationDelay + + let currentBearing = mapView.bearing + let newBearing: CLLocationDirection = Double(mapView.bearing) + bearing.shortestRotation(angle: CLLocationDirection(mapView.bearing)) + let bearingDegreesChange: CLLocationDirection = fabs(newBearing - Double(currentBearing)) + let degreesPerSecondMaxBearingAnimation: Double = 60.0 + let bearingAnimationDuration: TimeInterval = max(min(bearingDegreesChange / degreesPerSecondMaxBearingAnimation, 1.2), 0.6) + let bearingAnimationDelay: TimeInterval = max(endZoomAnimation - bearingAnimationDuration - 0.2, 0.0) + + let pitchAndAnchorAnimationDuration: TimeInterval = 0.8 + let pitchAndAnchorAnimationDelay: TimeInterval = max(endZoomAnimation - pitchAndAnchorAnimationDuration, 0.0) + + let transitionParameters = TransitionParameters( + cameraOptions, + centerAnimationDuration, + centerAnimationDelay, + zoomAnimationDuration, + zoomAnimationDelay, + bearingAnimationDuration, + bearingAnimationDelay, + pitchAndAnchorAnimationDuration, + pitchAndAnchorAnimationDelay + ) + + transition(transitionParameters) { + completion() + } + } + + func transitionFromHighZoomToLowZoom(_ cameraOptions: CameraOptions, completion: @escaping (() -> Void)) { + guard let mapView = mapView, + let zoom = cameraOptions.zoom, + let location = cameraOptions.center, + let bearing = cameraOptions.bearing else { + completion() + return + } + + let zoomLevelDistance = CLLocationDistance(abs(mapView.zoom - zoom)) + let levelsPerSecondMaxZoomAnimation: Double = 0.6 + let zoomAnimationDuration: TimeInterval = max(min(zoomLevelDistance / levelsPerSecondMaxZoomAnimation, 0.8), 0.2) + let zoomAnimationDelay: TimeInterval = 0.0 + let endZoomAnimation: TimeInterval = zoomAnimationDuration + zoomAnimationDelay + + let centerTranslationDistance = mapView.centerCoordinate.distance(to: location) + let metersPerSecondMaxCenterAnimation: Double = 1000.0 + let centerAnimationDuration: TimeInterval = max(min(centerTranslationDistance / metersPerSecondMaxCenterAnimation, 1.4), 0.6) + let centerAnimationDelay: TimeInterval = max(endZoomAnimation - centerAnimationDuration, 0.0) + + let bearingDegreesChange: CLLocationDirection = bearing.shortestRotation(angle: CLLocationDirection(mapView.bearing)) + let degreesPerSecondMaxBearingAnimation: Double = 60.0 + let bearingAnimationDuration: TimeInterval = max(min(bearingDegreesChange / degreesPerSecondMaxBearingAnimation, 1.2), 0.8) + let bearingAnimationDelay: TimeInterval = max(endZoomAnimation - bearingAnimationDuration - 0.4, 0.0) + + let pitchAndAnchorAnimationDuration: TimeInterval = 0.6 + let pitchAndAnchorAnimationDelay: TimeInterval = 0.0 + + let transitionParameters = TransitionParameters( + cameraOptions, + centerAnimationDuration, + centerAnimationDelay, + zoomAnimationDuration, + zoomAnimationDelay, + bearingAnimationDuration, + bearingAnimationDelay, + pitchAndAnchorAnimationDuration, + pitchAndAnchorAnimationDelay + ) + + transition(transitionParameters) { + completion() + } + } + + func transitionFromHighZoomToMidpoint(_ cameraOptions: CameraOptions, completion: @escaping (() -> Void)) { + guard let mapView = mapView, + let zoom = cameraOptions.zoom, + let location = cameraOptions.center, + let bearing = cameraOptions.bearing else { + completion() + return + } + + let zoomLevelDistance = CLLocationDistance(abs(mapView.zoom - zoom)) + let levelsPerSecondMaxZoomAnimation: Double = 0.6 + let zoomAnimationDuration: TimeInterval = max(min(zoomLevelDistance / levelsPerSecondMaxZoomAnimation, 0.8), 0.2) + let zoomAnimationDelay: TimeInterval = 0.0 + let endZoomAnimation: TimeInterval = zoomAnimationDuration + zoomAnimationDelay + + let centerTranslationDistance = mapView.centerCoordinate.distance(to: location) + let metersPerSecondMaxCenterAnimation: Double = 1000.0 + let centerAnimationDuration: TimeInterval = max(min(centerTranslationDistance / metersPerSecondMaxCenterAnimation, 1.4), 0.8) + let centerAnimationDelay: TimeInterval = max(endZoomAnimation - centerAnimationDuration, 0.0) + + let bearingDegreesChange: CLLocationDirection = bearing.shortestRotation(angle: CLLocationDirection(mapView.bearing)) + let degreesPerSecondMaxBearingAnimation: Double = 60.0 + let bearingAnimationDuration: TimeInterval = max(min(bearingDegreesChange / degreesPerSecondMaxBearingAnimation, 1.2), 0.8) + let bearingAnimationDelay: TimeInterval = max(endZoomAnimation - bearingAnimationDuration - 0.4, 0.0) + + let pitchAndAnchorAnimationDuration: TimeInterval = 0.6 + let pitchAndAnchorAnimationDelay: TimeInterval = 0.0 + + let transitionParameters = TransitionParameters( + cameraOptions, + centerAnimationDuration, + centerAnimationDelay, + zoomAnimationDuration, + zoomAnimationDelay, + bearingAnimationDuration, + bearingAnimationDelay, + pitchAndAnchorAnimationDuration, + pitchAndAnchorAnimationDelay + ) + + transition(transitionParameters) { + completion() + } + } + + func transition(_ transitionParameters: TransitionParameters, completion: @escaping (() -> Void)) { + guard let mapView = mapView, + let zoom = transitionParameters.cameraOptions.zoom, + let location = transitionParameters.cameraOptions.center, + let bearing = transitionParameters.cameraOptions.bearing, + let pitch = transitionParameters.cameraOptions.pitch, + let anchor = transitionParameters.cameraOptions.anchor, + let padding = transitionParameters.cameraOptions.padding else { + completion() + return + } + + stopAnimators() + + let animationsGroup = DispatchGroup() + + let centerTimingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.4, y: 0.0), controlPoint2: CGPoint(x: 0.6, y: 1.0)) + animatorCenter = mapView.cameraManager.makeCameraAnimator(duration: transitionParameters.centerAnimationDuration, timingParameters: centerTimingParameters) + animatorCenter.addAnimations { + mapView.centerCoordinate = location + } + animatorCenter.addCompletion { (animatingPosition) in + if animatingPosition == .end { + animationsGroup.leave() + } + } + + let zoomTimingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.2, y: 0.0), controlPoint2: CGPoint(x: 0.6, y: 1.0)) + animatorZoom = mapView.cameraManager.makeCameraAnimator(duration: transitionParameters.zoomAnimationDuration, timingParameters: zoomTimingParameters) + animatorZoom.addAnimations { + mapView.zoom = zoom + } + animatorZoom.addCompletion { (animatingPosition) in + if animatingPosition == .end { + animationsGroup.leave() + } + } + + let bearingTimingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.4, y: 0.0), controlPoint2: CGPoint(x: 0.6, y: 1.0)) + animatorBearing = mapView.cameraManager.makeCameraAnimator(duration: transitionParameters.bearingAnimationDuration, timingParameters: bearingTimingParameters) + animatorBearing.addAnimations { + mapView.bearing = bearing + } + animatorBearing.addCompletion { (animatingPosition) in + if animatingPosition == .end { + animationsGroup.leave() + } + } + + let pitchTimingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.6, y: 0.0), controlPoint2: CGPoint(x: 0.4, y: 1.0)) + animatorPitch = mapView.cameraManager.makeCameraAnimator(duration: transitionParameters.pitchAndAnchorAnimationDuration, timingParameters: pitchTimingParameters) + animatorPitch.addAnimations { + mapView.pitch = CGFloat(pitch) + mapView.anchor = anchor + } + animatorPitch.addCompletion { (animatingPosition) in + if animatingPosition == .end { + animationsGroup.leave() + } + } + + mapView.padding = padding + + let animations: [(CameraAnimator, TimeInterval)] = [ + (animatorCenter, transitionParameters.centerAnimationDelay), + (animatorZoom, transitionParameters.zoomAnimationDelay), + (animatorBearing, transitionParameters.bearingAnimationDelay), + (animatorPitch, transitionParameters.pitchAndAnchorAnimationDelay), + ] + + animations.forEach { (animator, delay) in + animationsGroup.enter() + animator.startAnimation(afterDelay: fmax(delay, 0.0)) + } + + animationsGroup.notify(queue: .main) { + self.resetAnimators() + completion() + } + } + + func transitionDestinationIsOffScreen(_ transitionDestination: CLLocationCoordinate2D, edgeInsets: UIEdgeInsets = .zero) -> Bool { + guard let mapView = mapView else { return false } + let inset: CGFloat = 40.0 + let halo = UIEdgeInsets(top: edgeInsets.top - inset, left: -inset, bottom: edgeInsets.bottom - inset, right: -inset) + + return !halo.rectValue(mapView.bounds).contains(mapView.point(for: transitionDestination)) + } + + func screenCenterPoint(_ pitchEffectCoefficient: Double, bounds: CGRect, edgeInsets: UIEdgeInsets) -> CGPoint { + let xCenter = max(((bounds.size.width - edgeInsets.left - edgeInsets.right) / 2.0) + edgeInsets.left, 0.0) + let height = (bounds.size.height - edgeInsets.top - edgeInsets.bottom) + let yCenter = max((height / 2.0) + edgeInsets.top, 0.0) + let yOffsetCenter = max((height / 2.0) - 7.0, 0.0) * CGFloat(pitchEffectCoefficient) + yCenter + + return CGPoint(x: xCenter, y: yOffsetCenter) + } +} diff --git a/Sources/MapboxNavigation/NavigationCameraType.swift b/Sources/MapboxNavigation/NavigationCameraType.swift new file mode 100644 index 00000000000..ec9ab0c1469 --- /dev/null +++ b/Sources/MapboxNavigation/NavigationCameraType.swift @@ -0,0 +1,16 @@ +/** + Possible types of `NavigationCamera`. + */ +public enum NavigationCameraType { + + /** + When such type is used `CameraOptions` will be optimized + specifically for CarPlay devices. + */ + case carPlay + + /** + Type, which is used for iPhone/iPad. + */ + case mobile +} diff --git a/Sources/MapboxNavigation/NavigationMapView.swift b/Sources/MapboxNavigation/NavigationMapView.swift index 54bfb714e25..43493884197 100755 --- a/Sources/MapboxNavigation/NavigationMapView.swift +++ b/Sources/MapboxNavigation/NavigationMapView.swift @@ -15,8 +15,8 @@ open class NavigationMapView: UIView { struct FrameIntervalOptions { static let durationUntilNextManeuver: TimeInterval = 7 static let durationSincePreviousManeuver: TimeInterval = 3 - static let defaultFramesPerSecond = PreferredFPS.maximum - static let pluggedInFramesPerSecond = PreferredFPS.lowPower + static let defaultFramesPerSecond = PreferredFPS.normal + static let pluggedInFramesPerSecond = PreferredFPS.maximum } /** @@ -26,21 +26,6 @@ open class NavigationMapView: UIView { */ public var minimumFramesPerSecond = PreferredFPS.normal - /** - Returns the altitude that the map camera initally defaults to. - */ - public var defaultAltitude: CLLocationDistance = 1000.0 - - /** - Returns the altitude the map conditionally zooms out to when user is on a motorway, and the maneuver length is sufficently long. - */ - public var zoomedOutMotorwayAltitude: CLLocationDistance = 2000.0 - - /** - Returns the threshold for what the map considers a "long-enough" maneuver distance to trigger a zoom-out when the user enters a motorway. - */ - public var longManeuverDistance: CLLocationDistance = 1000.0 - /** Maximum distance the user can tap for a selection to be valid when selecting an alternate route. */ @@ -54,7 +39,11 @@ open class NavigationMapView: UIView { */ public var roadClassesWithOverriddenCongestionLevels: Set? = nil - var cameraAnimator: CameraAnimator! + /** + `NavigationCamera`, which allows to control camera states. + */ + public private(set) var navigationCamera: NavigationCamera! + /** Controls whether to show congestion levels on alternative route lines. Defaults to `false`. @@ -111,9 +100,12 @@ open class NavigationMapView: UIView { @objc dynamic public var reducedAccuracyActivatedMode: Bool = false { didSet { let frame = CGRect(origin: .zero, size: 75.0) + let isHidden = userCourseView.isHidden userCourseView = reducedAccuracyActivatedMode ? UserHaloCourseView(frame: frame) : UserPuckCourseView(frame: frame) + + userCourseView.isHidden = isHidden } } @@ -128,14 +120,10 @@ open class NavigationMapView: UIView { public weak var delegate: NavigationMapViewDelegate? /** - The object that acts as the course tracking delegate of the map view. + Most recent user location, which is used to place `UserCourseView`. */ - public weak var courseTrackingDelegate: NavigationMapViewCourseTrackingDelegate? - - var userLocationForCourseTracking: CLLocation? - var altitude: CLLocationDistance + var mostRecentUserCourseViewLocation: CLLocation? var routes: [Route]? - var isAnimatingToOverheadMode = false var routePoints: RoutePoints? var routeLineGranularDistances: RouteLineGranularDistances? var routeRemainingDistancesIndex: Int? @@ -143,16 +131,6 @@ open class NavigationMapView: UIView { var fractionTraveled: Double = 0.0 var preFractionTraveled: Double = 0.0 var vanishingRouteLineUpdateTimer: Timer? = nil - - var shouldPositionCourseViewFrameByFrame = false { - didSet { - if shouldPositionCourseViewFrameByFrame { - mapView.update { - $0.render.preferredFramesPerSecond = .maximum - } - } - } - } var showsRoute: Bool { get { @@ -165,74 +143,6 @@ open class NavigationMapView: UIView { return true } } - - // TODO: When using previous version of Maps SDK `showsUserLocation` was overridden property of `MGLMapView`. Clarify whether it needs to be exposed. - open var showsUserLocation: Bool { - get { - if tracksUserCourse || userLocationForCourseTracking != nil { - return !userCourseView.isHidden - } - - return mapView.locationManager.locationOptions.puckType != .none - } - set { - if tracksUserCourse || userLocationForCourseTracking != nil { - mapView.update { - $0.location.puckType = .none - } - - userCourseView.isHidden = !newValue - } else { - userCourseView.isHidden = true - - mapView.update { - if newValue { - $0.location.puckType = .puck2D() - } else { - $0.location.puckType = .none - } - } - } - } - } - - /** - The minimum default insets from the content frame to the edges of the user course view. - */ - static let courseViewMinimumInsets = UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50) - - /** - Center point of the user course view in screen coordinates relative to the map view. - - seealso: NavigationMapViewDelegate.navigationMapViewUserAnchorPoint(_:) - */ - var userAnchorPoint: CGPoint { - if let anchorPoint = delegate?.navigationMapViewUserAnchorPoint(self), anchorPoint != .zero { - return anchorPoint - } - // TODO: Verify whether content insets verification is required. - let contentFrame = bounds - return CGPoint(x: contentFrame.midX, y: contentFrame.midY) - } - - /** - Determines whether the map should follow the user location and rotate when the course changes. - - seealso: NavigationMapViewCourseTrackingDelegate - */ - open var tracksUserCourse: Bool = false { - didSet { - if tracksUserCourse { - enableFrameByFrameCourseViewTracking(for: 3) - altitude = defaultAltitude - showsUserLocation = true - courseTrackingDelegate?.navigationMapViewDidStartTrackingCourse(self) - } else { - courseTrackingDelegate?.navigationMapViewDidStopTrackingCourse(self) - } - if let location = userLocationForCourseTracking { - updateCourseTracking(location: location, animated: true) - } - } - } /** A type that represents a `UIView` that is `CourseUpdatable`. @@ -256,16 +166,37 @@ open class NavigationMapView: UIView { */ private(set) var predictiveCacheManager: PredictiveCacheManager? + /** + Initializes a newly allocated `NavigationMapView` object with the specified frame rectangle. + + - parameter frame: The frame rectangle for the `NavigationMapView`. + */ public override init(frame: CGRect) { - altitude = defaultAltitude super.init(frame: frame) setupMapView(frame) commonInit() } + /** + Initializes a newly allocated `NavigationMapView` object with the specified frame rectangle and type of `NavigationCamera`. + + - parameter frame: The frame rectangle for the `NavigationMapView`. + - parameter navigationCameraType: Type of `NavigationCamera`, which is used for the current instance of `NavigationMapView`. + */ + public init(frame: CGRect, navigationCameraType: NavigationCameraType = .mobile) { + super.init(frame: frame) + + setupMapView(frame, navigationCameraType: navigationCameraType) + commonInit() + } + + /** + Returns a `NavigationMapView` object initialized from data in a given unarchiver. + + - parameter coder: An unarchiver object. + */ public required init?(coder: NSCoder) { - altitude = defaultAltitude super.init(coder: coder) setupMapView(self.bounds) @@ -273,14 +204,42 @@ open class NavigationMapView: UIView { } fileprivate func commonInit() { - makeGestureRecognizersRespectCourseTracking() - makeGestureRecognizersUpdateCourseView() setupGestureRecognizers() installUserCourseView() - showsUserLocation = false + subscribeForNotifications() } - func setupMapView(_ frame: CGRect) { + deinit { + unsubscribeFromNotifications() + } + + func subscribeForNotifications() { + NotificationCenter.default.addObserver(self, + selector: #selector(navigationCameraStateDidChange(_:)), + name: .navigationCameraStateDidChange, + object: navigationCamera) + } + + func unsubscribeFromNotifications() { + NotificationCenter.default.removeObserver(self, + name: .navigationCameraStateDidChange, + object: nil) + } + + @objc func navigationCameraStateDidChange(_ notification: Notification) { + guard let location = mostRecentUserCourseViewLocation, + let navigationCameraState = notification.userInfo?[NavigationCamera.NotificationUserInfoKey.state] as? NavigationCameraState else { return } + + switch navigationCameraState { + case .idle: + break + case .transitionToFollowing, .following, .transitionToOverview, .overview: + updateUserCourseView(location) + break + } + } + + func setupMapView(_ frame: CGRect, navigationCameraType: NavigationCameraType = .mobile) { guard let accessToken = AccountManager.shared.accessToken else { fatalError("Access token was not set.") } @@ -297,15 +256,16 @@ open class NavigationMapView: UIView { mapView.on(.renderFrameFinished) { [weak self] _ in guard let self = self, - self.shouldPositionCourseViewFrameByFrame, - let location = self.userLocationForCourseTracking else { return } - - self.userCourseView.center = self.mapView.screenCoordinate(for: location.coordinate).point + let location = self.mostRecentUserCourseViewLocation else { return } + self.updateUserCourseView(location, animated: false) } addSubview(mapView) mapView.pinTo(parentView: self) + + navigationCamera = NavigationCamera(mapView, navigationCameraType: navigationCameraType) + navigationCamera.follow() } func setupGestureRecognizers() { @@ -334,11 +294,6 @@ open class NavigationMapView: UIView { // MARK: - Overridden methods - open override func layoutMarginsDidChange() { - super.layoutMarginsDidChange() - enableFrameByFrameCourseViewTracking(for: 3) - } - open override func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() @@ -351,148 +306,71 @@ open class NavigationMapView: UIView { addSubview(imageView) } - open override func layoutSubviews() { - super.layoutSubviews() - - // If the map is in tracking mode, make sure we update the camera and anchor after the layout pass. - if tracksUserCourse { - updateCourseTracking(location: userLocationForCourseTracking) - - // TODO: Find appropriate place where anchor can be updated. - mapView.anchor = userAnchorPoint - } - } - /** Updates the map view’s preferred frames per second to the appropriate value for the current route progress. - This method accounts for the proximity to a maneuver and the current power source. It has no effect if `tracksUserCourse` is set to `true`. + This method accounts for the proximity to a maneuver and the current power source. + It has no effect if `NavigationCameraState` is in `.following` mode. */ - open func updatePreferredFrameRate(for routeProgress: RouteProgress) { - guard tracksUserCourse else { return } + public func updatePreferredFrameRate(for routeProgress: RouteProgress) { + guard navigationCamera.state == .following else { return } let stepProgress = routeProgress.currentLegProgress.currentStepProgress let expectedTravelTime = stepProgress.step.expectedTravelTime let durationUntilNextManeuver = stepProgress.durationRemaining let durationSincePreviousManeuver = expectedTravelTime - durationUntilNextManeuver - let conservativeFramesPerSecond = UIDevice.current.isPluggedIn ? FrameIntervalOptions.pluggedInFramesPerSecond : minimumFramesPerSecond - - var preferredFramesPerSecond = FrameIntervalOptions.pluggedInFramesPerSecond - if let upcomingStep = routeProgress.currentLegProgress.upcomingStep, - upcomingStep.maneuverDirection == .straightAhead || upcomingStep.maneuverDirection == .slightLeft || upcomingStep.maneuverDirection == .slightRight { - preferredFramesPerSecond = shouldPositionCourseViewFrameByFrame ? FrameIntervalOptions.defaultFramesPerSecond : conservativeFramesPerSecond - } else if durationUntilNextManeuver > FrameIntervalOptions.durationUntilNextManeuver && - durationSincePreviousManeuver > FrameIntervalOptions.durationSincePreviousManeuver { - preferredFramesPerSecond = shouldPositionCourseViewFrameByFrame ? FrameIntervalOptions.defaultFramesPerSecond : conservativeFramesPerSecond - } + var preferredFramesPerSecond = FrameIntervalOptions.defaultFramesPerSecond + let maneuverDirections: [ManeuverDirection] = [.straightAhead, .slightLeft, .slightRight] + if let maneuverDirection = routeProgress.currentLegProgress.upcomingStep?.maneuverDirection, + maneuverDirections.contains(maneuverDirection) || + (durationUntilNextManeuver > FrameIntervalOptions.durationUntilNextManeuver && + durationSincePreviousManeuver > FrameIntervalOptions.durationSincePreviousManeuver) { + preferredFramesPerSecond = UIDevice.current.isPluggedIn ? FrameIntervalOptions.pluggedInFramesPerSecond : minimumFramesPerSecond + } + mapView.update { $0.render.preferredFramesPerSecond = preferredFramesPerSecond } } - /** - Track position on a frame by frame basis. Used for first location update and when resuming tracking mode - */ - public func enableFrameByFrameCourseViewTracking(for duration: TimeInterval) { - NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(disableFrameByFramePositioning), object: nil) - perform(#selector(disableFrameByFramePositioning), with: nil, afterDelay: duration) - shouldPositionCourseViewFrameByFrame = true - } - - @objc fileprivate func disableFrameByFramePositioning() { - shouldPositionCourseViewFrameByFrame = false - } - // MARK: - User tracking methods func installUserCourseView() { - if let location = userLocationForCourseTracking { - updateCourseTracking(location: location) - } + userCourseView.isHidden = true mapView.addSubview(userCourseView) } - @objc private func disableUserCourseTracking() { - guard tracksUserCourse else { return } - tracksUserCourse = false - } - - public func updateCourseTracking(location: CLLocation?, camera: CameraOptions? = nil, animated: Bool = false) { - // While animating to overhead mode, don't animate the puck. - let duration: TimeInterval = animated && !isAnimatingToOverheadMode ? 1 : 0 - userLocationForCourseTracking = location - guard let location = location, CLLocationCoordinate2DIsValid(location.coordinate) else { - return - } + /** + Updates `UserCourseView` to provided location. + */ + public func updateUserCourseView(_ location: CLLocation, animated: Bool = false) { + guard CLLocationCoordinate2DIsValid(location.coordinate) else { return } + + mostRecentUserCourseViewLocation = location - let centerUserCourseView = { [weak self] in + // While animating to overview mode, don't animate the puck. + let duration: TimeInterval = animated && navigationCamera.state != .transitionToOverview ? 1 : 0 + UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear]) { [weak self] in guard let point = self?.mapView.screenCoordinate(for: location.coordinate).point else { return } - self?.userCourseView.center = point } - if tracksUserCourse { - centerUserCourseView() - - let zoomLevel = CGFloat(ZoomLevelForAltitude(altitude, - self.mapView.pitch, - location.coordinate.latitude, - self.mapView.bounds.size)) - - let camera = camera ?? CameraOptions(center: location.coordinate, - zoom: zoomLevel, - bearing: location.course, - pitch: 45) - - cameraAnimator = mapView.cameraManager.makeCameraAnimator(duration: duration, curve: .linear) - cameraAnimator.stopAnimation() - cameraAnimator.addAnimations { - if let center = camera.center { - self.mapView.centerCoordinate = center - } - - if let zoom = camera.zoom { - self.mapView.zoom = zoom - } - - if let bearing = camera.bearing { - self.mapView.bearing = bearing - } - - if let anchor = camera.anchor { - self.mapView.anchor = anchor - } - - if let pitch = camera.pitch { - self.mapView.pitch = pitch - } - - if let padding = camera.padding { - self.mapView.padding = padding - } - } - - cameraAnimator.startAnimation() - } else { - // Animate course view updates in overview mode - UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear], animations: centerUserCourseView) - } - userCourseView.update(location: location, pitch: mapView.pitch, direction: mapView.bearing, animated: animated, - tracksUserCourse: tracksUserCourse) + navigationCameraState: navigationCamera.state) } - // MARK: Feature Addition/removal properties and methods + // MARK: Feature addition/removal properties and methods /** Showcases route array. Adds routes and waypoints to map, and sets camera to point encompassing the route. + + - parameter routes: List of `Route` objects, which will be shown on `MapView.` + - parameter animated: Property, which determines whether camera movement will be animated while fitting first route. */ - public static let defaultPadding: UIEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20) - public func showcase(_ routes: [Route], animated: Bool = false) { guard let activeRoute = routes.first, let coordinates = activeRoute.shape?.coordinates, @@ -505,18 +383,17 @@ open class NavigationMapView: UIView { show(routes) showWaypoints(on: activeRoute) - fit(to: activeRoute, facing: 0, animated: animated) + navigationCamera.stop() + fitCamera(to: activeRoute, animated: animated) } - func fit(to route: Route, facing direction: CLLocationDirection = 0, animated: Bool = false) { - guard let shape = route.shape, !shape.coordinates.isEmpty else { return } - - let newCamera = mapView.cameraManager.camera(fitting: .lineString(shape), - edgePadding: safeArea + NavigationMapView.defaultPadding, - bearing: CGFloat(direction), - pitch: 0) - - mapView.cameraManager.setCamera(to: newCamera, animated: animated, completion: nil) + func fitCamera(to route: Route, animated: Bool = false) { + guard let routeShape = route.shape, !routeShape.coordinates.isEmpty else { return } + let edgeInsets = safeArea + UIEdgeInsets(top: 10.0, left: 20.0, bottom: 10.0, right: 20.0) + if let cameraOptions = mapView?.cameraManager.camera(fitting: .lineString(routeShape), + edgePadding: edgeInsets) { + mapView?.cameraManager.setCamera(to: cameraOptions, animated: animated) + } } public func show(_ routes: [Route], legIndex: Int = 0) { @@ -543,6 +420,23 @@ open class NavigationMapView: UIView { } } + /** + Sets initial `CameraOptions` for specific coordinate. + + - parameter coordinate: Coordinate, where `MapView` will be centered. + */ + func setInitialCamera(_ coordinate: CLLocationCoordinate2D) { + guard let navigationViewportDataSource = navigationCamera.viewportDataSource as? NavigationViewportDataSource else { return } + + let zoom = CGFloat(ZoomLevelForAltitude(navigationViewportDataSource.defaultAltitude, + mapView.pitch, + coordinate.latitude, + mapView.bounds.size)) + + mapView.cameraManager.setCamera(to: CameraOptions(center: coordinate, zoom: zoom)) + updateUserCourseView(CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) + } + @discardableResult func addMainRouteLayer(_ route: Route) -> String? { guard let shape = route.shape else { return nil } @@ -749,11 +643,11 @@ open class NavigationMapView: UIView { func defaultWaypointSymbolLayer() -> SymbolLayer { var symbols = SymbolLayer(id: IdentifierString.waypointSymbol) symbols.source = IdentifierString.waypointSource - symbols.layout?.textField = .expression(Exp(.toString){ - Exp(.get){ - "name" - } - }) + symbols.layout?.textField = .expression(Exp(.toString) { + Exp(.get) { + "name" + } + }) symbols.layout?.textSize = .constant(.init(10)) symbols.paint?.textOpacity = .expression(Exp(.switchCase) { Exp(.any) { @@ -1003,35 +897,6 @@ open class NavigationMapView: UIView { } } - /** - Sets the camera directly over a series of coordinates. - */ - public func setOverheadCameraView(from userLocation: CLLocation, along lineString: LineString, for padding: UIEdgeInsets) { - isAnimatingToOverheadMode = true - tracksUserCourse = false - - // TODO: Implement functionality which allows to change camera options based on traversed distance. - - let newCamera = mapView.cameraManager.camera(fitting: .lineString(lineString), - edgePadding: padding, - bearing: 0, - pitch: 0) - - mapView.cameraManager.setCamera(to: newCamera, animated: true, duration: 1) { [weak self] _ in - self?.isAnimatingToOverheadMode = false - } - - updateCourseView(to: userLocation, pitch: newCamera.pitch, direction: newCamera.bearing, animated: true) - } - - /** - Recenters the camera and begins tracking the user's location. - */ - public func recenterMap() { - tracksUserCourse = true - enableFrameByFrameCourseViewTracking(for: 3) - } - // MARK: - Gesture recognizers methods /** @@ -1102,62 +967,4 @@ open class NavigationMapView: UIView { return candidates } - - @objc func updateCourseView(_ sender: UIGestureRecognizer) { - if sender.state == .ended, let validAltitude = mapView.altitude { - altitude = validAltitude - enableFrameByFrameCourseViewTracking(for: 2) - } - - // Capture altitude for double tap and two finger tap after animation finishes - if sender is UITapGestureRecognizer, sender.state == .ended { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { - if let altitude = self.mapView.altitude { - self.altitude = altitude - } - }) - } - - if let panGesture = sender as? UIPanGestureRecognizer, - sender.state == .ended || sender.state == .cancelled { - let velocity = panGesture.velocity(in: self) - let didFling = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) > 100 - if didFling { - enableFrameByFrameCourseViewTracking(for: 1) - } - } - - if sender.state == .changed { - guard let location = userLocationForCourseTracking else { return } - updateCourseView(to: location) - } - } - - // MARK: - Utility methods - - /** - Modifies the gesture recognizers to also disable course tracking. - */ - func makeGestureRecognizersRespectCourseTracking() { - for gestureRecognizer in mapView.gestureRecognizers ?? [] - where gestureRecognizer is UIPanGestureRecognizer || gestureRecognizer is UIRotationGestureRecognizer { - gestureRecognizer.addTarget(self, action: #selector(disableUserCourseTracking)) - } - } - - func makeGestureRecognizersUpdateCourseView() { - for gestureRecognizer in mapView.gestureRecognizers ?? [] { - gestureRecognizer.addTarget(self, action: #selector(updateCourseView(_:))) - } - } - - private func updateCourseView(to location: CLLocation, pitch: CGFloat? = nil, direction: CLLocationDirection? = nil, animated: Bool = false) { - userCourseView.update(location: location, - pitch: pitch ?? mapView.pitch, - direction: direction ?? mapView.bearing, - animated: animated, - tracksUserCourse: tracksUserCourse) - - userCourseView.center = mapView.screenCoordinate(for: location.coordinate).point - } } diff --git a/Sources/MapboxNavigation/NavigationMapViewDelegate.swift b/Sources/MapboxNavigation/NavigationMapViewDelegate.swift index dfba4ffe9a4..479d1652a58 100644 --- a/Sources/MapboxNavigation/NavigationMapViewDelegate.swift +++ b/Sources/MapboxNavigation/NavigationMapViewDelegate.swift @@ -50,14 +50,6 @@ public protocol NavigationMapViewDelegate: class, UnimplementedLogging { - returns: Optionally, a `FeatureCollection` that defines the shape of the waypoint, or `nil` to use default behavior. */ func navigationMapView(_ navigationMapView: NavigationMapView, shapeFor waypoints: [Waypoint], legIndex: Int) -> FeatureCollection? - - /** - Asks the receiver to return a `CGPoint` to serve as the anchor for the user icon. - - important: The return value should be returned in the normal UIKit coordinate-space, NOT CoreAnimation's unit coordinate-space. - - parameter navigationMapView: The `NavigationMapView`. - - returns: A `CGPoint` (in regular coordinate-space) that represents the point on-screen where the user location icon should be drawn. - */ - func navigationMapViewUserAnchorPoint(_ navigationMapView: NavigationMapView) -> CGPoint } public extension NavigationMapViewDelegate { @@ -107,36 +99,4 @@ public extension NavigationMapViewDelegate { logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) return .zero } -} - -// MARK: - NavigationMapViewCourseTrackingDelegate methods - -/** - The `NavigationMapViewCourseTrackingDelegate` provides methods for responding to the `NavigationMapView` starting or stopping course tracking. - */ -public protocol NavigationMapViewCourseTrackingDelegate: class, UnimplementedLogging { - /** - Tells the receiver that the map is now tracking the user course. - - seealso: NavigationMapView.tracksUserCourse - - parameter mapView: The NavigationMapView. - */ - func navigationMapViewDidStartTrackingCourse(_ mapView: NavigationMapView) - - /** - Tells the receiver that `tracksUserCourse` was set to false, signifying that the map is no longer tracking the user course. - - seealso: NavigationMapView.tracksUserCourse - - parameter mapView: The NavigationMapView. - */ - func navigationMapViewDidStopTrackingCourse(_ mapView: NavigationMapView) -} - -public extension NavigationMapViewCourseTrackingDelegate { - - func navigationMapViewDidStartTrackingCourse(_ mapView: NavigationMapView) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } - - func navigationMapViewDidStopTrackingCourse(_ mapView: NavigationMapView) { - logUnimplemented(protocolType: NavigationMapViewDelegate.self, level: .debug) - } -} +} \ No newline at end of file diff --git a/Sources/MapboxNavigation/NavigationView.swift b/Sources/MapboxNavigation/NavigationView.swift index a36ec39de05..adfb57afa03 100644 --- a/Sources/MapboxNavigation/NavigationView.swift +++ b/Sources/MapboxNavigation/NavigationView.swift @@ -59,7 +59,8 @@ open class NavigationView: UIView { lazy var navigationMapView: NavigationMapView = { let navigationMapView: NavigationMapView = .forAutoLayout(frame: self.bounds) navigationMapView.delegate = delegate - navigationMapView.courseTrackingDelegate = delegate + navigationMapView.navigationCamera.viewportDataSource = NavigationViewportDataSource(navigationMapView.mapView, + viewportDataSourceType: .active) return navigationMapView }() @@ -189,11 +190,10 @@ open class NavigationView: UIView { private func updateDelegates() { navigationMapView.delegate = delegate - navigationMapView.courseTrackingDelegate = delegate } } -protocol NavigationViewDelegate: NavigationMapViewDelegate, InstructionsBannerViewDelegate, NavigationMapViewCourseTrackingDelegate, VisualInstructionDelegate { +protocol NavigationViewDelegate: NavigationMapViewDelegate, InstructionsBannerViewDelegate, VisualInstructionDelegate { func navigationView(_ view: NavigationView, didTapCancelButton: CancelButton) } diff --git a/Sources/MapboxNavigation/NavigationViewController.swift b/Sources/MapboxNavigation/NavigationViewController.swift index 583bf9839ae..f516bc5cec7 100644 --- a/Sources/MapboxNavigation/NavigationViewController.swift +++ b/Sources/MapboxNavigation/NavigationViewController.swift @@ -62,11 +62,6 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter return navigationService!.directions } - /** - An optional `CameraOptions` you can use to improve the initial transition from a previous viewport and prevent a trigger from an excessive significant location update. - */ - public var pendingCamera: CameraOptions? - /** The receiver’s delegate. */ @@ -329,34 +324,24 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter } func addRouteMapViewController(_ navigationOptions: NavigationOptions?) { - let mapViewController = RouteMapViewController(navigationService: self.navigationService, - delegate: self, - topBanner: addTopBanner(navigationOptions), - bottomBanner: addBottomBanner(navigationOptions)) - mapViewController.destination = route.legs.last?.destination - mapViewController.view.pinInSuperview() - mapViewController.reportButton.isHidden = !showsReportFeedback - mapViewController.view.translatesAutoresizingMaskIntoConstraints = false - - self.mapViewController = mapViewController - - embed(mapViewController, in: view) { (parent, map) -> [NSLayoutConstraint] in + let routeMapViewController = RouteMapViewController(navigationService: self.navigationService, + delegate: self, + topBanner: addTopBanner(navigationOptions), + bottomBanner: addBottomBanner(navigationOptions)) + routeMapViewController.destination = route.legs.last?.destination + routeMapViewController.view.pinInSuperview() + routeMapViewController.reportButton.isHidden = !showsReportFeedback + routeMapViewController.view.translatesAutoresizingMaskIntoConstraints = false + + self.mapViewController = routeMapViewController + + embed(routeMapViewController, in: view) { (parent, map) -> [NSLayoutConstraint] in return map.view.constraintsForPinning(to: parent.view) } - setInitialCoordinate(in: mapViewController) - } - - func setInitialCoordinate(in routeMapViewController: RouteMapViewController) { - guard let mapView = routeMapViewController.navigationMapView.mapView, - let centerCoordinate = routeMapViewController.navigationService.routeProgress.route.shape?.coordinates.first else { return } - - let zoom = CGFloat(ZoomLevelForAltitude(routeMapViewController.navigationMapView.defaultAltitude, - mapView.pitch, - centerCoordinate.latitude, - mapView.bounds.size)) - - mapView.cameraManager.setCamera(to: CameraOptions(center: centerCoordinate, zoom: zoom)) + if let coordinate = routeMapViewController.navigationService.routeProgress.route.shape?.coordinates.first { + routeMapViewController.navigationMapView.setInitialCamera(coordinate) + } } func addTopBanner(_ navigationOptions: NavigationOptions?) -> ContainerViewController { @@ -403,6 +388,9 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter return } navigationService(navigationService, didPassVisualInstructionPoint: firstInstruction, routeProgress: navigationService.routeProgress) + + // By default `NavigationCamera` in active guidance navigation should be set to `NavigationCameraState.following` state. + navigationMapView?.navigationCamera.follow() } open override func viewWillAppear(_ animated: Bool) { @@ -538,10 +526,6 @@ extension NavigationViewController: RouteMapViewControllerDelegate { } } - public func navigationMapViewUserAnchorPoint(_ navigationMapView: NavigationMapView) -> CGPoint { - return delegate?.navigationViewController(self, mapViewUserAnchorPoint: navigationMapView) ?? .zero - } - func mapViewControllerShouldAnnotateSpokenInstructions(_ routeMapViewController: RouteMapViewController) -> Bool { return annotatesSpokenInstructions } @@ -629,18 +613,13 @@ extension NavigationViewController: NavigationServiceDelegate { let shouldPrevent = navigationService.delegate?.navigationService(navigationService, shouldPreventReroutesWhenArrivingAt: destination) ?? RouteController.DefaultBehavior.shouldPreventReroutesWhenArrivingAtWaypoint let userHasArrivedAndShouldPreventRerouting = shouldPrevent && !progress.currentLegProgress.userHasArrivedAtWaypoint - if snapsUserLocationAnnotationToRoute, - userHasArrivedAndShouldPreventRerouting { + if snapsUserLocationAnnotationToRoute, userHasArrivedAndShouldPreventRerouting { mapViewController?.labelCurrentRoad(at: rawLocation, for: location) + mapViewController?.navigationMapView.updateUserCourseView(location, animated: true) } else { mapViewController?.labelCurrentRoad(at: rawLocation) } - if snapsUserLocationAnnotationToRoute, - userHasArrivedAndShouldPreventRerouting { - mapViewController?.navigationMapView.updateCourseTracking(location: location, animated: true) - } - attemptToHighlightBuildings(progress, with: location) // Finally, pass the message onto the `NavigationViewControllerDelegate`. @@ -675,10 +654,7 @@ extension NavigationViewController: NavigationServiceDelegate { let advancesToNextLeg = componentsWantAdvance && (delegate?.navigationViewController(self, didArriveAt: waypoint) ?? defaultBehavior) if service.routeProgress.isFinalLeg && advancesToNextLeg && showsEndOfRouteFeedback { - // In case of final destination present end of route view first and then re-center final destination. - showEndOfRouteFeedback { [weak self] _ in - self?.frameDestinationArrival(for: service.router.location) - } + showEndOfRouteFeedback() } return advancesToNextLeg } @@ -756,57 +732,23 @@ extension NavigationViewController: NavigationServiceDelegate { // At the same time this check will prevent building highlighting in case of arrival in overview mode/high altitude. if progress.fractionTraveled >= 1.0 { return } if waypointStyle == .annotation { return } - guard let navigationMapView = navigationMapView else { return } if currentLeg != progress.currentLeg { currentLeg = progress.currentLeg passedApproachingDestinationThreshold = false - mapViewController?.suppressAutomaticAltitudeChanges = false foundAllBuildings = false - navigationMapView.altitude = navigationMapView.defaultAltitude } - - let altitude = AltitudeForZoomLevel(16.1, - navigationMapView.mapView.pitch, - location.coordinate.latitude, - navigationMapView.mapView.frame.size) - + if !passedApproachingDestinationThreshold, progress.currentLegProgress.distanceRemaining < approachingDestinationThreshold { passedApproachingDestinationThreshold = true - mapViewController?.suppressAutomaticAltitudeChanges = true - } - - // Attempt to decrease altitude so that highlighted building becomes visible. - // This is required in cases when: - // - Switching from overview to follow mode. - // - Previous attempt to decrease altitude failed (happens when highlighted building is within destination - // threshold right after starting navigation). - // FIXME: When device was rotated to landscape mode altitude should be adjusted so that building is highlighted. - if passedApproachingDestinationThreshold, - navigationMapView.altitude == navigationMapView.defaultAltitude, - altitude < navigationMapView.altitude { - navigationMapView.altitude = altitude } - + if !foundAllBuildings, passedApproachingDestinationThreshold, let currentLegWaypoint = progress.currentLeg.destination?.targetCoordinate { - navigationMapView.highlightBuildings(at: [currentLegWaypoint], - in3D: waypointStyle == .extrudedBuilding ? true : false, - completion: { (result) -> Void in - self.foundAllBuildings = result - }) + navigationMapView?.highlightBuildings(at: [currentLegWaypoint], in3D: waypointStyle == .extrudedBuilding ? true : false, completion: { (found) in + self.foundAllBuildings = found + }) } } - - private func frameDestinationArrival(for location: CLLocation?) { - if waypointStyle == .annotation { return } - guard let mapViewController = self.mapViewController else { return } - guard let location = location else { return } - - // Update insets to be able to correctly center map view after presenting end of route view. - mapViewController.updateMapViewContentInsets() - // Update user course view to correctly place it in map view. - self.navigationMapView?.updateCourseTracking(location: location, animated: false) - } } // MARK: - StyleManagerDelegate @@ -916,6 +858,7 @@ extension NavigationViewController: TopBannerViewControllerDelegate { steps: remaining) } + navigationMapView?.navigationCamera.stop() mapViewController?.center(on: upcomingStep, route: route, legIndex: legIndex, @@ -929,12 +872,7 @@ extension NavigationViewController: TopBannerViewControllerDelegate { let legProgress = RouteLegProgress(leg: progress.route.legs[legIndex], stepIndex: stepIndex) let step = legProgress.currentStep self.preview(step: step, in: banner, remaining: progress.remainingSteps, route: progress.route, animated: false) - - // After selecting maneuver and dismissing steps table make sure to update contentInsets of NavigationMapView - // to correctly place selected maneuver in the center of the screen (taking into account top and bottom banner heights). - banner.dismissStepsTable { [weak self] in - self?.mapViewController?.updateMapViewContentInsets() - } + banner.dismissStepsTable() } public func topBanner(_ banner: TopBannerViewController, didDisplayStepsController: StepsViewController) { @@ -942,12 +880,6 @@ extension NavigationViewController: TopBannerViewControllerDelegate { } } -fileprivate extension Route { - func leg(containing step: RouteStep) -> RouteLeg? { - return legs.first { $0.steps.contains(step) } - } -} - // MARK: - BottomBannerViewControllerDelegate // Handling cancel action in new Bottom Banner container. diff --git a/Sources/MapboxNavigation/NavigationViewportDataSource.swift b/Sources/MapboxNavigation/NavigationViewportDataSource.swift new file mode 100644 index 00000000000..ed8d0f9f1b9 --- /dev/null +++ b/Sources/MapboxNavigation/NavigationViewportDataSource.swift @@ -0,0 +1,424 @@ +import MapboxMaps +import MapboxCoreNavigation +import Turf +import MapboxDirections + +/** + Class, which conforms to `ViewportDataSource` protocol and provides default implementation of it. + */ +public class NavigationViewportDataSource: ViewportDataSource { + + /** + Delegate, which is used to notify `NavigationCamera` regarding upcoming `CameraOptions` + related changes. + */ + public var delegate: ViewportDataSourceDelegate? + + /** + `CameraOptions`, which are used on iOS when transitioning to `NavigationCameraState.following` or + for continuous updates when already in `NavigationCameraState.following` state. + */ + public var followingMobileCamera: CameraOptions = CameraOptions() + + /** + `CameraOptions`, which are used on CarPlay when transitioning to `NavigationCameraState.following` or + for continuous updates when already in `NavigationCameraState.following` state. + */ + public var followingCarPlayCamera: CameraOptions = CameraOptions() + + /** + `CameraOptions`, which are used on iOS when transitioning to `NavigationCameraState.overview` or + for continuous updates when already in `NavigationCameraState.overview` state. + */ + public var overviewMobileCamera: CameraOptions = CameraOptions() + + /** + `CameraOptions`, which are used on CarPlay when transitioning to `NavigationCameraState.overview` or + for continuous updates when already in `NavigationCameraState.overview` state. + */ + public var overviewCarPlayCamera: CameraOptions = CameraOptions() + + /** + Value of maximum pitch, which will be taken into account when preparing `CameraOptions` during + active guidance navigation. + + Defaults to `45.0` degrees. + */ + public var maximumPitch: Double = 45.0 + + /** + Altitude that the `NavigationCamera` initally defaults to when navigation starts. + + Defaults to `1000.0` meters. + */ + public var defaultAltitude: CLLocationDistance = 1000.0 + + /** + Controls the distance on route after the current maneuver to include in the frame. + + Defaults to `100.0` meters. + */ + public var distanceToFrameAfterManeuver: CLLocationDistance = 100.0 + + /** + Controls how much the bearing can deviate from the location's bearing, in degrees. + + In case if set, the `bearing` property of `CameraOptions` during active guidance navigation + won't exactly reflect the bearing returned by the location, but will also be affected by the + direction to the upcoming framed geometry, to maximize the viewable area. + + Defaults to `20.0` degrees. + */ + public var maximumBearingSmoothingAngle: CLLocationDirection? = 20.0 + + /** + Value of default viewport padding. + */ + var viewportPadding: UIEdgeInsets = .zero + + weak var mapView: MapView? + + // MARK: - Initializer methods + + /** + Initializer of `NavigationViewportDataSource` object. + + - parameter mapView: Instance of `MapView`, which is going to be used for several operations, + which includes (but not limited to) subscription to raw location updates via `LocationConsumer` + (in case if `viewportDataSourceType` was set to `.raw`). `MapView` will be weakly stored by + `NavigationViewportDataSource`. + - parameter viewportDataSourceType: Type of locations, which will be used to prepare `CameraOptions`. + */ + public required init(_ mapView: MapView, viewportDataSourceType: ViewportDataSourceType = .passive) { + self.mapView = mapView + + subscribeForNotifications(viewportDataSourceType) + } + + deinit { + unsubscribeFromNotifications() + } + + // MARK: - Notifications observer methods + + func subscribeForNotifications(_ viewportDataSourceType: ViewportDataSourceType = .passive) { + switch viewportDataSourceType { + case .raw: + self.mapView?.locationManager.addLocationConsumer(newConsumer: self) + case .passive: + NotificationCenter.default.addObserver(self, + selector: #selector(progressDidChange(_:)), + name: .passiveLocationDataSourceDidUpdate, + object: nil) + case .active: + NotificationCenter.default.addObserver(self, + selector: #selector(progressDidChange(_:)), + name: .routeControllerProgressDidChange, + object: nil) + } + } + + func unsubscribeFromNotifications() { + NotificationCenter.default.removeObserver(self, + name: .routeControllerProgressDidChange, + object: nil) + + NotificationCenter.default.removeObserver(self, + name: .passiveLocationDataSourceDidUpdate, + object: nil) + } + + @objc func progressDidChange(_ notification: NSNotification) { + let passiveLocation = notification.userInfo?[PassiveLocationDataSource.NotificationUserInfoKey.locationKey] as? CLLocation + let activeLocation = notification.userInfo?[RouteController.NotificationUserInfoKey.locationKey] as? CLLocation + let routeProgress = notification.userInfo?[RouteController.NotificationUserInfoKey.routeProgressKey] as? RouteProgress + let cameraOptions = self.cameraOptions(passiveLocation: passiveLocation, + activeLocation: activeLocation, + routeProgress: routeProgress) + delegate?.viewportDataSource(self, didUpdate: cameraOptions) + + NotificationCenter.default.post(name: .navigationCameraViewportDidChange, object: self, userInfo: [ + NavigationCamera.NotificationUserInfoKey.cameraOptions: cameraOptions + ]) + } + + // MARK: - CameraOptions methods + + func cameraOptions(_ rawLocation: CLLocation? = nil, + passiveLocation: CLLocation? = nil, + activeLocation: CLLocation? = nil, + routeProgress: RouteProgress? = nil) -> [String: CameraOptions] { + updateFollowingCamera(rawLocation, + passiveLocation: passiveLocation, + activeLocation: activeLocation, + routeProgress: routeProgress) + + // In active guidance navigation, camera in overview mode is relevant, during free-drive + // navigation it's not used. + updateOverviewCamera(activeLocation, + routeProgress: routeProgress) + + let cameraOptions = [ + CameraOptions.followingMobileCamera: followingMobileCamera, + CameraOptions.overviewMobileCamera: overviewMobileCamera, + CameraOptions.followingCarPlayCamera: followingCarPlayCamera, + CameraOptions.overviewCarPlayCamera: overviewCarPlayCamera + ] + + return cameraOptions + } + + func updateFollowingCamera(_ rawLocation: CLLocation? = nil, + passiveLocation: CLLocation? = nil, + activeLocation: CLLocation? = nil, + routeProgress: RouteProgress? = nil) { + guard let mapView = mapView else { return } + + if let location = rawLocation ?? passiveLocation { + let followingWithoutRouteZoomLevel = CGFloat(14.0) + + followingMobileCamera.center = location.coordinate + followingMobileCamera.zoom = followingWithoutRouteZoomLevel + followingMobileCamera.bearing = 0.0 + followingMobileCamera.anchor = mapView.center + followingMobileCamera.pitch = 0.0 + followingMobileCamera.padding = .zero + + followingCarPlayCamera.center = location.coordinate + followingCarPlayCamera.zoom = followingWithoutRouteZoomLevel + followingCarPlayCamera.bearing = 0.0 + followingCarPlayCamera.anchor = mapView.center + followingCarPlayCamera.pitch = 0.0 + followingCarPlayCamera.padding = .zero + + return + } + + if let location = activeLocation, let routeProgress = routeProgress { + let pitchСoefficient = self.pitchСoefficient(routeProgress, currentCoordinate: location.coordinate) + let pitch = maximumPitch * pitchСoefficient + var compoundManeuvers: [[CLLocationCoordinate2D]] = [] + let stepIndex = routeProgress.currentLegProgress.stepIndex + let nextStepIndex = min(stepIndex + 1, routeProgress.currentLeg.steps.count - 1) + let coordinatesAfterCurrentStep = routeProgress.currentLeg.steps[nextStepIndex...].map({ $0.shape?.coordinates }) + for step in coordinatesAfterCurrentStep { + guard let stepCoordinates = step, let distance = stepCoordinates.distance() else { continue } + if distance > 0.0 && distance < 150.0 { + compoundManeuvers.append(stepCoordinates) + } else { + compoundManeuvers.append(stepCoordinates.trimmed(distance: distanceToFrameAfterManeuver)) + break + } + } + + let coordinatesForManeuverFraming = compoundManeuvers.reduce([], +) + let coordinatesToManeuver = routeProgress.currentLegProgress.currentStep.shape?.coordinates.sliced(from: location.coordinate) ?? [] + let centerLineString = LineString([location.coordinate, (coordinatesToManeuver + coordinatesForManeuverFraming).map({ mapView.point(for: $0) }).boundingBoxPoints.map({ mapView.coordinate(for: $0) }).centerCoordinate]) + let centerLineStringTotalDistance = centerLineString.distance() ?? 0.0 + let centerCoordDistance = centerLineStringTotalDistance * (1 - pitchСoefficient) + + var center: CLLocationCoordinate2D = location.coordinate + if let adjustedCenter = centerLineString.coordinateFromStart(distance: centerCoordDistance) { + center = adjustedCenter + } + + let averageIntersectionDistances = routeProgress.route.legs.map { (leg) -> [CLLocationDistance] in + return leg.steps.map { (step) -> CLLocationDistance in + if let firstStepCoordinate = step.shape?.coordinates.first, + let lastStepCoordinate = step.shape?.coordinates.last { + let intersectionLocations = [firstStepCoordinate] + (step.intersections?.map({ $0.location }) ?? []) + [lastStepCoordinate] + let intersectionDistances = intersectionLocations[1...].enumerated().map({ (index, intersection) -> CLLocationDistance in + return intersection.distance(to: intersectionLocations[index]) + }) + let filteredIntersectionDistances = intersectionDistances.filter { $0 > 20 } + let averageIntersectionDistance = filteredIntersectionDistances.reduce(0.0, +) / Double(filteredIntersectionDistances.count) + return averageIntersectionDistance + } + + return 0.0 + } + } + + let currentRouteLegIndex = routeProgress.legIndex + let currentRouteStepIndex = routeProgress.currentLegProgress.stepIndex + let numberOfIntersections = 10 + let lookaheadDistance = averageIntersectionDistances[currentRouteLegIndex][currentRouteStepIndex] * Double(numberOfIntersections) + let coordinatesForIntersections = coordinatesToManeuver.sliced(from: nil, to: LineString(coordinatesToManeuver).coordinateFromStart(distance: fmax(lookaheadDistance, 150.0))) + let bearing = self.bearing(location.course, coordinatesToManeuver: coordinatesForIntersections) + + followingMobileCamera.center = center + followingMobileCamera.zoom = CGFloat(self.zoom(coordinatesToManeuver + coordinatesForManeuverFraming, + pitch: pitch, + edgeInsets: viewportPadding, + defaultZoomLevel: 2.0, + maxZoomLevel: 16.35)) + followingMobileCamera.bearing = bearing + followingMobileCamera.anchor = self.anchor(pitchСoefficient, + maxPitch: maximumPitch, + bounds: mapView.bounds, + edgeInsets: viewportPadding) + followingMobileCamera.pitch = CGFloat(pitch) + followingMobileCamera.padding = viewportPadding + + let carPlayCameraPadding = mapView.safeArea + UIEdgeInsets(top: 10.0, left: 20.0, bottom: 10.0, right: 20.0) + followingCarPlayCamera.center = center + followingCarPlayCamera.zoom = CGFloat(self.zoom(coordinatesToManeuver + coordinatesForManeuverFraming, + pitch: pitch, + edgeInsets: carPlayCameraPadding, + defaultZoomLevel: 2.0, + maxZoomLevel: 16.35)) + followingCarPlayCamera.bearing = bearing + followingCarPlayCamera.anchor = self.anchor(pitchСoefficient, + maxPitch: maximumPitch, + bounds: mapView.bounds, + edgeInsets: carPlayCameraPadding) + followingCarPlayCamera.pitch = CGFloat(pitch) + followingCarPlayCamera.padding = carPlayCameraPadding + } + } + + func updateOverviewCamera(_ activeLocation: CLLocation?, routeProgress: RouteProgress?) { + guard let mapView = mapView, + let coordinate = activeLocation?.coordinate, + let heading = activeLocation?.course, + let routeProgress = routeProgress else { return } + + let stepIndex = routeProgress.currentLegProgress.stepIndex + let nextStepIndex = min(stepIndex + 1, routeProgress.currentLeg.steps.count - 1) + let coordinatesAfterCurrentStep = routeProgress.currentLeg.steps[nextStepIndex...].map({ $0.shape?.coordinates }) + let untraveledCoordinatesOnCurrentStep = routeProgress.currentLegProgress.currentStep.shape?.coordinates.sliced(from: coordinate) ?? [] + let remainingCoordinatesOnRoute = coordinatesAfterCurrentStep.flatten() + untraveledCoordinatesOnCurrentStep + + let center = remainingCoordinatesOnRoute.map({ mapView.point(for: $0) }).boundingBoxPoints.map({ mapView.coordinate(for: $0) }).centerCoordinate + + var zoom = self.zoom(remainingCoordinatesOnRoute, + edgeInsets: viewportPadding, + maxZoomLevel: 16.35) + + // In case if `NavigationCamera` is already in `NavigationCameraState.overview` value of bearing will be ignored. + let bearing = CLLocationDirection(mapView.bearing) + + heading.shortestRotation(angle: CLLocationDirection(mapView.bearing)) + + overviewMobileCamera.pitch = 0.0 + overviewMobileCamera.center = center + overviewMobileCamera.zoom = CGFloat(zoom) + overviewMobileCamera.anchor = self.anchor(0.0, + maxPitch: maximumPitch, + bounds: mapView.bounds, + edgeInsets: viewportPadding) + overviewMobileCamera.bearing = bearing + overviewMobileCamera.padding = viewportPadding + + let carPlayCameraPadding = mapView.safeArea + UIEdgeInsets(top: 10.0, left: 20.0, bottom: 10.0, right: 20.0) + + zoom = self.zoom(remainingCoordinatesOnRoute, + edgeInsets: carPlayCameraPadding, + maxZoomLevel: 16.35) + + overviewCarPlayCamera.pitch = 0.0 + overviewCarPlayCamera.center = center + overviewCarPlayCamera.zoom = CGFloat(zoom) + overviewCarPlayCamera.anchor = self.anchor(0.0, + maxPitch: maximumPitch, + bounds: mapView.bounds, + edgeInsets: carPlayCameraPadding) + overviewCarPlayCamera.bearing = bearing + overviewCarPlayCamera.padding = carPlayCameraPadding + } + + func bearing(_ initialBearing: CLLocationDirection, + coordinatesToManeuver: [CLLocationCoordinate2D]? = nil) -> CLLocationDirection { + var bearing = initialBearing + + if let coordinates = coordinatesToManeuver, + let firstCoordinate = coordinates.first, + let lastCoordinate = coordinates.last { + let directionToManeuver = firstCoordinate.direction(to: lastCoordinate) + let directionDiff = directionToManeuver.shortestRotation(angle: initialBearing) + let bearingMaxDiff = maximumBearingSmoothingAngle ?? 0.0 + if fabs(directionDiff) > bearingMaxDiff { + bearing += bearingMaxDiff * (directionDiff < 0.0 ? -1.0 : 1.0) + } else { + bearing = firstCoordinate.direction(to: lastCoordinate) + } + } + + let mapViewBearing = Double(mapView?.bearing ?? 0.0) + return mapViewBearing + bearing.shortestRotation(angle: mapViewBearing) + } + + func zoom(_ coordinates: [CLLocationCoordinate2D], + pitch: Double = 0.0, + edgeInsets: UIEdgeInsets = .zero, + defaultZoomLevel: Double = 12.0, + maxZoomLevel: Double = 22.0, + minZoomLevel: Double = 2.0) -> Double { + guard let mapView = mapView, + let boundingBox = BoundingBox(from: coordinates) else { return defaultZoomLevel } + + let mapViewInsetWidth = mapView.bounds.size.width - edgeInsets.left - edgeInsets.right + let mapViewInsetHeight = mapView.bounds.size.height - edgeInsets.top - edgeInsets.bottom + let widthDelta = mapViewInsetHeight * 2 - mapViewInsetWidth + let widthWithPitchEffect = CGFloat(mapViewInsetWidth + CGFloat(pitch / maximumPitch) * widthDelta) + let heightWithPitchEffect = CGFloat(mapViewInsetHeight + mapViewInsetHeight * CGFloat(sin(pitch * .pi / 180.0)) * 1.25) + let zoomLevel = boundingBox.zoomLevel(fitTo: CGSize(width: widthWithPitchEffect, height: heightWithPitchEffect)) + + return max(min(zoomLevel, maxZoomLevel), minZoomLevel) + } + + func anchor(_ pitchСoefficient: Double, + maxPitch: Double, + bounds: CGRect, + edgeInsets: UIEdgeInsets) -> CGPoint { + let xCenter = max(((bounds.size.width - edgeInsets.left - edgeInsets.right) / 2.0) + edgeInsets.left, 0.0) + let height = (bounds.size.height - edgeInsets.top - edgeInsets.bottom) + let yCenter = max((height / 2.0) + edgeInsets.top, 0.0) + let yOffsetCenter = max((height / 2.0) - 7.0, 0.0) * CGFloat(pitchСoefficient) + yCenter + + return CGPoint(x: xCenter, y: yOffsetCenter) + } + + func pitchСoefficient(_ routeProgress: RouteProgress, currentCoordinate: CLLocationCoordinate2D) -> Double { + var shouldIgnoreManeuver = false + if let upcomingStep = routeProgress.currentLeg.steps[safe: routeProgress.currentLegProgress.stepIndex + 1] { + if routeProgress.currentLegProgress.stepIndex == routeProgress.currentLegProgress.leg.steps.count - 2 { + shouldIgnoreManeuver = true + } + + let maneuvers: [ManeuverType] = [.continue, .merge, .takeOnRamp, .takeOffRamp, .reachFork] + if maneuvers.contains(upcomingStep.maneuverType) { + shouldIgnoreManeuver = true + } + } + + let coordinatesToManeuver = routeProgress.currentLegProgress.currentStep.shape?.coordinates.sliced(from: currentCoordinate) ?? [] + let defaultPitchСoefficient = 1.0 + guard let distance = LineString(coordinatesToManeuver).distance() else { return defaultPitchСoefficient } + let pitchEffectDistanceStart: CLLocationDistance = 180.0 + let pitchEffectDistanceEnd: CLLocationDistance = 150.0 + let pitchСoefficient = shouldIgnoreManeuver + ? defaultPitchСoefficient + : (max(min(distance, pitchEffectDistanceStart), pitchEffectDistanceEnd) - pitchEffectDistanceEnd) / (pitchEffectDistanceStart - pitchEffectDistanceEnd) + + return pitchСoefficient + } +} + +// MARK: - LocationConsumer delegate + +extension NavigationViewportDataSource: LocationConsumer { + + public var shouldTrackLocation: Bool { + get { + return true + } + set(newValue) { + // No-op + } + } + + public func locationUpdate(newLocation: Location) { + let cameraOptions = self.cameraOptions(newLocation.internalLocation) + delegate?.viewportDataSource(self, didUpdate: cameraOptions) + } +} diff --git a/Sources/MapboxNavigation/Route.swift b/Sources/MapboxNavigation/Route.swift index 5b286a8ddb2..23eed29657f 100644 --- a/Sources/MapboxNavigation/Route.swift +++ b/Sources/MapboxNavigation/Route.swift @@ -42,4 +42,8 @@ extension Route { return LineString(trimmedPrecedingCoordinates + followingPolyline.trimmed(from: followingPolyline.coordinates[0], distance: distance)!.coordinates.suffix(from: 1)) } } + + func leg(containing step: RouteStep) -> RouteLeg? { + return legs.first { $0.steps.contains(step) } + } } diff --git a/Sources/MapboxNavigation/RouteMapViewController.swift b/Sources/MapboxNavigation/RouteMapViewController.swift index f0d54ddc776..9c4b591c4d7 100644 --- a/Sources/MapboxNavigation/RouteMapViewController.swift +++ b/Sources/MapboxNavigation/RouteMapViewController.swift @@ -53,7 +53,7 @@ class RouteMapViewController: UIViewController { }() private struct Actions { - static let overview: Selector = #selector(RouteMapViewController.toggleOverview(_:)) + static let overview: Selector = #selector(RouteMapViewController.overview(_:)) static let mute: Selector = #selector(RouteMapViewController.toggleMute(_:)) static let feedback: Selector = #selector(RouteMapViewController.feedback(_:)) static let recenter: Selector = #selector(RouteMapViewController.recenter(_:)) @@ -73,32 +73,6 @@ class RouteMapViewController: UIViewController { } var detailedFeedbackEnabled: Bool = false - - var pendingCamera: CameraOptions? { - guard let parent = parent as? NavigationViewController else { - return nil - } - return parent.pendingCamera - } - - var tiltedCamera: CameraOptions { - get { - let currentCamera = navigationMapView.mapView.camera - let pitch: CGFloat = 45.0 - let zoom = CGFloat(ZoomLevelForAltitude(1000, - pitch, - // TODO: Find an alternative way of providing `latitude`. - router.location?.coordinate.latitude ?? 0.0, - navigationMapView.mapView.bounds.size)) - - return CameraOptions(center: currentCamera.center, - padding: currentCamera.padding, - anchor: currentCamera.anchor, - zoom: zoom, - bearing: currentCamera.bearing, - pitch: pitch) - } - } weak var delegate: RouteMapViewControllerDelegate? var navigationService: NavigationService! { @@ -112,18 +86,6 @@ class RouteMapViewController: UIViewController { return navigationService.router } - var isInOverviewMode = false { - didSet { - if isInOverviewMode { - navigationView.overviewButton.isHidden = true - navigationView.resumeButton.isHidden = false - navigationView.wayNameView.isHidden = true - } else { - navigationView.overviewButton.isHidden = false - navigationView.resumeButton.isHidden = true - } - } - } var currentLegIndexMapped = 0 var currentStepIndexMapped = 0 @@ -141,18 +103,26 @@ class RouteMapViewController: UIViewController { navigationMapView.routeLineTracksTraversal = routeLineTracksTraversal } } + + var viewportPadding: UIEdgeInsets { + let courseViewMinimumInsets = UIEdgeInsets(top: 75.0, left: 75.0, bottom: 75.0, right: 75.0) + var insets = navigationMapView.mapView.safeArea + insets += courseViewMinimumInsets + insets.top += topBannerContainerView.bounds.height + insets.bottom += bottomBannerContainerView.bounds.height + + return insets + } typealias LabelRoadNameCompletionHandler = (_ defaultRoadNameAssigned: Bool) -> Void var labelRoadNameCompletionHandler: (LabelRoadNameCompletionHandler)? - - /** - A Boolean value that determines whether the map altitude should change based on internal conditions. - */ - var suppressAutomaticAltitudeChanges: Bool = false convenience init(navigationService: NavigationService, delegate: RouteMapViewControllerDelegate? = nil, topBanner: ContainerViewController, bottomBanner: ContainerViewController) { self.init() + + resumeNotifications() + self.navigationService = navigationService self.delegate = delegate @@ -182,8 +152,6 @@ class RouteMapViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - - self.navigationMapView.tracksUserCourse = true self.navigationMapView.mapView.on(.styleLoaded) { [weak self] _ in self?.showRouteIfNeeded() @@ -199,7 +167,9 @@ class RouteMapViewController: UIViewController { navigationView.muteButton.addTarget(self, action: Actions.mute, for: .touchUpInside) navigationView.reportButton.addTarget(self, action: Actions.feedback, for: .touchUpInside) navigationView.resumeButton.addTarget(self, action: Actions.recenter, for: .touchUpInside) - resumeNotifications() + + self.navigationMapView.userCourseView.isHidden = false + self.navigationView.resumeButton.isHidden = true } deinit { @@ -214,20 +184,7 @@ class RouteMapViewController: UIViewController { $0.ornaments.showsCompass = false } - navigationMapView.tracksUserCourse = true - - if let camera = pendingCamera { - navigationMapView.mapView.cameraManager.setCamera(to: camera, completion: nil) - } else if let location = router.location, location.course > 0 { - navigationMapView.updateCourseTracking(location: location) - } else if let coordinates = router.routeProgress.currentLegProgress.currentStep.shape?.coordinates, let firstCoordinate = coordinates.first, coordinates.count > 1 { - let secondCoordinate = coordinates[1] - let course = firstCoordinate.direction(to: secondCoordinate) - let newLocation = CLLocation(coordinate: router.location?.coordinate ?? firstCoordinate, altitude: 0, horizontalAccuracy: 0, verticalAccuracy: 0, course: course, speed: 0, timestamp: Date()) - navigationMapView.updateCourseTracking(location: newLocation) - } else { - navigationMapView.mapView.cameraManager.setCamera(to: tiltedCamera, completion: nil) - } + navigationMapView.navigationCamera.follow() } override func viewDidAppear(_ animated: Bool) { @@ -240,14 +197,47 @@ class RouteMapViewController: UIViewController { } func resumeNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(orientationDidChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(orientationDidChange(_:)), + name: UIDevice.orientationDidChangeNotification, + object: nil) + + NotificationCenter.default.addObserver(self, + selector: #selector(navigationCameraStateDidChange(_:)), + name: .navigationCameraStateDidChange, + object: navigationMapView.navigationCamera) + subscribeToKeyboardNotifications() } + + @objc func navigationCameraStateDidChange(_ notification: Notification) { + guard let navigationCameraState = notification.userInfo?[NavigationCamera.NotificationUserInfoKey.state] as? NavigationCameraState else { return } + + updateNavigationCameraViewport() + + switch navigationCameraState { + case .transitionToFollowing, .following: + navigationView.overviewButton.isHidden = false + navigationView.resumeButton.isHidden = true + navigationView.wayNameView.isHidden = false + break + case .idle, .transitionToOverview, .overview: + navigationView.overviewButton.isHidden = true + navigationView.resumeButton.isHidden = false + navigationView.wayNameView.isHidden = true + break + } + } func suspendNotifications() { - NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) + NotificationCenter.default.removeObserver(self, + name: UIDevice.orientationDidChangeNotification, + object: nil) + + NotificationCenter.default.removeObserver(self, + name: .navigationCameraStateDidChange, + object: nil) + unsubscribeFromKeyboardNotifications() } @@ -260,26 +250,12 @@ class RouteMapViewController: UIViewController { } child.didMove(toParent: self) } - - @objc func recenter(_ sender: AnyObject) { - navigationMapView.tracksUserCourse = true - navigationMapView.enableFrameByFrameCourseViewTracking(for: 3) - isInOverviewMode = false - - navigationMapView.updateCourseTracking(location: navigationMapView.userLocationForCourseTracking, animated: true) - updateCameraAltitude(for: router.routeProgress) - - navigationMapView.addArrow(route: router.route, - legIndex: router.routeProgress.legIndex, - stepIndex: router.routeProgress.currentLegProgress.stepIndex + 1) - - delegate?.mapViewController(self, didCenterOn: navigationMapView.userLocationForCourseTracking!) + + @objc func overview(_ sender: Any) { + navigationMapView.navigationCamera.moveToOverview() } - func center(on step: RouteStep, route: Route, legIndex: Int, stepIndex: Int, animated: Bool = true, completion: CompletionHandler? = nil) { - navigationMapView.enableFrameByFrameCourseViewTracking(for: 1) - navigationMapView.tracksUserCourse = false - + func center(on step: RouteStep, route: Route, legIndex: Int, stepIndex: Int, animated: Bool = true, completion: CompletionHandler? = nil) { // TODO: Verify that camera is positioned correctly. let camera = CameraOptions(center: step.maneuverLocation, zoom: navigationMapView.mapView.zoom, @@ -295,14 +271,16 @@ class RouteMapViewController: UIViewController { navigationMapView.addArrow(route: router.routeProgress.route, legIndex: legIndex, stepIndex: stepIndex) } - @objc func toggleOverview(_ sender: Any) { - navigationMapView.enableFrameByFrameCourseViewTracking(for: 3) - if let shape = router.route.shape, - let userLocation = router.location { - navigationMapView.setOverheadCameraView(from: userLocation, along: shape, for: contentInset(forOverviewing: true)) - } - isInOverviewMode = true - updateMapViewComponents() + @objc func recenter(_ sender: AnyObject) { + guard let location = navigationMapView.mostRecentUserCourseViewLocation else { return } + + navigationMapView.updateUserCourseView(location) + delegate?.mapViewController(self, didCenterOn: location) + + navigationMapView.navigationCamera.follow() + navigationMapView.addArrow(route: router.route, + legIndex: router.routeProgress.legIndex, + stepIndex: router.routeProgress.currentLegProgress.stepIndex + 1) } @objc func toggleMute(_ sender: UIButton) { @@ -313,46 +291,21 @@ class RouteMapViewController: UIViewController { } @objc func feedback(_ sender: Any) { - showFeedback() - } - - func showFeedback(source: FeedbackSource = .user) { guard let parent = parent else { return } let feedbackViewController = FeedbackViewController(eventsManager: navigationService.eventsManager) feedbackViewController.detailedFeedbackEnabled = detailedFeedbackEnabled - parent.present(feedbackViewController, animated: true, completion: nil) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - navigationMapView.enableFrameByFrameCourseViewTracking(for: 3) + parent.present(feedbackViewController, animated: true) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - if navigationMapView.mapView.locationManager.locationOptions.puckType != .none && - !navigationMapView.tracksUserCourse { - // Don't move mapView content on rotation or when e.g. top banner expands. - return - } - - updateMapViewContentInsets() - } - - func updateMapViewContentInsets() { - navigationMapView.mapView.padding = contentInset(forOverviewing: isInOverviewMode) - navigationMapView.mapView.setNeedsUpdateConstraints() - - updateMapViewComponents() - } - - @objc func applicationWillEnterForeground(notification: NSNotification) { - navigationMapView.updateCourseTracking(location: router.location, animated: false) + updateMapViewOrnaments() } @objc func orientationDidChange(_ notification: Notification) { - updateMapViewContentInsets() + updateMapViewOrnaments() + updateNavigationCameraViewport() } func updateMapOverlays(for routeProgress: RouteProgress) { @@ -362,35 +315,6 @@ class RouteMapViewController: UIViewController { navigationMapView.removeArrow() } } - - func updateCameraAltitude(for routeProgress: RouteProgress, completion: CompletionHandler? = nil) { - // Adjust altitude only when we user course is being tracked. - guard navigationMapView.tracksUserCourse else { return } - - let zoomOutAltitude = navigationMapView.zoomedOutMotorwayAltitude - let defaultAltitude = navigationMapView.defaultAltitude - let isLongRoad = routeProgress.distanceRemaining >= navigationMapView.longManeuverDistance - let currentStep = routeProgress.currentLegProgress.currentStep - let upComingStep = routeProgress.currentLegProgress.upcomingStep - - // If the user is at the last turn maneuver, the map should zoom in to the default altitude. - let currentInstruction = routeProgress.currentLegProgress.currentStepProgress.currentSpokenInstruction - - // If the user is on a motorway, not exiting, and their segment is sufficently long, the map should zoom out to the motorway altitude. - // otherwise, zoom in if it's the last instruction on the step. - let currentStepIsMotorway = currentStep.isMotorway - let nextStepIsMotorway = upComingStep?.isMotorway ?? false - if currentStepIsMotorway, nextStepIsMotorway, isLongRoad { - setCamera(altitude: zoomOutAltitude) - } else if currentInstruction == currentStep.lastInstruction { - setCamera(altitude: defaultAltitude) - } - } - - private func setCamera(altitude: Double) { - guard navigationMapView.altitude != altitude else { return } - navigationMapView.altitude = altitude - } /** Modifies the gesture recognizers to also update the map’s frame rate. @@ -411,7 +335,7 @@ class RouteMapViewController: UIViewController { Method updates `logoView` and `attributionButton` margins to prevent incorrect alignment reported in https://github.com/mapbox/mapbox-navigation-ios/issues/2561. */ - func updateMapViewComponents() { + func updateMapViewOrnaments() { let bottomBannerHeight = bottomBannerContainerView.bounds.height let bottomBannerVerticalOffset = UIScreen.main.bounds.height - bottomBannerHeight - bottomBannerContainerView.frame.origin.y let defaultOffset: CGFloat = 10.0 @@ -433,37 +357,10 @@ class RouteMapViewController: UIViewController { } } - func contentInset(forOverviewing overviewing: Bool) -> UIEdgeInsets { - let instructionBannerHeight = topBannerContainerView.bounds.height - let bottomBannerHeight = bottomBannerContainerView.bounds.height - - // Inset by the safe area to avoid notches. - var insets = navigationMapView.mapView.safeArea - insets.top += instructionBannerHeight - insets.bottom += bottomBannerHeight - - if overviewing { - insets += NavigationMapView.courseViewMinimumInsets - - let routeLineWidths = RouteLineWidthByZoomLevel.map { $0.value } - insets += UIEdgeInsets(floatLiteral: Double(routeLineWidths.max() ?? 0)) - } else if navigationMapView.tracksUserCourse { - // Puck position calculation - position it just above the bottom of the content area. - var contentFrame = navigationMapView.mapView.bounds.inset(by: insets) - - // Avoid letting the puck go partially off-screen, and add a comfortable padding beyond that. - let courseViewBounds = navigationMapView.userCourseView.bounds - // If it is not possible to position it right above the content area, center it at the remaining space. - contentFrame = contentFrame.insetBy(dx: min(NavigationMapView.courseViewMinimumInsets.left + courseViewBounds.width / 2.0, contentFrame.width / 2.0), - dy: min(NavigationMapView.courseViewMinimumInsets.top + courseViewBounds.height / 2.0, contentFrame.height / 2.0)) - assert(!contentFrame.isInfinite) - - let y = contentFrame.maxY - let height = navigationMapView.mapView.bounds.height - insets.top = height - insets.bottom - 2 * (height - insets.bottom - y) + func updateNavigationCameraViewport() { + if let navigationViewportDataSource = navigationMapView.navigationCamera.viewportDataSource as? NavigationViewportDataSource { + navigationViewportDataSource.viewportPadding = viewportPadding } - - return insets } // MARK: - End of Route methods @@ -488,49 +385,29 @@ class RouteMapViewController: UIViewController { endOfRouteViewController.destination = destination navigationView.endOfRouteView?.isHidden = false - // flush layout queue - view.layoutIfNeeded() - navigationView.endOfRouteHideConstraint?.isActive = false navigationView.endOfRouteShowConstraint?.isActive = true - - navigationMapView.enableFrameByFrameCourseViewTracking(for: duration) - navigationMapView.mapView.setNeedsUpdateConstraints() - - let animate = { - self.view.layoutIfNeeded() - self.navigationView.floatingStackView.alpha = 0.0 - } - - let noAnimation = { - animate(); - completion?(true) - } - - guard duration > 0.0 else { return noAnimation() } - - navigationMapView.tracksUserCourse = false - UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: animate, completion: completion) - - guard let height = navigationView.endOfRouteHeightConstraint?.constant else { return } - let insets = UIEdgeInsets(top: topBannerContainerView.bounds.height, left: 20, bottom: height + 20, right: 20) - if let shape = route.shape, - let userLocation = navigationService.router.location?.coordinate, - !shape.coordinates.isEmpty, - let slicedLineString = shape.sliced(from: userLocation) { - - let newCamera = navigationMapView.mapView.cameraManager.camera(fitting: .lineString(slicedLineString), - edgePadding: insets, - bearing: CGFloat(navigationMapView.mapView.bearing), - pitch: 0) - let zoomLevel = CGFloat(ZoomLevelForAltitude(navigationMapView.altitude, - 0, - userLocation.latitude, - navigationMapView.mapView.bounds.size)) - newCamera.zoom = zoomLevel - - navigationMapView.mapView.cameraManager.setCamera(to: newCamera, completion: nil) + navigationMapView.navigationCamera.stop() + + if let height = navigationView.endOfRouteHeightConstraint?.constant { + self.navigationView.floatingStackView.alpha = 0.0 + let camera = navigationMapView.mapView.camera + // Since `padding` is not an animatable property `zoom` is increased to cover up abrupt camera change. + if let zoom = camera.zoom { + camera.zoom = zoom + 1.0 + } + camera.padding = UIEdgeInsets(top: topBannerContainerView.bounds.height, + left: 20, + bottom: height + 20, + right: 20) + navigationMapView.mapView.cameraManager.setCamera(to: camera, + animated: duration > 0.0 ? true : false, + duration: duration) { (animatingPosition) in + if animatingPosition == .end { + completion?(true) + } + } } } @@ -589,12 +466,8 @@ extension RouteMapViewController: NavigationComponent { navigationView.speedLimitView.signStandard = progress.currentLegProgress.currentStep.speedLimitSignStandard navigationView.speedLimitView.speedLimit = progress.currentLegProgress.currentSpeedLimit - } - - public func navigationService(_ service: NavigationService, didPassSpokenInstructionPoint instruction: SpokenInstruction, routeProgress: RouteProgress) { - if !suppressAutomaticAltitudeChanges { - updateCameraAltitude(for: routeProgress) - } + + updateNavigationCameraViewport() } func navigationService(_ service: NavigationService, didRerouteAlong route: Route, at location: CLLocation?, proactive: Bool) { @@ -612,15 +485,6 @@ extension RouteMapViewController: NavigationComponent { if annotatesSpokenInstructions { navigationMapView.showVoiceInstructionsOnMap(route: route) } - - if isInOverviewMode { - if let shape = route.shape, let userLocation = router.location { - navigationMapView.setOverheadCameraView(from: userLocation, along: shape, for: contentInset(forOverviewing: true)) - } - } else { - navigationMapView.tracksUserCourse = true - navigationView.wayNameView.isHidden = true - } } func navigationService(_ service: NavigationService, didRefresh routeProgress: RouteProgress) { @@ -658,17 +522,6 @@ extension RouteMapViewController: NavigationViewDelegate { return delegate?.label(label, willPresent: instruction, as: presented) } - // MARK: - NavigationMapViewCourseTrackingDelegate methods - - func navigationMapViewDidStartTrackingCourse(_ mapView: NavigationMapView) { - navigationView.resumeButton.isHidden = true - } - - func navigationMapViewDidStopTrackingCourse(_ mapView: NavigationMapView) { - navigationView.resumeButton.isHidden = false - navigationView.wayNameView.isHidden = true - } - // MARK: - NavigationMapViewDelegate methods func navigationMapView(_ navigationMapView: NavigationMapView, waypointCircleLayerWithIdentifier identifier: String, sourceIdentifier: String) -> CircleLayer? { delegate?.navigationMapView(navigationMapView, waypointCircleLayerWithIdentifier: identifier, sourceIdentifier: sourceIdentifier) @@ -690,16 +543,6 @@ extension RouteMapViewController: NavigationViewDelegate { delegate?.navigationMapView(navigationMapView, shapeFor: waypoints, legIndex: legIndex) } - func navigationMapViewUserAnchorPoint(_ navigationMapView: NavigationMapView) -> CGPoint { - // If the end of route component is showing, then put the anchor point slightly above the middle of the map. - if navigationView.endOfRouteView != nil, let show = navigationView.endOfRouteShowConstraint, show.isActive { - return CGPoint(x: navigationMapView.bounds.midX, y: (navigationMapView.bounds.height * 0.4)) - } - - // Otherwise, ask the delegate or return .zero. - return delegate?.navigationMapViewUserAnchorPoint(navigationMapView) ?? .zero - } - // TODO: Improve documentation. /** Updates the current road name label to reflect the road on which the user is currently traveling. diff --git a/Sources/MapboxNavigation/UIEdgeInsets.swift b/Sources/MapboxNavigation/UIEdgeInsets.swift index b4334c55877..858982db181 100644 --- a/Sources/MapboxNavigation/UIEdgeInsets.swift +++ b/Sources/MapboxNavigation/UIEdgeInsets.swift @@ -15,6 +15,13 @@ extension UIEdgeInsets { lhs.bottom += rhs.bottom lhs.right += rhs.right } + + func rectValue(_ rect: CGRect) -> CGRect { + return CGRect(x: rect.origin.x + self.left, + y: rect.origin.y + self.top, + width: rect.size.width - self.left - self.right, + height: rect.size.height - self.top - self.bottom) + } } extension UIEdgeInsets: ExpressibleByFloatLiteral { diff --git a/Sources/MapboxNavigation/UserCourseView.swift b/Sources/MapboxNavigation/UserCourseView.swift index e3492ca0173..cae66862312 100644 --- a/Sources/MapboxNavigation/UserCourseView.swift +++ b/Sources/MapboxNavigation/UserCourseView.swift @@ -8,21 +8,24 @@ public protocol CourseUpdatable where Self: UIView { /** Updates the view to reflect the given location and other camera properties. */ - func update(location: CLLocation, pitch: CGFloat, direction: CLLocationDegrees, animated: Bool, tracksUserCourse: Bool) + func update(location: CLLocation, pitch: CGFloat, direction: CLLocationDegrees, animated: Bool, navigationCameraState: NavigationCameraState) } public extension CourseUpdatable { - func update(location: CLLocation, pitch: CGFloat, direction: CLLocationDegrees, animated: Bool, tracksUserCourse: Bool) { - applyDefaultUserPuckTransformation(location: location, pitch: pitch, direction: direction, animated: animated, tracksUserCourse: tracksUserCourse) - } - func applyDefaultUserPuckTransformation(location: CLLocation, pitch: CGFloat, direction: CLLocationDegrees, animated: Bool, tracksUserCourse: Bool) { + func update(location: CLLocation, pitch: CGFloat, direction: CLLocationDegrees, animated: Bool, navigationCameraState: NavigationCameraState) { let duration: TimeInterval = animated ? 1 : 0 UIView.animate(withDuration: duration, delay: 0, options: [.beginFromCurrentState, .curveLinear], animations: { - let angle = tracksUserCourse ? 0 : CLLocationDegrees(direction - location.course) - self.layer.setAffineTransform(CGAffineTransform.identity.rotated(by: -CGFloat(angle.toRadians()))) - var transform = CATransform3DRotate(CATransform3DIdentity, CGFloat(CLLocationDegrees(pitch).toRadians()), 1.0, 0, 0) - transform = CATransform3DScale(transform, tracksUserCourse ? 1 : 0.5, tracksUserCourse ? 1 : 0.5, 1) + let isCameraFollowing = navigationCameraState == .following + let angle = CGFloat(CLLocationDegrees(direction - location.course).toRadians()) + let scale = CGFloat(isCameraFollowing ? 1.0 : 0.5) + + let isCameraTransitioning = navigationCameraState == .transitionToFollowing || navigationCameraState == .transitionToOverview + let pitch = CGFloat(isCameraTransitioning ? 0.0 : CLLocationDegrees(pitch).toRadians()) + + self.layer.setAffineTransform(CGAffineTransform.identity.rotated(by: -angle)) + var transform = CATransform3DRotate(CATransform3DIdentity, pitch, 1.0, 0, 0) + transform = CATransform3DScale(transform, scale, scale, 1) transform.m34 = -1.0 / 1000 // (-1 / distance to projection plane) self.layer.sublayerTransform = transform }, completion: nil) @@ -49,14 +52,19 @@ public class UserPuckCourseView: UIView, CourseUpdatable { /** Transforms the location of the user puck. */ - public func update(location: CLLocation, pitch: CGFloat, direction: CLLocationDegrees, animated: Bool, tracksUserCourse: Bool) { + public func update(location: CLLocation, pitch: CGFloat, direction: CLLocationDegrees, animated: Bool, navigationCameraState: NavigationCameraState) { let duration: TimeInterval = animated ? 1 : 0 UIView.animate(withDuration: duration, delay: 0, options: [.beginFromCurrentState, .curveLinear], animations: { - let angle = tracksUserCourse ? 0 : CLLocationDegrees(direction - location.course) - self.puckView.layer.setAffineTransform(CGAffineTransform.identity.rotated(by: -CGFloat(angle.toRadians()))) - - var transform = CATransform3DRotate(CATransform3DIdentity, CGFloat(CLLocationDegrees(pitch).toRadians()), 1.0, 0, 0) - transform = CATransform3DScale(transform, tracksUserCourse ? 1 : 0.5, tracksUserCourse ? 1 : 0.5, 1) + let isCameraFollowing = navigationCameraState == .following + let angle = CGFloat(CLLocationDegrees(direction - location.course).toRadians()) + let scale = CGFloat(isCameraFollowing ? 1.0 : 0.5) + + let isCameraTransitioning = navigationCameraState == .transitionToFollowing || navigationCameraState == .transitionToOverview + let pitch = CGFloat(isCameraTransitioning ? 0.0 : CLLocationDegrees(pitch).toRadians()) + + self.puckView.layer.setAffineTransform(CGAffineTransform.identity.rotated(by: -angle)) + var transform = CATransform3DRotate(CATransform3DIdentity, pitch, 1.0, 0, 0) + transform = CATransform3DScale(transform, scale, scale, 1) transform.m34 = -1.0 / 1000 // (-1 / distance to projection plane) self.layer.sublayerTransform = transform }, completion: nil) diff --git a/Sources/MapboxNavigation/UserHaloCourseView.swift b/Sources/MapboxNavigation/UserHaloCourseView.swift index 856bb565c35..6ede6a00888 100644 --- a/Sources/MapboxNavigation/UserHaloCourseView.swift +++ b/Sources/MapboxNavigation/UserHaloCourseView.swift @@ -7,21 +7,6 @@ import CoreLocation public class UserHaloCourseView: UIView, CourseUpdatable { private var lastLocationUpdate: Date? - /** - Transforms the location of the user halo. - */ - public func update(location: CLLocation, pitch: CGFloat, direction: CLLocationDegrees, animated: Bool, tracksUserCourse: Bool) { - let duration: TimeInterval = animated ? 1 : 0 - UIView.animate(withDuration: duration, delay: 0, options: [.beginFromCurrentState, .curveLinear], animations: { - let angle = tracksUserCourse ? 0 : CLLocationDegrees(direction - location.course) - self.haloView.layer.setAffineTransform(CGAffineTransform.identity.rotated(by: -CGFloat(angle.toRadians()))) - var transform = CATransform3DRotate(CATransform3DIdentity, CGFloat(CLLocationDegrees(pitch).toRadians()), 1.0, 0, 0) - transform = CATransform3DScale(transform, tracksUserCourse ? 1 : 0.5, tracksUserCourse ? 1 : 0.5, 1) - transform.m34 = -1.0 / 1000 // (-1 / distance to projection plane) - self.layer.sublayerTransform = transform - }, completion: nil) - } - // Sets the inner fill color of the user halo @objc public dynamic var haloColor: UIColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.5) { didSet { diff --git a/Sources/MapboxNavigation/ViewportDataSource.swift b/Sources/MapboxNavigation/ViewportDataSource.swift new file mode 100644 index 00000000000..dd9094c6378 --- /dev/null +++ b/Sources/MapboxNavigation/ViewportDataSource.swift @@ -0,0 +1,56 @@ +import MapboxMaps + +/** + Protocol, which is used to fill and store `CameraOptions` which will be used by + `NavigationCamera` for execution of transitions and continuous updates. + + By default Navigation SDK for iOS provides default implementation of `ViewportDataSource` + in `NavigationViewportDataSource`. + */ +public protocol ViewportDataSource: class { + + /** + Delegate, which is used to notify `NavigationCamera` regarding upcoming `CameraOptions` + related changes. + */ + var delegate: ViewportDataSourceDelegate? { get set } + + /** + `CameraOptions`, which are used on iOS when transitioning to `NavigationCameraState.following` or + for continuous updates when already in `NavigationCameraState.following` state. + */ + var followingMobileCamera: CameraOptions { get } + + /** + `CameraOptions`, which are used on CarPlay when transitioning to `NavigationCameraState.following` or + for continuous updates when already in `NavigationCameraState.following` state. + */ + var followingCarPlayCamera: CameraOptions { get } + + /** + `CameraOptions`, which are used on iOS when transitioning to `NavigationCameraState.overview` or + for continuous updates when already in `NavigationCameraState.overview` state. + */ + var overviewMobileCamera: CameraOptions { get } + + /** + `CameraOptions`, which are used on CarPlay when transitioning to `NavigationCameraState.overview` or + for continuous updates when already in `NavigationCameraState.overview` state. + */ + var overviewCarPlayCamera: CameraOptions { get } +} + +/** + Delegate, which is used to notify `NavigationCamera` regarding upcoming `CameraOptions` + related changes. + */ +public protocol ViewportDataSourceDelegate { + + /** + Notifies `NavigationCamera` that the camera options have changed in response to a location update. + + - parameter dataSource: Object, which conforms to `ViewportDataSource` protocol. + - parameter cameraOptions: Dictionary, which contains `CameraOptions` objects for both iOS and CarPlay. + */ + func viewportDataSource(_ dataSource: ViewportDataSource, didUpdate cameraOptions: [String: CameraOptions]) +} diff --git a/Sources/MapboxNavigation/ViewportDataSourceType.swift b/Sources/MapboxNavigation/ViewportDataSourceType.swift new file mode 100644 index 00000000000..f321f162527 --- /dev/null +++ b/Sources/MapboxNavigation/ViewportDataSourceType.swift @@ -0,0 +1,27 @@ +/** + Possible types of location related updates `NavigationViewportDataSource` can track. + `ViewportDataSourceType` can also be used in custom implementations of classes, which conform to + `ViewportDataSource`. + */ +public enum ViewportDataSourceType { + + /** + If `.raw` type is specified `NavigationViewportDataSource` will register `LocationConsumer` + provided by Maps SDK to be able to continuously get location updates from it. + */ + case raw + + /** + If `.passive` type is specified `NavigationViewportDataSource` will track location updates + (snapped to road) during free drive navigation by subscribing to + `Notification.Name.passiveLocationDataSourceDidUpdate` notifications. + */ + case passive + + /** + If `.active` type is specified `NavigationViewportDataSource` will track location updates + (snapped to road) during active guidance navigation by subscribing to + `Notification.Name.routeControllerProgressDidChange` notifications. + */ + case active +} diff --git a/docs/jazzy.yml b/docs/jazzy.yml index 0c2f531fc57..0077ae5099d 100644 --- a/docs/jazzy.yml +++ b/docs/jazzy.yml @@ -61,19 +61,10 @@ custom_categories: children: - NavigationMapView - NavigationMapViewDelegate - - NavigationMapViewCourseTrackingDelegate - - MGLMapView - - MGLAccountManager - - MGLMultiPolygonFeature - - MGLMultiPolyline - - MGLMultiPolylineFeature - - MGLPointAnnotation - - MGLPointFeature - - MGLPolygon - - MGLPolygonFeature - - MGLPolyline - - MGLPolylineFeature - - MGLStyle + - MapView + - AccountManager + - PointAnnotation + - Style - PassiveLocationManager - WaypointStyle - name: Styling @@ -128,9 +119,9 @@ custom_categories: - RouteControllerMaximumDistanceBeforeRecalculating - RouteControllerUserLocationSnappingDistance - MapOrnamentPosition - - MBCongestionAttribute - - MBCurrentLegAttribute - - MBRouteLineWidthByZoomLevel + - CongestionAttribute + - CurrentLegAttribute + - RouteLineWidthByZoomLevel - NavigationMapViewMinimumDistanceForOverheadZooming - NavigationViewMinimumVolumeForWarning - RouteControllerIncorrectCourseMultiplier @@ -159,3 +150,14 @@ custom_categories: - RoadClosureSubtype - FeedbackSource - EndOfRouteFeedback + - name: Camera + children: + - NavigationCamera + - NavigationCameraType + - NavigationCameraState + - CameraStateTransition + - NavigationCameraStateTransition + - ViewportDataSource + - NavigationViewportDataSource + - ViewportDataSourceType + - ViewportDataSourceDelegate