From 1763da38eed1c64659bcaa6d5ce03ecb5ee469cf Mon Sep 17 00:00:00 2001 From: Avi Cieplinski Date: Mon, 12 Apr 2021 15:38:21 -0700 Subject: [PATCH] Add visible annotations to the map at intersecting roads along the active route as well as at maneuver points. --- Example/ViewController.swift | 2 + MapboxNavigation.xcodeproj/project.pbxproj | 14 + NavigationMapView+RoadAnnotations.swift | 12 + .../MapboxCoreNavigation/CoreConstants.swift | 2 + .../CoreNavigationNavigator.swift | 5 +- .../NavigationService.swift | 2 +- Sources/MapboxNavigation/DayStyle.swift | 4 + .../MapboxNavigation/ElectronicHorizon.swift | 38 ++ ...vigationMapView+BuildingHighlighting.swift | 9 + ...ationMapView+IntersectionAnnotations.swift | 133 +++++ .../MapboxNavigation/NavigationMapView.swift | 467 +++++++++++++++++- .../AnnotationCentered.png | Bin 0 -> 4850 bytes .../AnnotationCentered.imageset/Contents.json | 12 + .../Contents.json | 12 + .../RouteInfoAnnotationLeftHanded.png | Bin 0 -> 2391 bytes .../Contents.json | 12 + .../RouteInfoAnnotationRightHanded.png | Bin 0 -> 2463 bytes .../Resources/Assets.xcassets/Contents.json | 6 +- .../RouteMapViewController.swift | 1 + Sources/MapboxNavigation/UIImage.swift | 27 + 20 files changed, 751 insertions(+), 7 deletions(-) create mode 100644 NavigationMapView+RoadAnnotations.swift create mode 100644 Sources/MapboxNavigation/ElectronicHorizon.swift create mode 100644 Sources/MapboxNavigation/NavigationMapView+IntersectionAnnotations.swift create mode 100644 Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationCentered.imageset/AnnotationCentered.png create mode 100644 Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationCentered.imageset/Contents.json create mode 100644 Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationLeftHanded.imageset/Contents.json create mode 100644 Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationLeftHanded.imageset/RouteInfoAnnotationLeftHanded.png create mode 100644 Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationRightHanded.imageset/Contents.json create mode 100644 Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationRightHanded.imageset/RouteInfoAnnotationRightHanded.png diff --git a/Example/ViewController.swift b/Example/ViewController.swift index 0f98dbf99af..0591543dcca 100755 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -453,6 +453,7 @@ class ViewController: UIViewController { func present(_ navigationViewController: NavigationViewController, completion: CompletionHandler? = nil) { navigationViewController.modalPresentationStyle = .fullScreen activeNavigationViewController = navigationViewController + activeNavigationViewController?.navigationMapView?.showIntersectionAnnotations = true present(navigationViewController, animated: true) { completion?() @@ -467,6 +468,7 @@ class ViewController: UIViewController { func dismissActiveNavigationViewController() { activeNavigationViewController?.dismiss(animated: true) { + self.activeNavigationViewController?.navigationMapView?.showIntersectionAnnotations = false self.activeNavigationViewController = nil } } diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index 28c53847770..693e468eac9 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -395,6 +395,10 @@ DAD903AF23E3DCC80057CF1F /* DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAD903AE23E3DCC80057CF1F /* DateTests.swift */; }; DADD82802161EC0300B8B47D /* UIViewAnimationOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADD827F2161EC0300B8B47D /* UIViewAnimationOptionsTests.swift */; }; DAFA92071F01735000A7FB09 /* DistanceFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351BEC0B1E5BCC72006FE110 /* DistanceFormatter.swift */; }; + F43EE329261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43EE328261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift */; }; + F43EE32A261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43EE328261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift */; }; + F488A0BE26261C4600A4CC8C /* NavigationMapView+IntersectionAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F488A0BD26261C4600A4CC8C /* NavigationMapView+IntersectionAnnotations.swift */; }; + F488A0C826261D8100A4CC8C /* ElectronicHorizon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F488A0C726261D8100A4CC8C /* ElectronicHorizon.swift */; }; F4BF512E24EAD7A50066A49B /* FeedbackSubtypeCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4BF512D24EAD7A50066A49B /* FeedbackSubtypeCollectionViewCell.swift */; }; F4C5A26F24EF1D16004ED0DD /* FeedbackSubtypeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C5A26E24EF1D16004ED0DD /* FeedbackSubtypeViewController.swift */; }; /* End PBXBuildFile section */ @@ -1021,6 +1025,9 @@ DAFEB36D2093A11F00A86A83 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; DAFEB36E2093A3E000A86A83 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; DAFEB36F2093A3EF00A86A83 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = Resources/ko.lproj/Localizable.stringsdict; sourceTree = ""; }; + F43EE328261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NavigationMapView+RoadAnnotations.swift"; sourceTree = ""; }; + F488A0BD26261C4600A4CC8C /* NavigationMapView+IntersectionAnnotations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NavigationMapView+IntersectionAnnotations.swift"; sourceTree = ""; }; + F488A0C726261D8100A4CC8C /* ElectronicHorizon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElectronicHorizon.swift; sourceTree = ""; }; F4BF512D24EAD7A50066A49B /* FeedbackSubtypeCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackSubtypeCollectionViewCell.swift; sourceTree = ""; }; F4C5A26E24EF1D16004ED0DD /* FeedbackSubtypeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackSubtypeViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1352,6 +1359,7 @@ 35002D721E5F6C830090E733 /* Supporting files */, AED6285522CBE4CE00058A51 /* ViewController+GuidanceCards.swift */, 8A092F9A25DEE81900CA7CF5 /* ViewController+FreeDrive.swift */, + F43EE328261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift */, 3577B877214FF35800094294 /* FavoritesList.swift */, 358D14651E5E3B7700ADE590 /* AppDelegate.swift */, 35379CFB21480BFB00FD402E /* AppDelegate+CarPlay.swift */, @@ -1658,6 +1666,8 @@ 8AE9081125FAA53300F37077 /* Collection.swift */, 8A8C3D97260175D20071D274 /* CLLocationDirection.swift */, 8A446644260A7B24008BA55E /* BoundingBox.swift */, + F488A0BD26261C4600A4CC8C /* NavigationMapView+IntersectionAnnotations.swift */, + F488A0C726261D8100A4CC8C /* ElectronicHorizon.swift */, ); name = Extensions; sourceTree = ""; @@ -2555,6 +2565,7 @@ 8DEDEF3421E3FBE80049E114 /* NavigationViewControllerDelegate.swift in Sources */, 8A446645260A7B24008BA55E /* BoundingBox.swift in Sources */, 8AD866F625CA1BF10019A638 /* NavigationCamera.swift in Sources */, + F488A0C826261D8100A4CC8C /* ElectronicHorizon.swift in Sources */, 8D5DFFF1207C04840093765A /* NSAttributedString.swift in Sources */, 35CF34B11F0A733200C2692E /* UIFont.swift in Sources */, 8AD866F925CA1BF10019A638 /* ViewportDataSource.swift in Sources */, @@ -2579,6 +2590,7 @@ 160D8279205996DA00D278D6 /* DataCache.swift in Sources */, 8AFF437125F847340053CBB1 /* CameraOptions.swift in Sources */, 351BEBF21E5BCC63006FE110 /* Style.swift in Sources */, + F488A0BE26261C4600A4CC8C /* NavigationMapView+IntersectionAnnotations.swift in Sources */, 43FB386923A202420064481E /* Route.swift in Sources */, 3EA937B1F4DF73EB004BA6BE /* InstructionPresenter.swift in Sources */, 5A1C075824BDEB44000A6330 /* PassiveLocationManager.swift in Sources */, @@ -2600,6 +2612,7 @@ buildActionMask = 2147483647; files = ( 358D14681E5E3B7700ADE590 /* ViewController.swift in Sources */, + F43EE329261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift in Sources */, C5D9800D1EFA8BA9006DBF2E /* CustomViewController.swift in Sources */, AED6285622CBE4CE00058A51 /* ViewController+GuidanceCards.swift in Sources */, 8A092F9B25DEE81900CA7CF5 /* ViewController+FreeDrive.swift in Sources */, @@ -2705,6 +2718,7 @@ C53F2EE720EBC95600D9798F /* WaypointConfirmationViewController.swift in Sources */, C5DE4B6220F6B6B3007AFBE6 /* CustomStyles.swift in Sources */, 8A0D5DB725DF2A86006F0919 /* StyledFeature.swift in Sources */, + F43EE32A261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift in Sources */, DA8805002316EAED00B54D87 /* ViewController+GuidanceCards.swift in Sources */, 8A092F9C25DEE81900CA7CF5 /* ViewController+FreeDrive.swift in Sources */, 35379CFD21480C0500FD402E /* AppDelegate+CarPlay.swift in Sources */, diff --git a/NavigationMapView+RoadAnnotations.swift b/NavigationMapView+RoadAnnotations.swift new file mode 100644 index 00000000000..3aa07b631a8 --- /dev/null +++ b/NavigationMapView+RoadAnnotations.swift @@ -0,0 +1,12 @@ +import UIKit +import Turf +import MapboxDirections +import MapboxCoreNavigation +import MapboxNavigation +import MapboxCoreMaps +import MapboxMaps + +// MARK: - Visible annotations on the map about the current drive + +extension NavigationMapView { +} diff --git a/Sources/MapboxCoreNavigation/CoreConstants.swift b/Sources/MapboxCoreNavigation/CoreConstants.swift index 4cb8b35def9..04d64a0a008 100644 --- a/Sources/MapboxCoreNavigation/CoreConstants.swift +++ b/Sources/MapboxCoreNavigation/CoreConstants.swift @@ -363,6 +363,8 @@ extension ElectronicHorizon { /** A key in the user info dictionary of a `Notification.Name.electronicHorizonDidEnterRoadObject` or `Notification.Name.electronicHorizonDidExitRoadObject` notification. The corresponding value is a `RoadObjectIdentifier` identifying the road object that the user entered or exited. */ public static let roadObjectIdentifierKey: NotificationUserInfoKey = .init(rawValue: "roadObjectIdentifier") + + public static let roadGraphIdentifierKey: NotificationUserInfoKey = .init(rawValue: "roadGraph") /** A key in the user info dictionary of a `Notification.Name.electronicHorizonDidEnterRoadObject` or `Notification.Name.electronicHorizonDidExitRoadObject` notification. The corresponding value is an `NSNumber` containing a Boolean value set to `true` if the user entered at the beginning or exited at the end of the road object, or `false` if they entered or exited somewhere along the road object. */ diff --git a/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift b/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift index 16f8d925c8b..333e40a815b 100644 --- a/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift +++ b/Sources/MapboxCoreNavigation/CoreNavigationNavigator.swift @@ -128,12 +128,15 @@ class Navigator { extension Navigator: ElectronicHorizonObserver { public func onPositionUpdated(for position: ElectronicHorizonPosition, distances: [String : MapboxNavigationNative.RoadObjectDistanceInfo]) { - let userInfo: [ElectronicHorizon.NotificationUserInfoKey: Any] = [ + var userInfo: [ElectronicHorizon.NotificationUserInfoKey: Any] = [ .positionKey: RoadGraph.Position(try! position.position()), .treeKey: ElectronicHorizon(try! position.tree()), .updatesMostProbablePathKey: try! position.type() == .UPDATE, .distancesByRoadObjectKey: distances.mapValues(RoadObjectDistanceInfo.init), ] + if let roadGraph = roadGraph { + userInfo.updateValue(roadGraph, forKey: .roadGraphIdentifierKey) + } NotificationCenter.default.post(name: .electronicHorizonDidUpdatePosition, object: nil, userInfo: userInfo) } diff --git a/Sources/MapboxCoreNavigation/NavigationService.swift b/Sources/MapboxCoreNavigation/NavigationService.swift index f658ed6d1d5..b035dce1ec8 100644 --- a/Sources/MapboxCoreNavigation/NavigationService.swift +++ b/Sources/MapboxCoreNavigation/NavigationService.swift @@ -249,7 +249,7 @@ public class MapboxNavigationService: NSObject, NavigationService { let routerType = routerType ?? DefaultRouter.self router = routerType.init(along: route, routeIndex: routeIndex, options: routeOptions, directions: self.directions, dataSource: self) NavigationSettings.shared.distanceUnit = routeOptions.locale.usesMetric ? .kilometer : .mile - + let eventType = eventsManagerType ?? NavigationEventsManager.self eventsManager = eventType.init(dataSource: self, accessToken: self.directions.credentials.accessToken) locationManager.activityType = routeOptions.activityType diff --git a/Sources/MapboxNavigation/DayStyle.swift b/Sources/MapboxNavigation/DayStyle.swift index 59dae88f625..e04d678b534 100644 --- a/Sources/MapboxNavigation/DayStyle.swift +++ b/Sources/MapboxNavigation/DayStyle.swift @@ -36,6 +36,10 @@ extension UIColor { class var alternativeTrafficSevere: UIColor { get { return #colorLiteral(red: 0.71, green: 0.51, blue: 0.51, alpha: 1.0) } } 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) } } + class var intersectionAnnotationDefaultBackgroundColor: UIColor { get { return #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } } + class var intersectionAnnotationSelectedBackgroundColor: UIColor { get { return #colorLiteral(red: 0.337254902, green: 0.6588235294, blue: 0.9843137255, alpha: 1) } } + class var intersectionAnnotationDefaultLabelColor: UIColor { get { return #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) } } + class var intersectionAnnotationSelectedLabelColor: UIColor { get { return #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } } } extension UIColor { diff --git a/Sources/MapboxNavigation/ElectronicHorizon.swift b/Sources/MapboxNavigation/ElectronicHorizon.swift new file mode 100644 index 00000000000..a554379857b --- /dev/null +++ b/Sources/MapboxNavigation/ElectronicHorizon.swift @@ -0,0 +1,38 @@ +import MapboxCoreNavigation + +extension ElectronicHorizon.Edge { + var mpp: [ElectronicHorizon.Edge]? { + + guard level == 0 else { return nil } + + var mostProbablePath = [self] + + for child in outletEdges { + if let childMPP = child.mpp { + mostProbablePath.append(contentsOf: childMPP) + } + } + + return mostProbablePath + } + + func edgeNames(roadGraph: RoadGraph) -> [String] { + guard let metadata = roadGraph.edgeMetadata(edgeIdentifier: identifier) else { + return [] + } + let names = metadata.names.map { name -> String in + switch name { + case .name(let name): + return name + case .code(let code): + return "(\(code))" + } + } + + // If the road is unnamed, fall back to the road class. + if names.isEmpty { + return ["\(metadata.mapboxStreetsRoadClass.rawValue)"] + } + return names + } +} diff --git a/Sources/MapboxNavigation/NavigationMapView+BuildingHighlighting.swift b/Sources/MapboxNavigation/NavigationMapView+BuildingHighlighting.swift index c6ab2673e4b..612a40979c9 100644 --- a/Sources/MapboxNavigation/NavigationMapView+BuildingHighlighting.swift +++ b/Sources/MapboxNavigation/NavigationMapView+BuildingHighlighting.swift @@ -101,7 +101,16 @@ extension NavigationMapView { highlightedBuildingsLayer.paint?.fillExtrusionColor = .constant(.init(color: buildingHighlightColor)) highlightedBuildingsLayer.paint?.fillExtrusionHeightTransition = StyleTransition(duration: 0.8, delay: 0) highlightedBuildingsLayer.paint?.fillExtrusionOpacityTransition = StyleTransition(duration: 0.8, delay: 0) + + #if false + if let _ = try? mapView.style.getSource(identifier: NavigationMapView.intersectionAnnotations, type: GeoJSONSource.self).get() { + mapView.style.addLayer(layer: highlightedBuildingsLayer, layerPosition: LayerPosition(above: nil, below: IdentifierString.intersectionAnnotationsLayer, at: nil)) + } else { + mapView.style.addLayer(layer: highlightedBuildingsLayer) + } + #else mapView.style.addLayer(layer: highlightedBuildingsLayer) + #endif } } diff --git a/Sources/MapboxNavigation/NavigationMapView+IntersectionAnnotations.swift b/Sources/MapboxNavigation/NavigationMapView+IntersectionAnnotations.swift new file mode 100644 index 00000000000..6673681a0ca --- /dev/null +++ b/Sources/MapboxNavigation/NavigationMapView+IntersectionAnnotations.swift @@ -0,0 +1,133 @@ +import CoreLocation +import UIKit +import MapboxDirections +import MapboxCoreNavigation +import Turf +import MapboxMaps + +extension NavigationMapView { + + struct EdgeIntersection { + var root: ElectronicHorizon.Edge + var branch: ElectronicHorizon.Edge + var rootMetadata: ElectronicHorizon.Edge.Metadata + var rootShape: LineString + var branchMetadata: ElectronicHorizon.Edge.Metadata + var branchShape: LineString + + var coordinate: CLLocationCoordinate2D? { + rootShape.coordinates.first + } + + var annotationPoint: CLLocationCoordinate2D? { + guard let length = branchShape.distance() else { return nil } + let targetDistance = min(length / 2, Double.random(in: 15...30)) + guard let annotationPoint = branchShape.coordinateFromStart(distance: targetDistance) else { return nil } + return annotationPoint + } + + var wayName: String? { + guard let roadName = rootMetadata.names.first else { return nil } + + switch roadName { + case .name(let name): + return name + case .code(let code): + return "(\(code))" + } + } + var intersectingWayName: String? { + guard let roadName = branchMetadata.names.first else { return nil } + + switch roadName { + case .name(let name): + return name + case .code(let code): + return "(\(code))" + } + } + + var incidentAngle: CLLocationDegrees { + return (branchMetadata.heading - rootMetadata.heading).wrap(min: 0, max: 360) + } + + var description: String { + return "EdgeIntersection: root: \(wayName ?? "") intersection: \(intersectingWayName ?? "") coordinate: \(String(describing: coordinate))" + } + } + + enum AnnotationTailPosition: Int { + case left + case right + case center + } + + class AnnotationCacheEntry: Equatable, Hashable { + var wayname: String + var coordinate: CLLocationCoordinate2D + var intersection: EdgeIntersection? + var feature: Feature + var lastAccessTime: Date + + init(coordinate: CLLocationCoordinate2D, wayname: String, intersection: EdgeIntersection? = nil, feature: Feature) { + self.wayname = wayname + self.coordinate = coordinate + self.intersection = intersection + self.feature = feature + self.lastAccessTime = Date() + } + + static func == (lhs: AnnotationCacheEntry, rhs: AnnotationCacheEntry) -> Bool { + return lhs.wayname == rhs.wayname + } + + func hash(into hasher: inout Hasher) { + hasher.combine(wayname.hashValue) + } + } + + class AnnotationCache { + private let maxEntryAge = TimeInterval(30) + var entries = Set() + var cachePruningTimer: Timer? + + init() { + // periodically prune the cache to remove entries that have been passed already + cachePruningTimer = Timer.scheduledTimer(withTimeInterval: 15, repeats: true, block: { [weak self] _ in + self?.prune() + }) + } + + deinit { + cachePruningTimer?.invalidate() + cachePruningTimer = nil + } + + func setValue(feature: Feature, coordinate: CLLocationCoordinate2D, intersection: EdgeIntersection?, for wayname: String) { + entries.insert(AnnotationCacheEntry(coordinate: coordinate, wayname: wayname, intersection: intersection, feature: feature)) + } + + func value(for wayname: String) -> AnnotationCacheEntry? { + let matchingEntry = entries.first { entry -> Bool in + entry.wayname == wayname + } + + if let matchingEntry = matchingEntry { + // update the timestamp used for pruning the cache + matchingEntry.lastAccessTime = Date() + } + + return matchingEntry + } + + private func prune() { + let now = Date() + + entries.filter { now.timeIntervalSince($0.lastAccessTime) > maxEntryAge }.forEach { remove($0) } + } + + public func remove(_ entry: AnnotationCacheEntry) { + entries.remove(entry) + } + } +} diff --git a/Sources/MapboxNavigation/NavigationMapView.swift b/Sources/MapboxNavigation/NavigationMapView.swift index 43493884197..8777380cbeb 100755 --- a/Sources/MapboxNavigation/NavigationMapView.swift +++ b/Sources/MapboxNavigation/NavigationMapView.swift @@ -75,7 +75,8 @@ open class NavigationMapView: UIView { static let waypointSource = "\(identifier)_waypointSource" static let waypointCircle = "\(identifier)_waypointCircle" static let waypointSymbol = "\(identifier)_waypointSymbol" - static let buildingExtrusionLayer = "\(identifier)buildingExtrusionLayer" + static let buildingExtrusionLayer = "\(identifier)_buildingExtrusionLayer" + static let intersectionAnnotationsLayer = "\(identifier)_intersectionAnnotations" } @objc dynamic public var trafficUnknownColor: UIColor = .trafficUnknown @@ -97,6 +98,11 @@ open class NavigationMapView: UIView { @objc dynamic public var maneuverArrowStrokeColor: UIColor = .defaultManeuverArrowStroke @objc dynamic public var buildingDefaultColor: UIColor = .defaultBuildingColor @objc dynamic public var buildingHighlightColor: UIColor = .defaultBuildingHighlightColor + @objc dynamic public var intersectionAnnotationDefaultBackgroundColor: UIColor = .intersectionAnnotationDefaultBackgroundColor + @objc dynamic public var intersectionAnnotationSelectedBackgroundColor: UIColor = .intersectionAnnotationSelectedBackgroundColor + @objc dynamic public var intersectionAnnotationDefaultLabelColor: UIColor = .intersectionAnnotationDefaultLabelColor + @objc dynamic public var intersectionAnnotationSelectedLabelColor: UIColor = .intersectionAnnotationSelectedLabelColor + @objc dynamic public var reducedAccuracyActivatedMode: Bool = false { didSet { let frame = CGRect(origin: .zero, size: 75.0) @@ -207,9 +213,11 @@ open class NavigationMapView: UIView { setupGestureRecognizers() installUserCourseView() subscribeForNotifications() + annotationCache = AnnotationCache() } - + deinit { + annotationCache = nil unsubscribeFromNotifications() } @@ -259,6 +267,10 @@ open class NavigationMapView: UIView { let location = self.mostRecentUserCourseViewLocation else { return } self.updateUserCourseView(location, animated: false) } + + mapView.on(.mapLoaded) { [weak self] _ in + self?.addAnnotationSymbolImages() + } addSubview(mapView) @@ -967,4 +979,455 @@ open class NavigationMapView: UIView { return candidates } + + public var showIntersectionAnnotations: Bool = false { + didSet { + guard oldValue != showIntersectionAnnotations else { return } + intersectionsToAnnotate = nil + if showIntersectionAnnotations { + NotificationCenter.default.addObserver(self, + selector: #selector(didUpdateElectronicHorizonPosition), + name: .electronicHorizonDidUpdatePosition, + object: nil) + } else { + removeRouteAnnotationsLayerFromStyle() + NotificationCenter.default.removeObserver(self, name: .electronicHorizonDidUpdatePosition, object: nil) + } + } + } + + private func updateIntersectionAnnotationSet(horizon: ElectronicHorizon, roadGraph: RoadGraph) { + + guard let currentWayname = horizon.start.edgeNames(roadGraph: roadGraph).first else { return } + + // grab the MPP from the Electronic Horizon + guard let edges = horizon.start.mpp else { return } + + var intersections = [EdgeIntersection]() + + var intersectingWaynames = [String]() + + for mppEdge in edges { + let metadata = roadGraph.edgeMetadata(edgeIdentifier: mppEdge.identifier) + guard metadata?.names != nil else { continue } + // look through all the edges to filter out ones we don't want to consider + // These are ones that lack a name, are very short, or are not on-screen + let level1Edges = mppEdge.outletEdges.filter { outEdge -> Bool in + // Criteria for accepting an edge as a candidate intersecting road + // • Is not on the MPP + // • Is a named road + // • Is not the current road being travelled + // • Is not a road already accepted (happens since there will be more than one outlet edge if a road continues through the current one) + // • Is of a large enough road class + // • Is of a non-trivial length in meters + // • Intersection point is currently visible on screen + + guard outEdge.level != 0 else { return false } + guard let edgeMetadata = roadGraph.edgeMetadata(edgeIdentifier: outEdge.identifier), let geometry = roadGraph.edgeShape(edgeIdentifier: mppEdge.identifier) else { return false } + + let names = edgeMetadata.names.map { name -> String in + switch name { + case .name(let name): + return name + case .code(let code): + return "(\(code))" + } + } + + guard let firstName = names.first, firstName != "" else { + // edge has no name + return false + } + guard firstName != currentWayname else { + // edge is for the currently travelled road + return false + } + + guard !intersectingWaynames.contains(firstName) else { + // an edge for this road is already chosen + return false + } + + guard ![MapboxStreetsRoadClass.service, MapboxStreetsRoadClass.ferry, MapboxStreetsRoadClass.path, MapboxStreetsRoadClass.majorRail, MapboxStreetsRoadClass.minorRail, MapboxStreetsRoadClass.serviceRail, MapboxStreetsRoadClass.aerialway, MapboxStreetsRoadClass.golf].contains(edgeMetadata.mapboxStreetsRoadClass) else { + // edge is of type that we choose not to label + return false + } + + guard edgeMetadata.length >= 5 else { + // edge is at least 5 meters long + return false + } + + guard let length = geometry.distance() else { return false } + + let targetDistance = min(length / 2, Double.random(in: 15...30)) + guard let annotationPoint = geometry.coordinateFromStart(distance: targetDistance) else { + // unable to find a coordinate to label + return false + } + + let onscreenPoint = self.mapView.point(for: annotationPoint, in: nil) + + guard mapView.bounds.insetBy(dx: 20, dy: 20).contains(onscreenPoint) else { + // intersection coordinate is not visible on screen + return false + } + + // acceptable intersection to label + intersectingWaynames.append(firstName) + return true + } + + // record the edge information for use in creating the annotation Turf.Feature + let rootMetadata: ElectronicHorizon.Edge.Metadata? = roadGraph.edgeMetadata(edgeIdentifier: mppEdge.identifier) + let rootShape: LineString? = roadGraph.edgeShape(edgeIdentifier: mppEdge.identifier) + for branch in level1Edges { + let branchMetadata: ElectronicHorizon.Edge.Metadata? = roadGraph.edgeMetadata(edgeIdentifier: branch.identifier) + let branchShape: LineString? = roadGraph.edgeShape(edgeIdentifier: branch.identifier) + guard let rootMetadata = rootMetadata, let rootShape = rootShape, let branchInfo = branchMetadata, let branchGeometry = branchShape else { return } + + intersections.append(EdgeIntersection(root: mppEdge, branch: branch, rootMetadata: rootMetadata, rootShape: rootShape, branchMetadata: branchInfo, branchShape: branchGeometry)) + } + } + + // sort the edges by distance from the user + if let userCoordinate = mostRecentUserCourseViewLocation?.coordinate { + intersections.sort { (intersection1, intersection2) -> Bool in + if let edge1Start = intersection1.coordinate, let edge2Start = intersection2.coordinate { + return userCoordinate.distance(to: edge1Start) < userCoordinate.distance(to: edge2Start) + } + return true + } + } + + // form a set of the names of current intersections + // we will use this to check if any old intersections are no longer relevant or any additional ones have been picked + let currentNames = intersections.compactMap { return $0.intersectingWayName } + let currentNameSet = Set(currentNames) + + // if the road name set hasn't changed then we can just short-circuit out + guard previousNameSet != currentNameSet else { return } + + // go ahead and update our list of currently labelled intersections + previousNameSet = currentNameSet + + // take up to 4 intersections to annotate. Limit it to prevent cluttering the map with too many annotations + intersectionsToAnnotate = Array(intersections.prefix(4)) + } + + var previousNameSet: Set? + var intersectionsToAnnotate: [EdgeIntersection]? + + open func updateAnnotations(for routeProgress: RouteProgress) { + var features = [Feature]() + + // add an annotation for the next step + + if let upcomingStep = routeProgress.upcomingStep { + let maneuverLocation = upcomingStep.maneuverLocation + var labelText = upcomingStep.names?.first ?? "" + let currentLeg = routeProgress.currentLeg + + if upcomingStep == currentLeg.steps.last, let destination = currentLeg.destination?.name { + labelText = destination + } + + if labelText == "", let destinationCodes = upcomingStep.destinationCodes, destinationCodes.count > 0 { + labelText = destinationCodes[0] + + destinationCodes.dropFirst().forEach { destination in + labelText += " / " + destination + } + } + + if labelText == "", let exitCodes = upcomingStep.exitCodes, let code = exitCodes.first { + labelText = "Exit \(code)" + } + + if labelText == "", let destination = upcomingStep.destinations?.first { + labelText = destination + } + + if labelText == "", let exitName = upcomingStep.exitNames?.first { + labelText = exitName + } + + if labelText != "" { + var featurePoint: Feature + if let cachedEntry = cachedAnnotationFeature(for: labelText) { + featurePoint = cachedEntry.feature + } else { + featurePoint = Feature(Point(maneuverLocation)) + + let tailPosition = AnnotationTailPosition.center + + // set the feature attributes which will be used in styling the symbol style layer + featurePoint.properties = ["highlighted": true, "tailPosition": tailPosition.rawValue, "text": labelText, "imageName": "AnnotationCentered-Highlighted", "sortOrder": 0] + + annotationCache?.setValue(feature: featurePoint, coordinate: maneuverLocation, intersection: nil, for: labelText) + } + features.append(featurePoint) + } + } + + guard let intersectionsToAnnotate = intersectionsToAnnotate else { return } + for (index, intersection) in intersectionsToAnnotate.enumerated() { + guard let coordinate = intersection.annotationPoint else { continue } + var featurePoint: Feature + + if let intersectingWayName = intersection.intersectingWayName, let cachedEntry = cachedAnnotationFeature(for: intersectingWayName) { + featurePoint = cachedEntry.feature + } else { + featurePoint = Feature(Point(coordinate)) + + let tailPosition = intersection.incidentAngle < 180 ? AnnotationTailPosition.left : AnnotationTailPosition.right + + let imageName = tailPosition == .left ? "AnnotationLeftHanded" : "AnnotationRightHanded" + + // set the feature attributes which will be used in styling the symbol style layer + featurePoint.properties = ["highlighted": false, "tailPosition": tailPosition.rawValue, "text": intersection.intersectingWayName, "imageName": imageName, "sortOrder": -index] + + if let intersectingWayName = intersection.intersectingWayName { + annotationCache?.setValue(feature: featurePoint, coordinate: coordinate, intersection: nil, for: intersectingWayName) + } + } + features.append(featurePoint) + } + + updateAnnotationLayer(with: FeatureCollection(features: features)) + } + + private func addAnnotationSymbolImages() { + guard let style = mapView.style, style.getStyleImage(with: "AnnotationLeftHanded") == nil, style.getStyleImage(with: "AnnotationRightHanded") == nil else { return } + + // Centered pin + if let image = UIImage(named: "AnnotationCentered", in: .mapboxNavigation, compatibleWith: nil) { + let stretchX = [ImageStretches(first: Float(20), second: Float(30)), ImageStretches(first: Float(90), second: Float(100))] + let stretchY = [ImageStretches(first: Float(26), second: Float(32))] + let imageContent = ImageContent(left: 20, top: 26, right: 100, bottom: 33) + + let regularAnnotationImage = image.tint(.intersectionAnnotationDefaultBackgroundColor) + + style.setStyleImage(image: regularAnnotationImage, + with: "AnnotationCentered", + stretchX: stretchX, + stretchY: stretchY, + scale: 2.0, + imageContent: imageContent) + + let highlightedAnnotationImage = image.tint(.intersectionAnnotationSelectedBackgroundColor) + style.setStyleImage(image: highlightedAnnotationImage, + with: "AnnotationCentered-Highlighted", + stretchX: stretchX, + stretchY: stretchY, + scale: 2.0, + imageContent: imageContent) + } + + let stretchX = [ImageStretches(first: Float(32), second: Float(42))] + let stretchY = [ImageStretches(first: Float(26), second: Float(32))] + let imageContent = ImageContent(left: 32, top: 26, right: 47, bottom: 33) + + // Right-hand pin + if let image = UIImage(named: "AnnotationRightHanded", in: .mapboxNavigation, compatibleWith: nil) { + let regularAnnotationImage = image.tint(.intersectionAnnotationDefaultBackgroundColor) + + style.setStyleImage(image: regularAnnotationImage, + with: "AnnotationRightHanded", + stretchX: stretchX, + stretchY: stretchY, + scale: 2.0, + imageContent: imageContent) + + let highlightedAnnotationImage = image.tint(.intersectionAnnotationSelectedBackgroundColor) + style.setStyleImage(image: highlightedAnnotationImage, + with: "AnnotationRightHanded-Highlighted", + stretchX: stretchX, + stretchY: stretchY, + scale: 2.0, + imageContent: imageContent) + } + + // Left-hand pin + if let image = UIImage(named: "AnnotationLeftHanded", in: .mapboxNavigation, compatibleWith: nil) { + let regularAnnotationImage = image.tint(.intersectionAnnotationDefaultBackgroundColor) + + style.setStyleImage(image: regularAnnotationImage, + with: "AnnotationLeftHanded", + stretchX: stretchX, + stretchY: stretchY, + scale: 2.0, + imageContent: imageContent) + + let highlightedAnnotationImage = image.tint(.intersectionAnnotationSelectedBackgroundColor) + style.setStyleImage(image: highlightedAnnotationImage, + with: "AnnotationLeftHanded-Highlighted", + stretchX: stretchX, + stretchY: stretchY, + scale: 2.0, + imageContent: imageContent) + } + } + + private func removeRouteAnnotationsLayerFromStyle() { + mapView.style.removeLayers([NavigationMapView.intersectionAnnotations]) + _ = mapView.style.removeSource(for: NavigationMapView.intersectionAnnotations) + } + + var annotationCache: AnnotationCache? + static let intersectionAnnotations = "intersectionAnnotations" + + private func cachedAnnotationFeature(for labelText: String) -> AnnotationCacheEntry? { + if let existingFeature = annotationCache?.value(for: labelText) { + // ensure the cached feature is still visible on-screen. If it is not then remove the entry and return nil + let unprojectedCoordinate = self.mapView.point(for: existingFeature.coordinate, in: nil) + if mapView.bounds.contains(unprojectedCoordinate) { + return existingFeature + } else { + annotationCache?.remove(existingFeature) + } + } + + return nil + } + + private func updateAnnotationLayer(with features: FeatureCollection) { + guard let style = mapView.style else { return } + let existingDataSource = try? mapView.style.getSource(identifier: NavigationMapView.intersectionAnnotations, type: GeoJSONSource.self).get() + if existingDataSource != nil { + _ = mapView.style.updateGeoJSON(for: NavigationMapView.intersectionAnnotations, with: features) + return + } else { + var dataSource = GeoJSONSource() + dataSource.data = .featureCollection(features) + mapView.style.addSource(source: dataSource, identifier: NavigationMapView.intersectionAnnotations) + } + + _ = mapView.style.removeStyleLayer(forLayerId: NavigationMapView.intersectionAnnotations) + + var shapeLayer = SymbolLayer(id: NavigationMapView.intersectionAnnotations) + shapeLayer.source = NavigationMapView.intersectionAnnotations + + shapeLayer.layout?.textField = .expression(Exp(.get) { + "text" + }) + + shapeLayer.layout?.iconImage = .expression(Exp(.get) { + "imageName" + }) + + shapeLayer.paint?.textColor = .expression(Exp(.switchCase) { + Exp(.any) { + Exp(.get) { + "highlighted" + } + } + self.intersectionAnnotationSelectedLabelColor + self.intersectionAnnotationDefaultLabelColor + }) + + shapeLayer.layout?.textSize = .constant(16) + shapeLayer.layout?.iconTextFit = .constant(.both) + shapeLayer.layout?.iconAllowOverlap = .constant(true) + shapeLayer.layout?.textAllowOverlap = .constant(true) + shapeLayer.layout?.textJustify = .constant(.center) + shapeLayer.layout?.symbolZOrder = .constant(.auto) + shapeLayer.layout?.textFont = .constant(["DIN Pro Medium"]) + shapeLayer.layout?.iconTextFitPadding = .constant([-4, 0, -3, 0]) + + style.addLayer(layer: shapeLayer, layerPosition: nil) + + let symbolSortKeyString = + """ + ["get", "sortOrder"] + """ + + if let expressionData = symbolSortKeyString.data(using: .utf8), let expJSONObject = try? JSONSerialization.jsonObject(with: expressionData, options: []) { + + try! mapView.__map.setStyleLayerPropertyForLayerId(NavigationMapView.intersectionAnnotations, + property: "symbol-sort-key", + value: expJSONObject) + } + + let expressionString = + """ + [ + "match", + ["get", "tailPosition"], + [0], + "bottom-left", + [1], + "bottom-right", + [2], + "bottom", + "center" + ] + """ + + if let expressionData = expressionString.data(using: .utf8), let expJSONObject = try? JSONSerialization.jsonObject(with: expressionData, options: []) { + + try! mapView.__map.setStyleLayerPropertyForLayerId(NavigationMapView.intersectionAnnotations, + property: "icon-anchor", + value: expJSONObject) + try! mapView.__map.setStyleLayerPropertyForLayerId(NavigationMapView.intersectionAnnotations, + property: "text-anchor", + value: expJSONObject) + } + + let offsetExpressionString = + """ + [ + "match", + ["get", "tailPosition"], + [0], + ["literal", [0.5, -1]], + [1], + ["literal", [-0.5, -1]], + [2], + ["literal", [0.0, -1]], + ["literal", [0.0, 0.0]] + ] + """ + + if let expressionData = offsetExpressionString.data(using: .utf8), let expJSONObject = try? JSONSerialization.jsonObject(with: expressionData, options: []) { + + try! mapView.__map.setStyleLayerPropertyForLayerId(NavigationMapView.intersectionAnnotations, + property: "icon-offset", + value: expJSONObject) + + try! mapView.__map.setStyleLayerPropertyForLayerId(NavigationMapView.intersectionAnnotations, + property: "text-offset", + value: expJSONObject) + } + } + + @objc func didUpdateElectronicHorizonPosition(_ notification: Notification) { + guard let horizon = notification.userInfo?[ElectronicHorizon.NotificationUserInfoKey.treeKey] as? ElectronicHorizon, let roadGraph = notification.userInfo?[ElectronicHorizon.NotificationUserInfoKey.roadGraphIdentifierKey] as? RoadGraph else { + return + } + + DispatchQueue.main.async { + self.updateIntersectionAnnotationSet(horizon: horizon, roadGraph: roadGraph) + } + } + + func edgeNames(identifier: ElectronicHorizon.Edge.Identifier, roadGraph: RoadGraph) -> [String] { + guard let metadata = roadGraph.edgeMetadata(edgeIdentifier: identifier) else { + return [] + } + let names = metadata.names.map { name -> String in + switch name { + case .name(let name): + return name + case .code(let code): + return "(\(code))" + } + } + + // If the road is unnamed, fall back to the road class. + if names.isEmpty { + return ["\(metadata.mapboxStreetsRoadClass.rawValue)"] + } + return names + } } diff --git a/Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationCentered.imageset/AnnotationCentered.png b/Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationCentered.imageset/AnnotationCentered.png new file mode 100644 index 0000000000000000000000000000000000000000..2f35bbc6bce2d6a05869f21e39696473b4e3affa GIT binary patch literal 4850 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 JPDHLkV1jIPao_*| literal 0 HcmV?d00001 diff --git a/Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationRightHanded.imageset/Contents.json b/Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationRightHanded.imageset/Contents.json new file mode 100644 index 00000000000..748a6ddd2a7 --- /dev/null +++ b/Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationRightHanded.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "RouteInfoAnnotationRightHanded.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationRightHanded.imageset/RouteInfoAnnotationRightHanded.png b/Sources/MapboxNavigation/Resources/Assets.xcassets/AnnotationRightHanded.imageset/RouteInfoAnnotationRightHanded.png new file mode 100644 index 0000000000000000000000000000000000000000..4b9e2e7f0f85e1744af55d0a5c08be05916d8030 GIT binary patch literal 2463 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 T?9|?Ge~t(i_PFYkzbE_`06%_> literal 0 HcmV?d00001 diff --git a/Sources/MapboxNavigation/Resources/Assets.xcassets/Contents.json b/Sources/MapboxNavigation/Resources/Assets.xcassets/Contents.json index da4a164c918..73c00596a7f 100644 --- a/Sources/MapboxNavigation/Resources/Assets.xcassets/Contents.json +++ b/Sources/MapboxNavigation/Resources/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Sources/MapboxNavigation/RouteMapViewController.swift b/Sources/MapboxNavigation/RouteMapViewController.swift index 9c4b591c4d7..6d9a22defca 100644 --- a/Sources/MapboxNavigation/RouteMapViewController.swift +++ b/Sources/MapboxNavigation/RouteMapViewController.swift @@ -442,6 +442,7 @@ extension RouteMapViewController: NavigationComponent { let stepIndex = progress.currentLegProgress.stepIndex navigationMapView.updatePreferredFrameRate(for: progress) + navigationMapView.updateAnnotations(for: progress) if currentLegIndexMapped != legIndex { navigationMapView.showWaypoints(on: route, legIndex: legIndex) navigationMapView.show([route], legIndex: legIndex) diff --git a/Sources/MapboxNavigation/UIImage.swift b/Sources/MapboxNavigation/UIImage.swift index f358838845e..4a8224c3097 100644 --- a/Sources/MapboxNavigation/UIImage.swift +++ b/Sources/MapboxNavigation/UIImage.swift @@ -43,4 +43,31 @@ extension UIImage { return scaledImage } + + // Produce a copy of the image with tint color applied. + // Useful for deployment to iOS versions prior to 13 where tinting support was added to UIImage natively. + func tint(_ tintColor: UIColor) -> UIImage { + let imageSize = size + let imageScale = scale + let contextBounds = CGRect(origin: .zero, size: imageSize) + + UIGraphicsBeginImageContextWithOptions(imageSize, false, imageScale) + + defer { UIGraphicsEndImageContext() } + + UIColor.black.setFill() + UIRectFill(contextBounds) + draw(at: .zero) + + guard let imageOverBlack = UIGraphicsGetImageFromCurrentImageContext() else { return self } + tintColor.setFill() + UIRectFill(contextBounds) + + imageOverBlack.draw(at: .zero, blendMode: .multiply, alpha: 1) + draw(at: .zero, blendMode: .destinationIn, alpha: 1) + + guard let finalImage = UIGraphicsGetImageFromCurrentImageContext() else { return self } + + return finalImage + } }