Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +108 −11 Original line number Original line Diff line number Diff line Loading @@ -191,12 +191,17 @@ fun AppContent( port = port, port = port, mapViewModel = mapViewModel, mapViewModel = mapViewModel, onMapInteraction = { onMapInteraction = { if (navController.currentBackStackEntry?.destination?.route?.startsWith("place_card") == true) { if (topOfBackStack?.destination?.route?.startsWith("place_card") == true) { navController.popBackStack() navController.popBackStack() } } }, }, onMapPoiClick = { onMapPoiClick = { if (topOfBackStack?.destination?.route?.startsWith("directions") != true && topOfBackStack?.destination?.route?.startsWith( "transit_itinerary_detail" ) != true ) { NavigationUtils.navigate(navController, Screen.PlaceCard(it)) NavigationUtils.navigate(navController, Screen.PlaceCard(it)) } }, }, onDropPin = { onDropPin = { val place = Place( val place = Place( Loading Loading @@ -227,7 +232,26 @@ fun AppContent( selectedOfflineArea = state.selectedOfflineArea, selectedOfflineArea = state.selectedOfflineArea, currentRoute = state.currentRoute, currentRoute = state.currentRoute, allRoutes = state.allRoutes, allRoutes = state.allRoutes, currentTransitItinerary = state.currentTransitItinerary currentTransitItinerary = state.currentTransitItinerary, onRouteAnnotationClick = { routeIndex -> // Handle route annotation click by updating the selected route index in AppContentState // The DirectionsScreen will observe this change and update the DirectionsViewModel if (state.allRoutes.isNotEmpty()) { val actualIndex = if (routeIndex == -1) { // If -1 is passed, it means the current selected route was tapped // Keep the current selection state.selectedRouteIndex ?: 0 } else { // Convert the reversed index back to the correct index // because routes are displayed in reverse order in the RouteLayer state.allRoutes.size - 1 - routeIndex } if (actualIndex >= 0 && actualIndex < state.allRoutes.size) { state.selectedRouteIndex = actualIndex } } } ) ) } else { } else { LaunchedEffect(key1 = port) { LaunchedEffect(key1 = port) { Loading @@ -249,7 +273,14 @@ fun AppContent( Screen.HOME_SEARCH, Screen.HOME_SEARCH, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> HomeRoute(state, homeViewModel, navController, topOfBackStack, appPreferenceRepository, backStackEntry) HomeRoute( state, homeViewModel, navController, topOfBackStack, appPreferenceRepository, backStackEntry ) } } composable( composable( Loading @@ -263,21 +294,39 @@ fun AppContent( Screen.NEARBY_TRANSIT, Screen.NEARBY_TRANSIT, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> NearbyTransitRoute(state, transitViewModel, navController, topOfBackStack, backStackEntry) NearbyTransitRoute( state, transitViewModel, navController, topOfBackStack, backStackEntry ) } } composable( composable( Screen.PLACE_CARD, Screen.PLACE_CARD, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> PlaceCardRoute(state, navController, topOfBackStack, appPreferenceRepository, backStackEntry) PlaceCardRoute( state, navController, topOfBackStack, appPreferenceRepository, backStackEntry ) } } composable( composable( Screen.OFFLINE_AREAS, Screen.OFFLINE_AREAS, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> OfflineAreasRoute(state, navController, topOfBackStack, appPreferenceRepository, backStackEntry) OfflineAreasRoute( state, navController, topOfBackStack, appPreferenceRepository, backStackEntry ) } } composable( composable( Loading Loading @@ -354,14 +403,31 @@ fun AppContent( Screen.DIRECTIONS, Screen.DIRECTIONS, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> DirectionsRoute(state, mapViewModel, navController, topOfBackStack, appPreferenceRepository, hasLocationPermission, onRequestLocationPermission, hasNotificationPermission, onRequestNotificationPermission, backStackEntry) DirectionsRoute( state, mapViewModel, navController, topOfBackStack, appPreferenceRepository, hasLocationPermission, onRequestLocationPermission, hasNotificationPermission, onRequestNotificationPermission, backStackEntry ) } } composable( composable( Screen.TRANSIT_ITINERARY_DETAIL, Screen.TRANSIT_ITINERARY_DETAIL, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> TransitItineraryDetailRoute(state, navController, topOfBackStack, appPreferenceRepository, backStackEntry) TransitItineraryDetailRoute( state, navController, topOfBackStack, appPreferenceRepository, backStackEntry ) } } composable(Screen.TURN_BY_TURN) { backStackEntry -> composable(Screen.TURN_BY_TURN) { backStackEntry -> Loading Loading @@ -409,6 +475,11 @@ private fun HomeRoute( appPreferenceRepository: AppPreferenceRepository, appPreferenceRepository: AppPreferenceRepository, backStackEntry: NavBackStackEntry backStackEntry: NavBackStackEntry ) { ) { LaunchedEffect(Unit) { state.mapPins.clear() state.currentRoute = null state.allRoutes = emptyList() } state.showToolbar = true state.showToolbar = true HomeScreenComposable( HomeScreenComposable( viewModel = homeViewModel, viewModel = homeViewModel, Loading Loading @@ -542,9 +613,15 @@ private fun RoutingProfilesRoute(state: AppContentState, navController: NavHostC @OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class) @Composable @Composable private fun ProfileEditorRoute(state: AppContentState, navController: NavHostController, backStackEntry: NavBackStackEntry) { private fun ProfileEditorRoute( state: AppContentState, navController: NavHostController, backStackEntry: NavBackStackEntry ) { LaunchedEffect(key1 = Unit) { LaunchedEffect(key1 = Unit) { state.mapPins.clear() state.mapPins.clear() state.currentRoute = null state.allRoutes = emptyList() } } val snackBarHostState = remember { SnackbarHostState() } val snackBarHostState = remember { SnackbarHostState() } Loading @@ -566,7 +643,11 @@ private fun ProfileEditorRoute(state: AppContentState, navController: NavHostCon @OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class) @Composable @Composable private fun ManagePlacesRoute(state: AppContentState, navController: NavHostController, backStackEntry: NavBackStackEntry) { private fun ManagePlacesRoute( state: AppContentState, navController: NavHostController, backStackEntry: NavBackStackEntry ) { state.showToolbar = true state.showToolbar = true val listIdRaw = backStackEntry.arguments?.getString("listId") val listIdRaw = backStackEntry.arguments?.getString("listId") Loading Loading @@ -617,6 +698,8 @@ private fun PlaceCardRoute( viewModel.setPlace(place) viewModel.setPlace(place) // Clear any existing pins and add the new one to ensure only one pin is shown at a time // Clear any existing pins and add the new one to ensure only one pin is shown at a time state.mapPins.clear() state.mapPins.clear() state.currentRoute = null state.allRoutes = emptyList() state.mapPins.add(place) state.mapPins.add(place) val previousBackStackEntry = navController.previousBackStackEntry val previousBackStackEntry = navController.previousBackStackEntry Loading Loading @@ -690,6 +773,8 @@ private fun OfflineAreasRoute( rememberBottomSheetScaffoldState(bottomSheetState = bottomSheetState) rememberBottomSheetScaffoldState(bottomSheetState = bottomSheetState) LaunchedEffect(key1 = Unit) { LaunchedEffect(key1 = Unit) { state.currentRoute = null state.allRoutes = emptyList() state.mapPins.clear() state.mapPins.clear() state.peekHeight = state.screenHeightDp / 3 // Approx, empirical state.peekHeight = state.screenHeightDp / 3 // Approx, empirical state.coroutineScope.launch { state.coroutineScope.launch { Loading Loading @@ -814,6 +899,16 @@ private fun DirectionsRoute( state.currentRoute = route state.currentRoute = route state.allRoutes = allRoutes state.allRoutes = allRoutes }) }) // Observe selectedRouteIndex changes from AppContentState and update DirectionsViewModel LaunchedEffect(state.selectedRouteIndex) { state.selectedRouteIndex?.let { selectedIndex -> if (selectedIndex >= 0 && selectedIndex < viewModel.routeState.value.routes.size) { viewModel.selectRoute(selectedIndex) } } } DisposableEffect(key1 = Unit) { DisposableEffect(key1 = Unit) { onDispose { onDispose { state.currentRoute = null state.currentRoute = null Loading Loading @@ -931,6 +1026,8 @@ private fun TransitItineraryDetailRoute( } } // Clear any existing pins // Clear any existing pins state.currentRoute = null state.allRoutes = emptyList() state.mapPins.clear() state.mapPins.clear() } } Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContentState.kt +2 −0 Original line number Original line Diff line number Diff line Loading @@ -54,6 +54,7 @@ class AppContentState( screenHeightDp: Dp = 0.dp, screenHeightDp: Dp = 0.dp, screenWidthDp: Dp = 0.dp, screenWidthDp: Dp = 0.dp, peekHeight: Dp = 0.dp, peekHeight: Dp = 0.dp, selectedRouteIndex: Int? = null, ) { ) { var fabHeight by mutableStateOf(fabHeight) var fabHeight by mutableStateOf(fabHeight) var selectedOfflineArea by mutableStateOf(selectedOfflineArea) var selectedOfflineArea by mutableStateOf(selectedOfflineArea) Loading @@ -64,6 +65,7 @@ class AppContentState( var screenHeightDp by mutableStateOf(screenHeightDp) var screenHeightDp by mutableStateOf(screenHeightDp) var screenWidthDp by mutableStateOf(screenWidthDp) var screenWidthDp by mutableStateOf(screenWidthDp) var peekHeight by mutableStateOf(peekHeight) var peekHeight by mutableStateOf(peekHeight) var selectedRouteIndex by mutableStateOf(selectedRouteIndex) } } @Composable @Composable Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/MapView.kt +119 −95 Original line number Original line Diff line number Diff line Loading @@ -19,7 +19,6 @@ package earth.maps.cardinal.ui.core package earth.maps.cardinal.ui.core import android.content.Context import android.content.Context import android.util.Log import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column Loading @@ -40,6 +39,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource Loading @@ -47,7 +47,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.compose.ui.unit.em import androidx.compose.ui.util.fastSumBy import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt import earth.maps.cardinal.R import earth.maps.cardinal.R Loading @@ -71,7 +70,12 @@ import io.github.dellisd.spatialk.geojson.Point import io.github.dellisd.spatialk.geojson.Polygon import io.github.dellisd.spatialk.geojson.Polygon import io.github.dellisd.spatialk.geojson.Position import io.github.dellisd.spatialk.geojson.Position import kotlinx.coroutines.launch import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.encodeToJsonElement import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState import org.maplibre.compose.expressions.dsl.Feature.get import org.maplibre.compose.expressions.dsl.Feature.has import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.image import org.maplibre.compose.expressions.dsl.image import org.maplibre.compose.expressions.dsl.offset import org.maplibre.compose.expressions.dsl.offset Loading Loading @@ -113,6 +117,7 @@ fun MapView( currentRoute: Route? = null, currentRoute: Route? = null, allRoutes: List<Route>, allRoutes: List<Route>, currentTransitItinerary: Itinerary? = null, currentTransitItinerary: Itinerary? = null, onRouteAnnotationClick: ((Int) -> Unit)? = null, ) { ) { val context = LocalContext.current val context = LocalContext.current val styleState = rememberStyleState() val styleState = rememberStyleState() Loading Loading @@ -161,7 +166,11 @@ fun MapView( ), ), onMapClick = { position, dpOffset -> onMapClick = { position, dpOffset -> mapViewModel.handleMapTap( mapViewModel.handleMapTap( cameraState, dpOffset, onMapPoiClick, onMapInteraction cameraState, dpOffset, onMapPoiClick, onMapInteraction, onRouteAnnotationClick, ) ) ClickResult.Consume ClickResult.Consume }, }, Loading Loading @@ -269,124 +278,139 @@ private fun OfflineBoundsLayer(selectedOfflineArea: OfflineArea?) { private fun RouteLayer(viewModel: MapViewModel, currentRoute: Route?, allRoutes: List<Route>) { private fun RouteLayer(viewModel: MapViewModel, currentRoute: Route?, allRoutes: List<Route>) { val annotations = remember(allRoutes) { viewModel.placeRouteAnnotations(allRoutes) } val annotations = remember(allRoutes) { viewModel.placeRouteAnnotations(allRoutes) } Log.d("annotations", "${annotations.values}") // Create all route features in a single collection val routeFeatures = allRoutes.reversed().mapIndexed { index, route -> allRoutes.reversed().forEachIndexed { index, route -> if (route == currentRoute) { return@forEachIndexed } val routePositions = route.geometry.map { coord -> val routePositions = route.geometry.map { coord -> Position(coord.lng, coord.lat) // [longitude, latitude] Position(coord.lng, coord.lat) // [longitude, latitude] } } val routeLineString = LineString(routePositions) val routeLineString = LineString(routePositions) val routeFeature = Feature(geometry = routeLineString) val routeSource = rememberGeoJsonSource( GeoJsonData.Features(FeatureCollection(features = listOf(routeFeature))) ) val desaturateAmount = 0.8f val desaturateAmount = 0.8f val polylineColor = colorResource(R.color.polyline_color).desaturate(desaturateAmount) val polylineCasingColor = if (route == currentRoute) { val polylineCasingColor = formatColorAsJson(colorResource(R.color.polyline_casing_color)) } else { formatColorAsJson( colorResource(R.color.polyline_casing_color).desaturate(desaturateAmount) colorResource(R.color.polyline_casing_color).desaturate(desaturateAmount) ) } val polylineColor = if (route == currentRoute) { formatColorAsJson(colorResource(R.color.polyline_color)) } else { formatColorAsJson( colorResource(R.color.polyline_color).desaturate(desaturateAmount) ) } Feature( geometry = routeLineString, properties = mapOf( "routeIndex" to Json.encodeToJsonElement(index.toString()), "routeColor" to polylineColor, "routeColorCasing" to polylineCasingColor, if(route == currentRoute) { "current" to Json.encodeToJsonElement(true) } else { "notCurrent" to Json.encodeToJsonElement(true) } ) ) }.toMutableList() // Create single source for all routes val routeSource = rememberGeoJsonSource( GeoJsonData.Features(FeatureCollection(features = routeFeatures)) ) // Route casing layer LineLayer( LineLayer( id = "route_line_casing_$index", source = routeSource, id = "route_lines_casing", source = routeSource, color = rgbColor( color = get("routeColorCasing").cast(), const((polylineCasingColor.red * 255.0).toInt()), // Blue color width = const(11.dp), const((polylineCasingColor.green * 255.0).toInt()), const((polylineCasingColor.blue * 255.0).toInt()) ), width = const(9.dp), opacity = const(1f), opacity = const(1f), cap = const(LineCap.Round), cap = const(LineCap.Round), join = const(LineJoin.Round), join = const(LineJoin.Round), ) ) // Route main line layer LineLayer( LineLayer( id = "route_line_$index", source = routeSource, id = "route_lines", source = routeSource, color = rgbColor( color = get("routeColor").cast(), const((polylineColor.red * 255.0).toInt()), // Blue color width = const(8.dp), const((polylineColor.green * 255.0).toInt()), const((polylineColor.blue * 255.0).toInt()) ), width = const(6.dp), opacity = const(1f), opacity = const(1f), cap = const(LineCap.Round), cap = const(LineCap.Round), join = const(LineJoin.Round), join = const(LineJoin.Round), ) ) annotations[route]?.let { annotationLatLng -> // Route casing layer RouteAnnotation(annotationLatLng, index, route) } } currentRoute?.let { route -> val routePositions = route.geometry.map { coord -> Position(coord.lng, coord.lat) // [longitude, latitude] } val routeLineString = LineString(routePositions) val routeFeature = Feature(geometry = routeLineString) val routeSource = rememberGeoJsonSource( GeoJsonData.Features(FeatureCollection(features = listOf(routeFeature))) ) val polylineColor = colorResource(R.color.polyline_color) val polylineCasingColor = colorResource(R.color.polyline_casing_color) LineLayer( LineLayer( id = "current_route_line_casing", source = routeSource, id = "route_lines_casing_selected", source = routeSource, color = rgbColor( color = get("routeColorCasing").cast(), const((polylineCasingColor.red * 255.0).toInt()), // Blue color filter = has("current"), const((polylineCasingColor.green * 255.0).toInt()), const((polylineCasingColor.blue * 255.0).toInt()) ), width = const(9.dp), width = const(9.dp), opacity = const(1f), opacity = const(1f), cap = const(LineCap.Round), cap = const(LineCap.Round), join = const(LineJoin.Round), join = const(LineJoin.Round), ) ) // Route main line layer LineLayer( LineLayer( id = "current_route_line", source = routeSource, id = "route_lines_selected", source = routeSource, color = rgbColor( color = get("routeColor").cast(), const((polylineColor.red * 255.0).toInt()), // Blue color filter = has("current"), const((polylineColor.green * 255.0).toInt()), const((polylineColor.blue * 255.0).toInt()) ), width = const(6.dp), width = const(6.dp), opacity = const(1f), opacity = const(1f), cap = const(LineCap.Round), cap = const(LineCap.Round), join = const(LineJoin.Round), join = const(LineJoin.Round), ) ) annotations[route]?.let { annotationLatLng -> RouteAnnotations( RouteAnnotation(annotationLatLng, index = null, route) allRoutes.reversed().mapIndexedNotNull { index, route -> } annotations[route]?.let { Triple(index, route, it) } } } ) } } @Composable @Composable private fun RouteAnnotation( private fun formatColorAsJson(polylineCasingColor: Color): JsonElement = Json.encodeToJsonElement( annotationLatLng: LatLng, "#${ index: Int?, String.format( route: Route "%02x%02x%02x", (polylineCasingColor.red * 255).toInt(), (polylineCasingColor.green * 255).toInt(), (polylineCasingColor.blue * 255).toInt() ) }" ) @Composable private fun RouteAnnotations( annotations: List<Triple<Int, Route, LatLng>> ) { ) { val index = index?.toString() ?: "selected" val features = annotations.map { triple -> val annotationFeature = Feature( val index = triple.first.toString() val route = triple.second val duration = formatDuration(route.steps.sumOf { it.duration }.toInt()) return@map Feature( geometry = Point( geometry = Point( coordinates = Position( coordinates = Position( longitude = annotationLatLng.longitude, latitude = annotationLatLng.latitude longitude = triple.third.longitude, latitude = triple.third.latitude ) ) ), properties = mapOf( "routeIndex" to Json.encodeToJsonElement(index), "duration" to Json.encodeToJsonElement(duration) ) ) ) ) } val annotationSource = rememberGeoJsonSource( val annotationSource = rememberGeoJsonSource( GeoJsonData.Features(FeatureCollection(features = listOf(annotationFeature))) GeoJsonData.Features(FeatureCollection(features = features)) ) ) val textHaloColor = MaterialTheme.colorScheme.surface val textHaloColor = MaterialTheme.colorScheme.surface val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface SymbolLayer( SymbolLayer( id = "route_annotation_$index", id = "route_annotations", source = annotationSource, source = annotationSource, textField = const(formatDuration(route.steps.sumOf { it.duration }.toInt())), textField = org.maplibre.compose.expressions.dsl.Feature["duration"].cast(), textAnchor = const(SymbolAnchor.Bottom), textAnchor = const(SymbolAnchor.Bottom), textColor = rgbColor( textColor = rgbColor( const((textColor.red * 255.0).toInt()), // Blue color const((textColor.red * 255.0).toInt()), // Blue color Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/MapViewModel.kt +20 −1 Original line number Original line Diff line number Diff line Loading @@ -199,8 +199,27 @@ class MapViewModel @Inject constructor( cameraState: CameraState, cameraState: CameraState, dpOffset: DpOffset, dpOffset: DpOffset, onMapPoiClick: (Place) -> Unit, onMapPoiClick: (Place) -> Unit, onMapInteraction: () -> Unit onMapInteraction: () -> Unit, onRouteAnnotationClick: ((Int) -> Unit)? = null, ) { ) { // Check for route annotation features first val routeAnnotationFeatures = cameraState.projection?.queryRenderedFeatures( dpOffset, layerIds = setOf("route_annotations", "route_lines_casing", "route_lines") ) val routeAnnotationFeature = routeAnnotationFeatures?.firstOrNull() if (routeAnnotationFeature != null) { // Extract route index from layer ID val routeIndex = routeAnnotationFeature.properties["routeIndex"]?.jsonPrimitive?.content?.toIntOrNull() if (routeIndex != null && onRouteAnnotationClick != null) { onRouteAnnotationClick(routeIndex) return } } val features = cameraState.projection?.queryRenderedFeatures( val features = cameraState.projection?.queryRenderedFeatures( dpOffset, dpOffset, layerIds = setOf( layerIds = setOf( Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt +29 −0 Original line number Original line Diff line number Diff line Loading @@ -493,6 +493,35 @@ class DirectionsViewModel @Inject constructor( } } } } /** * Selects a route by index from the available routes. * This method is called when a route annotation is tapped on the map. * * @param routeIndex The index of the route to select. Use -1 for the current selected route. */ fun selectRouteByIndex(routeIndex: Int) { val currentRoutes = routeState.value.routes if (currentRoutes.isNotEmpty()) { val actualIndex = if (routeIndex == -1) { // If -1 is passed, it means the current selected route was tapped // Keep the current selection routeState.value.selectedRouteIndex ?: 0 } else { // Convert the reversed index back to the correct index // because routes are displayed in reverse order in the RouteLayer currentRoutes.size - 1 - routeIndex } if (actualIndex >= 0 && actualIndex < currentRoutes.size) { routeStateRepository.selectRoute(actualIndex) } } } fun selectRoute(selectedIndex: Int) { routeStateRepository.selectRoute(selectedIndex) } companion object { companion object { private const val TAG = "DirectionsViewModel" private const val TAG = "DirectionsViewModel" } } Loading Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +108 −11 Original line number Original line Diff line number Diff line Loading @@ -191,12 +191,17 @@ fun AppContent( port = port, port = port, mapViewModel = mapViewModel, mapViewModel = mapViewModel, onMapInteraction = { onMapInteraction = { if (navController.currentBackStackEntry?.destination?.route?.startsWith("place_card") == true) { if (topOfBackStack?.destination?.route?.startsWith("place_card") == true) { navController.popBackStack() navController.popBackStack() } } }, }, onMapPoiClick = { onMapPoiClick = { if (topOfBackStack?.destination?.route?.startsWith("directions") != true && topOfBackStack?.destination?.route?.startsWith( "transit_itinerary_detail" ) != true ) { NavigationUtils.navigate(navController, Screen.PlaceCard(it)) NavigationUtils.navigate(navController, Screen.PlaceCard(it)) } }, }, onDropPin = { onDropPin = { val place = Place( val place = Place( Loading Loading @@ -227,7 +232,26 @@ fun AppContent( selectedOfflineArea = state.selectedOfflineArea, selectedOfflineArea = state.selectedOfflineArea, currentRoute = state.currentRoute, currentRoute = state.currentRoute, allRoutes = state.allRoutes, allRoutes = state.allRoutes, currentTransitItinerary = state.currentTransitItinerary currentTransitItinerary = state.currentTransitItinerary, onRouteAnnotationClick = { routeIndex -> // Handle route annotation click by updating the selected route index in AppContentState // The DirectionsScreen will observe this change and update the DirectionsViewModel if (state.allRoutes.isNotEmpty()) { val actualIndex = if (routeIndex == -1) { // If -1 is passed, it means the current selected route was tapped // Keep the current selection state.selectedRouteIndex ?: 0 } else { // Convert the reversed index back to the correct index // because routes are displayed in reverse order in the RouteLayer state.allRoutes.size - 1 - routeIndex } if (actualIndex >= 0 && actualIndex < state.allRoutes.size) { state.selectedRouteIndex = actualIndex } } } ) ) } else { } else { LaunchedEffect(key1 = port) { LaunchedEffect(key1 = port) { Loading @@ -249,7 +273,14 @@ fun AppContent( Screen.HOME_SEARCH, Screen.HOME_SEARCH, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> HomeRoute(state, homeViewModel, navController, topOfBackStack, appPreferenceRepository, backStackEntry) HomeRoute( state, homeViewModel, navController, topOfBackStack, appPreferenceRepository, backStackEntry ) } } composable( composable( Loading @@ -263,21 +294,39 @@ fun AppContent( Screen.NEARBY_TRANSIT, Screen.NEARBY_TRANSIT, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> NearbyTransitRoute(state, transitViewModel, navController, topOfBackStack, backStackEntry) NearbyTransitRoute( state, transitViewModel, navController, topOfBackStack, backStackEntry ) } } composable( composable( Screen.PLACE_CARD, Screen.PLACE_CARD, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> PlaceCardRoute(state, navController, topOfBackStack, appPreferenceRepository, backStackEntry) PlaceCardRoute( state, navController, topOfBackStack, appPreferenceRepository, backStackEntry ) } } composable( composable( Screen.OFFLINE_AREAS, Screen.OFFLINE_AREAS, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> OfflineAreasRoute(state, navController, topOfBackStack, appPreferenceRepository, backStackEntry) OfflineAreasRoute( state, navController, topOfBackStack, appPreferenceRepository, backStackEntry ) } } composable( composable( Loading Loading @@ -354,14 +403,31 @@ fun AppContent( Screen.DIRECTIONS, Screen.DIRECTIONS, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> DirectionsRoute(state, mapViewModel, navController, topOfBackStack, appPreferenceRepository, hasLocationPermission, onRequestLocationPermission, hasNotificationPermission, onRequestNotificationPermission, backStackEntry) DirectionsRoute( state, mapViewModel, navController, topOfBackStack, appPreferenceRepository, hasLocationPermission, onRequestLocationPermission, hasNotificationPermission, onRequestNotificationPermission, backStackEntry ) } } composable( composable( Screen.TRANSIT_ITINERARY_DETAIL, Screen.TRANSIT_ITINERARY_DETAIL, enterTransition = { slideInVertically(initialOffsetY = { it }) }, enterTransition = { slideInVertically(initialOffsetY = { it }) }, exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> exitTransition = { fadeOut(animationSpec = tween(600)) }) { backStackEntry -> TransitItineraryDetailRoute(state, navController, topOfBackStack, appPreferenceRepository, backStackEntry) TransitItineraryDetailRoute( state, navController, topOfBackStack, appPreferenceRepository, backStackEntry ) } } composable(Screen.TURN_BY_TURN) { backStackEntry -> composable(Screen.TURN_BY_TURN) { backStackEntry -> Loading Loading @@ -409,6 +475,11 @@ private fun HomeRoute( appPreferenceRepository: AppPreferenceRepository, appPreferenceRepository: AppPreferenceRepository, backStackEntry: NavBackStackEntry backStackEntry: NavBackStackEntry ) { ) { LaunchedEffect(Unit) { state.mapPins.clear() state.currentRoute = null state.allRoutes = emptyList() } state.showToolbar = true state.showToolbar = true HomeScreenComposable( HomeScreenComposable( viewModel = homeViewModel, viewModel = homeViewModel, Loading Loading @@ -542,9 +613,15 @@ private fun RoutingProfilesRoute(state: AppContentState, navController: NavHostC @OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class) @Composable @Composable private fun ProfileEditorRoute(state: AppContentState, navController: NavHostController, backStackEntry: NavBackStackEntry) { private fun ProfileEditorRoute( state: AppContentState, navController: NavHostController, backStackEntry: NavBackStackEntry ) { LaunchedEffect(key1 = Unit) { LaunchedEffect(key1 = Unit) { state.mapPins.clear() state.mapPins.clear() state.currentRoute = null state.allRoutes = emptyList() } } val snackBarHostState = remember { SnackbarHostState() } val snackBarHostState = remember { SnackbarHostState() } Loading @@ -566,7 +643,11 @@ private fun ProfileEditorRoute(state: AppContentState, navController: NavHostCon @OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class) @Composable @Composable private fun ManagePlacesRoute(state: AppContentState, navController: NavHostController, backStackEntry: NavBackStackEntry) { private fun ManagePlacesRoute( state: AppContentState, navController: NavHostController, backStackEntry: NavBackStackEntry ) { state.showToolbar = true state.showToolbar = true val listIdRaw = backStackEntry.arguments?.getString("listId") val listIdRaw = backStackEntry.arguments?.getString("listId") Loading Loading @@ -617,6 +698,8 @@ private fun PlaceCardRoute( viewModel.setPlace(place) viewModel.setPlace(place) // Clear any existing pins and add the new one to ensure only one pin is shown at a time // Clear any existing pins and add the new one to ensure only one pin is shown at a time state.mapPins.clear() state.mapPins.clear() state.currentRoute = null state.allRoutes = emptyList() state.mapPins.add(place) state.mapPins.add(place) val previousBackStackEntry = navController.previousBackStackEntry val previousBackStackEntry = navController.previousBackStackEntry Loading Loading @@ -690,6 +773,8 @@ private fun OfflineAreasRoute( rememberBottomSheetScaffoldState(bottomSheetState = bottomSheetState) rememberBottomSheetScaffoldState(bottomSheetState = bottomSheetState) LaunchedEffect(key1 = Unit) { LaunchedEffect(key1 = Unit) { state.currentRoute = null state.allRoutes = emptyList() state.mapPins.clear() state.mapPins.clear() state.peekHeight = state.screenHeightDp / 3 // Approx, empirical state.peekHeight = state.screenHeightDp / 3 // Approx, empirical state.coroutineScope.launch { state.coroutineScope.launch { Loading Loading @@ -814,6 +899,16 @@ private fun DirectionsRoute( state.currentRoute = route state.currentRoute = route state.allRoutes = allRoutes state.allRoutes = allRoutes }) }) // Observe selectedRouteIndex changes from AppContentState and update DirectionsViewModel LaunchedEffect(state.selectedRouteIndex) { state.selectedRouteIndex?.let { selectedIndex -> if (selectedIndex >= 0 && selectedIndex < viewModel.routeState.value.routes.size) { viewModel.selectRoute(selectedIndex) } } } DisposableEffect(key1 = Unit) { DisposableEffect(key1 = Unit) { onDispose { onDispose { state.currentRoute = null state.currentRoute = null Loading Loading @@ -931,6 +1026,8 @@ private fun TransitItineraryDetailRoute( } } // Clear any existing pins // Clear any existing pins state.currentRoute = null state.allRoutes = emptyList() state.mapPins.clear() state.mapPins.clear() } } Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContentState.kt +2 −0 Original line number Original line Diff line number Diff line Loading @@ -54,6 +54,7 @@ class AppContentState( screenHeightDp: Dp = 0.dp, screenHeightDp: Dp = 0.dp, screenWidthDp: Dp = 0.dp, screenWidthDp: Dp = 0.dp, peekHeight: Dp = 0.dp, peekHeight: Dp = 0.dp, selectedRouteIndex: Int? = null, ) { ) { var fabHeight by mutableStateOf(fabHeight) var fabHeight by mutableStateOf(fabHeight) var selectedOfflineArea by mutableStateOf(selectedOfflineArea) var selectedOfflineArea by mutableStateOf(selectedOfflineArea) Loading @@ -64,6 +65,7 @@ class AppContentState( var screenHeightDp by mutableStateOf(screenHeightDp) var screenHeightDp by mutableStateOf(screenHeightDp) var screenWidthDp by mutableStateOf(screenWidthDp) var screenWidthDp by mutableStateOf(screenWidthDp) var peekHeight by mutableStateOf(peekHeight) var peekHeight by mutableStateOf(peekHeight) var selectedRouteIndex by mutableStateOf(selectedRouteIndex) } } @Composable @Composable Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/MapView.kt +119 −95 Original line number Original line Diff line number Diff line Loading @@ -19,7 +19,6 @@ package earth.maps.cardinal.ui.core package earth.maps.cardinal.ui.core import android.content.Context import android.content.Context import android.util.Log import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column Loading @@ -40,6 +39,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource Loading @@ -47,7 +47,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.compose.ui.unit.em import androidx.compose.ui.util.fastSumBy import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt import earth.maps.cardinal.R import earth.maps.cardinal.R Loading @@ -71,7 +70,12 @@ import io.github.dellisd.spatialk.geojson.Point import io.github.dellisd.spatialk.geojson.Polygon import io.github.dellisd.spatialk.geojson.Polygon import io.github.dellisd.spatialk.geojson.Position import io.github.dellisd.spatialk.geojson.Position import kotlinx.coroutines.launch import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.encodeToJsonElement import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState import org.maplibre.compose.expressions.dsl.Feature.get import org.maplibre.compose.expressions.dsl.Feature.has import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.const import org.maplibre.compose.expressions.dsl.image import org.maplibre.compose.expressions.dsl.image import org.maplibre.compose.expressions.dsl.offset import org.maplibre.compose.expressions.dsl.offset Loading Loading @@ -113,6 +117,7 @@ fun MapView( currentRoute: Route? = null, currentRoute: Route? = null, allRoutes: List<Route>, allRoutes: List<Route>, currentTransitItinerary: Itinerary? = null, currentTransitItinerary: Itinerary? = null, onRouteAnnotationClick: ((Int) -> Unit)? = null, ) { ) { val context = LocalContext.current val context = LocalContext.current val styleState = rememberStyleState() val styleState = rememberStyleState() Loading Loading @@ -161,7 +166,11 @@ fun MapView( ), ), onMapClick = { position, dpOffset -> onMapClick = { position, dpOffset -> mapViewModel.handleMapTap( mapViewModel.handleMapTap( cameraState, dpOffset, onMapPoiClick, onMapInteraction cameraState, dpOffset, onMapPoiClick, onMapInteraction, onRouteAnnotationClick, ) ) ClickResult.Consume ClickResult.Consume }, }, Loading Loading @@ -269,124 +278,139 @@ private fun OfflineBoundsLayer(selectedOfflineArea: OfflineArea?) { private fun RouteLayer(viewModel: MapViewModel, currentRoute: Route?, allRoutes: List<Route>) { private fun RouteLayer(viewModel: MapViewModel, currentRoute: Route?, allRoutes: List<Route>) { val annotations = remember(allRoutes) { viewModel.placeRouteAnnotations(allRoutes) } val annotations = remember(allRoutes) { viewModel.placeRouteAnnotations(allRoutes) } Log.d("annotations", "${annotations.values}") // Create all route features in a single collection val routeFeatures = allRoutes.reversed().mapIndexed { index, route -> allRoutes.reversed().forEachIndexed { index, route -> if (route == currentRoute) { return@forEachIndexed } val routePositions = route.geometry.map { coord -> val routePositions = route.geometry.map { coord -> Position(coord.lng, coord.lat) // [longitude, latitude] Position(coord.lng, coord.lat) // [longitude, latitude] } } val routeLineString = LineString(routePositions) val routeLineString = LineString(routePositions) val routeFeature = Feature(geometry = routeLineString) val routeSource = rememberGeoJsonSource( GeoJsonData.Features(FeatureCollection(features = listOf(routeFeature))) ) val desaturateAmount = 0.8f val desaturateAmount = 0.8f val polylineColor = colorResource(R.color.polyline_color).desaturate(desaturateAmount) val polylineCasingColor = if (route == currentRoute) { val polylineCasingColor = formatColorAsJson(colorResource(R.color.polyline_casing_color)) } else { formatColorAsJson( colorResource(R.color.polyline_casing_color).desaturate(desaturateAmount) colorResource(R.color.polyline_casing_color).desaturate(desaturateAmount) ) } val polylineColor = if (route == currentRoute) { formatColorAsJson(colorResource(R.color.polyline_color)) } else { formatColorAsJson( colorResource(R.color.polyline_color).desaturate(desaturateAmount) ) } Feature( geometry = routeLineString, properties = mapOf( "routeIndex" to Json.encodeToJsonElement(index.toString()), "routeColor" to polylineColor, "routeColorCasing" to polylineCasingColor, if(route == currentRoute) { "current" to Json.encodeToJsonElement(true) } else { "notCurrent" to Json.encodeToJsonElement(true) } ) ) }.toMutableList() // Create single source for all routes val routeSource = rememberGeoJsonSource( GeoJsonData.Features(FeatureCollection(features = routeFeatures)) ) // Route casing layer LineLayer( LineLayer( id = "route_line_casing_$index", source = routeSource, id = "route_lines_casing", source = routeSource, color = rgbColor( color = get("routeColorCasing").cast(), const((polylineCasingColor.red * 255.0).toInt()), // Blue color width = const(11.dp), const((polylineCasingColor.green * 255.0).toInt()), const((polylineCasingColor.blue * 255.0).toInt()) ), width = const(9.dp), opacity = const(1f), opacity = const(1f), cap = const(LineCap.Round), cap = const(LineCap.Round), join = const(LineJoin.Round), join = const(LineJoin.Round), ) ) // Route main line layer LineLayer( LineLayer( id = "route_line_$index", source = routeSource, id = "route_lines", source = routeSource, color = rgbColor( color = get("routeColor").cast(), const((polylineColor.red * 255.0).toInt()), // Blue color width = const(8.dp), const((polylineColor.green * 255.0).toInt()), const((polylineColor.blue * 255.0).toInt()) ), width = const(6.dp), opacity = const(1f), opacity = const(1f), cap = const(LineCap.Round), cap = const(LineCap.Round), join = const(LineJoin.Round), join = const(LineJoin.Round), ) ) annotations[route]?.let { annotationLatLng -> // Route casing layer RouteAnnotation(annotationLatLng, index, route) } } currentRoute?.let { route -> val routePositions = route.geometry.map { coord -> Position(coord.lng, coord.lat) // [longitude, latitude] } val routeLineString = LineString(routePositions) val routeFeature = Feature(geometry = routeLineString) val routeSource = rememberGeoJsonSource( GeoJsonData.Features(FeatureCollection(features = listOf(routeFeature))) ) val polylineColor = colorResource(R.color.polyline_color) val polylineCasingColor = colorResource(R.color.polyline_casing_color) LineLayer( LineLayer( id = "current_route_line_casing", source = routeSource, id = "route_lines_casing_selected", source = routeSource, color = rgbColor( color = get("routeColorCasing").cast(), const((polylineCasingColor.red * 255.0).toInt()), // Blue color filter = has("current"), const((polylineCasingColor.green * 255.0).toInt()), const((polylineCasingColor.blue * 255.0).toInt()) ), width = const(9.dp), width = const(9.dp), opacity = const(1f), opacity = const(1f), cap = const(LineCap.Round), cap = const(LineCap.Round), join = const(LineJoin.Round), join = const(LineJoin.Round), ) ) // Route main line layer LineLayer( LineLayer( id = "current_route_line", source = routeSource, id = "route_lines_selected", source = routeSource, color = rgbColor( color = get("routeColor").cast(), const((polylineColor.red * 255.0).toInt()), // Blue color filter = has("current"), const((polylineColor.green * 255.0).toInt()), const((polylineColor.blue * 255.0).toInt()) ), width = const(6.dp), width = const(6.dp), opacity = const(1f), opacity = const(1f), cap = const(LineCap.Round), cap = const(LineCap.Round), join = const(LineJoin.Round), join = const(LineJoin.Round), ) ) annotations[route]?.let { annotationLatLng -> RouteAnnotations( RouteAnnotation(annotationLatLng, index = null, route) allRoutes.reversed().mapIndexedNotNull { index, route -> } annotations[route]?.let { Triple(index, route, it) } } } ) } } @Composable @Composable private fun RouteAnnotation( private fun formatColorAsJson(polylineCasingColor: Color): JsonElement = Json.encodeToJsonElement( annotationLatLng: LatLng, "#${ index: Int?, String.format( route: Route "%02x%02x%02x", (polylineCasingColor.red * 255).toInt(), (polylineCasingColor.green * 255).toInt(), (polylineCasingColor.blue * 255).toInt() ) }" ) @Composable private fun RouteAnnotations( annotations: List<Triple<Int, Route, LatLng>> ) { ) { val index = index?.toString() ?: "selected" val features = annotations.map { triple -> val annotationFeature = Feature( val index = triple.first.toString() val route = triple.second val duration = formatDuration(route.steps.sumOf { it.duration }.toInt()) return@map Feature( geometry = Point( geometry = Point( coordinates = Position( coordinates = Position( longitude = annotationLatLng.longitude, latitude = annotationLatLng.latitude longitude = triple.third.longitude, latitude = triple.third.latitude ) ) ), properties = mapOf( "routeIndex" to Json.encodeToJsonElement(index), "duration" to Json.encodeToJsonElement(duration) ) ) ) ) } val annotationSource = rememberGeoJsonSource( val annotationSource = rememberGeoJsonSource( GeoJsonData.Features(FeatureCollection(features = listOf(annotationFeature))) GeoJsonData.Features(FeatureCollection(features = features)) ) ) val textHaloColor = MaterialTheme.colorScheme.surface val textHaloColor = MaterialTheme.colorScheme.surface val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface SymbolLayer( SymbolLayer( id = "route_annotation_$index", id = "route_annotations", source = annotationSource, source = annotationSource, textField = const(formatDuration(route.steps.sumOf { it.duration }.toInt())), textField = org.maplibre.compose.expressions.dsl.Feature["duration"].cast(), textAnchor = const(SymbolAnchor.Bottom), textAnchor = const(SymbolAnchor.Bottom), textColor = rgbColor( textColor = rgbColor( const((textColor.red * 255.0).toInt()), // Blue color const((textColor.red * 255.0).toInt()), // Blue color Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/MapViewModel.kt +20 −1 Original line number Original line Diff line number Diff line Loading @@ -199,8 +199,27 @@ class MapViewModel @Inject constructor( cameraState: CameraState, cameraState: CameraState, dpOffset: DpOffset, dpOffset: DpOffset, onMapPoiClick: (Place) -> Unit, onMapPoiClick: (Place) -> Unit, onMapInteraction: () -> Unit onMapInteraction: () -> Unit, onRouteAnnotationClick: ((Int) -> Unit)? = null, ) { ) { // Check for route annotation features first val routeAnnotationFeatures = cameraState.projection?.queryRenderedFeatures( dpOffset, layerIds = setOf("route_annotations", "route_lines_casing", "route_lines") ) val routeAnnotationFeature = routeAnnotationFeatures?.firstOrNull() if (routeAnnotationFeature != null) { // Extract route index from layer ID val routeIndex = routeAnnotationFeature.properties["routeIndex"]?.jsonPrimitive?.content?.toIntOrNull() if (routeIndex != null && onRouteAnnotationClick != null) { onRouteAnnotationClick(routeIndex) return } } val features = cameraState.projection?.queryRenderedFeatures( val features = cameraState.projection?.queryRenderedFeatures( dpOffset, dpOffset, layerIds = setOf( layerIds = setOf( Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt +29 −0 Original line number Original line Diff line number Diff line Loading @@ -493,6 +493,35 @@ class DirectionsViewModel @Inject constructor( } } } } /** * Selects a route by index from the available routes. * This method is called when a route annotation is tapped on the map. * * @param routeIndex The index of the route to select. Use -1 for the current selected route. */ fun selectRouteByIndex(routeIndex: Int) { val currentRoutes = routeState.value.routes if (currentRoutes.isNotEmpty()) { val actualIndex = if (routeIndex == -1) { // If -1 is passed, it means the current selected route was tapped // Keep the current selection routeState.value.selectedRouteIndex ?: 0 } else { // Convert the reversed index back to the correct index // because routes are displayed in reverse order in the RouteLayer currentRoutes.size - 1 - routeIndex } if (actualIndex >= 0 && actualIndex < currentRoutes.size) { routeStateRepository.selectRoute(actualIndex) } } } fun selectRoute(selectedIndex: Int) { routeStateRepository.selectRoute(selectedIndex) } companion object { companion object { private const val TAG = "DirectionsViewModel" private const val TAG = "DirectionsViewModel" } } Loading