diff --git a/CHANGELOG.md b/CHANGELOG.md index a73f9c4ab8a..c9a67f15815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/examples/build.gradle b/examples/build.gradle index cb912173f23..e517b8dace8 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -106,6 +106,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 7da9df58ebd..6de1e8b5c86 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,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 @@ -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 @@ -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, - 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 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 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..c72011c9bd4 --- /dev/null +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteOptionsBuilderTest.kt @@ -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::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 + } + } +} 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 b27b4a01cd1..21320b8aa90 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 @@ -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") + } + 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, + routerOrigin: RouterOrigin + ) { + continuation.resume( + RequestRoutesResult.Successful( + routes, + routerOrigin + ) + ) + } + + override fun onFailure( + reasons: List, + 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]. */ diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/RequestRoutesResult.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/RequestRoutesResult.kt new file mode 100644 index 00000000000..2366f5562d5 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/RequestRoutesResult.kt @@ -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, + val routerOrigin: RouterOrigin, + ) : RequestRoutesResult() + + @ExperimentalPreviewMapboxNavigationAPI + data class Failed( + val reasons: List, + val routeOptions: RouteOptions + ) : RequestRoutesResult() +} 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..9e117de2758 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/LocationProvider.kt @@ -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 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 + } +} 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..61478f7542e --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/builder/NavRouteOptionsBuilder.kt @@ -0,0 +1,369 @@ +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 + + fun baseUrl(baseUrl: String): 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 + } + + override fun baseUrl(baseUrl: String): RouteOptionsBuilderWithWaypoints { + builder.baseUrl(baseUrl) + 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 367f3e9f356..f86aa68b21d 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.navigation.base.route.NavigationRoute import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.core.directions.session.RoutesExtra @@ -8,7 +7,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( @@ -17,9 +16,6 @@ internal interface TripSession { @RoutesExtra.RoutesUpdateReason reason: String ) - fun getRawLocation(): Location? - val zLevel: Int? - val locationMatcherResult: LocationMatcherResult? fun getRouteProgress(): RouteProgress? fun getState(): TripSessionState @@ -27,10 +23,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() @@ -56,11 +48,9 @@ internal interface TripSession { fun registerRoadObjectsOnRouteObserver( roadObjectsOnRouteObserver: RoadObjectsOnRouteObserver ) - fun unregisterRoadObjectsOnRouteObserver( roadObjectsOnRouteObserver: RoadObjectsOnRouteObserver ) - fun unregisterAllRoadObjectsOnRouteObservers() fun registerEHorizonObserver(eHorizonObserver: EHorizonObserver) 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..055dfe1b04a 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(relaxed = true).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..2d3f2011b82 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/infra/factories/Core.kt @@ -0,0 +1,50 @@ +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(), + isDegradedMapMatching: Boolean = false, +) = LocationMatcherResult( + enhancedLocation = enhancedLocation, + keyPoints = keyPoints, + isOffRoad = isOffRoad, + offRoadProbability = offRoadProbability, + isTeleport = isTeleport, + speedLimit = speedLimit, + roadEdgeMatchProbability = roadEdgeMatchProbability, + zLevel = zLevel, + road = road, + isDegradedMapMatching = isDegradedMapMatching, +) + +@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..2be57e72e33 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/builder/LocationFromTripSessionProviderTest.kt @@ -0,0 +1,144 @@ +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.assertNotNull +import junit.framework.Assert.assertNotSame +import junit.framework.Assert.assertTrue +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runBlockingTest +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() + } + + @Test + fun `timeout if can't get current location in reasonable time`() = runBlockingTest { + val testTripSession = TestTripSession() + val locationProvider = createLocationFromTripSessionProvider(testTripSession) + + val getLocationTask = async(start = CoroutineStart.UNDISPATCHED) { + locationProvider.getCurrentLocation() + } + advanceTimeBy(31_000) + + assertTrue("get location is still in progress", getLocationTask.isCompleted) + assertNotNull(getLocationTask.getCompletionExceptionOrNull()) + 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 + ) +}