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 2Note: 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 classThat 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 startedCheck 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 moduleYou 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.
Circuit navigation is a specialized navigation mode designed for closed-loop routes (circuits). It allows drivers to follow a circuit with advanced features like:
Position tracking as percentage (0-100%) along the circuit
Free drive mode when leaving the circuit - no automatic rerouting, driver rejoins manually
Automatic reroute mode - system automatically guides back to the circuit when driver leaves
Automatic detection of rejoining the circuit
Tracking of missed sections when driver leaves and rejoins the circuit
Support for multiple laps - percentage can exceed 100%
Circuit navigation is managed through the CircuitNavigation singleton class, which extends the standard Navigation features.
Pre-requisites
Before starting a circuit navigation session, you must:
Complete steps 1-4 from Tracking mode (initialize GPS, Navigation instance, attach MapView, add listeners)
Map-match your circuit waypoints using
GeoDecoder.mapMatchCircuitWaypoints()(see Geocoder module documentation)Important: You MUST use
mapMatchCircuitWaypointsfor circuit navigation. Other map-matching methods are not compatible with circuit navigation.Have a computed
Routeobject that represents your circuit (obtained from the Planner module using the map-matched waypoints)Define your
CircuitOptionsto configure circuit behavior
First, prepare your circuit waypoints by map-matching them:
import com.benomad.msdk.geocoder.GeoDecoder
import com.benomad.msdk.planner.waypoint.Waypoint
import com.benomad.msdk.navigation.TranspMode
// Define your circuit waypoints
val circuitWaypoints: List<Waypoint> = listOf(
Waypoint(GeoPoint(2.38305, 48.902475)),
Waypoint(GeoPoint(2.38405, 48.903475)),
// ... more waypoints defining your circuit
)
// Map-match the waypoints specifically for circuit navigation
GeoDecoder.mapMatchCircuitWaypoints(
transportationMode = TranspMode.EMERGENCY,
waypoints = circuitWaypoints,
callback = object : MapMatchedPointCallback {
override fun onResult(matches: List<MapMatchedPointResult>, matchCount: Int) {
if (matches.isNotEmpty()) {
// Extract map-matched waypoints
val mapMatchedWaypoints = matches.mapNotNull { it.mapMatchedWaypoint }
// Create a route plan with the map-matched waypoints
val routePlan = RoutePlan(
waypoints = mapMatchedWaypoints,
routeOptions = RouteOptions(/* your options */)
)
// Compute the circuit route
Planner.computeRoute(routePlan, object : RouteCallback {
override fun onRoute(route: Route?) {
if (route != null) {
// Now you can start circuit navigation with this route
startCircuitNavigation(route)
}
}
override fun onError(error: Error) {
// Handle error
}
})
}
}
override fun onError(error: Error) {
// Handle map-matching error
}
},
startUTurnThreshold = 3000
)Then, configure the circuit options and start the session:
import com.benomad.msdk.navigation.circuit.CircuitNavigation
import com.benomad.msdk.navigation.circuit.CircuitOptions
import com.benomad.msdk.navigation.circuit.PercentageInterval
import com.benomad.msdk.navigation.circuit.RerouteMode
// This function would be called from the route callback above
fun startCircuitNavigation(computedCircuitRoute: Route) {
val circuitNavigation = CircuitNavigation.getInstance()
// Configure circuit options
val circuitOptions = CircuitOptions(
interval = PercentageInterval(start = 0.0, end = 100.0), // Define the active circuit section
blockFromPercent = 100.0 // Percentage from which the route is blocked (100->nothing is blocked)
)
// Set the reroute mode (choose one)
circuitNavigation.rerouteMode = RerouteMode.REROUTE // Automatic rerouting when leaving circuit
// OR
circuitNavigation.rerouteMode = RerouteMode.FREE_DRIVE // Manual free drive mode when leaving circuit
// Start the circuit navigation session
val sessionError = circuitNavigation.startSession(
appContext = applicationContext,
notificationContent = notificationContent, // Optional, for foreground service
route = computedCircuitRoute, // Your circuit route (computed from map-matched waypoints)
instructionsIconsStyles = iconsStyles, // Optional, for instruction icons
circuitOptions = circuitOptions
)
if (sessionError != null) {
//handle error
}
}Important notes:
The circuit route MUST be created from waypoints that were map-matched using
GeoDecoder.mapMatchCircuitWaypoints(). Using waypoints from other map-matching methods will not work correctly with circuit navigation.The
circuitLengthproperty is automatically set when starting a circuit session and can be used to compute distance from percentage.See the complete workflow example above showing how to: map-match waypoints → compute route → start circuit navigation.
During circuit navigation, progress updates are delivered through the NavigationProgressListener, but with specialized progress types:
CircuitProgress(extendsGuidanceProgress) - used when actively following the circuit with guidanceFreeDriveProgress(extendsTrackingProgress) - used during free drive mode when off the circuit
Both types include circuit-specific properties:
circuitPercentage- position along the circuit (0.0 to 100.0)circuitNavigationState- current state of circuit navigation
override fun onNavigationProgressChanged(navigationProgress: TrackingProgress) {
when (navigationProgress) {
is CircuitProgress -> {
// Active circuit guidance
val percentage = navigationProgress.circuitPercentage // 0.0 to 100.0
val state = navigationProgress.circuitNavigationState // ON_CIRCUIT
// ... all GuidanceProgress properties available
}
is FreeDriveProgress -> {
// Free drive mode (off circuit)
val percentage = navigationProgress.circuitPercentage
val state = navigationProgress.circuitNavigationState // FREE_DRIVE
// ... all TrackingProgress properties available
}
is GuidanceProgress -> {
// Standard guidance (non-circuit)
}
else -> {
// Standard tracking
}
}
}The CircuitNavigationListener provides callbacks for circuit-specific events:
import com.benomad.msdk.navigation.listener.CircuitNavigationListener
import com.benomad.msdk.navigation.circuit.CircuitNavigationState
import com.benomad.msdk.core.svs.GeoPoint
val circuitNavigation = CircuitNavigation.getInstance()
circuitNavigation.addCircuitNavigationListener(object : CircuitNavigationListener {
override fun onCircuitNavigationStateChanged(
newState: CircuitNavigationState,
percentage: Double,
position: GeoPoint
) {
// Called whenever the circuit navigation state changes
// States: ON_CIRCUIT, ROUTING_TO_CIRCUIT, FREE_DRIVE
when (newState) {
CircuitNavigationState.ON_CIRCUIT -> {
// Driver is following the circuit
}
CircuitNavigationState.ROUTING_TO_CIRCUIT -> {
// Driver is drivind to the circuit, or left the circuit and being rerouted back
}
CircuitNavigationState.FREE_DRIVE -> {
// Driver is in free drive mode
}
}
}
override fun onCircuitLeft(percentage: Double, position: GeoPoint) {
// Called when the driver leaves the circuit path
// percentage: where on the circuit the driver left (0.0-100.0)
// position: geographic location where they left
}
override fun onCircuitResumed(
percentage: Double,
position: GeoPoint,
missedGeometry: List<GeoPoint>
) {
// Called when the driver rejoins the circuit after leaving it
// missedGeometry: list of points representing the section they missed
// Can be used to add a polyline on the map to show all missed sections
}
override fun onCanJoinCircuit(toPercent: Double) {
// Called periodically (every seconds) during FREE_DRIVE mode
// when the vehicle is back on the circuit but too far from where he left it
// toPercent: percentage where the vehicle could rejoin
// User can then decide to join manually:
// circuitNavigation.joinCircuit()
}
})Important: Don't forget to remove listeners when no longer needed:
circuitNavigation.removeCircuitNavigationListener(listener)
// or
circuitNavigation.removeAllCircuitNavigationListeners()4. Free drive mode
Free drive mode allows the driver to leave the circuit without automatic rerouting. The navigation system stops guiding back to the circuit, and the driver must manually rejoin.
Enabling free drive mode:
val error = circuitNavigation.enableFreeDrive()
if (error != null) {
//handle error (e.g., not in circuit navigation mode)
}When to use:
Set
rerouteMode = RerouteMode.FREE_DRIVEbefore starting the session to automatically enter free drive when leaving the circuitOr call
enableFreeDrive()manually during a session when the driver leaves the circuit
Rejoining the circuit:
While in free drive mode, when the driver rejoins the circuit and depending on the position on the circuit, either the Navigation resumes automatically or the onCanJoinCircuit(toPercent: Double) callback is triggered every seconds and needs to manually rejoin by calling CircuitNavigation.joinCircuit.
The threshold is by default set to 500 meters. That means that if the driver rejoins the circuit less than 500 meters from where he left the circuit the navigation resumes, otherwise he'll need to do it manually.
override fun onCanJoinCircuit(toPercent: Double) {
// Show UI to user asking if they want to rejoin
// If user confirms:
val success = circuitNavigation.joinCircuit()
if (success) {
// Successfully rejoined circuit at toPercent
}
// Otherwise we stay in Free Drive
}5. Reroute mode
Reroute mode changes the behavior of the navigation when the driver leaves the circuit.
Setting reroute mode:
import com.benomad.msdk.navigation.circuit.RerouteMode
val circuitNavigation = CircuitNavigation.getInstance()
// Automatic rerouting (default behavior)
circuitNavigation.rerouteMode = RerouteMode.REROUTE
// Free drive mode (no automatic rerouting)
circuitNavigation.rerouteMode = RerouteMode.FREE_DRIVEBehavior:
RerouteMode.REROUTE- When the driver leaves the circuit, navigation automatically calculates a route back to the circuit and provides guidanceRerouteMode.FREE_DRIVE- When the driver leaves the circuit, navigation enters free drive mode without automatic rerouting
You can change the reroute mode at any time during the circuit navigation session.
The CircuitNavigationState enum represents the current state of the circuit navigation:
enum class CircuitNavigationState {
ON_CIRCUIT, // Vehicle is following the circuit path with active guidance
ROUTING_TO_CIRCUIT, // Vehicle has left the circuit and is being rerouted back
FREE_DRIVE // Vehicle is in free drive mode, no automatic rerouting
}You can access the current state through:
CircuitProgress.circuitNavigationStateFreeDriveProgress.circuitNavigationStateCircuitNavigationListener.onCircuitNavigationStateChanged()callback
7. Utility functions
The CircuitNavigation class provides utility functions for working with circuit positions:
val circuitNavigation = CircuitNavigation.getInstance()
// Convert a geographic position to percentage along circuit (0.0-100.0)
val position = GeoPoint(longitude, latitude)
val percentage: Double? = circuitNavigation.getCircuitPercentageFromPosition(position)
// Convert a percentage to geographic position
val percent = 50.0
val geoPoint: GeoPoint? = circuitNavigation.getCircuitPositionFromPercent(percent)
// Control visibility of passed circuit segments on the map
circuitNavigation.hidePassedCircuit(applicationContext, hide = true)
// Get the circuit length (available after starting a session)
val length: Long = circuitNavigation.circuitLength // in metersTo stop the circuit navigation session:
val circuitNavigation = CircuitNavigation.getInstance()
// Pass context if you started with foreground notification
val error = circuitNavigation.stopSession(applicationContext)
if (error != null) {
//handle error
}This will:
Stop the navigation session
Clear circuit-specific state
Reset the circuit length to 0
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
}
}
}Vehicle positioning on the map
You can customize the position of the vehicle symbol on the map during navigation sessions using the following methods:
Vertical positioning
The setVehicleVerticalPosition method allows you to set the vertical position of the vehicle symbol on the map. The position is defined as a percentage of the MapView height, measured from the bottom of the screen.
val navigation = Navigation.getInstance()
// Set vehicle at 30% from the bottom of the screen (default is 23%)
val success = navigation.setVehicleVerticalPosition(0.30)
if (success) {
// Vehicle position was successfully updated
} else {
// No MapView is attached
}The verticalVehiclePos parameter accepts a value between 0.0 and 1.0:
0.0places the vehicle at the bottom of the screen0.23(default) places the vehicle at 23% from the bottom1.0places the vehicle at the top of the screen
Parameter validation: Values outside the range 0.0, 1.0 are rejected and the method returns false without changing the vehicle position.
This method returns true if the position was successfully set, or false if:
No MapView is currently attached to the Navigation instance, or
The parameter value is outside the valid range 0.0, 1.0
Horizontal positioning
The setVehicleHorizontalPosition method allows you to set the horizontal position of the vehicle symbol on the map. The position is defined as a percentage of the MapView width, where 0.5 represents the center.
val navigation = Navigation.getInstance()
// Shift vehicle to the left (0.3 = 30% from left edge)
val success = navigation.setVehicleHorizontalPosition(0.3)
if (success) {
// Vehicle position was successfully updated
} else {
// No MapView is attached
}The horizontalVehiclePos parameter accepts a value between 0.0 and 1.0:
0.0places the vehicle at the left edge of the screen0.5(default) centers the vehicle horizontally1.0places the vehicle at the right edge of the screen
Parameter validation: Values outside the range 0.0, 1.0 are rejected and the method returns false without changing the vehicle position.
This method returns true if the position was successfully set, or false if:
No MapView is currently attached to the Navigation instance, or
The parameter value is outside the valid range 0.0, 1.0
Note: These methods can be called at any time after attaching a MapView to the Navigation instance, whether a navigation session is running or not.
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
ChargePoolStopof theRoute(checkRoute.waypointsin 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.maneuverStyleNext chained maneuver style is defined with
InstructionsIconsStyles.chainedManeuverStyleSignpost 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
STOPTo 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 -> STOPThe 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 -> STOPYou 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 "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
SafetyCameraAlertListenerwhen 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.
Custom Alerts
The Navigation module provides a powerful custom alerts system that allows you to create and manage alerts for various points of interest and road attributes during navigation. These alerts can notify the driver when approaching specific locations or crossing certain boundaries.
Alert Types
The custom alerts system supports three main types of alerts:
Traffic Sign Alerts: Alert when approaching specific traffic signs (speed limits, stop signs, etc.)
POI Alerts: Alert when approaching Points of Interest based on POI class IDs
Border Crossing Alerts: Alert when crossing country borders
Each alert type can be configured with:
Timing parameters: Define when to start the alert (distance or time before the point)
Validity period: Define how long the alert should remain active
Repeat behavior: Choose whether to trigger the alert once or repeatedly
Creating Traffic Sign Alerts
Traffic sign alerts notify the driver when approaching specific types of traffic signs. Here's how to create them:
import com.benomad.msdk.navigation.alerts.AlertManager
import com.benomad.msdk.navigation.alerts.AlertParam
import com.benomad.msdk.core.carto.TrafficSign
// Get the AlertManager singleton
val alertManager = AlertManager.getInstance()
// Configure alert parameters
val alertParam = AlertParam(
startLimit = 200, // Start alert 200 meters before the traffic sign
validLimit = 50, // Keep alert active for 50 meters
startIsTime = false, // startLimit is in meters
validIsTime = false // validLimit is in meters
)
// Define traffic signs to alert for
val trafficSigns = listOf(
TrafficSign.SPEED_LIMIT_50,
TrafficSign.STOP,
TrafficSign.YIELD
)
// Create the alerts
val alerts = alertManager.createTrafficSignsAlert(
repeatAlert = true, // Trigger repeatedly while valid
alertParam = alertParam,
trafficSigns = trafficSigns
)
if (alerts != null) {
// Alerts created successfully
println("Created ${alerts.size} traffic sign alerts")
}Creating POI Alerts
POI alerts notify the driver when approaching specific categories of Points of Interest. You can filter by POI class IDs from CartoConst:
import com.benomad.msdk.navigation.alerts.AlertManager
import com.benomad.msdk.navigation.alerts.AlertParam
import com.benomad.msdk.core.carto.CartoConst
val alertManager = AlertManager.getInstance()
// Configure alert parameters using time-based limits
val alertParam = AlertParam(
startLimit = 30, // Start alert 30 seconds before the POI
validLimit = 10, // Keep alert active for 10 seconds
startIsTime = true, // startLimit is in seconds
validIsTime = true // validLimit is in seconds
)
// Define POI class IDs to alert for using CartoConst constants
val poiClassIDs = listOf(
CartoConst.KB_PETROL_STATION, // Gas stations (7311)
CartoConst.KB_RESTAURANT, // Restaurants (7315)
CartoConst.KB_HOSPITAL, // Hospitals (7321)
CartoConst.KB_PARKING_GARAGE // Parking garages (7313)
)
// Create the alerts
val alerts = alertManager.createPOIAlert(
repeatAlert = false, // Trigger only once
alertParam = alertParam,
classIDs = poiClassIDs
)
if (alerts != null) {
// Alerts created successfully
println("Created ${alerts.size} POI alerts")
}Available POI Class IDs: You can find all available POI class IDs in the CartoConst class.
Refer to com.benomad.msdk.core.carto.CartoConst for the complete list of POI categories.
Creating Border Crossing Alerts
Border crossing alerts notify the driver when crossing international borders:
import com.benomad.msdk.navigation.alerts.AlertManager
import com.benomad.msdk.navigation.alerts.AlertParam
val alertManager = AlertManager.getInstance()
// Configure alert parameters
val alertParam = AlertParam(
startLimit = 60, // Start alert 60 seconds before border
validLimit = 20, // Keep alert active for 20 seconds
startIsTime = true, // Use time-based limits
validIsTime = true
)
// Create border crossing alert
val alert = alertManager.createBorderCrossingAlert(
repeatAlert = true,
alertParam = alertParam
)
if (alert != null) {
// Alert created successfully
println("Border crossing alert created")
}Managing Alerts
Once alerts are created, you can enable, disable, or destroy them:
Enable or Disable an Alert
// Disable an alert
val disabledAlert = alertManager.enableAlert(alert, enabled = false)
// Re-enable the alert
val enabledAlert = alertManager.enableAlert(disabledAlert, enabled = true)Note: The enableAlert method returns a new instance of the alert with the updated state. Make sure to use the returned instance for subsequent operations.
Destroy an Alert
// Permanently remove an alert
val success = alertManager.destroyAlert(alert)
if (success) {
println("Alert destroyed successfully")
}Listening to Alert Items
During navigation, the system generates alert items when the vehicle approaches configured alert locations. You can listen to these items to react in real-time:
import com.benomad.msdk.navigation.alerts.AlertItem
import com.benomad.msdk.navigation.alerts.AlertStatus
import com.benomad.msdk.navigation.alerts.POIAlertItem
import com.benomad.msdk.navigation.alerts.TrafficSignAlertItem
import com.benomad.msdk.navigation.alerts.BorderCrossingAlertItem
// Add listener to receive alert item updates
// This is an example of how it might be used
fun onAlertItemReceived(alertItem: AlertItem) {
when (alertItem.status) {
AlertStatus.ALERT_VALID -> {
// Alert is currently valid - show notification to user
when (alertItem) {
is TrafficSignAlertItem -> {
println("Traffic sign ahead: ${alertItem.trafficSign}")
println("Distance: ${alertItem.dist} meters")
println("Time: ${alertItem.time} seconds")
}
is POIAlertItem -> {
println("POI ahead: Class ID ${alertItem.classID}")
println("Distance to destination: ${alertItem.distToDest} meters")
}
is BorderCrossingAlertItem -> {
println("Border crossing ahead")
println("Entering country: ${alertItem.countryCode}")
println("Distance: ${alertItem.dist} meters")
}
}
}
AlertStatus.ALERT_TO_DELETE -> {
// Alert is about to be removed - clean up UI
println("Alert ${alertItem.id} is being removed")
}
AlertStatus.ALERT_CREATED -> {
// Alert created but not yet valid
}
}
}Important Notes:
Alerts are only active during navigation sessions (tracking or guidance mode)
Alert items provide real-time information including distance and time to the alert point
The
AlertParamconfiguration allows flexible control over when and how alerts are triggeredTime-based limits use seconds, distance-based limits use meters
All created alerts are automatically managed by the AlertManager singleton