Navigation

The HERE SDK enables you to build a comprehensive turn-by-turn navigation experience. With this feature, your app can check the current device location against a calculated route and get navigational instructions just-in-time.

Navigation is supported for all available transport modes. For car and truck routes, the location of the device will be map-matched to the streets, while for pedestrian routes, locations are matched to unpaved dirt roads and other paths that would not be accessible to drivers.

Even without having a route to follow, the HERE SDK supports a specialized tracking mode, which provides information about the current street, the map-matched location and other supporting details such as speed limits. This mode is for drivers only.

Note that the HERE SDK provides no UI assets like, for example, maneuver arrows to indicate visual feedback. Instead, all information that is available along a route is given as simple data types, allowing you to choose your own assets where applicable.

A tailored navigation map view is not yet supported, however, usage of MapScheme.greyDay is recommended.

Voice guidance is provided as maneuver notifications that can be fed as a String into any platform TTS (Text-To-Speech) solution.

Turn-By-Turn Navigation

The basic principle of turn-by-turn navigation is to frequently receive a location including speed and bearing values which is then matched to a street and compared to the desired route. A maneuver instruction is given to let you orientate where you are and where you have to go next.

When leaving the route, you can be notified of the deviation in meters - so you can decide whether to calculate a new route. And finally, a location simulator allows you to test route navigation during the development phase.

Note: Important

Application developers using turn-by-turn navigation are required to thoroughly test their applications in all expected usage scenarios to ensure safe and correct behavior. Application developers are responsible for warning app users of obligations including but not limited to:

  • Do not follow instructions that may lead to an unsafe or illegal situation.
  • Obey all local laws.
  • Be aware that using a mobile phone or some of its features while driving may be prohibited.
  • Always keep hands free to operate the vehicle while driving.
  • The first priority while driving should be road safety.

Use a Navigator to Listen for Guidance Events

Before you can start to navigate to a destination, you need two things:

  • A Route to follow.
  • A location source that periodically tells you where you are.

Unless you have already calculated a route, create one: Getting a Route instance is shown here. If you only want to start the app in tracking mode, you can skip this step.

Another requirement is to provide a class that conforms to the LocationProvider protocol - as navigation is not possible without getting frequent updates on locations. The LocationProvider protocol defines a few methods that allow the HERE SDK to retrieve Location updates. In the next section you can find a possible implementation. Alternatively, you can set an instance of LocationSimulator. Here we choose to use a LocationProviderImplementation. Don't forget to start the provider to send events on new Locations:

locationProvider = LocationProviderImplementation()
locationProvider.start()

Now we can create a new Navigator instance where you can pass the locationProvider instance from above:

do {
    try navigator = Navigator(locationProvider: locationProvider)
} catch let engineInstantiationError {
    fatalError("Failed to initialize navigator. Cause: \(engineInstantiationError)")
}

Make sure to set the route you want to follow (unless you plan to be in tracking mode only):

navigator.route = route

As a next step you may want to set a few delegates to get notified on the route progress, on the current location, the next maneuver to take and on the route deviation:

navigator.navigableLocationDelegate = self
navigator.routeDeviationDelegate = self
navigator.routeProgressDelegate = self

And here we set the conforming methods to fulfil the RouteProgressDelegate, the NavigableLocationDelegate and the RouteDeviationDelegate protocols:

// Conform to RouteProgressDelegate.
// Notifies on the progress along the route including maneuver instructions.
func onRouteProgressUpdated(_ routeProgress: RouteProgress) {
    // [SectionProgress] is guaranteed to be non-empty.
    let distanceToDestination = routeProgress.sectionProgress.last!.remainingDistanceInMeters
    print("Distance to destination in meters: \(distanceToDestination)")
    let trafficDelayAhead = routeProgress.sectionProgress.last!.trafficDelayInSeconds
    print("Traffic delay ahead in seconds: \(trafficDelayAhead)")

    // Contains the progress for the next maneuver ahead and the next-next maneuvers, if any.
    let nextManeuverList = routeProgress.maneuverProgress
    guard let nextManeuverProgress = nextManeuverList.first else {
        print("No next maneuver available.")
        return
    }

    let nextManeuverIndex = nextManeuverProgress.maneuverIndex
    guard let nextManeuver = navigator.getManeuver(index: nextManeuverIndex) else {
        // Should never happen as we retrieved the next maneuver progress above.
        return
    }

    let action = nextManeuver.action
    let nextRoadName = nextManeuver.nextRoadName
    var road = nextRoadName == nil ? nextManeuver.nextRoadNumber : nextRoadName
    if action == ManeuverAction.arrive {
        // We are reaching destination, so there's no next road.
        let currentRoadName = nextManeuver.roadName
        road = currentRoadName == nil ? nextManeuver.roadNumber : currentRoadName
    }

    let logMessage = "'\(String(describing: action))' on \(road ?? "unnamed road") in \(nextManeuverProgress.remainingDistanceInMeters) meters."

    if previousManeuverIndex != nextManeuverIndex {
        // Log only new maneuvers and ignore changes in distance.
        showMessage("New maneuver: " + logMessage)
    }

    previousManeuverIndex = nextManeuverIndex
}

// Conform to NavigableLocationDelegate.
// Notifies on the current map-matched location and other useful information while driving or walking.
func onNavigableLocationUpdated(_ navigableLocation: NavigableLocation) {
    guard let mapMatchedLocation = navigableLocation.mapMatchedLocation else {
        showMessage("This new location could not be map-matched. Using raw location.")
        updateMapView(currentGeoCoordinates: navigableLocation.originalLocation.coordinates,
                      bearingInDegrees: navigableLocation.originalLocation.bearingInDegrees)
        return
    }

    print("Current street: \(String(describing: navigableLocation.streetName))")

    // Get speed limits for drivers.
    if navigableLocation.speedLimitInMetersPerSecond == nil {
        print("Warning: Speed limits unkown, data could not be retrieved.")
    } else if navigableLocation.speedLimitInMetersPerSecond == 0 {
        print("No speed limits on this road! Drive as fast as you feel safe ...")
    } else {
        print("Current speed limit (m/s): \(String(describing: navigableLocation.speedLimitInMetersPerSecond))")
    }

    updateMapView(currentGeoCoordinates: mapMatchedLocation.coordinates,
                  bearingInDegrees: mapMatchedLocation.bearingInDegrees)
}

// Conform to RouteDeviationDelegate.
// Notifies on a possible deviation from the route.
func onRouteDeviation(_ routeDeviation: RouteDeviation) {
    guard let lastLocationOnRoute = routeDeviation.lastLocationOnRoute else {
        print("User was never following the route.")
        return
    }

    var currentGeoCoordinates = routeDeviation.currentLocation.originalLocation.coordinates
    if let currentMapMatchedLocation = routeDeviation.currentLocation.mapMatchedLocation {
        currentGeoCoordinates = currentMapMatchedLocation.coordinates
    }

    var lastGeoCoordinates = lastLocationOnRoute.originalLocation.coordinates
    if let lastMapMatchedLocationOnRoute = lastLocationOnRoute.mapMatchedLocation {
        lastGeoCoordinates = lastMapMatchedLocationOnRoute.coordinates
    }

    let distanceInMeters = currentGeoCoordinates.distance(to: lastGeoCoordinates)
    print("RouteDeviation in meters is \(distanceInMeters)")
}

Inside the RouteProgressListener we can access detailed information on the progress per Section of the passed Route instance. A route may be split into several sections based on the number of waypoints and transport modes. Note that remainingDistanceInMeters and trafficDelayInSeconds are accumulated per section. We check the last item of the SectionProgress list to get the overall remaining distance to the destination and the estimated traffic delay.

Inside the RouteProgressDelegate we can also access the next manuever that lies ahead of us. For this we use the maneuverIndex:

// Contains the progress for the next maneuver ahead and the next-next maneuvers, if any.
let nextManeuverList = routeProgress.maneuverProgress
guard let nextManeuverProgress = nextManeuverList.first else {
    print("No next maneuver available.")
    return
}

let nextManeuverIndex = nextManeuverProgress.maneuverIndex
guard let nextManeuver = navigator.getManeuver(index: nextManeuverIndex) else {
    // Should never happen as we retrieved the next maneuver progress above.
    return
}

The maneuver information taken from navigator can be used to compose a display for a driver to indicate the next action and other useful informaton like the distance until this action takes place. It is recommended to not use this for textual representations, unless it is meant for debug purposes like shown in the example above. Use voice guidance instead (see below).

As the location provided by the device's GPS sensor may be inaccurate, the Navigator internally calculates a map-matched location that is given to us as part of the NavigableLocation object. This location is expected to be on a navigable path like, for example, a street. But it can also be off-track, in case the user has left the road - or if the GPS signal is too poor to find a map-matched location.

It is recommended to use the map-matched location to give the user visual feedback. For example, to update the current map view based on the map-matched location. An example for this can be seen below. Only if the location could not be map-matched, for example, when the user is off-road, it may be useful to fallback to the unmatched originalLocation.

Note that the maneuver instruction text is empty during navigation. It only contains localized instructions when taken from the route instance. The ManeuverAction enum is supposed to be used to show a visual indicator. Consult the API Reference for a full list of supported actions.

Some roads do not have a road name. In such a case, you can try to retrieve the road number instead, for example, when you are on a highway.

It is not required to trigger the above events yourself. Instead the Navigator will react on the provided locations as coming from the LocationProvider implementation.

If you detect a route deviation, you can decide based on distanceInMeters if you want to calculate a new route to the destination. distanceInMeters is the straight-line distance between the expected location on the route and your actual location. If that is considered too far, you only need to set the new route to the navigator instance - and all further events will be based on the new route. Note that previous events in the queue may still be delivered for the old route: If desired, you can attach new listeners after setting the new route to prevent this.

It is possible to feed in new locations either by implementing a platform positioning solution or by setting up a location simulator.

The basic information flow is:

LocationProvider => Location => Navigator => Events

It is the responsibility of the developer to feed in valid locations into the LocationProvider. For each provided location, the navigator will respond with appropriate events that indicate the progress along the route, including maneuvers and a possible deviation from the expected route. The resulting events depend on the accuracy and frequency of the provided location signals.

Below you can find an example on how to handle location updates:

private func updateMapView(currentGeoCoordinates: GeoCoordinates,
                           bearingInDegrees: Double?) {
    var orientation = MapCamera.OrientationUpdate()
    orientation.bearing = bearingInDegrees

    mapView.camera.lookAt(point: currentGeoCoordinates,
                          orientation: orientation,
                          distanceInMeters: ConstantsEnum.DEFAULT_DISTANCE_IN_METERS)
    navigationArrow.coordinates = currentGeoCoordinates
    trackingArrow.coordinates = currentGeoCoordinates
}

Here the main task is to update the map to the new location. Additionally, we rotate the map based on the current bearing. We also update a custom map marker to indicate the user's current location in form of a navigation arrow.

Screenshot: Turn-by-turn navigation example running on a device.

Note that we call updateMapView() from within the NavigableLocationDelegate - as we already have showed above. Each new location event results in a new NavigableLocation that holds a map-matched location calculated out of the original GPS signal that we have fed into the LocationProvider. This map-matched location can then be consumed - like for example, as we have done above in updateMapView().

To stop any ongoing navigation, call navigator.route = nil, reset the above delegates to nil or simply call stop() on the LocationProvider.

For the full source code, please check the corresponding navigation example app.

Implement a Location Provider

A location provider is mandatory to be set for a new Navigator instance. It can feed in location data from any source. Here we show an implementation that allows to switch between native location data from the device and simulated location data for test drives.

The requirement for the Navigator is that the class fulfils the LoctionProvider protocol:

class LocationProviderImplementation : LocationProvider,
                                       LocationDelegate,
                                       PlatformPositioningProviderDelegate {

    // Note: Must be declared as strong reference.
    var delegate: LocationDelegate?

    // Conforming to LocationProvider protocol.
    func start() {
        // Start sending location events.
    }

    // Conforming to LocationProvider protocol.
    func stop() {
        // Stop sending location events.
    }

The delegate member will be used by the navigator instance to hook in by setting a new LocationDelegate instance.

As a source for GPS data, we will use the PlatformPositioningProvider as shown in the Find your Location section.

To deliver events, we need to start our platformPositioningProvider and send events to the delegate. PlatformPositioningProvider uses the PlatformPositioningProviderDelegate protocol to deliver the location events to our class:

// Conforming to LocationProvider protocol.
func start() {
    platformPositioningProvider.startLocating()
}

// Conforming to PlatformPositioningProviderDelegate to receive platform GPS events.
func onLocationUpdated(location: CLLocation) {
    handleLocationUpdate(location: convertLocation(nativeLocation: location))
}

private func handleLocationUpdate(location: Location) {
    delegate?.onLocationUpdated(location)
}

Note that the HERE SDK does not yet offer a comprehensive positioning solution. Therefore, you need to convert any native location information to the required HERE SDK Location type. This type includes bearing and speed information along with the current geographic coordinates and other information that is consumed by the HERE SDK. The more accurate and complete the provided data is, the more precise the overall navigation experience will be. An example for convertLocation() can be found in the Find your Location section.

Set up a Location Simulator

During development, it may be convenient to playback the expected progress on a route for testing purposes. The LocationSimulator provides a continuous location signal that is taken from the original route coordinates. Setting up a location simulator can be done like shown below:

private func createLocationSimulator(route: Route) -> LocationSimulator {
    let locationSimulatorOptions = LocationSimulatorOptions(speedFactor: 10,
                                                            notificationIntervalInMilliseconds: 100)
    let locationSimulator: LocationSimulator

    do {
        try locationSimulator = LocationSimulator(route: route,
                                                  options: locationSimulatorOptions)
    } catch let instantiationError {
        fatalError("Failed to initialize LocationSimulator. Cause: \(instantiationError)")
    }

    locationSimulator.delegate = self
    locationSimulator.start()

    return locationSimulator
}

// Conform to LocationDelegate, which is required to send notifications from LocationSimulator.
func onLocationUpdated(_ location: Location) {
    if isSimulated {
        handleLocationUpdate(location: location)
    }
}

// Conform to LocationDelegate, which is required to send notifications from LocationSimulator.
func onLocationTimeout() {
    if isSimulated {
        delegate?.onLocationTimeout()
    }
}

Again, we call handleLocationUpdate() to forward the simulated location events to the navigator instance. We also forward possible location timeout signals. By setting LocationSimulatorOptions, we can specify, how fast the current location will move. By default, the speed factor is 1.0, which is equal to the average speed a user normally drives or walks along each route segment without taking into account any traffic-related constraints. The default speed may vary based on the road geometry, road condition and other statistical data, but it is never higher than the current speed limit. Values above 1.0 will increase the speed proportionally. If the route does not contain enough coordinates for the specified time interval, additional location events will be interpolated.

Make sure to stop any ongoing simulation before starting a new one:

if let locationSimulator = locationSimulator {
    locationSimulator.stop()
}

locationSimulator = createLocationSimulator(route: route)
locationSimulator!.start()
isSimulated = true;

Below you can see the full implementation of the class. It shows how you can seamlessly switch between simulated and real locations by calling enableRoutePlayback(route:) and enableDevicePositioning():

import CoreLocation
import heresdk
import UIKit

// A class that conforms the HERE SDK's LocationProvider protocol.
// This class is required by the Navigator to receive location updates from either the device or the LocationSimulator.
class LocationProviderImplementation : LocationProvider,
                                       LocationDelegate,
                                       PlatformPositioningProviderDelegate {

    // Conforms to the LocationProvider protocol.
    // Set by the Navigator instance to listen to location updates.
    // Note: Must be declared as strong reference.
    var delegate: LocationDelegate?

    var lastKnownLocation: Location?
    private let platformPositioningProvider: PlatformPositioningProvider
    private var locationSimulator: LocationSimulator?
    private var isSimulated: Bool = false

    // A loop to check for timeouts between location events.
    private lazy var timeoutDisplayLink: CADisplayLink = {
        let displayLink = CADisplayLink(target: self,
                                        selector: #selector(timeoutLoop))
        displayLink.preferredFramesPerSecond = 2
        displayLink.add(to: .current, forMode: .common)
        return displayLink
    }()

    init() {
        platformPositioningProvider = PlatformPositioningProvider()
        platformPositioningProvider.delegate = self
    }

    // Provides location updates based on the given route.
    func enableRoutePlayback(route: Route) {
        if let locationSimulator = locationSimulator {
            locationSimulator.stop()
        }

        locationSimulator = createLocationSimulator(route: route)
        locationSimulator!.start()
        isSimulated = true;
    }

    // Provides location updates based on the device's GPS sensor.
    func enableDevicePositioning() {
        if locationSimulator != nil {
            locationSimulator!.stop()
            locationSimulator = nil
        }

        isSimulated = false;
    }

    // Conforms to the LocationProvider protocol.
    func start() {
        platformPositioningProvider.startLocating()
        timeoutDisplayLink.isPaused = false
    }

    // Conforms to the PlatformPositioningProviderDelegate to receive platform GPS events.
    func onLocationUpdated(location: CLLocation) {
        if !isSimulated {
            handleLocationUpdate(location: convertLocation(nativeLocation: location))
        }
    }

    // Conforms to the LocationProvider protocol.
    func stop() {
        platformPositioningProvider.stopLocating()
        timeoutDisplayLink.isPaused = true
    }

    private func handleLocationUpdate(location: Location) {
        // The GPS location we received from either the platform or the LocationSimulator is forwarded to the Navigator.
        delegate?.onLocationUpdated(location)
        lastKnownLocation = location
    }

    @objc private func timeoutLoop() {
        if isSimulated {
            // LocationSimulator already includes simulated timeout events.
            return
        }

        if let lastKnownLocation = lastKnownLocation {
            let timeIntervalInSeconds = lastKnownLocation.timestamp.timeIntervalSinceNow * -1
            if timeIntervalInSeconds > 2 {
                //If the last location is older than 2 seconds, we forward a timeout event to the Navigator.
                delegate?.onLocationTimeout()
                print("GPS timeout detected: \(timeIntervalInSeconds)")
            }
        }
    }

    // Provides fake GPS signals based on the route geometry.
    // LocationSimulator can also be set directly to the Navigator, but here we want to have the flexibility to
    // switch between real and simulated GPS data.
    private func createLocationSimulator(route: Route) -> LocationSimulator {
        let locationSimulatorOptions = LocationSimulatorOptions(speedFactor: 10,
                                                                notificationIntervalInMilliseconds: 100)
        let locationSimulator: LocationSimulator

        do {
            try locationSimulator = LocationSimulator(route: route,
                                                      options: locationSimulatorOptions)
        } catch let instantiationError {
            fatalError("Failed to initialize LocationSimulator. Cause: \(instantiationError)")
        }

        locationSimulator.delegate = self
        locationSimulator.start()

        return locationSimulator
    }

    // Conforms to the LocationDelegate, which is required to send notifications from the LocationSimulator.
    func onLocationUpdated(_ location: Location) {
        if isSimulated {
            handleLocationUpdate(location: location)
        }
    }

    // Conforms to the LocationDelegate, which is required to send notifications from the LocationSimulator.
    func onLocationTimeout() {
        if isSimulated {
            delegate?.onLocationTimeout()
        }
    }

    // Converts platform CLLocation to the HERE SDK Location.
    private func convertLocation(nativeLocation: CLLocation) -> Location {
        let geoCoordinates = GeoCoordinates(latitude: nativeLocation.coordinate.latitude,
                                            longitude: nativeLocation.coordinate.longitude,
                                            altitude: nativeLocation.altitude)
        var location = Location(coordinates: geoCoordinates,
                                timestamp: nativeLocation.timestamp)
        location.bearingInDegrees = nativeLocation.course
        location.speedInMetersPerSecond = nativeLocation.speed
        location.horizontalAccuracyInMeters = nativeLocation.horizontalAccuracy
        location.verticalAccuracyInMeters = nativeLocation.verticalAccuracy

        return location
    }
}

Additionally, we added a timeoutDisplayLink to poll the timestamp of the lastKnownLocation. If the last location update is older than 2 seconds, we fire a timeout event. This is needed by the Navigator to evaluate, for example, if the user is driving through a tunnel or if the signal is simply lost:

let timeIntervalInSeconds = lastKnownLocation.timestamp.timeIntervalSinceNow * -1
if timeIntervalInSeconds > 2 {
    delegate?.onLocationTimeout()
}

Note that the LocationSimulator class already conforms to the LocationProvider protocol. That means that you can pass an instance of this class directly to a Navigator instance - this can be useful, if you only plan to simulate locations without a need to switch to real GPS location data.

Voice Guidance

While driving, the user's attention should stay focused on the route. You can construct visual representations from the provided maneuver data (see above), but you can also get localized textual representations that are meant to be spoken during turn-by-turn guidance. Since these maneuver notifications are provided as a String, it is possible to use them together with any TTS solution.

A few example notifications:

Voice message: After 1 kilometer turn left onto North Blaney Avenue.
Voice message: Now turn left.
Voice message: After 1 kilometer turn right onto Forest Avenue.
Voice message: Now turn right.
Voice message: After 400 meters turn right onto Park Avenue.
Voice message: Now turn right.

To get these notifications, set up a ManeuverNotificationDelegate:

navigator.maneuverNotificationDelegate = self

...

// Conform to ManeuverNotificationDelegate.
// Notifies on voice maneuver messages.
func onManeuverNotification(_ text: String) {
    voiceAssistant.speak(message: text)
}

Here we use a helper class called VoiceAssistant that wraps a Text-To-Speech engine to speak the maneuver notification. The engine uses Apple's AVSpeechSynthesizer class. If you are interested, you can find this class as part of the Navigation example app on GitHub.

You can also set a LanguageCode to localize the notification text and a UnitSystem to decide on metric or imperial length units:

private func setupVoiceGuidance() {
    let ttsLanguageCode = getLanguageCodeForDevice(supportedVoiceSkins: navigator.supportedLanguages())
    navigator.maneuverNotificationOptions = ManeuverNotificationOptions(language: ttsLanguageCode,
                                                                        unitSystem: UnitSystem.metric)

    // Set language to our TextToSpeech engine.
    let locale = LanguageCodeConverter.getLocale(languageCode: ttsLanguageCode)
    if voiceAssistant.setLanguage(locale: locale) {
        print("TextToSpeech engine uses this language: \(locale)")
    } else {
        print("TextToSpeech engine does not support this language: \(locale)")
    }
}

For this example, we take the device's preferred language settings. One possible way to get these is shown below:

private func getLanguageCodeForDevice(supportedVoiceSkins: [heresdk.LanguageCode]) -> LanguageCode {

    // 1. Determine if preferred device language is supported by our TextToSpeech engine.
    let identifierForCurrenDevice = Locale.preferredLanguages.first!
    var localeForCurrenDevice = Locale(identifier: identifierForCurrenDevice)
    if !voiceAssistant.isLanguageAvailable(identifier: identifierForCurrenDevice) {
        print("TextToSpeech engine does not support: \(identifierForCurrenDevice), falling back to en-US.")
        localeForCurrenDevice = Locale(identifier: "en-US")
    }

    // 2. Determine supported voice skins from HERE SDK.
    var languageCodeForCurrenDevice = LanguageCodeConverter.getLanguageCode(locale: localeForCurrenDevice)
    if !supportedVoiceSkins.contains(languageCodeForCurrenDevice) {
        print("No voice skins available for \(languageCodeForCurrenDevice), falling back to enUs.")
        languageCodeForCurrenDevice = LanguageCode.enUs
    }

    return languageCodeForCurrenDevice
}

Note that the HERE SDK supports 39 languages. You can query the languages from the Navigator with navigator.supportedLanguages(). All languages within the HERE SDK are specified as LanguageCode enum. To convert this to a Locale instance, you can use a LanguageCodeConverter. This is an open source utility class you find as part of the Navigation example app on GitHub.

Each of the supported languages to generate maneuver notifications is stored as a voice skin inside the HERE SDK framework. Unzip the framework and look for the folder voice_assets. You can manually remove assets you are not interested in to decrease the size of the HERE SDK package.

However, in order to fed the maneuver notification into a TTS engine, you also need to ensure that your preferred language is supported by the TTS engine. Usually each device comes with some preinstalled languages, but not all languages may be present initially.

Tracking

While you can use the Navigator class to start and stop turn-by-turn navigation, it is also possible to switch to a tracking mode that does not require a route to follow. This mode is also often referred to as the driver's assistance mode. It is available for car and truck transport modes.

To enable tracking, all you need is to call:

navigator.route = nil
locationProvider.enableDevicePositioning()

Here we enable getting real GPS locations, but you could also play back locations from any route using the LocationSimulator (as shown above).

Of course, it is possible to initialize the Navigator without setting a route instance - if you are only interested in tracking mode you don't need to set the route explicitly to nil. Note that in tracking mode you only get events for the NavigableLocationDelegate. All other delegates will simply not deliver events when a route is not set. This enables you to keep your delegates alive and to switch between free tracking and turn-by-turn-navigation on the fly.

Tracking can be useful, when drivers already know the directions to take, but would like to get additional information such as the current street name or any speed limits along the trip.

results matching ""

    No results matching ""