mapbox / mapbox/mapbox-navigation-android

Off route geometry disappears

Open
#3,787 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

archived
Dominant language
Kotlin
Stars
651
Forks
321
PR merge metrics
No merged PRs in 30d

Description

Please run code below, simple replace BasicNavigationFragment from your examples. Can you please tell me how to fix it so the off-route road will not disappear (it is the section at the end of the road)? Basically the point is to navigate also through waypoints I add manually, they are off the road. Thanks

package com.mapbox.navigation.examples.core

import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.ImageButton
import androidx.appcompat.widget.AppCompatImageButton
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson
import com.mapbox.android.core.location.LocationEngine
import com.mapbox.android.core.location.LocationEngineCallback
import com.mapbox.android.core.location.LocationEngineProvider
import com.mapbox.android.core.location.LocationEngineResult
import com.mapbox.api.directions.v5.models.*
import com.mapbox.geojson.Point
import com.mapbox.geojson.utils.PolylineUtils
import com.mapbox.mapboxsdk.Mapbox
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.location.LocationComponent
import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions
import com.mapbox.mapboxsdk.location.OnCameraTrackingChangedListener
import com.mapbox.mapboxsdk.location.modes.CameraMode
import com.mapbox.mapboxsdk.location.modes.RenderMode
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.navigation.base.internal.extensions.applyDefaultParams
import com.mapbox.navigation.base.internal.extensions.coordinates
import com.mapbox.navigation.base.internal.route.RouteUrl
import com.mapbox.navigation.base.trip.model.RouteProgress
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.directions.session.RoutesRequestCallback
import com.mapbox.navigation.core.replay.MapboxReplayer
import com.mapbox.navigation.core.replay.ReplayLocationEngine
import com.mapbox.navigation.core.replay.route.ReplayProgressObserver
import com.mapbox.navigation.core.telemetry.events.FeedbackEvent.UI
import com.mapbox.navigation.core.trip.session.*
import com.mapbox.navigation.examples.R
import com.mapbox.navigation.examples.utils.Utils
import com.mapbox.navigation.examples.utils.Utils.PRIMARY_ROUTE_BUNDLE_KEY
import com.mapbox.navigation.examples.utils.Utils.getRouteFromBundle
import com.mapbox.navigation.ui.NavigationButton
import com.mapbox.navigation.ui.NavigationConstants
import com.mapbox.navigation.ui.SoundButton
import com.mapbox.navigation.ui.camera.DynamicCamera
import com.mapbox.navigation.ui.camera.NavigationCamera
import com.mapbox.navigation.ui.feedback.FeedbackBottomSheet
import com.mapbox.navigation.ui.feedback.FeedbackBottomSheetListener
import com.mapbox.navigation.ui.feedback.FeedbackItem
import com.mapbox.navigation.ui.instruction.NavigationAlertView
import com.mapbox.navigation.ui.internal.utils.ViewUtils
import com.mapbox.navigation.ui.map.NavigationMapboxMap
import com.mapbox.navigation.ui.map.OnWayNameChangedListener
import com.mapbox.navigation.ui.summary.SummaryBottomSheet
import com.mapbox.navigation.ui.voice.NavigationSpeechPlayer
import com.mapbox.navigation.ui.voice.SpeechPlayerProvider
import com.mapbox.navigation.ui.voice.VoiceInstructionLoader
import kotlinx.android.synthetic.main.fragment_basic_navigation.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.Cache
import timber.log.Timber
import java.io.File
import java.lang.ref.WeakReference
import java.util.Locale

/**
* This fragment shows how to use the UI SDK standalone components
* including the InstructionView and SummaryBottomSheet and voice to
* build the turn-by-turn navigation experience with Navigation Core SDK.
*/
class BasicNavigationFragment :
Fragment(),
OnMapReadyCallback,
FeedbackBottomSheetListener,
OnWayNameChangedListener, OffRouteObserver {

private val routeOverviewPadding by lazy { buildRouteOverviewPadding() }

private lateinit var mapboxNavigation: MapboxNavigation
private var navigationMapboxMap: NavigationMapboxMap? = null
private lateinit var speechPlayer: NavigationSpeechPlayer
private lateinit var destination: LatLng
private lateinit var summaryBehavior: BottomSheetBehavior
private lateinit var routeOverviewButton: ImageButton
private lateinit var cancelBtn: AppCompatImageButton
private lateinit var feedbackButton: NavigationButton
private lateinit var instructionSoundButton: NavigationButton
private lateinit var alertView: NavigationAlertView
private val mapboxReplayer = MapboxReplayer()

private var mapboxMap: MapboxMap? = null
private var locationComponent: LocationComponent? = null
private var directionRoute: DirectionsRoute? = null

private var feedbackItem: FeedbackItem? = null
private var feedbackEncodedScreenShot: String? = null

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_basic_navigation, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViews()

mapView.onCreate(savedInstanceState)
mapView.getMapAsync(this)

initNavigation()
initializeSpeechPlayer()
}

override fun onResume() {
super.onResume()
mapView.onResume()
}

override fun onPause() {
super.onPause()
mapView.onPause()
}

override fun onStart() {
super.onStart()
mapView.onStart()
mapboxNavigation.registerOffRouteObserver(this)
}

override fun onStop() {
super.onStop()
mapView.onStop()
}

override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}

override fun onDestroyView() {
super.onDestroyView()
mapView.onDestroy()

mapboxReplayer.finish()
mapboxNavigation.unregisterTripSessionStateObserver(tripSessionStateObserver)
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
mapboxNavigation.unregisterBannerInstructionsObserver(bannerInstructionObserver)
mapboxNavigation.unregisterVoiceInstructionsObserver(voiceInstructionsObserver)

mapboxNavigation.stopTripSession()
mapboxNavigation.onDestroy()

speechPlayer.onDestroy()
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
// This is not the most efficient way to preserve the route on a device rotation.
// This is here to demonstrate that this event needs to be handled in order to
// redraw the route line after a rotation.
directionRoute?.let {
outState.putString(PRIMARY_ROUTE_BUNDLE_KEY, it.toJson())
}
}

override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
directionRoute = getRouteFromBundle(savedInstanceState)
}

@SuppressLint("MissingPermission")
override fun onMapReady(mapboxMap: MapboxMap) {
Timber.d("onMapReady")
this.mapboxMap = mapboxMap
mapboxMap.moveCamera(CameraUpdateFactory.zoomTo(15.0))

mapboxMap.setStyle(Style.MAPBOX_STREETS) { style ->
locationComponent = mapboxMap.locationComponent.apply {
activateLocationComponent(
LocationComponentActivationOptions.builder(
requireContext(),
style
).build()
)
cameraMode = CameraMode.TRACKING
isLocationComponentEnabled = true
}

GlobalScope.launch(Dispatchers.Main) {
delay(1000)
val originLocation = Point.fromLngLat(21.280353, 48.726650)
// locationComponent?.lastKnownLocation?.let { originLocation ->
mapboxNavigation.requestRoutes(
RouteOptions.builder().applyDefaultParams()
.profile(RouteUrl.PROFILE_DRIVING_TRAFFIC)
// .radiuses("100;100;100;10;10;10;10")
.accessToken(Utils.getMapboxAccessToken(requireContext()))
.coordinates(
originLocation,
null,
Point.fromLngLat(21.306181, 48.715949),
)
.alternatives(true)
.build(),
routesReqCallback
)
// }
}

navigationMapboxMap = NavigationMapboxMap(mapView, mapboxMap, this, true).apply {
addOnCameraTrackingChangedListener(cameraTrackingChangedListener)
addProgressChangeListener(mapboxNavigation)
setCamera(DynamicCamera(mapboxMap))
}

if (shouldSimulateRoute()) {
mapboxNavigation
.registerRouteProgressObserver(ReplayProgressObserver(mapboxReplayer))
mapboxReplayer.pushRealLocation(requireContext(), 0.0)
mapboxReplayer.playbackSpeed(3.toDouble())
mapboxReplayer.play()
}
mapboxNavigation
.navigationOptions
.locationEngine
.getLastLocation(locationListenerCallback)

directionRoute?.let {
navigationMapboxMap?.drawRoute(it)
mapboxNavigation.setRoutes(listOf(it))
startNavigation.isEnabled = true
}

if (directionRoute == null) {
Snackbar.make(
requireView(),
R.string.msg_long_press_map_to_place_waypoint,
Snackbar.LENGTH_SHORT
).show()
}
}
}

// InstructionView Feedback Bottom Sheet listener
override fun onFeedbackSelected(feedbackItem: FeedbackItem?) {
feedbackItem?.let { feedback ->
this.feedbackItem = feedback
sendFeedback()
}
}

override fun onFeedbackDismissed() {
// do nothing
}

override fun onWayNameChanged(wayName: String) {
wayNameView.updateWayNameText(wayName)
if (summaryBehavior.state == BottomSheetBehavior.STATE_HIDDEN) {
hideWayNameView()
} else {
showWayNameView()
}
}

@Suppress("DEPRECATION")
@SuppressLint("MissingPermission")
private fun initViews() {
startNavigation.apply {
visibility = View.VISIBLE
isEnabled = false
setOnClickListener {
Timber.d("start navigation")
if (mapboxNavigation.getRoutes().isNotEmpty()) {
updateCameraOnNavigationStateChange(true)
navigationMapboxMap?.startCamera(directionRoute!!)
mapboxNavigation.startTripSession()
// mapboxNavigation.getRerouteController()!!.interrupt()
}
}
}

summaryBottomSheet.visibility = View.GONE
summaryBehavior = BottomSheetBehavior.from(summaryBottomSheet).apply {
isHideable = false
setBottomSheetCallback(bottomSheetCallback)
}

routeOverviewButton = requireView().findViewById(R.id.routeOverviewBtn)
routeOverviewButton.setOnClickListener {
navigationMapboxMap?.showRouteOverview(routeOverviewPadding)
recenterBtn.show()
}

cancelBtn = requireView().findViewById(R.id.cancelBtn)
cancelBtn.setOnClickListener {
mapboxNavigation.stopTripSession()
}

recenterBtn.apply {
hide()
addOnClickListener {
recenterBtn.hide()
summaryBehavior.isHideable = false
summaryBehavior.state = BottomSheetBehavior.STATE_EXPANDED
showWayNameView()
navigationMapboxMap?.resetPadding()
navigationMapboxMap
?.resetCameraPositionWith(NavigationCamera.NAVIGATION_TRACKING_MODE_GPS)
}
}

wayNameView.apply {
visibility = View.GONE
}

feedbackButton = instructionView.retrieveFeedbackButton().apply {
hide()
addOnClickListener {
showFeedbackBottomSheet()
}
}
instructionSoundButton = instructionView.retrieveSoundButton().apply {
hide()
addOnClickListener {
val soundButton = instructionSoundButton
if (soundButton is SoundButton) {
speechPlayer.isMuted = soundButton.toggleMute()
}
}
}

alertView = instructionView.retrieveAlertView().apply {
hide()
}
}

private fun updateViews(tripSessionState: TripSessionState) {
when (tripSessionState) {
TripSessionState.STARTED -> {
startNavigation.visibility = View.GONE

summaryBottomSheet.visibility = View.VISIBLE
recenterBtn.hide()

instructionView.visibility = View.VISIBLE
feedbackButton.show()
instructionSoundButton.show()
showLogoAndAttribution()
}
TripSessionState.STOPPED -> {
startNavigation.visibility = View.VISIBLE
startNavigation.isEnabled = false

summaryBottomSheet.visibility = View.GONE
recenterBtn.hide()
hideWayNameView()

instructionView.visibility = View.GONE
feedbackButton.hide()
instructionSoundButton.hide()
}
}
}

private fun showLogoAndAttribution() {
summaryBottomSheet.viewTreeObserver.addOnGlobalLayoutListener(
object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
navigationMapboxMap?.retrieveMap()?.uiSettings?.apply {
val bottomMargin = summaryBottomSheet.measuredHeight
setLogoMargins(
logoMarginLeft,
logoMarginTop,
logoMarginRight,
bottomMargin
)
setAttributionMargins(
attributionMarginLeft,
attributionMarginTop,
attributionMarginRight,
bottomMargin
)
}
summaryBottomSheet.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
)
}

private fun initNavigation() {
val accessToken = Utils.getMapboxAccessToken(requireContext())
mapboxNavigation = MapboxNavigation(
MapboxNavigation.defaultNavigationOptionsBuilder(requireContext(), accessToken)
.locationEngine(getLocationEngine())
.build()
)
mapboxNavigation.apply {
registerTripSessionStateObserver(tripSessionStateObserver)
registerRouteProgressObserver(routeProgressObserver)
registerBannerInstructionsObserver(bannerInstructionObserver)
registerVoiceInstructionsObserver(voiceInstructionsObserver)
}
}

private fun initializeSpeechPlayer() {
val cache =
Cache(
File(
requireContext().cacheDir,
InstructionViewActivity.VOICE_INSTRUCTION_CACHE
),
10 * 1024 * 1024
)
val voiceInstructionLoader =
VoiceInstructionLoader(requireContext(), Mapbox.getAccessToken(), cache)
val speechPlayerProvider =
SpeechPlayerProvider(
requireContext(),
Locale.US.language,
true,
voiceInstructionLoader
)
speechPlayer = NavigationSpeechPlayer(speechPlayerProvider)
}

private fun showFeedbackBottomSheet() {
feedbackItem = null
feedbackEncodedScreenShot = null
requireFragmentManager().let {
mapboxMap?.snapshot(this::encodeSnapshot)
FeedbackBottomSheet.newInstance(
this,
NavigationConstants.FEEDBACK_BOTTOM_SHEET_DURATION
)
.show(it, FeedbackBottomSheet.TAG)
}
}

private fun sendFeedback() {
val feedback = feedbackItem
val screenShot = feedbackEncodedScreenShot
if (feedback != null && !screenShot.isNullOrEmpty()) {
mapboxNavigation.postUserFeedback(
feedback.feedbackType,
feedback.description,
UI,
screenShot,
feedback.feedbackSubType.toTypedArray()
)
showFeedbackSentSnackBar(
context = requireContext(),
view = if (summaryBehavior.state == BottomSheetBehavior.STATE_HIDDEN) {
recenterBtn
} else {
summaryBottomSheet
},
setAnchorView = true
)
}
}

private fun encodeSnapshot(snapshot: Bitmap) {
screenshotView.visibility = View.VISIBLE
screenshotView.setImageBitmap(snapshot)
mapView.visibility = View.INVISIBLE
feedbackEncodedScreenShot = ViewUtils.encodeView(ViewUtils.captureView(mapView))
screenshotView.visibility = View.INVISIBLE
mapView.visibility = View.VISIBLE

sendFeedback()
}

private fun showWayNameView() {
wayNameView.updateVisibility(!wayNameView.retrieveWayNameText().isNullOrEmpty())
}

private fun hideWayNameView() {
wayNameView.updateVisibility(false)
}

private fun buildRouteOverviewPadding(): IntArray {
val leftRightPadding =
resources
.getDimension(
com.mapbox.navigation.ui.R.dimen.mapbox_route_overview_left_right_padding
)
.toInt()
val paddingBuffer =
resources
.getDimension(
com.mapbox.navigation.ui.R.dimen.mapbox_route_overview_buffer_padding
)
.toInt()
val instructionHeight = (
resources
.getDimension(
com.mapbox.navigation.ui.R.dimen.mapbox_instruction_content_height
) +
paddingBuffer
)
.toInt()
val summaryHeight =
resources
.getDimension(com.mapbox.navigation.ui.R.dimen.mapbox_summary_bottom_sheet_height)
.toInt()
return intArrayOf(leftRightPadding, instructionHeight, leftRightPadding, summaryHeight)
}

private fun updateCameraOnNavigationStateChange(
navigationStarted: Boolean
) {
navigationMapboxMap?.apply {
if (navigationStarted) {
updateCameraTrackingMode(NavigationCamera.NAVIGATION_TRACKING_MODE_GPS)
updateLocationLayerRenderMode(RenderMode.GPS)
} else {
updateCameraTrackingMode(NavigationCamera.NAVIGATION_TRACKING_MODE_NONE)
updateLocationLayerRenderMode(RenderMode.COMPASS)
}
}
}

// Callbacks and Observers
private val routesReqCallback = object : RoutesRequestCallback {
override fun onRoutesReady(routes: List) {
Timber.d("route request success %s", routes.toString())
if (routes.isNotEmpty()) {
directionRoute = routes[0]
var routeJson = Gson().toJson(directionRoute)

val waypoints = PolylineUtils.decode(directionRoute!!.geometry()!!, 6)
val s = directionRoute!!.geometry()
val newPoints = ArrayList()
newPoints.add(Point.fromLngLat(21.306749, 48.714061))
newPoints.add(Point.fromLngLat(21.304810, 48.713679))
newPoints.add(Point.fromLngLat(21.304005, 48.714631))
newPoints.add(Point.fromLngLat(21.303678, 48.714644))
waypoints.addAll(newPoints)
val offroadGeometry = PolylineUtils.encode(newPoints, 6)

directionRoute!!.routeOptions()!!.coordinates().addAll(newPoints)
val newLegs = ArrayList()
directionRoute!!.legs()?.forEach {
newLegs.add(RouteLeg.builder().distance(it.distance()!!.plus(10.toDouble()))
.admins(it.admins())
.annotation(it.annotation())
.duration(it.duration())
.durationTypical(it.durationTypical())
.incidents(it.incidents())
.steps(getSteps(it.steps()!!, offroadGeometry))
.summary(it.summary()).build())
}
val newRoute = DirectionsRoute.builder().legs(newLegs).voiceLanguage(directionRoute!!.voiceLanguage())
.routeOptions(directionRoute!!.routeOptions())
.distance(directionRoute!!.distance() + 1000).duration(directionRoute!!.duration()).geometry(PolylineUtils.encode(waypoints, 6)).build()

directionRoute = newRoute
routeJson = Gson().toJson(directionRoute)
navigationMapboxMap?.drawRoute(directionRoute!!)
mapboxNavigation.setRoutes(listOf(newRoute))
startNavigation.visibility = View.VISIBLE
startNavigation.isEnabled = true
} else {
startNavigation.isEnabled = false
}
}

override fun onRoutesRequestFailure(throwable: Throwable, routeOptions: RouteOptions) {
}

override fun onRoutesRequestCanceled(routeOptions: RouteOptions) {
}
}

private fun getSteps(steps: List, geometry: String): MutableList {
val newSteps = ArrayList()
newSteps.addAll(steps)
newSteps.removeLast()
val stepToAlter = steps.get(steps.size - 1)
val alteredStep = LegStep.builder().bannerInstructions(stepToAlter.bannerInstructions()!!).distance(stepToAlter.distance())
.drivingSide(stepToAlter.drivingSide()).geometry(stepToAlter.geometry()).maneuver(
StepManeuver.builder().bearingBefore(stepToAlter.maneuver().bearingBefore())
.bearingAfter(stepToAlter.maneuver().bearingAfter())
.instruction("You will arrive to the dirt road entry point")
.modifier("right")
.rawLocation(arrayOf(stepToAlter.maneuver().location().longitude(), stepToAlter.maneuver().location().latitude()).toDoubleArray())
.type("turn")
.build()
).weight(stepToAlter.weight())
.intersections(stepToAlter.intersections()!!)
.mode(stepToAlter.mode())
.name(stepToAlter.name())
.duration(stepToAlter.duration())
.ref(stepToAlter.ref())
.build()
newSteps.add(alteredStep)

val newStep = LegStep.builder().distance(1000.toDouble()).duration(100.toDouble()).
drivingSide("right").mode("driving").name("destination").
maneuver(StepManeuver.builder().instruction("You are close to the end lucky star.").rawLocation(arrayOf(21.303678.toDouble(), 48.714644.toDouble()).toDoubleArray()).bearingAfter(100.toDouble()).bearingBefore(300.toDouble()).type("arrive").build())
.geometry(geometry).weight(300.toDouble()).build()

newSteps.add(newStep)

return newSteps
}

private val tripSessionStateObserver = object : TripSessionStateObserver {
override fun onSessionStateChanged(tripSessionState: TripSessionState) {
when (tripSessionState) {
TripSessionState.STARTED -> {
updateViews(TripSessionState.STARTED)

navigationMapboxMap
?.addOnWayNameChangedListener(this@BasicNavigationFragment)
navigationMapboxMap?.updateWaynameQueryMap(true)
}
TripSessionState.STOPPED -> {
updateViews(TripSessionState.STOPPED)

if (mapboxNavigation.getRoutes().isNotEmpty()) {
navigationMapboxMap?.hideRoute()
}

navigationMapboxMap
?.removeOnWayNameChangedListener(this@BasicNavigationFragment)
navigationMapboxMap?.updateWaynameQueryMap(false)

updateCameraOnNavigationStateChange(false)
}
}
}
}

private val routeProgressObserver = object : RouteProgressObserver {
override fun onRouteProgressChanged(routeProgress: RouteProgress) {
instructionView.updateDistanceWith(routeProgress)
summaryBottomSheet.update(routeProgress)
}
}

private val bannerInstructionObserver = object : BannerInstructionsObserver {
override fun onNewBannerInstructions(bannerInstructions: BannerInstructions) {
instructionView.updateBannerInstructionsWith(bannerInstructions)
}
}

private val voiceInstructionsObserver = object : VoiceInstructionsObserver {
override fun onNewVoiceInstructions(voiceInstructions: VoiceInstructions) {
speechPlayer.play(voiceInstructions)
}
}

private val cameraTrackingChangedListener = object : OnCameraTrackingChangedListener {
override fun onCameraTrackingChanged(currentMode: Int) {
}

override fun onCameraTrackingDismissed() {
if (mapboxNavigation.getTripSessionState() == TripSessionState.STARTED) {
summaryBehavior.isHideable = true
summaryBehavior.state = BottomSheetBehavior.STATE_HIDDEN
hideWayNameView()
}
}
}

private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (summaryBehavior.state == BottomSheetBehavior.STATE_HIDDEN) {
recenterBtn.show()
}
}

override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
}

private val locationListenerCallback = MyLocationEngineCallback(this)

private class MyLocationEngineCallback(fragment: BasicNavigationFragment) :
LocationEngineCallback {

private val fragmentRef = WeakReference(fragment)

override fun onSuccess(result: LocationEngineResult) {
result.locations.firstOrNull()?.let { location ->
Timber.d("location engine callback -> onSuccess location:%s", location)
fragmentRef.get()?.locationComponent?.forceLocationUpdate(location)
}
}

override fun onFailure(exception: Exception) {
Timber.e("location engine callback -> onFailure(%s)", exception.localizedMessage)
}
}

// If shouldSimulateRoute is true a ReplayRouteLocationEngine will be used which is intended
// for testing else a real location engine is used.
private fun getLocationEngine(): LocationEngine {
return if (shouldSimulateRoute()) {
ReplayLocationEngine(mapboxReplayer)
} else {
LocationEngineProvider.getBestLocationEngine(activity!!)
}
}

private fun shouldSimulateRoute(): Boolean {
return PreferenceManager.getDefaultSharedPreferences(requireContext())
.getBoolean(this.getString(R.string.simulate_route_key), true)
}

override fun onOffRouteStateChanged(offRoute: Boolean) {

}
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the BasicNavigationFragment example, especially the RouteOptions coordinates, OffRouteObserver registration, and NavigationMapboxMap.drawRoute call shown in the issue. Reproduce the route with manually added off-road waypoints and inspect how the requested route and rendered geometry change when navigation goes off route. Done means the off-route road section remains visible while navigating through those waypoints.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, kotlin
Domain
mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.