Planner
This module allows to plan and compute routes. You can then use the result to draw a route on a MapView using the Mapping module, and use it in the Navigation module to start a guidance session. It supports simple route calculation to one point to the other(s), via-points, alternative routes (only without via-points), consideration for route options, consumption calculation, optimization for electric vehicles... and more.
Module Planner
Dependencies
This module depends on other BeNomad's modules :
Core
Settings
Error Manager
Vehicle 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.core:core-ktx:1.9.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.10"
implementation "androidx.appcompat:appcompat:1.3.0"
implementation "com.google.android.material:material:1.8.0"Using the Planner
After the Core has been successfully initialized, you can start to use an instance of the Planner :
//this method will be used in examples
private fun initPlanner() : Planner?{
planner?.cancel()
planner = Planner()
val planner = this.planner ?: return null
if (!planner.isInitialized()) {
Log.e(
TAG,
"Planner not initialised ! Make sure that the Core Module was initialized successfully"
)
return null
}
return planner
}Note : you can have multiple instances of the Planner, and having each of them computing a route simultaneously (one route calculation by instance)
Compute routes
In order to compute routes, you need to have a list of map-matched GeoPoint.
You can obtain it using this code :
import com.benomad.msdk.geocoder.utils.AddressesUtils
AddressesUtils.buildAddresses(
geopoints, //your coordinates to compute a route
object : AddressesUtils.AddressesCreationCallback {
override fun onResult(addresses: List<Address>) {
checkAddressesLocation(
addresses,
object : AddressesUtils.MapMatchedLocationsCallback {
override fun onResult(mapMatchedGeopoints: List<GeoPoint>) {
//mapMatchedGeopoints can now be used to build the RoutePlan for computing a route
}
override fun onError(error: Error) {
//a least one address does not have a map-matched location
}
})
}
override fun onError(error: Error, addresses: List<Address?>, indexes: List<Int>) {
//at least one address couldn't be found
}
})To start computing routes :
//check previous section for the mapMatchedGeopoints variable
planner.computeRoute(
//define all the required information needed to calculate a route.
RoutePlan(
departures = listOf(mapMatchedGeopoints.first), //all the starting points for a route calculation
destinations = listOf(mapMatchedGeopoints.last), //all the final points for a route calculation
viaPoints = mapMatchedGeopoints.subList(1, mapMatchedGeopoints.size - 1), //all the viaPoints for the route
routeOptions = RouteOptions(maxAlternativeRoutes = 2) //used to define parameters for the route calculation, see the RouteOptions documentation for more info. In this example, the maximum number of alternative routes is set to 2. (maximum value for the SDK)
), object : ComputeRouteListener {
override fun onComputeError(
planner: Planner,
error: Error
) {
//an error occurred, onComputeFinished won't be called
}
override fun onComputeStarted(planner: Planner) {
Log.d(TAG, "onComputeStarted called")
}
override fun onProgress(planner: Planner, progress: Int) {
Log.d(TAG, "onProgress called with progress $progress")
}
override fun onComputeFinished(planner: Planner, routes: List<RouteResult>) {
Log.d(TAG, "onComputeFinished called with ${routes.size} routes")
val validComputedRoutes = mutableListOf<Route>()
for(routeResult in routes){
if(routeResult.error != null){
//handle error for this specific route result
//note that you can receive an error and still have a Route object that can be used
}
if(routeResult.route != null){
//add the route to the list
validComputedRoutes.add(routeResult.route!!)
}
}
}
})A Route object contains information like the estimated travel/driving duration, route length, route instructions, route bounding box (see the API reference of the Planner module for more details)
Note : matrix computation is not available for the moment. Therefore you can't use a RoutePlan that has multiple starting points and multiple arrival points. In other words, you can do 1 to n or n to 1 computations, but you can't do n to n computations.
Trace Route functionality
You can compute a route using a list of GPS measures.
A GPSMeasure contains at least the GPS coordinates of the point, and can also contain :
The speed in km/h
The GPS heading measure in degrees
The time of GPS satellites in millisecond offset from the Epoch
These optional parameters are used by the SDK in order to reduce risks of map-matching errors while computing the route. In other words, it allows the SDK to compute the route with more accuracy.
Note : The GPS heading measure is taken into account only if speed parameter >= 5 kph.
Here is an example of trace route usage :
planner.traceRoute(
gpsMeasures = gpsPoints,
vehicle = vehicle,
returnMapMatchedPoints = true,
getAdminPath = true,
language = Locale.getDefault(),
listener = object : TraceRouteListener {
override fun onComputeError(planner: Planner, error: Error) {
//an error occurred, route can't be computed
}
override fun onComputeStarted(planner: Planner) {
//route computation has started without error
}
override fun onComputeFinished(
planner: Planner,
result: RouteResult,
points: List<GeoMapMatchedPoint?>?
) {
//route computation has finished
val route = result.route
val error = result.error //an error can occur even if a route has been successfully computed
if(points != null){
for (mapMatchedPoint in points) {
if(mapMatchedPoint != null) { //point can be null if map-matching failed
val streetName = mapMatchedPoint.name
val adminPath = mapMatchedPoint.leftGeoPath
var address = ""
var separator = "/"
for (value in adminPath.withIndex()) {
if(value.index == adminPath.lastIndex){
separator = ""
}
address += "$value$separator" //build postal address
}
}
}
}
}
}
)In this example, all options are enabled :
returnMapMatchedPoints is set to true in order to get the list of the map-matched positions (computed from passed GPS measures)
getAdminPath is set to true to get the full postal address of map-matched points, meaning the administrative levels (city/department/region/country) in addition to the street name.
language is specified to set the preferential language for object's names (for example, objects return by GeoMapMatchedPoint.getLeftGeoPath will use the passed language if its available in the cartography.
Showing Waypoints of a Route on a MapView
Waypoints of a Route on a MapViewThe Route object provides a list of Waypoints that can be drawn on the MapView using dynamic layers. Check the Layers style section documentation of the Map module for more info about styles and dynamic layers.
val WAYPOINT_CLASS_ID = 12400 //class ID associated with a custom POI style (see the Map module documentation)
for(waypoint in route.waypoints){
dynamicLayer.newPoint(WAYPOINT_CLASS_ID, waypoint.geoPoint, emptyArray())
}Showing a Route on a MapView
Route on a MapViewWhen onComputeFinished is called with a list of routes, you can draw them on a MapView. In this example, all computed routed will be drawn with the same style
override fun onComputeFinished(planner: Planner, routes: List<Route>) {
val CUSTOM_ROUTE_CLASS_ID = 12300 //class ID associated with a custom polyline style (see the Map module documentation)
for(route in routes){
dynamicLayer.addForm(CUSTOM_ROUTE_CLASS_ID, route.polyline2D, emptyArray()) //you can check the documentation of the Map module for more info about dynamic layers of the MapView, and how to define a style for polylines.
}
}For a full example where routes are drawn with different styles that are dynamically changed depending on the user choice, you can check our samples.
Set the MapView centered on a specific route (full route visible on the MapView
MapView centered on a specific route (full route visible on the MapViewroute.boundingBox?.let { mapView?.zoomToRect(it, true) }Remove drawn routes from the MapView
MapViewRemoving a drawn route mean removing the class ID associated to this route(s) from the attached dynamic layer of the MapView :
dynamicLayer.remove(CUSTOM_ROUTE_CLASS_ID) //all layers associated with this class ID will be removed from the MapViewBuild a list of instructions' icons
To build a list of all instructions of a computed route, you need to initialize a SymbolicManeuverBuilder using a style and the computed route :
import com.benomad.msdk.planner.symbolicmaneuver.SymbolicManeuverBuilder
import com.benomad.msdk.planner.symbolicmaneuver.SymbolicManeuverStyle
import com.benomad.msdk.planner.utils.DimenUtils.dpToPixels
import android.content.res.Resources
import android.graphics.Color
val smBuilder = SymbolicManeuverBuilder()
//define a style for the maneuvers icons
val smStyle = SymbolicManeuverStyle(
DimenUtils.dpToPixels(
resources,
widthInDp //value in dp
).toInt(),
DimenUtils.dpToPixels(
resources,
heightInDp //value in dp
).toInt(),
penColor = Color.GREEN,
routePenColor = Color.WHITE,
closedPenColor = Color.GRAY,
)
smBuilder.initialize(smStyle, computedRoute) //computedRoute is obtained using Planner
smBuilder.isInit = true //this member is useful in guidance if you're handling a SymbolicManeuverBuilder by yourself To build the list of instructions, you can use this helper method :
import com.benomad.msdk.planner.symbolicmaneuver.InstructionUtils
val instructions: List<InstructionItem> = InstructionUtils.toInstructionItemsList(context, computedRoute,
smBuilder) //smBuilder is defined in previous exampleYou can also do it on your own using this example :
import android.content.Context
import android.graphics.Bitmap
import com.benomad.msdk.planner.route.Route
import com.benomad.msdk.planner.sheet.RouteSheet
import com.benomad.msdk.planner.utils.DistanceUtils
val instructions = computedRoute?.sheet?.instructions ?: emptyList() //get instructions of the computed route
val instructionItemList = mutableListOf<InstructionItem>()
for((index, instruction) in instructions.withIndex()){
val type = instructions[index].instructionType
var image : Bitmap? = null
if(customInstructionTypes.contains(type)) {
image = CustomInstructionsIconBuilder.getCustomInstructionIcon(context.applicationContext, type, isInDarkMode, false) //isInDarkMode is used for custom maneuvers icons
}
if(image == null) {
image = smBuilder.build(index)
}
instructionItemList.add(InstructionItem(image, instruction.toName ?: "", DistanceUtils.getDistanceFormattedText(context, instruction.length.toDouble())))
}
//instructionItemList contains all instructions of the route as InstructionItem objectsLane information visualization
For navigation instructions that involve lane guidance (such as "take the second lane from the left"), you can generate visual representations of the lanes to help drivers make the correct lane choices. The SDK automatically generates bitmap images showing which lanes are valid for the current instruction during navigation.
Architecture Overview
The lane information feature is automatically managed by the Navigation module through the InstructionsManager class. You don't need to directly instantiate or manage LaneInfoBuilder - it's handled internally. You only need to:
Create a
LaneInfoStyleto define the visual appearancePass it to the Navigation module via
InstructionsIconsStylesReceive the generated lane info bitmaps through the
InstructionsListenercallback
Creating a Lane Info Style
First, define a style for the lane visualization. The LaneInfoStyle class allows you to customize the appearance of the lane images:
import com.benomad.msdk.planner.laneinfo.LaneInfoStyle
import com.benomad.msdk.planner.utils.DimenUtils
import android.graphics.Color
// Create a lane info style with custom dimensions and colors
val laneInfoStyle = LaneInfoStyle(
maxWidth = DimenUtils.dpToPixels(resources, 500f).toInt(), // Width in pixels (converted from dp)
maxHeight = DimenUtils.dpToPixels(resources, 500f).toInt(), // Height in pixels (converted from dp)
bgClr = 0x80000000.toInt(), // Background color (semi-transparent black)
validLaneClr = 0xFFFFFFFF.toInt(), // Color for valid lanes (white)
nonValidLaneClr = 0xE0B0A080.toInt(), // Color for non-valid lanes (beige)
laneSeparatorClr = 0xFFB0B0B0.toInt() // Color for lane separators (gray)
)Color format: All colors use ARGB format (Alpha, Red, Green, Blue) as 32-bit integers.
Default colors:
Background: Semi-transparent black (
0x80000000)Valid lanes: White (
0xFFFFFFFF)Non-valid lanes: Beige (
0xE0B0A080)Lane separators: Gray (
0xFFB0B0B0)
You can customize these colors to match your application's design theme and provide good contrast for visibility while driving.
Configuring Instructions Icons Styles
To enable lane info generation, include the LaneInfoStyle in your InstructionsIconsStyles configuration:
import com.benomad.msdk.navigation.icon.InstructionsIconsStyles
import com.benomad.msdk.planner.symbolicmaneuver.SymbolicManeuverStyle
// Create a helper function to build the complete instructions styles
fun getInstructionsIconsStyles(resources: Resources, isAppInDarkMode: Boolean): InstructionsIconsStyles {
// Configure maneuver icon style
val maneuverStyle = SymbolicManeuverStyle(
maxWidth = DimenUtils.dpToPixels(resources, 500f).toInt(),
maxHeight = DimenUtils.dpToPixels(resources, 500f).toInt(),
penColor = Color.GRAY,
routePenColor = if (isAppInDarkMode) Color.WHITE else Color.BLACK
)
// Configure lane info style
val laneInfoStyle = LaneInfoStyle(
maxWidth = DimenUtils.dpToPixels(resources, 500f).toInt(),
maxHeight = DimenUtils.dpToPixels(resources, 500f).toInt()
// Use default colors or customize as needed
)
// Return the complete configuration
return InstructionsIconsStyles(
maneuverStyle = maneuverStyle,
chainedManeuverStyle = maneuverStyle, // Optional: for chained maneuvers
laneInfoStyle = laneInfoStyle, // Include lane info style
useDarkModeIcons = isAppInDarkMode
)
}Note: If laneInfoStyle is null in InstructionsIconsStyles, the InstructionsManager will not create a LaneInfoBuilder and no lane info images will be generated.
Starting Navigation with Lane Info
When starting navigation, pass the configured InstructionsIconsStyles to the Navigation module:
// Get instructions styles configuration
val instructionsStyles = getInstructionsIconsStyles(resources, isAppInDarkMode = false)
// Create instructions listener to receive lane info bitmaps
val instructionsListener = object : InstructionsListener {
override fun onNewInstruction(
instruction: String,
maneuver: Bitmap?,
chainedManeuver: Bitmap?,
signpost: Bitmap?,
laneInfo: Bitmap? // Lane info bitmap automatically generated
) {
// Update UI with the instruction and icons
updateInstructionUI(instruction, maneuver, laneInfo)
}
}
// Start navigation with the configured styles
Navigation.start(
route = route,
instructionsListener = instructionsListener,
instructionsIconsStyles = instructionsStyles
)
}Displaying Lane Info in UI
The lane info bitmap is received through the InstructionsListener.onNewInstruction() callback. Here's how to display it in your UI:
Using traditional Views:
// In your layout XML
<ImageView
android:id="@+id/lane_info_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="Lane information" />
// In your Activity/Fragment
override fun onNewInstruction(
instruction: String,
maneuver: Bitmap?,
chainedManeuver: Bitmap?,
signpost: Bitmap?,
laneInfo: Bitmap?
) {
if (laneInfo != null) {
laneInfoImageView.visibility = View.VISIBLE
laneInfoImageView.setImageBitmap(laneInfo)
} else if (maneuver != null) {
// Fallback to maneuver icon if no lane info
laneInfoImageView.visibility = View.VISIBLE
laneInfoImageView.setImageBitmap(maneuver)
} else {
laneInfoImageView.visibility = View.GONE
}
}Using Jetpack Compose:
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.asImageBitmap
@Composable
fun InstructionDisplay(
instructionIcon: Bitmap?,
laneInfoIcon: Bitmap?,
instruction: String
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
// Display lane info with priority over instruction icon
when {
laneInfoIcon != null -> {
Image(
bitmap = laneInfoIcon.asImageBitmap(),
contentDescription = "Lane info icon"
)
}
instructionIcon != null -> {
Image(
bitmap = instructionIcon.asImageBitmap(),
contentDescription = "Instruction icon"
)
}
}
Text(
text = instruction,
modifier = Modifier.padding(start = 16.dp)
)
}
}Important Notes
Automatic management: The
InstructionsManagerin the Navigation module automatically creates, initializes, and manages theLaneInfoBuilder. You don't need to manually instantiate it.Automatic disposal: Resources are automatically cleaned up when navigation ends or when a reroute occurs.
Null handling: The
laneInfoparameter inonNewInstruction()will benullif:The instruction doesn't require lane guidance
Lane info couldn't be generated for this instruction
laneInfoStylewas not provided inInstructionsIconsStylesDisplay priority: In most applications, lane info should be displayed with priority over regular maneuver icons when available, as it provides more detailed guidance.
Advanced: Direct Usage of LaneInfoBuilder
For advanced use cases where you need to generate lane info images outside of navigation (e.g., for a route preview), you can directly use LaneInfoBuilder:
import com.benomad.msdk.planner.laneinfo.LaneInfoBuilder
import com.benomad.msdk.planner.laneinfo.LaneInfoStyle
// Create and initialize the builder
val laneInfoBuilder = LaneInfoBuilder()
val laneInfoStyle = LaneInfoStyle(maxWidth = 300, maxHeight = 150)
laneInfoBuilder.initialize(laneInfoStyle, computedRoute)
// Generate lane info for a specific instruction
val instructionIndex = 5
val laneInfoBitmap: Bitmap? = laneInfoBuilder.build(instructionIndex)
// Use the bitmap...Note: This advanced usage is only needed for specific scenarios. For normal navigation, use the automatic integration through InstructionsIconsStyles as described above.
EV features
To use EV features of the Planner module, you need to use the Vehicle Manager module
If you're using hybrid maps, an internet connection is required to use EV features
Autonomy zone
It is possible to get the reachable area of a vehicle around a given map-matched location, while taking into account the battery level and characteristics of the vehicle. The result is a polygon which can be displayed on a MapView.
1. Get a matched location
First, you need to get a map-matched location. In this example, we're using the GPS Manager module to get the last known location of the device and use its coordinates to get a map-matched location :
GPSManager.getLastKnownLocation().let {
if(it != null) {
//get a map-matched location from the "basic" last known location found
Address.create(GeoPoint(it.longitude, it.latitude), object : AddressCreationCallback{
override fun onResult(address: Address?) {
if(address != null){
isochroneCenter = address.location
if(isochroneCenter != null){
//use the map-matched location to compute isochrone of the autonomy zone
computeIsochrone(isochroneCenter) //code of this function in next block
}else{
Toast.makeText(context, "Map-matched location not found", Toast.LENGTH_LONG).show()
}
}else{
Toast.makeText(context, "Map-matching failed", Toast.LENGTH_LONG).show()
}
}
})
}else{
Toast.makeText(context, "Location not found", Toast.LENGTH_LONG).show()
}
}2. Get the vehicle specifications
//class ID for the isochrone polygon that defines how it will be rendered on the MapView (see the section Layers style of the Map Module documentation for more details)
const val ISOCHRONE_ID = 12000L
val vehicleRepository: VehicleRepository by lazy {
BeMapVehicleRepository(
this,
"login",
"password",
"api_url",
)
}
val chosenVehicleModel = null
const val DEFAULT_CAR_ID = "44587c46-88a4-453e-9763-2cfb7a3661f4"
if (chosenVehicleModel == null) {
vehicleRepository.vehicleModelWithId(DEFAULT_CAR_ID)
.observe(viewLifecycleOwner) {
this.chosenVehicleModel = it //select default car
}
}
//set current battery level to 100% using full battery capacity of vehicle
chosenVehicle?.status?.currentEnergyLoad = ((chosenVehicle?.vehicleModel?.profile as EVProfile).battery)3. Get the reachable area
To get the reachable area, use the Planner.computeIsochrone function :
private fun launchReachableAreaIsochrone(mapMatchedLocation: GeoPoint){
val batteryCapacity = if(chosenVehicle?.vehicleModel?.profile is EVProfile) (chosenVehicle?.vehicleModel?.profile as EVProfile).battery else 0
val socLimit = chosenVehicle?.status?.currentEnergyLoad?.toInt() ?: batteryCapacity
//cancel current computation of this planner instance (if any) and checks that the planner is initialized and not null
val planner = initPlanner() ?: return
planner.computeIsochrone(
mapMatchedLocation,
socLimit*1000, //convert current battery level from kWh to Wh
RouteOptions(vehicle = chosenVehicle, routeCriteria = listOf(RouteCriteria.DEFAULT, RouteCriteria.NO_FERRY), routeOptim = RouteOptim.ECO),
object : ComputeIsochroneListener {
override fun onComputeError(planner: Planner, error: Error) {
//an error occurred
}
override fun onComputeStarted(planner: Planner) {
//isochrone computation start
}
override fun onProgress(planner: Planner, progress: Int) {
//isochrone computation progress
}
override fun onComputeFinished(planner: Planner, polygon: Polygon2D) {
//show the isochrone polygon on the MapView
dynamicLayer.addForm(ISOCHRONE_ID, polygon, emptyArray())
//zoom MapView to the bounding rectangle of the computed isochrone. MapView is now centered on the center of that bounding rectangle
mapView?.zoomToRect(polygon.boundingRect, true)
}
})
}Note : the RouteOptions passed to the runIsochrone method must have a routeOptim set to RouteOptim.ECO and routeCriteria must have at least RouteCriteria.DEFAULT in order to compute an isochrone that corresponds to the reachable area for an electric vehicle
You can also compute a reversed isochrone polygon. You can use the same example and use the computeReversedIsochrone function instead of the computeIsochrone
Check if a route's arrival is reachable with the current SoC
After computing routes, you can check if a route's arrival is reachable with the current battery level of the vehicle :
//assuming we have a route variable that corresponds to a computed Route
//you can use the example above for the initialization of the chosenVehicle variable
val currentSoc: Double? = route.value.routeOptions.vehicle?.status?.currentEnergyLoad
val maxAcc = route.value.routeOptions.vehicle?.vehicleModel?.profile?.maxAcc ?: 1.25
val maxDec = route.value.routeOptions.vehicle?.vehicleModel?.profile?.maxDec ?: -1.25
if(currentSoc != null) {
//checks if the route is reachable
val isArrivalReachable = route.isArrivalReachable(currentSoc, maxAcc, maxDec)
}Check if a route's arrival is reachable with a SoC at arrival above a defined percentage
You can also check if the arrival is reachable with a battery level above a specified percentage :
//assuming we have a route variable that corresponds to a computed Route
//you can use the example above for the initialization of the chosenVehicle variable
val currentSoc: Double? = route.value.routeOptions.vehicle?.status?.currentEnergyLoad
val batteryCapacity: Double = (route.value.routeOptions.vehicle?.vehicleModel?.profile as EVProfile).battery
val maxAcc = route.value.routeOptions.vehicle?.vehicleModel?.profile?.maxAcc ?: 1.25
val maxDec = route.value.routeOptions.vehicle?.vehicleModel?.profile?.maxDec ?: -1.25
val minSocAtArrival = 10.0 //in percentage
if(currentSoc != null && batteryCapacity 0){
//checks if the route is reachable with a SoC's percentage at arrival minSocAtArrival
val isArrivalReachable = route.isArrivalReachable(currentSoc, batteryCapacity, maxAcc, maxDec, minSocAtArrival)
}Find optimal charges on a route
Once you have computed routes, you can optimize them using RechargeParameters in order to add charging stations along the route :
socMin : to never go under a defined battery level during the whole route
socMinAtArrival : to reach the arrival above a defined battery level
socMax : the maximum battery level to reach while charging the vehicle on the charging stations added on the route
stopFixedTime : an additional fixed time that will be added at each charging station added on the route
chargingPointFilter : filters for the charging stations that will be added on the route (see ChargingPointFilter methods for more info)
Note : the socMinAtArrival should not be inferior to the socMin. If that's the case, the socMinAtArrival value will be set to the value of the socMin when computing EV routes.
Here is an example that uses RechargeParameters to optimize computed routes :
val planner = initPlanner() ?: return
val rechargeParams = RechargeParameters(10, 15, 95, 2*60, ChargingPointFilter())
val evProfile = chosenVehicle.vehicleModel?.profile as? EVProfile
val status = chosenVehicle.status
val computedRoutes = listOf(/* list of computed routes obtained using the Planner.computeRoute function */)
if(evProfile != null && status != null){
planner.computeRouteWithChargeStops(computedRoutes, evProfile, status, rechargeParams, object : ComputeRouteListener {
override fun onComputeError(
planner: Planner,
error: Error
) {
//an error occurred, onComputeFinished won't be called (no optimization)
}
override fun onComputeStarted(planner: Planner) {
Log.d(TAG, "onComputeStarted called")
}
override fun onProgress(planner: Planner, progress: Int) {
Log.d(TAG, "onProgress called with progress $progress")
}
override fun onComputeFinished(planner: Planner, routes: List<RouteResult>) {
Log.d(TAG, "onComputeFinished called with ${routes.size}")
val computedRoutesOptimized = mutableListOf<Route>()
for((i, routeResult) in routes.withIndex()) {
if(routeResult is EVRouteResult) { //smart cast
val optimizationResult = routeResult.optimChargeResult
val optimResultDescription = optimizationResult.text //the description of the optimization result for this route
if(routeResult.route != null) {
computedRoutesOptimized.add(routeResult.route!!) //add the optimized route
} else {
val routeNotOptimized = computedRoutes?.get(i) //get the initial computed route (as no optimization is available)
if(routeNotOptimized != null){
computedRoutesOptimized.add(routeNotOptimized)
}
}
}
}
//computedRoutesOptimized now contains optimized routes (or initial computed routes if no optimization for a route is available)
}
})
}Minimum SoC tolerance
A tolerance of 0,2 kWh is applied to the minimum SoC / minimum SoC at arrival. It means that if the vehicle has a battery capacity of 50 kWh, and the minimum SoC / minimum SoC at arrival is set to 10%, the SoC at arrival (at a charge stop or at the destination) planned by the optimization will be >= 9,6%. (0,4% of 50kWh = 0,2kWh)
If the initial SoC of the vehicle is < 2*socMin, the SDK uses a tolerance for the optimization of the route : in that case, the minimum SoC applied for optimizing the route is the initial SoC of the vehicle / 2. This tolerance is applied only for the first recharge.
For example, if the minimum SoC is set to 10%, and the initial SoC used to optimize the route is set to 13%, a tolerance is applied to the minimum SoC used for optimizing the route (only for the first recharge) because the initial SoC is < 2*10. (10 corresponds to the socMin value in percentage). In that example, the minimum SoC applied is 13/2 = 6,5%
Compute optimized routes
You can also compute optimized routes directly, in one step.
This method will return a route (with eventually alternatives) even if the EV optimization failed. For example if there not enough charging stations to follow the EVRoutePlan, an error will be found in the OptimChargeResult object.
Note: This method is the only one supporting the ECO mode for route computations.
val planner = initPlanner() ?: return
val rechargeParams = RechargeParameters(10, 15, 95, 2*60, ChargingPointFilter())
//assuming chosenVehicle is a Vehicle not null that has an EVProfile and a VehicleStatus with a currentEnergyLoad defined in kWh
val evRouteOptions = EVRouteOptions(vehicle = chosenVehicle!!, maxAlternativeRoutes = 2, rechargeParameters = rechargeParams)
//assuming addresses is a variable of type List<Address> that contains at least 2 entries
val evRoutePlan = EVRoutePlan(
departures = listOf(addresses.first().location ?: return),
destinations = listOf(addresses.last().location ?: return),
viaPoints = addresses.subList(1, addresses.size - 1).mapNotNull { it.location },
evRouteOptions
)
planner.computeRouteWithChargeStops(evRoutePlan, object : ComputeRouteListener {
override fun onComputeError(
planner: Planner,
error: Error
) {
//an error occurred, onComputeFinished won't be called
}
override fun onComputeStarted(planner: Planner) {
Log.d(TAG, "onComputeStarted called")
}
override fun onProgress(planner: Planner, progress: Int) {
Log.d(TAG, "onProgress called with progress $progress")
}
override fun onComputeFinished(planner: Planner, routes: List<RouteResult>) {
Log.d(TAG, "onComputeFinished called with ${routes.size}")
val computedRoutesOptimized = mutableListOf<Route>()
for(routeResult in routes){
if(routeResult is EVRouteResult) { //smart cast
val optimizationResult = routeResult.optimChargeResult
val optimResultDescription = optimizationResult.text //the description of the optimization result for this route
if(routeResult.route != null) {
computedRoutesOptimized.add(routeResult.route!!) //add the optimized route
}
}
}
//computedRoutesOptimized now contains optimized routes
})ECO Mode
If the driver wants to select the most economic route (in terms of energy consumption) for his/her travel (via the settings menu), use RouteOptim.ECO mode in your EVRouteOptions .
By selecting the Eco Mode, the system will provide a list of classified routes starting from the most to the least economic one.
Each route will have a maximum speed defined in Route.maxSpeedEta. The driver must respect this speed limitation during the whole itinerary, otherwise the energy consumption of the vehicle will be higher that expected and the planned itinerary might change.
Recommended Maximum Speeds
The recommended maximum speeds allows the EV-routing to define maximum speeds between waypoints. This option will try to reduce the overall travel duration by minimizing charging duration to the expense of increasing the driving duration.
To enable the Recommended Maximum Speeds, you have to set the RechargeParameters.useRecommendedMaxSpeed property to true.
After the EV-Route calculation, maximum speeds between waypoints will be available from the RouteWaypoint.waypoint.maxDrivingSpeed property : the speed limitation is defined from this waypoint to the next waypoint. The route can also have a maximum speed defined in Route.maxSpeedEta : the speed limitation is defined for the whole route (its value can't be lower than speed limitations defined between waypoints). The driver must respect these speed limitations, otherwise the energy consumption of the vehicle will be higher than expected and the planned itinerary might change.
Note : this option can be used with the Eco mode.