diff --git a/CHANGELOG.md b/CHANGELOG.md index 2389362ce7..e66969ad28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ * `NavigationMapViewDelegate.navigationMapView(_:alternativeRouteStyleLayerWithIdentifier:source:)` to style alternative routes. * `NavigationMapViewDelegate.navigationMapView(_:alternativeRouteCasingStyleLayerWithIdentifier:source:)` to style the casing of alternative routes. * Fixed an issue where the casing for the main route would not overlap alternative routes. ([#2377](https://github.com/mapbox/mapbox-navigation-ios/pull/2377)) + * Added `NavigationViewController.WaypointStyle` enum to control building highlighting. ([#2535](https://github.com/mapbox/mapbox-navigation-ios/pull/2535)) + * Added `NavigationMapView.highlightBuildings(at:in3D:)` method to highlight the destination building in `UIColor.defaultBuildingHighlightColor` as the user approaches it. Added `NavigationMapView.unhighlightBuildings` to give the ability to unhighlight buildings. ([#2535](https://github.com/mapbox/mapbox-navigation-ios/pull/2535)) ### Feedback diff --git a/Example/ViewController.swift b/Example/ViewController.swift index fd01ed7e79..5336516cf2 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -37,15 +37,14 @@ class ViewController: UIViewController { var response: RouteResponse? { didSet { - guard let routes = response?.routes, let current = routes.first, let steps = current.legs.first?.steps, !steps.isEmpty else { - startButton.isEnabled = false - mapView?.removeRoutes() + guard let routes = response?.routes, let currentRoute = routes.first else { + clearMapView() return - } + startButton.isEnabled = true mapView?.show(routes) - mapView?.showWaypoints(on: current) + mapView?.showWaypoints(on: currentRoute) } } @@ -135,10 +134,16 @@ class ViewController: UIViewController { waypoints = Array(waypoints.dropFirst()) } - let coordinates = mapView.convert(tap.location(in: mapView), toCoordinateFrom: mapView) + let destinationCoord = mapView.convert(tap.location(in: mapView), toCoordinateFrom: mapView) // Note: The destination name can be modified. The value is used in the top banner when arriving at a destination. - let waypoint = Waypoint(coordinate: coordinates, name: "Dropped Pin #\(waypoints.endIndex + 1)") + let waypoint = Waypoint(coordinate: destinationCoord, name: "Dropped Pin #\(waypoints.endIndex + 1)") + // Example of building highlighting. `targetCoordinate`, in this example, is used implicitly by NavigationViewController to determine which buildings to highlight. + waypoint.targetCoordinate = destinationCoord waypoints.append(waypoint) + + // Example of highlighting buildings in 2d and directly using the API on NavigationMapView. + let buildingHighlightCoordinates = waypoints.compactMap { $0.targetCoordinate } + mapView.highlightBuildings(at: buildingHighlightCoordinates, in3D: false) requestRoute() } @@ -150,17 +155,24 @@ class ViewController: UIViewController { } @IBAction func clearMapPressed(_ sender: Any) { - clearMap.isHidden = true - mapView?.removeRoutes() - mapView?.removeWaypoints() - waypoints.removeAll() - longPressHintView.isHidden = false + clearMapView() } @IBAction func startButtonPressed(_ sender: Any) { presentActionsAlertController() } + private func clearMapView() { + startButton.isEnabled = false + clearMap.isHidden = true + longPressHintView.isHidden = false + + mapView?.unhighlightBuildings() + mapView?.removeRoutes() + mapView?.removeWaypoints() + waypoints.removeAll() + } + private func presentActionsAlertController() { let alertController = UIAlertController(title: "Start Navigation", message: "Select the navigation type", preferredStyle: .actionSheet) @@ -237,6 +249,9 @@ class ViewController: UIViewController { // Render part of the route that has been traversed with full transparency, to give the illusion of a disappearing route. navigationViewController.mapView?.routeLineTracksTraversal = true + // Example of building highlighting in 3D. + navigationViewController.waypointStyle = .extrudedBuilding + presentAndRemoveMapview(navigationViewController, completion: beginCarPlayNavigation) } @@ -247,6 +262,9 @@ class ViewController: UIViewController { let navigationViewController = NavigationViewController(for: route, routeOptions: routeOptions, navigationOptions: options) navigationViewController.delegate = self + // Example of building highlighting in 2D. + navigationViewController.waypointStyle = .building + presentAndRemoveMapview(navigationViewController, completion: beginCarPlayNavigation) } @@ -388,7 +406,7 @@ extension ViewController: NavigationMapViewDelegate { guard let responseOptions = response?.options, case let .route(routeOptions) = responseOptions else { return } let modifiedOptions = routeOptions.without(waypoint: waypoint) - presentWaypointRemovalActionSheet { _ in + presentWaypointRemovalAlert { _ in self.requestRoute(with:modifiedOptions, success: self.defaultSuccess, failure: self.defaultFailure) } } @@ -400,18 +418,18 @@ extension ViewController: NavigationMapViewDelegate { self.response!.routes!.insert(route, at: 0) } - private func presentWaypointRemovalActionSheet(completionHandler approve: @escaping ((UIAlertAction) -> Void)) { - let title = NSLocalizedString("REMOVE_WAYPOINT_CONFIRM_TITLE", value: "Remove Waypoint?", comment: "Title of sheet confirming waypoint removal") - let message = NSLocalizedString("REMOVE_WAYPOINT_CONFIRM_MSG", value: "Do you want to remove this waypoint?", comment: "Message of sheet confirming waypoint removal") - let removeTitle = NSLocalizedString("REMOVE_WAYPOINT_CONFIRM_REMOVE", value: "Remove Waypoint", comment: "Title of alert sheet action for removing a waypoint") + private func presentWaypointRemovalAlert(completionHandler approve: @escaping ((UIAlertAction) -> Void)) { + let title = NSLocalizedString("REMOVE_WAYPOINT_CONFIRM_TITLE", value: "Remove Waypoint?", comment: "Title of alert confirming waypoint removal") + let message = NSLocalizedString("REMOVE_WAYPOINT_CONFIRM_MSG", value: "Do you want to remove this waypoint?", comment: "Message of alert confirming waypoint removal") + let removeTitle = NSLocalizedString("REMOVE_WAYPOINT_CONFIRM_REMOVE", value: "Remove Waypoint", comment: "Title of alert action for removing a waypoint") let cancelTitle = NSLocalizedString("REMOVE_WAYPOINT_CONFIRM_CANCEL", value: "Cancel", comment: "Title of action for dismissing waypoint removal confirmation sheet") - - let actionSheet = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) - let remove = UIAlertAction(title: removeTitle, style: .destructive, handler: approve) - let cancel = UIAlertAction(title: cancelTitle, style: .cancel, handler: nil) - [remove, cancel].forEach(actionSheet.addAction(_:)) - - self.present(actionSheet, animated: true, completion: nil) + + let waypointRemovalAlertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let removeAction = UIAlertAction(title: removeTitle, style: .destructive, handler: approve) + let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel, handler: nil) + [removeAction, cancelAction].forEach(waypointRemovalAlertController.addAction(_:)) + + self.present(waypointRemovalAlertController, animated: true, completion: nil) } } diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index 9615200a28..18d38f437e 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 16E3625C201265D600DF0592 /* ImageDownloadOperationSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E3625B201265D600DF0592 /* ImageDownloadOperationSpy.swift */; }; 16EF6C1E21193A9600AA580B /* CarPlayManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16EF6C1D21193A9600AA580B /* CarPlayManagerTests.swift */; }; 16EF6C22211BA4B300AA580B /* CarPlayMapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16EF6C21211BA4B300AA580B /* CarPlayMapViewController.swift */; }; + 1F25A90F24A52C1600737F01 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F25A90E24A52C1600737F01 /* URLSession.swift */; }; + 1FFDFD92249C1AA80091746A /* JunctionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFDFD91249C1AA70091746A /* JunctionView.swift */; }; 2B07444124B4832400615E87 /* TokenTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B07444024B4832400615E87 /* TokenTestViewController.swift */; }; 2B2B1EEE2424E55800FA18A6 /* SKUIDTestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2B1EED2424E55800FA18A6 /* SKUIDTestable.swift */; }; 2B5407EB24470B0A006C820B /* AVAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5407EA24470B0A006C820B /* AVAudioSession.swift */; }; @@ -34,8 +36,6 @@ 2B8098432411447E00FED452 /* MultiplexedSpeechSynthesizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B8098422411447E00FED452 /* MultiplexedSpeechSynthesizer.swift */; }; 2B81EC28241A237E00145086 /* SpeechSynthesizersControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B81EC27241A237E00145086 /* SpeechSynthesizersControllerTests.swift */; }; 2B91C9B12416357700E532A5 /* MapboxSpeechSynthesizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B91C9B02416357700E532A5 /* MapboxSpeechSynthesizer.swift */; }; - 1F25A90F24A52C1600737F01 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F25A90E24A52C1600737F01 /* URLSession.swift */; }; - 1FFDFD92249C1AA80091746A /* JunctionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFDFD91249C1AA70091746A /* JunctionView.swift */; }; 35002D611E5F6ADB0090E733 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 35002D5F1E5F6ADB0090E733 /* Assets.xcassets */; }; 35002D691E5F6B2F0090E733 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 35002D661E5F6B1B0090E733 /* Main.storyboard */; }; 3502231A205BC94E00E1449A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35022319205BC94E00E1449A /* Constants.swift */; }; @@ -251,6 +251,7 @@ 6441B16A1EFC64E50076499F /* WaypointConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6441B1691EFC64E50076499F /* WaypointConfirmationViewController.swift */; }; 64847A041F04629D003F3A69 /* Feedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64847A031F04629D003F3A69 /* Feedback.swift */; }; 7C12F2D8225B7C320010A931 /* DCA-Arboretum-dummy-faster-route.json in Resources */ = {isa = PBXBuildFile; fileRef = 7C12F2D7225B7C310010A931 /* DCA-Arboretum-dummy-faster-route.json */; }; + 8AA849E924E722410008EE59 /* WaypointStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AA849E824E722410008EE59 /* WaypointStyle.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 */; }; @@ -642,6 +643,8 @@ 16E3625B201265D600DF0592 /* ImageDownloadOperationSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloadOperationSpy.swift; sourceTree = ""; }; 16EF6C1D21193A9600AA580B /* CarPlayManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayManagerTests.swift; sourceTree = ""; }; 16EF6C21211BA4B300AA580B /* CarPlayMapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayMapViewController.swift; sourceTree = ""; }; + 1F25A90E24A52C1600737F01 /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; + 1FFDFD91249C1AA70091746A /* JunctionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JunctionView.swift; sourceTree = ""; }; 2B07444024B4832400615E87 /* TokenTestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenTestViewController.swift; sourceTree = ""; }; 2B2B1EED2424E55800FA18A6 /* SKUIDTestable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SKUIDTestable.swift; sourceTree = ""; }; 2B5407EA24470B0A006C820B /* AVAudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVAudioSession.swift; sourceTree = ""; }; @@ -651,8 +654,6 @@ 2B8098422411447E00FED452 /* MultiplexedSpeechSynthesizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiplexedSpeechSynthesizer.swift; sourceTree = ""; }; 2B81EC27241A237E00145086 /* SpeechSynthesizersControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechSynthesizersControllerTests.swift; sourceTree = ""; }; 2B91C9B02416357700E532A5 /* MapboxSpeechSynthesizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxSpeechSynthesizer.swift; sourceTree = ""; }; - 1F25A90E24A52C1600737F01 /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; - 1FFDFD91249C1AA70091746A /* JunctionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JunctionView.swift; sourceTree = ""; }; 35002D5D1E5F6ABB0090E733 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35002D5F1E5F6ADB0090E733 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 35002D671E5F6B1B0090E733 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -850,6 +851,7 @@ 6441B1691EFC64E50076499F /* WaypointConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WaypointConfirmationViewController.swift; path = Example/WaypointConfirmationViewController.swift; sourceTree = ""; }; 64847A031F04629D003F3A69 /* Feedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feedback.swift; sourceTree = ""; }; 7C12F2D7225B7C310010A931 /* DCA-Arboretum-dummy-faster-route.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "DCA-Arboretum-dummy-faster-route.json"; sourceTree = ""; }; + 8AA849E824E722410008EE59 /* WaypointStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointStyle.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 = ""; }; @@ -1336,6 +1338,7 @@ C5A6B2DC1F4CE8E8004260EA /* StyleType.swift */, 2B72EC5F2412AA800003B370 /* SystemSpeechSynthesizer.swift */, 35B1E2941F1FF8EC00A13D32 /* UserCourseView.swift */, + 8AA849E824E722410008EE59 /* WaypointStyle.swift */, ); path = MapboxNavigation; sourceTree = ""; @@ -2424,6 +2427,7 @@ 35DC9D911F4323AA001ECD64 /* LanesView.swift in Sources */, AE7DE6C421A47A03002653D1 /* CarPlaySearchController.swift in Sources */, DA8F3A7623B5D84900B56786 /* SpeedLimitView.swift in Sources */, + 8AA849E924E722410008EE59 /* WaypointStyle.swift in Sources */, 359D1B281FFE70D30052FA42 /* NavigationView.swift in Sources */, C5FFAC1520D96F5C009E7F98 /* CarPlayNavigationViewController.swift in Sources */, 8DE879661FBB9980002F06C0 /* EndOfRouteViewController.swift in Sources */, diff --git a/MapboxNavigation/DayStyle.swift b/MapboxNavigation/DayStyle.swift index 4398105d72..f4e8cbde24 100644 --- a/MapboxNavigation/DayStyle.swift +++ b/MapboxNavigation/DayStyle.swift @@ -26,6 +26,9 @@ extension UIColor { class var trafficHeavy: UIColor { get { return #colorLiteral(red: 1, green: 0.3019607843, blue: 0.3019607843, alpha: 1) } } class var trafficSevere: UIColor { get { return #colorLiteral(red: 0.5607843137, green: 0.1411764706, blue: 0.2784313725, alpha: 1) } } class var trafficAlternateLow: UIColor { get { return #colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1) } } + + class var defaultBuildingColor: UIColor { get { return #colorLiteral(red: 0.9833194452, green: 0.9843137255, blue: 0.9331936657, alpha: 0.8019049658) } } + class var defaultBuildingHighlightColor: UIColor { get { return #colorLiteral(red: 0.337254902, green: 0.6588235294, blue: 0.9843137255, alpha: 0.949406036) } } } extension UIColor { @@ -135,16 +138,18 @@ open class DayStyle: Style { ManeuverView.appearance(whenContainedInInstancesOf: [StepInstructionsView.self]).secondaryColor = .defaultTurnArrowSecondary ManeuverView.appearance().primaryColorHighlighted = .defaultTurnArrowPrimaryHighlighted ManeuverView.appearance().secondaryColorHighlighted = .defaultTurnArrowSecondaryHighlighted - NavigationMapView.appearance().maneuverArrowColor = .defaultManeuverArrow + NavigationMapView.appearance().maneuverArrowColor = .defaultManeuverArrow NavigationMapView.appearance().maneuverArrowStrokeColor = .defaultManeuverArrowStroke - NavigationMapView.appearance().routeAlternateColor = .defaultAlternateLine - NavigationMapView.appearance().routeCasingColor = .defaultRouteCasing - NavigationMapView.appearance().traversedRouteColor = .defaultTraversedRouteColor - NavigationMapView.appearance().trafficHeavyColor = .trafficHeavy - NavigationMapView.appearance().trafficLowColor = .trafficLow - NavigationMapView.appearance().trafficModerateColor = .trafficModerate - NavigationMapView.appearance().trafficSevereColor = .trafficSevere - NavigationMapView.appearance().trafficUnknownColor = .trafficUnknown + NavigationMapView.appearance().routeAlternateColor = .defaultAlternateLine + NavigationMapView.appearance().routeCasingColor = .defaultRouteCasing + NavigationMapView.appearance().traversedRouteColor = .defaultTraversedRouteColor + NavigationMapView.appearance().trafficHeavyColor = .trafficHeavy + NavigationMapView.appearance().trafficLowColor = .trafficLow + NavigationMapView.appearance().trafficModerateColor = .trafficModerate + NavigationMapView.appearance().trafficSevereColor = .trafficSevere + NavigationMapView.appearance().trafficUnknownColor = .trafficUnknown + NavigationMapView.appearance().buildingDefaultColor = .defaultBuildingColor + NavigationMapView.appearance().buildingHighlightColor = .defaultBuildingHighlightColor NavigationView.appearance().backgroundColor = #colorLiteral(red: 0.764706, green: 0.752941, blue: 0.733333, alpha: 1) NextBannerView.appearance().backgroundColor = #colorLiteral(red: 0.9675388083, green: 0.9675388083, blue: 0.9675388083, alpha: 1) NextBannerView.appearance(whenContainedInInstancesOf:[InstructionsCardContainerView.self]).backgroundColor = #colorLiteral(red: 0.9675388083, green: 0.9675388083, blue: 0.9675388083, alpha: 1) @@ -256,6 +261,8 @@ open class NightStyle: DayStyle { ManeuverView.appearance(whenContainedInInstancesOf: [StepInstructionsView.self]).primaryColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) ManeuverView.appearance(whenContainedInInstancesOf: [StepInstructionsView.self]).secondaryColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.3) NavigationMapView.appearance().routeAlternateColor = #colorLiteral(red: 0.7991961837, green: 0.8232284188, blue: 0.8481693864, alpha: 1) + NavigationMapView.appearance().buildingDefaultColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) + NavigationMapView.appearance().buildingHighlightColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1) NavigationView.appearance().backgroundColor = #colorLiteral(red: 0.0470588, green: 0.0509804, blue: 0.054902, alpha: 1) NextBannerView.appearance().backgroundColor = #colorLiteral(red: 0.103291966, green: 0.1482483149, blue: 0.2006777823, alpha: 1) NextInstructionLabel.appearance().normalTextColor = #colorLiteral(red: 0.9842069745, green: 0.9843751788, blue: 0.9841964841, alpha: 1) diff --git a/MapboxNavigation/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index 65c8915e5a..7116afcb4e 100644 --- a/MapboxNavigation/NavigationMapView.swift +++ b/MapboxNavigation/NavigationMapView.swift @@ -111,6 +111,8 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { static let instructionLabel = "\(identifierNamespace).instructionLabel" static let instructionCircle = "\(identifierNamespace).instructionCircle" + + static let buildingExtrusion = "\(identifierNamespace).buildingExtrusion" } // MARK: - Instance Properties @@ -125,6 +127,8 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { @objc dynamic public var traversedRouteColor: UIColor = .defaultTraversedRouteColor @objc dynamic public var maneuverArrowColor: UIColor = .defaultManeuverArrow @objc dynamic public var maneuverArrowStrokeColor: UIColor = .defaultManeuverArrowStroke + @objc dynamic public var buildingDefaultColor: UIColor = .defaultBuildingColor + @objc dynamic public var buildingHighlightColor: UIColor = .defaultBuildingHighlightColor var userLocationForCourseTracking: CLLocation? var animatesUserLocation: Bool = false @@ -389,7 +393,7 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { userCourseView.update(location: location, pitch: self.camera.pitch, direction: direction, animated: animated, tracksUserCourse: tracksUserCourse) } - //MARK: - Gesture Recognizers + //MARK: - Gesture Recognizers /** Fired when NavigationMapView detects a tap not handled elsewhere by other gesture recognizers. @@ -602,12 +606,19 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { func fadeRoute(_ fractionTraveled: Double) { guard let mainRouteLayer = style?.layer(withIdentifier: StyleLayerIdentifier.mainRoute) as? MGLLineStyleLayer, - let mainRouteLayerLineGradient = lineGradient(routeGradientStops.line, fractionTraveled: fractionTraveled), - let mainRouteCasingLayer = style?.layer(withIdentifier: StyleLayerIdentifier.mainRouteCasing) as? MGLLineStyleLayer, - let mainRouteCasingLayerLineGradient = lineGradient(routeGradientStops.casing, fractionTraveled: fractionTraveled) else { return } + let mainRouteCasingLayer = style?.layer(withIdentifier: StyleLayerIdentifier.mainRouteCasing) as? MGLLineStyleLayer else { return } - mainRouteLayer.lineGradient = mainRouteLayerLineGradient - mainRouteCasingLayer.lineGradient = mainRouteCasingLayerLineGradient + // In case if route was fully travelled - remove main route and its casing. + if fractionTraveled == 1.0 { + style?.remove([mainRouteLayer, mainRouteCasingLayer]) + return + } + + if let mainRouteLayerLineGradient = lineGradient(routeGradientStops.line, fractionTraveled: fractionTraveled), + let mainRouteCasingLayerLineGradient = lineGradient(routeGradientStops.casing, fractionTraveled: fractionTraveled) { + mainRouteLayer.lineGradient = mainRouteLayerLineGradient + mainRouteCasingLayer.lineGradient = mainRouteCasingLayerLineGradient + } } /** @@ -1335,3 +1346,91 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { enableFrameByFrameCourseViewTracking(for: 3) } } + +// MARK: - Building Extrusion Highlighting + +extension NavigationMapView { + + /** + Receives coordinates for searching the map for buildings. If buildings are found, they will be highlighted in 2D or 3D depending on the `in3D` value. + + - parameter coordinates: Coordinates which represent buildings locations. + - parameter extrudesBuildings: Switch which allows to highlight buildings in either 2D or 3D. Defaults to true. + */ + public func highlightBuildings(at coordinates: [CLLocationCoordinate2D], in3D extrudesBuildings: Bool = true) { + highlightBuildings(with: Set(coordinates.compactMap({ buildingIdentifier(at: $0) })), in3D: extrudesBuildings) + } + + /** + Removes the highlight from all buildings highlighted by `highlightBuildings(at:in3D:)`. + */ + public func unhighlightBuildings() { + guard let highlightedBuildingsLayer = style?.layer(withIdentifier: StyleLayerIdentifier.buildingExtrusion) else { return } + + style?.removeLayer(highlightedBuildingsLayer) + } + + private func addBuildingsLayer() -> MGLFillExtrusionStyleLayer? { + if let highlightedBuildingsLayer = style?.layer(withIdentifier: StyleLayerIdentifier.buildingExtrusion) as? MGLFillExtrusionStyleLayer { return highlightedBuildingsLayer } + guard let buildingsSource = style?.source(withIdentifier: "composite") else { return nil } + + let highlightedBuildingsLayer = MGLFillExtrusionStyleLayer(identifier: StyleLayerIdentifier.buildingExtrusion, source: buildingsSource) + highlightedBuildingsLayer.sourceLayerIdentifier = "building" + highlightedBuildingsLayer.fillExtrusionColor = NSExpression(forConstantValue: buildingDefaultColor) + highlightedBuildingsLayer.fillExtrusionOpacity = NSExpression(forConstantValue: 0.05) + highlightedBuildingsLayer.fillExtrusionHeightTransition = MGLTransition(duration: 0.8, delay: 0) + highlightedBuildingsLayer.fillExtrusionOpacityTransition = MGLTransition(duration: 0.8, delay: 0) + + style?.addLayer(highlightedBuildingsLayer) + + return highlightedBuildingsLayer + } + + private func buildingIdentifier(at coordinate: CLLocationCoordinate2D) -> Int64? { + let screenCoordinate = convert(coordinate, toPointTo: self) + guard let style = style else { return nil } + + let identifiers = Set(style.layers.compactMap({ $0 as? MGLVectorStyleLayer }).filter({ $0.sourceLayerIdentifier == "building" }).compactMap({ $0.identifier })) + let features = visibleFeatures(at: screenCoordinate, styleLayerIdentifiers: identifiers) + + if let feature = features.first, let identifier = feature.identifier as? Int64 { + return identifier + } + + return nil + } + + private func highlightBuildings(with identifiers: Set, in3D: Bool = false, extrudeAll: Bool = false) { + // In case if set with highlighted building identifiers is empty - do nothing. + if identifiers.isEmpty { return } + // Add layer which will be used to highlight buildings if it wasn't added yet. + guard let highlightedBuildingsLayer = addBuildingsLayer() else { return } + + if extrudeAll { + highlightedBuildingsLayer.predicate = NSPredicate(format: "extrude = 'true' AND underground = 'false'") + } else { + // Form a predicate to filter out the other buildings from the datasource so only the desired ones are included. + highlightedBuildingsLayer.predicate = NSPredicate(format: "extrude = 'true' AND underground = 'false' AND $featureIdentifier IN %@", identifiers.map { $0 }) + } + + // Buildings with identifiers will be highlighted with provided color. Rest of the buildings will be highlighted, but kept at a uniform color. + let highlightedBuildingsHeightExpression = NSExpression(format: "TERNARY(%@ = TRUE AND (%@ = TRUE OR $featureIdentifier IN %@), height, 0)", in3D as NSValue, extrudeAll as NSValue, identifiers.map { $0 }) + let colorsByBuilding = Dictionary(identifiers.map { (NSExpression(forConstantValue: $0), NSExpression(forConstantValue: buildingHighlightColor)) }) { (_, last) in last } + let highlightedBuildingsColorExpression = NSExpression(forMGLMatchingKey: NSExpression(forVariable: "featureIdentifier"), in: colorsByBuilding, default: NSExpression(forConstantValue: buildingDefaultColor)) + + let fillExtrusionHeightStops = [0: NSExpression(forConstantValue: 0), + 13: NSExpression(forConstantValue: 0), + 13.25: highlightedBuildingsHeightExpression] + + let fillExtrusionBaseStops = [0: NSExpression(forConstantValue: 0), + 13: NSExpression(forConstantValue: 0), + 13.25: NSExpression(forKeyPath: "min_height")] + + let opacityStops = [13: 0.5, 17: 0.8] + + highlightedBuildingsLayer.fillExtrusionHeight = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", fillExtrusionHeightStops) + highlightedBuildingsLayer.fillExtrusionBase = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", fillExtrusionBaseStops) + highlightedBuildingsLayer.fillExtrusionColor = highlightedBuildingsColorExpression + highlightedBuildingsLayer.fillExtrusionOpacity = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", opacityStops) + } +} diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index abe9e9201e..1a114b259b 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -147,6 +147,11 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter */ public var shouldManageApplicationIdleTimer = true + /** + Allows to control highlighting of the destination building on arrival. By default destination buildings will not be highlighted. + */ + public var waypointStyle: WaypointStyle = .annotation + var isConnectedToCarPlay: Bool { if #available(iOS 12.0, *) { return CarPlayManager.isConnected @@ -572,11 +577,45 @@ extension NavigationViewController: NavigationServiceDelegate { let advancesToNextLeg = componentsWantAdvance && (delegate?.navigationViewController(self, didArriveAt: waypoint) ?? defaultBehavior) if service.routeProgress.isFinalLeg && advancesToNextLeg && showsEndOfRouteFeedback { - showEndOfRouteFeedback() + // In case of final destination present end of route view first and then zoom in to extruded building. + showEndOfRouteFeedback { [weak self] _ in + self?.zoomInAndHighlightBuilding(for: service.router.location) + } } return advancesToNextLeg } + private func zoomInAndHighlightBuilding(for location: CLLocation?) { + if waypointStyle == .annotation { return } + guard let mapViewController = self.mapViewController else { return } + guard let location = location else { return } + + // Since all buildings are included in zoom level 16 and above as per + // https://docs.mapbox.com/vector-tiles/reference/mapbox-streets-v8/#building + // we make sure to zoom in before making update to insets, user course view etc + var currentZoomLevel = mapViewController.mapView.zoomLevel + let expectedZoomLevel = 16.5 + if currentZoomLevel < expectedZoomLevel { + currentZoomLevel = expectedZoomLevel + } + + let mapView = mapViewController.mapView + mapView.setCenter(location.coordinate, + zoomLevel: currentZoomLevel, + direction: location.course, + animated: true, + completionHandler: { + // Highlight buildings which were marked as target destination coordinate in waypoint. + mapView.highlightBuildings(at: self.routeOptions.waypoints.compactMap({ $0.targetCoordinate }), in3D: self.waypointStyle == .extrudedBuilding ? true : false) + + // 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. + mapView.updateCourseTracking(location: location, animated: false) + }) + } + public func showEndOfRouteFeedback(duration: TimeInterval = 1.0, completionHandler: ((Bool) -> Void)? = nil) { guard let mapController = mapViewController else { return } mapController.showEndOfRoute(duration: duration, completion: completionHandler) diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index e5352c2d61..ce42f0ce13 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -145,6 +145,9 @@ class RouteMapViewController: UIViewController { self?.showRouteIfNeeded() mapView.localizeLabels() mapView.showsTraffic = false + + // FIXME: In case when building highlighting feature is enabled due to style changes and no info currently being stored + // regarding building identification such highlighted building will disappear. } makeGestureRecognizersResetFrameRate() diff --git a/MapboxNavigation/WaypointStyle.swift b/MapboxNavigation/WaypointStyle.swift new file mode 100644 index 0000000000..ac98407c34 --- /dev/null +++ b/MapboxNavigation/WaypointStyle.swift @@ -0,0 +1,21 @@ +import Foundation + +/** + Enum denoting the types of the destination waypoint highlighting on arrival. + */ +public enum WaypointStyle: Int { + /** + Do not highlight destination waypoint on arrival. Destination annotation is always shown by default. + */ + case annotation + + /** + Highlight destination building on arrival in 2D. In case if destination building wasn't found only annotation will be shown. + */ + case building + + /** + Highlight destination building on arrival in 3D. In case if destination building wasn't found only annotation will be shown. + */ + case extrudedBuilding +} diff --git a/docs/jazzy.yml b/docs/jazzy.yml index 9ac220bd31..9e26bf64ea 100644 --- a/docs/jazzy.yml +++ b/docs/jazzy.yml @@ -77,6 +77,7 @@ custom_categories: - MGLPolylineFeature - MGLStyle - MGLVectorTileSource + - WaypointStyle - name: Styling children: - Style