Loading cardinal-android/app/src/main/java/earth/maps/cardinal/routing/FerrostarWrapperRepository.kt +98 −30 Original line number Diff line number Diff line Loading @@ -25,6 +25,10 @@ import earth.maps.cardinal.data.LocationRepository import earth.maps.cardinal.data.OrientationRepository import earth.maps.cardinal.data.RoutingMode import earth.maps.cardinal.data.room.RoutingProfileRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import javax.inject.Inject import javax.inject.Singleton Loading @@ -36,17 +40,39 @@ class FerrostarWrapperRepository @Inject constructor( private val orientationRepository: OrientationRepository, private val routingProfileRepository: RoutingProfileRepository ) { lateinit var walking: FerrostarWrapper lateinit var cycling: FerrostarWrapper lateinit var driving: FerrostarWrapper lateinit var truck: FerrostarWrapper lateinit var motorScooter: FerrostarWrapper lateinit var motorcycle: FerrostarWrapper private val _isInitialized = MutableStateFlow(false) val isInitialized = _isInitialized.asStateFlow() private var _walking: FerrostarWrapper? = null private var _cycling: FerrostarWrapper? = null private var _driving: FerrostarWrapper? = null private var _truck: FerrostarWrapper? = null private var _motorScooter: FerrostarWrapper? = null private var _motorcycle: FerrostarWrapper? = null val walking: FerrostarWrapper get() = _walking ?: throw IllegalStateException("Walking wrapper not initialized") val cycling: FerrostarWrapper get() = _cycling ?: throw IllegalStateException("Cycling wrapper not initialized") val driving: FerrostarWrapper get() = _driving ?: throw IllegalStateException("Driving wrapper not initialized") val truck: FerrostarWrapper get() = _truck ?: throw IllegalStateException("Truck wrapper not initialized") val motorScooter: FerrostarWrapper get() = _motorScooter ?: throw IllegalStateException("MotorScooter wrapper not initialized") val motorcycle: FerrostarWrapper get() = _motorcycle ?: throw IllegalStateException("Motorcycle wrapper not initialized") private val pendingOptions = mutableMapOf<RoutingMode, RoutingOptions>() val androidTtsObserver = AndroidTtsObserver(context) /** * Suspends the caller until the repository has been initialized with a Valhalla endpoint. * * Once [_isInitialized] becomes true, this function resumes. If the repository * is already initialized, it returns immediately. */ suspend fun awaitInitialization() { _isInitialized.filter { it }.first() } fun setValhallaEndpoint(endpoint: String) { walking = FerrostarWrapper( _walking = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -55,7 +81,7 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) cycling = FerrostarWrapper( _cycling = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -64,7 +90,7 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) driving = FerrostarWrapper( _driving = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -73,7 +99,7 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) truck = FerrostarWrapper( _truck = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -82,7 +108,7 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) motorScooter = FerrostarWrapper( _motorScooter = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -91,7 +117,7 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) motorcycle = FerrostarWrapper( _motorcycle = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -100,36 +126,78 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) _isInitialized.value = true // Apply pending options synchronized(pendingOptions) { pendingOptions.forEach { (mode, options) -> setOptionsForMode(mode, options) } pendingOptions.clear() } } /** * Updates the routing options for the specified mode by modifying the existing wrapper. * Updates the [RoutingOptions] for the specified [mode] by modifying the existing wrapper. * * If the wrapper for the given [mode] is not yet initialized (i.e., [setValhallaEndpoint] * hasn't been called), the options are stored in [pendingOptions] and applied * automatically once initialization completes. * * @param mode The [RoutingMode] to update (e.g., PEDESTRIAN, AUTO). * @param routingOptions The new configuration options to apply. */ fun setOptionsForMode(mode: RoutingMode, routingOptions: RoutingOptions) { when (mode) { RoutingMode.PEDESTRIAN -> walking.setOptions(routingOptions) RoutingMode.BICYCLE -> cycling.setOptions(routingOptions) RoutingMode.AUTO -> driving.setOptions(routingOptions) RoutingMode.TRUCK -> truck.setOptions(routingOptions) RoutingMode.MOTOR_SCOOTER -> motorScooter.setOptions(routingOptions) RoutingMode.MOTORCYCLE -> motorcycle.setOptions(routingOptions) else -> {} val wrapper = getWrapperForMode(mode) if (wrapper != null) { wrapper.setOptions(routingOptions) } else { synchronized(pendingOptions) { pendingOptions[mode] = routingOptions } } } /** * Resets the routing options for the specified mode to defaults by recreating the wrapper. * Resets the [RoutingOptions] for the specified [mode] to their default values. * * This retrieves the defaults from [routingProfileRepository]. If the [FerrostarWrapper] * for this mode is already initialized, the options are applied immediately. * * If the wrapper is not yet initialized (i.e., [setValhallaEndpoint] hasn't been called), * the default options are stored in [pendingOptions] and applied automatically * during initialization. * * @param mode The [RoutingMode] to reset (e.g., PEDESTRIAN, BICYCLE). */ fun resetOptionsToDefaultsForMode(mode: RoutingMode) { val defaultOptions = routingProfileRepository.createDefaultOptionsForMode(mode) when (mode) { RoutingMode.PEDESTRIAN -> walking.setOptions(defaultOptions) RoutingMode.BICYCLE -> cycling.setOptions(defaultOptions) RoutingMode.AUTO -> driving.setOptions(defaultOptions) RoutingMode.TRUCK -> truck.setOptions(defaultOptions) RoutingMode.MOTOR_SCOOTER -> motorScooter.setOptions(defaultOptions) RoutingMode.MOTORCYCLE -> motorcycle.setOptions(defaultOptions) else -> {} val wrapper = getWrapperForMode(mode) if (wrapper != null) { wrapper.setOptions(defaultOptions) } else { defaultOptions?.let { synchronized(pendingOptions) { pendingOptions[mode] = it } } } } /** * Resolves the internal [FerrostarWrapper] instance associated with a specific [RoutingMode].* * @param mode The [RoutingMode] for which to retrieve the wrapper. * @return The corresponding [FerrostarWrapper] (e.g., [_walking], [_cycling]), * or `null` if the mode is unrecognized or the wrapper hasn't been initialized via [setValhallaEndpoint]. */ private fun getWrapperForMode(mode: RoutingMode): FerrostarWrapper? = when (mode) { RoutingMode.PEDESTRIAN -> _walking RoutingMode.BICYCLE -> _cycling RoutingMode.AUTO -> _driving RoutingMode.TRUCK -> _truck RoutingMode.MOTOR_SCOOTER -> _motorScooter RoutingMode.MOTORCYCLE -> _motorcycle else -> null } } cardinal-android/app/src/main/java/earth/maps/cardinal/routing/RoutingOptions.kt +33 −10 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ interface AutoOptions { val maneuverPenalty: Double? val gateCost: Double? val privateAccessPenalty: Double? val tollBoothCost: Double? val useHighways: Double? val useTolls: Double? val useLivingStreets: Double? Loading @@ -66,11 +67,12 @@ interface AutoOptions { * Routing options for automobile mode. */ data class AutoRoutingOptions( override val costingType: String = "auto", override val costingType: String = COSTING_TYPE_AUTO, // Maneuver and access penalties override val maneuverPenalty: Double? = null, override val gateCost: Double? = null, override val maneuverPenalty: Double? = DEFAULT_MANEUVER_PENALTY, override val gateCost: Double? = DEFAULT_GATE_COST, override val tollBoothCost: Double? = DEFAULT_TOLL_BOOTH_COST, override val privateAccessPenalty: Double? = null, // Road type preferences (0-1 range) Loading @@ -88,17 +90,26 @@ data class AutoRoutingOptions( // HOV options override val excludeUnpaved: Boolean? = null, override val excludeCashOnlyTolls: Boolean? = null ) : RoutingOptions(), AutoOptions ) : RoutingOptions(), AutoOptions { companion object { const val COSTING_TYPE_AUTO = "auto" const val DEFAULT_MANEUVER_PENALTY = 25.0 const val DEFAULT_GATE_COST = 45.0 const val DEFAULT_TOLL_BOOTH_COST = 30.0 } } /** * Routing options for truck mode (extends auto with truck-specific parameters). */ data class TruckRoutingOptions( override val costingType: String = "truck", override val costingType: String = COSTING_TYPE_TRUCK, // Basic auto options override val maneuverPenalty: Double? = null, override val gateCost: Double? = null, override val gateCost: Double? = DEFAULT_GATE_COST, override val tollBoothCost: Double? = DEFAULT_TOLL_BOOTH_COST, override val privateAccessPenalty: Double? = null, override val useHighways: Double? = null, override val useTolls: Double? = null, Loading @@ -119,7 +130,14 @@ data class TruckRoutingOptions( val axleCount: Int? = null, val hazmat: Boolean? = null, val useTruckRoute: Double? = null // 0-1 range ) : RoutingOptions(), AutoOptions ) : RoutingOptions(), AutoOptions { companion object { const val COSTING_TYPE_TRUCK = "truck" const val DEFAULT_GATE_COST = 45.0 const val DEFAULT_TOLL_BOOTH_COST = 30.0 } } /** * Routing options for motor scooter mode. Loading @@ -130,6 +148,7 @@ data class MotorScooterRoutingOptions( // Basic auto options override val maneuverPenalty: Double? = null, override val gateCost: Double? = null, override val tollBoothCost: Double? = null, override val privateAccessPenalty: Double? = null, override val useHighways: Double? = null, override val useTolls: Double? = null, Loading @@ -156,6 +175,7 @@ data class MotorcycleRoutingOptions( // Basic auto options override val maneuverPenalty: Double? = null, override val gateCost: Double? = null, override val tollBoothCost: Double? = null, override val privateAccessPenalty: Double? = null, override val useHighways: Double? = null, override val useTolls: Double? = null, Loading Loading @@ -199,8 +219,7 @@ data class CyclingRoutingOptions( data class PedestrianRoutingOptions( override val costingType: String = "pedestrian", // Walking speed val walkingSpeed: Double? = null, // km/h val walkingSpeed: Double? = WALKING_SPEED_IN_KMH, // km/h // Path preferences (factors) val walkwayFactor: Double? = null, Loading @@ -211,4 +230,8 @@ data class PedestrianRoutingOptions( // Accessibility options val type: PedestrianType? = null ) : RoutingOptions() ) : RoutingOptions() { companion object { const val WALKING_SPEED_IN_KMH = 4.2 } } cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +5 −10 Original line number Diff line number Diff line Loading @@ -36,11 +36,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeContent Loading Loading @@ -112,7 +110,6 @@ import earth.maps.cardinal.routing.RouteRepository import earth.maps.cardinal.ui.directions.DirectionsScreen import earth.maps.cardinal.ui.directions.DirectionsViewModel import earth.maps.cardinal.ui.directions.RouteDisplayHandler import earth.maps.cardinal.ui.navigation.TurnByTurnNavigationScreen import earth.maps.cardinal.ui.home.HomeScreen import earth.maps.cardinal.ui.home.HomeViewModel import earth.maps.cardinal.ui.home.NearbyScreenContent Loading @@ -121,6 +118,7 @@ import earth.maps.cardinal.ui.home.OfflineAreasScreen import earth.maps.cardinal.ui.home.OfflineAreasViewModel import earth.maps.cardinal.ui.home.TransitScreenContent import earth.maps.cardinal.ui.home.TransitScreenViewModel import earth.maps.cardinal.ui.navigation.TurnByTurnNavigationScreen import earth.maps.cardinal.ui.place.PlaceCardScreen import earth.maps.cardinal.ui.place.PlaceCardViewModel import earth.maps.cardinal.ui.saved.ManagePlacesScreen Loading Loading @@ -1112,13 +1110,10 @@ private fun TurnByTurnRoute( } } val routingMode = routingModeJson?.let { try { Gson().fromJson(it, RoutingMode::class.java) } catch (_: Exception) { RoutingMode.AUTO } } ?: RoutingMode.AUTO val routingMode = runCatching { RoutingMode.entries.first { it.value.equals(routingModeJson, ignoreCase = true) } ?: RoutingMode.AUTO }.getOrElse { RoutingMode.AUTO } port?.let { port -> TurnByTurnNavigationScreen( Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt +6 −0 Original line number Diff line number Diff line Loading @@ -108,6 +108,9 @@ class DirectionsViewModel @Inject constructor( suspend fun initializeRoutingMode() { // Wait for FerrostarWrapperRepository to be initialized before setting options ferrostarWrapperRepository.awaitInitialization() // Set initial routing mode from preferences selectedRoutingMode = appPreferenceRepository.lastRoutingMode.value.let { modeString -> RoutingMode.entries.find { it.value == modeString } ?: RoutingMode.AUTO Loading Loading @@ -161,6 +164,9 @@ class DirectionsViewModel @Inject constructor( private fun fetchDrivingDirections(origin: Place, destination: Place) { viewModelScope.launch { // Wait for FerrostarWrapperRepository to be initialized ferrostarWrapperRepository.awaitInitialization() routeStateRepository.setLoading(true) try { Loading cardinal-android/app/src/test/java/earth/maps/cardinal/ui/directions/DirectionsViewModelTest.kt +22 −0 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. */ package earth.maps.cardinal.ui.directions import earth.maps.cardinal.MainCoroutineRule Loading Loading @@ -98,6 +116,8 @@ class DirectionsViewModelTest { ) coEvery { mockRoutingProfileRepository.getProfilesForMode(any()) } returns flowOf(emptyList()) // Mock FerrostarWrapperRepository coEvery { mockFerrostarWrapperRepository.awaitInitialization() } returns Unit coEvery { mockFerrostarWrapperRepository.resetOptionsToDefaultsForMode(any()) } returns Unit viewModel = DirectionsViewModel( Loading Loading @@ -256,6 +276,8 @@ class DirectionsViewModelTest { address = null ) mockFerrostarWrapperRepository.awaitInitialization() // Setup mock to throw an exception val mockFerrostarWrapper = mockk<FerrostarWrapper>() coEvery { mockFerrostarWrapperRepository.driving } returns mockFerrostarWrapper Loading Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/routing/FerrostarWrapperRepository.kt +98 −30 Original line number Diff line number Diff line Loading @@ -25,6 +25,10 @@ import earth.maps.cardinal.data.LocationRepository import earth.maps.cardinal.data.OrientationRepository import earth.maps.cardinal.data.RoutingMode import earth.maps.cardinal.data.room.RoutingProfileRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import javax.inject.Inject import javax.inject.Singleton Loading @@ -36,17 +40,39 @@ class FerrostarWrapperRepository @Inject constructor( private val orientationRepository: OrientationRepository, private val routingProfileRepository: RoutingProfileRepository ) { lateinit var walking: FerrostarWrapper lateinit var cycling: FerrostarWrapper lateinit var driving: FerrostarWrapper lateinit var truck: FerrostarWrapper lateinit var motorScooter: FerrostarWrapper lateinit var motorcycle: FerrostarWrapper private val _isInitialized = MutableStateFlow(false) val isInitialized = _isInitialized.asStateFlow() private var _walking: FerrostarWrapper? = null private var _cycling: FerrostarWrapper? = null private var _driving: FerrostarWrapper? = null private var _truck: FerrostarWrapper? = null private var _motorScooter: FerrostarWrapper? = null private var _motorcycle: FerrostarWrapper? = null val walking: FerrostarWrapper get() = _walking ?: throw IllegalStateException("Walking wrapper not initialized") val cycling: FerrostarWrapper get() = _cycling ?: throw IllegalStateException("Cycling wrapper not initialized") val driving: FerrostarWrapper get() = _driving ?: throw IllegalStateException("Driving wrapper not initialized") val truck: FerrostarWrapper get() = _truck ?: throw IllegalStateException("Truck wrapper not initialized") val motorScooter: FerrostarWrapper get() = _motorScooter ?: throw IllegalStateException("MotorScooter wrapper not initialized") val motorcycle: FerrostarWrapper get() = _motorcycle ?: throw IllegalStateException("Motorcycle wrapper not initialized") private val pendingOptions = mutableMapOf<RoutingMode, RoutingOptions>() val androidTtsObserver = AndroidTtsObserver(context) /** * Suspends the caller until the repository has been initialized with a Valhalla endpoint. * * Once [_isInitialized] becomes true, this function resumes. If the repository * is already initialized, it returns immediately. */ suspend fun awaitInitialization() { _isInitialized.filter { it }.first() } fun setValhallaEndpoint(endpoint: String) { walking = FerrostarWrapper( _walking = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -55,7 +81,7 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) cycling = FerrostarWrapper( _cycling = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -64,7 +90,7 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) driving = FerrostarWrapper( _driving = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -73,7 +99,7 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) truck = FerrostarWrapper( _truck = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -82,7 +108,7 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) motorScooter = FerrostarWrapper( _motorScooter = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -91,7 +117,7 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) motorcycle = FerrostarWrapper( _motorcycle = FerrostarWrapper( context, locationRepository, orientationRepository, Loading @@ -100,36 +126,78 @@ class FerrostarWrapperRepository @Inject constructor( androidTtsObserver, routingProfileRepository ) _isInitialized.value = true // Apply pending options synchronized(pendingOptions) { pendingOptions.forEach { (mode, options) -> setOptionsForMode(mode, options) } pendingOptions.clear() } } /** * Updates the routing options for the specified mode by modifying the existing wrapper. * Updates the [RoutingOptions] for the specified [mode] by modifying the existing wrapper. * * If the wrapper for the given [mode] is not yet initialized (i.e., [setValhallaEndpoint] * hasn't been called), the options are stored in [pendingOptions] and applied * automatically once initialization completes. * * @param mode The [RoutingMode] to update (e.g., PEDESTRIAN, AUTO). * @param routingOptions The new configuration options to apply. */ fun setOptionsForMode(mode: RoutingMode, routingOptions: RoutingOptions) { when (mode) { RoutingMode.PEDESTRIAN -> walking.setOptions(routingOptions) RoutingMode.BICYCLE -> cycling.setOptions(routingOptions) RoutingMode.AUTO -> driving.setOptions(routingOptions) RoutingMode.TRUCK -> truck.setOptions(routingOptions) RoutingMode.MOTOR_SCOOTER -> motorScooter.setOptions(routingOptions) RoutingMode.MOTORCYCLE -> motorcycle.setOptions(routingOptions) else -> {} val wrapper = getWrapperForMode(mode) if (wrapper != null) { wrapper.setOptions(routingOptions) } else { synchronized(pendingOptions) { pendingOptions[mode] = routingOptions } } } /** * Resets the routing options for the specified mode to defaults by recreating the wrapper. * Resets the [RoutingOptions] for the specified [mode] to their default values. * * This retrieves the defaults from [routingProfileRepository]. If the [FerrostarWrapper] * for this mode is already initialized, the options are applied immediately. * * If the wrapper is not yet initialized (i.e., [setValhallaEndpoint] hasn't been called), * the default options are stored in [pendingOptions] and applied automatically * during initialization. * * @param mode The [RoutingMode] to reset (e.g., PEDESTRIAN, BICYCLE). */ fun resetOptionsToDefaultsForMode(mode: RoutingMode) { val defaultOptions = routingProfileRepository.createDefaultOptionsForMode(mode) when (mode) { RoutingMode.PEDESTRIAN -> walking.setOptions(defaultOptions) RoutingMode.BICYCLE -> cycling.setOptions(defaultOptions) RoutingMode.AUTO -> driving.setOptions(defaultOptions) RoutingMode.TRUCK -> truck.setOptions(defaultOptions) RoutingMode.MOTOR_SCOOTER -> motorScooter.setOptions(defaultOptions) RoutingMode.MOTORCYCLE -> motorcycle.setOptions(defaultOptions) else -> {} val wrapper = getWrapperForMode(mode) if (wrapper != null) { wrapper.setOptions(defaultOptions) } else { defaultOptions?.let { synchronized(pendingOptions) { pendingOptions[mode] = it } } } } /** * Resolves the internal [FerrostarWrapper] instance associated with a specific [RoutingMode].* * @param mode The [RoutingMode] for which to retrieve the wrapper. * @return The corresponding [FerrostarWrapper] (e.g., [_walking], [_cycling]), * or `null` if the mode is unrecognized or the wrapper hasn't been initialized via [setValhallaEndpoint]. */ private fun getWrapperForMode(mode: RoutingMode): FerrostarWrapper? = when (mode) { RoutingMode.PEDESTRIAN -> _walking RoutingMode.BICYCLE -> _cycling RoutingMode.AUTO -> _driving RoutingMode.TRUCK -> _truck RoutingMode.MOTOR_SCOOTER -> _motorScooter RoutingMode.MOTORCYCLE -> _motorcycle else -> null } }
cardinal-android/app/src/main/java/earth/maps/cardinal/routing/RoutingOptions.kt +33 −10 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ interface AutoOptions { val maneuverPenalty: Double? val gateCost: Double? val privateAccessPenalty: Double? val tollBoothCost: Double? val useHighways: Double? val useTolls: Double? val useLivingStreets: Double? Loading @@ -66,11 +67,12 @@ interface AutoOptions { * Routing options for automobile mode. */ data class AutoRoutingOptions( override val costingType: String = "auto", override val costingType: String = COSTING_TYPE_AUTO, // Maneuver and access penalties override val maneuverPenalty: Double? = null, override val gateCost: Double? = null, override val maneuverPenalty: Double? = DEFAULT_MANEUVER_PENALTY, override val gateCost: Double? = DEFAULT_GATE_COST, override val tollBoothCost: Double? = DEFAULT_TOLL_BOOTH_COST, override val privateAccessPenalty: Double? = null, // Road type preferences (0-1 range) Loading @@ -88,17 +90,26 @@ data class AutoRoutingOptions( // HOV options override val excludeUnpaved: Boolean? = null, override val excludeCashOnlyTolls: Boolean? = null ) : RoutingOptions(), AutoOptions ) : RoutingOptions(), AutoOptions { companion object { const val COSTING_TYPE_AUTO = "auto" const val DEFAULT_MANEUVER_PENALTY = 25.0 const val DEFAULT_GATE_COST = 45.0 const val DEFAULT_TOLL_BOOTH_COST = 30.0 } } /** * Routing options for truck mode (extends auto with truck-specific parameters). */ data class TruckRoutingOptions( override val costingType: String = "truck", override val costingType: String = COSTING_TYPE_TRUCK, // Basic auto options override val maneuverPenalty: Double? = null, override val gateCost: Double? = null, override val gateCost: Double? = DEFAULT_GATE_COST, override val tollBoothCost: Double? = DEFAULT_TOLL_BOOTH_COST, override val privateAccessPenalty: Double? = null, override val useHighways: Double? = null, override val useTolls: Double? = null, Loading @@ -119,7 +130,14 @@ data class TruckRoutingOptions( val axleCount: Int? = null, val hazmat: Boolean? = null, val useTruckRoute: Double? = null // 0-1 range ) : RoutingOptions(), AutoOptions ) : RoutingOptions(), AutoOptions { companion object { const val COSTING_TYPE_TRUCK = "truck" const val DEFAULT_GATE_COST = 45.0 const val DEFAULT_TOLL_BOOTH_COST = 30.0 } } /** * Routing options for motor scooter mode. Loading @@ -130,6 +148,7 @@ data class MotorScooterRoutingOptions( // Basic auto options override val maneuverPenalty: Double? = null, override val gateCost: Double? = null, override val tollBoothCost: Double? = null, override val privateAccessPenalty: Double? = null, override val useHighways: Double? = null, override val useTolls: Double? = null, Loading @@ -156,6 +175,7 @@ data class MotorcycleRoutingOptions( // Basic auto options override val maneuverPenalty: Double? = null, override val gateCost: Double? = null, override val tollBoothCost: Double? = null, override val privateAccessPenalty: Double? = null, override val useHighways: Double? = null, override val useTolls: Double? = null, Loading Loading @@ -199,8 +219,7 @@ data class CyclingRoutingOptions( data class PedestrianRoutingOptions( override val costingType: String = "pedestrian", // Walking speed val walkingSpeed: Double? = null, // km/h val walkingSpeed: Double? = WALKING_SPEED_IN_KMH, // km/h // Path preferences (factors) val walkwayFactor: Double? = null, Loading @@ -211,4 +230,8 @@ data class PedestrianRoutingOptions( // Accessibility options val type: PedestrianType? = null ) : RoutingOptions() ) : RoutingOptions() { companion object { const val WALKING_SPEED_IN_KMH = 4.2 } }
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +5 −10 Original line number Diff line number Diff line Loading @@ -36,11 +36,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeContent Loading Loading @@ -112,7 +110,6 @@ import earth.maps.cardinal.routing.RouteRepository import earth.maps.cardinal.ui.directions.DirectionsScreen import earth.maps.cardinal.ui.directions.DirectionsViewModel import earth.maps.cardinal.ui.directions.RouteDisplayHandler import earth.maps.cardinal.ui.navigation.TurnByTurnNavigationScreen import earth.maps.cardinal.ui.home.HomeScreen import earth.maps.cardinal.ui.home.HomeViewModel import earth.maps.cardinal.ui.home.NearbyScreenContent Loading @@ -121,6 +118,7 @@ import earth.maps.cardinal.ui.home.OfflineAreasScreen import earth.maps.cardinal.ui.home.OfflineAreasViewModel import earth.maps.cardinal.ui.home.TransitScreenContent import earth.maps.cardinal.ui.home.TransitScreenViewModel import earth.maps.cardinal.ui.navigation.TurnByTurnNavigationScreen import earth.maps.cardinal.ui.place.PlaceCardScreen import earth.maps.cardinal.ui.place.PlaceCardViewModel import earth.maps.cardinal.ui.saved.ManagePlacesScreen Loading Loading @@ -1112,13 +1110,10 @@ private fun TurnByTurnRoute( } } val routingMode = routingModeJson?.let { try { Gson().fromJson(it, RoutingMode::class.java) } catch (_: Exception) { RoutingMode.AUTO } } ?: RoutingMode.AUTO val routingMode = runCatching { RoutingMode.entries.first { it.value.equals(routingModeJson, ignoreCase = true) } ?: RoutingMode.AUTO }.getOrElse { RoutingMode.AUTO } port?.let { port -> TurnByTurnNavigationScreen( Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt +6 −0 Original line number Diff line number Diff line Loading @@ -108,6 +108,9 @@ class DirectionsViewModel @Inject constructor( suspend fun initializeRoutingMode() { // Wait for FerrostarWrapperRepository to be initialized before setting options ferrostarWrapperRepository.awaitInitialization() // Set initial routing mode from preferences selectedRoutingMode = appPreferenceRepository.lastRoutingMode.value.let { modeString -> RoutingMode.entries.find { it.value == modeString } ?: RoutingMode.AUTO Loading Loading @@ -161,6 +164,9 @@ class DirectionsViewModel @Inject constructor( private fun fetchDrivingDirections(origin: Place, destination: Place) { viewModelScope.launch { // Wait for FerrostarWrapperRepository to be initialized ferrostarWrapperRepository.awaitInitialization() routeStateRepository.setLoading(true) try { Loading
cardinal-android/app/src/test/java/earth/maps/cardinal/ui/directions/DirectionsViewModelTest.kt +22 −0 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. */ package earth.maps.cardinal.ui.directions import earth.maps.cardinal.MainCoroutineRule Loading Loading @@ -98,6 +116,8 @@ class DirectionsViewModelTest { ) coEvery { mockRoutingProfileRepository.getProfilesForMode(any()) } returns flowOf(emptyList()) // Mock FerrostarWrapperRepository coEvery { mockFerrostarWrapperRepository.awaitInitialization() } returns Unit coEvery { mockFerrostarWrapperRepository.resetOptionsToDefaultsForMode(any()) } returns Unit viewModel = DirectionsViewModel( Loading Loading @@ -256,6 +276,8 @@ class DirectionsViewModelTest { address = null ) mockFerrostarWrapperRepository.awaitInitialization() // Setup mock to throw an exception val mockFerrostarWrapper = mockk<FerrostarWrapper>() coEvery { mockFerrostarWrapperRepository.driving } returns mockFerrostarWrapper Loading