So i found this turn-by-turn navigation example by mapbox.
It works just fine but but i would like to do some custom actions after the navigation has completed such as maybe enable a certain button that was initially hidden when the navigation was on progress or toast a message indicating that the navigation is completed. Since i'm actually new to Kotlin and the mapbox API in general, i can't really seem to figure out where in the code should i inject those particular actions.
Here is the Kotlin class that does the magic (I have discarded the import section):
/**
* This example demonstrates a basic turn-by-turn navigation experience by putting together some UI elements to showcase
* navigation camera transitions, guidance instructions banners and playback, and progress along the route.
*
* Before running the example make sure you have put your access_token in the correct place
* inside [app/src/main/res/values/mapbox_access_token.xml]. If not present then add this file
* at the location mentioned above and add the following content to it
*
* <?xml version="1.0" encoding="utf-8"?>
* <resources xmlns:tools="http://schemas.android.com/tools">
* <string name="mapbox_access_token"><PUT_YOUR_ACCESS_TOKEN_HERE></string>
* </resources>
*
* The example assumes that you have granted location permissions and does not enforce it. However,
* the permission is essential for proper functioning of this example. The example also uses replay
* location engine to facilitate navigation without actually physically moving.
*
* How to use this example:
* - You can long-click the map to select a destination.
* - The guidance will start to the selected destination while simulating location updates.
* You can disable simulation by commenting out the [replayLocationEngine] setter in [NavigationOptions].
* Then, the device's real location will be used.
* - At any point in time you can finish guidance or select a new destination.
* - You can use buttons to mute/unmute voice instructions, recenter the camera, or show the route overview.
*/
class TurnByTurnExperienceActivity : AppCompatActivity() {
private companion object {
private const val BUTTON_ANIMATION_DURATION = 1500L
}
/**
* Debug tool used to play, pause and seek route progress events that can be used to produce mocked location updates along the route.
*/
private val mapboxReplayer = MapboxReplayer()
/**
* Debug tool that mocks location updates with an input from the [mapboxReplayer].
*/
private val replayLocationEngine = ReplayLocationEngine(mapboxReplayer)
/**
* Debug observer that makes sure the replayer has always an up-to-date information to generate mock updates.
*/
private val replayProgressObserver = ReplayProgressObserver(mapboxReplayer)
/**
* Bindings to the example layout.
*/
private lateinit var binding: MapboxActivityTurnByTurnExperienceBinding
/**
* Mapbox Maps entry point obtained from the [MapView].
* You need to get a new reference to this object whenever the [MapView] is recreated.
*/
private lateinit var mapboxMap: MapboxMap
/**
* Mapbox Navigation entry point. There should only be one instance of this object for the app.
* You can use [MapboxNavigationProvider] to help create and obtain that instance.
*/
private lateinit var mapboxNavigation: MapboxNavigation
/**
* Used to execute camera transitions based on the data generated by the [viewportDataSource].
* This includes transitions from route overview to route following and continuously updating the camera as the location changes.
*/
private lateinit var navigationCamera: NavigationCamera
/**
* Produces the camera frames based on the location and routing data for the [navigationCamera] to execute.
*/
private lateinit var viewportDataSource: MapboxNavigationViewportDataSource
/*
* Below are generated camera padding values to ensure that the route fits well on screen while
* other elements are overlaid on top of the map (including instruction view, buttons, etc.)
*/
private val pixelDensity = Resources.getSystem().displayMetrics.density
private val overviewPadding: EdgeInsets by lazy {
EdgeInsets(
140.0 * pixelDensity,
40.0 * pixelDensity,
120.0 * pixelDensity,
40.0 * pixelDensity
)
}
private val landscapeOverviewPadding: EdgeInsets by lazy {
EdgeInsets(
30.0 * pixelDensity,
380.0 * pixelDensity,
110.0 * pixelDensity,
20.0 * pixelDensity
)
}
private val followingPadding: EdgeInsets by lazy {
EdgeInsets(
180.0 * pixelDensity,
40.0 * pixelDensity,
150.0 * pixelDensity,
40.0 * pixelDensity
)
}
private val landscapeFollowingPadding: EdgeInsets by lazy {
EdgeInsets(
30.0 * pixelDensity,
380.0 * pixelDensity,
110.0 * pixelDensity,
40.0 * pixelDensity
)
}
/**
* Generates updates for the [MapboxManeuverView] to display the upcoming maneuver instructions
* and remaining distance to the maneuver point.
*/
private lateinit var maneuverApi: MapboxManeuverApi
/**
* Generates updates for the [MapboxTripProgressView] that include remaining time and distance to the destination.
*/
private lateinit var tripProgressApi: MapboxTripProgressApi
/**
* Generates updates for the [routeLineView] with the geometries and properties of the routes that should be drawn on the map.
*/
private lateinit var routeLineApi: MapboxRouteLineApi
/**
* Draws route lines on the map based on the data from the [routeLineApi]
*/
private lateinit var routeLineView: MapboxRouteLineView
/**
* Generates updates for the [routeArrowView] with the geometries and properties of maneuver arrows that should be drawn on the map.
*/
private val routeArrowApi: MapboxRouteArrowApi = MapboxRouteArrowApi()
/**
* Draws maneuver arrows on the map based on the data [routeArrowApi].
*/
private lateinit var routeArrowView: MapboxRouteArrowView
/**
* Stores and updates the state of whether the voice instructions should be played as they come or muted.
*/
private var isVoiceInstructionsMuted = false
set(value) {
field = value
if (value) {
binding.soundButton.muteAndExtend(BUTTON_ANIMATION_DURATION)
voiceInstructionsPlayer.volume(SpeechVolume(0f))
} else {
binding.soundButton.unmuteAndExtend(BUTTON_ANIMATION_DURATION)
voiceInstructionsPlayer.volume(SpeechVolume(1f))
}
}
/**
* Extracts message that should be communicated to the driver about the upcoming maneuver.
* When possible, downloads a synthesized audio file that can be played back to the driver.
*/
private lateinit var speechApi: MapboxSpeechApi
/**
* Plays the synthesized audio files with upcoming maneuver instructions
* or uses an on-device Text-To-Speech engine to communicate the message to the driver.
*/
private lateinit var voiceInstructionsPlayer: MapboxVoiceInstructionsPlayer
/**
* Observes when a new voice instruction should be played.
*/
private val voiceInstructionsObserver = VoiceInstructionsObserver { voiceInstructions ->
speechApi.generate(voiceInstructions, speechCallback)
}
/**
* Based on whether the synthesized audio file is available, the callback plays the file
* or uses the fall back which is played back using the on-device Text-To-Speech engine.
*/
private val speechCallback =
MapboxNavigationConsumer<Expected<SpeechError, SpeechValue>> { expected ->
expected.fold(
{ error ->
// play the instruction via fallback text-to-speech engine
voiceInstructionsPlayer.play(
error.fallback,
voiceInstructionsPlayerCallback
)
},
{ value ->
// play the sound file from the external generator
voiceInstructionsPlayer.play(
value.announcement,
voiceInstructionsPlayerCallback
)
}
)
}
/**
* When a synthesized audio file was downloaded, this callback cleans up the disk after it was played.
*/
private val voiceInstructionsPlayerCallback =
MapboxNavigationConsumer<SpeechAnnouncement> { value ->
// remove already consumed file to free-up space
speechApi.clean(value)
}
/**
* [NavigationLocationProvider] is a utility class that helps to provide location updates generated by the Navigation SDK
* to the Maps SDK in order to update the user location indicator on the map.
*/
private val navigationLocationProvider = NavigationLocationProvider()
/**
* Gets notified with location updates.
*
* Exposes raw updates coming directly from the location services
* and the updates enhanced by the Navigation SDK (cleaned up and matched to the road).
*/
private val locationObserver = object : LocationObserver {
var firstLocationUpdateReceived = false
override fun onNewRawLocation(rawLocation: Location) {
// not handled
}
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
val enhancedLocation = locationMatcherResult.enhancedLocation
// update location puck's position on the map
navigationLocationProvider.changePosition(
location = enhancedLocation,
keyPoints = locationMatcherResult.keyPoints,
)
// update camera position to account for new location
viewportDataSource.onLocationChanged(enhancedLocation)
viewportDataSource.evaluate()
// if this is the first location update the activity has received,
// it's best to immediately move the camera to the current user location
if (!firstLocationUpdateReceived) {
firstLocationUpdateReceived = true
navigationCamera.requestNavigationCameraToOverview(
stateTransitionOptions = NavigationCameraTransitionOptions.Builder()
.maxDuration(0) // instant transition
.build()
)
}
}
}
/**
* Gets notified with progress along the currently active route.
*/
private val routeProgressObserver = RouteProgressObserver { routeProgress ->
// update the camera position to account for the progressed fragment of the route
viewportDataSource.onRouteProgressChanged(routeProgress)
viewportDataSource.evaluate()
// draw the upcoming maneuver arrow on the map
val style = mapboxMap.getStyle()
if (style != null) {
val maneuverArrowResult = routeArrowApi.addUpcomingManeuverArrow(routeProgress)
routeArrowView.renderManeuverUpdate(style, maneuverArrowResult)
}
// update top banner with maneuver instructions
val maneuvers = maneuverApi.getManeuvers(routeProgress)
maneuvers.fold(
{ error ->
Toast.makeText(
this@TurnByTurnExperienceActivity,
error.errorMessage,
Toast.LENGTH_SHORT
).show()
},
{
binding.maneuverView.visibility = View.VISIBLE
binding.maneuverView.renderManeuvers(maneuvers)
}
)
// update bottom trip progress summary
binding.tripProgressView.render(
tripProgressApi.getTripProgress(routeProgress)
)
}
/**
* Gets notified whenever the tracked routes change.
*
* A change can mean:
* - routes get changed with [MapboxNavigation.setRoutes]
* - routes annotations get refreshed (for example, congestion annotation that indicate the live traffic along the route)
* - driver got off route and a reroute was executed
*/
private val routesObserver = RoutesObserver { routeUpdateResult ->
if (routeUpdateResult.routes.isNotEmpty()) {
// generate route geometries asynchronously and render them
val routeLines = routeUpdateResult.routes.map { RouteLine(it, null) }
routeLineApi.setRoutes(
routeLines
) { value ->
mapboxMap.getStyle()?.apply {
routeLineView.renderRouteDrawData(this, value)
}
}
// update the camera position to account for the new route
viewportDataSource.onRouteChanged(routeUpdateResult.routes.first())
viewportDataSource.evaluate()
} else {
// remove the route line and route arrow from the map
val style = mapboxMap.getStyle()
if (style != null) {
routeLineApi.clearRouteLine { value ->
routeLineView.renderClearRouteLineValue(
style,
value
)
}
routeArrowView.render(style, routeArrowApi.clearArrows())
}
// remove the route reference from camera position evaluations
viewportDataSource.clearRouteData()
viewportDataSource.evaluate()
}
}
@SuppressLint("MissingPermission")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = MapboxActivityTurnByTurnExperienceBinding.inflate(layoutInflater)
setContentView(binding.root)
mapboxMap = binding.mapView.getMapboxMap()
// initialize the location puck
binding.mapView.location.apply {
this.locationPuck = LocationPuck2D(
bearingImage = ContextCompat.getDrawable(
this@TurnByTurnExperienceActivity,
R.drawable.mapbox_navigation_puck_icon
)
)
setLocationProvider(navigationLocationProvider)
enabled = true
}
// initialize Mapbox Navigation
mapboxNavigation = if (MapboxNavigationProvider.isCreated()) {
MapboxNavigationProvider.retrieve()
} else {
MapboxNavigationProvider.create(
NavigationOptions.Builder(this.applicationContext)
.accessToken(getString(R.string.mapbox_access_token))
// comment out the location engine setting block to disable simulation
.locationEngine(replayLocationEngine)
.build()
)
}
// initialize Navigation Camera
viewportDataSource = MapboxNavigationViewportDataSource(mapboxMap)
navigationCamera = NavigationCamera(
mapboxMap,
binding.mapView.camera,
viewportDataSource
)
// set the animations lifecycle listener to ensure the NavigationCamera stops
// automatically following the user location when the map is interacted with
binding.mapView.camera.addCameraAnimationsLifecycleListener(
NavigationBasicGesturesHandler(navigationCamera)
)
navigationCamera.registerNavigationCameraStateChangeObserver { navigationCameraState ->
// shows/hide the recenter button depending on the camera state
when (navigationCameraState) {
NavigationCameraState.TRANSITION_TO_FOLLOWING,
NavigationCameraState.FOLLOWING -> binding.recenter.visibility = View.INVISIBLE
NavigationCameraState.TRANSITION_TO_OVERVIEW,
NavigationCameraState.OVERVIEW,
NavigationCameraState.IDLE -> binding.recenter.visibility = View.VISIBLE
}
}
// set the padding values depending on screen orientation and visible view layout
if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
viewportDataSource.overviewPadding = landscapeOverviewPadding
} else {
viewportDataSource.overviewPadding = overviewPadding
}
if (this.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
viewportDataSource.followingPadding = landscapeFollowingPadding
} else {
viewportDataSource.followingPadding = followingPadding
}
// make sure to use the same DistanceFormatterOptions across different features
val distanceFormatterOptions = mapboxNavigation.navigationOptions.distanceFormatterOptions
// initialize maneuver api that feeds the data to the top banner maneuver view
maneuverApi = MapboxManeuverApi(
MapboxDistanceFormatter(distanceFormatterOptions)
)
// initialize bottom progress view
tripProgressApi = MapboxTripProgressApi(
TripProgressUpdateFormatter.Builder(this)
.distanceRemainingFormatter(
DistanceRemainingFormatter(distanceFormatterOptions)
)
.timeRemainingFormatter(
TimeRemainingFormatter(this)
)
.percentRouteTraveledFormatter(
PercentDistanceTraveledFormatter()
)
.estimatedTimeToArrivalFormatter(
EstimatedTimeToArrivalFormatter(this, TimeFormat.NONE_SPECIFIED)
)
.build()
)
// initialize voice instructions api and the voice instruction player
speechApi = MapboxSpeechApi(
this,
getString(R.string.mapbox_access_token),
Locale.US.language
)
voiceInstructionsPlayer = MapboxVoiceInstructionsPlayer(
this,
getString(R.string.mapbox_access_token),
Locale.US.language
)
// initialize route line, the withRouteLineBelowLayerId is specified to place
// the route line below road labels layer on the map
// the value of this option will depend on the style that you are using
// and under which layer the route line should be placed on the map layers stack
val mapboxRouteLineOptions = MapboxRouteLineOptions.Builder(this)
.withRouteLineBelowLayerId("road-label")
.build()
routeLineApi = MapboxRouteLineApi(mapboxRouteLineOptions)
routeLineView = MapboxRouteLineView(mapboxRouteLineOptions)
// initialize maneuver arrow view to draw arrows on the map
val routeArrowOptions = RouteArrowOptions.Builder(this).build()
routeArrowView = MapboxRouteArrowView(routeArrowOptions)
// load map style
mapboxMap.loadStyleUri(
Style.MAPBOX_STREETS
) {
// add long click listener that search for a route to the clicked destination
binding.mapView.gestures.addOnMapLongClickListener { point ->
findRoute(point)
true
}
}
// initialize view interactions
binding.stop.setOnClickListener {
clearRouteAndStopNavigation()
}
binding.recenter.setOnClickListener {
navigationCamera.requestNavigationCameraToFollowing()
binding.routeOverview.showTextAndExtend(BUTTON_ANIMATION_DURATION)
}
binding.routeOverview.setOnClickListener {
navigationCamera.requestNavigationCameraToOverview()
binding.recenter.showTextAndExtend(BUTTON_ANIMATION_DURATION)
}
binding.soundButton.setOnClickListener {
// mute/unmute voice instructions
isVoiceInstructionsMuted = !isVoiceInstructionsMuted
}
// set initial sounds button state
binding.soundButton.unmute()
// start the trip session to being receiving location updates in free drive
// and later when a route is set also receiving route progress updates
mapboxNavigation.startTripSession()
}
override fun onStart() {
super.onStart()
// register event listeners
mapboxNavigation.registerRoutesObserver(routesObserver)
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
mapboxNavigation.registerLocationObserver(locationObserver)
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
mapboxNavigation.registerRouteProgressObserver(replayProgressObserver)
if (mapboxNavigation.getRoutes().isEmpty()) {
// if simulation is enabled (ReplayLocationEngine set to NavigationOptions)
// but we're not simulating yet,
// push a single location sample to establish origin
mapboxReplayer.pushEvents(
listOf(
ReplayRouteMapper.mapToUpdateLocation(
eventTimestamp = 0.0,
point = Point.fromLngLat(-122.39726512303575, 37.785128345296805)
)
)
)
mapboxReplayer.playFirstLocation()
}
}
override fun onStop() {
super.onStop()
// unregister event listeners to prevent leaks or unnecessary resource consumption
mapboxNavigation.unregisterRoutesObserver(routesObserver)
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
mapboxNavigation.unregisterLocationObserver(locationObserver)
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)
mapboxNavigation.unregisterRouteProgressObserver(replayProgressObserver)
}
override fun onDestroy() {
super.onDestroy()
MapboxNavigationProvider.destroy()
speechApi.cancel()
voiceInstructionsPlayer.shutdown()
}
private fun findRoute(destination: Point) {
val originLocation = navigationLocationProvider.lastLocation
val originPoint = originLocation?.let {
Point.fromLngLat(it.longitude, it.latitude)
} ?: return
// execute a route request
// it's recommended to use the
// applyDefaultNavigationOptions and applyLanguageAndVoiceUnitOptions
// that make sure the route request is optimized
// to allow for support of all of the Navigation SDK features
mapboxNavigation.requestRoutes(
RouteOptions.builder()
.applyDefaultNavigationOptions()
.applyLanguageAndVoiceUnitOptions(this)
.coordinatesList(listOf(originPoint, destination))
// provide the bearing for the origin of the request to ensure
// that the returned route faces in the direction of the current user movement
.bearingsList(
listOf(
Bearing.builder()
.angle(originLocation.bearing.toDouble())
.degrees(45.0)
.build(),
null
)
)
.build(),
object : RouterCallback {
override fun onRoutesReady(
routes: List<DirectionsRoute>,
routerOrigin: RouterOrigin
) {
setRouteAndStartNavigation(routes)
}
override fun onFailure(
reasons: List<RouterFailure>,
routeOptions: RouteOptions
) {
// no impl
}
override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) {
// no impl
}
}
)
}
private fun setRouteAndStartNavigation(routes: List<DirectionsRoute>) {
// set routes, where the first route in the list is the primary route that
// will be used for active guidance
mapboxNavigation.setRoutes(routes)
// start location simulation along the primary route
startSimulation(routes.first())
// show UI elements
binding.soundButton.visibility = View.VISIBLE
binding.routeOverview.visibility = View.VISIBLE
binding.tripProgressCard.visibility = View.VISIBLE
// move the camera to overview when new route is available
navigationCamera.requestNavigationCameraToOverview()
}
private fun clearRouteAndStopNavigation() {
// clear
mapboxNavigation.setRoutes(listOf())
// stop simulation
mapboxReplayer.stop()
// hide UI elements
binding.soundButton.visibility = View.INVISIBLE
binding.maneuverView.visibility = View.INVISIBLE
binding.routeOverview.visibility = View.INVISIBLE
binding.tripProgressCard.visibility = View.INVISIBLE
}
private fun startSimulation(route: DirectionsRoute) {
mapboxReplayer.run {
stop()
clearEvents()
val replayEvents = ReplayRouteMapper().mapDirectionsRouteGeometry(route)
pushEvents(replayEvents)
seekTo(replayEvents.first())
play()
}
}
}
So can someone help me where or how to inject something like
if(navigation == completed){
//Do some stuff
}
You already have a RouteProgressObserver
, so you could check the route state and when it is COMPLETE you can do something. See https://docs.mapbox.com/android/navigation/guides/turn-by-turn-navigation/route-progress/#listen-to-progress-change for more information about route progress.