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.
Note
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.
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
Reusable assets for use in your own applications can be found in the MSDKUI open source project from HERE - available on GitHub under this link.
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.
Voice guidance is provided as maneuver notifications that can be fed as a String
into any platform TTS (Text-To-Speech) solution.
Note
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.
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:
_startGuidance(HERE.Route route) {
try {
_visualNavigator = HERE.VisualNavigator();
} on InstantiationException {
throw Exception("Initialization of VisualNavigator failed.");
}
_visualNavigator!.startRendering(_hereMapController!);
_visualNavigator!.maneuverNotificationListener = HERE.ManeuverNotificationListener((String maneuverText) {
print("ManeuverNotifications: $maneuverText");
});
_visualNavigator!.route = route;
_setupLocationSource(_visualNavigator!, route);
}
_setupLocationSource(HERE.LocationListener locationListener, HERE.Route route) {
try {
_locationSimulator = HERE.LocationSimulator.withRoute(route, HERE.LocationSimulatorOptions.withDefaults());
} on InstantiationException {
throw Exception("Initialization of LocationSimulator failed.");
}
_locationSimulator!.listener = locationListener;
_locationSimulator!.start();
}
This code 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
. Note that in this case, these maneuver instructions are meant to be spoken to a driver and they may be strings like "Turn left onto Invalidenstraße in 500 meters.". More detailed maneuver instructions are, of course, 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:
- Create a
Route
. Without a route to follow you cannot start guidance. - Create a
VisualNavigator
instance and start rendering (or create a Navigator
instance if you want to render the guidance view on your own). - Set a
Route
to the VisualNavigator
. - 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.
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.
Tip: Make sure to to import the following packages to avoid name clashes with the Route / Navigator classes from Flutter:
import 'package:here_sdk/navigation.dart';
import 'package:here_sdk/routing.dart' as HERE;
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 to simulate Location
events - or follow the implementation from the Get Locations guide to listen to non-simulated Location
events. This is also shown as part of the navigation_app example.
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:
_locationSimulationProvider = new HEREPositioningSimulator();
Next, we can create a new VisualNavigator
instance and set it as listener to the HEREPositioningSimulator
from above. Note that the VisualNavigator
class conforms to the LocationListener
interface that defines the onLocationUpdated()
method to receive locations.
try {
_visualNavigator = VisualNavigator();
} on InstantiationException {
throw Exception("Initialization of VisualNavigator failed.");
}
_visualNavigator.route = route;
_locationSimulationProvider.startLocating(route, _visualNavigator);
In addition, make sure to set the route you want to follow (unless you plan to be in tracking mode only).
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 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:
_visualNavigator.routeProgressListener = RouteProgressListener((RouteProgress routeProgress) {
List<SectionProgress> sectionProgressList = routeProgress.sectionProgress;
SectionProgress lastSectionProgress = sectionProgressList.elementAt(sectionProgressList.length - 1);
print('Distance to destination in meters: ' + lastSectionProgress.remainingDistanceInMeters.toString());
print('Traffic delay ahead in seconds: ' + lastSectionProgress.trafficDelayInSeconds.toString());
List<ManeuverProgress> nextManeuverList = routeProgress.maneuverProgress;
if (nextManeuverList.isEmpty) {
print('No next maneuver available.');
return;
}
ManeuverProgress nextManeuverProgress = nextManeuverList.first;
int nextManeuverIndex = nextManeuverProgress.maneuverIndex;
Maneuver? nextManeuver = _visualNavigator.getManeuver(nextManeuverIndex);
if (nextManeuver == null) {
return;
}
ManeuverAction action = nextManeuver.action;
String roadName = _getRoadName(nextManeuver);
String logMessage = describeEnum(action) +
' on ' +
roadName +
' in ' +
nextManeuverProgress.remainingDistanceInMeters.toString() +
' meters.';
if (_previousManeuverIndex != nextManeuverIndex) {
print('New maneuver: $logMessage');
} else {
print('Maneuver update: $logMessage');
}
_previousManeuverIndex = nextManeuverIndex;
});
_visualNavigator.navigableLocationListener =
NavigableLocationListener((NavigableLocation currentNavigableLocation) {
MapMatchedLocation? mapMatchedLocation = currentNavigableLocation.mapMatchedLocation;
if (mapMatchedLocation == null) {
print('This new location could not be map-matched. Are you off-road?');
return;
}
var speed = currentNavigableLocation.originalLocation.speedInMetersPerSecond;
var accuracy = currentNavigableLocation.originalLocation.speedAccuracyInMetersPerSecond;
print("Driving speed (m/s): $speed plus/minus an accuracy of: $accuracy");
});
_visualNavigator.routeDeviationListener = RouteDeviationListener((RouteDeviation routeDeviation) {
HERE.Route? route = _visualNavigator.route;
if (route == null) {
return;
}
MapMatchedLocation? currentMapMatchedLocation = routeDeviation.currentLocation.mapMatchedLocation;
GeoCoordinates currentGeoCoordinates = currentMapMatchedLocation == null
? routeDeviation.currentLocation.originalLocation.coordinates
: currentMapMatchedLocation.coordinates;
GeoCoordinates lastGeoCoordinatesOnRoute;
if (routeDeviation.lastLocationOnRoute != null) {
MapMatchedLocation? lastMapMatchedLocationOnRoute = routeDeviation.lastLocationOnRoute!.mapMatchedLocation;
lastGeoCoordinatesOnRoute = lastMapMatchedLocationOnRoute == null
? routeDeviation.lastLocationOnRoute!.originalLocation.coordinates
: lastMapMatchedLocationOnRoute.coordinates;
} else {
print('User was never following the route. So, we take the start of the route instead.');
lastGeoCoordinatesOnRoute = route.sections.first.departurePlace.originalCoordinates!;
}
int distanceInMeters = currentGeoCoordinates.distanceTo(lastGeoCoordinatesOnRoute) as int;
print('RouteDeviation in meters is ' + distanceInMeters.toString());
});
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.
Note that the trafficDelayInSeconds
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 RouteProgressListener
we also can access the next maneuver that lies ahead of us. For this we use the maneuverIndex
:
List<ManeuverProgress> nextManeuverList = routeProgress.maneuverProgress;
if (nextManeuverList.isEmpty) {
print('No next maneuver available.');
return;
}
ManeuverProgress nextManeuverProgress = nextManeuverList.first;
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).
However, it can be useful to display localized street names or numbers (such as highway numbers), that can be retrieved as follows:
String _getRoadName(Maneuver maneuver) {
RoadTexts currentRoadTexts = maneuver.roadTexts;
RoadTexts nextRoadTexts = maneuver.nextRoadTexts;
List<i18n.Locale> locales = [i18n.Locale.parse("en-US")];
String? currentRoadName = currentRoadTexts.names.getPreferredValueForLocales(locales);
String? currentRoadNumber = currentRoadTexts.numbers.getPreferredValueForLocales(locales);
String? nextRoadName = nextRoadTexts.names.getPreferredValueForLocales(locales);
String? nextRoadNumber = nextRoadTexts.numbers.getPreferredValueForLocales(locales);
String? roadName = nextRoadName == null ? nextRoadNumber : nextRoadName;
if (maneuver.nextRoadType == RoadType.highway) {
roadName = nextRoadNumber == null ? nextRoadName : nextRoadNumber;
}
if (maneuver.action == ManeuverAction.arrive) {
roadName = currentRoadName == null ? currentRoadNumber : currentRoadName;
}
roadName ??= 'unnamed road';
return roadName;
}
You can get localized texts for the road name based on a list of preferred languages. If no language is available, the default language is returned. In most cases, this will be the name of the road as shown on the local signs. You can get the default road texts also directly via currentRoadTexts.names.getDefaultValue()
.
Note
Alternatively, use the RoadTextsListener
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. 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
Note that the maneuver instruction text (nextManeuver.text
) 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.
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 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_app example shows how to reroute after a deviation of 30 meters.
Note
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 listeners 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 to keep the made choices for an route alternative and route options.
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.
The HERE SDK offers several APIs to handle route deviations:
- Recalculate the entire route with the
RoutingEngine
with new or updated RouteOptions
to provide new route alternatives. - 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 calculate with the OfflineRouteEngine
does no longer include traffic information. - 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.
Note
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
.
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.
Note
As of now, the returnToRoute()
feature supports only car routes.
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
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
. - For use with the
OfflineRoutingEngine
, you will also need to set the normalized fraction of the route which was already travelled along the route, which is available from the RouteDeviation
event: routeDeviation.fractionTraveled
. For the online RoutingEngine
this parameter is ignored. The fractionTraveled
parameter is based on the last known location of the driver on the route. When the user left the route, no RouteProgress
will be delivered. This value is normalized to be a value between 0 (no progress) and 1 (destination reached). The part of the route that was already travelled will be ignored by the route calculation. - The new starting
Waypoint
, which may be the current map matched location of the driver.
The new starting point can be retrieved from the RouteDeviation
event:
MapMatchedLocation currentMapMatchedLocation = routeDeviation.currentLocation.mapMatchedLocation;
GeoCoordinates currentGeoCoordinates = currentMapMatchedLocation == null
? routeDeviation.currentLocation.originalLocation.coordinates
: currentMapMatchedLocation.coordinates;
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 != null && currentMapMatchedLocation.bearingInDegrees != null) {
newStartingPoint.headingInDegrees = currentMapMatchedLocation.bearingInDegrees;
}
Finally, we can calculate the new route:
routingEngine.returnToRoute(originalRoute, newStartingPoint, routeFractionTravelled, (routingError, routes) {
if (routingError == null) {
HERE.Route newRoute = routes.first;
} else {
}
});
Note
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
.
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) or shorter (based on route length) than the current route you are driving on.
The difference to the original route
is provided in meters and or seconds:
void _startDynamicSearchForBetterRoutes(HERE.Route route) {
try {
_dynamicRoutingEngine.start(
route,
DynamicRoutingListener((Route newRoute, int etaDifferenceInSeconds, int distanceDifferenceInMeters) {
print('DynamicRoutingEngine: Calculated a new route.');
print('DynamicRoutingEngine: etaDifferenceInSeconds: $etaDifferenceInSeconds.');
print('DynamicRoutingEngine: distanceDifferenceInMeters: $distanceDifferenceInMeters.');
}, (RoutingError routingError) {
final error = routingError.toString();
print('Error while dynamically searching for a better route: $error');
}));
} on DynamicRoutingEngineStartException {
throw Exception("Start of DynamicRoutingEngine failed. Is the RouteHandle missing?");
}
}
An example implementation for this can be found in the corresponding navigation example app.
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 based on 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, in most cases, getting location updates happens 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 towards 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(_hereMapController);
Screenshot: Turn-by-turn navigation example running on a device.
In addition, you can stop following the current location with:
_visualNavigator.cameraMode = CameraTrackingMode.disabled;
By default, camera tracking is enabled. And thus, the map is always centered on the current location. This can be temprorarily 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 = null
. Reset the above listeners to null or simply call stop()
on your location provider (depends on the actual implementation of your 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.
_visualNavigator.destinationReachedListener = DestinationReachedListener(() {
print('Destination reached. Stopping turn-by-turn navigation.');
stopNavigation();
});
_visualNavigator.milestoneReachedListener = MilestoneReachedListener((Milestone milestone) {
if (milestone.waypointIndex != null) {
print('A user-defined waypoint was reached, index of waypoint: ' + milestone.waypointIndex.toString());
print('Original coordinates: ' + milestone.originalCoordinates.toString());
} else {
print('A system defined waypoint was reached at ' + milestone.mapMatchedCoordinates.toString());
}
});
The lambda_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.
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.
Note
The provided speed limits are only valid for cars.
An implementation example can be found in the navigation_app example you can find on GitHub:
_visualNavigator.speedLimitListener = SpeedLimitListener((SpeedLimit speedLimit) {
double? currentSpeedLimit = _getCurrentSpeedLimit(speedLimit);
if (currentSpeedLimit == null) {
print("Warning: Speed limits unkown, data could not be retrieved.");
} else if (currentSpeedLimit == 0) {
print("No speed limits on this road! Drive as fast as you feel safe ...");
} else {
print("Current speed limit (m/s): $currentSpeedLimit");
}
});
double? _getCurrentSpeedLimit(SpeedLimit speedLimit) {
print("speedLimitInMetersPerSecond: " + speedLimit.speedLimitInMetersPerSecond.toString());
print("schoolZoneSpeedLimitInMetersPerSecond: " + speedLimit.schoolZoneSpeedLimitInMetersPerSecond.toString());
print("timeDependentSpeedLimitInMetersPerSecond: " + speedLimit.timeDependentSpeedLimitInMetersPerSecond.toString());
print("advisorySpeedLimitInMetersPerSecond: " + speedLimit.advisorySpeedLimitInMetersPerSecond.toString());
print("fogSpeedLimitInMetersPerSecond: " + speedLimit.fogSpeedLimitInMetersPerSecond.toString());
print("rainSpeedLimitInMetersPerSecond: " + speedLimit.rainSpeedLimitInMetersPerSecond.toString());
print("snowSpeedLimitInMetersPerSecond: " + speedLimit.snowSpeedLimitInMetersPerSecond.toString());
return speedLimit.effectiveSpeedLimitInMetersPerSecond();
}
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
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:
_visualNavigator.speedWarningListener = SpeedWarningListener((SpeedWarningStatus speedWarningStatus) {
if (speedWarningStatus == SpeedWarningStatus.speedLimitExceeded) {
SystemSound.play(SystemSoundType.click);
print('Speed limit exceeded.');
}
if (speedWarningStatus == SpeedWarningStatus.speedLimitRestored) {
print('Driver is again slower than current speed limit (plus an optional offset.)');
}
});
Note
Note that lambda_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 lambda_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 speedLimitRestored
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
:
void setupSpeedWarnings() {
double lowSpeedOffsetInMetersPerSecond = 2;
double highSpeedOffsetInMetersPerSecond = 4;
double highSpeedBoundaryInMetersPerSecond = 25;
SpeedLimitOffset speedLimitOffset = SpeedLimitOffset(
lowSpeedOffsetInMetersPerSecond,
highSpeedOffsetInMetersPerSecond,
highSpeedBoundaryInMetersPerSecond,
);
_visualNavigator.speedWarningOptions = 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.
Receive Safety Cameras Events
You can attach a SafetyCameraWarningListener
to the Navigator
or VisualNavigator
to get notfied on SafetyCameraWarning
events that inform on cameras that detect the speed of a driver.
Getting notifications on safety cameras - or speed cameras - is not available for each country. 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 RoadAttributesListener
you can receive events on the road attributes. The events are fired whenever an attribute changes - while you are traveling on that road.
_visualNavigator.roadAttributesListener = RoadAttributesListener((RoadAttributes roadAttributes) {
print('Received road attributes update.');
if (roadAttributes.isBridge) {
print('Road attributes: This is a bridge.');
}
if (roadAttributes.isControlledAccess) {
print('Road attributes: This is a controlled access road.');
}
if (roadAttributes.isDirtRoad) {
print('Road attributes: This is a dirt road.');
}
if (roadAttributes.isDividedRoad) {
print('Road attributes: This is a divided road.');
}
if (roadAttributes.isNoThrough) {
print('Road attributes: This is a no through road.');
}
if (roadAttributes.isPrivate) {
print('Road attributes: This is a private road.');
}
if (roadAttributes.isRamp) {
print('Road attributes: This is a ramp.');
}
if (roadAttributes.isRightDrivingSide) {
print('Road attributes: isRightDrivingSide = ' + roadAttributes.isRightDrivingSide.toString());
}
if (roadAttributes.isRoundabout) {
print('Road attributes: This is a roundabout.');
}
if (roadAttributes.isTollway) {
print('Road attributes change: This is a road with toll costs.');
}
if (roadAttributes.isTunnel) {
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
Note
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.
Note
Currently, left-hand driving countries are not supported.
The following code snippet shows how to retrieve the information which lanes to take:
_visualNavigator.maneuverViewLaneAssistanceListener =
ManeuverViewLaneAssistanceListener((ManeuverViewLaneAssistance laneAssistance) {
List<Lane> lanes = laneAssistance.lanesForNextManeuver;
logLaneRecommendations(lanes);
List<Lane> nextLanes = laneAssistance.lanesForNextNextManeuver;
if (nextLanes.isNotEmpty) {
print("Attention, the next next maneuver is very close.");
print("Please take the following lane(s) after the next maneuver: ");
logLaneRecommendations(nextLanes);
}
});
...
void logLaneRecommendations(List<Lane> lanes) {
int laneNumber = 0;
for (Lane lane in lanes) {
if (lane.recommendationState == LaneRecommendationState.recommended) {
print("Lane $laneNumber leads to next maneuver, but not to the next next maneuver.");
}
if (lane.recommendationState == LaneRecommendationState.highlyRecommended) {
print("Lane $laneNumber leads to next maneuver and eventually to the next next maneuver.");
}
if (lane.recommendationState == LaneRecommendationState.notRecommended) {
print("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:
if (_previousManeuverIndex != nextManeuverIndex) {
}
View the code for the RouteProgressListener
above and you can find how to get the nextManeuverIndex
, which will tell you when a new maneuver has to be taken.
In addition, 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 when one of the following conditions is met:
- The junction has at least a bifurcation.
- The junction has at least two lanes whose directions do not follow the current route.
Unlike ManeuverViewLaneAssistance
, you can detect when the junction has been passed by checking the list if it is empty or not:
_visualNavigator.junctionViewLaneAssistanceListener =
JunctionViewLaneAssistanceListener((JunctionViewLaneAssistance junctionViewLaneAssistance) {
List<Lane> lanes = junctionViewLaneAssistance.lanesForNextJunction;
if (lanes.isEmpty) {
print("You have passed the complex junction.");
} else {
print("Attention, a complex junction is ahead.");
logLaneRecommendations(lanes);
}
});
The JunctionViewLaneAssistance
may interfere with a ManeuverViewLaneAssistance
event, as it only recommends the lanes that allow you to pass the complex junction and to stay on the route. It can happen that this is not containing the same lane recommendations as the lanes from a previous ManeuverViewLaneAssistance
event that was sent earlier for a maneuver that takes place after the complex junction has been passed. Therefore, it is recommended to only use JunctionViewLaneAssistance
information as an additional hint for a driver while passing a complex junction. After the junction has been passed, make sure to highlight any previous ManeuverViewLaneAssistance
information - in case it is still valid and the manuever action has not been taken place (see above). Preferrably, show JunctionViewLaneAssistance
information in a separate view to not confuse drivers with quickly changing lane informations.
Either way, the ManeuverViewLaneAssistance
event can be equal to the JunctionViewLaneAssistance
event or it can contain lane recommendations that are also included in JunctionViewLaneAssistance
as JunctionViewLaneAssistance
can recommend more lanes to safely pass a complex junction - but not every of those lanes may lead to the next maneuver.
Keep in mind, that without a route to follow, you will not get any lane assistance related events.
Truck Guidance
The HERE SDK supports premium truck routing and guidance with a variety of features. During navigation you can attach a listener to get notified on truck restrictions ahead, such as narrow tunnels and so on, see the following code snippet:
_visualNavigator.truckRestrictionsWarningListener =
TruckRestrictionsWarningListener((List<TruckRestrictionWarning> list) {
for (TruckRestrictionWarning truckRestrictionWarning in list) {
print("TruckRestrictionWarning in ${truckRestrictionWarning.distanceInMeters} meters.");
if (truckRestrictionWarning.weightRestriction != null) {
WeightRestrictionType type = truckRestrictionWarning.weightRestriction!.type;
int value = truckRestrictionWarning.weightRestriction!.valueInKilograms;
print("TruckRestriction for weight (kg): ${type.toString()}: $value");
}
if (truckRestrictionWarning.dimensionRestriction != null) {
DimensionRestrictionType type = truckRestrictionWarning.dimensionRestriction!.type;
int value = truckRestrictionWarning.dimensionRestriction!.valueInCentimeters;
print("TruckRestriction for dimension: ${type.toString()}: $value");
}
}
});
You can calculate your route specifically for trucks. See the routing section for more details. 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. - 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
and tunnel categories. Those can also be set via TruckOptions
. - You can enable a map layer scheme that is optimized to show truck-specific information on the map:
MapScene.Layers.VehicleRestrictions
.
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
:
_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.
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 a location provider on GitHub. The navigation_app example shows how HERE Positioning can be used for navigation.
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 'package:here_sdk/core.dart' as HERE;
import 'package:here_sdk/core.errors.dart';
import 'package:here_sdk/navigation.dart' as HERE;
import 'package:here_sdk/routing.dart' as HERE;
class HEREPositioningSimulator {
HERE.LocationSimulator? _locationSimulator;
void startLocating(HERE.Route route, HERE.LocationListener locationListener) {
_locationSimulator?.stop();
_locationSimulator = _createLocationSimulator(route, locationListener);
_locationSimulator!.start();
}
void stop() {
_locationSimulator?.stop();
}
HERE.LocationSimulator _createLocationSimulator(HERE.Route route, HERE.LocationListener locationListener) {
final double speedFactor = 2;
final notificationIntervalInMilliseconds = 500;
HERE.LocationSimulatorOptions locationSimulatorOptions = HERE.LocationSimulatorOptions(
speedFactor,
notificationIntervalInMilliseconds,
);
HERE.LocationSimulator locationSimulator;
try {
locationSimulator = HERE.LocationSimulator.withRoute(route, locationSimulatorOptions);
} on InstantiationException {
throw Exception("Initialization of LocationSimulator failed.");
}
locationSimulator.listener = 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.
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_app example 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.
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.maneuverNotificationListener = ManeuverNotificationListener((String voiceText) {
print('Voice guidance text: $voiceText');
});
Here we just print the text. You can use a TTS plugin, such as flutter_tts, to translate the voice text to an audible message a driver can hear.
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 (enUs
, metric
):
void setupVoiceTextMessages() {
LanguageCode languageCode = LanguageCode.enGb;
List<LanguageCode> supportedVoiceSkins = VisualNavigator.getAvailableLanguagesForManeuverNotifications();
if (supportedVoiceSkins.contains(languageCode)) {
_visualNavigator.maneuverNotificationOptions = ManeuverNotificationOptions(languageCode, UnitSystem.metric);
} else {
print('Warning: Requested voice skin is not supported.');
}
}
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.
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. 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
- Serbian: sr-CS_anonymous_compact
- Swedish: sv-SE_alva_compact
- Thai: th-TH_kanya_compact
- Turkish: tr-TR_cem_compact
- Ukrainian: uk-UA_anonymous_compact
- Vietnamese: vi-VN_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
Unzip 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.
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.
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 null listener - see above.
Note that stopRendering()
is executed asynchronously. Therefore, it's best to await its completion via await _visualNavigator.stopRendering()
before you execute other sensitive code.
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.route = null;
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
Note that in tracking mode you only get events for listeners such as the NavigableLocationListener
or the SpeedWarningListener
that can fire without the need for a route to follow. Other listeners such as the RouteProgressListener
do not deliver events when a route is not set.
This enables you to keep your listeners 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 listeners 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.