Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/unreleased/bugfixes/6840.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Improved `NavigationView` camera behavior to go back into overview state if routes change during route preview state.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.mapbox.navigation.ui.app.internal.controller

import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.ui.app.internal.Action
import com.mapbox.navigation.ui.app.internal.State
Expand All @@ -13,7 +12,6 @@ import com.mapbox.navigation.ui.voice.api.MapboxAudioGuidance
* This class is responsible for playing voice instructions. Use the [AudioAction] to turning the
* audio on or off.
*/
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class AudioGuidanceStateController(
private val store: Store
) : StateController() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.mapbox.navigation.ui.app.internal.controller

import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.ui.app.internal.Action
import com.mapbox.navigation.ui.app.internal.State
Expand All @@ -9,8 +8,9 @@ import com.mapbox.navigation.ui.app.internal.camera.CameraAction
import com.mapbox.navigation.ui.app.internal.camera.CameraState
import com.mapbox.navigation.ui.app.internal.camera.TargetCameraMode
import com.mapbox.navigation.ui.app.internal.navigation.NavigationState
import com.mapbox.navigation.ui.app.internal.routefetch.RoutePreviewState
import kotlinx.coroutines.flow.distinctUntilChanged

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class CameraStateController(
private val store: Store,
) : StateController() {
Expand All @@ -21,8 +21,16 @@ class CameraStateController(
override fun onAttached(mapboxNavigation: MapboxNavigation) {
super.onAttached(mapboxNavigation)

store.select { it.navigation }.observe { navigationState ->
when (navigationState) {
store.state.distinctUntilChanged { old, new ->
if (new.navigation is NavigationState.RoutePreview) {
new.previewRoutes !is RoutePreviewState.Ready ||
old.navigation is NavigationState.RoutePreview &&
new.previewRoutes == old.previewRoutes
} else {
new.navigation == old.navigation
}
}.observe { state ->
when (state.navigation) {
NavigationState.FreeDrive, NavigationState.RoutePreview -> {
store.dispatch(CameraAction.SetCameraMode(TargetCameraMode.Overview))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package com.mapbox.navigation.ui.app.internal.controller

import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.ui.app.internal.Action
import com.mapbox.navigation.ui.app.internal.State
import com.mapbox.navigation.ui.app.internal.Store
import com.mapbox.navigation.ui.app.internal.destination.DestinationAction

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class DestinationStateController(
store: Store
) : StateController() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.mapbox.navigation.ui.app.internal.controller

import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.internal.extensions.flowLocationMatcherResult
import com.mapbox.navigation.core.trip.session.LocationMatcherResult
Expand All @@ -9,7 +8,6 @@ import com.mapbox.navigation.ui.app.internal.State
import com.mapbox.navigation.ui.app.internal.Store
import com.mapbox.navigation.ui.app.internal.location.LocationAction

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class LocationStateController(
private val store: Store
) : StateController() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.mapbox.navigation.ui.app.internal.controller

import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.internal.extensions.flowOnFinalDestinationArrival
import com.mapbox.navigation.ui.app.internal.Action
Expand All @@ -16,7 +15,6 @@ import kotlinx.coroutines.launch
* [NavigationStateAction] received.
* @param store the default [NavigationState]
*/
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class NavigationStateController(
private val store: Store
) : StateController() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.mapbox.navigation.ui.app.internal.controller

import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.internal.extensions.flowRoutesUpdated
Expand All @@ -9,7 +8,6 @@ import com.mapbox.navigation.ui.app.internal.State
import com.mapbox.navigation.ui.app.internal.Store
import com.mapbox.navigation.ui.app.internal.routefetch.RoutesAction

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class RouteStateController(private val store: Store) : StateController() {
init {
store.register(this)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.mapbox.navigation.ui.app.internal.controller

import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.ui.app.internal.Reducer
import com.mapbox.navigation.ui.base.lifecycle.UIComponent

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
abstract class StateController : UIComponent(), Reducer
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package com.mapbox.navigation.ui.app.internal.controller

import com.mapbox.geojson.Point
import com.mapbox.maps.EdgeInsets
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.testing.MainCoroutineRule
import com.mapbox.navigation.ui.app.internal.camera.CameraAction
import com.mapbox.navigation.ui.app.internal.camera.CameraAction.SetCameraMode
import com.mapbox.navigation.ui.app.internal.camera.TargetCameraMode
import com.mapbox.navigation.ui.app.internal.navigation.NavigationState
import com.mapbox.navigation.ui.app.internal.routefetch.RoutePreviewState
import com.mapbox.navigation.ui.app.testing.TestStore
import io.mockk.every
import io.mockk.mockk
Expand Down Expand Up @@ -40,7 +42,7 @@ class CameraStateControllerTest {
}

@Test
fun `when action toIdle updates camera mode`() = coroutineRule.runBlockingTest {
fun `when action toIdle updates camera mode`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

Expand All @@ -51,21 +53,20 @@ class CameraStateControllerTest {
}

@Test
fun `when action toIdle should copy currentCamera mode value to savedCameraMode`() =
coroutineRule.runBlockingTest {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())
fun `when action toIdle should copy currentCamera mode value to savedCameraMode`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

val initialCameraMode = TargetCameraMode.Following
testStore.dispatch(SetCameraMode(initialCameraMode))
testStore.dispatch(SetCameraMode(TargetCameraMode.Idle))
val initialCameraMode = TargetCameraMode.Following
testStore.dispatch(SetCameraMode(initialCameraMode))
testStore.dispatch(SetCameraMode(TargetCameraMode.Idle))

val cameraState = testStore.state.value.camera
assertEquals(initialCameraMode, cameraState.savedCameraMode)
}
val cameraState = testStore.state.value.camera
assertEquals(initialCameraMode, cameraState.savedCameraMode)
}

@Test
fun `when action toOverview updates camera mode`() = coroutineRule.runBlockingTest {
fun `when action toOverview updates camera mode`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

Expand All @@ -76,29 +77,27 @@ class CameraStateControllerTest {
}

@Test
fun `when action toFollowing updates camera mode and zoomUpdatesAllowed`() =
coroutineRule.runBlockingTest {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())
fun `when action toFollowing updates camera mode and zoomUpdatesAllowed`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

testStore.dispatch(SetCameraMode(TargetCameraMode.Following))
testStore.dispatch(SetCameraMode(TargetCameraMode.Following))

val cameraState = testStore.state.value.camera
assertEquals(TargetCameraMode.Following, cameraState.cameraMode)
}
val cameraState = testStore.state.value.camera
assertEquals(TargetCameraMode.Following, cameraState.cameraMode)
}

@Test
fun `when action UpdatePadding updates cameraPadding`() =
coroutineRule.runBlockingTest {
val padding = EdgeInsets(1.0, 2.0, 3.0, 4.0)
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())
fun `when action UpdatePadding updates cameraPadding`() {
val padding = EdgeInsets(1.0, 2.0, 3.0, 4.0)
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

testStore.dispatch(CameraAction.UpdatePadding(padding))
testStore.dispatch(CameraAction.UpdatePadding(padding))

val cameraState = testStore.state.value.camera
assertEquals(padding, cameraState.cameraPadding)
}
val cameraState = testStore.state.value.camera
assertEquals(padding, cameraState.cameraPadding)
}

@Test
fun `on SaveMapState action should save map camera state in the store`() {
Expand All @@ -119,64 +118,131 @@ class CameraStateControllerTest {
}

@Test
fun `camera is set to overview in route preview mode`() =
coroutineRule.runBlockingTest {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())
fun `camera is unchanged in route preview mode without preview routes`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

testStore.updateState { it.copy(navigation = NavigationState.DestinationPreview) }
testStore.dispatch(SetCameraMode(TargetCameraMode.Following))
testStore.updateState { state ->
state.copy(
navigation = NavigationState.RoutePreview,
previewRoutes = RoutePreviewState.Empty,
)
}

val state = testStore.state.value.copy(navigation = NavigationState.RoutePreview)
testStore.setState(state)
assertEquals(TargetCameraMode.Following, testStore.state.value.camera.cameraMode)
}

assertEquals(TargetCameraMode.Overview, testStore.state.value.camera.cameraMode)
@Test
fun `camera is set to overview in route preview mode with preview routes`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

testStore.updateState { it.copy(navigation = NavigationState.DestinationPreview) }
testStore.dispatch(SetCameraMode(TargetCameraMode.Following))
testStore.updateState { state ->
state.copy(
navigation = NavigationState.RoutePreview,
previewRoutes = RoutePreviewState.Ready(listOf(mockk())),
)
}

assertEquals(TargetCameraMode.Overview, testStore.state.value.camera.cameraMode)
}

@Test
fun `camera is set to overview in free drive mode`() =
coroutineRule.runBlockingTest {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())
fun `camera is restored to overview in route preview mode with new preview routes`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

testStore.updateState { state ->
state.copy(
navigation = NavigationState.RoutePreview,
previewRoutes = RoutePreviewState.Ready(listOf(mockk())),
)
}
testStore.dispatch(SetCameraMode(TargetCameraMode.Following))
testStore.updateState { state ->
state.copy(
navigation = NavigationState.RoutePreview,
previewRoutes = RoutePreviewState.Ready(listOf(mockk())),
)
}

assertEquals(TargetCameraMode.Overview, testStore.state.value.camera.cameraMode)
}

val state = testStore.state.value.copy(navigation = NavigationState.FreeDrive)
testStore.setState(state)
@Test
fun `camera is unchanged in route preview mode with the same preview routes`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())
val route = mockk<NavigationRoute>()

assertEquals(TargetCameraMode.Overview, testStore.state.value.camera.cameraMode)
testStore.updateState { state ->
state.copy(
navigation = NavigationState.RoutePreview,
previewRoutes = RoutePreviewState.Ready(listOf(route)),
)
}
testStore.dispatch(SetCameraMode(TargetCameraMode.Following))
testStore.updateState { state ->
state.copy(
navigation = NavigationState.RoutePreview,
previewRoutes = RoutePreviewState.Ready(listOf(route)),
)
}

assertEquals(TargetCameraMode.Following, testStore.state.value.camera.cameraMode)
}

@Test
fun `camera is set to following in active navigation mode`() =
coroutineRule.runBlockingTest {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())
fun `camera is set to overview in free drive mode`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

val state = testStore.state.value.copy(navigation = NavigationState.ActiveNavigation)
testStore.setState(state)
testStore.updateState { it.copy(navigation = NavigationState.DestinationPreview) }
testStore.dispatch(SetCameraMode(TargetCameraMode.Following))
testStore.updateState { it.copy(navigation = NavigationState.FreeDrive) }

assertEquals(TargetCameraMode.Following, testStore.state.value.camera.cameraMode)
}
assertEquals(TargetCameraMode.Overview, testStore.state.value.camera.cameraMode)
}

@Test
fun `camera is set to following in arrival mode`() =
coroutineRule.runBlockingTest {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())
fun `camera is set to following in active navigation mode`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

val state = testStore.state.value.copy(navigation = NavigationState.Arrival)
testStore.setState(state)
testStore.updateState { it.copy(navigation = NavigationState.RoutePreview) }
testStore.dispatch(SetCameraMode(TargetCameraMode.Overview))
testStore.updateState { it.copy(navigation = NavigationState.ActiveNavigation) }

assertEquals(TargetCameraMode.Following, testStore.state.value.camera.cameraMode)
}
assertEquals(TargetCameraMode.Following, testStore.state.value.camera.cameraMode)
}

@Test
fun `camera is set to idle in destination preview mode`() =
coroutineRule.runBlockingTest {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())
fun `camera is set to following in arrival mode`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

val state = testStore.state.value.copy(navigation = NavigationState.DestinationPreview)
testStore.setState(state)
testStore.updateState { it.copy(navigation = NavigationState.ActiveNavigation) }
testStore.dispatch(SetCameraMode(TargetCameraMode.Overview))
testStore.updateState { it.copy(navigation = NavigationState.Arrival) }

assertEquals(TargetCameraMode.Idle, testStore.state.value.camera.cameraMode)
}
assertEquals(TargetCameraMode.Following, testStore.state.value.camera.cameraMode)
}

@Test
fun `camera is set to idle in destination preview mode`() {
val sut = CameraStateController(testStore)
sut.onAttached(mockMapboxNavigation())

testStore.updateState { it.copy(navigation = NavigationState.FreeDrive) }
testStore.dispatch(SetCameraMode(TargetCameraMode.Overview))
testStore.updateState { it.copy(navigation = NavigationState.DestinationPreview) }

assertEquals(TargetCameraMode.Idle, testStore.state.value.camera.cameraMode)
}

private fun mockMapboxNavigation(): MapboxNavigation {
val mapboxNavigation = mockk<MapboxNavigation>(relaxed = true)
Expand Down