Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.view.View.VISIBLE
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.bindgen.Expected
import com.mapbox.geojson.Point
Expand Down Expand Up @@ -52,6 +53,7 @@ import com.mapbox.navigation.ui.maps.location.NavigationLocationProvider
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowApi
import com.mapbox.navigation.ui.maps.route.arrow.api.MapboxRouteArrowView
import com.mapbox.navigation.ui.maps.route.arrow.model.RouteArrowOptions
import com.mapbox.navigation.ui.maps.route.line.MapboxRouteLineApiExtensions.setNavigationRoutes
import com.mapbox.navigation.ui.maps.route.line.MapboxRouteLineApiExtensions.setRoutes
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineApi
import com.mapbox.navigation.ui.maps.route.line.api.MapboxRouteLineView
Expand Down Expand Up @@ -89,6 +91,8 @@ class MapboxNavigationActivity : AppCompatActivity() {
// location puck integration
private val navigationLocationProvider = NavigationLocationProvider()

private val waypoints = mutableListOf<Point>()

// camera
private lateinit var navigationCamera: NavigationCamera
private lateinit var viewportDataSource: MapboxNavigationViewportDataSource
Expand Down Expand Up @@ -229,15 +233,19 @@ class MapboxNavigationActivity : AppCompatActivity() {
}

private val routesObserver = RoutesObserver { result ->
if (result.routes.isNotEmpty()) {
if (result.navigationRoutes.isNotEmpty()) {
// generate route geometries asynchronously and render them
CoroutineScope(Dispatchers.Main).launch {
val result = routeLineAPI.setRoutes(
listOf(RouteLine(result.routes.first(), null))
)
val style = mapboxMap.getStyle()
if (style != null) {
routeLineView.renderRouteDrawData(style, result)
lifecycleScope.launch {
routeLineAPI.setNavigationRoutes(
newRoutes = result.navigationRoutes,
alternativeRoutesMetadata = mapboxNavigation.getAlternativeMetadataFor(
result.navigationRoutes
)
).apply {
routeLineView.renderRouteDrawData(
binding.mapView.getMapboxMap().getStyle()!!,
this
)
}
}

Expand Down Expand Up @@ -399,7 +407,7 @@ class MapboxNavigationActivity : AppCompatActivity() {
routeLineView.initializeLayers(style)
// add long click listener that search for a route to the clicked destination
binding.mapView.gestures.addOnMapLongClickListener { point ->
findRoute(point)
addWaypoint(point)
true
}
}
Expand Down Expand Up @@ -460,24 +468,26 @@ class MapboxNavigationActivity : AppCompatActivity() {
voiceInstructionsPlayer.shutdown()
}

private fun findRoute(destination: Point) {
private fun addWaypoint(destination: Point) {
val origin = navigationLocationProvider.lastLocation?.let {
Point.fromLngLat(it.longitude, it.latitude)
} ?: return

waypoints.add(destination)

mapboxNavigation.requestRoutes(
RouteOptions.builder()
.applyDefaultNavigationOptions()
.applyLanguageAndVoiceUnitOptions(this)
.coordinatesList(listOf(origin, destination))
.layersList(listOf(mapboxNavigation.getZLevel(), null))
.alternatives(true)
.coordinatesList(listOf(origin) + waypoints)
.build(),
object : NavigationRouterCallback {
override fun onRoutesReady(
routes: List<NavigationRoute>,
routerOrigin: RouterOrigin
) {
setRouteAndStartNavigation(routes)
setRoutePreview(routes)
}

override fun onFailure(
Expand All @@ -494,9 +504,25 @@ class MapboxNavigationActivity : AppCompatActivity() {
)
}

private fun setRouteAndStartNavigation(route: List<NavigationRoute>) {


private fun setRoutePreview(route: List<NavigationRoute>) {
// set route
mapboxNavigation.setNavigationRoutes(route)
mapboxNavigation.previewNavigationRoutes(route)
binding.navigate.visibility = VISIBLE
binding.navigate.setOnClickListener {
setRoute()
binding.navigate.visibility = INVISIBLE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why INVISIBLE, not GONE? The same question for xml.

}

// move the camera to overview when new route is available
navigationCamera.requestNavigationCameraToOverview()
}

private fun setRoute() {
// set route
mapboxNavigation.setNavigationRoutes(mapboxNavigation.getPreviewedNavigationRoutes())
waypoints.clear()

// show UI elements
binding.soundButton.visibility = VISIBLE
Expand All @@ -506,12 +532,12 @@ class MapboxNavigationActivity : AppCompatActivity() {
binding.soundButton.unmuteAndExtend(2000L)

// move the camera to overview when new route is available
navigationCamera.requestNavigationCameraToOverview()
navigationCamera.requestNavigationCameraToFollowing()
}

private fun clearRouteAndStopNavigation() {
// clear
mapboxNavigation.setRoutes(listOf())
mapboxNavigation.clearRoutes()

// hide UI elements
binding.soundButton.visibility = INVISIBLE
Expand Down
10 changes: 10 additions & 0 deletions examples/src/main/res/layout/layout_activity_navigation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/navigate"
android:visibility="invisible"
android:text="Navigate"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>

<androidx.cardview.widget.CardView
android:id="@+id/tripProgressCard"
android:layout_width="0dp"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package com.mapbox.navigation.instrumentation_tests.core

import android.location.Location
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.MapboxNavigationProvider
import com.mapbox.navigation.core.directions.session.RoutesExtra
import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
import com.mapbox.navigation.core.trip.session.NavigationSessionState
import com.mapbox.navigation.core.trip.session.NavigationSessionStateV2
import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity
import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.routeProgressUpdates
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.routesUpdates
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.sdkTest
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.waitForNewRoute
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.waitForPreviewRoute
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.waitForRoutesCleanUp
import com.mapbox.navigation.instrumentation_tests.utils.routes.RoutesProvider
import com.mapbox.navigation.instrumentation_tests.utils.routes.RoutesProvider.toNavigationRoutes
import com.mapbox.navigation.testing.ui.BaseTest
import com.mapbox.navigation.testing.ui.utils.getMapboxAccessTokenFromResources
import com.mapbox.navigation.testing.ui.utils.runOnMainSync
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.Objects
import kotlin.reflect.typeOf

class PreviewRoutesTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.java) {

override fun setupMockLocation(): Location = mockLocationUpdatesRule.generateLocationUpdate {
latitude = 38.894721
longitude = -77.031991
}

@get:Rule
val mapboxNavigationRule = MapboxNavigationRule()
private lateinit var mapboxNavigation: MapboxNavigation

@Before
fun setUp() {
runOnMainSync {
mapboxNavigation = MapboxNavigationProvider.create(
NavigationOptions.Builder(activity)
.accessToken(getMapboxAccessTokenFromResources(activity))
.build()
)
}
}

@Test
fun preview_route_from_free_drive() = sdkTest {
val routes = RoutesProvider.dc_very_short(activity).toNavigationRoutes()
pushPrimaryRouteOriginAsLocation(routes)
mapboxNavigation.startTripSession()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also add tests:

  1. where you don't start trip session to check the transition from Idle to RoutePreview.
  2. Where you are in AG and then stop trip session and set routes preview (to check AG -> RoutePreview transition)


mapboxNavigation.previewNavigationRoutes(routes)
val previewRouteUpdate = mapboxNavigation.waitForPreviewRoute()

assertEquals(routes, previewRouteUpdate.navigationRoutes)
assertEquals(routes, mapboxNavigation.getPreviewedNavigationRoutes())
assertEquals(emptyList<NavigationRoute>(), mapboxNavigation.getNavigationRoutes())
assertIs<NavigationSessionState.FreeDrive>(mapboxNavigation.getNavigationSessionState())
assertIs<NavigationSessionStateV2.RoutePreview>(mapboxNavigation.getNavigationSessionStateV2())
}

@Test
fun preview_route_with_alternative_from_idle() = sdkTest {
val routes = RoutesProvider.dc_short_with_alternative(activity).toNavigationRoutes()
pushPrimaryRouteOriginAsLocation(routes)

mapboxNavigation.previewNavigationRoutes(routes)
val previewRouteUpdate = mapboxNavigation.waitForPreviewRoute()
val previewedRouteMetadata = mapboxNavigation.getAlternativeMetadataFor(
previewRouteUpdate.navigationRoutes[1]
)

assertNotNull(previewedRouteMetadata)
}

@Test
fun start_active_guidance_after_preview() = sdkTest {
val routes = RoutesProvider.dc_short_with_alternative(activity).toNavigationRoutes()
pushPrimaryRouteOriginAsLocation(routes)
mapboxNavigation.startTripSession()
mapboxNavigation.previewNavigationRoutes(routes)
mapboxNavigation.waitForPreviewRoute()

mapboxNavigation.setNavigationRoutes(mapboxNavigation.getPreviewedNavigationRoutes())
val activeGuidanceRouteUpdate = mapboxNavigation.waitForNewRoute()

assertEquals(routes, activeGuidanceRouteUpdate.navigationRoutes)
assertIs<NavigationSessionState.ActiveGuidance>(mapboxNavigation.getNavigationSessionState())
assertIs<NavigationSessionStateV2.ActiveGuidance>(mapboxNavigation.getNavigationSessionStateV2())
}

@Test
fun switch_to_free_drive_after_preview() = sdkTest {
val routes = RoutesProvider.dc_very_short(activity).toNavigationRoutes()
pushPrimaryRouteOriginAsLocation(routes)
mapboxNavigation.startTripSession()
mapboxNavigation.previewNavigationRoutes(routes)
mapboxNavigation.waitForPreviewRoute()

mapboxNavigation.clearRoutes()
val freeDriveRoutesUpdate = mapboxNavigation.waitForRoutesCleanUp()

assertEquals(emptyList<NavigationRoute>(), freeDriveRoutesUpdate.navigationRoutes)
assertEquals(emptyList<NavigationRoute>(), mapboxNavigation.getNavigationRoutes())
assertIs<NavigationSessionState.FreeDrive>(mapboxNavigation.getNavigationSessionState())
assertIs<NavigationSessionStateV2.FreeDrive>(mapboxNavigation.getNavigationSessionStateV2())
}

@Test
fun start_preview_after_active_guidance() = sdkTest {
val routes = RoutesProvider.dc_short_with_alternative(activity).toNavigationRoutes()
pushPrimaryRouteOriginAsLocation(routes)
mapboxNavigation.startTripSession()
mapboxNavigation.setNavigationRoutes(routes)
mapboxNavigation.waitForNewRoute()
val activeGuidanceRouteProgress = mapboxNavigation.routeProgressUpdates().first()

val routeUpdates = mutableListOf<RoutesUpdatedResult>()
mapboxNavigation.registerRoutesObserver {
routeUpdates.add(it)
}
mapboxNavigation.previewNavigationRoutes(mapboxNavigation.getNavigationRoutes())
mapboxNavigation.waitForPreviewRoute()
val previewRouteProgress = withTimeoutOrNull(1) {
mapboxNavigation.routeProgressUpdates().first()
}

assertEquals(
listOf(RoutesExtra.ROUTES_UPDATE_REASON_NEW, RoutesExtra.ROUTES_UPDATE_REASON_PREVIEW),
routeUpdates.map { it.reason }
)
assertNotNull(activeGuidanceRouteProgress)
assertNull(previewRouteProgress)
assertIs<NavigationSessionState.FreeDrive>(mapboxNavigation.getNavigationSessionState())
assertIs<NavigationSessionStateV2.RoutePreview>(mapboxNavigation.getNavigationSessionStateV2())
}

private fun pushPrimaryRouteOriginAsLocation(routes: List<NavigationRoute>) {
val origin = routes.first().routeOptions.coordinatesList().first()
mockLocationUpdatesRule.pushLocationUpdate {
latitude = origin.latitude()
longitude = origin.longitude()
}
}
}

private inline fun <reified T> assertIs(obj: Any) {
assertTrue(
"expected an instance of ${T::class.java.name}, but it is $obj (${obj.javaClass.name})",
obj is T
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,24 @@ suspend fun MapboxNavigation.setNavigationRoutesAndWaitForAlternativesUpdate(
waitForAlternativeRoute()
}

suspend fun MapboxNavigation.waitForNewRoute() {
suspend fun MapboxNavigation.waitForNewRoute(): RoutesUpdatedResult =
waitForRoutesUpdate(RoutesExtra.ROUTES_UPDATE_REASON_NEW)
}

suspend fun MapboxNavigation.waitForRoutesCleanUp(): RoutesUpdatedResult =
waitForRoutesUpdate(RoutesExtra.ROUTES_UPDATE_REASON_CLEAN_UP)

suspend fun MapboxNavigation.waitForAlternativeRoute() {
waitForRoutesUpdate(RoutesExtra.ROUTES_UPDATE_REASON_ALTERNATIVE)
}

suspend fun MapboxNavigation.waitForPreviewRoute(): RoutesUpdatedResult {
return waitForRoutesUpdate(RoutesExtra.ROUTES_UPDATE_REASON_PREVIEW)
}

private suspend fun MapboxNavigation.waitForRoutesUpdate(
@RoutesExtra.RoutesUpdateReason reason: String
) {
routesUpdates()
): RoutesUpdatedResult {
return routesUpdates()
.filter { it.reason == reason }
.first()
}
Expand Down
Loading