-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
navigation specific route options builder #5427
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
|
@@ -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 | ||
|
@@ -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 = | ||
|
@@ -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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't throw but rather return an error message. |
||
} | ||
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]. | ||
*/ | ||
|
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class and internal suspend fun TripSessionLocationProvider.currentLocation() = suspendCancellableCoroutine<CurrentLocation> { continuation ->
// return immediately if locationMatcherResult has already been set
locationMatcherResult?.also {
continuation.resume(it.toCurrentLocation())
return@suspendCancellableCoroutine
}
// subscribe for the next locationMatcherResult
val observer = object : LocationObserver {
override fun onNewRawLocation(rawLocation: Location) = Unit
override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) {
unregisterLocationObserver(this)
continuation.resume(locationMatcherResult.toCurrentLocation())
}
}
registerLocationObserver(observer)
continuation.invokeOnCancellation {
unregisterLocationObserver(observer)
}
}
private fun LocationMatcherResult.toCurrentLocation() = CurrentLocation(
point = enhancedLocation.toPoint(),
bearing = enhancedLocation.bearing.toDouble(),
zLevel = zLevel
) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you. I agree it can. I'm not sure I understand why an extension function is better than a wrapper class. Please share your thoughts about exception function vs wrapper class . Not sure if the provided code is expected to be fully functional but it will crash on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There is nothing wrong with wrapper classes. They are extremely useful in languages that doesn't provide any other mechanism to add new functionality to an existing class or when we need to store some state. In this specific case, we can extend TripSessionLocationProvider functionality by utilizing Kotlin extensions.
I am assuming the crash is caused by ConcurrentModificationException. We should create new ticket for patching There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I replace wrapper by an extension function then |
||
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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep it consistent with other APIs that are in the Nav SDK.