diff --git a/Examples/Swift/Example-Swift-BridgingHeader.h b/Examples/Swift/Example-Swift-BridgingHeader.h new file mode 100644 index 00000000000..bff1b1ff4f0 --- /dev/null +++ b/Examples/Swift/Example-Swift-BridgingHeader.h @@ -0,0 +1,3 @@ +#pragma once + +#import "MGLMapView+CustomAdditions.h" diff --git a/Examples/Swift/MGLMapView+CustomAdditions.h b/Examples/Swift/MGLMapView+CustomAdditions.h new file mode 100644 index 00000000000..5c6b305a0e1 --- /dev/null +++ b/Examples/Swift/MGLMapView+CustomAdditions.h @@ -0,0 +1,10 @@ +#import + +@interface MGLMapView (CustomAdditions) + +// FIXME: This will be removed once https://github.com/mapbox/mapbox-gl-native/issues/6867 is implemented +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations; + +@property (nonatomic, readonly) CLLocationManager *locationManager; + +@end diff --git a/Examples/Swift/MGLMapView+CustomAdditions.m b/Examples/Swift/MGLMapView+CustomAdditions.m new file mode 100644 index 00000000000..0b9e9b425e2 --- /dev/null +++ b/Examples/Swift/MGLMapView+CustomAdditions.m @@ -0,0 +1,11 @@ +#import "MGLMapView+CustomAdditions.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wincomplete-implementation" +// Supressing compiler warning until https://github.com/mapbox/mapbox-gl-native/issues/6867 is implemented +@implementation MGLMapView (CustomAdditions) +#pragma clang diagnostic pop + +@dynamic locationManager; + +@end diff --git a/Examples/Swift/ViewController.swift b/Examples/Swift/ViewController.swift index 2701ec4f88d..36ea6b25d6f 100644 --- a/Examples/Swift/ViewController.swift +++ b/Examples/Swift/ViewController.swift @@ -37,10 +37,6 @@ class ViewController: UIViewController, MGLMapViewDelegate, CLLocationManagerDel var alertController: UIAlertController! - // In this example, we show you how you can create custom UIView that is used to show the user's location. - // Set `showCustomUserPuck` to true to view the custom user puck. - var showCustomUserPuck = false - override func viewDidLoad() { super.viewDidLoad() @@ -261,37 +257,7 @@ extension ViewController: NavigationViewControllerDelegate { navigationViewController.present(confirmationController, animated: true, completion: nil) } - - func navigationMapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { - guard annotation is MGLUserLocation && showCustomUserPuck else { return nil } - - let reuseIdentifier = "userPuck" - var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) - - if annotationView == nil { - annotationView = CustomAnnotationView(reuseIdentifier: reuseIdentifier) - annotationView!.frame = CGRect(x: 0, y: 0, width: 40, height: 40) - annotationView!.backgroundColor = .red - } - - return annotationView - } } - -class CustomAnnotationView: MGLUserLocationAnnotationView { - override func layoutSubviews() { - super.layoutSubviews() - - // Force the annotation view to maintain a constant size when the map is tilted. - scalesWithViewingDistance = false - - // Use CALayer’s corner radius to turn this view into a circle. - layer.cornerRadius = frame.width / 2 - layer.borderWidth = 2 - layer.borderColor = UIColor.white.cgColor - } -} - class CustomNightStyle: DayStyle { required init() { diff --git a/MapboxCoreNavigation/NavigationLocationManager.swift b/MapboxCoreNavigation/NavigationLocationManager.swift index 9e070c083b0..eb96be59292 100644 --- a/MapboxCoreNavigation/NavigationLocationManager.swift +++ b/MapboxCoreNavigation/NavigationLocationManager.swift @@ -1,5 +1,8 @@ import Foundation import CoreLocation +#if os(iOS) +import UIKit +#endif #if os(iOS) import UIKit diff --git a/MapboxCoreNavigation/RouteController.swift b/MapboxCoreNavigation/RouteController.swift index 6784c579e4c..2ff20c7f58a 100644 --- a/MapboxCoreNavigation/RouteController.swift +++ b/MapboxCoreNavigation/RouteController.swift @@ -263,7 +263,7 @@ open class RouteController: NSObject { /** The most recently received user location. - This is a raw location received from `locationManager`. To obtain an idealized location, use the `snappedLocation` property. + This is a raw location received from `locationManager`. To obtain an idealized location, use the `location` property. */ var rawLocation: CLLocation? diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index 860e8e97ee2..22799d2bfb4 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -103,6 +103,7 @@ 359574AA1F28CCBB00838209 /* LocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359574A91F28CCBB00838209 /* LocationTests.swift */; }; 359D00CF1E732D7100C2E770 /* Polyline.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 354A01BC1E66259600D765C2 /* Polyline.framework */; }; 35A5413B1EFC052700E49846 /* RouteOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35A5413A1EFC052700E49846 /* RouteOptions.swift */; }; + 35B1E2951F1FF8EC00A13D32 /* UserCourseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35B1E2941F1FF8EC00A13D32 /* UserCourseView.swift */; }; 35B711D21E5E7AD2001EDA8D /* MapboxNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35B711D11E5E7AD2001EDA8D /* MapboxNavigationTests.swift */; }; 35B711D41E5E7AD2001EDA8D /* MapboxNavigation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 351BEBD71E5BCC28006FE110 /* MapboxNavigation.framework */; }; 35B839491E2E3D5D0045A868 /* MBRouteController.m in Sources */ = {isa = PBXBuildFile; fileRef = 35B839481E2E3D5D0045A868 /* MBRouteController.m */; }; @@ -118,7 +119,6 @@ 35CC141A1F79A43B009E872A /* Turf.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 35CC14141F799496009E872A /* Turf.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 35CF34B11F0A733200C2692E /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35CF34B01F0A733200C2692E /* UIFont.swift */; }; 35D457A71E2D253100A89946 /* MBRouteController.h in Headers */ = {isa = PBXBuildFile; fileRef = 35D457A61E2D253100A89946 /* MBRouteController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 35D825FB1E6A2DBE0088F83B /* MGLMapView+MGLNavigationAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 35D825F91E6A2DBE0088F83B /* MGLMapView+MGLNavigationAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 35D825FC1E6A2DBE0088F83B /* MGLMapView+MGLNavigationAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 35D825FA1E6A2DBE0088F83B /* MGLMapView+MGLNavigationAdditions.m */; }; 35D825FE1E6A2EC60088F83B /* MapboxNavigation.h in Headers */ = {isa = PBXBuildFile; fileRef = 35D825FD1E6A2EC60088F83B /* MapboxNavigation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 35DC9D8D1F431E59001ECD64 /* route.json in Resources */ = {isa = PBXBuildFile; fileRef = C52D09CD1DEF5E5100BE3C5C /* route.json */; }; @@ -166,6 +166,8 @@ C5E7A31C1F4F6828001CB015 /* NavigationRouteOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5E7A31B1F4F6828001CB015 /* NavigationRouteOptions.swift */; }; C5EA98711F19414200C8AA16 /* MapboxMobileEvents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C549F8311F17F2C5001A0A2D /* MapboxMobileEvents.framework */; }; C5EA98721F19414C00C8AA16 /* MapboxMobileEvents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C549F8311F17F2C5001A0A2D /* MapboxMobileEvents.framework */; }; + DA23C9611F4FC05C00BA9522 /* MGLMapView+MGLNavigationAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 35D825F91E6A2DBE0088F83B /* MGLMapView+MGLNavigationAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DA23C9641F4FC0A600BA9522 /* MGLMapView+CustomAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = DA23C9631F4FC0A600BA9522 /* MGLMapView+CustomAdditions.m */; }; DAAE5F301EAE4C4700832871 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DAAE5F321EAE4C4700832871 /* Localizable.strings */; }; DAB2CCE71DF7AFDF001B2FE1 /* dc-line.geojson in Resources */ = {isa = PBXBuildFile; fileRef = DAB2CCE61DF7AFDE001B2FE1 /* dc-line.geojson */; }; DAFA92071F01735000A7FB09 /* DistanceFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351BEC0B1E5BCC72006FE110 /* DistanceFormatter.swift */; }; @@ -383,6 +385,7 @@ 359574A91F28CCBB00838209 /* LocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationTests.swift; sourceTree = ""; }; 35A1D3651E6624EF00A48FE8 /* Mapbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Mapbox.framework; path = Carthage/Build/iOS/Mapbox.framework; sourceTree = ""; }; 35A5413A1EFC052700E49846 /* RouteOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteOptions.swift; sourceTree = ""; }; + 35B1E2941F1FF8EC00A13D32 /* UserCourseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCourseView.swift; sourceTree = ""; }; 35B711CF1E5E7AD2001EDA8D /* MapboxNavigationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MapboxNavigationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 35B711D11E5E7AD2001EDA8D /* MapboxNavigationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxNavigationTests.swift; sourceTree = ""; }; 35B711D31E5E7AD2001EDA8D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -455,6 +458,8 @@ C5D9800C1EFA8BA9006DBF2E /* CustomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomViewController.swift; sourceTree = ""; }; C5D9800E1EFBCDAD006DBF2E /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; C5E7A31B1F4F6828001CB015 /* NavigationRouteOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouteOptions.swift; sourceTree = ""; }; + DA23C9621F4FC0A600BA9522 /* MGLMapView+CustomAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MGLMapView+CustomAdditions.h"; sourceTree = ""; }; + DA23C9631F4FC0A600BA9522 /* MGLMapView+CustomAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MGLMapView+CustomAdditions.m"; sourceTree = ""; }; DA3327391F50C6DA00C5EE88 /* sl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Main.strings; sourceTree = ""; }; DA33273A1F50C6FC00C5EE88 /* sl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Main.strings; sourceTree = ""; }; DA33273B1F50C70E00C5EE88 /* sl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Navigation.strings; sourceTree = ""; }; @@ -496,6 +501,7 @@ DAAE5F311EAE4C4700832871 /* Base */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; DAAE5F331EAE4C5A00832871 /* zh-Hans */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; DAB2CCE61DF7AFDE001B2FE1 /* dc-line.geojson */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "dc-line.geojson"; sourceTree = ""; }; + DACCD9CD1F1FE05C00BB09A1 /* Example-Swift-BridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Example-Swift-BridgingHeader.h"; sourceTree = ""; }; DAE7114C1F22E94E009AED76 /* it */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Main.strings; sourceTree = ""; }; DAE7114D1F22E966009AED76 /* it */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Main.strings; sourceTree = ""; }; DAE7114E1F22E977009AED76 /* it */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Navigation.strings; sourceTree = ""; }; @@ -596,6 +602,7 @@ 35002D721E5F6C830090E733 /* Supporting files */ = { isa = PBXGroup; children = ( + DACCD9CD1F1FE05C00BB09A1 /* Example-Swift-BridgingHeader.h */, 355D20DB1EF30A6D0012B1E0 /* tunnel.route */, 354D9F871EF2FE900006FAA8 /* tunnel.json */, 35002D661E5F6B1B0090E733 /* Main.storyboard */, @@ -647,6 +654,7 @@ 35025F3E1F051DD2002BA3EA /* DialogViewController.swift */, 35375EC01F31FA86004CE727 /* Settings.swift */, C5A6B2DC1F4CE8E8004260EA /* StyleType.swift */, + 35B1E2941F1FF8EC00A13D32 /* UserCourseView.swift */, ); path = MapboxNavigation; sourceTree = ""; @@ -713,6 +721,8 @@ 358D14671E5E3B7700ADE590 /* ViewController.swift */, C5D9800C1EFA8BA9006DBF2E /* CustomViewController.swift */, 6441B1691EFC64E50076499F /* WaypointConfirmationViewController.swift */, + DA23C9621F4FC0A600BA9522 /* MGLMapView+CustomAdditions.h */, + DA23C9631F4FC0A600BA9522 /* MGLMapView+CustomAdditions.m */, ); name = Swift; path = Examples/Swift; @@ -934,8 +944,8 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + DA23C9611F4FC05C00BA9522 /* MGLMapView+MGLNavigationAdditions.h in Headers */, 35D825FE1E6A2EC60088F83B /* MapboxNavigation.h in Headers */, - 35D825FB1E6A2DBE0088F83B /* MGLMapView+MGLNavigationAdditions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1380,6 +1390,7 @@ C58159011EA6D02700FC6C3D /* MGLVectorSource.swift in Sources */, 351BEBF51E5BCC63006FE110 /* RouteManeuverViewController.swift in Sources */, 351BEC011E5BCC63006FE110 /* TurnArrowView.swift in Sources */, + 35B1E2951F1FF8EC00A13D32 /* UserCourseView.swift in Sources */, 351BEBFA1E5BCC63006FE110 /* RouteTableViewController.swift in Sources */, 35025F3F1F051DD2002BA3EA /* DialogViewController.swift in Sources */, 351BEC0E1E5BCC72006FE110 /* DashedLineView.swift in Sources */, @@ -1418,6 +1429,7 @@ files = ( 358D14681E5E3B7700ADE590 /* ViewController.swift in Sources */, C5D9800D1EFA8BA9006DBF2E /* CustomViewController.swift in Sources */, + DA23C9641F4FC0A600BA9522 /* MGLMapView+CustomAdditions.m in Sources */, 6441B16A1EFC64E50076499F /* WaypointConfirmationViewController.swift in Sources */, 358D14661E5E3B7700ADE590 /* AppDelegate.swift in Sources */, ); @@ -1808,6 +1820,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.mapbox.Example-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Examples/Swift/Example-Swift-BridgingHeader.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 3.0; }; @@ -1829,6 +1842,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.mapbox.Example-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Examples/Swift/Example-Swift-BridgingHeader.h"; SWIFT_VERSION = 3.0; }; name = Release; diff --git a/MapboxNavigation/DayStyle.swift b/MapboxNavigation/DayStyle.swift index 9224ed62f48..2baa12346dd 100644 --- a/MapboxNavigation/DayStyle.swift +++ b/MapboxNavigation/DayStyle.swift @@ -80,6 +80,8 @@ open class DayStyle: Style { FloatingButton.appearance().tintColor = tintColor FloatingButton.appearance().backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) + UserPuckCourseView.appearance().puckColor = #colorLiteral(red: 0.149, green: 0.239, blue: 0.341, alpha: 1) + // Maneuver view (Page view) ManeuverView.appearance().backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) diff --git a/MapboxNavigation/MGLMapView+MGLNavigationAdditions.h b/MapboxNavigation/MGLMapView+MGLNavigationAdditions.h index 70e6c5324b3..cf48c63edc8 100644 --- a/MapboxNavigation/MGLMapView+MGLNavigationAdditions.h +++ b/MapboxNavigation/MGLMapView+MGLNavigationAdditions.h @@ -2,12 +2,8 @@ @interface MGLMapView (MGLNavigationAdditions) -// FIXME: This will be removed once https://github.com/mapbox/mapbox-gl-native/issues/6867 is implemented -- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations; +- (void)mapViewDidFinishRenderingFrameFullyRendered:(BOOL)fullyRendered; -// FIXME: This will be removed once https://github.com/mapbox/mapbox-navigation-ios/issues/352 is implemented. -- (void)validateLocationServices; - -@property (nonatomic, readonly) CLLocationManager *locationManager; +@property (nonatomic, readonly) CADisplayLink * _Nullable displayLink; @end diff --git a/MapboxNavigation/MGLMapView+MGLNavigationAdditions.m b/MapboxNavigation/MGLMapView+MGLNavigationAdditions.m index 5f98bb1c375..2f336130fa6 100644 --- a/MapboxNavigation/MGLMapView+MGLNavigationAdditions.m +++ b/MapboxNavigation/MGLMapView+MGLNavigationAdditions.m @@ -2,10 +2,11 @@ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wincomplete-implementation" -// Supressing compiler warning until https://github.com/mapbox/mapbox-gl-native/issues/6867 is implemented @implementation MGLMapView (MGLNavigationAdditions) #pragma clang diagnostic pop -@dynamic locationManager; +- (CADisplayLink *)displayLink { + return [self valueForKey:@"_displayLink"]; +} @end diff --git a/MapboxNavigation/NavigationMapView.swift b/MapboxNavigation/NavigationMapView.swift index a8cb617a151..cca54b53f9f 100644 --- a/MapboxNavigation/NavigationMapView.swift +++ b/MapboxNavigation/NavigationMapView.swift @@ -1,4 +1,5 @@ import Foundation +import Mapbox import MapboxDirections import MapboxCoreNavigation import Turf @@ -34,18 +35,14 @@ open class NavigationMapView: MGLMapView { let arrowCasingSymbolLayerIdentifier = "arrowCasingSymbolLayer" let arrowSymbolSourceIdentifier = "arrowSymbolSource" let currentLegAttribute = "isCurrentLeg" - - var manuallyUpdatesLocation: Bool = false { - didSet { - if manuallyUpdatesLocation { - locationManager.stopUpdatingLocation() - locationManager.stopUpdatingHeading() - locationManager.delegate = nil - } else { - validateLocationServices() - } - } - } + + let routeLineWidthAtZoomLevels: [Int: MGLStyleValue] = [ + 10: MGLStyleValue(rawValue: 8), + 13: MGLStyleValue(rawValue: 9), + 16: MGLStyleValue(rawValue: 12), + 19: MGLStyleValue(rawValue: 24), + 22: MGLStyleValue(rawValue: 30) + ] dynamic public var trafficUnknownColor: UIColor = .trafficUnknown dynamic public var trafficLowColor: UIColor = .trafficLow @@ -54,55 +51,267 @@ open class NavigationMapView: MGLMapView { dynamic public var trafficSevereColor: UIColor = .trafficSevere dynamic public var routeCasingColor: UIColor = .defaultRouteCasing + public override init(frame: CGRect) { + super.init(frame: frame) + + makeGestureRecognizersRespectCourseTracking() + makeGestureRecognizersUpdateCourseView() + + UIDevice.current.addObserver(self, forKeyPath: "batteryState", options: [.initial, .new], context: nil) + } + + public required init?(coder decoder: NSCoder) { + super.init(coder: decoder) + + makeGestureRecognizersRespectCourseTracking() + makeGestureRecognizersUpdateCourseView() + + UIDevice.current.addObserver(self, forKeyPath: "batteryState", options: [.initial, .new], context: nil) + } + + /** Modifies the gesture recognizers to also disable course tracking. */ + func makeGestureRecognizersRespectCourseTracking() { + for gestureRecognizer in gestureRecognizers ?? [] + where gestureRecognizer is UIPanGestureRecognizer || gestureRecognizer is UIRotationGestureRecognizer { + gestureRecognizer.addTarget(self, action: #selector(disableUserCourseTracking)) + } + } + + func makeGestureRecognizersResetInactivityTimer() { + for gestureRecognizer in gestureRecognizers ?? [] { + gestureRecognizer.addTarget(self, action: #selector(resetInactivityTimer(_:))) + } + } + + func makeGestureRecognizersUpdateCourseView() { + for gestureRecognizer in gestureRecognizers ?? [] { + gestureRecognizer.addTarget(self, action: #selector(updateCourseView(_:))) + } + } + + open override func anchorPoint(forGesture gesture: UIGestureRecognizer) -> CGPoint { + if tracksUserCourse { + return userAnchorPoint + } else { + return super.anchorPoint(forGesture: gesture) + } + } + + deinit { + UIDevice.current.removeObserver(self, forKeyPath: "batteryState") + } + + open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "batteryState" { + let batteryState = UIDevice.current.batteryState + isPluggedIn = batteryState == .charging || batteryState == .full + } + } + + func updateCourseView(_ sender: UIGestureRecognizer) { + + if sender.state == .ended { + altitude = self.camera.altitude + 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: { + self.altitude = self.camera.altitude + }) + } + + if let pan = sender as? UIPanGestureRecognizer { + if sender.state == .ended || sender.state == .cancelled { + let velocity = pan.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 } + userCourseView?.layer.removeAllAnimations() + userCourseView?.center = convert(location.coordinate, toPointTo: self) + } + } + + var shouldPositionCourseViewFrameByFrame = false + + // Track position on a frame by frame basis. Used for first location update and when resuming tracking mode + 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 + } + + func resetInactivityTimer(_ sender: UIGestureRecognizer) { + if sender.state == .began { + isInactive = false + } + else if sender.state == .ended || sender.state == .failed { + resetInactivityTimer() + } + } + var showsRoute: Bool { get { return style?.layer(withIdentifier: routeLayerIdentifier) != nil } } - open override var delegate: MGLMapViewDelegate? { - didSet { - refreshShowsUserLocation() + public weak var navigationMapDelegate: NavigationMapViewDelegate? + weak var courseTrackingDelegate: NavigationMapViewCourseTrackingDelegate! + + open override var showsUserLocation: Bool { + get { + if tracksUserCourse || userLocationForCourseTracking != nil { + return !(userCourseView?.isHidden ?? true) + } + return super.showsUserLocation + } + set { + if tracksUserCourse || userLocationForCourseTracking != nil { + super.showsUserLocation = false + + if userCourseView == nil { + userCourseView = UserPuckCourseView(frame: CGRect(origin: .zero, size: CGSize(width: 75, height: 75))) + } + userCourseView?.isHidden = !newValue + } else { + userCourseView?.isHidden = true + super.showsUserLocation = newValue + } } } - public weak var navigationMapDelegate: NavigationMapViewDelegate? { + var userLocationForCourseTracking: CLLocation? + var animatesUserLocation: Bool = false + + fileprivate let inactivityInterval: TimeInterval = 10 + fileprivate let decreasedFrameInterval: Int = 12 + + func resetInactivityTimer() { + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(inactivityTimerFinished), object: nil) + self.perform(#selector(inactivityTimerFinished), with: nil, afterDelay: inactivityInterval) + } + + func inactivityTimerFinished() { + isInactive = true + } + + fileprivate var isInactive: Bool = false { didSet { - refreshShowsUserLocation() + if isInactive { + displayLink?.frameInterval = isPluggedIn ? 1 : decreasedFrameInterval + } else { + displayLink?.frameInterval = 1 + } } } - open override var showsUserLocation: Bool { - get { - return super.showsUserLocation + var isPluggedIn: Bool = false + + @objc func disableUserCourseTracking() { + tracksUserCourse = false + } + + var altitude: CLLocationDistance = 1000 + let defaultAltitude: CLLocationDistance = 1000 + + public func updateCourseTracking(location: CLLocation?, animated: Bool) { + animatesUserLocation = animated + userLocationForCourseTracking = location + guard let location = location, CLLocationCoordinate2DIsValid(location.coordinate) else { + return } - set { - super.showsUserLocation = newValue + if tracksUserCourse { + let point = userAnchorPoint + let padding = UIEdgeInsets(top: point.y, left: point.x, bottom: bounds.height - point.y, right: bounds.width - point.x) + let newCamera = MGLMapCamera(lookingAtCenter: location.coordinate, fromDistance: altitude, pitch: 45, heading: location.course) + let function: CAMediaTimingFunction? = animated ? CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) : nil + let duration: TimeInterval = animated ? 1 : 0 + setCamera(newCamera, withDuration: duration, animationTimingFunction: function, edgePadding: padding, completionHandler: nil) + } + + let duration: TimeInterval = animated ? 1 : 0 + UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear, .beginFromCurrentState], animations: { + self.userCourseView?.center = self.convert(location.coordinate, toPointTo: self) + }, completion: nil) + + if let userCourseView = userCourseView as? UserCourseView { + userCourseView.update(location: location, pitch: camera.pitch, direction: direction, animated: animated, tracksUserCourse: tracksUserCourse) } } /** - Force MGLMapView to ask its delegate for the user location annotation’s view, in case that previously happened before there was a delegate. + Center point of the user course view in screen coordinates relative to the map view. */ - func refreshShowsUserLocation() { - showsUserLocation = false - showsUserLocation = true + var userAnchorPoint: CGPoint { + if let anchorPoint = navigationMapDelegate?.navigationMapViewUserAnchorPoint?(self), anchorPoint != .zero { + return anchorPoint + } + + let contentFrame = UIEdgeInsetsInsetRect(bounds, contentInset) + let courseViewWidth = userCourseView?.frame.width ?? 0 + let courseViewHeight = userCourseView?.frame.height ?? 0 + let edgePadding = UIEdgeInsets(top: 50 + courseViewHeight / 2, + left: 50 + courseViewWidth / 2, + bottom: 50 + courseViewHeight / 2, + right: 50 + courseViewWidth / 2) + return CGPoint(x: max(min(contentFrame.midX, + contentFrame.maxX - edgePadding.right), + contentFrame.minX + edgePadding.left), + y: max(max(min(contentFrame.minY + contentFrame.height * 0.8, + contentFrame.maxY - edgePadding.bottom), + contentFrame.minY + edgePadding.top), + contentFrame.minY + contentFrame.height * 0.5)) } - override open func locationManager(_ manager: CLLocationManager!, didUpdateLocations locations: [CLLocation]!) { - guard let location = locations.first else { return } - - if let modifiedLocation = navigationMapDelegate?.navigationMapView?(self, shouldUpdateTo: location) { - super.locationManager(manager, didUpdateLocations: [modifiedLocation]) - } else { - super.locationManager(manager, didUpdateLocations: locations) + 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) + } } } - override open func validateLocationServices() { - if !manuallyUpdatesLocation { - super.validateLocationServices() + open override func mapViewDidFinishRenderingFrameFullyRendered(_ fullyRendered: Bool) { + super.mapViewDidFinishRenderingFrameFullyRendered(fullyRendered) + + guard shouldPositionCourseViewFrameByFrame else { return } + guard let location = userLocationForCourseTracking else { return } + + userCourseView?.center = convert(location.coordinate, toPointTo: self) + } + + /** + A `UIView` used to indicate the user’s location and course on the map. + + If the view conforms to `UserCourseView`, its `UserCourseView.update(location:pitch:direction:animated:)` method is frequently called to ensure that its visual appearance matches the map’s camera. + */ + public var userCourseView: UIView? { + didSet { + if let userCourseView = userCourseView { + addSubview(userCourseView) + } } } @@ -549,9 +758,6 @@ extension Dictionary where Key == Int, Value: MGLStyleValue { @objc public protocol NavigationMapViewDelegate: class { - @objc(navigationMapView:shouldUpdateToLocation:) - optional func navigationMapView(_ mapView: NavigationMapView, shouldUpdateTo location: CLLocation) -> CLLocation? - @objc optional func navigationMapView(_ mapView: NavigationMapView, routeStyleLayerWithIdentifier identifier: String, source: MGLSource) -> MGLStyleLayer? @objc optional func navigationMapView(_ mapView: NavigationMapView, waypointStyleLayerWithIdentifier identifier: String, source: MGLSource) -> MGLStyleLayer? @@ -568,4 +774,12 @@ public protocol NavigationMapViewDelegate: class { @objc(navigationMapView:shapeDescribingWaypoints:) optional func navigationMapView(_ mapView: NavigationMapView, shapeFor waypoints: [Waypoint]) -> MGLShape? + + @objc(navigationMapViewUserAnchorPoint:) + optional func navigationMapViewUserAnchorPoint(_ mapView: NavigationMapView) -> CGPoint +} + +protocol NavigationMapViewCourseTrackingDelegate: class { + func navigationMapViewDidStartTrackingCourse(_ mapView: NavigationMapView) + func navigationMapViewDidStopTrackingCourse(_ mapView: NavigationMapView) } diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index b1495099ea2..e5cbda74323 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -130,7 +130,7 @@ public protocol NavigationViewControllerDelegate { /** Returns a view object to mark the given point annotation object on the map. - The user location annotation view can also be customized via this method. When annotation is an instance of `MGLUserLocation`, return an instance of `MGLUserLocationAnnotationView` (or a subclass thereof). + The user location annotation view can also be customized via this method. When annotation is an instance of `MGLUserLocation`, return an instance of `MGLUserLocationAnnotationView` (or a subclass thereof). Note that, when `NavigationMapView.tracksUserCourse` is set to `true`, the map view uses a distinct user course view; to customize it, set the `NavigationMapView.userCourseView` property of the map view returned by this view controller’s `mapView` property. */ @objc optional func navigationMapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? @@ -152,6 +152,11 @@ public protocol NavigationViewControllerDelegate { - parameter feedbackType: The type of feedback event that was sent. */ @objc optional func navigationViewController(_ viewController: NavigationViewController, didSend feedbackId: String, feedbackType: FeedbackType) + + /** + Returns the center point of the user course view in screen coordinates relative to the map view. + */ + @objc optional func navigationViewController(_ navigationViewController: NavigationViewController, mapViewUserAnchorPoint mapView: NavigationMapView) -> CGPoint } /** @@ -238,11 +243,11 @@ public class NavigationViewController: NavigationPulleyViewController, RouteMapV } /** - Provides access to the navigation's `MGLMapView` with all its styling capabilities. + The main map view displayed inside the view controller. - Note that you should not change the `mapView`'s delegate. + - note: Do not change this map view’s delegate. */ - public var mapView: MGLMapView? { + public var mapView: NavigationMapView? { get { return mapViewController?.mapView } @@ -253,11 +258,7 @@ public class NavigationViewController: NavigationPulleyViewController, RouteMapV By default, this property is set to `true`, causing the user location annotation to be snapped to the route. */ - public var snapsUserLocationAnnotationToRoute = true { - didSet { - mapViewController?.snapsUserLocationAnnotationToRoute = snapsUserLocationAnnotationToRoute - } - } + public var snapsUserLocationAnnotationToRoute = true /** Toggles sending of UILocalNotification upon upcoming steps when application is in the background. Defaults to `true`. @@ -578,6 +579,10 @@ public class NavigationViewController: NavigationPulleyViewController, RouteMapV func mapViewController(_ mapViewController: RouteMapViewController, didSend feedbackId: String, feedbackType: FeedbackType) { navigationDelegate?.navigationViewController?(self, didSend: feedbackId, feedbackType: feedbackType) } + + func mapViewController(_ mapViewController: RouteMapViewController, mapViewUserAnchorPoint mapView: NavigationMapView) -> CGPoint? { + return navigationDelegate?.navigationViewController?(self, mapViewUserAnchorPoint: mapView) + } } extension NavigationViewController: RouteControllerDelegate { @@ -609,8 +614,14 @@ extension NavigationViewController: RouteControllerDelegate { } public func routeController(_ routeController: RouteController, didUpdate locations: [CLLocation]) { - mapViewController?.mapView.locationManager(routeController.locationManager, didUpdateLocations: locations) - + if snapsUserLocationAnnotationToRoute, let location = routeController.location ?? locations.last { + mapViewController?.mapView.updateCourseTracking(location: location, animated: true) + mapViewController?.labelCurrentRoad(at: location) + } else if let location = locations.last { + mapViewController?.mapView.updateCourseTracking(location: location, animated: true) + mapViewController?.labelCurrentRoad(at: location) + } + if !(routeController.locationManager is SimulatedLocationManager) { mapViewController?.statusView.hide(delay: 3, animated: true) } diff --git a/MapboxNavigation/PollyVoiceController.swift b/MapboxNavigation/PollyVoiceController.swift index c486c1924ae..ca76e8306a0 100644 --- a/MapboxNavigation/PollyVoiceController.swift +++ b/MapboxNavigation/PollyVoiceController.swift @@ -2,6 +2,7 @@ import Foundation import AWSPolly import AVFoundation import MapboxCoreNavigation +import CoreLocation /** `PollyVoiceController` extends the default `RouteVoiceController` by providing support for AWSPolly. `RouteVoiceController` will be used as a fallback during poor network conditions. diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 99b6011dee0..1e4d329166a 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -22,21 +22,12 @@ class RouteMapViewController: UIViewController { @IBOutlet weak var statusView: StatusView! @IBOutlet weak var laneViewsContainerView: LanesContainerView! - /** - Determines whether the user location annotation is moved from the raw user location reported by the device to the nearest location along the route. - - By default, this property is set to `true`, causing the user location annotation to be snapped to the route. - */ - var snapsUserLocationAnnotationToRoute = true - var routePageViewController: RoutePageViewController! var routeTableViewController: RouteTableViewController? let routeStepFormatter = RouteStepFormatter() var route: Route { return routeController.routeProgress.route } var previousStep: RouteStep? - - var hasFinishedLoadingMap = false var pendingCamera: MGLMapCamera? { guard let parent = parent as? NavigationViewController else { @@ -47,7 +38,7 @@ class RouteMapViewController: UIViewController { var tiltedCamera: MGLMapCamera { get { let camera = mapView.camera - camera.altitude = 600 + camera.altitude = 1000 camera.pitch = 45 return camera } @@ -60,7 +51,25 @@ class RouteMapViewController: UIViewController { weak var routeController: RouteController! let distanceFormatter = DistanceFormatter(approximate: true) var arrowCurrentStep: RouteStep? - var isInOverviewMode = false + var isInOverviewMode = false { + didSet { + if isInOverviewMode { + overviewButton.isHidden = true + recenterButton.isHidden = false + wayNameView.isHidden = true + mapView.logoView.isHidden = true + } else { + overviewButton.isHidden = false + recenterButton.isHidden = true + mapView.logoView.isHidden = false + } + + if let controller = routePageViewController.currentManeuverPage { + controller.step = currentStep + routePageViewController.updateManeuverViewForStep() + } + } + } var currentLegIndexMapped = 0 override func viewDidLoad() { @@ -69,9 +78,10 @@ class RouteMapViewController: UIViewController { distanceFormatter.numberFormatter.locale = .nationalizedCurrent + mapView.tracksUserCourse = true mapView.delegate = self mapView.navigationMapDelegate = self - mapView.manuallyUpdatesLocation = true + mapView.courseTrackingDelegate = self overviewButton.applyDefaultCornerRadiusShadow(cornerRadius: overviewButton.bounds.midX) reportButton.applyDefaultCornerRadiusShadow(cornerRadius: reportButton.bounds.midX) @@ -81,6 +91,7 @@ class RouteMapViewController: UIViewController { wayNameView.applyDefaultCornerRadiusShadow() laneViewsContainerView.isHidden = true statusView.isHidden = true + isInOverviewMode = false resumeNotifications() } @@ -97,33 +108,24 @@ class RouteMapViewController: UIViewController { if let camera = pendingCamera { mapView.camera = camera + } else if let firstCoordinate = route.coordinates?.first { + let location = CLLocation(latitude: firstCoordinate.latitude, longitude: firstCoordinate.longitude) + mapView.updateCourseTracking(location: location, animated: false) } else { - let camera = tiltedCamera - if let coordinates = route.coordinates, coordinates.count > 1 { - camera.centerCoordinate = coordinates.first! - camera.heading = coordinates[0].direction(to: coordinates[1]) - } - mapView.setCamera(camera, animated: false) - } - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - // For some reason, when completing a maneuver this function is called. - // If we try to set the insets/align twice, the UI locks momentarily. - if mapView.userLocationVerticalAlignment != .bottom { - mapView.setUserLocationVerticalAlignment(.bottom, animated: false) - mapView.setContentInset(contentInsets, animated: false) + mapView.setCamera(tiltedCamera, animated: false) } + + mapView.enableFrameByFrameCourseViewTracking(for: 3) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - mapView.setUserTrackingMode(.followWithCourse, animated: false) + mapView.tracksUserCourse = true showRouteIfNeeded() currentLegIndexMapped = routeController.routeProgress.legIndex + mapView.enableFrameByFrameCourseViewTracking(for: 3) } func resumeNotifications() { @@ -137,33 +139,15 @@ class RouteMapViewController: UIViewController { } @IBAction func recenter(_ sender: AnyObject) { - mapView.camera = tiltedCamera - mapView.setUserTrackingMode(.followWithCourse, animated: true) - mapView.logoView.isHidden = false - - guard let controller = routePageViewController.currentManeuverPage else { return } - controller.step = currentStep - routePageViewController.updateManeuverViewForStep() + mapView.tracksUserCourse = true + mapView.enableFrameByFrameCourseViewTracking(for: 3) + isInOverviewMode = false } @IBAction func toggleOverview(_ sender: Any) { - if isInOverviewMode { - overviewButton.isHidden = false - mapView.logoView.isHidden = false - mapView.camera = tiltedCamera - mapView.setUserTrackingMode(.followWithCourse, animated: true) - } else { - wayNameView.isHidden = true - overviewButton.isHidden = true - mapView.logoView.isHidden = true - updateVisibleBounds() - } - - isInOverviewMode = !isInOverviewMode - - guard let controller = routePageViewController.currentManeuverPage else { return } - controller.step = currentStep - routePageViewController.updateManeuverViewForStep() + mapView.enableFrameByFrameCourseViewTracking(for: 3) + updateVisibleBounds() + isInOverviewMode = true } @IBAction func toggleMute(_ sender: UIButton) { @@ -211,14 +195,19 @@ class RouteMapViewController: UIViewController { } } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + mapView.enableFrameByFrameCourseViewTracking(for: 3) + } + func updateVisibleBounds() { - guard let userLocation = self.mapView.userLocation?.coordinate else { return } + guard let userLocation = routeController.locationManager.location?.coordinate else { return } let overviewContentInset = UIEdgeInsets(top: 65, left: 20, bottom: 55, right: 20) let slicedLine = Polyline(routeController.routeProgress.route.coordinates!).sliced(from: userLocation, to: routeController.routeProgress.route.coordinates!.last).coordinates let line = MGLPolyline(coordinates: slicedLine, count: UInt(slicedLine.count)) - mapView.userTrackingMode = .none + mapView.tracksUserCourse = false let camera = mapView.camera camera.pitch = 0 camera.heading = 0 @@ -239,7 +228,7 @@ class RouteMapViewController: UIViewController { if isInOverviewMode { updateVisibleBounds() } else { - mapView.userTrackingMode = .followWithCourse + mapView.tracksUserCourse = true wayNameView.isHidden = true } } @@ -366,6 +355,20 @@ extension RouteMapViewController: PulleyPrimaryContentControllerDelegate { } } +// MARK: NavigationMapViewCourseTrackingDelegate + +extension RouteMapViewController: NavigationMapViewCourseTrackingDelegate { + func navigationMapViewDidStartTrackingCourse(_ mapView: NavigationMapView) { + recenterButton.isHidden = true + mapView.logoView.isHidden = false + } + + func navigationMapViewDidStopTrackingCourse(_ mapView: NavigationMapView) { + recenterButton.isHidden = false + mapView.logoView.isHidden = true + } +} + // MARK: NavigationMapViewDelegate extension RouteMapViewController: NavigationMapViewDelegate { @@ -406,14 +409,8 @@ extension RouteMapViewController: NavigationMapViewDelegate { return delegate?.navigationMapView(mapView, viewFor: annotation) } - func mapViewDidFinishLoadingMap(_ mapView: MGLMapView) { - hasFinishedLoadingMap = true - } - - func navigationMapView(_ mapView: NavigationMapView, shouldUpdateTo location: CLLocation) -> CLLocation? { - let snappedLocation = routeController.location - labelCurrentRoad(at: snappedLocation ?? location) - return snapsUserLocationAnnotationToRoute ? snappedLocation : nil + func navigationMapViewUserAnchorPoint(_ mapView: NavigationMapView) -> CGPoint { + return delegate?.mapViewController(self, mapViewUserAnchorPoint: mapView) ?? .zero } /** @@ -424,7 +421,7 @@ extension RouteMapViewController: NavigationMapViewDelegate { func labelCurrentRoad(at location: CLLocation) { guard let style = mapView.style, let stepCoordinates = routeController.routeProgress.currentLegProgress.currentStep.coordinates, - recenterButton.isHidden && hasFinishedLoadingMap else { + recenterButton.isHidden else { return } @@ -506,31 +503,12 @@ extension RouteMapViewController: NavigationMapViewDelegate { // MARK: MGLMapViewDelegate extension RouteMapViewController: MGLMapViewDelegate { - func mapView(_ mapView: MGLMapView, didChange mode: MGLUserTrackingMode, animated: Bool) { - if isInOverviewMode && mode != .followWithCourse { - recenterButton.isHidden = false - mapView.logoView.isHidden = true - wayNameView.isHidden = true - } else { - if mode != .followWithCourse { - recenterButton.isHidden = false - mapView.logoView.isHidden = true - } else { - recenterButton.isHidden = true - mapView.logoView.isHidden = false - } - } - - if isInOverviewMode { - overviewButton.isHidden = false - recenterButton.isHidden = true - mapView.logoView.isHidden = false - isInOverviewMode = false - } - } - func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) { - if mapView.userTrackingMode == .none && !isInOverviewMode { + var userTrackingMode = mapView.userTrackingMode + if let mapView = mapView as? NavigationMapView, mapView.tracksUserCourse { + userTrackingMode = .followWithCourse + } + if userTrackingMode == .none && !isInOverviewMode { wayNameView.isHidden = true } } @@ -564,14 +542,15 @@ extension RouteMapViewController: RoutePageViewControllerDelegate { updateLaneViews(step: step, alertLevel: .high) - if !isInOverviewMode { if didSwipe, step != routeController.routeProgress.currentLegProgress.upComingStep { + mapView.enableFrameByFrameCourseViewTracking(for: 1) + mapView.tracksUserCourse = false mapView.setCenter(step.maneuverLocation, zoomLevel: mapView.zoomLevel, direction: step.initialHeading!, animated: true, completionHandler: nil) - } else if mapView.userTrackingMode != .followWithCourse { - view.layoutIfNeeded() - mapView.camera = tiltedCamera - mapView.setUserTrackingMode(.followWithCourse, animated: true) + } + + if didSwipe, step == routeController.routeProgress.currentLegProgress.upComingStep { + mapView.tracksUserCourse = true } } @@ -626,4 +605,6 @@ protocol RouteMapViewControllerDelegate: class { func mapViewControllerDidOpenFeedback(_ mapViewController: RouteMapViewController) func mapViewControllerDidCancelFeedback(_ mapViewController: RouteMapViewController) func mapViewController(_ mapViewController: RouteMapViewController, didSend feedbackId: String, feedbackType: FeedbackType) + + func mapViewController(_ mapViewController: RouteMapViewController, mapViewUserAnchorPoint mapView: NavigationMapView) -> CGPoint? } diff --git a/MapboxNavigation/UserCourseView.swift b/MapboxNavigation/UserCourseView.swift new file mode 100644 index 00000000000..eb6a126892c --- /dev/null +++ b/MapboxNavigation/UserCourseView.swift @@ -0,0 +1,194 @@ +import UIKit +import Turf +import Mapbox + +let PuckSize: CGFloat = 45 +let ArrowSize = PuckSize * 0.6 + +// TODO: Remove when toRadians() is exposed in Turf +extension CLLocationDegrees { + func toRadians() -> LocationRadians { + return self * .pi / 180.0 + } +} + +/** + A view that represents the user’s location and course on a `NavigationMapView`. + */ +@objc(MBUserCourseView) +public protocol UserCourseView { + /** + Updates the view to reflect the given location and other camera properties. + */ + func update(location: CLLocation, pitch: CGFloat, direction: CLLocationDegrees, animated: Bool, tracksUserCourse: Bool) +} + +/** + A view representing the user’s location on screen. + */ +public class UserPuckCourseView: UIView, UserCourseView { + + // Sets the color on the user puck + public dynamic var puckColor: UIColor = #colorLiteral(red: 0.149, green: 0.239, blue: 0.341, alpha: 1) { + didSet { + puckView.puckColor = puckColor + } + } + + // Sets the fill color on the circle around the user puck + public dynamic var fillColor: UIColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) { + didSet { + puckView.fillColor = fillColor + } + } + + // Sets the shadow color around the user puck + public dynamic var shadowColor: UIColor = #colorLiteral(red: 0.149, green: 0.239, blue: 0.341, alpha: 0.16) { + didSet { + puckView.shadowColor = shadowColor + } + } + + var puckView: UserPuckStyleKitView! + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + func commonInit() { + puckView = UserPuckStyleKitView(frame: bounds) + backgroundColor = .clear + puckView.backgroundColor = .clear + addSubview(puckView) + } + + var location: CLLocation? + + var pitch: CLLocationDegrees = 0 + + var direction: CLLocationDirection = 0 + + public func update(location: CLLocation, pitch: CGFloat, direction: CLLocationDegrees, animated: Bool, tracksUserCourse: Bool) { + self.location = location + self.direction = direction + self.pitch = CLLocationDegrees(pitch) + 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(self.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) + } +} + +class UserPuckStyleKitView: UIView { + + var fillColor: UIColor = UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000) { + didSet { + setNeedsDisplay() + } + } + + var puckColor: UIColor = UIColor(red: 0.149, green: 0.239, blue: 0.341, alpha: 1.000) { + didSet { + setNeedsDisplay() + } + } + + var shadowColor: UIColor = UIColor(red: 0.149, green: 0.239, blue: 0.341, alpha: 0.160) { + didSet { + setNeedsDisplay() + } + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + drawNavigation_puck(fillColor: fillColor, puckColor: puckColor, shadowColor: shadowColor, circleColor: fillColor) + } + + func drawNavigation_puck(fillColor: UIColor = UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000), puckColor: UIColor = UIColor(red: 0.149, green: 0.239, blue: 0.341, alpha: 1.000), shadowColor: UIColor = UIColor(red: 0.149, green: 0.239, blue: 0.341, alpha: 0.160), circleColor: UIColor = UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000)) { + + //// Canvas 2 + //// navigation_pluck + //// Oval 7 + //// path0_fill Drawing + let path0_fillPath = UIBezierPath(ovalIn: CGRect(x: 9, y: 9, width: 57, height: 57)) + fillColor.setFill() + path0_fillPath.fill() + + + //// Group 4 + //// path1_stroke_2x Drawing + let path1_stroke_2xPath = UIBezierPath() + path1_stroke_2xPath.move(to: CGPoint(x: 37.5, y: 75)) + path1_stroke_2xPath.addCurve(to: CGPoint(x: 75, y: 37.5), controlPoint1: CGPoint(x: 58.21, y: 75), controlPoint2: CGPoint(x: 75, y: 58.21)) + path1_stroke_2xPath.addLine(to: CGPoint(x: 57, y: 37.5)) + path1_stroke_2xPath.addCurve(to: CGPoint(x: 37.5, y: 57), controlPoint1: CGPoint(x: 57, y: 48.27), controlPoint2: CGPoint(x: 48.27, y: 57)) + path1_stroke_2xPath.addLine(to: CGPoint(x: 37.5, y: 75)) + path1_stroke_2xPath.close() + path1_stroke_2xPath.move(to: CGPoint(x: 75, y: 37.5)) + path1_stroke_2xPath.addCurve(to: CGPoint(x: 37.5, y: 0), controlPoint1: CGPoint(x: 75, y: 16.79), controlPoint2: CGPoint(x: 58.21, y: 0)) + path1_stroke_2xPath.addLine(to: CGPoint(x: 37.5, y: 18)) + path1_stroke_2xPath.addCurve(to: CGPoint(x: 57, y: 37.5), controlPoint1: CGPoint(x: 48.27, y: 18), controlPoint2: CGPoint(x: 57, y: 26.73)) + path1_stroke_2xPath.addLine(to: CGPoint(x: 75, y: 37.5)) + path1_stroke_2xPath.close() + path1_stroke_2xPath.move(to: CGPoint(x: 37.5, y: 0)) + path1_stroke_2xPath.addCurve(to: CGPoint(x: 0, y: 37.5), controlPoint1: CGPoint(x: 16.79, y: 0), controlPoint2: CGPoint(x: 0, y: 16.79)) + path1_stroke_2xPath.addLine(to: CGPoint(x: 18, y: 37.5)) + path1_stroke_2xPath.addCurve(to: CGPoint(x: 37.5, y: 18), controlPoint1: CGPoint(x: 18, y: 26.73), controlPoint2: CGPoint(x: 26.73, y: 18)) + path1_stroke_2xPath.addLine(to: CGPoint(x: 37.5, y: 0)) + path1_stroke_2xPath.close() + path1_stroke_2xPath.move(to: CGPoint(x: 0, y: 37.5)) + path1_stroke_2xPath.addCurve(to: CGPoint(x: 37.5, y: 75), controlPoint1: CGPoint(x: 0, y: 58.21), controlPoint2: CGPoint(x: 16.79, y: 75)) + path1_stroke_2xPath.addLine(to: CGPoint(x: 37.5, y: 57)) + path1_stroke_2xPath.addCurve(to: CGPoint(x: 18, y: 37.5), controlPoint1: CGPoint(x: 26.73, y: 57), controlPoint2: CGPoint(x: 18, y: 48.27)) + path1_stroke_2xPath.addLine(to: CGPoint(x: 0, y: 37.5)) + path1_stroke_2xPath.close() + shadowColor.setFill() + path1_stroke_2xPath.fill() + + + + + //// path0_fill 2 Drawing + let path0_fill2Path = UIBezierPath(ovalIn: CGRect(x: 9, y: 9, width: 57, height: 57)) + circleColor.setFill() + path0_fill2Path.fill() + + + + + //// Page 1 + //// Fill 1 + //// path3_fill Drawing + let path3_fillPath = UIBezierPath() + path3_fillPath.move(to: CGPoint(x: 39.2, y: 28.46)) + path3_fillPath.addCurve(to: CGPoint(x: 38.02, y: 27.69), controlPoint1: CGPoint(x: 39, y: 27.99), controlPoint2: CGPoint(x: 38.54, y: 27.68)) + path3_fillPath.addCurve(to: CGPoint(x: 36.8, y: 28.49), controlPoint1: CGPoint(x: 37.5, y: 27.7), controlPoint2: CGPoint(x: 37.02, y: 28.01)) + path3_fillPath.addLine(to: CGPoint(x: 27.05, y: 45.83)) + path3_fillPath.addCurve(to: CGPoint(x: 27.28, y: 47.26), controlPoint1: CGPoint(x: 26.83, y: 46.32), controlPoint2: CGPoint(x: 26.92, y: 46.89)) + path3_fillPath.addCurve(to: CGPoint(x: 28.71, y: 47.54), controlPoint1: CGPoint(x: 27.65, y: 47.64), controlPoint2: CGPoint(x: 28.21, y: 47.75)) + path3_fillPath.addLine(to: CGPoint(x: 37.07, y: 44.03)) + path3_fillPath.addCurve(to: CGPoint(x: 38.06, y: 44.02), controlPoint1: CGPoint(x: 37.39, y: 43.89), controlPoint2: CGPoint(x: 37.75, y: 43.89)) + path3_fillPath.addLine(to: CGPoint(x: 46.26, y: 47.34)) + path3_fillPath.addCurve(to: CGPoint(x: 47.71, y: 47.03), controlPoint1: CGPoint(x: 46.75, y: 47.54), controlPoint2: CGPoint(x: 47.32, y: 47.42)) + path3_fillPath.addCurve(to: CGPoint(x: 48, y: 45.59), controlPoint1: CGPoint(x: 48.09, y: 46.64), controlPoint2: CGPoint(x: 48.2, y: 46.07)) + path3_fillPath.addLine(to: CGPoint(x: 39.2, y: 28.46)) + path3_fillPath.close() + path3_fillPath.usesEvenOddFillRule = true + puckColor.setFill() + path3_fillPath.fill() + } +}