Skip to content

Commit ad3f0ba

Browse files
VysotskiVadimgithub-actions[bot]
authored andcommitted
NAVAND-5543 API to switch between alternatives (#8985) (#9097)
GitOrigin-RevId: 82d210ac2089007a202f897824bd6408c63875bf
1 parent 5de37ff commit ad3f0ba

File tree

5 files changed

+157
-0
lines changed

5 files changed

+157
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Added experimental `MapboxNavigation#switchToAlternativeRoute` that support switching between alternative routes with different amount of waypoints.

instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteAlternativesTest.kt

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import com.mapbox.geojson.Point
99
import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI
1010
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
1111
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
12+
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
1213
import com.mapbox.navigation.base.internal.route.routeOptions
1314
import com.mapbox.navigation.base.route.NavigationRoute
15+
import com.mapbox.navigation.base.trip.model.RouteProgressState
1416
import com.mapbox.navigation.core.MapboxNavigation
1517
import com.mapbox.navigation.core.directions.session.RoutesExtra
1618
import com.mapbox.navigation.core.internal.extensions.flowLocationMatcherResult
@@ -23,13 +25,15 @@ import com.mapbox.navigation.testing.ui.utils.coroutines.routeProgressUpdates
2325
import com.mapbox.navigation.testing.ui.utils.coroutines.routesUpdates
2426
import com.mapbox.navigation.testing.ui.utils.coroutines.sdkTest
2527
import com.mapbox.navigation.testing.ui.utils.coroutines.setNavigationRoutesAsync
28+
import com.mapbox.navigation.testing.ui.utils.coroutines.switchToAlternativeAsync
2629
import com.mapbox.navigation.testing.utils.history.MapboxHistoryTestRule
2730
import com.mapbox.navigation.testing.utils.http.MockDirectionsRequestHandler
2831
import com.mapbox.navigation.testing.utils.http.MockDynamicDirectionsRefreshHandler
2932
import com.mapbox.navigation.testing.utils.location.MockLocationReplayerRule
3033
import com.mapbox.navigation.testing.utils.location.stayOnPosition
3134
import com.mapbox.navigation.testing.utils.openRawResource
3235
import com.mapbox.navigation.testing.utils.readRawFileText
36+
import com.mapbox.navigation.testing.utils.routes.RoutesProvider
3337
import com.mapbox.navigation.testing.utils.withMapboxNavigation
3438
import kotlinx.coroutines.CoroutineScope
3539
import kotlinx.coroutines.CoroutineStart
@@ -416,6 +420,102 @@ class RouteAlternativesTest : BaseCoreNoCleanUpTest() {
416420
}
417421
}
418422

423+
@Test
424+
fun switch_from_multi_leg_primary_to_single_leg_alternative_and_back() = sdkTest {
425+
withMapboxNavigation(
426+
historyRecorderRule = mapboxHistoryTestRule,
427+
) { mapboxNavigation ->
428+
val mockRoute = RoutesProvider.dc_short_alternative_has_more_legs(context)
429+
mockWebServerRule.requestHandlers.addAll(mockRoute.mockRequestHandlers)
430+
431+
val routes = mapboxNavigation.requestRoutes(
432+
RouteOptions.builder()
433+
.applyDefaultNavigationOptions()
434+
.applyLanguageAndVoiceUnitOptions(context)
435+
.baseUrl(mockWebServerRule.baseUrl)
436+
.waypointsPerRoute(true)
437+
.coordinatesList(mockRoute.routeWaypoints).build(),
438+
).getSuccessfulResultOrThrowException().routes
439+
440+
mockLocationReplayerRule.playRoute(
441+
routes[0].directionsRoute,
442+
eventsToDrop = 15,
443+
)
444+
mapboxNavigation.startTripSession()
445+
mapboxNavigation.setNavigationRoutes(routes)
446+
447+
mapboxNavigation.routeProgressUpdates()
448+
.filter {
449+
it.currentLegProgress?.legIndex == 0 &&
450+
it.currentRouteGeometryIndex > 16
451+
}
452+
.first()
453+
454+
mapboxNavigation.switchToAlternativeRoute(routes[1])
455+
456+
val routeProgress = mapboxNavigation.routeProgressUpdates()
457+
.first {
458+
it.currentState == RouteProgressState.TRACKING &&
459+
it.navigationRoute.id == routes[1].id
460+
}
461+
assertEquals(
462+
1,
463+
routeProgress.remainingWaypoints,
464+
)
465+
}
466+
}
467+
468+
@Test
469+
fun switch_to_alternative_when_no_routes_are_set() = sdkTest {
470+
withMapboxNavigation(
471+
historyRecorderRule = mapboxHistoryTestRule,
472+
) { mapboxNavigation ->
473+
setupMockRequestHandlers()
474+
val routes = mapboxNavigation.requestNavigationRoutes(startCoordinates)
475+
val result = mapboxNavigation.switchToAlternativeAsync(routes[0])
476+
assertTrue(result.isError)
477+
}
478+
}
479+
480+
@Test
481+
fun switch_to_not_tracking_alternative() = sdkTest {
482+
withMapboxNavigation(
483+
historyRecorderRule = mapboxHistoryTestRule,
484+
) { mapboxNavigation ->
485+
setupMockRequestHandlers()
486+
val routes = mapboxNavigation.requestNavigationRoutes(startCoordinates)
487+
val differentRoutes = mapboxNavigation.requestNavigationRoutes(continueCoordinates)
488+
489+
stayOnPosition(startCoordinates.first(), 0f) {
490+
mapboxNavigation.startTripSession()
491+
mapboxNavigation.setNavigationRoutes(routes)
492+
mapboxNavigation.routeProgressUpdates().first()
493+
494+
val result = mapboxNavigation.switchToAlternativeAsync(differentRoutes[0])
495+
assertTrue(result.isError)
496+
}
497+
}
498+
}
499+
500+
@Test
501+
fun use_primary_during_switching_to_alternative() = sdkTest {
502+
withMapboxNavigation(
503+
historyRecorderRule = mapboxHistoryTestRule,
504+
) { mapboxNavigation ->
505+
setupMockRequestHandlers()
506+
val routes = mapboxNavigation.requestNavigationRoutes(startCoordinates)
507+
508+
stayOnPosition(startCoordinates.first(), 0f) {
509+
mapboxNavigation.startTripSession()
510+
mapboxNavigation.setNavigationRoutes(routes)
511+
mapboxNavigation.routeProgressUpdates().first()
512+
513+
val result = mapboxNavigation.switchToAlternativeAsync(routes.first())
514+
assertTrue(result.isError)
515+
}
516+
}
517+
}
518+
419519
private fun setupMockRequestHandlers() {
420520
// Nav-native requests alternate routes, so we are only
421521
// ensuring the initial route has alternatives.

libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/utils/coroutines/Adapters.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ suspend fun MapboxNavigation.setNavigationRoutesAsync(
216216
setNavigationRoutes(routes, initialLegIndex, callback)
217217
}
218218

219+
@ExperimentalPreviewMapboxNavigationAPI
220+
suspend fun MapboxNavigation.switchToAlternativeAsync(
221+
alternativeRoute: NavigationRoute,
222+
) = suspendCoroutine<Expected<RoutesSetError, RoutesSetSuccess>> { continuation ->
223+
val callback = RoutesSetCallback { result -> continuation.resume(result) }
224+
switchToAlternativeRoute(alternativeRoute, callback)
225+
}
226+
219227
sealed class RouteRequestResult {
220228
data class Success(
221229
val routes: List<NavigationRoute>,

navigation/api/current.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ package com.mapbox.navigation.core {
9898
method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void startTripSessionWithPermissionCheck(boolean withForegroundService = true);
9999
method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void startTripSessionWithPermissionCheck();
100100
method public void stopTripSession();
101+
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void switchToAlternativeRoute(com.mapbox.navigation.base.route.NavigationRoute alternativeRoute, com.mapbox.navigation.core.RoutesSetCallback? callback = null);
101102
method public void unregisterArrivalObserver(com.mapbox.navigation.core.arrival.ArrivalObserver arrivalObserver);
102103
method public void unregisterBannerInstructionsObserver(com.mapbox.navigation.core.trip.session.BannerInstructionsObserver bannerInstructionsObserver);
103104
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void unregisterDeveloperMetadataObserver(com.mapbox.navigation.core.DeveloperMetadataObserver developerMetadataObserver);

navigation/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
2525
import com.mapbox.navigation.base.internal.accounts.SkuIdProvider
2626
import com.mapbox.navigation.base.internal.accounts.SkuIdProviderImpl
2727
import com.mapbox.navigation.base.internal.clearCache
28+
import com.mapbox.navigation.base.internal.extensions.internalAlternativeRouteIndices
2829
import com.mapbox.navigation.base.internal.performance.PerformanceTracker
2930
import com.mapbox.navigation.base.internal.tilestore.NavigationTileStoreOwner
3031
import com.mapbox.navigation.base.internal.trip.notification.TripNotificationInterceptorOwner
@@ -1070,6 +1071,52 @@ class MapboxNavigation @VisibleForTesting internal constructor(
10701071
)
10711072
}
10721073

1074+
/**
1075+
* Switches [MapboxNavigation] to alternative route, i.e. the selected alternative become primary
1076+
* route, while primary route becomes alternative.
1077+
* Limitation: switch to alternative route could be performed only when [getTripSessionState]
1078+
* is [TripSessionState.STARTED] and after at lest one [RouteProgress] was emitted after
1079+
* the latest routes update.
1080+
* @param alternativeRoute is an alternative route the navigation should switch to. It should be
1081+
* present among [getNavigationRoutes] and should not be the same as primary route.
1082+
* @param callback notifies about result.
1083+
*/
1084+
@ExperimentalPreviewMapboxNavigationAPI
1085+
fun switchToAlternativeRoute(
1086+
alternativeRoute: NavigationRoute,
1087+
callback: RoutesSetCallback? = null,
1088+
) {
1089+
val routeProgress = tripSession.getRouteProgress()
1090+
if (routeProgress == null) {
1091+
val errorMessage = "No route progress available"
1092+
logE(errorMessage, LOG_CATEGORY)
1093+
callback?.onRoutesSet(ExpectedFactory.createError(RoutesSetError(errorMessage)))
1094+
return
1095+
}
1096+
val alternativeIndices = routeProgress
1097+
.internalAlternativeRouteIndices()[alternativeRoute.id]
1098+
val allRoutes = getNavigationRoutes()
1099+
val alternativeSwitchTo = allRoutes.firstOrNull { it.id == alternativeRoute.id }
1100+
if (alternativeIndices == null || alternativeSwitchTo == null) {
1101+
val errorMessage = "Can't switch to alternative ${alternativeRoute.id} " +
1102+
"as it isn't present among currently tracked alternatives: " +
1103+
"${allRoutes.drop(1).map { it.id }}"
1104+
logE(errorMessage, LOG_CATEGORY)
1105+
callback?.onRoutesSet(
1106+
ExpectedFactory.createError(RoutesSetError(errorMessage)),
1107+
)
1108+
return
1109+
}
1110+
logI(LOG_CATEGORY) {
1111+
"Switching to ${alternativeSwitchTo.id} leg ${alternativeIndices.legIndex}"
1112+
}
1113+
val newRoutes = allRoutes.toMutableList().apply {
1114+
remove(alternativeSwitchTo)
1115+
add(0, alternativeSwitchTo)
1116+
}
1117+
setNavigationRoutes(newRoutes, alternativeIndices.legIndex)
1118+
}
1119+
10731120
/***
10741121
* Sets routes to preview.
10751122
* Triggers an update in [RoutesPreviewObserver] and changes [MapboxNavigation.getRoutesPreview].

0 commit comments

Comments
 (0)