diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/RouteStateRepository.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/RouteStateRepository.kt index dbbd580303f440eb4efc989eb5f14e1395445303..8031761d303307c2e4146a63379b50635d855295 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/RouteStateRepository.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/RouteStateRepository.kt @@ -19,6 +19,7 @@ package earth.maps.cardinal.data import androidx.annotation.VisibleForTesting +import earth.maps.cardinal.ui.directions.DirectionUiError import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -29,6 +30,7 @@ data class RouteState( val isLoading: Boolean = false, val error: String? = null, val selectedRouteIndex: Int? = null, + val directionError: DirectionUiError? = null, ) class RouteStateRepository { @@ -40,7 +42,7 @@ class RouteStateRepository { } fun setRoutes(routes: List) { - _routeState.value = _routeState.value.copy(routes = routes, isLoading = false, error = null) + _routeState.value = _routeState.value.copy(routes = routes, isLoading = false, error = null, directionError = null) } fun selectRoute(index: Int) { @@ -51,6 +53,10 @@ class RouteStateRepository { _routeState.value = _routeState.value.copy(isLoading = false, error = error) } + fun setDirectionError(directionUiError: DirectionUiError?) { + _routeState.value = _routeState.value.copy(isLoading = false, directionError = directionUiError, error = null) + } + fun clear() { _routeState.value = RouteState() } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionUiError.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionUiError.kt new file mode 100644 index 0000000000000000000000000000000000000000..bbe0c625fcd2c9234e12c22bbca0bde1e8a10cc9 --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionUiError.kt @@ -0,0 +1,35 @@ +/* + * Cardinal Maps + * Copyright (C) 2025 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package earth.maps.cardinal.ui.directions + +import uniffi.ferrostar.ParsingException + +sealed class DirectionUiError { + object DistanceExceeded : DirectionUiError() + data class Unknown(val message: String?) : DirectionUiError() +} + +object DirectionCodes { + const val DISTANCE_EXCEEDED = "DistanceExceeded" +} + +fun ParsingException.InvalidStatusCode.toRouteError(): DirectionUiError = + when (this.code) { + DirectionCodes.DISTANCE_EXCEEDED -> DirectionUiError.DistanceExceeded + else -> DirectionUiError.Unknown(this.message) + } \ No newline at end of file diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt index fc42a7b1fb075ebcf971d64488a2eeab261b4c34..19d8664c3c341745150f8f8b0870878642ba5324 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt @@ -25,11 +25,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing @@ -72,7 +70,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.max import androidx.navigation.NavController import earth.maps.cardinal.R.dimen import earth.maps.cardinal.R.drawable @@ -499,6 +496,22 @@ private fun NonTransitRouteResults( ) } + routeState.directionError != null -> { + val message = when (routeState.directionError) { + DirectionUiError.DistanceExceeded -> stringResource(string.long_itinerary_error_message) + is DirectionUiError.Unknown -> routeState.directionError.message?.let { + stringResource(string.directions_error, it) + } ?: stringResource(string.unknown_error) + } + Text( + text = message, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .fillMaxWidth() + .padding(dimensionResource(dimen.padding)) + ) + } + routeState.routes.isNotEmpty() -> { FerrostarRouteResults( routeState = routeState, diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt index 20a3594a4b980d013573f08cdb62f26d558bf6a6..4d027df09dcd9da09b24c27563e3900d3c8f9cce 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt @@ -50,11 +50,11 @@ import earth.maps.cardinal.ui.core.Screen import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uniffi.ferrostar.GeographicCoordinate +import uniffi.ferrostar.ParsingException import uniffi.ferrostar.UserLocation import uniffi.ferrostar.Waypoint import uniffi.ferrostar.WaypointKind @@ -173,6 +173,8 @@ class DirectionsViewModel @Inject constructor( } routeStateRepository.setRoutes(routes) + } catch (invalidStatusCode: ParsingException.InvalidStatusCode) { + routeStateRepository.setDirectionError(invalidStatusCode.toRouteError()) } catch (e: Exception) { Log.e(TAG, "Error while fetching route", e) routeStateRepository.setError( diff --git a/cardinal-android/app/src/main/res/values/strings.xml b/cardinal-android/app/src/main/res/values/strings.xml index cceb14e1eb402398903baee19089b8b46c082e60..08f8feeff73a088240790f46483098ef3efed0d0 100644 --- a/cardinal-android/app/src/main/res/values/strings.xml +++ b/cardinal-android/app/src/main/res/values/strings.xml @@ -155,6 +155,8 @@ Routing Profile Routing Mode Directions + This itinerary is too long + Unknown error occurred From To Profile: %1$s diff --git a/cardinal-android/app/src/test/java/earth/maps/cardinal/ui/directions/DirectionsViewModelTest.kt b/cardinal-android/app/src/test/java/earth/maps/cardinal/ui/directions/DirectionsViewModelTest.kt index 4c9b73ef77dbc202d0dd3f11ec7232540a87d85e..4bb06eb831135189f1aba2ea26d29d7d3c63c34e 100644 --- a/cardinal-android/app/src/test/java/earth/maps/cardinal/ui/directions/DirectionsViewModelTest.kt +++ b/cardinal-android/app/src/test/java/earth/maps/cardinal/ui/directions/DirectionsViewModelTest.kt @@ -22,6 +22,7 @@ import earth.maps.cardinal.routing.RouteRepository import earth.maps.cardinal.transit.TransitousService import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.coVerifyOrder import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -38,6 +39,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import uniffi.ferrostar.ParsingException import uniffi.ferrostar.Route import kotlin.time.ExperimentalTime @@ -87,6 +89,7 @@ class DirectionsViewModelTest { coEvery { mockRouteStateRepository.setRoutes(any()) } returns Unit coEvery { mockPlanStateRepository.setPlanResponse(any()) } returns Unit coEvery { mockRouteStateRepository.setError(any()) } returns Unit + coEvery { mockRouteStateRepository.setDirectionError(any()) } returns Unit coEvery { mockPlanStateRepository.setError(any()) } returns Unit // Mock routing profile repository @@ -272,6 +275,51 @@ class DirectionsViewModelTest { coVerify { mockRouteStateRepository.setError(errorMessage) } } + @Test + fun `fetchDirectionsIfNeeded should handle too long error`() = runTest { + val fromPlace = Place( + id = "1", + name = "New York", + latLng = LatLng(40.7128, -74.0060), + address = null + ) + val toPlace = Place( + id = "2", + name = "London", + latLng = LatLng(51.5074, -0.1278), + address = null + ) + + val mockFerrostarWrapper = mockk() + coEvery { mockFerrostarWrapperRepository.driving } returns mockFerrostarWrapper + + val distanceExceededException = ParsingException.InvalidStatusCode( + code = DirectionCodes.DISTANCE_EXCEEDED, + description = "Distance exceeded" + ) + coEvery { + mockFerrostarWrapper.core.getRoutes(any(), any()) + } throws distanceExceededException + + val state = viewModel.routeState.value + assertEquals(RouteState(), state) + assertFalse(state.isLoading) + assertTrue(state.routes.isEmpty()) + + viewModel.updateFromPlace(fromPlace) + viewModel.updateToPlace(toPlace) + + advanceUntilIdle() + + assertFalse(state.isLoading) + val updatedState = viewModel.routeState.value + assertTrue(updatedState.routes.isEmpty()) + coVerifyOrder { + mockRouteStateRepository.setLoading(true) + mockRouteStateRepository.setDirectionError(DirectionUiError.DistanceExceeded) + } + } + @Test fun `fetchDirectionsIfNeeded should not fetch if fromPlace is null`() = runTest { val toPlace = Place(