Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor location snapping #408

Merged
merged 6 commits into from
Jul 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Examples/Swift/CustomViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class CustomViewController: UIViewController, MGLMapViewDelegate, AVSpeechSynthe
let locationManager = simulateLocation ? SimulatedLocationManager(route: userRoute!) : NavigationLocationManager()

routeController = RouteController(along: userRoute!, directions: directions, locationManager: locationManager)
routeController.snapsUserLocationAnnotationToRoute = true

mapView.userLocationVerticalAlignment = .center
mapView.userTrackingMode = .followWithCourse
Expand Down
63 changes: 57 additions & 6 deletions MapboxCoreNavigation/RouteController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,6 @@ open class RouteController: NSObject {
}
}

/**
If true, the user puck is snapped to closest location on the route.
Defaults to false.
*/
public var snapsUserLocationAnnotationToRoute = true

var isRerouting = false
var lastRerouteLocation: CLLocation?

Expand Down Expand Up @@ -231,6 +225,62 @@ open class RouteController: NSObject {
locationManager.stopUpdatingHeading()
}

/**
The most recently received user location.

This is a raw location received from `locationManager`. To obtain an idealized location, use the `snappedLocation` property.
*/
var rawLocation: CLLocation?

/**
The most recently received user location, snapped to the route line.

This property contains a `CLLocation` object located along the route line near the most recently received user location. This property is set to `nil` if the route controller is unable to snap the user’s location to the route line for some reason.
*/
public var location: CLLocation? {
guard let location = rawLocation, userIsOnRoute(location) else { return nil }
guard let stepCoordinates = routeProgress.currentLegProgress.currentStep.coordinates else { return nil }
guard let snappedCoordinate = closestCoordinate(on: stepCoordinates, to: location.coordinate) else { return location }

guard location.course != -1, location.speed >= 0 else {
return location
}

let nearByCoordinates = routeProgress.currentLegProgress.nearbyCoordinates
guard let closest = closestCoordinate(on: nearByCoordinates, to: location.coordinate) else { return location }
let slicedLine = polyline(along: nearByCoordinates, from: closest.coordinate, to: nearByCoordinates.last)
let userDistanceBuffer = location.speed * RouteControllerDeadReckoningTimeInterval

// Get closest point infront of user
guard let pointOneSliced = coordinate(at: userDistanceBuffer, fromStartOf: slicedLine) else { return location }
guard let pointOneClosest = closestCoordinate(on: nearByCoordinates, to: pointOneSliced) else { return location }
guard let pointTwoSliced = coordinate(at: userDistanceBuffer * 2, fromStartOf: slicedLine) else { return location }
guard let pointTwoClosest = closestCoordinate(on: nearByCoordinates, to: pointTwoSliced) else { return location }

// Get direction of these points
let pointOneDirection = closest.coordinate.direction(to: pointOneClosest.coordinate)
let pointTwoDirection = closest.coordinate.direction(to: pointTwoClosest.coordinate)
let wrappedPointOne = wrap(pointOneDirection, min: -180, max: 180)
let wrappedPointTwo = wrap(pointTwoDirection, min: -180, max: 180)
let wrappedCourse = wrap(location.course, min: -180, max: 180)
let relativeAnglepointOne = wrap(wrappedPointOne - wrappedCourse, min: -180, max: 180)
let relativeAnglepointTwo = wrap(wrappedPointTwo - wrappedCourse, min: -180, max: 180)
let averageRelativeAngle = (relativeAnglepointOne + relativeAnglepointTwo) / 2
let absoluteDirection = wrap(wrappedCourse + averageRelativeAngle, min: 0 , max: 360)

guard differenceBetweenAngles(absoluteDirection, location.course) < RouteControllerMaxManipulatedCourseAngle else {
return location
}

let course = averageRelativeAngle <= RouteControllerMaximumAllowedDegreeOffsetForTurnCompletion ? absoluteDirection : location.course

guard snappedCoordinate.distance < RouteControllerUserLocationSnappingDistance else {
return location
}

return CLLocation(coordinate: snappedCoordinate.coordinate, altitude: location.altitude, horizontalAccuracy: location.horizontalAccuracy, verticalAccuracy: location.verticalAccuracy, course: course, speed: location.speed, timestamp: location.timestamp)
}

/**
Send feedback about the current road segment/maneuver to the Mapbox data team.

Expand Down Expand Up @@ -330,6 +380,7 @@ extension RouteController: CLLocationManagerDelegate {
guard let location = locations.last else {
return
}
self.rawLocation = location

delegate?.routeController?(self, didUpdateLocations: [location])

Expand Down
13 changes: 10 additions & 3 deletions MapboxNavigation/NavigationMapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,16 @@ open class NavigationMapView: MGLMapView {

@objc
public protocol NavigationMapViewDelegate: class {
@objc optional func navigationMapView(_ mapView: NavigationMapView, shouldUpdateTo location: CLLocation) -> CLLocation?
@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, routeCasingStyleLayerWithIdentifier identifier: String, source: MGLSource) -> MGLStyleLayer?
@objc optional func navigationMapView(_ mapView: NavigationMapView, shapeDescribing route: Route) -> MGLShape?
@objc optional func navigationMapView(_ mapView: NavigationMapView, simplifiedShapeDescribing route: Route) -> MGLShape?

@objc(navigationMapView:shapeDescribingRoute:)
optional func navigationMapView(_ mapView: NavigationMapView, shapeDescribing route: Route) -> MGLShape?

@objc(navigationMapView:simplifiedShapeDescribingRoute:)
optional func navigationMapView(_ mapView: NavigationMapView, simplifiedShapeDescribing route: Route) -> MGLShape?
}
11 changes: 11 additions & 0 deletions MapboxNavigation/NavigationViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,17 @@ public class NavigationViewController: NavigationPulleyViewController, RouteMapV
}
}

/**
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.
*/
public var snapsUserLocationAnnotationToRoute = true {
didSet {
mapViewController?.snapsUserLocationAnnotationToRoute = snapsUserLocationAnnotationToRoute
}
}

/**
Toggles sending of UILocalNotification upon upcoming steps when application is in the background. Defaults to `true`.
*/
Expand Down
195 changes: 83 additions & 112 deletions MapboxNavigation/RouteMapViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ class RouteMapViewController: UIViewController {
@IBOutlet weak var wayNameLabel: WayNameLabel!
@IBOutlet weak var wayNameView: UIView!

/**
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()
Expand Down Expand Up @@ -281,131 +288,95 @@ extension RouteMapViewController: NavigationMapViewDelegate {
hasFinishedLoadingMap = true
}

@objc(navigationMapView:shouldUpdateTo:)
func navigationMapView(_ mapView: NavigationMapView, shouldUpdateTo location: CLLocation) -> CLLocation? {

guard routeController.userIsOnRoute(location) else { return nil }
guard let stepCoordinates = routeController.routeProgress.currentLegProgress.currentStep.coordinates else { return nil }
guard let snappedCoordinate = closestCoordinate(on: stepCoordinates, to: location.coordinate) else { return location }

// Add current way name to UI
if let style = mapView.style, recenterButton.isHidden && hasFinishedLoadingMap {
let closestCoordinate = snappedCoordinate.coordinate
let roadLabelLayerIdentifier = "roadLabelLayer"
var streetsSources = style.sources.flatMap {
$0 as? MGLVectorSource
}.filter {
$0.isMapboxStreets
}

// Add Mapbox Streets if the map does not already have it
if streetsSources.isEmpty {
let source = MGLVectorSource(identifier: "mapboxStreetsv7", configurationURL: URL(string: "mapbox://mapbox.mapbox-streets-v7")!)
style.addSource(source)
streetsSources.append(source)
}
let snappedLocation = routeController.location
labelCurrentRoad(at: snappedLocation ?? location)
return snapsUserLocationAnnotationToRoute ? snappedLocation : nil
}

/**
Updates the current road name label to reflect the road on which the user is currently traveling.

- parameter location: The user’s current location.
*/
func labelCurrentRoad(at location: CLLocation) {
guard let style = mapView.style,
let stepCoordinates = routeController.routeProgress.currentLegProgress.currentStep.coordinates,
recenterButton.isHidden && hasFinishedLoadingMap else {
return
}

let closestCoordinate = location.coordinate
let roadLabelLayerIdentifier = "roadLabelLayer"
var streetsSources = style.sources.flatMap {
$0 as? MGLVectorSource
}.filter {
$0.isMapboxStreets
}

// Add Mapbox Streets if the map does not already have it
if streetsSources.isEmpty {
let source = MGLVectorSource(identifier: "mapboxStreetsv7", configurationURL: URL(string: "mapbox://mapbox.mapbox-streets-v7")!)
style.addSource(source)
streetsSources.append(source)
}

if let mapboxSteetsSource = streetsSources.first, style.layer(withIdentifier: roadLabelLayerIdentifier) == nil {
let streetLabelLayer = MGLLineStyleLayer(identifier: roadLabelLayerIdentifier, source: mapboxSteetsSource)
streetLabelLayer.sourceLayerIdentifier = "road_label"
streetLabelLayer.lineOpacity = MGLStyleValue(rawValue: 1)
streetLabelLayer.lineWidth = MGLStyleValue(rawValue: 20)
streetLabelLayer.lineColor = MGLStyleValue(rawValue: .white)
style.insertLayer(streetLabelLayer, at: 0)
}

let userPuck = mapView.convert(closestCoordinate, toPointTo: mapView)
let features = mapView.visibleFeatures(at: userPuck, styleLayerIdentifiers: Set([roadLabelLayerIdentifier]))
var smallestLabelDistance = Double.infinity
var currentName: String?

for feature in features {
var allLines: [MGLPolyline] = []

if let mapboxSteetsSource = streetsSources.first, style.layer(withIdentifier: roadLabelLayerIdentifier) == nil {
let streetLabelLayer = MGLLineStyleLayer(identifier: roadLabelLayerIdentifier, source: mapboxSteetsSource)
streetLabelLayer.sourceLayerIdentifier = "road_label"
streetLabelLayer.lineOpacity = MGLStyleValue(rawValue: 1)
streetLabelLayer.lineWidth = MGLStyleValue(rawValue: 20)
streetLabelLayer.lineColor = MGLStyleValue(rawValue: .white)
style.insertLayer(streetLabelLayer, at: 0)
if let line = feature as? MGLPolylineFeature {
allLines.append(line)
} else if let lines = feature as? MGLMultiPolylineFeature {
allLines = lines.polylines
}

let userPuck = mapView.convert(closestCoordinate, toPointTo: mapView)
let features = mapView.visibleFeatures(at: userPuck, styleLayerIdentifiers: Set([roadLabelLayerIdentifier]))
var smallestLabelDistance = Double.infinity
var currentName: String?

for feature in features {
var allLines: [MGLPolyline] = []
for line in allLines {
let featureCoordinates = Array(UnsafeBufferPointer(start: line.coordinates, count: Int(line.pointCount)))
let slicedLine = polyline(along: stepCoordinates, from: closestCoordinate)

if let line = feature as? MGLPolylineFeature {
allLines.append(line)
} else if let lines = feature as? MGLMultiPolylineFeature {
allLines = lines.polylines
}
let lookAheadDistance:CLLocationDistance = 10
guard let pointAheadFeature = coordinate(at: lookAheadDistance, fromStartOf: polyline(along: featureCoordinates, from: closestCoordinate)) else { continue }
guard let pointAheadUser = coordinate(at: lookAheadDistance, fromStartOf: slicedLine) else { continue }
guard let reversedPoint = coordinate(at: lookAheadDistance, fromStartOf: polyline(along: featureCoordinates.reversed(), from: closestCoordinate)) else { continue }

for line in allLines {
let featureCoordinates = Array(UnsafeBufferPointer(start: line.coordinates, count: Int(line.pointCount)))
let slicedLine = polyline(along: stepCoordinates, from: closestCoordinate)

let lookAheadDistance:CLLocationDistance = 10
guard let pointAheadFeature = coordinate(at: lookAheadDistance, fromStartOf: polyline(along: featureCoordinates, from: closestCoordinate)) else { continue }
guard let pointAheadUser = coordinate(at: lookAheadDistance, fromStartOf: slicedLine) else { continue }
guard let reversedPoint = coordinate(at: lookAheadDistance, fromStartOf: polyline(along: featureCoordinates.reversed(), from: closestCoordinate)) else { continue }

let distanceBetweenPointsAhead = pointAheadFeature - pointAheadUser
let distanceBetweenReversedPoint = reversedPoint - pointAheadUser
let minDistanceBetweenPoints = min(distanceBetweenPointsAhead, distanceBetweenReversedPoint)
let distanceBetweenPointsAhead = pointAheadFeature - pointAheadUser
let distanceBetweenReversedPoint = reversedPoint - pointAheadUser
let minDistanceBetweenPoints = min(distanceBetweenPointsAhead, distanceBetweenReversedPoint)

if minDistanceBetweenPoints < smallestLabelDistance {
smallestLabelDistance = minDistanceBetweenPoints

if minDistanceBetweenPoints < smallestLabelDistance {
smallestLabelDistance = minDistanceBetweenPoints

if let line = feature as? MGLPolylineFeature, let name = line.attribute(forKey: "name") as? String {
currentName = name
} else if let line = feature as? MGLMultiPolylineFeature, let name = line.attribute(forKey: "name") as? String {
currentName = name
} else {
currentName = nil
}
if let line = feature as? MGLPolylineFeature, let name = line.attribute(forKey: "name") as? String {
currentName = name
} else if let line = feature as? MGLMultiPolylineFeature, let name = line.attribute(forKey: "name") as? String {
currentName = name
} else {
currentName = nil
}
}
}

if smallestLabelDistance < 5 && currentName != nil {
wayNameLabel.text = currentName
wayNameView.isHidden = false
} else {
wayNameView.isHidden = true
}
}


// Snap user and course to route
guard routeController.snapsUserLocationAnnotationToRoute else {
return location
}

guard location.course != -1, location.speed >= 0 else {
return location
}

let nearByCoordinates = routeController.routeProgress.currentLegProgress.nearbyCoordinates
guard let closest = closestCoordinate(on: nearByCoordinates, to: location.coordinate) else { return location }
let slicedLine = polyline(along: nearByCoordinates, from: closest.coordinate, to: nearByCoordinates.last)
let userDistanceBuffer = location.speed * RouteControllerDeadReckoningTimeInterval

// Get closest point infront of user
guard let pointOneSliced = coordinate(at: userDistanceBuffer, fromStartOf: slicedLine) else { return location }
guard let pointOneClosest = closestCoordinate(on: nearByCoordinates, to: pointOneSliced) else { return location }
guard let pointTwoSliced = coordinate(at: userDistanceBuffer * 2, fromStartOf: slicedLine) else { return location }
guard let pointTwoClosest = closestCoordinate(on: nearByCoordinates, to: pointTwoSliced) else { return location }

// Get direction of these points
let pointOneDirection = closest.coordinate.direction(to: pointOneClosest.coordinate)
let pointTwoDirection = closest.coordinate.direction(to: pointTwoClosest.coordinate)
let wrappedPointOne = wrap(pointOneDirection, min: -180, max: 180)
let wrappedPointTwo = wrap(pointTwoDirection, min: -180, max: 180)
let wrappedCourse = wrap(location.course, min: -180, max: 180)
let relativeAnglepointOne = wrap(wrappedPointOne - wrappedCourse, min: -180, max: 180)
let relativeAnglepointTwo = wrap(wrappedPointTwo - wrappedCourse, min: -180, max: 180)
let averageRelativeAngle = (relativeAnglepointOne + relativeAnglepointTwo) / 2
let absoluteDirection = wrap(wrappedCourse + averageRelativeAngle, min: 0 , max: 360)

guard differenceBetweenAngles(absoluteDirection, location.course) < RouteControllerMaxManipulatedCourseAngle else {
return location
}

let course = averageRelativeAngle <= RouteControllerMaximumAllowedDegreeOffsetForTurnCompletion ? absoluteDirection : location.course

guard snappedCoordinate.distance < RouteControllerUserLocationSnappingDistance else {
return location
if smallestLabelDistance < 5 && currentName != nil {
wayNameLabel.text = currentName
wayNameView.isHidden = false
} else {
wayNameView.isHidden = true
}

return CLLocation(coordinate: snappedCoordinate.coordinate, altitude: location.altitude, horizontalAccuracy: location.horizontalAccuracy, verticalAccuracy: location.verticalAccuracy, course: course, speed: location.speed, timestamp: location.timestamp)
}
}

Expand Down