Skip to content

Commit

Permalink
Added new experimental API for requesting a route
Browse files Browse the repository at this point in the history
  • Loading branch information
VysotskiVadim committed Mar 30, 2022
1 parent a1ac57f commit 5db5746
Show file tree
Hide file tree
Showing 16 changed files with 1,363 additions and 55 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Mapbox welcomes participation and contributions from everyone.
## Unreleased

#### Features
- Added `MapboxNavigation:requestRoutes` that is an experimentation API to safely request a route. [#5427](https://github.com/mapbox/mapbox-navigation-android/pull/5427)

#### Bug fixes and improvements
- Fixed `HistoryEventMapper#mapNavigationRoute` for when `SetRouteHistoryRecord` has empty `routeRequest`. [#5614](https://github.com/mapbox/mapbox-navigation-android/pull/5614)
Expand Down
1 change: 1 addition & 0 deletions examples/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ dependencies {

//Coroutines
implementation dependenciesList.coroutinesAndroid
implementation dependenciesList.androidXLifecycleRuntime

// Support libraries
implementation dependenciesList.androidXCore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import android.view.View.VISIBLE
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.mapbox.api.directions.v5.models.DirectionsRoute
import com.mapbox.api.directions.v5.models.RouteOptions
import androidx.lifecycle.lifecycleScope
import com.mapbox.bindgen.Expected
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
Expand All @@ -22,17 +21,15 @@ import com.mapbox.maps.plugin.LocationPuck2D
import com.mapbox.maps.plugin.animation.camera
import com.mapbox.maps.plugin.gestures.gestures
import com.mapbox.maps.plugin.locationcomponent.location
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.base.TimeFormat
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
import com.mapbox.navigation.base.formatter.DistanceFormatterOptions
import com.mapbox.navigation.base.options.EventsAppMetadata
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.base.route.RouterCallback
import com.mapbox.navigation.base.route.RouterFailure
import com.mapbox.navigation.base.route.RouterOrigin
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.MapboxNavigationProvider
import com.mapbox.navigation.core.RequestRoutesResult
import com.mapbox.navigation.core.directions.session.RoutesObserver
import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
Expand Down Expand Up @@ -459,43 +456,23 @@ class MapboxNavigationActivity : AppCompatActivity() {
voiceInstructionsPlayer.shutdown()
}

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

mapboxNavigation.requestRoutes(
RouteOptions.builder()
.applyDefaultNavigationOptions()
.applyLanguageAndVoiceUnitOptions(this)
.coordinatesList(listOf(origin, destination))
.layersList(listOf(mapboxNavigation.getZLevel(), null))
.build(),
object : RouterCallback {
override fun onRoutesReady(
routes: List<DirectionsRoute>,
routerOrigin: RouterOrigin
) {
setRouteAndStartNavigation(routes.first())
}

override fun onFailure(
reasons: List<RouterFailure>,
routeOptions: RouteOptions
) {
// no impl
}

override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) {
// no impl
}
lifecycleScope.launchWhenCreated {
val result = mapboxNavigation.requestRoutes { builder ->
builder
.fromCurrentLocation()
.toDestination(destination)
}
)
if (result is RequestRoutesResult.Successful) {
setRouteAndStartNavigation(result.routes.first())
}
}
}

private fun setRouteAndStartNavigation(route: DirectionsRoute) {
private fun setRouteAndStartNavigation(route: NavigationRoute) {
// set route
mapboxNavigation.setRoutes(listOf(route))
mapboxNavigation.setNavigationRoutes(listOf(route))

// show UI elements
binding.soundButton.visibility = VISIBLE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.mapbox.navigation.instrumentation_tests.core

import android.location.Location
import androidx.test.platform.app.InstrumentationRegistry
import com.mapbox.api.directions.v5.DirectionsCriteria
import com.mapbox.geojson.Point
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.core.MapboxNavigationProvider
import com.mapbox.navigation.core.RequestRoutesResult
import com.mapbox.navigation.instrumentation_tests.R
import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity
import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule
import com.mapbox.navigation.instrumentation_tests.utils.http.MockDirectionsRequestHandler
import com.mapbox.navigation.instrumentation_tests.utils.readRawFileText
import com.mapbox.navigation.instrumentation_tests.utils.runOnMainSync
import com.mapbox.navigation.testing.ui.BaseTest
import com.mapbox.navigation.testing.ui.utils.getMapboxAccessTokenFromResources
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class RouteOptionsBuilderTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.java) {

@get:Rule
val mapboxNavigationRule = MapboxNavigationRule()

@Test
fun navigateFromCurrentLocation() {
mockWebServerRule.requestHandlers.add(
MockDirectionsRequestHandler(
profile = DirectionsCriteria.PROFILE_DRIVING,
jsonResponse = readRawFileText(activity, R.raw.reroute_response_dc_very_short),
expectedCoordinates = null,
relaxedExpectedCoordinates = true
)
)

val mapboxNavigation = runOnMainSync {
val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
MapboxNavigationProvider.create(
NavigationOptions.Builder(context)
.accessToken(getMapboxAccessTokenFromResources(context))
.build()
)
}

val routeRequest = runBlocking(Dispatchers.Main) {
mapboxNavigation.startTripSession()
mapboxNavigation.requestRoutes { builder ->
builder
.fromCurrentLocation()
.toDestination(
coordinate = Point.fromLngLat(2.0, 2.0)
)
.profileDriving()
.baseUrl(mockWebServerRule.baseUrl)
} as RequestRoutesResult.Successful
}

val routeOptions = routeRequest.routes.first().routeOptions
assertEquals(
listOf(
Point.fromLngLat(1.0, 1.0),
Point.fromLngLat(2.0, 2.0),
),
routeOptions.coordinatesList()
)
}

override fun setupMockLocation(): Location {
return mockLocationUpdatesRule.generateLocationUpdate {
longitude = 1.0
latitude = 1.0
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ fun runOnMainSync(runnable: Runnable) =
/**
* Runs the code block on the app's main thread and blocks until the block returns.
*/
fun runOnMainSync(fn: () -> Unit) =
InstrumentationRegistry.getInstrumentation().runOnMainSync(fn)
fun <T> runOnMainSync(fn: () -> T): T {
var result: T? = null
InstrumentationRegistry.getInstrumentation().runOnMainSync {
result = fn()
}
return result ?: error("got no result")
}

fun Int.loopFor(millis: Long) {
Espresso.onView(ViewMatchers.withId(this)).perform(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ import com.mapbox.navigation.core.routealternatives.RouteAlternativesError
import com.mapbox.navigation.core.routealternatives.RouteAlternativesObserver
import com.mapbox.navigation.core.routealternatives.RouteAlternativesRequestCallback
import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdater
import com.mapbox.navigation.core.routeoptions.builder.LocationFromTripSessionProvider
import com.mapbox.navigation.core.routeoptions.builder.NavRouteOptionsBuilder
import com.mapbox.navigation.core.routeoptions.builder.NoWaypointsOptionsBuilder
import com.mapbox.navigation.core.routeoptions.builder.RouteOptionsBuilderWithWaypoints
import com.mapbox.navigation.core.routerefresh.RouteRefreshController
import com.mapbox.navigation.core.routerefresh.RouteRefreshControllerProvider
import com.mapbox.navigation.core.telemetry.MapboxNavigationTelemetry
Expand Down Expand Up @@ -120,8 +124,10 @@ import com.mapbox.navigator.TilesConfig
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import java.lang.reflect.Field
import java.util.Locale
import kotlin.coroutines.resume

private const val MAPBOX_NAVIGATION_USER_AGENT_BASE = "mapbox-navigation-android"
private const val MAPBOX_NAVIGATION_TOKEN_EXCEPTION_ROUTER =
Expand Down Expand Up @@ -709,6 +715,61 @@ class MapboxNavigation @VisibleForTesting internal constructor(
return directionsSession.requestRoutes(routeOptions, callback)
}

@ExperimentalPreviewMapboxNavigationAPI
suspend fun requestRoutes(
optionsBlock: (NoWaypointsOptionsBuilder) -> RouteOptionsBuilderWithWaypoints
): RequestRoutesResult {
if (tripSession.getState() != TripSessionState.STARTED) {
error("trip session should be started")
}
val builder = NavRouteOptionsBuilder(LocationFromTripSessionProvider(tripSession))
optionsBlock(builder)
builder.applyLanguageAndVoiceUnitOptions(navigationOptions.applicationContext)
val routeOptions = builder.build()
return suspendCancellableCoroutine { continuation ->
val requestId = requestRoutes(
routeOptions,
object : NavigationRouterCallback {
override fun onRoutesReady(
routes: List<NavigationRoute>,
routerOrigin: RouterOrigin
) {
continuation.resume(
RequestRoutesResult.Successful(
routes,
routerOrigin
)
)
}

override fun onFailure(
reasons: List<RouterFailure>,
routeOptions: RouteOptions
) {
continuation.resume(
RequestRoutesResult.Failed(
reasons,
routeOptions
)
)
}

override fun onCanceled(
routeOptions: RouteOptions,
routerOrigin: RouterOrigin
) {
if (!continuation.isCancelled) {
error("request was unexpectedly cancelled from outside")
}
}
}
)
continuation.invokeOnCancellation {
cancelRouteRequest(requestId)
}
}
}

/**
* Cancels a specific route request using the ID returned by [requestRoutes].
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.mapbox.navigation.core

import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.base.route.RouterFailure
import com.mapbox.navigation.base.route.RouterOrigin

@ExperimentalPreviewMapboxNavigationAPI
sealed class RequestRoutesResult {
@ExperimentalPreviewMapboxNavigationAPI
data class Successful(
val routes: List<NavigationRoute>,
val routerOrigin: RouterOrigin,
) : RequestRoutesResult()

@ExperimentalPreviewMapboxNavigationAPI
data class Failed(
val reasons: List<RouterFailure>,
val routeOptions: RouteOptions
) : RequestRoutesResult()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.mapbox.navigation.core.routeoptions.builder

import android.location.Location
import com.mapbox.geojson.Point
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
import com.mapbox.navigation.core.trip.session.LocationObserver
import com.mapbox.navigation.core.trip.session.TripSessionLocationProvider
import com.mapbox.navigation.utils.internal.toPoint
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import kotlin.coroutines.resume

internal interface LocationProvider {
suspend fun getCurrentLocation(): CurrentLocation
}

internal data class CurrentLocation(
val point: Point,
val bearing: Double?,
val zLevel: Int?
)

internal class LocationFromTripSessionProvider(
private val tripSessionLocationProvider: TripSessionLocationProvider
) : LocationProvider {
override suspend fun getCurrentLocation(): CurrentLocation {
val currentLocation = tripSessionLocationProvider.locationMatcherResult
return currentLocation?.toCurrentLocation() ?: waitForTheFirstLocationEventWithTimeout()
}

private suspend fun waitForTheFirstLocationEventWithTimeout() =
withTimeout(GETTING_LOCATION_TIMEOUT_MILLISECONDS) {
waitForTheFirstLocationEvent()
}

private suspend fun waitForTheFirstLocationEvent(): CurrentLocation {
val (result, cleanup) = suspendCancellableCoroutine<Pair<CurrentLocation, () -> Unit>>
{ continuation ->
val observer = object : LocationObserver {
override fun onNewRawLocation(rawLocation: Location) {
}

override fun onNewLocationMatcherResult(
locationMatcherResult: LocationMatcherResult
) {
continuation.resume(
Pair(
locationMatcherResult.toCurrentLocation(),
{
tripSessionLocationProvider.unregisterLocationObserver(this)
}
)
)
}
}
tripSessionLocationProvider.registerLocationObserver(observer)
continuation.invokeOnCancellation {
tripSessionLocationProvider.unregisterLocationObserver(observer)
}
}
cleanup()
return result
}

private fun LocationMatcherResult.toCurrentLocation() = CurrentLocation(
point = enhancedLocation.toPoint(),
bearing = enhancedLocation.bearing.toDouble(),
zLevel = zLevel
)

private companion object {
private const val GETTING_LOCATION_TIMEOUT_MILLISECONDS = 30_000L
}
}
Loading

0 comments on commit 5db5746

Please sign in to comment.