Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c993eedd authored by Ellen Poe's avatar Ellen Poe
Browse files

feat: tapping on alternate routes selects them

parent cba365e2
Loading
Loading
Loading
Loading
Loading
+108 −11
Original line number Original line Diff line number Diff line
@@ -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(
@@ -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) {
@@ -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(
@@ -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(
@@ -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 ->
@@ -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,
@@ -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() }
@@ -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")
@@ -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
@@ -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 {
@@ -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
@@ -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()
        }
        }


+2 −0
Original line number Original line Diff line number Diff line
@@ -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)
@@ -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
+119 −95
Original line number Original line Diff line number Diff line
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
@@ -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
                },
                },
@@ -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
+20 −1
Original line number Original line Diff line number Diff line
@@ -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(
+29 −0
Original line number Original line Diff line number Diff line
@@ -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"
    }
    }