diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/MainActivity.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/MainActivity.kt index 1bbb6ae0..a143b5d2 100644 --- a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/MainActivity.kt +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/MainActivity.kt @@ -53,6 +53,8 @@ class MainActivity : ComponentActivity() { ) val location by myViewModel.location.collectAsStateWithLifecycle() + val placesAlongRoute by myViewModel.placesAlongRoute.collectAsStateWithLifecycle() + val routeReady by myViewModel.routeReady.collectAsStateWithLifecycle() fun onShowSnackbar(message: String) { scope.launch { @@ -80,11 +82,18 @@ class MainActivity : ComponentActivity() { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> NavigationScreen( modifier = Modifier.padding(innerPadding), - deviceLocation = location + deviceLocation = location, + placesAlongRoute = placesAlongRoute, + routeReady = routeReady, + onClearSearchResults = { + myViewModel.clearSearchResults() + }, + onSearchClicked = { + myViewModel.searchAlongRoute(it) + } ) } } } } } - diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationScreen.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationScreen.kt index ec0b9fbc..a967869f 100644 --- a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationScreen.kt +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationScreen.kt @@ -2,12 +2,25 @@ package com.google.maps.android.compose.navigation import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -20,6 +33,7 @@ import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.navigation.NavigationView +import com.google.android.libraries.places.api.model.Place import com.google.maps.android.compose.ComposeMapColorScheme import com.google.maps.android.compose.GoogleMap import com.google.maps.android.compose.MarkerComposable @@ -30,7 +44,11 @@ import com.google.maps.android.compose.rememberMarkerState @Composable fun NavigationScreen( deviceLocation: LatLng?, - modifier: Modifier = Modifier + placesAlongRoute: List, + onClearSearchResults: () -> Unit, + onSearchClicked: (String) -> Unit, + routeReady: Boolean, + modifier: Modifier = Modifier, ) { val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom( @@ -53,6 +71,59 @@ fun NavigationScreen( Column( modifier = modifier ) { + if (placesAlongRoute.isNotEmpty()) { + OutlinedCard( + modifier = Modifier.height(250.dp).fillMaxWidth() + ) { + Box( + modifier = Modifier.padding(16.dp), + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(placesAlongRoute) { place -> + PlaceItem(place) + } + } + + IconButton( + onClick = onClearSearchResults, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + ) { + Icon(Icons.Filled.Close, contentDescription = "Close") + } + } + } + } else if (routeReady) { + OutlinedCard( + modifier = Modifier.fillMaxWidth() + ) { + Row(modifier = Modifier.fillMaxWidth()) { + Button( + modifier = Modifier.weight(1f), + onClick = { + onSearchClicked("Spicy Vegetarian Food") + } + ) { + Text("Spicy Veg") + } + Button( + modifier = Modifier.weight(1f), + onClick = { + onSearchClicked("Pizza") + } + ) { + Text("Pizza") + } + } + } + } + GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState, @@ -89,3 +160,9 @@ fun NavigationScreen( } } } + +@Composable +fun PlaceItem(place: Place) { + // Should probably filter these. I would not use the place object directly. + Text(place.displayName ?: "Unknown place") +} diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationViewModel.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationViewModel.kt index 0d644a09..12fd158c 100644 --- a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationViewModel.kt +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/NavigationViewModel.kt @@ -31,10 +31,14 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import com.google.android.gms.tasks.CancellationTokenSource import com.google.android.libraries.navigation.ListenableResultFuture +import com.google.android.libraries.places.api.model.EncodedPolyline +import com.google.android.libraries.places.api.model.Place +import com.google.android.libraries.places.api.model.SearchAlongRouteParameters +import com.google.android.libraries.places.api.net.kotlin.awaitSearchByText +import com.google.maps.android.ktx.utils.latLngListEncode import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -import com.google.maps.android.compose.navigation.BuildConfig class NavigationViewModel( private val placesClient: PlacesClient, @@ -60,6 +64,12 @@ class NavigationViewModel( private var navigator: Navigator? = null + private val _placesAlongRoute = MutableStateFlow>(emptyList()) + val placesAlongRoute = _placesAlongRoute.asStateFlow() + + private val _routeReady = MutableStateFlow(false) + val routeReady = _routeReady.asStateFlow() + init { viewModelScope.launch { _hasLocationPermission.collect() { @@ -204,9 +214,43 @@ class NavigationViewModel( travelMode(RoutingOptions.TravelMode.DRIVING) } + // Listen for route changes + navigator.addRouteChangedListener { + Log.d("NavigationContainer", "Route changed") + _routeReady.value = true + // viewModelScope.launch { + // val placeFields = listOf(Place.Field.ID, Place.Field.DISPLAY_NAME) + // searchAlongRoute("Spicy Vegetarian Food", placeFields) + // } + } + navigateToPlace(chautauquaDinningHall, routingOptions) } + private suspend fun searchAlongRoute(searchText: String, placeFields: List) { + val route = navigator?.currentRouteSegment?.latLngs + + if (route.isNullOrEmpty()) { + Log.d("NavigationContainer", "No route found") + return + } + + val encodedPolyline = EncodedPolyline.newInstance(route.latLngListEncode()) + + val searchAlongRouteParameters = SearchAlongRouteParameters.newInstance(encodedPolyline) + + val response = placesClient.awaitSearchByText(searchText, placeFields) { + setSearchAlongRouteParameters(searchAlongRouteParameters) + maxResultCount = 10 + } + + response.places.forEach { + Log.d("Gollum", "Place ID: ${it.id}, Display Name: ${it.displayName}") + } + + _placesAlongRoute.value = response.places + } + override fun onError(@NavigationApi.ErrorCode errorCode: Int) { when (errorCode) { NavigationApi.ErrorCode.NOT_AUTHORIZED -> displayMessage( @@ -235,5 +279,14 @@ class NavigationViewModel( _uiEvent.emit(UiEvent.ShowSnackbar(message)) } } -} + fun clearSearchResults() { + _placesAlongRoute.value = emptyList() + } + + fun searchAlongRoute(searchText: String) { + viewModelScope.launch { + searchAlongRoute(searchText, listOf(Place.Field.ID, Place.Field.DISPLAY_NAME)) + } + } +} diff --git a/navigation-app/src/main/java/com/google/maps/android/compose/navigation/SearchAlongRouteExample.kt b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/SearchAlongRouteExample.kt new file mode 100644 index 00000000..e16659d9 --- /dev/null +++ b/navigation-app/src/main/java/com/google/maps/android/compose/navigation/SearchAlongRouteExample.kt @@ -0,0 +1,81 @@ +package com.google.maps.android.compose.navigation + +import android.util.Log +import com.google.android.gms.maps.model.LatLng +import com.google.android.libraries.navigation.NavigationApi +import com.google.android.libraries.navigation.NavigationApi.NavigatorListener +import com.google.android.libraries.navigation.Navigator +import com.google.android.libraries.places.api.model.EncodedPolyline +import com.google.android.libraries.places.api.model.Place +import com.google.android.libraries.places.api.model.SearchAlongRouteParameters +import com.google.android.libraries.places.api.net.PlacesClient +import com.google.maps.android.ktx.utils.latLngListEncode +import com.google.android.libraries.places.api.net.kotlin.awaitSearchByText +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Holds an instance of the navigator class. Most likely an activity or a view model. Take a look at NavigationViewModel + */ +class NavigationContainer( + private val placesClient: PlacesClient, + private val scope: CoroutineScope, +) : NavigatorListener { + private var navigator: Navigator? = null + + override fun onError(@NavigationApi.ErrorCode errorCode: Int) { + when (errorCode) { + NavigationApi.ErrorCode.NOT_AUTHORIZED -> displayMessage( + "Error loading Navigation SDK: Your API key is " + + "invalid or not authorized to use the Navigation SDK." + ) + + NavigationApi.ErrorCode.TERMS_NOT_ACCEPTED -> displayMessage( + "Error loading Navigation SDK: User did not accept " + + "the Navigation Terms of Use." + ) + + NavigationApi.ErrorCode.NETWORK_ERROR -> displayMessage("Error loading Navigation SDK: Network error.") + NavigationApi.ErrorCode.LOCATION_PERMISSION_MISSING -> displayMessage( + "Error loading Navigation SDK: Location permission " + + "is missing." + ) + + else -> displayMessage("Error loading Navigation SDK: $errorCode") + } + } + + private fun displayMessage(message: String) { + // Could show a snackbar or toast here + Log.w("NavigationContainer", message) + } + + override fun onNavigatorReady(navigator: Navigator?) { + this.navigator = navigator ?: error("Navigator is null") + navigator.addRouteChangedListener { + Log.d("NavigationContainer", "Route changed") + scope.launch { + navigator.currentRouteSegment?.latLngs?.let { route -> + //pass the encoded string to a function to perform the search + searchPlacesAlongRoute(route) + } + } + } + } + + private suspend fun searchPlacesAlongRoute(route: List) { + val encodedPolyline = EncodedPolyline.newInstance(route.latLngListEncode()) + val placeFields = listOf(Place.Field.ID, Place.Field.DISPLAY_NAME) + + val searchAlongRouteParameters = SearchAlongRouteParameters.newInstance(encodedPolyline) + + val response = placesClient.awaitSearchByText("Spicy Vegetarian Food", placeFields) { + setSearchAlongRouteParameters(searchAlongRouteParameters) + maxResultCount = 10 + } + + response.places.forEach { + Log.d("Places API", "Place ID: ${it.id}, Display Name: ${it.displayName}") + } + } +}