From 27eb97778b2efa86d1fb10505372919a8ad59889 Mon Sep 17 00:00:00 2001 From: VysotskiVadim Date: Thu, 27 Jan 2022 17:09:16 +0100 Subject: [PATCH] Added new experimental API for requesting a route --- CHANGELOG.md | 1 + examples/build.gradle | 1 + .../examples/core/MapboxNavigationActivity.kt | 63 ++- .../core/RouteOptionsBuilderTest.kt | 62 +++ .../utils/ViewTestUtils.kt | 9 +- .../navigation/core/MapboxNavigation.kt | 15 + .../routeoptions/builder/LocationProvider.kt | 66 +++ .../builder/NavRouteOptionsBuilder.kt | 362 ++++++++++++++ .../core/trip/session/TripSession.kt | 10 +- .../session/TripSessionLocationProvider.kt | 14 + .../core/infra/factories/Android.kt | 13 +- .../navigation/core/infra/factories/Core.kt | 48 ++ .../LocationFromTripSessionProviderTest.kt | 126 +++++ .../NavRouteOptionsBuilderJavaTest.java | 50 ++ .../builder/NavRouteOptionsBuilderTest.kt | 463 ++++++++++++++++++ 15 files changed, 1257 insertions(+), 46 deletions(-) create mode 100644 instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteOptionsBuilderTest.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/LocationProvider.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilder.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/TripSessionLocationProvider.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/infra/factories/Core.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/LocationFromTripSessionProviderTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilderJavaTest.java create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilderTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index de1d3fa7e2d..fbd1801fc68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Mapbox welcomes participation and contributions from everyone. ## Unreleased - Add `MapboxNavigationApp.isSetup` to ensure views do not reset `MapboxNavigation`. Add `MapboxNavigationApp.getObserver` to be able to access registered observers. [#5358](https://github.com/mapbox/mapbox-navigation-android/pull/5358) +- Added `MapboxNavigation:buildRouteOptions` that is an experimentation API to safely request a route. [#5427](https://github.com/mapbox/mapbox-navigation-android/pull/5427) #### Features diff --git a/examples/build.gradle b/examples/build.gradle index 5737afdd12c..b3bf6d551e1 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -107,6 +107,7 @@ dependencies { //Coroutines implementation dependenciesList.coroutinesAndroid + implementation dependenciesList.androidXLifecycleRuntime // Support libraries implementation dependenciesList.androidXCore diff --git a/examples/src/main/java/com/mapbox/navigation/examples/core/MapboxNavigationActivity.kt b/examples/src/main/java/com/mapbox/navigation/examples/core/MapboxNavigationActivity.kt index e03a5807b69..6ecc356e72d 100644 --- a/examples/src/main/java/com/mapbox/navigation/examples/core/MapboxNavigationActivity.kt +++ b/examples/src/main/java/com/mapbox/navigation/examples/core/MapboxNavigationActivity.kt @@ -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.DirectionsRoute import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.base.common.logger.model.Message @@ -23,9 +24,8 @@ 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 @@ -468,38 +468,37 @@ 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, - routerOrigin: RouterOrigin - ) { - setRouteAndStartNavigation(routes.first()) - } - - override fun onFailure( - reasons: List, - routeOptions: RouteOptions - ) { - // no impl - } - - override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) { - // no impl - } + lifecycleScope.launchWhenCreated { + val routeOptions = mapboxNavigation.buildRouteOptions { builder -> + builder + .fromCurrentLocation() + .toDestination(destination) } - ) + mapboxNavigation.requestRoutes( + routeOptions, + object : RouterCallback { + override fun onRoutesReady( + routes: List, + routerOrigin: RouterOrigin + ) { + setRouteAndStartNavigation(routes.first()) + } + + override fun onFailure( + reasons: List, + routeOptions: RouteOptions + ) { + // no impl + } + + override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) { + // no impl + } + } + ) + } } private fun setRouteAndStartNavigation(route: DirectionsRoute) { diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteOptionsBuilderTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteOptionsBuilderTest.kt new file mode 100644 index 00000000000..858734671b6 --- /dev/null +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteOptionsBuilderTest.kt @@ -0,0 +1,62 @@ +package com.mapbox.navigation.instrumentation_tests.core + +import android.location.Location +import androidx.test.platform.app.InstrumentationRegistry +import com.mapbox.api.directions.v5.models.RouteOptions +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.instrumentation_tests.activity.EmptyTestActivity +import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule +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::class.java){ + + @get:Rule + val mapboxNavigationRule = MapboxNavigationRule() + + @Test + fun navigateFromCurrentLocation() { + val mapboxNavigation = runOnMainSync { + val context = InstrumentationRegistry.getInstrumentation().getTargetContext() + MapboxNavigationProvider.create( + NavigationOptions.Builder(context) + .accessToken(getMapboxAccessTokenFromResources(context)) + .build() + ) + } + val routeOptions: RouteOptions = runBlocking(Dispatchers.Main) { + mapboxNavigation.buildRouteOptions { builder -> + builder + .fromCurrentLocation() + .toDestination( + coordinate = Point.fromLngLat(2.0, 2.0) + ) + } + } + + 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 + } + } +} \ No newline at end of file diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/ViewTestUtils.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/ViewTestUtils.kt index 667bff9a0df..c1240e4fde5 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/ViewTestUtils.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/ViewTestUtils.kt @@ -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 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( diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt index 4aade5c89fb..d7149425a44 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt @@ -56,6 +56,10 @@ import com.mapbox.navigation.core.routealternatives.RouteAlternativesControllerP 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 @@ -597,6 +601,17 @@ class MapboxNavigation @VisibleForTesting internal constructor( return directionsSession.requestRoutes(routeOptions, routesRequestCallback) } + @ExperimentalPreviewMapboxNavigationAPI + suspend fun buildRouteOptions( + optionsBlock: (NoWaypointsOptionsBuilder) -> RouteOptionsBuilderWithWaypoints + ): RouteOptions { + tripSession.start(false) + val builder = NavRouteOptionsBuilder(LocationFromTripSessionProvider(tripSession)) + optionsBlock(builder) + builder.applyLanguageAndVoiceUnitOptions(navigationOptions.applicationContext) + return builder.build() + } + /** * Cancels a specific route request using the ID returned by [requestRoutes]. */ diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/LocationProvider.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/LocationProvider.kt new file mode 100644 index 00000000000..ed72314ad2e --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/LocationProvider.kt @@ -0,0 +1,66 @@ +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.yield +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() ?: waitForTheFirstLocationEvent() + } + + private suspend fun waitForTheFirstLocationEvent(): CurrentLocation { + val (result, cleanup) = suspendCancellableCoroutine 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) + } + } + yield() // you can't remove listener in the listener's callback + cleanup() + return result + } + + private fun LocationMatcherResult.toCurrentLocation() = CurrentLocation( + point = enhancedLocation.toPoint(), + bearing = enhancedLocation.bearing.toDouble(), + zLevel = zLevel + ) +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilder.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilder.kt new file mode 100644 index 00000000000..58e6e438a32 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilder.kt @@ -0,0 +1,362 @@ +package com.mapbox.navigation.core.routeoptions.builder + +import android.content.Context +import com.mapbox.api.directions.v5.DirectionsCriteria +import com.mapbox.api.directions.v5.models.Bearing +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.geojson.Point +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions +import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions + +@ExperimentalPreviewMapboxNavigationAPI +interface NoWaypointsOptionsBuilder { + fun fromStartLocation( + point: Point, + bearing: Double? = null, + zLevel: Int? + ): WaypointsInProgressBuilder + + fun fromCurrentLocation(): WaypointsInProgressBuilder +} + +@ExperimentalPreviewMapboxNavigationAPI +interface WaypointsInProgressBuilder { + fun addIntermediateWaypoint( + coordinate: Point, + name: String? = null, + bearing: Double? = null, + zLevel: Int? = null, + targetCoordinate: Point? = null + ): WaypointsInProgressBuilder + + fun addIntermediateSilentWaypoint( + coordinate: Point, + bearing: Double? = null, + zLevel: Int? = null, + ): WaypointsInProgressBuilder + + fun toDestination( + coordinate: Point, + name: String? = null, + bearing: Double? = null, + zLevel: Int? = null, + targetCoordinate: Point? = null + ): RouteOptionsBuilderWithWaypoints +} + +@ExperimentalPreviewMapboxNavigationAPI +interface RouteOptionsBuilderWithWaypoints { + fun profileDriving( + drivingSpecificSetup: DrivingSpecificSetup.() -> Unit = { } + ): RouteOptionsBuilderWithWaypoints + + fun profileDrivingTraffic( + drivingSpecificSetup: DrivingSpecificSetup.() -> Unit = { } + ): RouteOptionsBuilderWithWaypoints + + fun profileWalking( + walkingSpecificSetup: WalkingSpecificSetup.() -> Unit = { } + ): RouteOptionsBuilderWithWaypoints + + fun profileCycling( + cyclingSpecificSetup: CyclingSpecificSetup.() -> Unit = { } + ): RouteOptionsBuilderWithWaypoints +} + +@ExperimentalPreviewMapboxNavigationAPI +internal class NavRouteOptionsBuilder internal constructor( + private val locationProvider: LocationProvider +) : NoWaypointsOptionsBuilder, WaypointsInProgressBuilder, RouteOptionsBuilderWithWaypoints { + + private lateinit var destination: Waypoint + private val builder = RouteOptions.builder() + private var locationFrom: CurrentLocation? = null + private val intermediateWaypoints = mutableListOf() + private var profile: String? = null + + override fun fromCurrentLocation(): WaypointsInProgressBuilder { + return this + } + + override fun toDestination( + coordinate: Point, + name: String?, + bearing: Double?, + zLevel: Int?, + targetCoordinate: Point?, + ): RouteOptionsBuilderWithWaypoints { + destination = Waypoint( + coordinate = coordinate, + name = name, + bearing = bearing, + zLevel = zLevel, + targetCoordinate = targetCoordinate, + ) + return this + } + + internal fun applyLanguageAndVoiceUnitOptions(context: Context): NavRouteOptionsBuilder { + builder.applyLanguageAndVoiceUnitOptions(context) + return this + } + + internal suspend fun build(): RouteOptions { + val locationFrom = (locationFrom ?: locationProvider.getCurrentLocation()).let { + Waypoint( + coordinate = it.point, + name = null, + bearing = it.bearing, + zLevel = it.zLevel, + isSilent = false, + targetCoordinate = null, + ) + } + val allWaypoints = listOf(locationFrom) + intermediateWaypoints + listOf(destination) + val notSilentWaypoints = listOf(locationFrom) + + intermediateWaypoints.filter { !it.isSilent } + + listOf(destination) + return builder + .applyDefaultNavigationOptions(profile ?: DirectionsCriteria.PROFILE_DRIVING_TRAFFIC) + .bearingsList( + allWaypoints.map { + if (it.bearing != null) { + Bearing.builder() + .angle(it.bearing) + .build() + } else null + } + ) + .layersList(allWaypoints.map { it.zLevel }) + .coordinatesList(allWaypoints.map { it.coordinate }) + .waypointNamesList(notSilentWaypoints.map { it.name }) + .waypointIndicesList( + allWaypoints.mapIndexedNotNull { index, waypoint -> + if (waypoint.isSilent) { + null + } else index + } + ) + .waypointTargetsList(notSilentWaypoints.map { it.targetCoordinate }) + .build() + } + + override fun fromStartLocation( + point: Point, + bearing: Double?, + zLevel: Int? + ): WaypointsInProgressBuilder { + locationFrom = CurrentLocation( + point, + bearing, + zLevel + ) + return this + } + + override fun addIntermediateWaypoint( + coordinate: Point, + name: String?, + bearing: Double?, + zLevel: Int?, + targetCoordinate: Point?, + ): WaypointsInProgressBuilder { + intermediateWaypoints.add( + Waypoint( + coordinate = coordinate, + name = name, + bearing = bearing, + zLevel = zLevel, + targetCoordinate = targetCoordinate + ) + ) + return this + } + + override fun addIntermediateSilentWaypoint( + coordinate: Point, + bearing: Double?, + zLevel: Int?, + ): WaypointsInProgressBuilder { + intermediateWaypoints.add( + Waypoint( + coordinate = coordinate, + bearing = bearing, + zLevel = zLevel, + name = null, + isSilent = true, + targetCoordinate = null + ) + ) + return this + } + + override fun profileDriving( + drivingSpecificSetup: DrivingSpecificSetup.() -> Unit + ): RouteOptionsBuilderWithWaypoints { + profile = DirectionsCriteria.PROFILE_DRIVING + DrivingSpecificSetup(builder).drivingSpecificSetup() + return this + } + + override fun profileDrivingTraffic( + drivingSpecificSetup: DrivingSpecificSetup.() -> Unit + ): RouteOptionsBuilderWithWaypoints { + profile = DirectionsCriteria.PROFILE_DRIVING_TRAFFIC + DrivingSpecificSetup(builder).drivingSpecificSetup() + return this + } + + override fun profileWalking( + walkingSpecificSetup: WalkingSpecificSetup.() -> Unit + ): RouteOptionsBuilderWithWaypoints { + profile = DirectionsCriteria.PROFILE_WALKING + WalkingSpecificSetup(builder).walkingSpecificSetup() + return this + } + + override fun profileCycling( + cyclingSpecificSetup: CyclingSpecificSetup.() -> Unit + ): RouteOptionsBuilderWithWaypoints { + profile = DirectionsCriteria.PROFILE_CYCLING + CyclingSpecificSetup(builder).cyclingSpecificSetup() + return this + } +} + +@ExperimentalPreviewMapboxNavigationAPI +class CyclingSpecificSetup internal constructor( + private val routeOptionsBuilder: RouteOptions.Builder +) { + fun exclude(cyclingSpecificExcludeSetup: CyclingSpecificExclude.() -> Unit) { + val excludes = CyclingSpecificExclude().apply(cyclingSpecificExcludeSetup).excludeList + routeOptionsBuilder.excludeList(excludes) + } + + class CyclingSpecificExclude { + + internal val excludeList = mutableListOf() + + fun ferry() { + excludeList.add(DirectionsCriteria.EXCLUDE_FERRY) + } + + fun cashOnlyTolls() { + excludeList.add(DirectionsCriteria.EXCLUDE_CASH_ONLY_TOLLS) + } + } +} + +@ExperimentalPreviewMapboxNavigationAPI +class WalkingSpecificSetup internal constructor( + private val routeOptionsBuilder: RouteOptions.Builder +) { + + fun exclude(walkingSpecificExcludeSetup: WalingSpecificExclude.() -> Unit) { + val excludes = WalingSpecificExclude().apply(walkingSpecificExcludeSetup).excludeList() + routeOptionsBuilder.excludeList(excludes) + } + + fun walkwayBias(bias: DirectionBias) { + routeOptionsBuilder.walkwayBias(bias.rawValue) + } + + class WalingSpecificExclude { + + private val excludeList = mutableListOf() + + fun cashOnlyTolls() { + excludeList.add(DirectionsCriteria.EXCLUDE_CASH_ONLY_TOLLS) + } + + fun excludeList() = excludeList + } +} + +@JvmInline +@ExperimentalPreviewMapboxNavigationAPI +value class DirectionBias(val rawValue: Double) { + companion object { + val low = DirectionBias(-1.0) + val medium = DirectionBias(0.0) + val high = DirectionBias(1.0) + } +} + +@ExperimentalPreviewMapboxNavigationAPI +class DrivingSpecificSetup internal constructor( + private val routeOptionsBuilder: RouteOptions.Builder +) { + fun exclude(block: DrivingSpecificExclude.() -> Unit) { + val excludeList = DrivingSpecificExclude().apply(block).excludeList() + routeOptionsBuilder.excludeList(excludeList) + } + + fun include(block: DrivingSpecificInclude.() -> Unit) { + val includeList = DrivingSpecificInclude().apply(block).includeList + routeOptionsBuilder.includeList(includeList) + } + + fun maxHeight(maxVehicleHeight: Double) { + routeOptionsBuilder.maxHeight(maxVehicleHeight) + } + + fun maxWidth(maxVehicleWidth: Double) { + routeOptionsBuilder.maxWidth(maxVehicleWidth) + } + + class DrivingSpecificInclude { + + internal val includeList = mutableListOf() + + fun hov3() { + includeList.add(DirectionsCriteria.INCLUDE_HOV3) + } + + fun hov2() { + includeList.add(DirectionsCriteria.INCLUDE_HOV2) + } + + fun hot() { + includeList.add(DirectionsCriteria.INCLUDE_HOT) + } + } + + class DrivingSpecificExclude { + + private val excludeList = mutableListOf() + + fun toll() { + excludeList.add(DirectionsCriteria.EXCLUDE_TOLL) + } + + fun unpaved() { + excludeList.add(DirectionsCriteria.EXCLUDE_UNPAVED) + } + + fun ferry() { + excludeList.add(DirectionsCriteria.EXCLUDE_FERRY) + } + + fun motorway() { + excludeList.add(DirectionsCriteria.EXCLUDE_MOTORWAY) + } + + fun cashOnlyTolls() { + excludeList.add(DirectionsCriteria.EXCLUDE_CASH_ONLY_TOLLS) + } + + internal fun excludeList(): List? { + return excludeList + } + } +} + +private data class Waypoint( + val coordinate: Point, + val name: String?, + val bearing: Double?, + val zLevel: Int?, + val isSilent: Boolean = false, + val targetCoordinate: Point? +) diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/TripSession.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/TripSession.kt index 5deccbb6a1c..a9100b299b6 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/TripSession.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/TripSession.kt @@ -1,6 +1,5 @@ package com.mapbox.navigation.core.trip.session -import android.location.Location import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.core.directions.session.RoutesExtra @@ -9,7 +8,7 @@ import com.mapbox.navigation.core.trip.service.TripService import com.mapbox.navigation.core.trip.session.eh.EHorizonObserver import com.mapbox.navigator.FallbackVersionsObserver -internal interface TripSession { +internal interface TripSession : TripSessionLocationProvider { val tripService: TripService fun setRoutes( @@ -18,9 +17,6 @@ internal interface TripSession { @RoutesExtra.RoutesUpdateReason reason: String ) - fun getRawLocation(): Location? - val zLevel: Int? - val locationMatcherResult: LocationMatcherResult? fun getRouteProgress(): RouteProgress? fun getState(): TripSessionState @@ -28,10 +24,6 @@ internal interface TripSession { fun stop() fun isRunningWithForegroundService(): Boolean - fun registerLocationObserver(locationObserver: LocationObserver) - fun unregisterLocationObserver(locationObserver: LocationObserver) - fun unregisterAllLocationObservers() - fun registerRouteProgressObserver(routeProgressObserver: RouteProgressObserver) fun unregisterRouteProgressObserver(routeProgressObserver: RouteProgressObserver) fun unregisterAllRouteProgressObservers() diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/TripSessionLocationProvider.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/TripSessionLocationProvider.kt new file mode 100644 index 00000000000..b3a5cf45dbc --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/TripSessionLocationProvider.kt @@ -0,0 +1,14 @@ +package com.mapbox.navigation.core.trip.session + +import android.location.Location + +internal interface TripSessionLocationProvider { + + fun getRawLocation(): Location? + val zLevel: Int? + val locationMatcherResult: LocationMatcherResult? + + fun registerLocationObserver(locationObserver: LocationObserver) + fun unregisterLocationObserver(locationObserver: LocationObserver) + fun unregisterAllLocationObservers() +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/infra/factories/Android.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/infra/factories/Android.kt index 0dbc205bdd5..28edcaea44e 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/infra/factories/Android.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/infra/factories/Android.kt @@ -1,8 +1,15 @@ package com.mapbox.navigation.core.infra.factories import android.location.Location +import io.mockk.every +import io.mockk.mockk -fun createLocation(longitude: Double = 0.0, latitude: Double = 0.0) = Location("").apply { - setLatitude(latitude) - setLongitude(longitude) +fun createLocation( + longitude: Double = 0.0, + latitude: Double = 0.0, + bearing: Double = 0.0, +) = mockk().apply { + every { this@apply.latitude } returns latitude + every { this@apply.longitude } returns longitude + every { this@apply.bearing } returns bearing.toFloat() } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/infra/factories/Core.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/infra/factories/Core.kt new file mode 100644 index 00000000000..f89c8d6f205 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/infra/factories/Core.kt @@ -0,0 +1,48 @@ +package com.mapbox.navigation.core.infra.factories + +import android.location.Location +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.internal.factory.RoadFactory +import com.mapbox.navigation.base.road.model.Road +import com.mapbox.navigation.base.speed.model.SpeedLimit +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import com.mapbox.navigator.Shield + +fun createLocationMatcherResult( + enhancedLocation: Location = createLocation(), + keyPoints: List = emptyList(), + isOffRoad: Boolean = false, + offRoadProbability: Float = 0.0f, + isTeleport: Boolean = false, + speedLimit: SpeedLimit? = null, + roadEdgeMatchProbability: Float = 1.0f, + zLevel: Int? = 1, + road: Road = createRoad(), +) = LocationMatcherResult( + enhancedLocation = enhancedLocation, + keyPoints = keyPoints, + isOffRoad = isOffRoad, + offRoadProbability = offRoadProbability, + isTeleport = isTeleport, + speedLimit = speedLimit, + roadEdgeMatchProbability = roadEdgeMatchProbability, + zLevel = zLevel, + road = road, +) + +@OptIn(ExperimentalMapboxNavigationAPI::class) +fun createRoad( + text: String = "test", + imageBaseUlr: String? = null, + shield: Shield? = null +) = RoadFactory.buildRoadObject( + createNavigationStatus( + roads = listOf( + com.mapbox.navigator.Road( + text, + imageBaseUlr, + shield + ) + ) + ) +) diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/LocationFromTripSessionProviderTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/LocationFromTripSessionProviderTest.kt new file mode 100644 index 00000000000..f730166a452 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/LocationFromTripSessionProviderTest.kt @@ -0,0 +1,126 @@ +package com.mapbox.navigation.core.routeoptions.builder + +import android.location.Location +import com.mapbox.navigation.core.infra.factories.createLocation +import com.mapbox.navigation.core.infra.factories.createLocationMatcherResult +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 junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertNotSame +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import org.junit.Test + +class LocationFromTripSessionProviderTest { + + @Test + fun `current location is taken from the trip session`() = runBlocking { + val testTripSession = TestTripSession().apply { + updateMatchedLocation( + createLocationMatcherResult( + enhancedLocation = createLocation( + longitude = 1.0, + latitude = 2.0, + bearing = 3.0 + ), + zLevel = 4 + ) + ) + } + val locationProvider = createLocationFromTripSessionProvider(testTripSession) + + val currentLocation = locationProvider.getCurrentLocation() + + assertEquals(1.0, currentLocation.point.longitude()) + assertEquals(2.0, currentLocation.point.latitude()) + assertEquals(3.0, currentLocation.bearing) + assertEquals(4, currentLocation.zLevel) + } + + @Test + fun `wait for update if location isn't available yet`() = runBlocking { + val testTripSession = TestTripSession() + val locationProvider = createLocationFromTripSessionProvider(testTripSession) + + val currentLocationDeferred = async(start = CoroutineStart.UNDISPATCHED) { + locationProvider.getCurrentLocation() + } + assertFalse(currentLocationDeferred.isCompleted) + + testTripSession.updateMatchedLocation( + createLocationMatcherResult( + enhancedLocation = createLocation( + longitude = 1.0, + latitude = 2.0, + bearing = 3.0 + ), + zLevel = 4 + ) + ) + val currentLocation = currentLocationDeferred.await() + assertEquals(1.0, currentLocation.point.longitude()) + assertEquals(2.0, currentLocation.point.latitude()) + assertEquals(3.0, currentLocation.bearing) + assertEquals(4, currentLocation.zLevel) + testTripSession.verifyNoActiveSubscriptions() + } + + @Test + fun `cancel getting current location`() = runBlocking { + val testTripSession = TestTripSession() + val locationProvider = createLocationFromTripSessionProvider(testTripSession) + val getLocationTask = async(start = CoroutineStart.UNDISPATCHED) { + locationProvider.getCurrentLocation() + } + testTripSession.verifySomeActiveSubscriptions() + + getLocationTask.cancel() + + testTripSession.verifyNoActiveSubscriptions() + } +} + +private fun createLocationFromTripSessionProvider( + tripSessionLocationProvider: TripSessionLocationProvider = TestTripSession() +) = LocationFromTripSessionProvider( + tripSessionLocationProvider = tripSessionLocationProvider +) + +class TestTripSession : TripSessionLocationProvider { + + private val locationObservers = mutableListOf() + + override fun getRawLocation(): Location = TODO("implement if you need it") + override val zLevel: Int? get() = locationMatcherResult?.zLevel + override var locationMatcherResult: LocationMatcherResult? = null + + override fun registerLocationObserver(locationObserver: LocationObserver) { + locationObservers.add(locationObserver) + } + + override fun unregisterLocationObserver(locationObserver: LocationObserver) { + locationObservers.remove(locationObserver) + } + + override fun unregisterAllLocationObservers() { + locationObservers.clear() + } + + fun updateMatchedLocation( + locationMatcherResult: LocationMatcherResult, + ) { + this.locationMatcherResult = locationMatcherResult + locationObservers.forEach { it.onNewLocationMatcherResult(locationMatcherResult) } + } + + fun verifySomeActiveSubscriptions() { + assertNotSame(0, locationObservers.size) + } + + fun verifyNoActiveSubscriptions() { + assertEquals(0, locationObservers.size) + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilderJavaTest.java b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilderJavaTest.java new file mode 100644 index 00000000000..f6b72ebf631 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilderJavaTest.java @@ -0,0 +1,50 @@ +package com.mapbox.navigation.core.routeoptions.builder; + +import com.mapbox.api.directions.v5.DirectionsCriteria; +import com.mapbox.api.directions.v5.models.RouteOptions; +import com.mapbox.geojson.Point; +import kotlin.Unit; +import org.junit.Test; + +import java.util.ArrayList; + +import static com.mapbox.navigation.core.routeoptions.builder.NavRouteOptionsBuilderTestKt.testRouteOptionsSetup; +import static org.junit.Assert.assertEquals; + +public class NavRouteOptionsBuilderJavaTest { + @Test + public void useOptionsBuilderFromJava() { + RouteOptions options = testRouteOptionsSetup((builder) -> builder + .fromCurrentLocation() + .addIntermediateWaypoint( + Point.fromLngLat(3.0, 3.0), + "test name", + null, + null, + null + ) + .toDestination( + Point.fromLngLat(4.0, 4.0), + null, + null, + null, + null + ) + .profileDrivingTraffic((drivingSpecificSetup) -> { + drivingSpecificSetup.exclude((drivingSpecificExclude) -> { + drivingSpecificExclude.cashOnlyTolls(); + return Unit.INSTANCE; + }); + return Unit.INSTANCE; + }) + ); + + assertEquals(3, options.coordinatesList().size()); + assertEquals( + new ArrayList() {{ + add(DirectionsCriteria.EXCLUDE_CASH_ONLY_TOLLS); + }}, + options.excludeList() + ); + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilderTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilderTest.kt new file mode 100644 index 00000000000..a3eb72fb7d8 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilderTest.kt @@ -0,0 +1,463 @@ +@file:OptIn(ExperimentalPreviewMapboxNavigationAPI::class) + +package com.mapbox.navigation.core.routeoptions.builder + +import com.mapbox.api.directions.v5.DirectionsCriteria +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.geojson.Point +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class NavRouteOptionsBuilderTest { + + @Test + fun `route from current location to destination contains all coordinates`() { + val currentLocationProvider = TestLocationProvider().apply { + currentLocation = Point.fromLngLat(3.0, 3.0) + } + + val options = testRouteOptionsSetup(currentLocationProvider) { builder -> + builder + .fromCurrentLocation() + .toDestination(Point.fromLngLat(1.0, 1.0)) + } + + assertEquals( + listOf( + Point.fromLngLat(3.0, 3.0), + Point.fromLngLat(1.0, 1.0), + ), + options.coordinatesList(), + ) + } + + @Test + fun `minimal route options contains all default parameters`() { + val options = testRouteOptionsSetup { + it.fromCurrentLocationToTestDestination() + } + assertTrue(options.steps() == true) + // TODO: check other fields? + } + + @Test + fun `current position contains bearing and z level`() { + val locationProvider = TestLocationProvider().apply { + currentBearing = 6.8 + currentZLevel = 4 + } + + val options = testRouteOptionsSetup(locationProvider) { + it.fromCurrentLocationToTestDestination() + } + + assertEquals(2, options.bearingsList()?.size) + assertEquals(6.8, options.bearingsList()!![0].angle(), 0.01) + assertEquals(4, options.layersList()!![0]) + } + + @Test + fun `specify properties for the destination waypoint`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocation() + .toDestination( + coordinate = Point.fromLngLat(1.0, 1.0), + name = "testName", + bearing = 45.0, + zLevel = 6 + ) + } + + assertEquals("testName", options.waypointNamesList()?.get(1)) + assertEquals( + 45.0, + options.bearingsList()!![1].angle(), + 0.01 + ) + assertEquals( + 6, + options.layersList()!![1] + ) + } + + @Test + fun `specify drop off location for an intermediate waypoint`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocation() + .addIntermediateWaypoint( + coordinate = Point.fromLngLat(6.0, 6.0), + targetCoordinate = Point.fromLngLat(7.0, 7.0), + ) + .applyTestDestination() + } + assertEquals( + listOf(null, Point.fromLngLat(7.0, 7.0), null), + options.waypointTargetsList() + ) + } + + @Test + fun `specify drop off location for destination when silent waypoint is present`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocation() + .addIntermediateSilentWaypoint( + coordinate = Point.fromLngLat(3.0, 3.0) + ) + .toDestination( + coordinate = Point.fromLngLat(5.0, 5.0), + targetCoordinate = Point.fromLngLat(6.0, 6.0) + ) + } + assertEquals( + listOf(null, Point.fromLngLat(6.0, 6.0)), + options.waypointTargetsList() + ) + } + + @Test + fun `specify custom start point, bearing and zLevel`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromStartLocation( + point = Point.fromLngLat(7.0, 7.0), + bearing = 90.0, + zLevel = 1 + ) + .toDestination( + coordinate = Point.fromLngLat(1.0, 1.0) + ) + } + + assertEquals( + Point.fromLngLat(7.0, 7.0), + options.coordinatesList()[0] + ) + assertEquals( + 1, + options.layersList()?.get(0) + ) + assertEquals( + 90.0, + options.bearingsList()!![0].angle(), + 0.01 + ) + } + + @Test + fun `add intermediate waypoint`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocation() + .addIntermediateWaypoint( + coordinate = Point.fromLngLat(7.0, 7.0) + ) + .applyTestDestination() + } + assertEquals(3, options.coordinatesList().size) + assertEquals( + Point.fromLngLat(7.0, 7.0), + options.coordinatesList()[1] + ) + } + + @Test + fun `add named intermediate waypoint`() { + val options = testRouteOptionsSetup { builder -> + builder.fromCurrentLocation() + .addIntermediateWaypoint( + coordinate = Point.fromLngLat(1.0, 1.0), + name = "test name", + bearing = 45.0, + zLevel = 6 + ) + .applyTestDestination() + } + assertEquals( + "test name", + options.waypointNamesList()?.get(1) + ) + assertEquals( + 45.0, + options.bearingsList()!![1].angle(), + 0.01 + ) + assertEquals( + 6, + options.layersList()!![1] + ) + } + + @Test + fun `add silent waypoint`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocation() + .addIntermediateSilentWaypoint( + coordinate = Point.fromLngLat(1.0, 1.0), + bearing = 45.0, + zLevel = 6 + ) + .applyTestDestination() + } + + assertEquals(3, options.coordinatesList().size) + assertEquals( + Point.fromLngLat(1.0, 1.0), + options.coordinatesList()[1] + ) + assertEquals( + 45.0, + options.bearingsList()!![1].angle(), + 0.01 + ) + assertEquals( + 6, + options.layersList()!![1] + ) + assertEquals( + listOf(0, 2), + options.waypointIndicesList() + ) + } + + @Test + fun `mix silent and named waypoints`() { + val options = testRouteOptionsSetup { builder -> + builder.fromCurrentLocation() + .addIntermediateSilentWaypoint( + coordinate = Point.fromLngLat(1.0, 1.0) + ) + .addIntermediateWaypoint( + coordinate = Point.fromLngLat(2.0, 2.0), + name = "test" + ) + .applyTestDestination() + } + + assertEquals( + listOf(null, "test", null), + options.waypointNamesList() + ) + } + + @Test + fun `driving profile with excluded toll and unpaved`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocation() + .applyTestDestination() + .profileDriving { + exclude { + toll() + unpaved() + } + } + } + + assertEquals(DirectionsCriteria.PROFILE_DRIVING, options.profile()) + assertEquals( + listOf( + DirectionsCriteria.EXCLUDE_TOLL, + DirectionsCriteria.EXCLUDE_UNPAVED + ), + options.excludeList() + ) + } + + @Test + fun `driving-traffic profile with excluded motorway, ferry, and cash only tolls`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocation() + .applyTestDestination() + .profileDrivingTraffic { + exclude { + motorway() + ferry() + cashOnlyTolls() + } + } + } + + assertEquals(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC, options.profile()) + assertEquals( + listOf( + DirectionsCriteria.EXCLUDE_MOTORWAY, + DirectionsCriteria.EXCLUDE_FERRY, + DirectionsCriteria.EXCLUDE_CASH_ONLY_TOLLS + ), + options.excludeList() + ) + } + + @Test + fun `walking profile with excluded cash only tolls`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocationToTestDestination() + .profileWalking { + exclude { + cashOnlyTolls() + } + } + } + + assertEquals(DirectionsCriteria.PROFILE_WALKING, options.profile()) + assertEquals( + listOf(DirectionsCriteria.EXCLUDE_CASH_ONLY_TOLLS), + options.excludeList() + ) + } + + @Test + fun `cycling profile with excluded ferry`() = runBlocking { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocationToTestDestination() + .profileCycling { + exclude { + ferry() + } + } + } + + assertEquals(DirectionsCriteria.PROFILE_CYCLING, options.profile()) + assertEquals( + listOf(DirectionsCriteria.EXCLUDE_FERRY), + options.excludeList() + ) + } + + @Test + fun `cycling profile with excluded cash only tolls`() = runBlocking { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocationToTestDestination() + .profileCycling { + exclude { + cashOnlyTolls() + } + } + } + + assertEquals(DirectionsCriteria.PROFILE_CYCLING, options.profile()) + assertEquals( + listOf(DirectionsCriteria.EXCLUDE_CASH_ONLY_TOLLS), + options.excludeList() + ) + } + + @Test + fun `driving profile with hot and hov2 includes`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocationToTestDestination() + .profileDriving { + include { + hot() + hov2() + } + } + } + + assertEquals( + listOf(DirectionsCriteria.INCLUDE_HOT, DirectionsCriteria.INCLUDE_HOV2), + options.includeList() + ) + } + + @Test + fun `driving-traffic profile with hov3 include`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocationToTestDestination() + .profileDrivingTraffic { + include { + hov3() + } + } + } + + assertEquals( + listOf(DirectionsCriteria.INCLUDE_HOV3), + options.includeList() + ) + } + + @Test + fun `driving profile with max width `() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocationToTestDestination() + .profileDriving { + maxWidth(2.0) + } + } + + assertEquals(2.0, options.maxWidth()) + } + + @Test + fun `driving-traffic profile with max height `() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocationToTestDestination() + .profileDrivingTraffic { + maxHeight(2.0) + } + } + + assertEquals(2.0, options.maxHeight()) + } + + @Test + fun `avoid walkway in walking profile`() { + val options = testRouteOptionsSetup { builder -> + builder + .fromCurrentLocationToTestDestination() + .profileWalking { + walkwayBias(DirectionBias.low) + } + } + + assertEquals(-1.0, options.walkwayBias()) + } +} + +@JvmOverloads +internal fun testRouteOptionsSetup( + locationProvider: LocationProvider = TestLocationProvider(), + block: (NoWaypointsOptionsBuilder) -> RouteOptionsBuilderWithWaypoints +): RouteOptions { + val builder = NavRouteOptionsBuilder( + locationProvider + ) + block(builder) + return runBlocking { builder.build() } +} + +internal fun NoWaypointsOptionsBuilder.fromCurrentLocationToTestDestination() = this + .fromCurrentLocation() + .applyTestDestination() + +internal fun WaypointsInProgressBuilder.applyTestDestination() = + toDestination(Point.fromLngLat(1.0, 1.0)) + +private class TestLocationProvider : LocationProvider { + + var currentLocation: Point = Point.fromLngLat(0.0, 0.0) + var currentBearing: Double? = null + var currentZLevel: Int? = null + + override suspend fun getCurrentLocation() = CurrentLocation( + point = currentLocation, + bearing = currentBearing, + zLevel = currentZLevel + ) +}