Navigation

This module offers real-time tracking and guidance features for both gas vehicles and electric vehicles. You need to provide a GPS source using the GPS Manager module to initialize the navigation engine.

Dependencies

This module depends on other BeNomad's modules :

  • Core

  • Settings

  • Error Manager

  • Mapping

  • Planner

  • Vehicle Manager

  • Geocoder

  • GPS

  • Audio Manager

Note : in order to use this module, you have to initialize the Core module with a valid purchase UUID. See the documentation of the Core module for more details.

Other module's dependencies :

implementation "androidx.appcompat:appcompat:1.3.0"
implementation "com.google.android.material:material:1.8.0"
implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
implementation "androidx.navigation:navigation-ui-ktx:2.3.5"
implementation "androidx.core:core-ktx:1.9.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1"

Tracking mode

The Tracking mode allows the driver to have access to real-time data while driving :

  • Current speed : the current speed in km/h of the driver

  • Current speed limit : the current speed limit in km/h of the current road

  • Current address : the current address that corresponds to the driver location

  • Current GPS position : the current user "raw" location

  • Current "map-matched" position : the map-matched location that corresponds to the position of the user on the current road.

  • Current "map-matched heading" : the current orientation of the vehicle (angle) on the current map-matched location in degrees

You can find more details in the API Reference of the TrackingProgress class.

Note: For an electric vehicle, the driver can also have access to the current energy load that is estimated by the SDK based on the vehicle profile, the SoC value given by the driver and its GPS location updates. Check the SoC estimation section for more details.

Here are the steps to start a tracking session and have access to this data :

val source = LocationFromBuiltInGPS(applicationContext) //other gps data sources are available, check the documentation of the GPSManager for more details
val wasGpsStarted = if (source.isFeatureEnabled()) {
GPSManager.start(source)
} else {
null
}
val error: Error?
if (wasGpsStarted != null) {
if (wasGpsStarted) {
error = Navigation.getInstance().initLocationDataSource(source)
} else {
val enum = GPSErrorCodes.SOURCE_NOT_STARTED
error = Error(ErrorType.GPS, enum.code, enum.messageId, enum.detailedMessage)
}
} else {
val enum = GPSErrorCodes.SOURCE_NOT_AVAILABLE
error = Error(ErrorType.GPS, enum.code, enum.messageId, enum.detailedMessage)
}
//handle error or continue to step 2

Note: you can use Navigation.isInit method to check if navigation engine is initialized with a LocationDataSource.

The instance has to be initialized with the application context to initialize the AudioManager used for vocal instructions and load icons.

val navigation = Navigation.getInstance(applicationContext)

You can also specify a custom path for the navigation icon using navigationIconPath argument.

The MapView must be attached after the OnMapReady callback has been called. Check the documentation of the Map module for more details.

//mapViewError is null if no error occurred while attaching the MapView
val mapViewError = navigation.attachMapView(mapView)

if(mapViewError == null) {
//see the NavigationMapViewMode enum class for more details about all available view modes
navigation.setNavigationMapViewMode(NavigationMapViewMode.GUIDANCE_VIEW_AUTO) //can be changed after session is started
//continue to step 3
} else {
//handle error
}

There are several listeners that you can use for a navigation session. Its important that you implement the NavigationErrorListener to be notified when an error occurs during a navigation To do that, add the NavigationErrorListener using Navigation.addNavigationErrorListener(listener: NavigationErrorListener) Then, you have to override onNavigationError(error: Error, state: NavigationProgressState? = null)

Here are the other listeners that you can use :

ArrivalListener : callbacks called when reaching via-points and destination.
Add your listener using Navigation.addArrivalListener(listener: ArrivalListener)

ReroutingListener : notifies when a re-route starts.
Add your listener using Navigation.addReroutingListener(listener: ReroutingListener)

CurrentRouteListener : notifies when a re-route is finished and provides the up-to-date Route object.
Add your listener using Navigation.addCurrentRouteListener(listener: CurrentRouteListener)

InstructionsListener : to get instructions' icons and textual instructions in real-time.
Add your listener using Navigation.addInstructionsListener(listener: InstructionsListener)

SpeedLimitListener : notifies about speed limit changes during the navigation.
Add your listener using Navigation.addSpeedLimitListener(listener: SpeedLimitListener)

NavigationProgressListener : notifies about the navigation progress by returning updated info each second.
Add your listener using Navigation.addNavigationProgressListener(listener: NavigationProgressListener)


Check the API reference of the Navigation module (package listener) for more details.

Note : don't forget to add your listeners, otherwise callbacks won't be triggered

In order to automatically remove listeners without the need to call Navigation.removeAllNavigationProgressListeners(), you can make the Navigation instance observe the lifecycle of your activity :

lifecycle.addObserver(Navigation.getInstance()) //call this from your Activity class

That way, session will be stopped and observers removed if activity is destroyed.

5. Start the tracking session

To get tracking data updates :

//the view model implements the NavigationProgressListener and gets data updates in the onNavigationProgressChanged listener
navigation.addNavigationProgressListener(viewModel)

In the view model :

override fun onNavigationProgressChanged(navigationProgress: TrackingProgress) {
//update your view model
}

Then you can start the tracking session :

//sessionError is null if no error occurred while starting the tracking session
val sessionError = navigation.startSession()

You should call startSession from an async context and handle result on main thread

Note : if you want to start a foreground service to have location updates while the application is in background, you have to specify the first argument which is the context, and the second which is required data for the foreground notification.

To stop the tracking session :

//you can pass null instead of the application context if you didn't pass it in the startSession method. Its only needed here to stop the foreground notification service
navigation.stopSession(applicationContext)

Guidance mode

The Guidance mode allows the driver to have access to the tracking data and also the data about its current navigation on the passed route. Check the documentation of the Planner module to see how you can compute routes that will be used for the guidance mode.

Take note that the first point of the route passed to the navigation engine is not considered as a via-point (ArrivalListener.onViaPointReached listener won't be called). It is considered as the real user location. It means that user is not guided to the first item of Route.waypoints but to the next one. For example, if the computed route has only a departure and a destination, user is directly guided to the destination when guidance is started (as the departure corresponds to its location). If the first point of the route does not correspond to the current GPS location, it could be necessary to inject the current GPS location as the first point of the route (guidance will start immediately) or duplicate the first point if you want to be notified by the ArrivalListener.onViaPointReached listener.

Pre-requisites

In order to start a guidance session, follow the 4 first steps as to start a tracking session. Then, follow the next steps.

1. Define the guidance's view mode

navigation.setNavigationMapViewMode(NavigationMapViewMode.GUIDANCE_VIEW_AUTO) //can be changed after session is started

Check the NavigationMapViewMode enumeration in the API reference to see all available view modes

2. Get guidance data updates

//the view model implements the NavigationProgressListener and gets data updates in the onNavigationProgressChanged callback
navigation.addNavigationProgressListener(viewModel)

In the view model, use smart cast to get guidance data updates :

override fun onNavigationProgressChanged(navigationProgress: TrackingProgress) {
if(navigationProgress is GuidanceProgress){
//update your view model with guidance data updates
}
}

3. Start a guidance session

//null if no error occurred while starting the guidance session
val sessionError = navigation.startSession(route = computedRoute) //computedRoute is a Route object obtained using Planner module

You should call Navigation.startSession from an async context and handle result on main thread There are other arguments for Navigation.startSession that are explained in the dedicated sections : Notification with guidance data and Instructions.

When a view mode other than NavigationMapViewMode.GUIDANCE_VIEW_FREE is defined with Navigation.setNavigationMapViewMode, gestures are disabled.

To enable them when user touches the map, there is a dedicated listener :

val navigation = Navigation.getInstance()

mapView.onUserInteractionListener = object : OnUserInteractionListener {
override fun onMapViewTouched() {
if (navigation.getNavigationMapViewMode() != NavigationMapViewMode.GUIDANCE_VIEW_FREE) {
navigation.setNavigationMapViewMode(NavigationMapViewMode.GUIDANCE_VIEW_FREE)

if (navigation.getSessionState() == SessionState.GUIDANCE) {
//show a button to set previous view mode
}
}
}

To enable them when map center changes (for example after calling MapView.zoomTo), there is another listener :

val navigation = Navigation.getInstance()

mapView.onUserInteractionListener = object : OnMapCenterChangeListener {
override fun onCenterChanged() {
if (navigation.getNavigationMapViewMode() != NavigationMapViewMode.GUIDANCE_VIEW_FREE) {
navigation.setNavigationMapViewMode(NavigationMapViewMode.GUIDANCE_VIEW_FREE)

if (navigation.getSessionState() == SessionState.GUIDANCE) {
//show a button to set previous view mode
}
}
}

Re-routing

When you start a guidance session, the route object passed to Navigation.startSession won't be up-to-date during guidance. It means that you can't use it during a guidance session. This Route object will be updated each time a re-route has finished. It happens when the current itinerary is re-calculated (for instance, when the user drives off the itinerary).

To get data from an up-to-date Route object, you have to use the dedicated listener. Before starting the guidance session, add your listener using method addCurrentRouteListener(listener: CurrentRouteListener) of Navigation class. Then, the object/class of CurrentRouteListener type can override the method fun onNewRoute(route: Route, evOptimChargeResult: OptimChargeResult?) to receive the updated Route object.

You can also be notified when a re-route has started. To be notified, add your listener before starting the guidance session using method addReroutingListener(listener: ReroutingListener) of Navigation class. Then, the object/class of ReroutingListener type can override the method fun onRerouteStart() to be notified when a re-route starts.

Notification with guidance data

When starting a guidance session using Navigation.startSession, you can decide to use a standard notification service that will show :

  • Remaining distance to destination

  • Remaining duration to destination

If the guidance session uses a Route calculated using an EVProfile, is also shows :

  • Current SoC (estimated by the SDK)

  • Estimated SoC at next ChargePoolStop of the Route (check Route.waypoints in API reference)

To enable the notification service, you have to specify both appContext and notificationContent arguments of Navigation.startSession method. If ForegroundNotificationContent.symbolicManeuverStyle is specified, symbolic maneuvers will be shown in the notification during guidance.

To stop the notification service, call `navigation.stopForegroundNotification(applicationContext)`. The service will automatically be stopped when application is stopped.

Using this kind of Service allows the application to access background location updates without limitation. You can also build your own Foreground service in order to keep these location updates when your app is in background.

Instructions

To receive instructions during guidance, you can use dedicated listeners of InstructionsListener.

First, add your listener using method addInstructionsListener(listener: InstructionsListener) of Navigation class. (add it before starting a guidance session). Then, the object/class of InstructionsListener type can override onNewTextInstruction(instruction: String) to receive textual instructions in real-time.

In order to receive also maneuvers/signposts icons, the object/class of InstructionsListener type have to override onNewInstruction(instruction: String, maneuver: Bitmap?, chainedManeuver: Bitmap?, signpost: Bitmap?) to receive both text and instructions' icons.

To receive these icons, styles has to be defined in Navigation.startSession using the parameter instructionsIconsStyles.

This parameter of type InstructionsIconsStyles allows you te define a different style for :

  • Current maneuver style is defined with InstructionsIconsStyles.maneuverStyle

  • Next chained maneuver style is defined with InstructionsIconsStyles.chainedManeuverStyle

  • Signpost style is defined with InstructionsIconsStyles.signPostStyle

Note: if a style is not defined, you won't receive the corresponding icon. It means that if you only want to receive the current maneuver icon, you only have to define the InstructionsIconsStyles.maneuverStyle

Custom instructions' icons

There are 5 instructions types that do not have dedicated maneuvers icon. A maneuvers icon will be generated, but it won't be a dedicated icon for the maneuver. For example, the generated icon for the EXIT_MOTORWAY instruction could be an icon indicating to turn right, but it won't have any explicit distinction with an icon that indicates to turn right.

You have the possibility to override the icons for these 5 instructions (from enum class InstructionType) :

ENTER_MOTORWAY
EXIT_MOTORWAY
TAKE_FERRY
LEAVE_FERRY
STOP

To override the generated icons, you have to use png images that must be deployed when initializing the Core (check the Core module documentation for more info about the resources deployment).

These custom instructions' icons must be located in the deployed resources. The default relative path from the deployed resources is : img custom_maneuvers. (you can find an example in the samples, in the assets of the application) You can change the relative path of these custom instructions' icons with CustomInstructionsIconBuilder.relativeCustomIconsPath member.

As you can define a different style for chained maneuvers in startSession.instructionsIconsStyles, you can do the same with custom icons, by defining other png files for the chained maneuvers. By default, these custom icons for chained maneuvers are located in img custom_maneuvers chained_maneuvers. You can change the relative path of these custom icons for chained maneuvers with CustomInstructionsIconBuilder.chainedManeuversRelativePath, which is the relative path for these custom chained maneuvers icons from CustomInstructionsIconBuilder.relativeCustomIconsPath.

To be able to handle dark/light mode, there are custom icons dedicated to the light mode, and custom icons dedicated to the dark mode. The name of the custom icons for the light mode must be :

enter_motorway_light_mode.png -> ENTER_MOTORWAY
exit_motorway_light_mode.png -> EXIT_MOTORWAY
take_ferry_light_mode.png -> TAKE_FERRY
leave_ferry_light_mode.png -> LEAVE_FERRY
stop_light_mode.png -> STOP

The name of the custom icons for the dark mode must be :

enter_motorway_dark_mode.png -> ENTER_MOTORWAY
exit_motorway_dark_mode.png -> EXIT_MOTORWAY
take_ferry_dark_mode.png -> TAKE_FERRY
leave_ferry_dark_mode.png -> LEAVE_FERRY
stop_dark_mode.png -> STOP

You can check the assets folder of the samples for a full example.

The mode can be specified in startSession.instructionsIconsStyles, using InstructionsIconsStyles.useDarkModeIcons. If set to false, icons dedicated to the light mode will be used, otherwise icons dedicated to the dark mode will be used. You will receive these custom icons in the dedicated listener onNewInstruction(instruction: String, maneuver: Bitmap?, chainedManeuver: Bitmap?, signpost: Bitmap?)

Note: These custom icons has to be loaded first by calling Navigation.getInstance(applicationContext). You have to call it once (you must pass the application context in argument, otherwise icons won't be loaded) before starting a session.

Attributions for custom instructions' icons located in the assets of the samples (benomad_resources/img/custom_maneuvers)

Link to the source of "enter_highway_black.png", "enter_highway_white.png", "exit_highway_black.png" and "exit_highway_white.png" resources

Link to the source of "enter_ferry_black.png", "enter_ferry_white.png", "exit_ferry_black.png", "exit_ferry_white.png" resources

Link to the source of "arrival_flag_black.png" and "arrival_flag_white.png" resources

Build a list of instructions' icons in guidance

First, check the section "Build a list of instructions' icons" in the documentation of the Planner module.

If you want to keep an up-to-date list of instructions during guidance, you have to initialize the SymbolicManeuverBuilder with the new Route object (check Re-routing) :

override fun onNewRoute(route: Route, evOptimChargeResult: OptimChargeResult?) {
symbolicManeuverBuilder?.initialize(style, route)
//build new list of instructions
}

To start a navigation session with EV features, the route must be computed and optimized for a specific EVProfile. Check the documentation of the Planner module, section EV features.

SoC estimation

The navigation engine is capable of estimating the current SoC (state of Charge) of the vehicle in real time during a tracking or a guidance session.
To retrieve this SoC, use the NavigationProgressListener (check how to use it in 2. Get guidance data updates) :

override fun onNavigationProgressChanged(navigationProgress: TrackingProgress) {
/**
* Estimation of the vehicle's SoC in real time
*/
//get battery capacity of the vehicle set in the RoutePlan to compute the route
val batteryCapacity = (route?.routeOptions?.vehicle?.vehicleModel?.profile as EVProfile?)?.battery ?: -1.0
val energyLoad = navigationProgress.currentEnergyLoad
val soc =
if (batteryCapacity >= 0.0 && energyLoad >= 0.0) Vehicle.getEnergyLoadInPercentage(
energyLoad,
batteryCapacity
) else -1.0
if(soc >= 0) {
//valid SoC value
} else {
//invalid SoC value
}

/**
* Estimation of the vehicle's SoC for the next charge stop in real time
*/
if (navigationProgress is GuidanceProgress) {
val energyLoadAtNextChargeStop = navigationProgress.energyLoadAtNextChargeStop
val socAtNextChargeStop =
if (batteryCapacity >= 0.0 && energyLoadAtNextChargeStop >= 0.0) Vehicle.getEnergyLoadInPercentage(
energyLoadAtNextChargeStop,
batteryCapacity
) else -1.0
if(socAtNextChargeStop >= 0) {
//valid SoC value for the next charge stop
} else {
//invalid SoC value
}
}
}

Update current SoC / EV Parameters

To update the current SoC of the vehicle during guidance, use Navigation.updateVehicleStatus(vehicleStatus: VehicleStatus) : Error? To update the RechargeParameters, use Navigation.updateRouteOptions(routeOptions: RouteOptions): Error?

Here is an example that updates current SoC (VehicleStatus), minimum SoC and minimum SoC at arrival (RechargeParameters) :

val newSoc = ... //new SoC value
val newSocMin = ... //new minimum SoC
val newSocMinAtArrival = ... //new minimum SoC at arrival
val navigation = Navigation.getInstance()

//Update vehicle status (SoC)
var needReroute = false
val vehicle = route.routeOptions?.vehicle
val vehicleStatus = vehicle?.status
if (vehicleStatus != null) {
val profile = vehicle.vehicleModel?.profile
if (profile != null && profile is EVProfile) {
vehicleStatus.currentEnergyLoad =
Vehicle.getEnergyLoadFromPercentage(newSoc, profile.battery)

val statusError = navigation.updateVehicleStatus(vehicleStatus)
if(statusError != null) {
//handle error
} else {
needReroute = true
}
}
}

//Update recharge parameters (minimum SoC / minimum SoC at arrival)
val routeOptions = route.routeOptions
if (routeOptions != null && routeOptions is EVRouteOptions) {
if (routeOptions.rechargeParameters.socMin != newSocMin || routeOptions.rechargeParameters.socMinArrival != newSocMinAtArrival) {
routeOptions.rechargeParameters.socMin = newSocMin
routeOptions.rechargeParameters.socMinArrival = newSocMinAtArrival
val routeOptionsError = navigation.updateRouteOptions(routeOptions)
if(routeOptionsError != null) {
//handle error
} else {
needReroute = true
}
}
} else {
//issue with the route object : it should have defined EVRouteOptions
}

if (needReroute) {
navigation.reroute() //must be called to update guidance
}

EV alerting

You can use a dedicated listener to be notified when next charge stop is not reachable with a SoC >= minimal SoC defined in RechargeParameters.socMin of the EVRoutePlan used to compute the Route. First, add the listener using Navigation.addEVAlertingListener(listener: EVAlertingListener) Then, add the method override : override fun onEnergyLoadAlert(isReachable: Boolean)

The callback is triggered when the state changes : if next charge stop becomes unreachable with defined RechargeParameters, onEnergyLoadAlert is triggered and isReachable value is false. Then, if the next charge stop becomes reachable again (for instance, after updating the current SoC value of after an EV-rerouting), the callback is triggered and isReachable value is true.

EV re-routing

To start a new EV optimization of the itinerary during guidance (for instance, when the EV alerting callback is triggered and user wants to optimize the itinerary to find a solution using defined EVParameters), you have to call Navigation.reroute(length : Int = 0, offset : Int = 0, findCharges: Boolean = false) and specify the findCharges argument to true :

navigation.reroute(findCharges = true)

Then, a re-route will start and you'll obtain the result of the optimization in the onNewRoute(route: Route, evOptimChargeResult : OptimChargeResult?) callback :

override fun onNewRoute(route: Route, evOptimChargeResult: OptimChargeResult?) {
val optimizationResultText = evOptimChargeResult.text //optimization's result text in english
val optimizationResultCode = evOptimChargeResult.code //code can be used to show a custom text
}

If a new solution was found, the new Route object contains updated charge stops in Route.waypoints. If no solution was found, the new Route object contains same charge stops as before.

Note : a tolerance can be applied when using EV re-routing, check "Minimum SoC tolerance" section of the Planner module for more info.

Charge stop info

When a route is optimized for an electric vehicle, charge stops are added as via-points along the route, based on the EVRouteOptions defined in the EVRoutePlan used to compute the Route.

During guidance, the SoC estimated for the next charge stop can change, and therefore the charge duration needed at the next charge stop can change too in order to respect the planned SoC at the end of the charge (at the departure of the charging station).

This duration doesn't take into account the defined RechargeParameters.stopFixedTime.

Here is how you can get the real-time charge duration needed for the next charge stop :

override fun onNavigationProgressChanged(navigationProgress: TrackingProgress) {
if (navigationProgress is GuidanceProgress) {
//Get charge duration at next charge stop in minutes
val chargeDuration =
TimeConverter.convertSecondsToMinutes(navigationProgress.nextChargeStopDuration)
}

. . .

}

Make sure to check Re-routing section first.

Here is how to get info of the next charge stop during guidance :

//viewModel.nextChargeStopDuration corresponds to GuidanceProgress.nextChargeStopDuration from onNavigationProgressChanged callback of NavigationProgressListener
val nextChargeStopDuration = viewModel.nextChargeStopDuration.value
//Check that there is at least one remaining charge stop on the itinerary
if(nextChargeStopDuration != null && nextChargeStopDuration 0.0) {
//route object is obtained in onNewRoute callback
val waypoints = route.waypoints
if(waypoints != null) {
var nextChargePoolStop: ChargePoolStop? = null
for(routeWaypoint in waypoints.withIndex()) {
//viewModel.nextWaypointIndex corresponds to GuidanceProgress.nextWaypointIndex from onNavigationProgressChanged callback of NavigationProgressListener
if(routeWaypoint.index >= (viewModel.nextWaypointIndex.value ?: -1)) {
if(routeWaypoint.value.chargePoolStop != null){
nextChargePoolStop = routeWaypoint.value.chargePoolStop!!
break
}
}
}
if(nextChargePoolStop != null) {
//Get the SoC after charge planned for this route
val socAfterCharge = nextChargePoolStop.socAfterCharge
//Other info about the ChargePoolStop are available, you'll find all details in the API reference
} else {
//no charge pool stop left on the route
}
} else {
//issue with the Route object
}
}

Safety Camera Alerts

Safety camera alerts are supported in the Navigation module when they are available in the map data. Alerts can only work when a navigation is in progress, whether in guidance or tracking mode.

You can enable or disable the alerts using the member: isSafetyCameraAlertsEnabled (enabled by default). When the alerts are enabled, the Navigation will notify:

  • with audio alerts when entering a safety camera alert zone,

  • with the SafetyCameraAlertListener when entering / leaving a safety camera alert zone.

First, add the listener using Navigation.addSafetyCameraAlertListener(listener: SafetyCameraAlertListener) Then, add the method overrides :

override fun onSafetyCameraAlertStart(type: SafetyCameraType, speedLimit: Int?) {
// Your code
}

override fun onSafetyCameraAlertStop() {
// Your code
}

Note: all safety cameras are hidden on the map.

Packages

Link copied to clipboard
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard