From 97a9c8d52059196a68cc08dd4af7ff40ad5f0dd6 Mon Sep 17 00:00:00 2001 From: Kyle Madsen <> Date: Thu, 23 Apr 2020 19:03:59 -0700 Subject: [PATCH] Add arrival controller for waypoints --- .../examples/core/MultipleStopsActivity.kt | 20 +- .../navigation/core/MapboxNavigation.kt | 38 ++ .../core/stops/ArrivalController.kt | 108 ++++++ .../core/stops/ArrivalProgressObserver.kt | 79 +++++ .../core/trip/session/MapboxTripSession.kt | 14 + .../core/trip/session/TripSession.kt | 2 + .../core/stops/ArrivalProgressObserverTest.kt | 327 ++++++++++++++++++ .../internal/MapboxNativeNavigator.kt | 9 +- .../internal/MapboxNativeNavigatorImpl.kt | 12 +- 9 files changed, 597 insertions(+), 12 deletions(-) create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalController.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalProgressObserver.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/stops/ArrivalProgressObserverTest.kt diff --git a/examples/src/main/java/com/mapbox/navigation/examples/core/MultipleStopsActivity.kt b/examples/src/main/java/com/mapbox/navigation/examples/core/MultipleStopsActivity.kt index 7570db88da3..13ec5c4f7d1 100644 --- a/examples/src/main/java/com/mapbox/navigation/examples/core/MultipleStopsActivity.kt +++ b/examples/src/main/java/com/mapbox/navigation/examples/core/MultipleStopsActivity.kt @@ -23,9 +23,12 @@ import com.mapbox.mapboxsdk.maps.MapboxMap import com.mapbox.mapboxsdk.maps.OnMapReadyCallback import com.mapbox.mapboxsdk.maps.Style import com.mapbox.navigation.base.internal.extensions.applyDefaultParams +import com.mapbox.navigation.base.trip.model.RouteLegProgress import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.directions.session.RoutesRequestCallback import com.mapbox.navigation.core.replay.route.ReplayRouteLocationEngine +import com.mapbox.navigation.core.stops.ArrivalController +import com.mapbox.navigation.core.stops.ArrivalOptions import com.mapbox.navigation.examples.R import com.mapbox.navigation.examples.utils.Utils import com.mapbox.navigation.examples.utils.extensions.toPoint @@ -109,7 +112,7 @@ class MultipleStopsActivity : AppCompatActivity(), OnMapReadyCallback { ) } - fun initLocationEngine() { + private fun initLocationEngine() { val requestLocationUpdateRequest = LocationEngineRequest.Builder(DEFAULT_INTERVAL_IN_MILLISECONDS) .setPriority(LocationEngineRequest.PRIORITY_NO_POWER) @@ -149,6 +152,7 @@ class MultipleStopsActivity : AppCompatActivity(), OnMapReadyCallback { @SuppressLint("MissingPermission") fun initListeners() { + mapboxNavigation?.attachArrivalController(arrivalObserver) startNavigation.setOnClickListener { navigationMapboxMap?.updateCameraTrackingMode(NavigationCamera.NAVIGATION_TRACKING_MODE_GPS) navigationMapboxMap?.updateLocationLayerRenderMode(RenderMode.GPS) @@ -161,6 +165,20 @@ class MultipleStopsActivity : AppCompatActivity(), OnMapReadyCallback { } } + private val arrivalObserver = object : ArrivalController { + val arrivalOptions = ArrivalOptions.Builder() + .arriveInSeconds(60.0) + .build() + override fun arrivalOptions(): ArrivalOptions = arrivalOptions + + override fun onStopArrival(routeLegProgress: RouteLegProgress): Boolean { + // This example shows you can use both time and distance. + // Move to the next step when the distance is small + Timber.i("arrival_debug legIndex=${routeLegProgress.legIndex()} distanceRemaining=${routeLegProgress.distanceRemaining()}") + return routeLegProgress.distanceRemaining() < 5.0 + } + } + override fun onStart() { super.onStart() mapView.onStart() 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 667dffa08cd..f83d31f846b 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 @@ -37,6 +37,9 @@ import com.mapbox.navigation.core.directions.session.RoutesRequestCallback import com.mapbox.navigation.core.fasterroute.FasterRouteController import com.mapbox.navigation.core.fasterroute.FasterRouteObserver import com.mapbox.navigation.core.routerefresh.RouteRefreshController +import com.mapbox.navigation.core.stops.ArrivalController +import com.mapbox.navigation.core.stops.ArrivalProgressObserver +import com.mapbox.navigation.core.stops.AutoArrivalController import com.mapbox.navigation.core.telemetry.MapboxNavigationTelemetry import com.mapbox.navigation.core.telemetry.events.FeedbackEvent import com.mapbox.navigation.core.trip.service.TripService @@ -141,6 +144,7 @@ constructor( private val internalOffRouteObserver = createInternalOffRouteObserver() private val fasterRouteController: FasterRouteController private val routeRefreshController: RouteRefreshController + private val arrivalProgressObserver: ArrivalProgressObserver private var notificationChannelField: Field? = null private val MAPBOX_NAVIGATION_NOTIFICATION_PACKAGE_NAME = @@ -208,6 +212,9 @@ constructor( fasterRouteController = FasterRouteController(directionsSession, tripSession, logger) routeRefreshController = RouteRefreshController(directionsSession, tripSession, logger) routeRefreshController.start() + + arrivalProgressObserver = ArrivalProgressObserver(tripSession) + attachArrivalController() } /** @@ -449,6 +456,37 @@ constructor( tripSession.unregisterStateObserver(tripSessionStateObserver) } + /** + * Attach your own controller to determine when drivers arrived at stops via [ArrivalController] + * Use [navigateNextRouteLeg] to manually move navigator to the next stop. To reset to the + * automatic arrival controller, call attachArrivalController() + * + * @param arrivalController ArrivalObserver + */ + @JvmOverloads fun attachArrivalController(arrivalController: ArrivalController = AutoArrivalController()) { + arrivalProgressObserver.attach(arrivalController) + tripSession.registerRouteProgressObserver(arrivalProgressObserver) + } + + /** + * Disable arrival at stops completely. Use this if you want to write your + * own mechanism for handling arrival at stops. + */ + fun removeArrivalController() { + tripSession.unregisterRouteProgressObserver(arrivalProgressObserver) + } + + /** + * After arriving at a stop, this can be used to manually decide when to start + * navigating to the next stop. Use the [ArrivalController] to control when to + * call navigateNextRouteLeg. + * + * @return true if navigation to next stop could be started, false otherwise + */ + fun navigateNextRouteLeg(): Boolean { + return arrivalProgressObserver.navigateNextRouteLeg() + } + /** * Start observing faster routes for a trip session via [FasterRouteObserver] * diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalController.kt new file mode 100644 index 00000000000..ae486b43fec --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalController.kt @@ -0,0 +1,108 @@ +package com.mapbox.navigation.core.stops + +import com.mapbox.navigation.base.trip.model.RouteLegProgress +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.base.trip.model.RouteProgressState +import com.mapbox.navigation.core.MapboxNavigation + +/** + * When navigating to points of interest, you may want to control the arrival + * experience. This interface gives you options to change arrival via [MapboxNavigation.attachArrivalController] + */ +interface ArrivalController { + + /** + * Override the options for your arrival callback. + */ + fun arrivalOptions(): ArrivalOptions + + /** + * Based on your [ArrivalOptions], this will be called as the next stop is approached. + * To manually navigate the next leg, return false and call [MapboxNavigation.navigateNextRouteLeg] + * + * @return true to automatically move to the next step, false to do it manually + */ + fun onStopArrival(routeLegProgress: RouteLegProgress): Boolean + + /** + * Once the [RouteProgress.currentState] has reached [RouteProgressState.ROUTE_ARRIVED] + * for the last stop, this will be called once. + */ + fun onRouteArrival(routeProgress: RouteProgress) {} +} + +/** + * The default controller for arrival. This will move onto the next leg automatically + * if there is one. + */ +class AutoArrivalController : ArrivalController { + + /** + * Default arrival options + */ + override fun arrivalOptions(): ArrivalOptions = ArrivalOptions.Builder().build() + + /** + * By default this will move onto the next step. + */ + override fun onStopArrival(routeLegProgress: RouteLegProgress): Boolean { + return true + } +} + +/** + * Choose when to be notified of arrival. + */ +data class ArrivalOptions( + + /** + * While the next stop is less than [arrivalInSeconds] away, + * [ArrivalController.onStopArrival] will be called + */ + val arrivalInSeconds: Double?, + + /** + * While the next stop is less than [arrivalInMeters] away, + * [ArrivalController.onStopArrival] will be called + */ + val arrivalInMeters: Double? +) { + /** + * Build your arrival options. + */ + class Builder { + + private var arrivalInSeconds: Double? = 5.0 + private var arrivalInMeters: Double? = 40.0 + + /** + * (Recommended) Use time estimation for arrival, arrival is influenced by traffic conditions. + * Arrive when the estimated time to a stop is less than or equal to this threshold + */ + fun arriveInSeconds(arriveInSeconds: Double?): Builder { + this.arrivalInSeconds = arriveInSeconds + return this + } + + /** + * Arrive when the estimated distance to a stop is less than or equal to this threshold + */ + fun arriveInMeters(arriveInMeters: Double?): Builder { + this.arrivalInMeters = arriveInMeters + return this + } + + /** + * Build the object. If you want to disable this feature use [MapboxNavigation.removeArrivalController] + */ + fun build(): ArrivalOptions { + check(arrivalInSeconds != null || arrivalInSeconds != null) { + "Choose a method to be notified of arrival, time and/or distance." + } + return ArrivalOptions( + arrivalInSeconds = arrivalInSeconds, + arrivalInMeters = arrivalInMeters + ) + } + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalProgressObserver.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalProgressObserver.kt new file mode 100644 index 00000000000..bb4f69e0369 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/stops/ArrivalProgressObserver.kt @@ -0,0 +1,79 @@ +package com.mapbox.navigation.core.stops + +import com.mapbox.navigation.base.trip.RouteProgressObserver +import com.mapbox.navigation.base.trip.model.RouteLegProgress +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.base.trip.model.RouteProgressState +import com.mapbox.navigation.core.trip.session.TripSession + +internal class ArrivalProgressObserver( + private val tripSession: TripSession +) : RouteProgressObserver { + + private var arrivalController: ArrivalController = AutoArrivalController() + private var arrivedForRoute = false + + fun attach(arrivalController: ArrivalController) { + this.arrivalController = arrivalController + } + + fun navigateNextRouteLeg(): Boolean { + val numberOfLegs = tripSession.getRouteProgress()?.route()?.legs()?.size + ?: return false + val legIndex = tripSession.getRouteProgress()?.currentLegProgress()?.legIndex() + ?: return false + val nextLegIndex = legIndex + 1 + return if (nextLegIndex < numberOfLegs) { + val navigationStatus = tripSession.updateLegIndex(nextLegIndex) + return nextLegIndex == navigationStatus.legIndex + } else { + true + } + } + + override fun onRouteProgressChanged(routeProgress: RouteProgress) { + val routeLegProgress = routeProgress.currentLegProgress() + ?: return + + val arrivalOptions = arrivalController.arrivalOptions() + if (routeProgress.currentState() == RouteProgressState.ROUTE_ARRIVED && !hasMoreLegs(routeProgress)) { + doOnRouteArrival(routeProgress) + } else if (arrivalOptions.arrivalInSeconds != null) { + checkStopArrivalTime(arrivalOptions.arrivalInSeconds, routeLegProgress) + } else if (arrivalOptions.arrivalInMeters != null) { + checkStopArrivalDistance(arrivalOptions.arrivalInMeters, routeLegProgress) + } + arrivedForRoute = (routeProgress.currentState() ?: RouteProgressState.ROUTE_UNCERTAIN) == RouteProgressState.ROUTE_ARRIVED + } + + private fun hasMoreLegs(routeProgress: RouteProgress): Boolean { + val currentLegIndex = routeProgress.currentLegProgress()?.legIndex() + val lastLegIndex = routeProgress.route()?.legs()?.lastIndex + return (currentLegIndex != null && lastLegIndex != null) && currentLegIndex < lastLegIndex + } + + private fun checkStopArrivalTime(arrivalInSeconds: Double, routeLegProgress: RouteLegProgress) { + if (routeLegProgress.durationRemaining() <= arrivalInSeconds) { + doOnStopArrival(routeLegProgress) + } + } + + private fun checkStopArrivalDistance(arrivalInMeters: Double, routeLegProgress: RouteLegProgress) { + if (routeLegProgress.distanceRemaining() <= arrivalInMeters) { + doOnStopArrival(routeLegProgress) + } + } + + private fun doOnStopArrival(routeLegProgress: RouteLegProgress) { + val moveToNextLeg = arrivalController.onStopArrival(routeLegProgress) + if (moveToNextLeg) { + navigateNextRouteLeg() + } + } + + private fun doOnRouteArrival(routeProgress: RouteProgress) { + if (!arrivedForRoute) { + arrivalController.onRouteArrival(routeProgress) + } + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/MapboxTripSession.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/MapboxTripSession.kt index 096de4198e8..bf708b2582a 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/MapboxTripSession.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/MapboxTripSession.kt @@ -24,6 +24,7 @@ import com.mapbox.navigation.navigator.internal.TripStatus import com.mapbox.navigation.utils.internal.JobControl import com.mapbox.navigation.utils.internal.ThreadController import com.mapbox.navigation.utils.internal.ifNonNull +import com.mapbox.navigator.NavigationStatus import java.util.Date import java.util.concurrent.CopyOnWriteArraySet import java.util.concurrent.TimeUnit @@ -341,6 +342,19 @@ class MapboxTripSession( } } + /** + * Follows a new leg of the already loaded directions. + * Returns an initialized navigation status if no errors occurred + * otherwise, it returns an invalid navigation status state. + * + * @param legIndex new leg index + * + * @return an initialized [NavigationStatus] if no errors, invalid otherwise + */ + override fun updateLegIndex(legIndex: Int): NavigationStatus { + return navigator.updateLegIndex(legIndex) + } + /** * Updates the configuration to enable or disable the extended kalman filter (EKF). * 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 607a41a6b13..119416a132a 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 @@ -8,6 +8,7 @@ import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.navigation.base.trip.RouteProgressObserver import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.core.trip.service.TripService +import com.mapbox.navigator.NavigationStatus internal interface TripSession { @@ -50,4 +51,5 @@ internal interface TripSession { fun updateSensorEvent(sensorEvent: SensorEvent) fun useExtendedKalmanFilter(useEKF: Boolean) + fun updateLegIndex(legIndex: Int): NavigationStatus } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/stops/ArrivalProgressObserverTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/stops/ArrivalProgressObserverTest.kt new file mode 100644 index 00000000000..d50111765c5 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/stops/ArrivalProgressObserverTest.kt @@ -0,0 +1,327 @@ +package com.mapbox.navigation.core.stops + +import com.mapbox.navigation.base.trip.model.RouteLegProgress +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.base.trip.model.RouteProgressState +import com.mapbox.navigation.core.trip.session.TripSession +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +internal class ArrivalProgressObserverTest { + + private val tripSession: TripSession = mockk() + private val arrivalProgressObserver = ArrivalProgressObserver(tripSession) + + @Before + fun setup() { + every { tripSession.getRouteProgress() } returns null + } + + @Test + fun `should not crash with null route progress values`() { + val routeProgress: RouteProgress = mockk { + every { currentState() } returns RouteProgressState.LOCATION_TRACKING + every { route() } returns mockk { + every { legs() } returns listOf( + mockk(), mockk(), mockk() // This route has three legs + ) + every { routeIndex() } returns null + every { currentLegProgress() } returns mockk { + every { legIndex() } returns 0 + every { durationRemaining() } returns 0.0 + every { distanceRemaining() } returns 0.0f + } + } + } + + arrivalProgressObserver.onRouteProgressChanged(routeProgress) + } + + @Test + fun `should notify stop arrival when arrived at waypoint`() { + val onStopArrivalCalls = slot() + val onRouteArrivalCalls = slot() + val customArrivalController: ArrivalController = mockk { + every { onStopArrival(capture(onStopArrivalCalls)) } returns false + every { onRouteArrival(capture(onRouteArrivalCalls)) } returns Unit + every { arrivalOptions() } returns mockk { + every { arrivalInSeconds } returns null + every { arrivalInMeters } returns 10.0 + } + } + + arrivalProgressObserver.attach(customArrivalController) + arrivalProgressObserver.onRouteProgressChanged(mockk { + every { currentState() } returns RouteProgressState.ROUTE_ARRIVED + every { route() } returns mockk { + every { legs() } returns listOf(mockk(), mockk(), mockk()) + } + every { currentLegProgress() } returns mockk { + every { legIndex() } returns 1 + every { durationRemaining() } returns 2.0 + every { distanceRemaining() } returns 8.0f + } + }) + + assertTrue(onStopArrivalCalls.isCaptured) + assertFalse(onRouteArrivalCalls.isCaptured) + } + + @Test + fun `should notify route arrival when arrived at last stop`() { + val onStopArrivalCalls = slot() + val onRouteArrivalCalls = slot() + val customArrivalController: ArrivalController = mockk { + every { onStopArrival(capture(onStopArrivalCalls)) } returns false + every { onRouteArrival(capture(onRouteArrivalCalls)) } returns Unit + every { arrivalOptions() } returns mockk { + every { arrivalInSeconds } returns null + every { arrivalInMeters } returns 10.0 + } + } + + arrivalProgressObserver.attach(customArrivalController) + arrivalProgressObserver.onRouteProgressChanged(mockk { + every { currentState() } returns RouteProgressState.ROUTE_ARRIVED + every { route() } returns mockk { + every { legs() } returns listOf(mockk(), mockk(), mockk()) + } + every { currentLegProgress() } returns mockk { + every { legIndex() } returns 2 + every { durationRemaining() } returns 2.0 + every { distanceRemaining() } returns 8.0f + } + }) + + assertFalse(onStopArrivalCalls.isCaptured) + assertTrue(onRouteArrivalCalls.isCaptured) + } + + @Test + fun `should notify route arrival when arrived at last stop only once`() { + val onStopArrivalCalls = slot() + val onRouteArrivalCalls = mutableListOf() + val customArrivalController: ArrivalController = mockk { + every { onStopArrival(capture(onStopArrivalCalls)) } returns false + every { onRouteArrival(capture(onRouteArrivalCalls)) } returns Unit + every { arrivalOptions() } returns mockk { + every { arrivalInSeconds } returns null + every { arrivalInMeters } returns 10.0 + } + } + val routeProgress: RouteProgress = mockk { + every { currentState() } returns RouteProgressState.ROUTE_ARRIVED + every { route() } returns mockk { + every { legs() } returns listOf(mockk(), mockk(), mockk()) + } + every { currentLegProgress() } returns mockk { + every { legIndex() } returns 2 + every { durationRemaining() } returns 2.0 + every { distanceRemaining() } returns 8.0f + } + } + + arrivalProgressObserver.attach(customArrivalController) + arrivalProgressObserver.onRouteProgressChanged(routeProgress) + arrivalProgressObserver.onRouteProgressChanged(routeProgress) + arrivalProgressObserver.onRouteProgressChanged(routeProgress) + + assertEquals(1, onRouteArrivalCalls.size) + assertFalse(onStopArrivalCalls.isCaptured) + } + + @Test + fun `should notify observers with time option`() { + val onArrivalCalls = slot() + val customArrivalController: ArrivalController = mockk { + every { onStopArrival(capture(onArrivalCalls)) } returns false + every { arrivalOptions() } returns mockk { + every { arrivalInSeconds } returns 5.0 + every { arrivalInMeters } returns null + } + } + + arrivalProgressObserver.attach(customArrivalController) + arrivalProgressObserver.onRouteProgressChanged(mockk { + every { currentState() } returns RouteProgressState.LOCATION_TRACKING + every { currentLegProgress() } returns mockk { + every { durationRemaining() } returns 1.0 + every { distanceRemaining() } returns 15.0f + } + }) + + assertTrue(onArrivalCalls.isCaptured) + assertEquals(onArrivalCalls.captured.durationRemaining(), 1.0, 0.001) + } + + @Test + fun `should notify observers with distance option`() { + val onArrivalCalls = slot() + val customArrivalController: ArrivalController = mockk { + every { onStopArrival(capture(onArrivalCalls)) } returns false + every { arrivalOptions() } returns mockk { + every { arrivalInSeconds } returns null + every { arrivalInMeters } returns 10.0 + } + } + + arrivalProgressObserver.attach(customArrivalController) + arrivalProgressObserver.onRouteProgressChanged(mockk { + every { currentState() } returns RouteProgressState.LOCATION_TRACKING + every { currentLegProgress() } returns mockk { + every { durationRemaining() } returns 2.0 + every { distanceRemaining() } returns 8.0f + } + }) + + assertEquals(onArrivalCalls.captured.distanceRemaining(), 8.0f, 0.001f) + } + + @Test + fun `should notify arrival if arrived on attach`() { + val onArrivalCalls = slot() + val customArrivalController: ArrivalController = mockk { + every { onStopArrival(capture(onArrivalCalls)) } returns false + every { arrivalOptions() } returns mockk { + every { arrivalInSeconds } returns 5.0 + every { arrivalInMeters } returns null + } + } + val routeProgress: RouteProgress = mockk { + every { currentState() } returns RouteProgressState.LOCATION_TRACKING + every { currentLegProgress() } returns mockk { + every { durationRemaining() } returns 1.0 + every { distanceRemaining() } returns 15.0f + } + } + every { tripSession.getRouteProgress() } returns routeProgress + + arrivalProgressObserver.attach(customArrivalController) + arrivalProgressObserver.onRouteProgressChanged(routeProgress) + + assertTrue(onArrivalCalls.isCaptured) + } + + @Test + fun `should not navigate to next stop before arrival`() { + val onArrivalCalls = slot() + val customArrivalController: ArrivalController = mockk { + every { onStopArrival(capture(onArrivalCalls)) } returns false + every { arrivalOptions() } returns mockk { + every { arrivalInMeters } returns 10.0 + every { arrivalInSeconds } returns 5.0 + } + } + + arrivalProgressObserver.attach(customArrivalController) + arrivalProgressObserver.onRouteProgressChanged(mockk { + every { currentState() } returns RouteProgressState.LOCATION_TRACKING + every { currentLegProgress() } returns mockk { + every { durationRemaining() } returns 360.0 + every { distanceRemaining() } returns 80.0f + } + }) + + assertFalse(onArrivalCalls.isCaptured) + } + + @Test + fun `should navigate to next stop automatically by default`() { + every { tripSession.getRouteProgress() } returns mockk { + every { route() } returns mockk { + every { legs() } returns listOf( + mockk(), mockk(), mockk() // This route has three legs + ) + every { routeIndex() } returns "0" + every { currentLegProgress() } returns mockk { + every { legIndex() } returns 1 + } + } + } + every { tripSession.updateLegIndex(2) } returns mockk { + every { legIndex } returns 2 + } + + arrivalProgressObserver.onRouteProgressChanged(mockk { + every { currentState() } returns RouteProgressState.LOCATION_TRACKING + every { currentLegProgress() } returns mockk { + every { durationRemaining() } returns 0.0 + every { distanceRemaining() } returns 0.0f + } + }) + + verify { tripSession.updateLegIndex(2) } + } + + @Test + fun `should navigate to next stop automatically using options`() { + val testNavigateNextRouteLeg = true + val routeProgress: RouteProgress = mockk { + every { currentState() } returns RouteProgressState.LOCATION_TRACKING + every { route() } returns mockk { + every { legs() } returns listOf( + mockk(), mockk(), mockk() // This route has three legs + ) + every { routeIndex() } returns "0" + every { currentLegProgress() } returns mockk { + every { legIndex() } returns 0 + every { durationRemaining() } returns 0.0 + every { distanceRemaining() } returns 0.0f + } + } + } + every { tripSession.getRouteProgress() } returns routeProgress + val customArrivalController: ArrivalController = mockk { + every { onStopArrival(any()) } returns testNavigateNextRouteLeg + every { arrivalOptions() } returns mockk { + every { arrivalInSeconds } returns 0.0 + every { arrivalInMeters } returns null + } + } + every { tripSession.updateLegIndex(any()) } returns mockk { + every { legIndex } returns 1 + } + + arrivalProgressObserver.attach(customArrivalController) + arrivalProgressObserver.onRouteProgressChanged(routeProgress) + + verify(exactly = 1) { tripSession.updateLegIndex(1) } + } + + @Test + fun `should not navigate to next stop automatically using options`() { + val testNavigateNextRouteLeg = false + every { tripSession.getRouteProgress() } returns mockk { + every { currentState() } returns RouteProgressState.LOCATION_TRACKING + every { route() } returns mockk { + every { legs() } returns listOf( + mockk(), mockk(), mockk() // This route has three legs + ) + every { routeIndex() } returns "0" + every { currentLegProgress() } returns mockk { + every { legIndex() } returns 0 + every { durationRemaining() } returns 0.0 + every { distanceRemaining() } returns 0.0f + } + } + } + val customArrivalController: ArrivalController = mockk { + every { onStopArrival(any()) } returns testNavigateNextRouteLeg + every { arrivalOptions() } returns mockk { + every { arrivalInSeconds } returns 0.0 + every { arrivalInMeters } returns null + } + } + + arrivalProgressObserver.attach(customArrivalController) + + verify(exactly = 0) { tripSession.updateLegIndex(any()) } + } +} diff --git a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt index a7303556ba7..1a1ecd386d6 100644 --- a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt +++ b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt @@ -121,15 +121,14 @@ interface MapboxNativeNavigator { /** * Follows a new route and leg of the already loaded directions. - * Returns an initialized route state if no errors occurred - * otherwise, it returns an invalid route state. + * Returns an initialized navigation status if no errors occurred + * otherwise, it returns an invalid navigation status state. * - * @param routeIndex new route index * @param legIndex new leg index * - * @return an initialized route state as [NavigationStatus] + * @return an initialized [NavigationStatus] if no errors, invalid otherwise */ - fun updateLegIndex(routeIndex: Int, legIndex: Int): NavigationStatus + fun updateLegIndex(legIndex: Int): NavigationStatus // Free Drive diff --git a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt index d384ac3aeb9..de1d1e51b5e 100644 --- a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt +++ b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt @@ -51,6 +51,7 @@ object MapboxNativeNavigatorImpl : MapboxNativeNavigator { private const val GRID_SIZE = 0.0025f private const val BUFFER_DILATION: Short = 1 private const val TWO_LEGS: Short = 2 + private const val PRIMARY_ROUTE_INDEX = 0 private val navigator: Navigator = Navigator() private var route: DirectionsRoute? = null @@ -180,16 +181,15 @@ object MapboxNativeNavigatorImpl : MapboxNativeNavigator { /** * Follows a new route and leg of the already loaded directions. - * Returns an initialized route state if no errors occurred - * otherwise, it returns an invalid route state. + * Returns an initialized navigation status if no errors occurred + * otherwise, it returns an invalid navigation status state. * - * @param routeIndex new route index * @param legIndex new leg index * - * @return an initialized route state as [NavigationStatus] + * @return an initialized [NavigationStatus] if no errors, invalid otherwise */ - override fun updateLegIndex(routeIndex: Int, legIndex: Int): NavigationStatus = - navigator.changeRouteLeg(routeIndex, legIndex) + override fun updateLegIndex(legIndex: Int): NavigationStatus = + navigator.changeRouteLeg(PRIMARY_ROUTE_INDEX, legIndex) // Free Drive /**