Start Guidance

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.

Note

Navigation is supported for all available transport modes - except for publicTransit. Public transit routes may lead to unsafe and unexpected results when being used for navigation.

Navigation support for bus routes is currently limited: We expect enhancements for bus specific lane assistance and more precise turn-by-turn instructions for bus navigation in a future release of the HERE SDK.

The transport mode can vary across the Route, for example, if you walk through a park to reach a sightseeing spot, you may need to leave a car. After the route is calculated, the transport mode is attached to each Section of a Route object.

For car, truck, taxi, bus and scooter routes, the location of the device will be map-matched to streets, while for other modes, such as pedestrian routes, locations may be matched - in addition - to unpaved dirt roads and other paths that would not be accessible to drivers. On the other hand, certain roads like highways are not navigable for pedestrians. Bicycle routes can make use of all available paths - except highways.

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

Note that the HERE SDK provides no UI assets for 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.

Note

Tip: Reusable assets for use in your own iOS applications can be found in the MSDKUI open source project from HERE - available on GitHub under this link. More reusable icons can be found in the official HERE Icon Library.

A tailored navigation map view can be optionally rendered with the VisualNavigator. Once startRendering() is called, it will add a preconfigured MapMarker3D instance in form of an arrow to indicate the current direction - and incoming location updates are smoothly interpolated. In addition, the map orientation is changed to the best suitable default values.

The preconfigured MapMarker3D instance can also be customized by setting your own model - or it can be disabled. Internally, the VisualNavigator uses a LocationIndicator instance and thus you can set also a custom LocationIndicator to the VisualNavigator. When this is done, you also need to manually add, remove and update the instance. Similar, as when you already use a LocationIndicator instance on the map view, see the related map items section.

By default, the style of the LocationIndicator will be determined from the transport mode that can be set for the VisualNavigator. If a route is set, then it is taken from the route instance instead. If a custom asset is used, then the style must be switched directly via the LocationIndicator class.

Note

The NavigationCustom example app shows how to switch to a custom LocationIndicator and to a different type when navigation has stopped. It also shows how the navigation perspective can be customized.

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

Note

Full offline support: All navigation features work also without an internet connection when offline map data has been cached, installed or preloaded: Only a few features require an online connection, for example, when using the DynamicRouteEngine to search online for traffic-optimized routes. Unless otherwise noted, all mentioned features below will work offline.

Unlike for other engines, the HERE SDK will automatically try to download online data when guidance reaches regions that have not been cached, installed or preloaded beforehand. And vice versa, it will make use of this offline map data when it is available on a device - even if an online connection is available.

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. This notification can help you 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.
  • Make road safety the first priority while driving.

All code snippets from the below sections are also available on GitHub as part of the Navigation example app. This app shows the code in connection and provides a testable driving experience and best practices such as keeping the screen alive during guidance. However, it does, not cover every aspect of a full-blown production-ready application. For example, the app does not show how to enable getting location updates while an app may operate in background.

If you are interested in getting background location updates, you can check the related section in the Get Locations guide. Note that as long as you provide location updates, all navigation events will seamlessly continue to be delivered - even if the device screen is locked or the map view is paused.

In addition, you can also find a NavigationQuickStart app on GitHub that shows how to start guidance with just a few lines of code. See also the next section.

Get Started

Before we look into the navigation features of the HERE SDK in greater detail, lets first see a short coding example that shows how to start guidance with speakable maneuver instructions and a guidance view:

private func startGuidance(route: Route) {
    do {
        // Without a route set, this starts tracking mode.
        try visualNavigator = VisualNavigator()
    } catch let engineInstantiationError {
        fatalError("Failed to initialize VisualNavigator. Cause: \(engineInstantiationError)")
    }

    // This enables a navigation view including a rendered navigation arrow.
    visualNavigator!.startRendering(mapView: mapView)

    // Hook in one of the many delegates. Here we set up a delegate to get instructions on the maneuvers to take while driving.
    // For more details, please check the "Navigation" example app and the Developer Guide.
    visualNavigator!.maneuverNotificationDelegate = self

    // Set a route to follow. This leaves tracking mode.
    visualNavigator!.route = route

    // VisualNavigator acts as LocationDelegate to receive location updates directly from a location provider.
    // Any progress along the route is a result of getting a new location fed into the VisualNavigator.
    setupLocationSource(locationDelegate: visualNavigator!, route: route)
}

// Conform to ManeuverNotificationDelegate.
func onManeuverNotification(_ text: String) {
    print("ManeuverNotifications: \(text)")
}

private func setupLocationSource(locationDelegate: LocationDelegate, route: Route) {
    do {
        // Provides fake GPS signals based on the route geometry.
        try locationSimulator = LocationSimulator(route: route,
                                                  options: LocationSimulatorOptions())
    } catch let instantiationError {
        fatalError("Failed to initialize LocationSimulator. Cause: \(instantiationError)")
    }

    locationSimulator!.delegate = locationDelegate
    locationSimulator!.start()
}

This code excerpt will start a guidance view and it will print maneuver instructions to the console until you have reached the destination defined in the provided route (for the full code including declarations see the NavigationQuickStart example app.). Note that the maneuver instructions are meant to be spoken to a driver and they may contain strings like "Turn left onto Invalidenstraße in 500 meters.". More detailed maneuver instructions are also available - they are showed in the sections below.

Note that above we are using the simulation feature of the HERE SDK to acquire location updates. Of course, you can also feed real location updates into the VisualNavigator.

The basic principles of any navigation app are:

  1. Create a Route. Without a route to follow you cannot start guidance.
  2. Create a VisualNavigator instance and start rendering (or create a Navigator instance if you want to render the guidance view on your own).
  3. Set a Route to the VisualNavigator.
  4. Fed in location updates into the VisualNavigator. Without location data, no route progress along a route can be detected. This can be simulated like shown above - or you can feed real location updates.

As a quick start, take a look at the NavigationQuickStart example app on GitHub and see how this works in action. If you read on, you can learn more about the many navigation features the HERE SDK has to offer.

Note

When setting a Waypoint you can influence on which side of the road a driver should reach the stop by setting a sideOfStreetHint. If a driver is moving, a bearing value can help to determine the initial direction by setting the headingInDegrees to a Waypoint. This can help to avoid unnecessary u-turns if the next destination lies in the back of a driver. Note that this can also help to optimize routes for pedestrians, for example, to avoid unnecessary street crossings.

Use a Navigator to Listen for Guidance Events

As briefly mentioned above, before you can start to navigate to a destination, you need two things:

  • A Route to follow. The Route must be set to the Navigator or VisualNavigator instance to start navigation.
  • A location source that periodically tells the Navigator or VisualNavigator instance 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.

Note

During turn-by-turn navigation, you will get all Maneuver information from the Navigator or the VisualNavigator instance - synced with your current Location. As long as you navigate, do not take the Manuever data from the Route object directly.

You have two choices to start guidance. Either by using the headless Navigator - or with the help of the VisualNavigator. Both provide the same interfaces, as the Navigator offers a subset of the VisualNavigator, but the VisualNavigator provides visual rendering assistance on top - with features such as smooth interpolation between discrete Location updates.

Another requirement is to provide Location instances - as navigation is not possible without getting frequent updates on the current location. For this you can use a provider implementation that can be found on GitHub.

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

The basic information flow is:

Location Provider => Location => (Visual)Navigator => Events

Note that you can set any Location source as "location provider". Only onLocationUpdated() has to be called on the Navigator or VisualNavigator.

It is the responsibility of the developer to feed in valid locations into the VisualNavigator. For each received location, the VisualNavigator 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.

First off, create a new instance of our reference implementation to acquire locations:

herePositioningProvider = HEREPositioningProvider()

Next, we can create a new VisualNavigator instance and set it as delegate to the HEREPositioningProvider from above. Note that the VisualNavigator class conforms to the LocationDelegate protocol that defines the onLocationUpdated(location:) method to receive locations.

do {
    try visualNavigator = VisualNavigator()
} catch let engineInstantiationError {
    fatalError("Failed to initialize VisualNavigator. Cause: \(engineInstantiationError)")
}

// Now visualNavigator will receive locations from the HEREPositioningProvider.
herePositioningProvider.startLocating(locationDelegate: visualNavigator,
                                      accuracy: .navigation)

In addition, make sure to set the route you want to follow (unless you plan to be in tracking mode only):

visualNavigator.route = route

Note

If you do not plan to use the VisualNavigator's rendering capabilities, you can also use the Navigator class instead. This class uses the same code under the hood and behaves exactly like the VisualNavigator, but it offers no support for rendering a specialized navigation view.

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

visualNavigator.navigableLocationDelegate = self
visualNavigator.routeDeviationDelegate = self
visualNavigator.routeProgressDelegate = self

And here we set the conforming methods to fulfill 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!.trafficDelay
    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 = visualNavigator.getManeuver(index: nextManeuverIndex) else {
        // Should never happen as we retrieved the next maneuver progress above.
        return
    }

    let action = nextManeuver.action
    let roadName = getRoadName(maneuver: nextManeuver)
    let logMessage = "'\(String(describing: action))' on \(roadName) in \(nextManeuverProgress.remainingDistanceInMeters) meters."

    if previousManeuverIndex != nextManeuverIndex {
        // Log only new maneuvers and ignore changes in distance.
        showMessage("New maneuver: " + logMessage)
    } else {
        // A maneuver update contains a different distance to reach the next maneuver.
        showMessage("Maneuver update: " + 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 navigableLocation.mapMatchedLocation != nil else {
        print("The currentNavigableLocation could not be map-matched. Are you off-road?")
        return
    }

    let speed = navigableLocation.originalLocation.speedInMetersPerSecond
    let accuracy = navigableLocation.originalLocation.speedAccuracyInMetersPerSecond
    print("Driving speed: \(String(describing: speed)) plus/minus accuracy of \(String(describing: accuracy)).")
}

// Conform to RouteDeviationDelegate.
// Notifies on a possible deviation from the route.
func onRouteDeviation(_ routeDeviation: RouteDeviation) {
    guard let route = visualNavigator.route else {
        // May happen in rare cases when route was set to nil inbetween.
        return
    }

    // Get current geographic coordinates.
    var currentGeoCoordinates = routeDeviation.currentLocation.originalLocation.coordinates
    if let currentMapMatchedLocation = routeDeviation.currentLocation.mapMatchedLocation {
        currentGeoCoordinates = currentMapMatchedLocation.coordinates
    }

    // Get last geographic coordinates on route.
    var lastGeoCoordinates: GeoCoordinates?
    if let lastLocationOnRoute = routeDeviation.lastLocationOnRoute {
        lastGeoCoordinates = lastLocationOnRoute.originalLocation.coordinates
        if let lastMapMatchedLocationOnRoute = lastLocationOnRoute.mapMatchedLocation {
            lastGeoCoordinates = lastMapMatchedLocationOnRoute.coordinates
        }
    } else {
        print("User was never following the route. So, we take the start of the route instead.")
        lastGeoCoordinates = route.sections.first?.departurePlace.originalCoordinates
    }

    guard let lastGeoCoordinatesOnRoute = lastGeoCoordinates else {
        print("No lastGeoCoordinatesOnRoute found. Should never happen.")
        return
    }

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

    // Now, an application needs to decide if the user has deviated far enough and
    // what should happen next: For example, you can notify the user or simply try to
    // calculate a new route. When you calculate a new route, you can, for example,
    // take the current location as new start and keep the destination - another
    // option could be to calculate a new route back to the lastMapMatchedLocationOnRoute.
    // At least, make sure to not calculate a new route every time you get a RouteDeviation
    // event as the route calculation happens asynchronously and takes also some time to
    // complete.
    // The deviation event is sent any time an off-route location is detected: It may make
    // sense to await around 3 events before deciding on possible actions.   
}

Inside the RouteProgressDelegate 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 trafficDelay are already accumulated per section. We check the last item of the SectionProgress list to get the overall remaining distance to the destination and the overall estimated traffic delay.

Note that the trafficDelay is based upon the time when the Route data was calculated - therefore, the traffic delay is not refreshed during guidance. The value is only updated along the progressed sections based on the initial data. Use the DynamicRoutingEngine to periodically request optimized routes based on the current traffic situation.

Inside the RouteProgressDelegate we can also access the next maneuver 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 = visualNavigator.getManeuver(index: nextManeuverIndex) else {
    // Should never happen as we retrieved the next maneuver progress above.
    return
}

The maneuver information taken from visualNavigator can be used to compose a display for a driver to indicate the next action and other useful information 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).

However, it can be useful to display localized street names or numbers (such as highway numbers), that can be retrieved as follows:

func getRoadName(maneuver: Maneuver) -> String {
    let currentRoadTexts = maneuver.roadTexts
    let nextRoadTexts = maneuver.nextRoadTexts

    let currentRoadName = currentRoadTexts.names.defaultValue()
    let currentRoadNumber = currentRoadTexts.numbersWithDirection.defaultValue()
    let nextRoadName = nextRoadTexts.names.defaultValue()
    let nextRoadNumber = nextRoadTexts.numbersWithDirection.defaultValue()

    var roadName = nextRoadName == nil ? nextRoadNumber : nextRoadName

    // On highways, we want to show the highway number instead of a possible road name,
    // while for inner city and urban areas road names are preferred over road numbers.
    if maneuver.nextRoadType == RoadType.highway {
        roadName = nextRoadNumber == nil ? nextRoadName : nextRoadNumber
    }

    if maneuver.action == ManeuverAction.arrive {
        // We are approaching destination, so there's no next road.
        roadName = currentRoadName == nil ? currentRoadNumber : currentRoadName
    }

    // Nil happens only in rare cases, when also the fallback above is nil.
    return roadName ?? "unnamed road"
}

You can get the default road texts directly via currentRoadTexts.names.defaultValue, like shown above. In most cases, this will be the name of the road as shown on the local signs.

Alternatively, you can get localized texts for the road name based on a list of preferred languages via currentRoadTexts.names.preferredValue(for: [locale]). If no language is available, the default language is returned.

Note

You can use the RoadTextsDelegate to get notified on the current RoadTexts you are driving on, e.g. during tracking mode.

As the location provided by the device's GPS sensor may be inaccurate, the VisualNavigator 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 such as 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. 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. Below we choose to use the rendering capabilities of the VisualNavigator to automatically update the map view.

Note

A maneuver icon as indicated by the ManeuverAction enum is recommended to be shown as a visual indicator during navigation - while the Maneuver instruction text (nextManeuver.text) fits more into a list to preview maneuvers before starting a trip: These localized instructions are descriptive and will be understandable outside of an ongoing guidance context. However, commonly, they can be presented together with the corresponding ManeuverAction icons you can find in the open-source HERE Icon Library. Find more details on this in the Routing section.

In opposition, nextManeuver.roadTexts, nextManeuver.nextRoadTexts and nextManeuver.exitSignTexts are meant to be shown as part of turn-by-turn maneuvers during navigation: They are only non-empty when the Maneuver is taken from Navigator or VisualNavigator. If taken from a Route instance, these attributes are always empty.

Some roads, such as highways, do not have a name. Instead, in such cases, you can try to retrieve the road number. Keep also in mind, that there may be unnamed roads somewhere in the world.

Below table demonstrates the usage of maneuver properties:

Maneuver Properties
RoutingEngine Navigator / VisualNavigator Examples
maneuver.text Provides a non-empty string. Provides a non-empty string. Example output for text: "Turn right onto Detmolder Straße towards A100.".
maneuver.roadTexts Provides empty strings. Provides non-empty strings. Example output for roadTexts.names.defaultValue(): "Stadtring".
maneuver.nextRoadTexts Provides empty strings. Provides non-empty strings. Example output for nextRoadTexts.names.defaultValue(): "Halenseestraße".
maneuver.exitSignTexts Provides empty strings. Provides non-empty strings. Example output for exitSignTexts.defaultValue(): "Hamburg".

Note

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

If you detect a route deviation, you can decide based on distanceInMeters if you want to reroute users to their destination. Note that for a full route recalculation you may want to use the same route parameters. See next section for more details on how to get back to the route.

In the above example, we calculate the distance based on the coordinates contained in RouteDeviation: distanceInMeters. This is the straight-line distance between the expected location on the route and your actual location. If that is considered too far, you can set a newly calculated route to the visualNavigator instance - and all further events will be based on the new route.

Keep in mind, that in a drive guidance scenario, lastLocationOnRoute and mapMatchedLocation can be null. If routeDeviation.lastLocationOnRoute is null, then the user was never following the route - this can happen when the starting position is farther away from the road network. Usually, the Navigator / VisualNavigator will try to match Location updates to a road: If a driver is too far away, the location cannot be matched.

Note

Note that previous events in the queue may still be delivered once for the old route - as the events are delivered asynchronously. If desired, you can attach new delegates after setting the new route to prevent this.

The Navigation example app shows how to detect the deviation.

Listen for Road Sign Events

Along a road you can find many shields. While driving you can receive detailed notifications on these shields by setting a RoadSignWarningDelegate.

The resulting RoadSignWarning event contains information on the shield, including information such as RoadSignType and RoadSignCategory.

By default, the event will be fired with the same distance threshold as for other warners:

  • On highways, the event is fired approximately 2000 meters ahead.
  • On rural roads, the event is fired approximately 1500 meters ahead.
  • In cities, the event is fired approximately 1000 meters ahead.

With RoadSignWarningOptions you can set a filter on which shields you want to get notified.

Some examples of priority road signs.

Note that not all road shields are included. RoadSignType lists all supported types. For example, road signs showing speed limits are excluded, as these shields can be detected with the dedicated SpeedLimitDelegate.

The below code snippet shows a usage example:

private func setupRoadSignWarnings() {
    var roadSignWarningOptions = RoadSignWarningOptions()
    // Set a filter to get only shields relevant for trucks and heavyTrucks.
    roadSignWarningOptions.vehicleTypesFilter = [RoadSignVehicleType.trucks, RoadSignVehicleType.heavyTrucks]
    visualNavigator.roadSignWarningOptions = roadSignWarningOptions
}

...

// Conform to the RoadShieldsWarningDelegate.
// Notifies on road shields as they appear along the road.
func onRoadSignWarningUpdated(_ roadSignWarning: RoadSignWarning) {
    print("Road sign distance (m): \(roadSignWarning.distanceToRoadSignInMeters)")
    print("Road sign type: \(roadSignWarning.type.rawValue)")

    if let signValue = roadSignWarning.signValue {
        // Optional text as it is printed on the local road sign.
        print("Road sign text: " + signValue.text)
    }

    // For more road sign attributes, please check the API Reference.
}

RoadSignWarning events are issued exactly two times:

  • When DistanceType is AHEAD and distanceToRoadSignInMeters is > 0.
  • When DistanceType is PASSED and distanceToRoadSignInMeters is 0.

Note

For positional warners that notify on a singular object along a road, such as a safety camera, a road sign or a realistic view, there is always only one active warning happening at a time: This means that after each ahead event always a passed event will follow to avoid cases where two AHEAD warnings for a single object are active at the same time.

Listen for Toll Stops

Another warner type is the TollStopWarningDelegate that provides events on upcoming toll booths.

The event will be fired with the same distance threshold as for other warners:

  • On highways, the event is fired approximately 2000 meters ahead.
  • On rural roads, the event is fired approximately 1500 meters ahead.
  • In cities, the event is fired approximately 1000 meters ahead.

Like all warners, the event will be issued in tracking mode and during turn-by-turn navigation.

Inside the TollBoothLane class you can find information which lanes at a toll stop are suitable per vehicle type, as well as other information such as the accepted payment methods.

// Conform to TollStopWarningDelegate.
// Notifies on upcoming toll stops. Uses the same notification
// thresholds as other warners and provides events with or without a route to follow.
func onTollStopWarning(_ tollStop: TollStop) {
    let lanes = tollStop.lanes

    // The lane at index 0 is the leftmost lane adjacent to the middle of the road.
    // The lane at the last index is the rightmost lane.
    let laneNumber = 0
    for tollBoothLane in lanes {
        // Log which vehicles types are allowed on this lane that leads to the toll booth.
        logLaneAccess(laneNumber, tollBoothLane.access);
        let tollBooth = tollBoothLane.booth;
        let tollCollectionMethods = tollBooth.tollCollectionMethods
        let paymentMethods = tollBooth.paymentMethods
        // The supported collection methods like ticket or automatic / electronic.
        for collectionMethod in tollCollectionMethods {
            print("This toll stop supports collection via: \(collectionMethod).")
        }
        // The supported payment methods like cash or credit card.
        for paymentMethod in paymentMethods {
            print("This toll stop supports payment via: \(paymentMethod).")
        }
    }
}

func logLaneAccess(_ laneNumber: Int, _ laneAccess: LaneAccess) {
    print("Lane access for lane \(laneNumber).")
    print("Automobiles are allowed on this lane: \(laneAccess.automobiles).")
    print("Buses are allowed on this lane: \(laneAccess.buses).")
    print("Taxis are allowed on this lane: \(laneAccess.taxis).")
    print("Carpools are allowed on this lane: \(laneAccess.carpools).")
    print("Pedestrians are allowed on this lane: \(laneAccess.pedestrians).")
    print("Trucks are allowed on this lane: \(laneAccess.trucks).")
    print("ThroughTraffic is allowed on this lane: \(laneAccess.throughTraffic).")
    print("DeliveryVehicles are allowed on this lane: \(laneAccess.deliveryVehicles).")
    print("EmergencyVehicles are allowed on this lane: \(laneAccess.emergencyVehicles).")
    print("Motorcycles are allowed on this lane: \(laneAccess.motorcycles).")
}

Note that more information, like the exact price to pay and the location of a toll both is available as part of a Route object. Such information may be useful to extract from a route before starting the trip. For example, tapable MapMarker items can be used to indicate the toll stations along a route. During guidance, such detailed information could potentially distract a driver. Therefore, it is recommended to provide such information in advance.

Handle Route Deviations

As we have seen in the above section, the RouteDeviation event can be used to detect when a driver leaves the original route. Note that this can happen accidentally or intentionally, for example, when a driver decides while driving to take another route to the destination - ignoring the previous made choices for a route alternative and route options.

As shown above, you can detect the distance from the current location of the driver to the last known location on the route. Based on that distance, an application may decide whether it's time to calculate an entire new route or to guide the user back to the original route to keep the made choices for an route alternative and route options.

The HERE SDK does not recalculate routes automatically, it only notfies on the deviation distance - therefore any logic on how to get back to the route has to be implemented on app side.

Note

Tip: The RouteDeviation event will be fired for each new location update. To avoid unnecessary handling of the event, it may be advisable to wait for a few seconds to check if the driver is still deviating. If the event is no longer fired, it means that the driver is back on the route. Keep in mind that the route calculation happens asynchronously and that it is an app decision when and how to start a new route calculation. However, a new route can be set at any time during navigation to the Navigator or VisualNavigator instance and the upcoming events will be updated based on the newly set Route instance.

It is worth to mention that there can be also cases where a user is off-road. After a new route has been set, the user may still be off-road - therefore, the user has not been able to follow the route yet: In such a case you would still receive deviation events for the newly set route and routeDeviation.lastLocationOnRoute is null. If the current location of the user is not changing, it may be advisable to not start a new route calculation again.

The HERE SDK offers several APIs to handle route deviations:

  1. Recalculate the entire route with the RoutingEngine with new or updated RouteOptions to provide new route alternatives. If you use the current location of the user as new starting point, make sure to also specify a bearing direction for the first Waypoint.
  2. Use the returnToRoute() method to calculate a new route to reach the originally chosen route alternative. It is available for the online RoutingEngine and the OfflineRouteEngine. Note that a route calculated with the OfflineRouteEngine does no longer include traffic information.
  3. Refresh the old route with routingEngine.refreshRoute() using a new starting point that must lie on the original route and optionally update the route options. Requires a RouteHandle to identify the original route. This option does not provide the path from a deviated location back to the route, so it is not suitable for the deviation use case on its own.
  4. On top, the HERE SDK offers the DynamicRoutingEngine, that allows to periodically request optimized routes based on the current traffic situation. It requires a route that was calculated online as it requires a RouteHandle. This is engine is meant to find better routes while the user is still following the route. Therefore, it may not be the best choice for the deviation use case, although it requires the current location as input.

The 1st and 3rd option are covered in the Routing section. Note that the 3rd option to refresh the original route does not provide the path from a deviated location back to the route. Therefore, it is not covered below. However, an application may choose to use it to substract the travelled portion from the route and let users reach the new starting point on their own.

Based on parameters such as the distance and location of the deviated location an application needs to decide which option to offer to a driver.

However, the general recommendation is to use returnToRoute() when a deviation is detected as it will be the best option to route a user back to the original chosen route alternative - in case your app offers several route alternatives to be selected by a user.

Note

When the provided location from your GPS source is close enough to the route, then no map-matching is done to save resources. Only when the location is farther away from the route, then the HERE SDK will try to map-match the location to a street.

Return to a Route After Deviation

Calculate a route online or offline that returns to the original route with the RoutingEngine or the OfflineRoutingEngine. Use the returnToRoute() method when you want to keep the originally chosen route, but want to help the driver to navigate back to the route as quickly as possible.

Note

returnToRoute() is just one possible option to handle route deviations. See above for alternative options. For example, in some cases, it may be advisable to calculate an entire new route to the user's destination.

As of now, the returnToRoute() feature supports the same transport modes as the engine - you can use both, the OfflineRoutingEngine and the RoutingEngine. When executing the method with the RoutingEngine, only public transit routes are not supported - all other available transport modes for the RoutingEngine are supported.

Note

The returnToRoute() of the OfflineRoutingEngine method requires cached or already downloaded map data. In most cases, the path back to the original route may be already cached while the driver deviated from the route. However, if the deviation is too large, consider to calculate a new route instead.

The route calculation requires the following parameters:

  • The original Route, which is available from the Navigator / VisualNavigator.
  • You will also need to set the part of the route that was already travelled. This information is provided by the RouteDeviation event.
  • The new starting Waypoint, which may be the current map matched location of the driver.

When using the online RoutingEngine, it is required that the original Route contains a RouteHandle - or route calculation results in a NO_ROUTE_HANDLE error. For the OfflineRoutingEngine this is not necessary.

The new starting point can be retrieved from the RouteDeviation event:

// Get current geographic coordinates.
var currentGeoCoordinates = routeDeviation.currentLocation.originalLocation.coordinates
if let currentMapMatchedLocation = routeDeviation.currentLocation.mapMatchedLocation {
    currentGeoCoordinates = currentMapMatchedLocation.coordinates
}

// If too far away, consider to calculate a new route instead.
Waypoint newStartingPoint = Waypoint(currentGeoCoordinates)

With the online RoutingEngine it can happen that a completely new route is calculated - for example, when the user can reach the destination faster than with the previously chosen route alternative. The OfflineRoutingEngine preferrably reuses the non-travelled portion of the route.

In general, the algorithm will try to find the fastest way back to the original route, but it will also respect the distance to the destination. The new route will try to preserve the shape of the original route if possible.

Stopovers that are not already travelled will not be skipped. For pass-through waypoints, there is no guarantee that the new route will take them into consideration at all.

Optionally, you can improve the route calculation by setting the heading direction of a driver:

if currentMapMatchedLocation.bearingInDegrees != nil {
    newStartingPoint.headingInDegrees = currentMapMatchedLocation.bearingInDegrees
}

Finally, we can calculate the new route:

routingEngine.returnToRoute(originalRoute,
                            startingPoint: newStartingPoint,
                            lastTraveledSectionIndex: routeDeviation.lastTraveledSectionIndex,
                            traveledDistanceOnLastSectionInMeters: routeDeviation.traveledDistanceOnLastSectionInMeters) { (routingError, routes) in
        if (routingError == nil) {
            let newRoute = routes?.first
            // ...
        } else {
            // Handle error.
        }
}

Note

Since the CalculateRouteCompletionHandler is reused, a list of routes is provided. However, the list will only contain one route. The error handling follows the same logic as for the RoutingEngine.

As a general guideline for the online and offline usage, the returnToRoute() feature will try to reuse the already calculated portion of the originalRoute that lies ahead. Traffic data is only updated and taken into account when used with the online RoutingEngine.

The resulting new route will also use the same OptimizationMode as found in the originalRoute.

However, for best results, it is recommended to use the online RoutingEngine to get traffic-optimized routes.

Dynamically Find Better Routes

Use the DynamicRoutingEngine to periodically request optimized routes based on the current traffic situation. This engine searches for new routes that are faster (based on ETA) than the current route you are driving on.

The DynamicRoutingEngine requires an online connection and a RouteHandle. When trying to search for a better route offline or when the RouteHandle is not enabled, a routing error is propagated:

// Enable route handle.
var carOptions = CarOptions()
carOptions.routeOptions.enableRouteHandle = true

By setting DynamicRoutingEngineOptions, you can define the minTimeDifference before getting notified on a better route. The minTimeDifference is compared to the remaining ETA of the currently set route. The DynamicRoutingEngineOptions also allow to set a pollInterval to determine how often the engine should search for better routes.

private class func createDynamicRoutingEngine() -> DynamicRoutingEngine {
    // Both, minTimeDifference and minTimeDifferencePercentage, will be checked:
    // When the poll interval is reached, the smaller difference will win.
    let minTimeDifferencePercentage = 0.1
    let minTimeDifferenceInSeconds: TimeInterval = 1
    let pollIntervalInSeconds: TimeInterval = 5 * 60

    let dynamicRoutingOptions =
        DynamicRoutingEngineOptions(minTimeDifferencePercentage: minTimeDifferencePercentage,
                                    minTimeDifference: minTimeDifferenceInSeconds,
                                    pollInterval: pollIntervalInSeconds)

    do {
        // With the dynamic routing engine you can poll the HERE backend services to search for routes with less traffic.
        // This can happen during guidance - or you can periodically update a route that is shown in a route planner.
        //
        // Make sure to call dynamicRoutingEngine.updateCurrentLocation(...) to trigger execution. If this is not called,
        // no events will be delivered even if the next poll interval has been reached.
        return try DynamicRoutingEngine(options: dynamicRoutingOptions)
    } catch let engineInstantiationError {
        fatalError("Failed to initialize DynamicRoutingEngine. Cause: \(engineInstantiationError)")
    }
}

Note that by setting a minTimeDifference of 0, you will get no events. The same applies for minTimeDifferencePercentage. Make sure to set a value >= 0 in order to get events.

When receiving a better route, the difference to the original route is provided in meters and seconds:

private func startDynamicSearchForBetterRoutes(_ route: Route) {
    do {
        // Note that the engine will be internally stopped, if it was started before.
        // Therefore, it is not necessary to stop the engine before starting it again.
        try dynamicRoutingEngine.start(route: route, delegate: self)
    } catch let instantiationError {
        fatalError("Start of DynamicRoutingEngine failed: \(instantiationError). Is the RouteHandle missing?")
    }
}

// Conform to the DynamicRoutingDelegate.
// Notifies on traffic-optimized routes that are considered better than the current route.
func onBetterRouteFound(newRoute: Route,
                        etaDifferenceInSeconds: Int32,
                        distanceDifferenceInMeters: Int32) {
    print("DynamicRoutingEngine: Calculated a new route.")
    print("DynamicRoutingEngine: etaDifferenceInSeconds: \(etaDifferenceInSeconds).")
    print("DynamicRoutingEngine: distanceDifferenceInMeters: \(distanceDifferenceInMeters).")

    // An implementation needs to decide when to switch to the new route based
    // on above criteria.
}

// Conform to the DynamicRoutingDelegate.
func onRoutingError(routingError: RoutingError) {
    print("Error while dynamically searching for a better route: \(routingError).")
}

Based on the provided etaDifferenceInSeconds and distanceDifferenceInMeters in comparison to the current route, an application can decide if the newRoute should be used. If so, it can be set to the Navigator or VisualNavigator at any time.

Note

Note that the DynamicRoutingEngine will not be aware of newly set routes: That means, if you are detecting a route deviation and try to calculate a new route in parallel, for example, by calling routingEngine.returnToRoute(...), then you need to inform the DynamicRoutingEngine after a new route is set to the navigator instance. In order to do so, call stop() and then start(...) on the dynamicRoutingEngine instance to start it again with the new route. This can be done right after the route was set to the navigator - outside of the onBetterRouteFound() callback. The RouteDeviation event allows you to calculate how far a user has deviated from a route (see Return to a Route After Deviation).

For simplicity, the recommended flow to set new routes looks like this:

  1. Decide if a new route should be set.
  2. If yes, stop the DynamicRoutingEngine.
  3. Set the new route: navigator.route = newRoute.
  4. Start the DynamicRoutingEngine with the new route.

Make sure to call these steps outside of the onBetterRouteFound() event: Use a local flag and follow the above steps when receiving a new location update - for example, before calling dynamicRoutingEngine.updateCurrentLocation(..), see below.

Note that passing a new route during guidance can have an impact on the user experience - it is recommended, to inform users about the change. And it is also recommended to let a user define the criteria upfront. For example, not any etaDifferenceInSeconds may justify to follow a new route.

Note

Although the DynamicRoutingEngine can be used to update traffic information and ETA periodically, there is no guarantee that the new route is not different. In addition, the DynamicRoutingEngine informs on distanceDifferenceInMeters - but an unchanged route length does not necessarily mean that the route shape is the same. However, if only the ETA has changed and length is the same, then it is likely that only the ETA got updated due to an updated traffic situation: If it is crucial for you to stay on the original route, you need to compare the coordinates of the route shape - or consider to calculate a new route on your own with routingEngine.refreshRoute(). Calling refreshRoute() will not change the route shape. See Routing section for more details. In opposition, keep in mind that the intended use of the DynamicRoutingEngine is to find better routes and for this it is most often desired to follow a new route shape to bypass any traffic obstacles. Also, a better route can only be found when traffic obstacles are present (or gone) in the route ahead.

Make sure to update the last map-matched location of the driver and set it to the DynamicRoutingEngine as soon as you get it - for example, as part of the RouteProgress or NavigableLocation update. This is important, so that a better route always starts close to the current location of the driver:

dynamicRoutingEngine.updateCurrentLocation(mapMatchedLocation: lastMapMatchedLocation,
                                           sectionIndex: routeProgress.sectionIndex)

The DynamicRoutingEngine requires a map matched location that lies on the route. If the user has deviated from the route, then you will receive a RoutingError.couldNotMatchOrigin.

The lastMapMatchedLocation you can get from the NavigableLocationListener and sectionIndex from the RouteProgressListener. It is recommended to call updateCurrentLocation() when receiving events from the RouteProgressListener.

Note that after reaching the destination, the engine will not be automatically stopped from running. Therefore, it is recommended to call stop() when the engine is no longer needed.

An example implementation for this can be found in the corresponding navigation example app.

Update Traffic on Route

It is crucial to provide updated arrival times (ETA) including traffic delay time and to inform on traffic obstacles ahead of the current route. How to update traffic information on your trip?

There can be two scenarios:

  • Stay on the existing route: In this case, use the RoutingEngine to call refreshRoute() periodically.
  • Find better route alternatives to bypass traffic obstacles: Use the DynamicRouteEngine. If users should follow the new Route, you need to set it to the Navigator or VisualNavigator instance.

You can take the updated ETA, traffic delay and traffic jam information directly from the Route object.

More on these options can be found in the above section.

Note that traffic visualization on the route itself is supported when you render the route polyline on your own. You can find an example for this in the Routing section. This section also explains how to extract traffic information from a Route object.

Note

Alternatively, you can enable the traffic flow layer on the map. With the default settings of the VisualNavigator, the traffic flow lines will still be visible besides the route polyline on any zoom level. For example, the HERE WeGo application uses this approach to visualize the current traffic situation.

Update the Map View using Visual Navigator

You can either react on the location updates yourself or use the VisualNavigator for this.

Typically, during navigation you want to:

  • Follow the current location on the map.
  • Show a location arrow indicating the current direction.
  • Rotate the map towards the current direction.
  • Add other visual assets, for example, maneuver arrows.

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 VisualNavigator. This map-matched location can then be consumed to update the map view.

One caveat is that getting location updates happens in most cases frequently, but nevertheless in discrete steps - this means that between each location may lie a few hundred meters. When updating the camera to the new location, this may cause a little jump.

On the other hand, when using the rendering capabilities of the VisualNavigator, you can benefit from smoothly interpolated movements: Depending on the speed of the driver, the missing coordinates between two location updates are interpolated and the target map location is automatically updated for you.

In addition, the VisualNavigator tilts the map, rotates the map into the heading direction and shows a 3D location arrow and a LocationIndicator. All of this can be activated with one line of code:

visualNavigator.startRendering(mapView: mapView)

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

In addition, you can stop following the current location with:

visualNavigator.cameraBehavior = nil

And enable it again with:

// Alternatively, use DynamicCameraBehavior to auto-zoom the camera during guidance.
visualNavigator.cameraBehavior = FixedCameraBehavior()

By default, camera tracking is enabled. And thus, the map is always centered on the current location. This can be temporarily disabled to allow the user to pan away manually and to interact with the map during navigation or tracking. The 3D location arrow will then keep moving, but the map will not move. Once the camera tracking mode is enabled again, the map will jump to the current location and smoothly follow the location updates again.

To stop any ongoing navigation, call visualNavigator.route = nil, reset the above delegates to nil or simply call stop() on your location provider. More information can be found in the stop navigation section below.

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

Customize the Navigation Experience

The NavigationCustom example app shows how to switch to a custom LocationIndicator and to a different type when navigation has stopped. It also shows how the navigation perspective can be customized. Find the example apps on GitHub.

  • With the CameraBehavior you can customize how the map view will look like during guidance. It allows to set an auto-zoom behavior with the DynamicCameraBehavior or a static tilt and zoom orientation with the FixedCameraBehavior that can be updated programmatically. It allows also other options like changing the principal point. The SpeedBasedCameraBehavior also provides customization options and it is the best choice during tracking mode.
  • With ManeuverNotificationOptions you can specify when TTS voice commands should be forwarded.

If you need more customization options for the map view, consider to use the Navigator instead of the VisualNavigator. With the headless Navigator, you get the same features, but no default or customizable render options - instead, you can render the whole map view on your own - for example, if you want to have bigger route lines or any other visual customization, you can use the general rendering capabilities of the HERE SDK.

When using the Navigator, in order to still render a smooth map experience, you have to take care to update the map view's current target location yourself: A location provider will send new location updates only in discrete steps, which will - even when delivered with a high frequency - lead to a "jumping" map view. Therefore, it is recommended to use the InterpolatedLocationDelegate to get the same smoothened location updates as the VisualNavigator.

Route Eat-Up

By default, the VisualNavigator renders a Route with different colors to visually separate the travelled part behind the current location from the part ahead of the user. This can disabled or customized. By default, the same colors are used as for the HERE WeGo mobile application.

If you want to disable the route eat-up visualization, call:

visualNavigator.isRouteProgressVisible = false

Default VisualNavigatorColors are available for day & night mode. For example, to switch colors depending on the daytime. The default colors can be customized like shown below:

private func customizeVisualNavigatorColors() {
    let routeAheadColor = UIColor.blue
    let routeBehindColor = UIColor.red
    let routeAheadOutlineColor = UIColor.yellow
    let routeBehindOutlineColor = UIColor.gray
    let maneuverArrowColor = UIColor.green

    let visualNavigatorColors = VisualNavigatorColors.dayColors()
    let routeProgressColors = RouteProgressColors(
        ahead: routeAheadColor,
        behind: routeBehindColor,
        outlineAhead: routeAheadOutlineColor,
        outlineBehind: routeBehindOutlineColor
    )

    // Sets the color used to draw maneuver arrows.
    visualNavigatorColors.maneuverArrowColor = maneuverArrowColor
    // Sets route color for a single transport mode. Other modes are kept using defaults.
    visualNavigatorColors.setRouteProgressColors(sectionTransportMode: SectionTransportMode.car, routeProgressColors: routeProgressColors)
    // Sets the adjusted colors for route progress and maneuver arrows based on the day color scheme.
    visualNavigator?.colors = visualNavigatorColors
}

Note that this also allows to change the colors of maneuver arrows that are rendered along to path to indicate the next turns.

Receive Waypoint Events

The VisualNavigator / Navigator classes provide more useful notifications. Below is an example of how to receive notifications on passed waypoints. Note that it is possible to be notified at the destination waypoint in two alternative ways:

  • The first delegate below notifies when the destination is reached - and therefore navigation can be stopped.
  • Whereas the second delegate below shows how to get notified on all types of waypoints including the destination waypoint, but excluding any passThrough waypoints.
// Conform to DestinationReachedDelegate.
// Notifies when the destination of the route is reached.
func onDestinationReached() {
    showMessage("Destination reached.")
    // Guidance has stopped. Now consider to, for example,
    // switch to tracking mode or stop rendering or locating or do anything else that may
    // be useful to support your app flow.
    // If the DynamicRoutingEngine was started before, consider to stop it now.
}

// Conform to MilestoneStatusDelegate.
// Notifies when a waypoint on the route is reached or missed.
func onMilestoneStatusUpdated(milestone: Milestone, status: MilestoneStatus) {
    if milestone.waypointIndex != nil && status == MilestoneStatus.reached {
        print("A user-defined waypoint was reached, index of waypoint: \(String(describing: milestone.waypointIndex))")
        print("Original coordinates: \(String(describing: milestone.originalCoordinates))")
    } else if milestone.waypointIndex != nil && status == MilestoneStatus.missed {
        print("A user-defined waypoint was missed, index of waypoint: \(String(describing: milestone.waypointIndex))")
        print("Original coordinates: \(String(describing: milestone.originalCoordinates))")
    } else if milestone.waypointIndex == nil && status == MilestoneStatus.reached {
        // For example, when transport mode changes due to a ferry a system-defined waypoint may have been added.
        print("A system-defined waypoint was reached, index of waypoint: \(String(describing: milestone.mapMatchedCoordinates))")
    } else if milestone.waypointIndex == nil && status == MilestoneStatus.missed {
        // For example, when transport mode changes due to a ferry a system-defined waypoint may have been added.
        print("A system-defined waypoint was missed, index of waypoint: \(String(describing: milestone.mapMatchedCoordinates))")
    }
}

The onMilestoneStatusUpdated() method provides a Milestone instance that contains the information about the passed or missed waypoints along the route. Note that only stopover waypoints are included. Also, the destination waypoint is included and any other stopover waypoint that was added by a user. In addition, waypoints added by the HERE SDK are included, for example, when there is a need to take a ferry. However, the first waypoint - which is the starting point of your trip - is excluded. Waypoints of type passThrough are also excluded.

A Milestone includes an index that refers to the waypoint list set by the user when calculating the route. If it is not available, then the Milestone refers to a waypoint that was set during the route calculation - for example, when an additional stopover was included by the routing algorithm to indicate that a ferry must be taken.

The MilestoneStatus enum indicates if the corresponding Milestone has been reached or missed.

Receive Speed Limit Events

By implementing the SpeedLimitDelegate you can receive events on the speed limits that are available along a road. These can be the speed limits as indicated on the local signs, as well as warnings on special speed situations, like for example, speed limits that are only valid for specific weather conditions.

Speed limits that are marked as conditional may be time-dependent. For example, speed limits for school zones can be valid only for a specific time of the day. In this case, the HERE SDK compares the device time with the time range of the speed limit. If the speed limit is currently valid, it will be propagated as event, otherwise not.

An implementation example can be found in the Navigation example app you can find on GitHub:

// Conform to SpeedLimitDelegate.
// Notifies on the current speed limit valid on the current road.
func onSpeedLimitUpdated(_ speedLimit: SpeedLimit) {
    let speedLimit = getCurrentSpeedLimit(speedLimit)

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

private func getCurrentSpeedLimit(_ speedLimit: SpeedLimit) -> Double? {
    // Note that all values can be nil if no data is available.

    // The regular speed limit if available. In case of unbounded speed limit, the value is zero.
    print("speedLimitInMetersPerSecond: \(String(describing: speedLimit.speedLimitInMetersPerSecond))")

    // A conditional school zone speed limit as indicated on the local road signs.
    print("schoolZoneSpeedLimitInMetersPerSecond: \(String(describing: speedLimit.schoolZoneSpeedLimitInMetersPerSecond))")

    // A conditional time-dependent speed limit as indicated on the local road signs.
    // It is in effect considering the current local time provided by the device's clock.
    print("timeDependentSpeedLimitInMetersPerSecond: \(String(describing: speedLimit.timeDependentSpeedLimitInMetersPerSecond))")

    // A conditional non-legal speed limit that recommends a lower speed,
    // for example, due to bad road conditions.
    print("advisorySpeedLimitInMetersPerSecond: \(String(describing: speedLimit.advisorySpeedLimitInMetersPerSecond))")

    // A weather-dependent speed limit as indicated on the local road signs.
    // The HERE SDK cannot detect the current weather condition, so a driver must decide
    // based on the situation if this speed limit applies.
    print("fogSpeedLimitInMetersPerSecond: \(String(describing: speedLimit.fogSpeedLimitInMetersPerSecond))")
    print("rainSpeedLimitInMetersPerSecond: \(String(describing: speedLimit.rainSpeedLimitInMetersPerSecond))")
    print("snowSpeedLimitInMetersPerSecond: \(String(describing: speedLimit.snowSpeedLimitInMetersPerSecond))")

    // For convenience, this returns the effective (lowest) speed limit between
    // - speedLimitInMetersPerSecond
    // - schoolZoneSpeedLimitInMetersPerSecond
    // - timeDependentSpeedLimitInMetersPerSecond
    return speedLimit.effectiveSpeedLimitInMetersPerSecond()
}

Note that speed limits depend on the specified transport mode. Currently, the HERE SDK differentiates for cars and trucks based on the legal commercial vehicle regulations per country (CVR). That means, the above SpeedLimit event can indicate a lower speed limit for trucks: For example, on a highway, the speed limit will be at most 80 km/h in Germany - while for cars there may be a speed limit indicated that is 130 km/h or higher. Use map version 32 or higher to get CVR speed limits. On lower map versions trucks will receive the same speed limits as cars. Note that the map version can be updated with the MapUpdater - even if there are no downloaded regions - as navigation will only request the map data of the same version that is currently stored into the map cache. Therefore, keep in mind that this applies to both, online and offline usage.

Info

For trucks, we recommend to also specify the TruckSpecifications inside the RouteOptions. The property grossWeightInKilograms can have an impact on the speed limit for trucks. For most countries this has an impact on the legally allowed speed limit. If no weight is set, only the legally highest allowed speed limits for trucks will be forwarded - as the HERE SDK will then assume the truck's weight is very low. Speed limits for trucks are determined according to the local commercial vehicle regulations (CVR). Note that some countries like Japan regulate this different. However, routes calculated for trucks will not deliver speed limits suitable for cars - for example, if your truck's weight is below 3.5 T, consider to calculate a car route instead.

Attention: For tracking mode, call navigator.trackingTransportProfile(vehicleProfile: vehicleProfile) and set a VehicleProfile with e.g. truck transport mode if you are a truck driver: By default, car is assumed and you will only receive speed limits that are valid for cars - make sure to specify also other vehicle properties like weight according to your vehicle.

Note

For routes in Japan, you can set the special flag isLightTruck via TruckSpecifications. This flag indicates whether the truck is light enough to be classified as a car. The flag should not be used in countries other than Japan. Note that this is a beta release of this feature.

Receive Speed Warning Events

Although you can detect when you exceed speed limits yourself when you receive a new speed limit event (see above), there is a more convenient solution that can help you implement a speed warning feature for your app.

Note

This does not warn when temporary speed limits such as weather-dependent speed limits are exceeded.

onSpeedWarningStatusChanged() will notify as soon as the driver exceeds the current speed limit allowed. And it will also notify as soon as the driver is driving slower again after exceeding the speed limit:

// Conform to SpeedWarningDelegate.
// Notifies when the current speed limit is exceeded.
func onSpeedWarningStatusChanged(_ status: SpeedWarningStatus) {
    if status == SpeedWarningStatus.speedLimitExceeded {
        // Driver is faster than current speed limit (plus an optional offset).
        // Play a notification sound to alert the driver.
        // Note that this may not include temporary special speed limits, see SpeedLimitDelegate.
        AudioServicesPlaySystemSound(SystemSoundID(1016))
    }

    if status == SpeedWarningStatus.speedLimitRestored {
        print("Driver is again slower than current speed limit (plus an optional offset).")
    }
}

Note

Note that onSpeedWarningStatusChanged() does not notify when there is no speed limit data available. This information is only available as part of a NavigableLocation instance.

A SpeedWarningStatus is only delivered once the current speed is exceeded or when it is restored again - for example, when a driver is constantly driving too fast, only one event is fired.

onSpeedWarningStatusChanged() notifies dependent on the current road's speed limits and the driver's speed. This means that you can get speed warning events also in tracking mode independent of a route. And, consequently, you can receive a speedLimitRestored event when the route has changed - after driving slower again.

Optionally, you can define an offset that is added to the speed limit value. You will be notified only when you exceed the speed limit, including the offset. Below, we define two offsets, one for lower and the other for higher speed limits. The boundary is defined by highSpeedBoundaryInMetersPerSecond:

private func setupSpeedWarnings() {
    let speedLimitOffset = SpeedLimitOffset(lowSpeedOffsetInMetersPerSecond: 2,
                                            highSpeedOffsetInMetersPerSecond: 4,
                                            highSpeedBoundaryInMetersPerSecond: 25)
    visualNavigator.speedWarningOptions = SpeedWarningOptions(speedLimitOffset: speedLimitOffset)
}

Here we set the highSpeedBoundaryInMetersPerSecond to 25 m/s: If a speed limit sign is showing a value above 25 m/s, the offset used is highSpeedOffsetInMetersPerSecond. If it is below 25 m/s, the offset used is lowSpeedOffsetInMetersPerSecond.

For the example values used above,

  • if the speed limit on the road is 27 m/s, the (high) speed offset used is 4 m/s. This means we will only receive a warning notification when we are driving above 31 m/s = 27 m/s + 4 m/s. The highSpeedOffsetInMetersPerSecond is used, as the current speed limit is greater than highSpeedBoundaryInMetersPerSecond.

  • if the speed limit on the road is 20 m/s, the (low) speed offset used is 2 m/s. This means we will only receive a warning notification when we are driving above 22 m/s = 20 m/s + 2 m/s. The lowSpeedOffsetInMetersPerSecond is used, as the current speed limit is smaller than highSpeedBoundaryInMetersPerSecond.

You can also set negative offset values as well. This may be useful if you want to make sure you never exceed the speed limit by having a buffer before you reach the limit. Note that you will never get notifications when you drive too slow, for example, slower than a defined offset - unless a previous speed warning has been restored.

Note

Regarding the vehicle specifications, the same rules apply as mentioned above for speed limits.

Receive Safety Cameras Events

You can attach a SafetyCameraWarningDelegate to the Navigator or VisualNavigator to get notfied on SafetyCameraWarning events that inform on cameras that detect the speed of a driver.

For most countries, this includes only permanently installed cameras. The HERE SDK does not inform whether the cameras are currently active - or not.

Getting notifications on safety cameras - also know as "speed cameras" - is not available for all countries, due to the local laws and regulations. Note that for some countries, like in France, precise location information for speed cameras is disallowed by law: Instead, here the notifications can only be given with less accuracy to meet the governmental guidelines. For most countries, however, precise location information is allowed.

As of now, the below listed countries are supported.

Coverage for Safety Cameras

  • United States of America
  • United Kingdom of Great Britain and Northern Ireland
  • United Arab Emirates
  • Turkey
  • Thailand
  • Taiwan
  • Sweden
  • Spain
  • South Africa
  • Slovenia
  • Slovakia
  • Singapore
  • Serbia
  • Saudi Arabia
  • Russian Federation
  • Romania
  • Qatar
  • Portugal
  • Poland
  • Oman
  • Norway
  • Netherlands
  • Mexico
  • Malaysia
  • Macao
  • Luxembourg
  • Lithuania
  • Latvia
  • Kuwait
  • Kazakhstan
  • Italy
  • Israel
  • Isle of Man
  • Iceland
  • Hungary
  • Hong Kong
  • Greece
  • France
  • Finland
  • Estonia
  • Denmark
  • Czechia
  • Cyprus
  • Croatia
  • Chile
  • Canada
  • Bulgaria
  • Brazil
  • Bosnia and Herzegovina
  • Belgium
  • Belarus
  • Bahrain
  • Azerbaijan
  • Austria
  • Argentina
  • Andorra

Get Road Attributes

By implementing the RoadAttributesDelegate you can receive events on the road attributes. The events are fired whenever an attribute changes - while you are traveling on that road.

// Conform to the RoadAttributesDelegate.
// Notifies on the attributes of the current road including usage and physical characteristics.
func onRoadAttributesUpdated(_ roadAttributes: RoadAttributes) {
    // This is called whenever any road attribute has changed.
    // If all attributes are unchanged, no new event is fired.
    // Note that a road can have more than one attribute at the same time.
    print("Received road attributes update.")

    if (roadAttributes.isBridge) {
      // Identifies a structure that allows a road, railway, or walkway to pass over another road, railway,
      // waterway, or valley serving map display and route guidance functionalities.
        print("Road attributes: This is a bridge.")
    }
    if (roadAttributes.isControlledAccess) {
      // Controlled access roads are roads with limited entrances and exits that allow uninterrupted
      // high-speed traffic flow.
        print("Road attributes: This is a controlled access road.")
    }
    if (roadAttributes.isDirtRoad) {
      // Indicates whether the navigable segment is paved.
        print("Road attributes: This is a dirt road.")
    }
    if (roadAttributes.isDividedRoad) {
      // Indicates if there is a physical structure or painted road marking intended to legally prohibit
      // left turns in right-side driving countries, right turns in left-side driving countries,
      // and U-turns at divided intersections or in the middle of divided segments.
        print("Road attributes: This is a divided road.")
    }
    if (roadAttributes.isNoThrough) {
      // Identifies a no through road.
        print("Road attributes: This is a no through road.")
    }
    if (roadAttributes.isPrivate) {
      // Private identifies roads that are not maintained by an organization responsible for maintenance of
      // public roads.
        print("Road attributes: This is a private road.")
    }
    if (roadAttributes.isRamp) {
      // Range is a ramp: connects roads that do not intersect at grade.
        print("Road attributes: This is a ramp.")
    }
    if (roadAttributes.isRightDrivingSide) {
      // Indicates if vehicles have to drive on the right-hand side of the road or the left-hand side.
      // For example, in New York it is always true and in London always false as the United Kingdom is
      // a left-hand driving country.
        print("Road attributes: isRightDrivingSide = \(roadAttributes.isRightDrivingSide)")
    }
    if (roadAttributes.isRoundabout) {
      // Indicates the presence of a roundabout.
        print("Road attributes: This is a roundabout.")
    }
    if (roadAttributes.isTollway) {
      // Identifies a road for which a fee must be paid to use the road.
        print("Road attributes change: This is a road with toll costs.")
    }
    if (roadAttributes.isTunnel) {
      // Identifies an enclosed (on all sides) passageway through or under an obstruction.
        print("Road attributes: This is a tunnel.")
    }
}

An implementation example can be found in the Navigation example app you can find on GitHub.

The HERE SDK itself is not reacting on such events as roadAttributes.isTunnel. An application may decide to switch to a night map scheme as long as isTunnel is true. Internally, the HERE SDK is using a tunnel interpolation algorithm to provide this detection - as usually the GPS signal is very weak or even lost while being in a tunnel.

Get Lane Assistance

The HERE SDK provides lane recommendations to help a driver to stay on the route. When no Route is set, no lane assistance is provided.

Two independent delegates can be set to obtain the following events before reaching a junction (including intersections and roundabouts):

  • ManeuverViewLaneAssistance: Provides a list of Lane recommendations if the next route maneuver takes place at a junction - regardless if the junction is considered complex or not.
  • JunctionViewLaneAssistance: Provides a list of Lane recommendations only for complex junctions - regardless if a maneuver takes place at the junction or not. This event is not delivered for non-complex junctions.

A complex junction is defined as follows:

  • The junction has at least a bifurcation.
  • The junction has at least two lanes whose directions do not follow the current route.

Both events can be delivered for the same junction or for different ones. A Lane instance contains information such as the available lanes on the current road, their direction category and whether the lane is recommended or not.

Both events are fired 300 meters ahead of a junction for non-highways and 1300 meters ahead of a junction on highways. However, for now the distance to the next complex junction is not exposed as part of the JunctionViewLaneAssistance event. For ManeuverViewLaneAssistance, the distance is available as part of the distance to the next maneuver which is available via the RouteProgress event.

Each lane can lead to multiple directions stored in LaneDirectionCategory:

  • straight: A lane that goes straight up.
  • slightlyLeft: A lane that goes slightly left around 45 degrees.
  • slightlyRight: A lane that goes slightly right around 45 degrees.
  • quiteLeft: A lane that goes quite left around 90 degrees.
  • quiteRight: A lane that goes quite right around 90 degrees.
  • hardLeft: A lane that goes hard left around 135 degrees.
  • hardRight: A lane that goes hard right around 135 degrees.
  • uTurnLeft: A lane that makes a left u-turn around 180 degrees.
  • uTurnRight: A lane that makes a right u-turn around 180 degrees.

Note that all members can be true or false at the same time. Theoretically, all members can be true when the lane leads to all multiple directions. Most lanes, however, lead to one or two directions, for example, quiteLeft and quiteRight will be true when the lane splits up into two separate lanes.

To give visual feedback for the driver, it is recommended to create one transparent image asset for each of the nine possible directions. Each image can then be used as an overlay and several images can be blended into one lane pictogram that indicates the possible directions per lane on a road.

Most importantly, while the vehicle is traveling along the route, you can tell the driver which lane to take: This information is stored in the Lane.recommendationState and it is recommended to highlight the pictogram of the recommended lane.

Illustration: Example of a possible visualization for a road with three lanes where the two leftmost roads lead to the next maneuver.

Note that the lane assistance information does not contain the lanes of the contraflow, instead it only describes the lanes of the current driving direction. The list of lanes is always ordered from the leftmost lane (index 0) to the rightmost lane (last index) of the road.

This way, lane assistance works the same for both, left-hand and righ-hand driving countries.

Note

Check roadAttributes.isRightDrivingSide to know if you are in a left-hand driving country. Maneuver instructions and other notifications automatically adapt to the country. For lane assistance, the code will work the same, regardless of the country, as the list of lanes is always ordered from left - starting with index 0 - to right.

It is recommended to show ManeuverViewLaneAssistance events immediately when the event is received. The event is synchronized with the ManeuverNotificationDelegate to receive voice guidance events.

Lane information provided by JunctionViewLaneAssistance events is recommended to be shown in a separate UI area indicating that there is an upcoming complex junction that needs attention.

Get Lane Recommendations For Maneuvers at a Junction with ManeuverViewLaneAssistance

The ManeuverViewLaneAssistance event provides the recommended lanes at a junction where a maneuver takes place. On the map this maneuver is visualized by a maneuver arrow when the VisualNavigator is rendering the MapView. The location of the junction can be retrieved from the next Maneuver that is available as part of the RouteProgress event.

Each ManeuverViewLaneAssistance event is synchronized with the corresponding maneuver voice notification as sent by the ManeuverNotificationDelegate: This means that for most roads, the event arrives simultaneously and at the same frequency as the maneuver voice notification text that describes the next maneuver with the distance to the junction. As described below, this event can be used for a TTS engine to speak the maneuver message to the driver.

Similar to the other events described above, you can attach a ManeuverViewLaneAssistanceDelegate to the Navigator or VisualNavigator. The resulting ManeuverViewLaneAssistance object contains information about the available lanes on the current road and information such as their directions.

The following code snippet shows how to retrieve the information which lanes to take:

// Conform to the ManeuverViewLaneAssistanceDelegate.
// Notifies which lane(s) lead to the next (next) maneuvers.
func onLaneAssistanceUpdated(_ laneAssistance: ManeuverViewLaneAssistance) {
    // This lane list is guaranteed to be non-empty.
    let lanes = laneAssistance.lanesForNextManeuver
    logLaneRecommendations(lanes)

    let nextLanes = laneAssistance.lanesForNextNextManeuver
    if !nextLanes.isEmpty {
        print("Attention, the next next maneuver is very close.")
        print("Please take the following lane(s) after the next maneuver: ")
        logLaneRecommendations(nextLanes)
    }
}

private func logLaneRecommendations(_ lanes: [Lane]) {
    // The lane at index 0 is the leftmost lane adjacent to the middle of the road.
    // The lane at the last index is the rightmost lane.
    var laneNumber = 0
    for lane in lanes {
        // This state is only possible if laneAssistance.lanesForNextNextManeuver is not empty.
        // For example, when two lanes go left, this lanes leads only to the next maneuver,
        // but not to the maneuver after the next maneuver, while the highly recommended lane also leads
        // to this next next maneuver.
        if lane.recommendationState == .recommended {
            print("Lane \(laneNumber) leads to next maneuver, but not to the next next maneuver.")
        }

        // If laneAssistance.lanesForNextNextManeuver is not empty, this lane leads also to the
        // maneuver after the next maneuver.
        if lane.recommendationState == .highlyRecommended {
            print("Lane \(laneNumber) leads to next maneuver and eventually to the next next maneuver.")
        }

        if lane.recommendationState == .notRecommended {
            print("Do not take lane \(laneNumber) to follow the route.")
        }

        logLaneDetails(laneNumber, lane)

        laneNumber += 1
    }
}

func logLaneDetails(_ laneNumber: Int, _ lane: Lane) {
  // All directions can be true or false at the same time.
  // The possible lane directions are valid independent of a route.
  // If a lane leads to multiple directions and is recommended, then all directions lead to
  // the next maneuver.
  // You can use this information like in a bitmask to visualize the possible directions
  // with a set of image overlays.
  let laneDirectionCategory = lane.directionCategory
  print("Directions for lane \(laneNumber):")
  print("laneDirectionCategory.straight: \(laneDirectionCategory.straight)")
  print("laneDirectionCategory.slightlyLeft: \(laneDirectionCategory.slightlyLeft)")
  print("laneDirectionCategory.quiteLeft: \(laneDirectionCategory.quiteLeft)")
  print("laneDirectionCategory.hardLeft: \(laneDirectionCategory.hardLeft)")
  print("laneDirectionCategory.uTurnLeft: \(laneDirectionCategory.uTurnLeft)")
  print("laneDirectionCategory.slightlyRight: \(laneDirectionCategory.slightlyRight)")
  print("laneDirectionCategory.quiteRight: \(laneDirectionCategory.quiteRight)")
  print("laneDirectionCategory.hardRight: \(laneDirectionCategory.hardRight)")
  print("laneDirectionCategory.uTurnRight: \(laneDirectionCategory.uTurnRight)")

  // More information on each lane is available in these bitmasks (boolean):
  // LaneType provides lane properties such as if parking is allowed.
    _ = lane.type
  // LaneAccess provides which vehicle type(s) are allowed to access this lane.
    _ = lane.access
}

The laneAssistance.lanesForNextNextManeuver is normally an empty list, but there may be cases when two maneuvers are very close. In such cases, this list holds the information for the lanes to take immediately after the current maneuver is reached.

Until the next maneuver is reached, the information about the lanes to take is valid. It should be hidden once the next maneuver is reached or replaced by the information contained in any new ManeuverViewLaneAssistance event:

// See above code snippet for the RouteProgressDelegate.
if previousManeuverIndex != nextManeuverIndex {
    // A new maneuver: Remove stale lane assistance info.
}

View the code for the RouteProgressDelegate above and you can find how to get the nextManeuverIndex, which will tell you when a new maneuver has to be taken.

Get Lane Recommendations For Complex Junctions with JunctionViewLaneAssistance

In addition to ManeuverViewLaneAssistance (see above), the HERE SDK provides JunctionViewLaneAssistance events that notify on the available lanes at complex junctions - even if there is no actual maneuver happening at that junction. These notifications work in parallel to ManeuverViewLaneAssistance, but will only fire before reaching a complex junction (see above).

In comparison to ManeuverViewLaneAssistance, the JunctionViewLaneAssistance event can recommend more lanes to safely pass a complex junction - but not every of those lanes may lead to the next maneuver after passing the junction.

Unlike ManeuverViewLaneAssistance, you can detect when the junction has been passed by checking the list if it is empty or not:

// Conform to the JunctionViewLaneAssistanceDelegate.
// Notfies which lane(s) lead to the next maneuvers at complex junctions.
func onLaneAssistanceUpdated(_ laneAssistance: JunctionViewLaneAssistance) {
    let lanes = laneAssistance.lanesForNextJunction        
    if (lanes.isEmpty) {
      print("You have passed the complex junction.")
    } else {
      print("Attention, a complex junction is ahead.")
      logLaneRecommendations(lanes)
    }
}

When the complex junction has been passed, it is recommended to update the UI of your app to remove the lane information. JunctionViewLaneAssistance events can be considered as an additional hint which lanes to take at complex junctions - especially, when no maneuver takes places at such junctions, because this information is not provided with the ManeuverViewLaneAssistance event.

Keep in mind, that without a route to follow, you will not get any lane assistance related events.

Get Realistic Views for Signposts and Junction Views

With the RealisticViewWarningDelegate you can receive SVG string data for signpost shields and complex junction views in 3D. The RealisticViewWarning event contains SVG data for both, signposts and junction views. Note that the warning is only delivered for complex junctions (see above).

private func setupRealisticViewWarnings() {
    let realisticViewWarningOptions = RealisticViewWarningOptions(aspectRatio: AspectRatio.aspectRatio3X4, darkTheme: false)
    visualNavigator.realisticViewWarningOptions = realisticViewWarningOptions

    visualNavigator.realisticViewWarningDelegate = self
}

// Notifies on signposts together with complex junction views.
// Signposts are shown as they appear along a road on a shield to indicate the upcoming directions and
// destinations, such as cities or road names.
// Junction views appear as a 3D visualization (as a static image) to help the driver to orientate.
//
// Optionally, you can use a feature-configuration to preload the assets as part of a Region.
//
// The event matches the notification for complex junctions, see JunctionViewLaneAssistance.
// Note that the SVG data for junction view is composed out of several 3D elements,
// a horizon and the actual junction geometry.
func onRealisticViewWarningUpdated(_ realisticViewWarning: RealisticViewWarning) {
    let distance = realisticViewWarning.distanceToRealisticViewInMeters
    let distanceType: DistanceType = realisticViewWarning.distanceType

    // Note that DistanceType.reached is not used for Signposts and junction views
    // as a junction is identified through a location instead of an area.
    if distanceType == DistanceType.ahead {
        print("A RealisticView ahead in: " + String(distance) + " meters.")
    } else if distanceType == DistanceType.passed {
        print("A RealisticView just passed.")
    }

    let realisticView = realisticViewWarning.realisticView
    guard let signpostSvgImageContent = realisticView?.signpostSvgImageContent,
          let junctionViewSvgImageContent = realisticView?.junctionViewSvgImageContent
    else {
        print("A RealisticView just passed. No SVG data delivered.")
        return
    }

    // The resolution-independent SVG data can now be used in an application to visualize the image.
    // Use a SVG library of your choice to create an SVG image out of the SVG string.
    // Both SVGs contain the same dimension and the signpostSvgImageContent should be shown on top of
    // the junctionViewSvgImageContent.
    // The images can be quite detailed, therefore it is recommended to show them on a secondary display
    // in full size.
    print("signpostSvgImage: \(signpostSvgImageContent)")
    print("junctionViewSvgImage: \(junctionViewSvgImageContent)")
}

The realisticView.signpostSvgImageContent is meant to be overlayed on top of the realisticView.junctionViewSvgImageContent. Both images can be requested in the same aspect ratio. This way, both images will have the same dimensions and can be rendered at the same top-left position.

Screenshot: A junction view overlayed with a signpost image.

Note that the HERE SDK only delivers the SVG as string, so you need to use a 3rd party library to render the SVG string content, such as SvgView. In order to use the correct fonts, the HERE SDK provides a free-to-use font package, see below.

Note

The data for junction views is optimized to occupy only around 2 MB, while the signpost data occupies only a few KB. However, it is recommended to use the available feature-configurations to preload the image data in advance, see our Optimization Guide for more details.

While you can use the 16:9 resolution in landscape format, you can use it also in portrait mode to not cover the full screen: However, since the SVG assets are quite detailed it is recommended to shown them fullscreen on a secondary display.

Note

For positional warners that notify on a singular object along a road, such as a safety camera, a road sign or a realistic view, there is always only one active warning happening at a time: This means that after each ahead event always a passed event will follow to avoid cases where two AHEAD warnings for a single object are active at the same time.

Take a look at the Navigation example app on GitHub for a usage example.

Note

The RealisticView feature is released as a beta release, so there could be a few bugs and unexpected behaviors. Related APIs may change for new releases without a deprecation process.

Integrate a SVG Renderer and HERE Fonts

In order to render the signposts SVGs (see above), we recommend to use the SvgView plugin. In addition, you need the required TTF fonts that are defined in the SVG content as font families. These fonts can be found in the HERE SDK distribution package.

  1. Integrate SVGView according to the instructions of the plugin vendor. Make sure to be okay with the license of the vendor. Use CocoaPods with pod 'SVGView' or integrate the plugin manually.

  2. Add the import SVGView statement to your code and check if the integration of the plugin was successful.

  3. Extract the SignpostFonts.zip archive as found in the HERE SDK distribution package (the one that also contains the binaries for the HERE SDK). Copy the content to your Xcode project's folder. Then, in Xcode, right-click on the left project pane and select "Add files to [your project] ..." and choose the TTF font files you want to include. This will automatically add the font to the target.

  4. Modify the plist file to specify the added fonts:

<key>UIAppFonts</key>
<array>
    <string>FiraGO-Map.ttf</string>
    <string>SignText-Bold.ttf</string>
    <string>SignTextNarrow-Bold.ttf</string>
    <string>SourceHanSansSC-Normal.ttf</string>
</array>
  1. Render the SVG content:
private func showRealisticViews(signpostSvgImageContent: String,
                                junctionViewSvgImageContent: String) {
    var signpostSvgImageContent = signpostSvgImageContent

    // Create a View from the SVG strings with the SVGView library.
    let signpostSvgImage = SVGView(string: signpostSvgImageContent)
    let junctionViewSvgImage = SVGView(string: junctionViewSvgImageContent)

    if (signpostSvgImage.svg == nil || junctionViewSvgImage.svg == nil) {
        print("Unexpectedly, the SVG string could not be parsed.")
        return
    }

    // Define the layout for the realistic view.
    struct SVGUIView: View {
        var junctionViewSvgImage: SVGView?
        var signpostSvgImage: SVGView?
        var width: CGFloat = 0
        var height: CGFloat = 0
        var body: some View {
            ZStack {
                // Show the signpost image on top of the junction view image.
                junctionViewSvgImage
                signpostSvgImage
            }.frame(width: width, height: height)
        }
    }

    // Set the content for the realistic view.
    // We use the hosting view's frame to show the content fullscreen.
    //
    // Attention: In a production-app, be careful to not distract a driver.
    // It is recommended to show this on a secondary display that resets
    // automatically after some time or when the junction was passed.
    var realisticView = SVGUIView()
    realisticView.signpostSvgImage = signpostSvgImage
    realisticView.junctionViewSvgImage = junctionViewSvgImage
    realisticView.width = view.frame.size.width
    realisticView.height = view.frame.size.height

    // The SVGView library supports only iOS 14 and Swift UI:
    // Therefore, here we optionally need to convert the View to a UIView
    // by using a hosting controller.
    let uiView = UIHostingController(rootView: realisticView).view!

    // ... now show the view.
}

As of now, the following TTF fonts are provided by the HERE SDK. They are free-to-use in your own commerical and non-commercial projects. Be sure to check the license file included in the SignpostFonts.zip archive for each font:

  • SourceHanSansSC-Normal.ttf: This font is mainly used in Macao, Taiwan, Hongkong.
  • FiraGO-Map.ttf: This font is mainly used in Israel.
  • SignText-Bold.ttf: This font is mainly used in Vietnam.
  • SignTextNarrow-Bold.ttf: This font is used in all countries, except for the above countries.

Note

If a font family that is specified in the SVG content is not found, then usually the SVG plugin of your choice will render a default font which may not look as expected.

Get Evironmental Zone Warnings

Environmental Zones, also known as Low Emission Zones (LEZ) or Clean Air Zones (CAZ), are designated areas within cities or regions where certain restrictions or regulations are implemented to improve air quality and reduce pollution. These zones aim to discourage or limit the entry of vehicles that emit high levels of pollutants, such as nitrogen dioxide (NO2) and particulate matter (PM).

The specific rules and regulations of environmental zones can vary between different cities and countries. Typically, vehicles that do not meet certain emission standards are either prohibited from entering the zone or required to pay a fee.

Environmental zone designations and their corresponding rules are typically determined by local or regional authorities in collaboration with transportation and environmental agencies.

The HERE SDK notfies on upcoming environmental zones like so:

// Conform to EnvironmentalZoneWarningDelegate.
func onEnvironmentalZoneWarningsUpdated(_ environmentalZonesWarnings: [heresdk.EnvironmentalZoneWarning]) {
    // The list is guaranteed to be non-empty.
    environmentalZonesWarnings.forEach { environmentalZoneWarning in
        let distanceType = environmentalZoneWarning.distanceType
        if distanceType == .ahead {
            print("A EnvironmentalZone ahead in: \(environmentalZoneWarning.distanceInMeters) meters.")
        } else if distanceType == .reached {
            print("A EnvironmentalZone has been reached.")
        } else if distanceType == .passed {
            print("A EnvironmentalZone just passed.")
        }

        // The official name of the environmental zone (example: "Zone basse émission Bruxelles").
        let name = environmentalZoneWarning.name
        // The description of the environmental zone for the default language.
        let description = environmentalZoneWarning.description.defaultValue
        // The environmental zone ID - uniquely identifies the zone in the HERE map data.
        let zoneID = environmentalZoneWarning.zoneId
        // The website of the environmental zone, if available - nil otherwise.
        let websiteUrl = environmentalZoneWarning.websiteUrl
        print("environmentalZoneWarning: description: \(String(describing: description))")
        print("environmentalZoneWarning: name: \(name)")
        print("environmentalZoneWarning: zoneID: \(zoneID)")
        print("environmentalZoneWarning: websiteUrl: \(websiteUrl ?? "nil")")
    }
}

Use EnvironmentalZoneWarningOptions to change the default distance notification thresholds for urban, highway and rural road types. See also below.

Notification Frequency

All events, such as RouteProgress, are sent in response to a location update. When using the LocationEngine, use at least LocationAccuracy.navigation for which the update frequency is as close to one second as possible.

Exceptions from this rule are event-driven notifications such as for RouteDeviation, RoadAttributes, RoadTexts, Milestone, or ManeuverViewLaneAssistance events: After each location update there is a check if such events need to be delivered. Usually, this happens when the attribute changes on the current road.

For some events, the threshold is configurable, such as for maneuver notification texts via ManeuverNotificationTimingOptions.

For warners such as SpeedLimit, SafetyCameraWarning, TollStop, RoadSignWarning or RealisticViewWarning events the notification thresholds (except TruckRestrictionWarning events, see below) are the following:

  • In cities, the AHEAD event is sent 1000 m ahead.
  • On rural roads, the event is sent 1500 m ahead.
  • On highways, the event is sent 2000 m ahead.

    The notification thresholds for TruckRestrictionWarning events:

  • In cities, the AHEAD event is sent 500 m ahead.

  • On rural roads, the event is sent 750 m ahead.
  • On highways, the event is sent 1500 m ahead.

Note

All warners are supported during tracking mode. This means that you will get events for each warning type also when driving freely around - without following a particular route or after a destination has been reached.

In addition, all warners are not specific for a route or the set RouteOptions - unless explicitely mentioned (like for truck speed limits). For example, you may receive TruckRestrictionWarning events while following a pedestrian route - if you wish so. In general, warner events are generated based on the map-matched location fed into the navigator. For example, pedestrian routes are most often map-matched to the same side of the road as for other transport modes ignoring sideways due to the precision of the GPS signal.

Truck Guidance

The HERE SDK supports premium truck routing and guidance with a variety of features. For example, during navigation you can attach a delegate to get notified on truck restrictions ahead, such as narrow tunnels. Other examples of possible restrictions can be bridges that are not high enough to be passed by a bigger truck or roads where the weight of the truck is beyond the permissible weight of the road.

See the following code snippet:

// Conform to the TruckRestrictionsWarningDelegate.
// Notifies truck drivers on road restrictions ahead. Called whenever there is a change.
func onTruckRestrictionsWarningUpdated(_ restrictions: [TruckRestrictionWarning]) {
    // The list is guaranteed to be non-empty.
    for truckRestrictionWarning in restrictions {
        if truckRestrictionWarning.distanceType == DistanceType.ahead {
            print("TruckRestrictionWarning ahead in \(truckRestrictionWarning.distanceInMeters) meters.")
        } else if truckRestrictionWarning.distanceType == DistanceType.reached {
            print("A restriction has been reached.")
        } else if truckRestrictionWarning.distanceType == DistanceType.passed {
            // If not preceded by a "reached"-notification, this restriction was valid only for the passed location.
            print("A restriction was just passed.")
        }

        // One of the following restrictions applies, if more restrictions apply at the same time,
        // they are part of another TruckRestrictionWarning element contained in the list.
        if truckRestrictionWarning.weightRestriction != nil {
            let type = truckRestrictionWarning.weightRestriction!.type
            let value = truckRestrictionWarning.weightRestriction!.valueInKilograms
            print("TruckRestriction for weight (kg): \(type): \(value)")
        } else if truckRestrictionWarning.dimensionRestriction != nil {
            // Can be either a length, width or height restriction of the truck. For example, a height
            // restriction can apply for a tunnel. Other possible restrictions are delivered in
            // separate TruckRestrictionWarning objects contained in the list, if any.
            let type = truckRestrictionWarning.dimensionRestriction!.type
            let value = truckRestrictionWarning.dimensionRestriction!.valueInCentimeters
            print("TruckRestriction for dimension: \(type): \(value)")
        } else {
            print("TruckRestriction: General restriction - no trucks allowed.")
        }
    }
}

The DistanceType.reached notifies when a truck restriction has been reached. The event is followed by passed, when the restriction has been passed. If the restriction has no length, then reached is skipped and only a passed event is sent. Note that the ahead event is always sent first.

Note that a restriction for each distance type is exactly given only one time. If you want to continuously notify a driver with updated distance information, you can do so, by tracking the RouteProgress which includes a frequent distance update to the destination.

If all restrictions are nil, then a general truck restriction applies. The type of the restriction can be also seen from the TruckRestrictionWarningType.

When comparing the restriction warnings with the MapFeatures.vehicleRestrictions layer on the map, note that some restrictions may be valid only for one direction of a road.

Note

When guidance is stopped by setting a nil route or a new route, then any restriction that was announced with an ahead notification, will instantly result in a passed event to clear pending restriction warnings. While following a route - any restriction that lies not on the route is filtered out, but as soon as a driver deviates far enough (more than 15 meters) from a route, then supported restrictions ahead on the current road will lead again to restriction warnings.

The notification thresholds for truck restrictions differ slightly from other warners, find the thresholds listed here.

The TruckRestrictionWarning event is based on the map data of the road network ahead. It delivers restrictions regardless of the currently set TransportMode.

Note

When calculating a route, you can specify TruckOptions including TruckSpecifications. This may have an influence on the resulting Route. However, it does not influence the TruckRestrictionWarning event: Most restrictions found in the map data ahead are forwarded. Therefore, it may make sense for an application to filter out restriction warnings that are not relevant for the current vehicle. Note that this event is also delivering events in tracking mode when there is no route to follow.

More details on truck routing are given in the routing section. For example, there you can find how to calculate a route specifically for trucks. In general, if a route contains the Truck transportation type, it is optimized for trucks.

In addition, you can specify several avoidance options, for example, to exclude certain city areas. All this can be specified before the route gets calculated and passed into the Navigator or VisualNavigator.

Worth to mention are also the following features:

  • You can specify vehicle restrictions such as truck dimensions or if a truck is carrying hazardous goods via TruckOptions that can contain TruckSpecifications and HazardousGood lists. With this information you can shape the truck route. To get notified on upcoming truck restrictions, listen to the TruckRestrictionWarning event as shown above.
  • You can listen for certain RoadAttributes as explained above.
  • When transport mode is set to truck, SpeedLimit events will indicate the commercial vehicle regulated (CVR) speed limits that may be lower than for cars. Consider to specify also the TruckSpecifications inside the RouteOptions when calculating the route. For tracking mode, call navigator.trackingTransportProfile(vehicleProfile: vehicleProfile) and set a VehicleProfile with truck transport mode. By default, for tracking, car is assumed: Make sure to specify other vehicle properties like weight according to your truck.
  • Worth to mention, grossWeightInKilograms and weightInKilograms will effect CVR speed limits, as well as route restrictions and the estimated arrival time. Without setting proper TruckSpecifications, routes and notfifcations may be inapprobiate.
  • You can exclude emission zones to not pollute the air in sensible inner city areas via AvoidanceOptions. With this you can also avoid certain RoadFeatures like tunnels. Those can be set via TruckOptions and are then excluded from route calculation.
  • You can enable a map layer scheme that shows safety camera icons on the map: MapScene.Layers.safetyCameras. Note: This layer is also suitable for cars.
  • You can enable a map layer scheme that is optimized to show truck-specific information on the map: MapScene.Layers.vehicleRestrictions. It offers several MapFeatureModes, for example, to highlight active and inactive restrictions as purple lines on an affected road - a gray line or a gray icon means that the restriction is inactive. If a road is crossing such a purple line - and the road itself is not indicated as purple - then this restriction does not apply on the current road. Note that an icon does not necessarily indicate an exact location: For example, in case of a restricted road an icon may be placed centered on the restricted road - or, if the restriction is longer, the icon may be repeated several times for the same restriction along one or several roads. The icon itself is localized per country and represents the type of restriction. For most restrictions, the location and the type of the restriction is also indicated through the TruckRestrictionWarning event (as shown above).
  • Use TruckAmenities which contains information on showers or rest rooms in the Details of a Place. Search along a route corridor and check if a place contains truck amenities. Enable this feature by calling searchEngine.setCustomOption() with "show" as name and "truck" as value.

MapScene.Layers.vehicleRestrictions

Off-Road Guidance

With off-road guidance you can help customers to reach a destination that lies off-road. Usually, guidance stops at the map-matched destination. A destination is considered to be off-road when it can't be map-matched to a road network - for example, when the destination lies in the middle of a forrest.

Screenshot: Off-road guidance.

You can detect if the destination is off-road or not and inform the user when the map-matched destination is reached:

// Conform to DestinationReachedDelegate.
// Notifies when the destination of the route is reached.
func onDestinationReached() {
    guard let lastSection = lastCalculatedRoute?.sections.last else {
        // A new route is calculated, drop out.
        return
    }
    if lastSection.arrivalPlace.isOffRoad() {
        print("End of navigable route reached.")
        let message1 = "Your destination is off-road."
        let message2 = "Follow the dashed line with caution."
        // Note that for this example we inform the user via UI.
        uiCallback?.onManeuverEvent(action: ManeuverAction.arrive,
                                   message1: message1,
                                   message2: message2)
    } else {
        print("Destination reached.")
        let distanceText = "0 m"
        let message = "You have reached your destination."
        uiCallback?.onManeuverEvent(action: ManeuverAction.arrive,
                                   message1: distanceText,
                                   message2: message)
    }
}

For this example, we use on app-side a uiCallback mechanism to update our view. This code is not relevant here, so it is omitted. It can be found in the accompanying Rerouting example app on GitHub.

If the map-matched destination - for example, the road that leads to a forrest - is not reached, then a user will get no off-road guidance. Off-road guidance is only started after receiving the onDestinationReached() event.

Note

When off-road events start, then there is no way to go back to the normal guidance mode to receive RouteProgress events - unless a new route is set.

Add the following code to provide guidance to the off-road destination:

// Conform to OffRoadProgressDelegate.
// Notifies on the progress when heading towards an off-road destination.
// Off-road progress events will be sent only after the user has reached
// the map-matched destination and the original destination is off-road.
// Note that when a location cannot be map-matched to a road, then it is considered
// to be off-road.
func onOffRoadProgressUpdated(_ offRoadProgress: heresdk.OffRoadProgress) {
    let distanceText = convertDistance(meters: offRoadProgress.remainingDistanceInMeters)
    // Bearing of the destination compared to the user's current position.
    // The bearing angle indicates the direction into which the user should walk in order
    // to reach the off-road destination - when the device is held up in north-up direction.
    // For example, when the top of the screen points to true north, then 180° means that
    // the destination lies in south direction. 315° would mean the user has to head north-west, and so on.
    let message = "Direction of your destination: \(round(offRoadProgress.bearingInDegrees))°"
    uiCallback?.onManeuverEvent(action: ManeuverAction.arrive,
                                message1: distanceText,
                                message2: message)
}

// Conform to OffRoadDestinationReachedDelegate.
// Notifies when the off-road destination of the route has been reached (if any).
func onOffRoadDestinationReached() {
    print("Off-road destination reached.")
    let distanceText = "0 m"
    let message = "You have reached your off-road destination."
    uiCallback?.onManeuverEvent(action: ManeuverAction.arrive,
                                message1: distanceText,
                                message2: message)
}

For this example, we use on app-side a convertDistance() method to convert distances to meters and kilometers. This code is not relevant for use with the HERE SDK, so its implementation is omitted above.

Note that off-road guidance does not provide help before the user has reached the map-matched destination. Also, there is no support for off-road passages during a trip, for example, when a user deviates from a road or moves on unknown roads. Only when a user reaches the last point of the route that can be reached with the known road network, then off-road guidance is started.

By default, the dashed line that leads to the off-road destination is shown on the map - even if none of the above code is present. This can be controlled like so:

// Enable off-road visualization (if any) with a dotted straight-line
// between the map-matched and the original destination (which is off-road).
// Note that the color of the dashed line can be customized, if desired.
// The line will not be rendered if the destination is not off-road.
// By default, this is enabled.
visualNavigator.isOffRoadDestinationVisible = true

Note

This feature does not guide a user along a path or is providing any kind of maneuver information. Please make sure that the provided information is handled with care: Only a straight-line is drawn from the current location of the user to the off-road destination. In reality, the destination may be unreachable or may lie in dangerous territory that is impossible to be travelled by pedestrians. Make sure to notify your users accordingly.

Implement a Location Provider

A location provider is necessary to be provide Location instances to the VisualNavigator. It can feed location data from any source. Here we plan to use an implementation that allows to switch between native location data from the device and simulated location data for test drives.

As already mentioned above, the VisualNavigator conforms to the LocationDelegate protocol, so it can be used as delegate for classes that call onLocationUpdated(location:).

As a source for location data, we use a HEREPositioningProvider that is based on the code as shown in the Find your Location section.

Note

For navigation it is recommended to use LocationAccuracy.navigation when starting the LocationEngine as this guarantees the best results during turn-by-turn navigation.

To deliver events, we need to start the herePositioningProvider:

herePositioningProvider.startLocating(locationDelegate: visualNavigator,
                                      accuracy: .navigation)

The required HERE SDK Location type includes bearing and speed information along with the current geographic coordinates and other information that is consumed by the VisualNavigator. The more accurate and complete the provided data is, the more precise the overall navigation experience will be.

Note that the bearing value taken from the Location object determines the direction of movement which is then indicated by the LocationIndicator asset that rotates into that direction. When the user is not moving, then the last rotation is kept until a new bearing value is set. Depending on the source for the Location data, this value can be more or less accurate.

Internally, the timestamp of a Location is used to evaluate, for example, if the user is driving through a tunnel or if the signal is simply lost.

You can find a reference implementation of the location provider on GitHub.

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.

Below we integrate the LocationSimulator as an alternative provider to allow switching between real location updates and simulated ones.

import heresdk

// A class that provides simulated location updates along a given route.
// The frequency of the provided updates can be set via LocationSimulatorOptions.
class HEREPositioningSimulator {

    private var locationSimulator: LocationSimulator?

    func startLocating(locationDelegate: LocationDelegate, route: Route) {
        if let locationSimulator = locationSimulator {
            locationSimulator.stop()
        }

        locationSimulator = createLocationSimulator(locationDelegate: locationDelegate, route: route)
        locationSimulator!.start()
    }

    func stopLocating() {
        if locationSimulator != nil {
            locationSimulator!.stop()
            locationSimulator = nil
        }
    }

    // Provides fake GPS signals based on the route geometry.
    private func createLocationSimulator(locationDelegate: LocationDelegate,
                                         route: Route) -> LocationSimulator {
        let notificationIntervalInSeconds: TimeInterval = 0.5
        let locationSimulatorOptions = LocationSimulatorOptions(speedFactor: 2,
                                                                notificationInterval: notificationIntervalInSeconds)
        let locationSimulator: LocationSimulator

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

        locationSimulator.delegate = locationDelegate
        locationSimulator.start()

        return locationSimulator
    }
}

In addition, by setting LocationSimulatorOptions, we can specify, how fast the current simulated location will move. By default, the notificationInterval is 1 s and the speedFactor 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 contains insufficient coordinates for the specified time interval, additional location events will be interpolated by the VisualNavigator.

Note

The locations emitted by the LocationSimulator are not interpolated and they are provided based on the source. In case of a Route, the coordinates of the route geometry will be used (which are very close to each other). In case of a GPXTrack, the coordinates are emitted based on the GPX data: For example, if there are hundrets of meters between two coordinates, then only those two coordinates are emitted based on the time settings. However, when fed into the VisualNavigator, the rendered map animations will be interpolated by the VisualNavigator.

The VisualNavigator will skip animations if the distance between consecutive Location updates is greater than 100 m. If the speedFactor is increased, the distance between location updates changes as well - if the notification interval is not adjusted accordingly: For example, if you want to change the speed factor to 8, you should also change the notification interval to 125 ms (1000 ms / 8) in order to keep the distance between the Location updates consistent. The notificationInterval and the speedFactor are inversely proportional. Accordingly, for a speedFactor of 3, the recommended notificationInterval is 330 ms.

The code below shows how you can seamlessly switch between simulated and real locations by calling enableRoutePlayback(route:) and enableDevicePositioning():

// Provides location updates based on the given route.
func enableRoutePlayback(route: Route) {
    herePositioningProvider.stopLocating()
    herePositioningSimulator.startLocating(locationDelegate: visualNavigator, route: route)
}

// Provides location updates based on the device's GPS sensor.
func enableDevicePositioning() {
    herePositioningSimulator.stopLocating()
    herePositioningProvider.startLocating(locationDelegate: visualNavigator,
                                          accuracy: .navigation)
}

Note that we need to ensure to stop any ongoing simulation or real location source before starting a new one.

You can see the code from above included in the Navigation example app on GitHub.

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.

Note

Maneuver notifications are targeted at drivers. It is not recommended to use them for pedestrian guidance.

Note that the HERE SDK does not include pre-recorded voice skins. This means, you need to integrate a TTS engine for playback. Below you can find more details and examples for audio playback.

Example notifications (provided as strings):

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:

visualNavigator.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.

Optionally, you can also enable natural guidance: ManeuverNotification texts can be enhanced to include significant objects (such as traffic lights or stop signs) along a route to make maneuvers better understandable. Example: "At the next traffic light turn left onto Wall street". By default, this feature is disabled. To enable it, add at least one NaturalGuidanceType such as trafficLight to ManeuverNotificationOptions via the list of includedNaturalGuidanceTypes.

You can set a LanguageCode to localize the notification text and a UnitSystem to decide on metric or imperial length units. Make sure to call this before a route is set, as otherwise default settings (en-US, metric) will be used. For more ManeuverNotificationOptions consult the API Reference.

private func setupVoiceGuidance() {
    let ttsLanguageCode = getLanguageCodeForDevice(supportedVoiceSkins: VisualNavigator.availableLanguagesForManeuverNotifications())
    visualNavigator.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
}

Internally, maneuver notification texts are generated from the Maneuver data you can access from the RouteProgress event. The RouteProgress event is frequently generated based on the passed location updates. For maneuver notifications you can specify how often and when the text messages should be generated. This can be specified via ManeuverNotificationTimingOptions.

For each ManeuverNotificationType you can set ManeuverNotificationTimingOptions for each transport mode and road type. You can also specify the timings in distance in meters or in seconds:

  1. ManeuverNotificationType.range: 1st notification. Specified via rangeNotificationDistanceInMeters or rangeNotificationTimeInSeconds.
  2. ManeuverNotificationType.reminder: 2nd notification. Specified via reminderNotificationDistanceInMeters or reminderNotificationTimeInSeconds.
  3. ManeuverNotificationType.distance: 3rd notification. Specified via distanceNotificationDistanceInMeters or distanceNotificationTimeInSeconds.
  4. ManeuverNotificationType.action: 4th notification.Specified via actionNotificationDistanceInMeters or actionNotificationTimeInSeconds.

The types are ordered by distance. If you want to customize TTS messages only a few meters before the maneuver, set only distanceNotificationDistanceInMeters. You can also omit types via maneuverNotificationOptions.includedNotificationTypes. For example, if you set only the distance type, you will not get any other notifications - even when the maneuver takes place (= action).

Below we tweak only the distance type value for cars (= transport mode) on highways (road type) and keep all other values as they are:

// Get currently set values - or default values, if no values have been set before.
// By default, notifications for cars on highways are sent 1300 meters before the maneuver
// takes place.
var carHighwayTimings =
  navigator.getManeuverNotificationTimingOptions(transportMode: TransportMode.car,
                                                 roadType: RoadType.highway)

// Set ManeuverNotificationType.distance (3rd notification):
// Set a new value for cars on highways and keep all other values unchanged.
carHighwayTimings.distanceNotificationDistanceInMeters = 1500

// Apply the changes.
navigator.setManeuverNotificationTimingOptions(transportMode: TransportMode.car,
                                               roadType: RoadType.highway,
                                               options: carHighwayTimings)

// By default, we keep all types. If you set an empty list you will disallow generating the texts.
// The names of the type indicate the use case: For example, range is the farthest notification.
// action is the notification when the maneuver takes place.
// And reminder and distance are two optional notifications when approaching the maneuver.
maneuverNotificationOptions.includedNotificationTypes = [
  ManeuverNotificationType.range, // 1st notification.
  ManeuverNotificationType.reminder, // 2nd notification.
  ManeuverNotificationType.distance, // 3rd notification.
  ManeuverNotificationType.action // 4th notification.
]

By default, the maneuver notification texts are in plain orthographic form ("Wall Street"). Some TTS engines support phonemes, which add the plain text "Wall Street" additionally also in SSML notation: ""wɔːl"striːt". This results in a better pronunciation. Call the below code to enable phonemes:

// Add phoneme support with SSML.
// Note that phoneme support may not be supported by all TTS engines.
maneuverNotificationOptions.enablePhoneme = true
maneuverNotificationOptions.notificationFormatOption = NotificationFormatOption.ssml

Example voice message with phonemes:

After 300 meters turn right onto <lang xml:lang="ENG"><phoneme alphabet="nts"  ph="&quot;wɔːl&quot;striːt" orthmode="ignorepunct">Wall Street</phoneme></lang>.

Take a look at the API Reference to find more options that can be set via ManeuverNotificationOptions.

Note that the HERE SDK supports 37 languages. You can query the languages from the VisualNavigator with VisualNavigator.availableLanguagesForManeuverNotifications(). 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.

Note

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 feed the maneuver notification into a TTS engine, you also need to ensure that your preferred language is supported by the TTS engine of your choice. Usually each device comes with some preinstalled languages, but not all languages may be present initially.

Note

The SpatialAudioNavigation example app shows how to use the VisualNavigator together with native code for iOS to play back the TTS audio messages with audio panning to indicate directions via the stereo panorama. You can find the example on GitHub.

Supported Languages for Voice Guidance

Below you can find a list of all supported voice languages together with the name of the related voice skin that is stored inside the HERE SDK framework:

  • Arabic (Saudi Arabia): voice_package_ar-SA
  • Czech: voice_package_cs-CZ
  • Danish: voice_package_da-DK
  • German: voice_package_de-DE
  • Greek: voice_package_el-GR
  • English (British): voice_package_en-GB
  • English (United States): voice_package_en-US
  • Spanish (Spain): voice_package_es-ES
  • Spanish (Mexico): voice_package_es-MX
  • Farsi (Iran): voice_package_fa-IR
  • Finnish: voice_package_fi-FI
  • French (Canada): voice_package_fr-CA
  • French: voice_package_fr-FR
  • Hebrew: voice_package_he-IL
  • Hindi: voice_package_hi-IN
  • Croatian: voice_package_hr-HR
  • Hungarian: voice_package_hu-HU
  • Indonesian: (Bahasa) voice_package_id-ID
  • Italian: voice_package_it-IT
  • Japanese: voice_package_ja-JP
  • Korean: voice_package_ko-KR
  • Norwegian: (Bokmål) voice_package_nb-NO
  • Dutch: voice_package_nl-NL
  • Portuguese (Portugal) voice_package_pt-PT
  • Portuguese (Brazil): voice_package_pt-BR
  • Polish: voice_package_pt-PT
  • Romanian: voice_package_ro-RO
  • Russian: voice_package_ru-RU
  • Slovak: voice_package_sk-SK
  • Serbian: voice_package_sr-CS
  • Swedish: voice_package_sv-SE
  • Thai: voice_package_th-TH
  • Turkish: voice_package_tr-TR
  • Ukrainian: voice_package_uk-UA
  • Vietnamese: voice_package_vi-VN
  • Chinese (Simplified China): voice_package_zh-CN
  • Chinese (Traditional Hong Kong): voice_package_zh-HK
  • Chinese (Traditional Taiwan): voice_package_zh-TW

Open the HERE SDK framework and search for the voice_assets folder. If you want to shrink the size of the framework, you can remove the voice packages you do not need.

Spatial Audio Maneuver Notifications

The same voiceText as provided by the ManeuverNotificationDelegate (see above) can be also enhanced with spatial audio information.

Spatial audio maneuver notifications allow to adjust the stereo panorama of the text-to-speech strings in real-time. This happens based on the maneuver location in relation to a driver sitting in a vehicle.

For this, use the SpatialManeuverNotificationDelegate instead (or in parallel) of the ManeuverNotificationDelegate. It triggers notifications when spatial maneuvers are available. In addition, add a SpatialManeuverAzimuthDelegate to trigger the azimuth elements which compose one of the spatial audio trajectories defined by the HERE SDK. The resulting SpatialTrajectoryData contains the next azimuth angle to be used and it indicates wether the spatial audio trajectory has finished or not.

Use SpatialManeuverAudioCuePanning to start panning and pass CustomPanningData to update the estimatedAudioCueDuration of the SpatialManeuver and to customize its initialAzimuthInDegrees and sweepAzimuthInDegrees properties.

Get Road Shield Icons

With iconProvider.createRoadShieldIcon(...) you can asynchronously create a UIImage that depicts a road number such as "A7" or "US-101" - as it already appears on the map view.

The creation of road shield icons happens offline and does not require an internet connection.

Examples of road shield icons.

You can show the icons as part of a route preview before starting guidance or, for example, during guidance for the next maneuver to provide a visual hint where the maneuver takes place.

Showing a road shield together with the next maneuver.

All required information to generate a road shield icon is part of a Route object.

The icon itself is generated from RoadShieldIconProperties that require parameters such as RouteType and LocalizedRoadNumber. These parameters can be retrieved from the Span of a Route object.

Use span.getShieldText(..) to get the shieldText for use with the RoadShieldIconProperties. With span.roadNumbers you can get a list of LocalizedRoadNumber items with additional information such as RouteType (level 1 to 6, indicating whether a road is a major road or not) and CardinalDirection (such as in "101 West").

With IconProvider.IconCallback you can receive the resulting image - or an error.

Note

Note that this is a beta release of this feature, so there could be a few bugs and unexpected behaviors. Related APIs may change for new releases without a deprecation process.

As an example, below you can find a possible solution how to show / hide a road shield icon from with in the RouteProgress event:

visualNavigator.setRouteProgressListener(new RouteProgressListener() {
    @Override
    public void onRouteProgressUpdated(@NonNull RouteProgress routeProgress) {
        List<ManeuverProgress> maneuverProgressList = routeProgress.maneuverProgress;
        ManeuverProgress nextManeuverProgress = maneuverProgressList.get(0);
        if (nextManeuverProgress == null) {
            Log.d(TAG, "No next maneuver available.");
            return;
        }

        int nextManeuverIndex = nextManeuverProgress.maneuverIndex;
        Maneuver nextManeuver = visualNavigator.getManeuver(nextManeuverIndex);

        // ...
        // Here you can extract maneuver information from nextManeuverProgress.
        // ...

        if (previousManeuver == nextManeuver) {
            // We are still trying to reach the next maneuver.
            return;
        }
        previousManeuver = nextManeuver;

        // A new maneuver takes places. Hide the existing road shield icon, if any.  
        uiCallback.onHideRoadShieldIcon();

        Span maneuverSpan = getSpanForManeuver(visualNavigator.getRoute(), nextManeuver);
        if (maneuverSpan != null) {
            // Asynchronously create the icon and show it.
            createRoadShieldIconForSpan(maneuverSpan);
        }
    }
});


// Conform to RouteProgressDelegate.
func onRouteProgressUpdated(_ routeProgress: heresdk.RouteProgress) {
    let maneuverProgressList = routeProgress.maneuverProgress
    guard let nextManeuverProgress = maneuverProgressList.first else {
        print("No next maneuver available.")
        return
    }

    // ...
    // Here you can extract maneuver information from nextManeuverProgress.
    // ...

    let nextManeuverIndex = nextManeuverProgress.maneuverIndex
    let nextManeuver = visualNavigator.getManeuver(index: nextManeuverIndex)

    if previousManeuver == nextManeuver {
        // We are still trying to reach the next maneuver.
        return;
    }
    previousManeuver = nextManeuver;

    // A new maneuver takes places. Hide the existing road shield icon, if any.
    uiCallback?.onHideRoadShieldIcon()

    guard let maneuverSpan = getSpanForManeuver(route: visualNavigator.route!,
                                                maneuver: nextManeuver!) else {
        return
    }
    createRoadShieldIconForSpan(maneuverSpan)
}

For this example, , we use on app-side a uiCallback mechanism to update our view. This code is not relevant here, so it is omitted, but it can be found in accompanying Rerouting example app on GitHub.

You can get the span for a maneuver like so:

private func getSpanForManeuver(route: Route, maneuver: Maneuver) -> Span? {
    let index = Int(maneuver.sectionIndex)
    let sectionOfManeuver = route.sections[index]
    let spansInSection = sectionOfManeuver.spans

    // The last maneuver is located on the last span.
    // Note: Its offset points to the last GeoCoordinates of the route's polyline:
    // maneuver.offset = sectionOfManeuver.geometry.vertices.last.
    if maneuver.action == ManeuverAction.arrive {
        return spansInSection.last
    }

    let indexOfManeuverInSection = maneuver.offset
    for span in spansInSection {
        // A maneuver always lies on the first point of a span. Except for the
        // the destination that is located somewhere on the last span (see above).
        let firstIndexOfSpanInSection = span.sectionPolylineOffset
        if firstIndexOfSpanInSection >= indexOfManeuverInSection {
            return span
        }
    }

    // Should never happen.
    return nil
}

Below you can find the code how to extract the information to generate the road shield icon:

private func createRoadShieldIconForSpan(_ span: Span) {
    guard !span.roadNumbers.items.isEmpty else {
        // Road shields are only provided for roads that have route numbers such as US-101 or A100.
        // Many streets in a city like "Invalidenstr." have no route number.
        return
    }

    // For simplicity, we use the 1st item as fallback. There can be more numbers you can pick per desired language.
    guard var localizedRoadNumber = span.roadNumbers.items.first else {
        // First time should not be nil when list is not empty.
        return
    }

    // Desired locale identifier for the road shield text.
    let desiredLocale = Locale(identifier: "en_US")
    for roadNumber in span.roadNumbers.items {
        if roadNumber.localizedNumber.locale == desiredLocale {
            localizedRoadNumber = roadNumber
            break
        }
    }

    // The route type indicates if this is a major road or not.
    let routeType = localizedRoadNumber.routeType
    // The text that should be shown on the road shield.
    let shieldText = span.getShieldText(roadNumber: localizedRoadNumber)
    // This text is used to additionally determine the road shield's visuals.
    let routeNumberName = localizedRoadNumber.localizedNumber.text

    if lastRoadShieldText == shieldText {
        // It looks like this shield was already created before, so we opt out.
        return
    }

    lastRoadShieldText = shieldText

    // Most icons can be created even if some properties are empty.
    // If countryCode is empty, then this will result in an IconProviderError.ICON_NOT_FOUND. Practically,
    // the country code should never be null, unless when there is a very rare data issue.
    let countryCode = span.countryCode ?? ""
    let stateCode = span.countryCode ?? ""

    let roadShieldIconProperties = RoadShieldIconProperties(
        routeType: routeType,
        countryCode: countryCode,
        stateCode: stateCode,
        routeNumberName: routeNumberName,
        shieldText: shieldText
    )

    // Set the desired constraints. The icon will fit in while preserving its aspect ratio.
    let widthConstraintInPixels: UInt32 = ManeuverView.roadShieldDimConstraints
    let heightConstraintInPixels: UInt32 = ManeuverView.roadShieldDimConstraints

    // Create the icon offline. Several icons could be created in parallel, but in reality, the road shield
    // will not change very quickly, so that a previous icon will not be overwritten by a parallel call.
    iconProvider.createRoadShieldIcon(properties: roadShieldIconProperties,
                                      mapScheme: MapScheme.normalDay,
                                      widthConstraintInPixels: widthConstraintInPixels,
                                      heightConstraintInPixels: heightConstraintInPixels,
                                      callback: handleIconProviderCallback)
}

Note that we use an additional flag lastRoadShieldText to check if the icon was already created.

The handleIconProviderCallback we can implement like shown below:

private func handleIconProviderCallback(image: UIImage?,
                                        description: String?,
                                        error: IconProviderError?) {
    if let iconProviderError = error {
        print("Cannot create road shield icon: \(iconProviderError.rawValue)")
        return
    }

    // If iconProviderError is nil, it is guaranteed that bitmap and description are not nil.
    guard let roadShieldIcon = image else {
        return
    }

    // A further description of the generated icon, such as "Federal" or "Autobahn".
    let shieldDescription = description ?? ""
    print("New road shield icon: \(shieldDescription)")

    // An implementation can now decide to show the icon, for example, together with the
    // next maneuver actions.
    uiCallback?.onRoadShieldEvent(roadShieldIcon: roadShieldIcon)
}

Again, we use on app-side a uiCallback mechanism to update our view. This code is not relevant here, so it is omitted, but it can be found in accompanying Rerouting example app on GitHub.

Stop Navigation

While turn-by-turn navigation automatically starts when a route is set and the LocationPrivider is started, stopping navigation depends on the possible scenario:

Either, you want to stop navigation and switch to tracking mode (see below) to receive map-matched locations while still following a path - or you want to stop navigation without going back to tracking mode. For the first case, you only need to set the current route to nil. This will only stop propagating all turn-by-turn navigation related events, but keep the ones alive to receive map-matched location updates and, for example, speed warning information. Note that propagation of turn-by-turn navigation events is automatically stopped when reaching the desired destination. Once you set a route again, all turn-by-turn navigation related events will be propagated again.

If you want to stop navigation without going back to tracking mode - for example, to get only un-map-matched location updates directly from a location provider - it is good practice to stop getting all events from the VisualNavigator. For this you should set all delegates individually to nil.

You can reuse your location provider implementation to consume location updates in your app. With HERE positioning you can set multiple LocationDelegate instances.

When you use the VisualNavigator, call stopRendering(). Once called, the MapView will be no longer under control by the VisualNavigator:

  • Settings, like map orientation, camera distance or tilt, which may have been altered during rendering are no longer updated. They will keep the last state before stopRendering() was called. For example, if the map was tilted during guidance, it will stay tilted. Thus, it is recommended to apply the desired camera settings after stopRendering() is called.
  • The map will no longer move to the current location - even if you continue to feed new locations into the VisualNavigator.
  • The default or custom location indicator owned by the VisualNavigator will be hidden again.
  • Note that all location-based events such as the RouteProgress will be still delivered unless you unsubscribe by setting a nil delegate - see above.

Note

Since the VisualNavigator operates on a MapView instance, it is recommended to call stopRendering() before deinitializing a MapView. In addition, it is recommended to stop LocationSimulator and DynamicRoutingEngine in case they were started before. However, when a MapView is paused, it is not necessary to also stop the VisualNavigator. The VisualNavigator stops automatically to render when the MapView is paused and it starts rendering when the MapView is resumed (when the VisualNavigator was rendering before).

Tracking

While you can use the VisualNavigator 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 all transport modes - except for public transit. Public transit routes may lead to unsafe and unexpected results when being used for tracking. Although all other transport modes are supported, tracking is most suitable for car and truck transport modes.

To enable tracking, all you need is to call:

visualNavigator.route = nil
herePositioningProvider.startLocating(locationDelegate: visualNavigator,
                                              accuracy: .navigation)

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 VisualNavigator 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

Note that in tracking mode you only get events for delegates such as the NavigableLocationDelegate or the SpeedWarningDelegate that can fire without the need for a route to follow. Other delegates such as the RouteProgressDelegate do 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.

Consult the API Reference for an overview to see which delegates work in tracking mode.

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.

When tracking is enabled, it is also recommended to enable the SpeedBasedCameraBehavior:

visualNavigator.cameraBehavior = SpeedBasedCameraBehavior()

This camera mode is automatically adjusting the camera's location to optimize the map view based on the current driving speed.

In order to stop following the camera, call:

visualNavigator.setCameraBehavior(null)

This can be also useful during guidance, if you want to temporarily enable gesture handling. It is recommended to automatically switch back to tracking if turn-by-turn navigation is ongoing - in order to not distract a driver.

Prepare a Trip

The HERE SDK provides support for route prefetching of map data. This allows to improve the user experience - for example, during turn-by-turn navigation to handle temporary network losses gracefully.

Note that this is not needed if offline maps are already downloaded for the region where a trip takes place. In this case, all map data is already there, and no network connection is needed. Unlike, for example, the dedicated OfflineRoutingEngine, the Navigator or VisualNavigator will decide automatically when it is necessary to fallback to cached data or offline map data. In general, navigation requires map data, even if it is executed headless without showing a map view. The reason for this is that map data needs to be accessed during navigation for map matching and, for example, to notify on certain road attributes like speed limits. This data is taken from the available data on the device - or in case it is not there, it needs to be downloaded during navigation. Therefore, it can be beneficial to prefetch more data in anticipation of the road ahead. Without prefetching, temporary connection losses can be handled less gracefully.

Note

Note that this is a beta release of this feature, so there could be a few bugs and unexpected behaviors. Related APIs may change for new releases without a deprecation process.

The RoutePrefetcher constructor requires a SDKNativeEngine instance as only parameter. You can get it via SDKNativeEngine.sharedInstance after the HERE SDK has been initialized.

With the RoutePrefetcher you can download map data in advance. The map data will be loaded into the map cache. Note that the map cache has its own size constraints and may already contain data: The RoutePrefetcher may need to evict old cached data in order to store new map data.

  • It is recommended to call once routePrefetcher.prefetchAroundLocation(currentGeoCoordinates) before starting a trip. This call prefetches map data around the provided location with a radius of 2 km into the map cache and it ensures, that there is enough map data available when a user starts to follow the route - assuming that the route starts from the current location of the user.

  • After navigation has started, consider to call once routePrefetcher.prefetchAroundRouteOnIntervals​(navigator): It prefetches map data within a corridor along the route that is currently set to the provided Navigator instance. If no route is set, no data will be prefetched. The route corridor defaults to a length of 10 km and a width of 5 km. Map data is prefetched only in discrete intervals. Prefetching starts 1 km before reaching the end of the current corridor. Prefetching happens based on the current map-matched location - as indicated by the RouteProgress event. The first prefetching will start after travelling a distance of 9 km along the route. If a new route is set to the navigator, it is not necessary to call this method again - however, it has also no negative impact when it is called twice or more times.

The Navigation example app shows an example how to use the RoutePrefetcher.

If the RoutePrefetcher was successfully used at the start of a route - and then later the connectivity is lost, the cached data will be preserved even across future power cycles until the map cache is evicted. More about the map cache's eviction policy can be found here.

  • For convenience, you can alternatively call both methods together before starting navigation. However, as a trade-off, there might not be enough time to prefetch all required data when the trip starts soon thereafter.
  • Keep in mind that prefetchAroundRouteOnIntervals() increases network traffic continuously during guidance.

Of course, guidance will be also possible without any prefetched data, but the experience may be less optimized:

Both calls help to optimize temporary offline use cases that rely on cached map data. While the prefetchAroundLocation() can be also used outside of a navigation use case, prefetchAroundRouteOnIntervals() requires an ongoing navigation scenario.

Alternatively, you can prefetch map data for the entire route in advance. Use RoutePrefetcher.prefetchGeoCorridor() to prefetch tile data for a GeoCorridor created from the route's shape. Since this can take a little longer - depending on the length of the route, the corridor's width and the network - a progress is reported via PrefetchStatusListener.onProgress(). Once the operation is completed, an PrefetchStatusListener.onComplete() event is sent.

Note

In case the route passes already downloaded Region data, then these parts of the corridor are reused and not downloaded again.

results matching ""

    No results matching ""