Navigation

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

Navigation is supported for all available transport modes. 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 the car. After the route is calculated, the transport mode is attached to each Section of the route. For car and truck routes, the location of the device will be map-matched to the streets, while for pedestrian routes, locations are matched to unpaved dirt roads and other paths that would not be accessible to drivers.

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

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

Tip: Reusable assets for use in your own Android applications can be found in the MSDKUI open source project from HERE - available on GitHub under this link.

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

Offline guidance is supported. Turn-by-turn navigation and tracking fully works offline on already cached or downloaded offline map data - as long as the route does not reach regions without cached or preloaded map data.

Turn-By-Turn Navigation

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

When leaving the route, you can be notified of the deviation in meters. This notification can help you to decide whether or not 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 such behavior, you can check the related section in the Get Locations guide.

Use a Navigator to Listen for Guidance Events

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

  • A Route to follow. 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.

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

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 = new HEREPositioningProvider();

Next, we can create a new VisualNavigator instance and set it as listener to the HEREPositioningProvider from above. Note that the VisualNavigator class conforms to the LocationListener interface that defines the onLocationUpdated() method to receive locations.

try {
    visualNavigator = new VisualNavigator();
} catch (InstantiationErrorException e) {
    throw new RuntimeException("Initialization of VisualNavigator failed: " + e.error.name());
}

// Now visualNavigator will receive locations from the HEREPositioningProvider.
herePositioningProvider.startLocating(visualNavigator, LocationAccuracy.NAVIGATION);

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

visualNavigator.setRoute(route);

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

// Notifies on the progress along the route including maneuver instructions.
visualNavigator.setRouteProgressListener(new RouteProgressListener() {
    @Override
    public void onRouteProgressUpdated(@NonNull RouteProgress routeProgress) {
        List<SectionProgress> sectionProgressList = routeProgress.sectionProgress;
        // sectionProgressList is guaranteed to be non-empty.
        SectionProgress lastSectionProgress = sectionProgressList.get(sectionProgressList.size() - 1);
        Log.d(TAG, "Distance to destination in meters: " + lastSectionProgress.remainingDistanceInMeters);
        Log.d(TAG, "Traffic delay ahead in seconds: " + lastSectionProgress.trafficDelayInSeconds);

        // Contains the progress for the next maneuver ahead and the next-next maneuvers, if any.
        List<ManeuverProgress> nextManeuverList = routeProgress.maneuverProgress;

        ManeuverProgress nextManeuverProgress = nextManeuverList.get(0);
        if (nextManeuverProgress == null) {
            Log.d(TAG, "No next maneuver available.");
            return;
        }

        int nextManeuverIndex = nextManeuverProgress.maneuverIndex;
        Maneuver nextManeuver = visualNavigator.getManeuver(nextManeuverIndex);
        if (nextManeuver == null) {
            // Should never happen as we retrieved the next maneuver progress above.
            return;
        }

        ManeuverAction action = nextManeuver.getAction();
        String nextRoadName = nextManeuver.getNextRoadName();
        String road = nextRoadName == null ? nextManeuver.getNextRoadNumber() : nextRoadName;

        // On highways, we want to show the highway number instead of a possible street name,
        // while for inner city and urban areas street names are preferred over road numbers.
        if (nextManeuver.getNextRoadType() == RoadType.HIGHWAY) {
            road = nextManeuver.getNextRoadNumber() == null ? nextRoadName : nextManeuver.getNextRoadNumber();
        }

        if (action == ManeuverAction.ARRIVE) {
            // We are approaching the destination, so there's no next road.
            String currentRoadName = nextManeuver.getRoadName();
            road = currentRoadName == null ? nextManeuver.getRoadNumber() : currentRoadName;
        }

        if (road == null) {
            // Happens only in rare cases, when also the fallback is null.
            road = "unnamed road";
        }

        String logMessage = action.name() + " on " + road +
                " in " + nextManeuverProgress.remainingDistanceInMeters + " meters.";

        if (previousManeuverIndex != nextManeuverIndex) {
            snackbar.setText("New maneuver: " + logMessage).show();
        } else {
            // A maneuver update contains a different distance to reach the next maneuver.
            snackbar.setText("Maneuver update: " + logMessage).show();
        }

        previousManeuverIndex = nextManeuverIndex;
    }
});

// Notifies on the current map-matched location and other useful information while driving or walking.
visualNavigator.setNavigableLocationListener(new NavigableLocationListener() {
    @Override
    public void onNavigableLocationUpdated(@NonNull NavigableLocation currentNavigableLocation) {
        MapMatchedLocation mapMatchedLocation = currentNavigableLocation.mapMatchedLocation;
        if (mapMatchedLocation == null) {
            Log.d(TAG, "The currentNavigableLocation could not be map-matched. Are you off-road?");
            return;
        }

        Double speed = currentNavigableLocation.originalLocation.speedInMetersPerSecond;
        Double accuracy = currentNavigableLocation.originalLocation.speedAccuracyInMetersPerSecond;
        Log.d(TAG, "Driving speed (m/s): " + speed + "plus/minus an accuracy of: " +accuracy);
    }
});

// Notifies on a possible deviation from the route.
visualNavigator.setRouteDeviationListener(new RouteDeviationListener() {
    @Override
    public void onRouteDeviation(@NonNull RouteDeviation routeDeviation) {
        Route route = visualNavigator.getRoute();
        if (route == null) {
            // May happen in rare cases when route was set to null inbetween.
            return;
        }

        // Get current geographic coordinates.
        MapMatchedLocation currentMapMatchedLocation = routeDeviation.currentLocation.mapMatchedLocation;
        GeoCoordinates currentGeoCoordinates = currentMapMatchedLocation == null ?
                routeDeviation.currentLocation.originalLocation.coordinates : currentMapMatchedLocation.coordinates;

        // Get last geographic coordinates on route.
        GeoCoordinates lastGeoCoordinatesOnRoute;
        if (routeDeviation.lastLocationOnRoute != null) {
            MapMatchedLocation lastMapMatchedLocationOnRoute = routeDeviation.lastLocationOnRoute.mapMatchedLocation;
            lastGeoCoordinatesOnRoute = lastMapMatchedLocationOnRoute == null ?
                    routeDeviation.lastLocationOnRoute.originalLocation.coordinates : lastMapMatchedLocationOnRoute.coordinates;
        } else {
            Log.d(TAG, "User was never following the route. So, we take the start of the route instead.");
            lastGeoCoordinatesOnRoute = route.getSections().get(0).getDeparturePlace().originalCoordinates;
        }

        int distanceInMeters = (int) currentGeoCoordinates.distanceTo(lastGeoCoordinatesOnRoute);
        Log.d(TAG, "RouteDeviation in meters is " + distanceInMeters);
    }
});

Here we set a RouteProgressListener, a NavigableLocationListener and a RouteDeviationListener.

Inside the RouteProgressListener we can access detailed information on the progress per Section of the passed Route instance. A route may be split into several sections based on the number of waypoints and transport modes. Note that remainingDistanceInMeters and trafficDelayInSeconds are 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.

Inside the RouteProgressListener we also can 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.
List<ManeuverProgress> nextManeuverList = routeProgress.maneuverProgress;

ManeuverProgress nextManeuverProgress = nextManeuverList.get(0);
if (nextManeuverProgress == null) {
    Log.d(TAG, "No next maneuver available.");
    return;
}

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

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 as shown in the example above. Use voice guidance instead (see below).

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

It is recommended to use the map-matched location to give the user visual feedback. For example, to update the current map view based on the map-matched location. Only if the location could not be map-matched, such as, 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 that the maneuver instruction text (nextManeuver.getText()) is empty during navigation when taken from VisualNavigator. It only contains localized instructions when taken from the route instance. The ManeuverAction enum is supposed to be used to show a visual indicator. Consult the API Reference for a full list of supported actions.

Some roads, such as highways, do not have a road name. Instead, you can try to retrieve the road number.

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 route recalculation you may want to use the same route parameters. However, there is no automatic mechanism to choose the same route alternative as before - unless you provide UI or some other logic for this.

In the above example, we calculate the distance based on the coordinates contained in RouteDeviation: distanceInMeters. This indicates 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. The Navigation example app shows how to reroute after a deviation of 30 meters.

Note that previous events in the queue may still be delivered at least one time for the old route - as the events are delivered asynchronously. To prevent this, if desired, you can attach new delegates after setting the new route.

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 accidentially 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 alternaive 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 - while keeping the made choices for an route alternative and route options.

The HERE SDK offers three choices:

  1. Re-calculate the entire route with the RoutingEngine with new or updated RouteOptions to provide new route alternatives.
  2. Use the ReturnToRouteEngine to calculate a new route to reach the original route.
  3. Refresh the old route with routingEngine.refreshRoute() using a new starting point from the original route and optionally update the route options. Use a RouteHandle to identify the original route.

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.

Use the ReturnToRouteEngine 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.

The ReturnToRouteEngine requires cached or already downloaded map data. It therefore returns faster. 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.

As of now, the ReturnToRouteEngine supports only car routes.

Create a ReturnToRouteEngine to calculate a new route offline that returns to the original route:

ReturnToRouteEngine returnToRouteEngine = null;
try {
    // Requires cached or downloaded map data.
    returnToRouteEngine = new ReturnToRouteEngine();
} catch (InstantiationErrorException e) {
    // Handle exception.
}

The route calculation requires three parameters:

  • The original Route.
  • The normalized routeFractionTravelled of the route which was already travelled along the route.
  • The new starting Waypoint, which may be the current map matched location of the driver.

The routeFractionTravelled can be retrieved as follows:

int totalLengthToDestinationInMeters = originalRoute.getLengthInMeters();
int currentLengthToDestinationInMeters = remainingDistanceInMeters; // See RouteProgress.
// The normalized length of the already travelled path, between 0 and 1:
int travelledLengthInMeters = totalLengthToDestinationInMeters - currentLengthToDestinationInMeters;
double routeFractionTravelled = travelledLengthInMeters / totalLengthToDestinationInMeters;

From the RouteProgress (see above) event we can access the remainingDistanceInMeters:

List<SectionProgress> sectionProgressList = routeProgress.sectionProgress;
// sectionProgressList is guaranteed to be non-empty.
SectionProgress lastSectionProgress = sectionProgressList.get(sectionProgressList.size() - 1);
Log.d(TAG, "Distance to destination in meters: " + lastSectionProgress.remainingDistanceInMeters);

The remainingDistanceInMeters is based on the last known location of the driver on the route - for example, when the user left the route, no RouteProgress will be delivered. Once we know how much is left to reach the destination, we can substract this from the total length of the route to know how far we have travelled already along the route. This value is then normalized to get the routeFractionTravelled - which will be a value between 0 (no progress) and 1 (destination reached).

The next parameter is the new starting point, which can be retrieved from the RouteDeviation event:

// Get current geographic coordinates.
MapMatchedLocation currentMapMatchedLocation = routeDeviation.currentLocation.mapMatchedLocation;
GeoCoordinates currentGeoCoordinates = currentMapMatchedLocation == null ?
        routeDeviation.currentLocation.originalLocation.coordinates : currentMapMatchedLocation.coordinates;

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

Finally, we can calculate the new route:

// The new route starts from the newStartingPoint to the last known location on the route specified by the already
// travelled path (routeFractionTravelled) and from there the old route is used.
// No new section is added for the old + new part of the route.
returnToRouteEngine.returnToRoute(originalRoute, routeFractionTravelled, newStartingPoint, new CalculateRouteCallback() {
    @Override
    public void onRouteCalculated(@Nullable RoutingError routingError, @Nullable List<Route> list) {
        if (routingError == null) {
            Route newRoute = list.get(0);
            // ...
        } else {
            // Handle error.
        }
    }
});

Since the CalculateRouteCallback 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.

The ReturnToRouteEngine will use the originalRoute to reuse the already calclated portion of the route that lies ahead. It will also use the same OptimizationMode as found in the route.

To stop any ongoing navigation, call visualNavigator.setRoute(null). Reset the above delegates to null 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.

Receive Waypoint Events with Visual Navigator

The VisualNavigator class provides 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 example below notifies when the destination is reached - and therefore navigation can be stopped. Whereas the second code snippet below shows how to get notified on all types of waypoints including the destination waypoint.

// Notifies when the destination of the route is reached.
visualNavigator.setDestinationReachedListener(new DestinationReachedListener() {
    @Override
    public void onDestinationReached() {
        String message = "Destination reached. Stopping turn-by-turn navigation.";
        Snackbar.make(mapView, message, Snackbar.LENGTH_LONG).show();
        stopNavigation();
    }
});

// Notifies when a waypoint on the route is reached.
visualNavigator.setMilestoneReachedListener(new MilestoneReachedListener() {
    @Override
    public void onMilestoneReached(@NonNull Milestone milestone) {
        if (milestone.waypointIndex != null) {
            Log.d(TAG, "A user-defined waypoint was reached, index of waypoint: " + milestone.waypointIndex);
            Log.d(TAG,"Original coordinates: " + milestone.originalCoordinates);
        } else {
            // For example, when transport mode changes due to a ferry.
            Log.d(TAG,"A system defined waypoint was reached at " + milestone.mapMatchedCoordinates);
        }
    }
});

The onMilestoneReached() method provides a Milestone instance that contains the information about the passed waypoints along the route. Note that only stopover waypoints are included. 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.

Receive Speed Limit Events

By implementing the SpeedLimitListener 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.

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

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 that currently this feature does not include special speed limits for a SpecialSpeedSituation such as specific weather conditions.

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

// Notifies when the current speed limit is exceeded.
visualNavigator.setSpeedWarningListener(new SpeedWarningListener() {
    @Override
    public void onSpeedWarningStatusChanged(SpeedWarningStatus speedWarningStatus) {
        if (speedWarningStatus == SpeedWarningStatus.SPEED_LIMIT_EXCEEDED) {
            // 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.
            Uri ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
            Ringtone ringtone = RingtoneManager.getRingtone(context, ringtoneUri);
            ringtone.play();
        }

        if (speedWarningStatus == SpeedWarningStatus.SPEED_LIMIT_RESTORED) {
            Log.d(TAG, "Driver is again slower than current speed limit (plus an optional offset).");
        }
    }
});

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.

The onSpeedWarningStatusChanged() notification is 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 SPEED_LIMIT_RESTORED event when the route has changed - after driver's speed slows 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 one for higher speed limits. The boundary is defined by highSpeedBoundaryInMetersPerSecond:

private void setupSpeedWarnings() {
    double lowSpeedOffsetInMetersPerSecond = 2;
    double highSpeedOffsetInMetersPerSecond = 4;
    double highSpeedBoundaryInMetersPerSecond = 25;
    SpeedLimitOffset speedLimitOffset = new SpeedLimitOffset(
            lowSpeedOffsetInMetersPerSecond, highSpeedOffsetInMetersPerSecond, highSpeedBoundaryInMetersPerSecond);

    visualNavigator.setSpeedWarningOptions(new SpeedWarningOptions(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. 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.

Get Lane Assistance

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.

Similar to the other events described above, you can also attach a LaneAssistanceListener to the navigator. It only sends events when a Route is set. The resulting LaneAssistance object contains information about the available lanes on the current road and information such as their directions.

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.

Currently, left-hand driving countries are not supported.

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

// Notifies which lane(s) lead to the next (next) maneuvers.
visualNavigator.setLaneAssistanceListener(new LaneAssistanceListener() {
    @Override
    public void onLaneAssistanceUpdated(@NonNull LaneAssistance laneAssistance) {
        // This lane list is guaranteed to be non-empty.
        List<Lane> lanes = laneAssistance.lanesForNextManeuver;
        logLaneRecommendations(lanes);

        List<Lane> nextLanes = laneAssistance.lanesForNextNextManeuver;
        if (!nextLanes.isEmpty()) {
            Log.d(TAG, "Attention, the next next maneuver is very close.");
            Log.d(TAG, "Please take the following lane(s) after the next maneuver: ");
            logLaneRecommendations(nextLanes);
        }
    }
});

...

private void logLaneRecommendations(List<Lane> 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.
    // Note: Left-hand countries are not yet supported.
    int laneNumber = 0;
    for (Lane lane : 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 == LaneRecommendationState.RECOMMENDED) {
            Log.d(TAG,"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 == LaneRecommendationState.HIGHLY_RECOMMENDED) {
            Log.d(TAG,"Lane " + laneNumber + " leads to next maneuver and eventually to the next next maneuver.");
        }

        if (lane.recommendationState == LaneRecommendationState.NOT_RECOMMENDED) {
            Log.d(TAG,"Do not take lane " + laneNumber + " to follow the route.");
        }

        laneNumber++;
    }
}

Note that each LaneAssistance event is synchronized with some of the vocal commands you can receive via the ManeuverNotificationListener (see below). This means that for most roads, the event arrives simultaneously and at the same frequency as the voice guidance event that describes when to take the next maneuver.

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 LaneAssistance event:

// See above code snippet for the RouteProgressListener.
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.

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. Below 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 LocationListener interface, so it can be used as listener for classes that call onLocationUpdated().

As source for location data, we will 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:

// Call this to start getting locations from a device's sensor.
herePositioningProvider.startLocating(visualNavigator, LocationAccuracy.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.

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 com.here.sdk.core.LocationListener;
import com.here.sdk.core.errors.InstantiationErrorException;
import com.here.sdk.navigation.LocationSimulator;
import com.here.sdk.navigation.LocationSimulatorOptions;
import com.here.sdk.routing.Route;

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

    private LocationSimulator locationSimulator;

    // Starts route playback.
    // Does nothing when engine is already running.
    public void startLocating(LocationListener locationListener, Route route) {
        if (locationSimulator != null) {
            locationSimulator.stop();
        }

        locationSimulator = createLocationSimulator(locationListener, route);
        locationSimulator.start();
    }

    // Does nothing when engine is already stopped.
    public void stopLocating() {
        if (locationSimulator != null) {
            locationSimulator.stop();
            locationSimulator = null;
        }
    }

    // Provides fake GPS signals based on the route geometry.
    private LocationSimulator createLocationSimulator(LocationListener locationListener, Route route) {
        double speedFactor = 2;
        int notificationIntervalInMilliseconds = 500;
        LocationSimulatorOptions locationSimulatorOptions =
                new LocationSimulatorOptions(speedFactor, notificationIntervalInMilliseconds);

        LocationSimulator locationSimulator;

        try {
            locationSimulator = new LocationSimulator(route, locationSimulatorOptions);
        } catch (InstantiationErrorException e) {
            throw new RuntimeException("Initialization of LocationSimulator failed: " + e.error.name());
        }

        locationSimulator.setListener(locationListener);

        return locationSimulator;
    }
}

In addition, by setting LocationSimulatorOptions, we can specify, how fast the current simulated location will move. By default, the speed factor is 1.0, which is equal to the average speed a user normally drives or walks along each route segment without taking into account any traffic-related constraints. The default speed may vary based on the road geometry, road condition and other statistical data, but it is never higher than the current speed limit. Values above 1.0 will increase the speed proportionally. If the route contains insufficient coordinates for the specified time interval, additional location events will be interpolated.

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

// Provides simulated location updates based on the given route.
public void enableRoutePlayback(Route route) {
    herePositioningProvider.stopLocating();
    herePositioningSimulator.startLocating(visualNavigator, route);
}

// Provides location updates based on the device's GPS sensor.
public void enableDevicePositioning() {
    herePositioningSimulator.stopLocating();
    herePositioningProvider.startLocating(visualNavigator, LocationAccuracy.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.

Example notifications:

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

To get these notifications, set up a ManeuverNotificationListener:

visualNavigator.setManeuverNotificationListener(new ManeuverNotificationListener() {
    @Override
    public void onManeuverNotification(@NonNull String voiceText) {
        voiceAssistant.speak(voiceText);
    }
});

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

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, otherwise the default settings is (EN_US, METRIC):

private void setupVoiceGuidance() {
    LanguageCode ttsLanguageCode = getLanguageCodeForDevice(VisualNavigator.getAvailableLanguagesForManeuverNotifications());
    visualNavigator.setManeuverNotificationOptions(new ManeuverNotificationOptions(ttsLanguageCode, UnitSystem.METRIC));

    // Set language to our TextToSpeech engine.
    Locale locale = LanguageCodeConverter.getLocale(ttsLanguageCode);
    if (voiceAssistant.setLanguage(locale)) {
        Log.d(TAG, "TextToSpeech engine uses this language: " + locale);
    } else {
        Log.e(TAG, "TextToSpeech engine does not support this language: " + locale);
    }
}

For this example, we take the device's preferred language settings shown below:

private LanguageCode getLanguageCodeForDevice(List<LanguageCode> supportedVoiceSkins) {

    // 1. Determine if preferred device language is supported by our TextToSpeech engine.
    Locale localeForCurrenDevice = Locale.getDefault();
    if (!voiceAssistant.isLanguageAvailable(localeForCurrenDevice)) {
        Log.e(TAG, "TextToSpeech engine does not support: " + localeForCurrenDevice + ", falling back to EN_US.");
        localeForCurrenDevice = new Locale("en", "US");
    }

    // 2. Determine supported voice skins from HERE SDK.
    LanguageCode languageCodeForCurrenDevice = LanguageCodeConverter.getLanguageCode(localeForCurrenDevice);
    if (!supportedVoiceSkins.contains(languageCodeForCurrenDevice)) {
        Log.e(TAG, "No voice skins available for " + languageCodeForCurrenDevice + ", falling back to EN_US.");
        languageCodeForCurrenDevice = LanguageCode.EN_US;
    }

    return languageCodeForCurrenDevice;
}

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

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

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

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): ar-SA_tarik_compact
  • Czech: cs-CZ_iveta_compact
  • Danish: da-DK_magnus_compact
  • German: de-DE_anna_compact
  • Greek: el-GR_nikos_compact
  • English (British): en-GB_serena_compact
  • English (United States): en-US_tom_compact
  • Spanish (Spain): es-ES_jorge_compact
  • Spanish (Mexico): es-MX_angelica_compact
  • Farsi (Iran): fa-IR_anonymous_compact
  • Finnish: fi-FI_onni_compact
  • French (Canada): fr-CA_chantal_compact
  • French: fr-FR_audrey_compact
  • Hebrew: he-IL_carmit_compact
  • Hindi: hi-IN_lekha_compact
  • Croatian: hr-HR_anonymous_compact
  • Hungarian: hu-HU_mariska_compact
  • Indonesian: (Bahasa) id-ID_damayanti_compact
  • Italian: it-IT_alice_compact
  • Japanese: ja-JP_sakura_compact
  • Korean: ko-KR_sora_compact
  • Norwegian: (Bokmål) nb-NO_henrik_compact
  • Dutch: nl-NL_claire_compact
  • Portuguese (Brazil): pt-BR_luciana_compact
  • Polish: pt-PT_joana_compact
  • Romanian: ro-RO_ioana_compact
  • Russian: ru-RU_katya_compact
  • Slovak: sk-SK_laura_compact
  • Swedish: sv-SE_alva_compact
  • Thai: th-TH_kanya_compact
  • Turkish: tr-TR_cem_compact
  • Ukrainian: uk-UA_anonymous_compact
  • Chinese (Simplified China): zh-CN_tian-tian_compact
  • Chinese (Traditional Hong Kong): zh-HK_sin-ji_compact
  • Chinese (Traditional Taiwan): zh-TW_mei-jia_compact

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 null. 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 listeners individually to null.

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

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 car and truck transport modes.

To enable tracking, you only need to call:

visualNavigator.setRoute(null);
herePositioningProvider.startLocating(visualNavigator, LocationAccuracy.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 null. Note that in tracking mode you only get events for the NavigableLocationListener and the SpeedWarningListener. All other listeners may simply not deliver events when a route is not set (consult the API Reference for an overview to see which listeners work in tracking mode). This enables you to keep your listeners attached and to switch between free tracking and turn-by-turn-navigation on the fly.

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

results matching ""

    No results matching ""