diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cd89d58..ebeeb8b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.kouros.navigation" minSdk = 33 targetSdk = 36 - versionCode = 64 - versionName = "0.2.0.64" + versionCode = 66 + versionName = "0.2.0.66" base.archivesName = "navi-$versionName" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt index 6536711..8265b19 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -1,7 +1,6 @@ package com.kouros.navigation.ui import android.Manifest -import android.annotation.SuppressLint import android.app.AppOpsManager import android.location.LocationManager import android.os.Bundle @@ -11,32 +10,22 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresPermission -import androidx.compose.animation.core.animateIntAsState -import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.layout.Box 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.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -68,6 +57,8 @@ import com.kouros.navigation.model.testSingle import com.kouros.navigation.ui.app.AppViewModel import com.kouros.navigation.ui.app.appViewModel import com.kouros.navigation.ui.navigation.AppNavGraph +import com.kouros.navigation.ui.navigation.NavigationSheet +import com.kouros.navigation.ui.search.SearchSheet import com.kouros.navigation.ui.theme.NavigationTheme import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.bearing @@ -80,8 +71,10 @@ import kotlinx.coroutines.runBlocking import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.location.DesiredAccuracy import org.maplibre.compose.location.Location +import org.maplibre.compose.location.UserLocationState import org.maplibre.compose.location.rememberDefaultLocationProvider import org.maplibre.compose.location.rememberUserLocationState +import org.maplibre.compose.style.BaseStyle import org.maplibre.spatialk.geojson.Position import kotlin.time.Duration.Companion.seconds @@ -108,7 +101,7 @@ class MainActivity : ComponentActivity() { routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) routeModel.startNavigation(newRoute) routeData.value = routeModel.curRoute.routeGeoJson - // checkMock() + // checkMock() } } @@ -121,6 +114,7 @@ class MainActivity : ComponentActivity() { SimulationType.GPX -> gpx( context = applicationContext, mock ) + SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock) } } @@ -178,30 +172,19 @@ class MainActivity : ComponentActivity() { } enableEdgeToEdge() setContent { - CheckPermissionScreen() + CheckPermissionScreen(app = { + AppNavGraph( + mainActivity = this + ) + }) } } - @SuppressLint("MissingPermission") - @Composable - fun CheckPermissionScreen() { - val permissions = listOf( - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION, - ) - PermissionScreen( - permissions = permissions, - requiredPermissions = listOf(permissions.first()), - onGranted = { - Application() - }, - ) - } - @OptIn(ExperimentalMaterial3Api::class) @Composable fun StartScreen( - navController: NavHostController) { + navController: NavHostController + ) { val appViewModel: AppViewModel = appViewModel() val darkMode by appViewModel.darkMode.collectAsState() @@ -209,6 +192,10 @@ class MainActivity : ComponentActivity() { val locationProvider = rememberDefaultLocationProvider( updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest ) + val lastRoute by appViewModel.lastRoute.collectAsState() + if (lastRoute.isNotEmpty()) { + navigationViewModel.route.value = lastRoute + } val userLocationState = rememberUserLocationState(locationProvider) if (!useMock) { val locationState = locationProvider.location.collectAsState() @@ -217,112 +204,41 @@ class MainActivity : ComponentActivity() { val step: StepData? by stepData.observeAsState() val nextStep: StepData? by nextStepData.observeAsState() - var collapsedHeight by remember { - mutableIntStateOf(300) - } - val scope = rememberCoroutineScope() - fun closeSheet() { - scope.launch { - collapsedHeight = if (routeModel.isNavigating()) { - 150 - } else { - 300 - } - } - } - - val configuration = LocalConfiguration.current - val screenHeight = configuration.screenHeightDp - var expandedType by remember { - mutableStateOf(ExpandedType.COLLAPSED) - } - - val height by animateIntAsState( - when (expandedType) { - ExpandedType.HALF -> screenHeight / 2 - ExpandedType.FULL -> screenHeight - ExpandedType.COLLAPSED -> collapsedHeight - } - ) - val bottomSheetScaffoldState = rememberBottomSheetScaffoldState() NavigationTheme(useDarkTheme = darkMode == 1) { - BottomSheetScaffold( - scaffoldState = bottomSheetScaffoldState, - sheetShape = RoundedCornerShape( - bottomStart = 0.dp, - bottomEnd = 0.dp, - topStart = 12.dp, - topEnd = 12.dp - ), - sheetContent = { - var isUpdated = false - Box( - Modifier - .fillMaxWidth() - .height(height.dp) - .pointerInput(Unit) { - detectVerticalDragGestures( - onVerticalDrag = { change, dragAmount -> - change.consume() - if (!isUpdated) { - expandedType = when { - dragAmount < 0 && expandedType == ExpandedType.COLLAPSED -> { - ExpandedType.HALF - } - dragAmount < 0 && expandedType == ExpandedType.HALF -> { - ExpandedType.FULL - } - - dragAmount > 0 && expandedType == ExpandedType.FULL -> { - ExpandedType.HALF - } - - dragAmount > 0 && expandedType == ExpandedType.HALF -> { - ExpandedType.COLLAPSED - } - else -> { - ExpandedType.FULL - } - } - isUpdated = true - } - }, - onDragEnd = { - isUpdated = false - } - ) - } - ) { - SheetContent(step, nextStep) { closeSheet() } - } - }, - sheetPeekHeight = height.dp - ) { - MapView( - applicationContext, - userLocationState, - step, - nextStep, - cameraPosition, - routeData, - tilt, - baseStyle, + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { + SheetLayout( + map = { _ -> + Map( + userLocationState, step, nextStep, baseStyle, navController + ) + }, + menu = { SheetContent(navController, step, nextStep) }, ) - if (!routeModel.isNavigating()) { - Settings(navController, modifier = Modifier.fillMaxWidth()) - } } } } @Composable - fun Application() { - val appViewModel: AppViewModel = appViewModel() - val lastRoute by appViewModel.lastRoute.collectAsState() - if (lastRoute.isNotEmpty()) { - navigationViewModel.route.value = lastRoute + private fun Map( + userLocationState: UserLocationState, + step: StepData?, + nextStep: StepData?, + baseStyle: BaseStyle.Json, + navController: NavHostController + ) { + MapView( + applicationContext, + userLocationState, + step, + nextStep, + cameraPosition, + routeData, + tilt, + baseStyle, + ) + if (!routeModel.isNavigating()) { + Settings(navController, modifier = Modifier.fillMaxWidth()) } - AppNavGraph(this) } @Composable @@ -347,10 +263,10 @@ class MainActivity : ComponentActivity() { @Composable fun SheetContent( - step: StepData?, nextStep: StepData?, closeSheet: () -> Unit + navController: NavHostController, step: StepData?, nextStep: StepData? ) { if (!routeModel.isNavigating()) { - SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() } + SearchSheet(applicationContext, navController, navigationViewModel, lastLocation) { } } else { if (step != null) { NavigationSheet( @@ -358,7 +274,7 @@ class MainActivity : ComponentActivity() { routeModel, step, nextStep, - { stopNavigation { closeSheet() } }, + { stopNavigation {} }, { simulateNavigation() }) } } @@ -446,8 +362,7 @@ class MainActivity : ComponentActivity() { fun simulateNavigation() { simulate( - routeModel = routeModel, - mock = mock + routeModel = routeModel, mock = mock ) } diff --git a/app/src/main/java/com/kouros/navigation/ui/MapView.kt b/app/src/main/java/com/kouros/navigation/ui/MapView.kt index dfaf07c..206960f 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MapView.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MapView.kt @@ -1,6 +1,7 @@ package com.kouros.navigation.ui import android.content.Context +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -18,6 +19,7 @@ import com.kouros.navigation.car.map.NavigationImage import com.kouros.navigation.data.StepData import com.kouros.navigation.ui.app.AppViewModel import com.kouros.navigation.ui.app.appViewModel +import com.kouros.navigation.ui.navigation.NavigationInfo import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.location.LocationTrackingEffect @@ -62,6 +64,8 @@ fun MapView( val showBuildings by appViewModel.show3D.collectAsState() val darkMode by appViewModel.darkMode.collectAsState() + val dark = darkMode == 1 || darkMode == 2 && isSystemInDarkTheme() + Column { NavigationInfo(step, nextStep) Box(contentAlignment = Alignment.Center) { @@ -88,7 +92,7 @@ fun MapView( duration = 1.seconds ) } - NavigationImage(paddingValues, width, height / 6, "", darkMode) + NavigationImage(paddingValues, width, height / 6, "", dark) } } } diff --git a/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt b/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt index f076465..fc7de6a 100644 --- a/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt +++ b/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt @@ -1,5 +1,7 @@ package com.kouros.navigation.ui +import android.Manifest +import android.annotation.SuppressLint import android.content.Intent import android.provider.Settings import androidx.compose.animation.animateContentSize @@ -37,6 +39,25 @@ import androidx.core.net.toUri * * By default it assumes that all [permissions] are required. */ + +@SuppressLint("MissingPermission") +@Composable +fun CheckPermissionScreen( + app: @Composable () -> Unit, +) { + val permissions = listOf( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ) + PermissionScreen( + permissions = permissions, + requiredPermissions = listOf(permissions.first()), + onGranted = { + app() + }, + ) +} + @OptIn(ExperimentalPermissionsApi::class) @Composable fun PermissionScreen( diff --git a/app/src/main/java/com/kouros/navigation/ui/SheetLayout.kt b/app/src/main/java/com/kouros/navigation/ui/SheetLayout.kt new file mode 100644 index 0000000..636afcf --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/ui/SheetLayout.kt @@ -0,0 +1,81 @@ +package com.kouros.navigation.ui + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SheetLayout( + map: @Composable (PaddingValues) -> Unit, + menu: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + val sheetState = rememberBottomSheetScaffoldState() + BottomSheetScaffold( + sheetPeekHeight = 180.dp, + scaffoldState = sheetState, + sheetSwipeEnabled = true, + sheetDragHandle = { + ExpandCollapseButton( + sheetState.bottomSheetState.targetValue == SheetValue.Expanded, + onExpand = { sheetState.bottomSheetState.expand() }, + onCollapse = { sheetState.bottomSheetState.partialExpand() }, + modifier = Modifier.fillMaxWidth(), + ) + }, + sheetContent = { + Box( + modifier = + Modifier.background(BottomSheetDefaults.ContainerColor) + .consumeWindowInsets(PaddingValues(top = 56.dp)) + .requiredHeight(500.dp) + ) { + menu() + } + }, + modifier = modifier, + ) { padding -> + map(padding) + } +} + +@Composable +private fun ExpandCollapseButton( + expanded: Boolean, + onExpand: suspend () -> Unit, + onCollapse: suspend () -> Unit, + modifier: Modifier = Modifier, +) { + val degrees by animateFloatAsState(targetValue = if (expanded) 180f else 0f) + val coroutineScope = rememberCoroutineScope() + IconButton( + modifier = modifier, + onClick = { coroutineScope.launch { if (expanded) onCollapse() else onExpand() } }, + ) { + Icon( + painter = painterResource(com.kouros.android.cars.carappservice.R.drawable.keyboard_arrow_up_24px), + contentDescription = if (expanded) "Collapse" else "Expand", + modifier = Modifier.rotate(degrees), + ) + } +} diff --git a/app/src/main/java/com/kouros/navigation/ui/navigation/AppNavGraph.kt b/app/src/main/java/com/kouros/navigation/ui/navigation/AppNavGraph.kt index 779be24..aae796f 100644 --- a/app/src/main/java/com/kouros/navigation/ui/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/kouros/navigation/ui/navigation/AppNavGraph.kt @@ -4,7 +4,9 @@ import androidx.compose.runtime.Composable import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.kouros.navigation.MainApplication.Companion.navigationViewModel import com.kouros.navigation.ui.MainActivity +import com.kouros.navigation.ui.search.SearchScreen import com.kouros.navigation.ui.settings.SettingsRoute @@ -17,5 +19,7 @@ fun AppNavGraph(mainActivity: MainActivity) { composable("display_settings") { SettingsRoute("display_settings", navController) { navController.popBackStack() } } composable("nav_settings") { SettingsRoute("nav_settings", navController) { navController.popBackStack() } } composable("settings") { SettingsRoute("settings", navController) { navController.popBackStack() } } + composable("search") { SearchScreen(navController, navController.context, navigationViewModel, mainActivity.lastLocation) { navController.popBackStack() } + } } } diff --git a/app/src/main/java/com/kouros/navigation/ui/NavigationScreen.kt b/app/src/main/java/com/kouros/navigation/ui/navigation/NavigationScreen.kt similarity index 97% rename from app/src/main/java/com/kouros/navigation/ui/NavigationScreen.kt rename to app/src/main/java/com/kouros/navigation/ui/navigation/NavigationScreen.kt index 630d2af..fc8dfa3 100644 --- a/app/src/main/java/com/kouros/navigation/ui/NavigationScreen.kt +++ b/app/src/main/java/com/kouros/navigation/ui/navigation/NavigationScreen.kt @@ -1,4 +1,4 @@ -package com.kouros.navigation.ui +package com.kouros.navigation.ui.navigation import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt b/app/src/main/java/com/kouros/navigation/ui/navigation/NavigationSheet.kt similarity index 98% rename from app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt rename to app/src/main/java/com/kouros/navigation/ui/navigation/NavigationSheet.kt index d8c1529..b2911cf 100755 --- a/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt +++ b/app/src/main/java/com/kouros/navigation/ui/navigation/NavigationSheet.kt @@ -1,4 +1,4 @@ -package com.kouros.navigation.ui +package com.kouros.navigation.ui.navigation import android.content.Context import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt b/app/src/main/java/com/kouros/navigation/ui/search/SearchScreen.kt similarity index 52% rename from app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt rename to app/src/main/java/com/kouros/navigation/ui/search/SearchScreen.kt index bd87c53..2c35bc0 100644 --- a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt +++ b/app/src/main/java/com/kouros/navigation/ui/search/SearchScreen.kt @@ -1,33 +1,35 @@ -package com.kouros.navigation.ui +package com.kouros.navigation.ui.search +import android.annotation.SuppressLint import android.content.Context import android.location.Location import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -35,119 +37,93 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController import com.kouros.data.R import com.kouros.navigation.data.Place import com.kouros.navigation.data.PlaceColor import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.model.NavigationViewModel +import com.kouros.navigation.ui.theme.NavigationTheme import com.kouros.navigation.utils.location + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SearchSheet( - applicationContext: Context, - viewModel: NavigationViewModel, +fun SearchScreen( + navController: NavHostController, + context: Context, + navigationViewModel: NavigationViewModel, location: Location, - closeSheet: () -> Unit + function: () -> Unit ) { - val searchResults = mutableListOf() - val recentPlaces = viewModel.places.observeAsState() - val search = viewModel.searchPlaces.observeAsState() - if (search.value != null) { - searchResults.addAll(search.value!!) - } - val textFieldState = rememberTextFieldState() - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - ) { - SearchBar( - textFieldState = textFieldState, - searchPlaces = emptyList(), - searchResults = searchResults, - viewModel = viewModel, - context = applicationContext, - location = location, - closeSheet = { closeSheet() } - ) - Home(applicationContext, viewModel, location, closeSheet = { closeSheet() }) - if (recentPlaces.value != null) { - val items = listOf(recentPlaces) - if (items.isNotEmpty()) { - RecentPlaces( - recentPlaces.value!!, - viewModel, - applicationContext, - location, - closeSheet + NavigationTheme(true) { + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + stringResource(id = R.string.search_action_title), + ) + }, + navigationIcon = { + IconButton(onClick = function) { + Icon( + painter = painterResource(R.drawable.arrow_back_24px), + contentDescription = stringResource(id = R.string.accept_action_title), + modifier = Modifier.size(48.dp, 48.dp), + ) + } + }, ) + }, + ) + { padding -> + val scrollState = rememberScrollState() + Column(Modifier.padding(top = 50.dp)) { + SearchBar(context, navigationViewModel, location) + Categories(context, navigationViewModel, location, closeSheet = { }) } } } } -@Composable -fun Home( - applicationContext: Context, - viewModel: NavigationViewModel, - location: Location, - closeSheet: () -> Unit -) { - Row(horizontalArrangement = Arrangement.SpaceBetween) { - Button(onClick = { - val places = viewModel.loadRecentPlace(applicationContext) - val toLocation = location(places.first()!!.longitude, places.first()!!.latitude) - viewModel.loadRoute(applicationContext, location, toLocation, 0F) - closeSheet() - }) { - Icon( - painter = painterResource(id = R.drawable.ic_place_white_24dp), - "Home", - modifier = Modifier.size(24.dp, 24.dp), - ) - Text("Home") - } - Button(onClick = { - }) { - Icon( - painter = painterResource(id = R.drawable.ic_favorite_white_24dp), - "Work", - modifier = Modifier.size(24.dp, 24.dp), - ) - Text("Arbeit") - } - } -} - -private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) { - viewModel.searchPlaces(it, location) -} - @OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchBar( - textFieldState: TextFieldState, - searchPlaces: List, - searchResults: List, - viewModel: NavigationViewModel, context: Context, + navigationViewModel: NavigationViewModel, location: Location, - closeSheet: () -> Unit -) { + ) { + + val searchResults = mutableListOf() + val search = navigationViewModel.searchPlaces.observeAsState() + if (search.value != null) { + searchResults.addAll(search.value!!) + } + val textFieldState = rememberTextFieldState() var expanded by rememberSaveable { mutableStateOf(false) } + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + SearchBar( - windowInsets = WindowInsets.captionBar, colors = SearchBarDefaults.colors( containerColor = MaterialTheme.colorScheme.secondaryContainer ), inputField = { SearchBarDefaults.InputField( + modifier = Modifier.focusRequester(focusRequester), leadingIcon = { Icon( painter = painterResource(id = R.drawable.search_48px), @@ -158,24 +134,64 @@ fun SearchBar( query = textFieldState.text.toString(), onQueryChange = { textFieldState.edit { replace(0, length, it) } }, onSearch = { - searchPlaces(viewModel, location, it) - expanded = false + navigationViewModel.searchPlaces(it, location) + expanded = true }, expanded = expanded, - onExpandedChange = { expanded = it }, + onExpandedChange = { + //expanded = it + }, placeholder = { Text(context.getString(R.string.search_action_title)) } ) }, expanded = expanded, onExpandedChange = { }, ) { - if (searchPlaces.isNotEmpty()) { - Text(context.getString(R.string.recent_destinations)) - RecentPlaces(searchPlaces, viewModel, context, location, closeSheet) - } if (searchResults.isNotEmpty()) { - Text("Search places") - SearchPlaces( searchResults, viewModel, context, location, closeSheet) + SearchPlaces(searchResults, navigationViewModel, context, location, { }) + } + } +} + +@Composable +fun Categories( + applicationContext: Context, + viewModel: NavigationViewModel, + location: Location, + closeSheet: () -> Unit +) { + val scrollState = rememberScrollState() + Row( + horizontalArrangement = Arrangement.SpaceAround, + modifier = Modifier.horizontalScroll(scrollState) + ) { + Button(onClick = { + val places = viewModel.loadRecentPlace(applicationContext) + val toLocation = location(places.first()!!.longitude, places.first()!!.latitude) + viewModel.loadRoute(applicationContext, location, toLocation, 0F) + closeSheet() + }) { + Icon( + painter = painterResource(id = R.drawable.local_gas_station_24), + applicationContext.getString(R.string.fuel_station), + modifier = Modifier.size(24.dp, 24.dp), + ) + } + Button(onClick = { + }) { + Icon( + painter = painterResource(id = R.drawable.ev_station_24px), + "Work", + modifier = Modifier.size(24.dp, 24.dp), + ) + } + Button(onClick = { + }) { + Icon( + painter = painterResource(id = R.drawable.local_pharmacy_24px), + "Work", + modifier = Modifier.size(24.dp, 24.dp), + ) } } } @@ -193,56 +209,7 @@ private fun SearchPlaces( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - if (searchResults.isNotEmpty()) { - items(searchResults, key = { it.placeId }) { place -> - Row { - Icon( - painter = painterResource(id = R.drawable.ic_place_white_24dp), - "Navigation", - tint = color.copy(alpha = 1f), - modifier = Modifier.size(24.dp, 24.dp), - ) - ListItem( - headlineContent = { Text(place.displayName) }, - modifier = Modifier - .clickable { - val pl = Place( - name = place.name, - longitude = place.lon.toDouble(), - latitude = place.lat.toDouble(), - postalCode = place.address.postcode, - city = place.address.city, - street = place.address.road - ) - viewModel.saveRecent(context,pl) - val toLocation = - location(place.lon.toDouble(), place.lat.toDouble()) - viewModel.loadRoute(context, location, toLocation, 0F) - closeSheet() - } - .fillMaxWidth() - ) - HorizontalDivider(color = Color.Gray) - } - } - } - } -} - -@Composable -private fun RecentPlaces( - recentPlaces: List, - viewModel: NavigationViewModel, - context: Context, - location: Location, - closeSheet: () -> Unit -) { - val color = remember { PlaceColor } - LazyColumn( - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - items(recentPlaces, key = { it.id }) { place -> + items(searchResults, key = { it.placeId }) { place -> Row { Icon( painter = painterResource(id = R.drawable.ic_place_white_24dp), @@ -251,10 +218,20 @@ private fun RecentPlaces( modifier = Modifier.size(24.dp, 24.dp), ) ListItem( - headlineContent = { Text("${place.street} ${place.postalCode} ${place.city}") }, + headlineContent = { Text(place.displayName) }, modifier = Modifier .clickable { - val toLocation = location(place.longitude, place.latitude) + val pl = Place( + name = place.name, + longitude = place.lon.toDouble(), + latitude = place.lat.toDouble(), + postalCode = place.address.postcode, + city = place.address.city, + street = place.address.road + ) + viewModel.saveRecent(context, pl) + val toLocation = + location(place.lon.toDouble(), place.lat.toDouble()) viewModel.loadRoute(context, location, toLocation, 0F) closeSheet() } @@ -265,3 +242,5 @@ private fun RecentPlaces( } } } + + diff --git a/app/src/main/java/com/kouros/navigation/ui/search/SearchSheet.kt b/app/src/main/java/com/kouros/navigation/ui/search/SearchSheet.kt new file mode 100644 index 0000000..5fe6f5d --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/ui/search/SearchSheet.kt @@ -0,0 +1,180 @@ +package com.kouros.navigation.ui.search + +import android.content.Context +import android.location.Location +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import com.kouros.data.R +import com.kouros.navigation.data.Place +import com.kouros.navigation.data.PlaceColor +import com.kouros.navigation.model.NavigationViewModel +import com.kouros.navigation.utils.location + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchSheet( + applicationContext: Context, + navController: NavHostController, + viewModel: NavigationViewModel, + location: Location, + closeSheet: () -> Unit +) { + val recentPlaces = viewModel.recentPlaces.observeAsState() + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + SearchBar( + modifier = Modifier.clickable { + navController.navigate("search") + }, + colors = SearchBarDefaults.colors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ), + expanded = false, + onExpandedChange = { navController.navigate("search") }, + inputField = { + SearchBarDefaults.InputField( + enabled = false, + modifier = Modifier.clickable { + navController.navigate("search") + }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.search_48px), + applicationContext.getString(R.string.search_action_title), + modifier = Modifier.size(24.dp, 24.dp), + ) + }, + query = applicationContext.getString(R.string.search_action_title), + onQueryChange = { }, + onSearch = { + + }, + expanded = false, + onExpandedChange = { }, + placeholder = { applicationContext.getString(R.string.search_action_title) } + ) + }, + ) { + + } + + Home(applicationContext, viewModel, location, closeSheet = { closeSheet() }) + if (recentPlaces.value != null) { + val items = listOf(recentPlaces) + if (items.isNotEmpty()) { + Column(Modifier.padding(all = 10.dp)) { + Text(applicationContext.getString(R.string.recent_destinations)) + RecentPlaces( + recentPlaces.value!!, + viewModel, + applicationContext, + location, + closeSheet + ) + } + } + } + } +} + +@Composable +fun Home( + applicationContext: Context, + viewModel: NavigationViewModel, + location: Location, + closeSheet: () -> Unit +) { + Row(horizontalArrangement = Arrangement.SpaceBetween) { + Button(onClick = { + val places = viewModel.loadRecentPlace(applicationContext) + val toLocation = location(places.first()!!.longitude, places.first()!!.latitude) + viewModel.loadRoute(applicationContext, location, toLocation, 0F) + closeSheet() + }) { + Icon( + painter = painterResource(id = R.drawable.ic_place_white_24dp), + "Home", + modifier = Modifier.size(24.dp, 24.dp), + ) + Text("Home") + } + Button(onClick = { + }) { + Icon( + painter = painterResource(id = R.drawable.ic_favorite_white_24dp), + "Work", + modifier = Modifier.size(24.dp, 24.dp), + ) + Text("Arbeit") + } + } +} + + + +@Composable +private fun RecentPlaces( + recentPlaces: List, + viewModel: NavigationViewModel, + context: Context, + location: Location, + closeSheet: () -> Unit +) { + val color = remember { PlaceColor } + LazyColumn( + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + items(recentPlaces, key = { it.id }) { place -> + Row { + Icon( + painter = painterResource(id = R.drawable.ic_place_white_24dp), + "Navigation", + tint = color.copy(alpha = 1f), + modifier = Modifier.size(24.dp, 24.dp), + ) + ListItem( + headlineContent = { Text("${place.street} ${place.postalCode} ${place.city}") }, + modifier = Modifier + .clickable { + val toLocation = location(place.longitude, place.latitude) + viewModel.loadRoute(context, location, toLocation, 0F) + closeSheet() + } + .fillMaxWidth() + ) + HorizontalDivider(color = Color.Gray) + } + } + } +} diff --git a/app/src/main/java/com/kouros/navigation/ui/settings/DisplayScreen.kt b/app/src/main/java/com/kouros/navigation/ui/settings/DisplayScreen.kt index 3c2ef87..d2bdfa6 100644 --- a/app/src/main/java/com/kouros/navigation/ui/settings/DisplayScreen.kt +++ b/app/src/main/java/com/kouros/navigation/ui/settings/DisplayScreen.kt @@ -33,7 +33,7 @@ import com.kouros.navigation.ui.theme.NavigationTheme @OptIn(ExperimentalMaterial3Api::class) @Composable -fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) { +fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) { val darkMode by viewModel.darkMode.collectAsState() val show3D by viewModel.show3D.collectAsState() @@ -47,13 +47,13 @@ fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) { ) }, navigationIcon = { - IconButton(onClick = navigateBack) { - Icon( - painter = painterResource(R.drawable.arrow_back_24px), - contentDescription = stringResource(id = R.string.accept_action_title), - modifier = Modifier.size(48.dp, 48.dp), - ) - } + IconButton(onClick = navigateBack) { + Icon( + painter = painterResource(R.drawable.arrow_back_24px), + contentDescription = stringResource(id = R.string.accept_action_title), + modifier = Modifier.size(48.dp, 48.dp), + ) + } }, ) }, diff --git a/app/src/main/java/com/kouros/navigation/ui/theme/Color.kt b/app/src/main/java/com/kouros/navigation/ui/theme/Color.kt index e347284..9370fca 100644 --- a/app/src/main/java/com/kouros/navigation/ui/theme/Color.kt +++ b/app/src/main/java/com/kouros/navigation/ui/theme/Color.kt @@ -2,7 +2,7 @@ package com.kouros.navigation.ui.theme import androidx.compose.ui.graphics.Color -val md_theme_light_primary = Color(0xFF825500) +val md_theme_light_primary = Color(0xFF6FE5E1) val md_theme_light_onPrimary = Color(0xFFFFFFFF) val md_theme_light_primaryContainer = Color(0xFFFFDDB3) val md_theme_light_onPrimaryContainer = Color(0xFF291800) @@ -33,7 +33,7 @@ val md_theme_light_surfaceTint = Color(0xFF825500) val md_theme_light_outlineVariant = Color(0xFFD3C4B4) val md_theme_light_scrim = Color(0xFF000000) -val md_theme_dark_primary = Color(0xFFFFB951) +val md_theme_dark_primary = Color(0xFF8CD6EF) val md_theme_dark_onPrimary = Color(0xFF452B00) val md_theme_dark_primaryContainer = Color(0xFF633F00) val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3) diff --git a/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt b/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt index 1319608..63ed86f 100644 --- a/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt +++ b/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt @@ -102,7 +102,7 @@ fun NavigationTheme( } MaterialTheme( - colorScheme = if (useDarkTheme) darkColorScheme() else colorScheme, + colorScheme = if (useDarkTheme) DarkColors else colorScheme, typography = typography, content = content, shapes = shapes, diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index 64a3b0e..c6b503a 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -33,7 +33,6 @@ import com.kouros.navigation.data.Constants.TILT import com.kouros.navigation.data.Constants.homeVogelhart import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.model.BaseStyleModel -import com.kouros.navigation.model.RouteModel import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.calculateTilt import com.kouros.navigation.utils.calculateZoom @@ -46,6 +45,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraState +import org.maplibre.compose.expressions.dsl.zoom import org.maplibre.compose.style.BaseStyle import org.maplibre.spatialk.geojson.Position @@ -261,6 +261,7 @@ class SurfaceRenderer( val paddingValues = getPaddingValues(height, viewStyle) val cameraState = cameraState(paddingValues, position, tilt) val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode) + val dark = darkMode == 1 || darkMode == 2 && carContext.isDarkMode MapLibre( cameraState, @@ -271,7 +272,7 @@ class SurfaceRenderer( speedCameras, showBuildings ) - ShowPosition(cameraState, position, paddingValues, darkMode) + ShowPosition(cameraState, position, paddingValues, dark) } /** @@ -283,18 +284,18 @@ class SurfaceRenderer( cameraState: CameraState, position: CameraPosition?, paddingValues: PaddingValues, - darkMode: Int + darkMode: Boolean ) { val cameraDuration = duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing) val currentSpeed: Float? by speed.observeAsState() - val speed: Int? by maxSpeed.observeAsState() + val maximumSpeed: Int? by maxSpeed.observeAsState() val streetName: String? by street.observeAsState() if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) { DrawNavigationImages( paddingValues, currentSpeed, - speed!!, + maximumSpeed!!, width, height, streetName, @@ -340,7 +341,7 @@ class SurfaceRenderer( updateCameraPosition( cameraPosition.value!!.bearing, newZoom, - cameraPosition.value!!.target, + cameraPosition.value!!.target, tilt ) } } @@ -379,7 +380,7 @@ class SurfaceRenderer( updateCameraPosition( bearing, zoom, - Position(location.longitude, location.latitude) + Position(location.longitude, location.latitude), tilt ) lastBearing = cameraPosition.value!!.bearing lastLocation = location @@ -387,11 +388,19 @@ class SurfaceRenderer( } } + /** + * Sets route data for active navigation and switches to VIEW mode. + */ + fun setRouteData() { + routeData.value = routeModel.curRoute.routeGeoJson + viewStyle = ViewStyle.VIEW + } + /** * Updates camera position with new bearing, zoom, and target. * Posts update to LiveData for UI observation. */ - private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) { + private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position, tilt: Double) { synchronized(this) { cameraPosition.postValue( cameraPosition.value!!.copy( @@ -408,9 +417,15 @@ class SurfaceRenderer( /** * Sets route data for active navigation and switches to VIEW mode. */ - fun setRouteData() { - routeData.value = routeModel.curRoute.routeGeoJson + fun clearRouteData() { + routeData.value = "" viewStyle = ViewStyle.VIEW + cameraPosition.postValue( + cameraPosition.value!!.copy( + zoom = 16.0 + ) + ) + tilt = TILT } /** @@ -424,18 +439,21 @@ class SurfaceRenderer( * Sets up route preview mode with overview camera position. * Calculates appropriate zoom based on route distance. */ - fun setPreviewRouteData(routeModel: RouteModel) { + fun setPreviewRouteData(routeModel: RouteCarModel) { viewStyle = ViewStyle.PREVIEW with(routeModel) { routeData.value = curRoute.routeGeoJson centerLocation = curRoute.centerLocation previewDistance = curRoute.summary.distance } + tilt = 0.0 updateCameraPosition( 0.0, - previewZoom(previewDistance), - Position(centerLocation.longitude, centerLocation.latitude) + previewZoom(centerLocation, previewDistance), + Position(centerLocation.longitude, centerLocation.latitude), 0.0 + ) + } /** @@ -448,7 +466,7 @@ class SurfaceRenderer( updateCameraPosition( 0.0, 14.0, - target = Position(location.longitude, location.latitude) + target = Position(location.longitude, location.latitude), tilt ) } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt index fb2c0ab..0a4dcfe 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicText import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -19,6 +20,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.drawText @@ -33,6 +35,8 @@ import com.kouros.navigation.data.Constants import com.kouros.navigation.data.NavigationColor import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.SpeedColor +import com.kouros.navigation.utils.isMetricSystem +import com.kouros.navigation.utils.location import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.rememberCameraState @@ -59,6 +63,7 @@ import org.maplibre.compose.sources.Source import org.maplibre.compose.sources.getBaseSource import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.style.BaseStyle +import org.maplibre.spatialk.geojson.BoundingBox import org.maplibre.spatialk.geojson.Position @@ -296,7 +301,7 @@ fun DrawNavigationImages( width: Int, height: Int, streetName: String?, - darkMode: Int, + darkMode: Boolean, ) { NavigationImage(padding, width, height, streetName, darkMode) if (speed != null) { @@ -314,7 +319,7 @@ fun NavigationImage( width: Int, height: Int, streetName: String?, - darkMode: Int + darkMode: Boolean ) { val imageSize = (height / 8) @@ -323,9 +328,9 @@ fun NavigationImage( val textMeasurerStreet = rememberTextMeasurer() val street = streetName.toString() val styleStreet = TextStyle( - fontSize = 14.sp, + fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = if (darkMode == 1) Color.White else navigationColor, + color = if (darkMode) Color.White else navigationColor, ) val textLayoutStreet = remember(street) { textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis) @@ -348,28 +353,31 @@ fun NavigationImage( .size(imageSize.dp, imageSize.dp) .scale(scaleX = 1f, scaleY = 0.7f), ) + Canvas( modifier = Modifier - .size((width / 5).dp, (height/4).dp) + .size(textLayoutStreet.size.width.dp, textLayoutStreet.size.height.dp * 6 ) ) { - if (!streetName.isNullOrEmpty()) { + if (street.isNotEmpty()) { + val topLeftX = center.x - textLayoutStreet.size.width / 2 + val topLeftY = center.y + textLayoutStreet.size.height drawRoundRect( topLeft = Offset( - x = center.x - textLayoutStreet.size.width / 2 , - y = center.y + textLayoutStreet.size.height, + x = topLeftX , + y = topLeftY, ), - color = if (darkMode == 1) navigationColor else Color.White, + color = if (darkMode) navigationColor else Color.White, cornerRadius = CornerRadius(x = 10f, y = 10f), ) drawText( textMeasurer = textMeasurerStreet, - text = streetName, + text = street, overflow = TextOverflow.Ellipsis, maxLines = 1, style = styleStreet, topLeft = Offset( - x = center.x - textLayoutStreet.size.width / 2, - y = center.y + textLayoutStreet.size.height + 10, + x = topLeftX, + y = topLeftY + 10, ) ) } @@ -384,6 +392,7 @@ private fun CurrentSpeed( curSpeed: Float, maxSpeed: Int ) { + val radius = 34 Box( modifier = Modifier @@ -395,9 +404,11 @@ private fun CurrentSpeed( ) { val textMeasurerSpeed = rememberTextMeasurer() val textMeasurerKm = rememberTextMeasurer() - val speed = (curSpeed * 3.6).toInt().toString() - val kmh = "km/h" + val speed = if (isMetricSystem()) (curSpeed * 3.6).toInt().toString() else (curSpeed * 3.6 * 0.6214).toInt().toString() + + val kmh = if (isMetricSystem()) "km/h" else "mph" + val styleSpeed = TextStyle( fontSize = 22.sp, fontWeight = FontWeight.Bold, @@ -433,7 +444,7 @@ private fun CurrentSpeed( ) drawText( textMeasurer = textMeasurerKm, - text = "km/h", + text = kmh, style = styleKm, topLeft = Offset( x = center.x - textLayoutKm.size.width / 2, diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt index 3cb5c9a..0021673 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt @@ -17,30 +17,32 @@ class Simulation { lifecycleScope: LifecycleCoroutineScope, updateLocation: (Location) -> Unit ) { - val points = routeModel.curRoute.waypoints - if (points.isEmpty()) return - simulationJob?.cancel() - var lastLocation = Location(LocationManager.FUSED_PROVIDER) - var curBearing = 0f - simulationJob = lifecycleScope.launch { - for (point in points) { - val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply { - latitude = point[1] - longitude = point[0] - bearing = curBearing - speedAccuracyMetersPerSecond = 1.0f // ~1 m/s - speed = 13.0f // ~50 km/h - time = System.currentTimeMillis() - elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() + if (routeModel.navState.route.isRouteValid()) { + val points = routeModel.curRoute.waypoints + if (points.isEmpty()) return + simulationJob?.cancel() + var lastLocation = Location(LocationManager.FUSED_PROVIDER) + var curBearing = 0f + simulationJob = lifecycleScope.launch { + for (point in points) { + val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply { + latitude = point[1] + longitude = point[0] + bearing = curBearing + speedAccuracyMetersPerSecond = 1.0f // ~1 m/s + speed = 13.0f // ~50 km/h + time = System.currentTimeMillis() + elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() + } + curBearing = lastLocation.bearingTo(fakeLocation) + // Update your app's state as if a real GPS update occurred + updateLocation(fakeLocation) + // Wait before moving to the next point (e.g., every 1 second) + delay(1000) + lastLocation = fakeLocation } - curBearing = lastLocation.bearingTo(fakeLocation) - // Update your app's state as if a real GPS update occurred - updateLocation(fakeLocation) - // Wait before moving to the next point (e.g., every 2 seconds) - delay(1000) - lastLocation = fakeLocation + routeModel.stopNavigation() } - routeModel.stopNavigation() } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt index 7b59c55..674fc77 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt @@ -116,8 +116,9 @@ class CategoryScreen( } else { row.addText("${(it.distance / 1000).round(1)} km") } - if (category == Constants.CHARGING_STATION) { - row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}") + if (category == CHARGING_STATION) { + if (it.tags.socketType2 != null) + row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}") } else { row.addText(carText("${it.tags.openingHours}")) } @@ -134,7 +135,7 @@ class CategoryScreen( setResult( Place( name = name, - category = Constants.CHARGING_STATION, + category = CHARGING_STATION, latitude = it.lat, longitude = it.lon ) diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt index f3b1fbc..0cd422b 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt @@ -377,7 +377,6 @@ class NavigationScreen( */ private fun stopAction(): Action { return Action.Builder() - .setTitle(carContext.getString(R.string.stop_action_title)) .setIcon( CarIcon.Builder( IconCompat.createWithResource( @@ -531,6 +530,8 @@ class NavigationScreen( * Pushes the search screen and handles the search result. */ private fun startSearchScreen() { + navigationViewModel.recentPlaces.value = emptyList() + navigationViewModel.previewRoute.value = "" screenManager .pushForResult( SearchScreen( @@ -558,16 +559,22 @@ class NavigationScreen( * Loads a route to the specified place and sets it as the destination. */ fun navigateToPlace(place: Place) { + val preview = navigationViewModel.previewRoute.value navigationType = NavigationType.VIEW val location = location(place.longitude, place.latitude) navigationViewModel.saveRecent(carContext, place) currentNavigationLocation = location - navigationViewModel.loadRoute( - carContext, - surfaceRenderer.lastLocation, - location, - surfaceRenderer.carOrientation - ) + if (preview.isNullOrEmpty()) { + navigationViewModel.loadRoute( + carContext, + surfaceRenderer.lastLocation, + location, + surfaceRenderer.carOrientation + ) + } else { + routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex) + navigationViewModel.route.value = preview + } routeModel.navState = routeModel.navState.copy(destination = place) invalidate() } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt index fceb4da..303deb6 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt @@ -17,7 +17,6 @@ import androidx.car.app.model.Row import androidx.car.app.model.Template import androidx.core.graphics.drawable.IconCompat import androidx.lifecycle.Observer -import androidx.lifecycle.asLiveData import com.kouros.data.R import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.navigation.RouteCarModel @@ -27,58 +26,52 @@ import com.kouros.navigation.data.Constants.RECENT import com.kouros.navigation.data.Place import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.utils.getSettingsRepository +import com.kouros.navigation.utils.location +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking class PlaceListScreen( private val carContext: CarContext, private val surfaceRenderer: SurfaceRenderer, private val category: String, - private val navigationViewModel: NavigationViewModel + private val navigationViewModel: NavigationViewModel, + private val places: List ) : Screen(carContext) { - var places = listOf() - val observer = Observer> { newPlaces -> - places = newPlaces - invalidate() - } + val routeModel = RouteCarModel() - val observerAddress = Observer> { newContacts -> - places = newContacts - invalidate() + var place = Place() + + val previewObserver = Observer { route -> + if (route.isNotEmpty()) { + val repository = getSettingsRepository(carContext) + val routingEngine = runBlocking { repository.routingEngineFlow.first() } + routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) + routeModel.startNavigation(route) + surfaceRenderer.setPreviewRouteData(routeModel) + screenManager + .pushForResult( + RoutePreviewScreen( + carContext, + surfaceRenderer, + place, + navigationViewModel, + routeModel = routeModel + ) + ) { obj: Any? -> + if (obj != null) { + setResult(obj) + finish() + } + } + } } init { - if (category == RECENT) { - navigationViewModel.places.observe(this, observer) - } - if (category == CONTACTS) { - navigationViewModel.contactAddress.observe(this, observerAddress) - } - if (category == FAVORITES) { - navigationViewModel.favorites.observe(this, observer) - } loadPlaces() - } - - fun loadPlaces() { - if (category == RECENT) { - navigationViewModel.loadRecentPlaces( - carContext, - surfaceRenderer.lastLocation, - surfaceRenderer.carOrientation - ) - } - if (category == CONTACTS) { - navigationViewModel.loadContacts(carContext) - } - if (category == FAVORITES) { - navigationViewModel.loadFavorites( - carContext, - surfaceRenderer.lastLocation, - surfaceRenderer.carOrientation - ) - } + navigationViewModel.previewRoute.observe(this, previewObserver) } override fun onGetTemplate(): Template { @@ -91,10 +84,10 @@ class PlaceListScreen( "" } val row = Row.Builder() - // .setImage(contactIcon(it.avatar, it.category)) + // .setImage(contactIcon(it.avatar, it.category)) .setTitle("$street ${it.city}") .setOnClickListener { - val place = Place( + place = Place( 0, it.name, it.category, @@ -103,29 +96,22 @@ class PlaceListScreen( it.postalCode, it.city, it.street, - // avatar = null + // avatar = null + ) + val location = location(place.longitude, place.latitude) + navigationViewModel.loadPreviewRoute( + carContext, + surfaceRenderer.lastLocation, + location, + surfaceRenderer.carOrientation ) - screenManager - .pushForResult( - RoutePreviewScreen( - carContext, - surfaceRenderer, - place, - navigationViewModel - ) - ) { obj: Any? -> - if (obj != null) { - setResult(obj) - finish() - } - } } if (category != CONTACTS) { row.addText(SpannableString(" ").apply { setSpan( DistanceSpan.create( Distance.create( - (it.distance/1000).toDouble(), + (it.distance / 1000).toDouble(), Distance.UNIT_KILOMETERS ) ), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE @@ -184,4 +170,10 @@ class PlaceListScreen( } return CarIcon.Builder(IconCompat.createWithContentUri(avatar)).build() } + + fun loadPlaces() { + if (category == CONTACTS) { + navigationViewModel.loadContacts(carContext) + } + } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt index 3c7cc3b..e754f67 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt @@ -2,10 +2,12 @@ package com.kouros.navigation.car.screen import android.os.CountDownTimer import android.text.SpannableString +import androidx.activity.OnBackPressedCallback import androidx.annotation.DrawableRes import androidx.car.app.CarContext import androidx.car.app.CarToast import androidx.car.app.Screen +import androidx.car.app.constraints.ConstraintManager import androidx.car.app.model.Action import androidx.car.app.model.Action.FLAG_DEFAULT import androidx.car.app.model.ActionStrip @@ -20,88 +22,79 @@ import androidx.car.app.model.Row import androidx.car.app.model.Template import androidx.car.app.navigation.model.MapController import androidx.car.app.navigation.model.MapWithContentTemplate +import androidx.car.app.versioning.CarAppApiLevels import androidx.core.graphics.drawable.IconCompat -import androidx.lifecycle.Observer +import androidx.lifecycle.Lifecycle import com.kouros.data.R import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.navigation.NavigationUtils import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.data.Place +import com.kouros.navigation.data.route.Routes import com.kouros.navigation.model.NavigationViewModel -import com.kouros.navigation.utils.getSettingsRepository -import com.kouros.navigation.utils.location -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking +import com.kouros.navigation.model.RouteModel import java.math.BigDecimal import java.math.RoundingMode +import kotlin.math.min /** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */ class RoutePreviewScreen( carContext: CarContext, private var surfaceRenderer: SurfaceRenderer, private var destination: Place, - private val navigationViewModel: NavigationViewModel + private val navigationViewModel: NavigationViewModel, + private val routeModel: RouteCarModel ) : Screen(carContext) { private var isFavorite = false - val routeModel = RouteCarModel() - + val maxListItems: Int = 3 val navigationUtils = NavigationUtils(carContext) - val observer = Observer { route -> - if (route.isNotEmpty()) { - val repository = getSettingsRepository(carContext) - val routingEngine = runBlocking { repository.routingEngineFlow.first() } - routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) - routeModel.startNavigation(route) - surfaceRenderer.setPreviewRouteData(routeModel) + + private val backPressedCallback = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { invalidate() } } init { - navigationViewModel.previewRoute.observe(this, observer) - val location = location(destination.longitude, destination.latitude) - navigationViewModel.loadPreviewRoute( - carContext, - surfaceRenderer.lastLocation, - location, - surfaceRenderer.carOrientation - ) + carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback) } override fun onGetTemplate(): Template { - val navigateActionIcon: CarIcon = CarIcon.Builder( - IconCompat.createWithResource( - carContext, R.drawable.navigation_48px - ) - ).build() - val navigateAction = Action.Builder() - .setFlags(FLAG_DEFAULT) - .setIcon(navigateActionIcon) - .setOnClickListener { this.onNavigate() } - .build() + val itemListBuilder = ItemList.Builder() - var i = 0 - routeModel.route.routes.forEach { _ -> - itemListBuilder.addItem(createRow(i++, navigateAction)) + if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) { + val listLimit = min( + maxListItems, + carContext.getCarService(ConstraintManager::class.java) + .getContentLimit( + ConstraintManager.CONTENT_LIMIT_TYPE_LIST + ) + ) + var index = 0 + routeModel.route.routes.forEach { route -> + itemListBuilder.addItem(createRow(route, index++)) + } } + val header = Header.Builder() .setStartHeaderAction(Action.BACK) .setTitle(carContext.getString(R.string.route_preview)) - .addEndHeaderAction( + + if (routeModel.route.routes.size == 1) { + header.addEndHeaderAction( favoriteAction() ) - .addEndHeaderAction( + header.addEndHeaderAction( deleteFavoriteAction() ) - .build() - + } val message = if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) { - createRouteText(0) + createRouteText(routeModel.route.routes.first()) } else { CarText.Builder("Wait") .build() @@ -110,7 +103,7 @@ class RoutePreviewScreen( val timer = object : CountDownTimer(5000, 1000) { override fun onTick(millisUntilFinished: Long) {} override fun onFinish() { - onNavigate() + onNavigate(0) } } timer.start() @@ -118,18 +111,27 @@ class RoutePreviewScreen( val content = if (routeModel.route.routes.size > 1) { ListTemplate.Builder() - .setHeader(header) + .setHeader(header.build()) .setSingleList(itemListBuilder.build()) .build() } else { + val navigateActionIcon: CarIcon = CarIcon.Builder( + IconCompat.createWithResource( + carContext, R.drawable.navigation_48px + ) + ).build() + val navigateAction = Action.Builder() + .setFlags(FLAG_DEFAULT) + .setIcon(navigateActionIcon) + .setOnClickListener { this.onNavigate(0) } + .build() MessageTemplate.Builder( message ) - .setHeader(header) + .setHeader(header.build()) .addAction(navigateAction) .setLoading(message.toString() == "Wait") .build() - } return MapWithContentTemplate.Builder() .setContentTemplate(content) @@ -176,7 +178,7 @@ class RoutePreviewScreen( private fun deleteFavoriteAction(): Action = Action.Builder() .setOnClickListener { if (isFavorite) { - navigationViewModel.deleteFavorite(carContext,destination) + navigationViewModel.deleteFavorite(carContext, destination) } isFavorite = !isFavorite finish() @@ -192,10 +194,10 @@ class RoutePreviewScreen( ) .build() - private fun createRouteText(index: Int): CarText { - val time = routeModel.route.routes[index].summary.duration + private fun createRouteText(route: Routes): CarText { + val time = route.summary.duration val length = - BigDecimal((routeModel.route.routes[index].summary.distance) / 1000).setScale( + BigDecimal((route.summary.distance) / 1000).setScale( 1, RoundingMode.HALF_EVEN ) @@ -207,18 +209,43 @@ class RoutePreviewScreen( .build() } - private fun createRow(index: Int, action: Action): Row { - val route = createRouteText(index) - val titleText = "$index" - return Row.Builder() - .setTitle(route) - .setOnClickListener { onRouteSelected(index) } - .addText(titleText) - .addAction(action) + private fun createRow(route: Routes, index: Int): Row { + val navigateActionIcon: CarIcon = CarIcon.Builder( + IconCompat.createWithResource( + carContext, R.drawable.navigation_48px + ) + ).build() + val navigateAction = Action.Builder() + .setFlags(FLAG_DEFAULT) + .setIcon(navigateActionIcon) + .setOnClickListener { this.onNavigate(index) } .build() + + val routeText = createRouteText(route) + var street = "" + var maxDistance = 0.0 + routeModel.route.routes[index].legs.first().steps.forEach { + if (it.distance > maxDistance) { + maxDistance = it.distance + street = it.street + } + } + val delay = (route.summary.trafficDelay / 60).toInt().toString() + + val row = Row.Builder() + .setTitle(routeText) + .setOnClickListener { onRouteSelected(index) } + .addText(street) + .addAction(navigateAction) + + if (route.summary.trafficDelay > 60) { + row.addText("$delay min") + } + return row.build() } - private fun onNavigate() { + private fun onNavigate(index: Int) { + destination.routeIndex = index setResult(destination) finish() } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt index 1e62609..1df9fad 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt @@ -16,7 +16,9 @@ import com.kouros.data.R import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.data.Category -import com.kouros.navigation.data.Constants +import com.kouros.navigation.data.Constants.CATEGORIES +import com.kouros.navigation.data.Constants.FAVORITES +import com.kouros.navigation.data.Constants.RECENT import com.kouros.navigation.data.Place import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.model.NavigationViewModel @@ -30,11 +32,13 @@ class SearchScreen( var isSearchComplete: Boolean = false + var category = "" + var categories: List = listOf( - Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)), - // Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)), - Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)), - Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites)) + Category(id = RECENT, name = carContext.getString(R.string.recent_destinations)), + // Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)), + Category(id = CATEGORIES, name = carContext.getString(R.string.category_title)), + Category(id = FAVORITES, name = carContext.getString(R.string.favorites)) ) lateinit var searchResult: List @@ -44,8 +48,50 @@ class SearchScreen( invalidate() } + val observerRecentPlaces = Observer> { newPlaces -> + if (newPlaces.isNotEmpty()) { + screenManager + .pushForResult( + PlaceListScreen( + carContext, + surfaceRenderer, + RECENT, + navigationViewModel, + newPlaces + ) + ) { obj: Any? -> + surfaceRenderer.clearRouteData() + if (obj != null) { + setResult(obj) + finish() + } + } + } + } + + val observerFavorites = Observer> { newPlaces -> + screenManager + .pushForResult( + PlaceListScreen( + carContext, + surfaceRenderer, + FAVORITES, + navigationViewModel, + newPlaces + ) + ) { obj: Any? -> + if (obj != null) { + setResult(obj) + finish() + } + } + } + + init { navigationViewModel.searchPlaces.observe(this, observer) + navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) + navigationViewModel.favorites.observe(this, observerFavorites) } override fun onGetTemplate(): Template { @@ -62,7 +108,7 @@ class SearchScreen( .setTitle(it.name) .setImage(categoryIcon(it.id)) .setOnClickListener { - if (it.id == Constants.CATEGORIES) { + if (it.id == CATEGORIES) { screenManager .pushForResult( CategoriesScreen( @@ -78,20 +124,20 @@ class SearchScreen( } } } else { - screenManager - .pushForResult( - PlaceListScreen( - carContext, - surfaceRenderer, - it.id, - navigationViewModel - ) - ) { obj: Any? -> - if (obj != null) { - setResult(obj) - finish() - } - } + if (it.id == RECENT) { + navigationViewModel.loadRecentPlaces( + carContext, + surfaceRenderer.lastLocation, + surfaceRenderer.carOrientation + ) + } + if (it.id == FAVORITES) { + navigationViewModel.loadFavorites( + carContext, + surfaceRenderer.lastLocation, + surfaceRenderer.carOrientation + ) + } } } .setBrowsable(true) @@ -122,13 +168,15 @@ class SearchScreen( } fun categoryIcon(category: String?): CarIcon { - val resId : Int = when (category) { - Constants.RECENT -> { + val resId: Int = when (category) { + RECENT -> { R.drawable.ic_place_white_24dp } - Constants.FAVORITES -> { + + FAVORITES -> { R.drawable.ic_favorite_white_24dp } + else -> { R.drawable.navigation_48px } @@ -148,7 +196,7 @@ class SearchScreen( searchResult.forEach { searchItemListBuilder.addItem( Row.Builder() - .setTitle("${(it.distance/1000).toInt()} km ${it.displayName} ") + .setTitle("${(it.distance / 1000).toInt()} km ${it.displayName} ") .setOnClickListener { navigateToPlace(it) } diff --git a/common/data/src/main/java/com/kouros/navigation/data/Data.kt b/common/data/src/main/java/com/kouros/navigation/data/Data.kt index 11aead2..77f2128 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Data.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Data.kt @@ -43,7 +43,8 @@ data class Place( var street: String? = null, var distance: Float = 0F, //var avatar: Uri? = null, - var lastDate: Long = 0 + var lastDate: Long = 0, + var routeIndex: Int = 0 ) data class ContactData( diff --git a/common/data/src/main/java/com/kouros/navigation/data/Route.kt b/common/data/src/main/java/com/kouros/navigation/data/Route.kt index 69887da..f2d9033 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Route.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Route.kt @@ -23,12 +23,14 @@ data class Route( val routeEngine: Int, val routes: List, var currentStepIndex: Int = 0, + var routeIndex : Int = 0, ) { data class Builder( var routeEngine: Int = 0, var summary: Summary = Summary(), var routes: List = emptyList(), + var routeIndex : Int = 0 ) { fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine } @@ -38,6 +40,8 @@ data class Route( } fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine } + fun routeIndex(routeIndex: Int) = apply { this.routeIndex = routeIndex} + fun route(route: String) = apply { if (route.isNotEmpty() && route != "[]") { val gson = GsonBuilder().serializeNulls().create() @@ -68,6 +72,7 @@ data class Route( return Route( routeEngine = this.routeEngine, routes = this.routes, + routeIndex = this.routeIndex ) } @@ -75,6 +80,7 @@ data class Route( return Route( routeEngine = 0, routes = emptyList(), + routeIndex = 0 ) } } @@ -82,7 +88,7 @@ data class Route( fun legs(): List { return if (routes.isNotEmpty()) { - routes.first().legs + routes[routeIndex].legs } else { emptyList() } diff --git a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt index 6a81463..227e22a 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt @@ -58,6 +58,7 @@ class TomTomRepository : NavigationRepository() { "&vehicleMaxSpeed=120&vehicleCommercial=false" + "&instructionsType=text&language=$language§ionType=lanes" + "&routeRepresentation=encodedPolyline" + + "&maxAlternatives=2" + "&vehicleEngineType=combustion$filter&key=$tomtomApiKey" return fetchUrl( url, diff --git a/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt index 80bf786..d0f02b5 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt @@ -3,10 +3,8 @@ package com.kouros.navigation.model //import com.kouros.navigation.data.Preferences.boxStore import android.content.Context import android.location.Location -import android.util.Log import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList -import androidx.compose.ui.platform.isDebugInspectorInfoEnabled import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -58,7 +56,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo } /** LiveData containing list of recent navigation destinations */ - val places: MutableLiveData> by lazy { + val recentPlaces: MutableLiveData> by lazy { MutableLiveData() } @@ -144,11 +142,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo val settingsRepository = getSettingsRepository(context) val rp = settingsRepository.recentPlacesFlow.first() val gson = GsonBuilder().serializeNulls().create() - val recentPlaces = gson.fromJson(rp, Places::class.java) + val places = gson.fromJson(rp, Places::class.java) val pl = mutableListOf() var id: Long = 0 if (rp.isNotEmpty()) { - for (place in recentPlaces.places) { + for (place in places.places) { if (place.category.equals(Constants.RECENT)) { val plLocation = location(place.longitude, place.latitude) if (place.latitude != 0.0) { @@ -167,7 +165,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo } } } - places.postValue(pl) + recentPlaces.postValue(pl) } catch (e: Exception) { e.printStackTrace() } diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt index 0af684b..b4c6f3a 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt @@ -34,6 +34,7 @@ open class RouteModel { route = Route.Builder() .routeEngine(navState.routingEngine) .route(routeString) + .routeIndex(navState.currentRouteIndex) .build() ) if (hasLegs()) { @@ -42,7 +43,7 @@ open class RouteModel { } fun hasLegs(): Boolean { - return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty() + return navState.route.routes.isNotEmpty() && navState.route.routes[navState.currentRouteIndex].legs.isNotEmpty() } fun stopNavigation() { diff --git a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt index 6b1a763..cc751c2 100644 --- a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt +++ b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt @@ -9,8 +9,11 @@ import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.model.NavigationViewModel +import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking +import java.lang.Math.toDegrees +import java.lang.Math.toRadians import java.time.LocalDateTime import java.time.ZoneId import java.time.ZoneOffset @@ -19,6 +22,8 @@ import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.Locale import kotlin.math.absoluteValue +import kotlin.math.cos +import kotlin.math.ln import kotlin.math.pow import kotlin.math.roundToInt import kotlin.time.Duration @@ -29,7 +34,7 @@ object NavigationUtils { fun getViewModel(context: Context): NavigationViewModel { val repository = getSettingsRepository(context) - val routeEngine = runBlocking { repository.routingEngineFlow.first() } + val routeEngine = runBlocking { repository.routingEngineFlow.first() } return when (routeEngine) { RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository()) RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository()) @@ -53,15 +58,36 @@ fun calculateZoom(speed: Double?): Double { return zoom } -fun previewZoom(previewDistance: Double): Double { - when (previewDistance / 1000) { - in 0.0..10.0 -> return 13.5 - in 10.0..20.0 -> return 11.5 - in 20.0..30.0 -> return 10.5 - } - return 9.5 +fun previewZoom(centerLocation: Location, previewDistance: Double): Double { + return calculateZoomFromBoundingBox(centerLocation, previewDistance / 1000) } + +fun calculateZoomFromBoundingBox(centerLocation: Location, previewDistance: Double): Double { + + val earthRadius = 6371.0 + val maxLat = centerLocation.latitude + toDegrees(previewDistance / earthRadius) + val minLat = centerLocation.latitude - toDegrees(previewDistance / earthRadius) + val maxLon = centerLocation.longitude + toDegrees(previewDistance / earthRadius / cos(toRadians(centerLocation.latitude))) + val minLon = centerLocation.longitude - toDegrees(previewDistance / earthRadius / cos(toRadians(centerLocation.latitude))) + + var zoomLevel: Double + val latDiff = maxLat - minLat + val lngDiff = maxLon - minLon + + val maxDiff = if(lngDiff > latDiff) lngDiff else latDiff + + if (maxDiff < 360 / 2.0.pow(20.0)) { + zoomLevel = 21.0 + } else { + zoomLevel = (-1 * ((ln(maxDiff) / ln(2.0)) - (ln(360.0) / ln(2.0)))); + if (zoomLevel < 1) + zoomLevel = 1.0; + } + return zoomLevel + 1.2 +} + + fun calculateTilt(newZoom: Double, tilt: Double): Double = if (newZoom < 13) { 0.0 @@ -119,10 +145,10 @@ fun isMetricSystem(): Boolean { return !setOf("US", "UK", "LR", "MM").contains(country) } -fun formattedDistance(distanceMode : Int, distance: Double): Pair { +fun formattedDistance(distanceMode: Int, distance: Double): Pair { var currentDistance = distance var displayUnit: Int - if (distanceMode == 1 || distanceMode == 0 && isMetricSystem()) { + if (distanceMode == 1 || distanceMode == 0 && isMetricSystem()) { displayUnit = if (currentDistance > 1000.0) { currentDistance /= 1000.0 Distance.UNIT_KILOMETERS diff --git a/common/data/src/main/res/raw/tomom_routing.json b/common/data/src/main/res/raw/tomom_routing.json index 3e93a2b..7b70deb 100644 --- a/common/data/src/main/res/raw/tomom_routing.json +++ b/common/data/src/main/res/raw/tomom_routing.json @@ -24,7 +24,7 @@ }, { "key": "departAt", - "value": "2026-03-04T14:38:14.253Z" + "value": "2026-03-12T09:44:05.900Z" }, { "key": "guidanceVersion", @@ -44,15 +44,15 @@ }, { "key": "locations", - "value": "48.18575,11.57937:48.11648,11.59432" + "value": "48.18610,11.57923:48.11654,11.59449" }, { "key": "maxAlternatives", - "value": "0" + "value": "2" }, { "key": "maxPathAlternatives", - "value": "0" + "value": "2" }, { "key": "routeRepresentation", @@ -64,11 +64,11 @@ }, { "key": "sectionType", - "value": "lanes" + "value": "traffic" }, { "key": "sectionType", - "value": "traffic" + "value": "lanes" }, { "key": "traffic", @@ -119,46 +119,28 @@ "routes": [ { "summary": { - "lengthInMeters": 11122, - "travelTimeInSeconds": 1483, - "trafficDelayInSeconds": 175, - "trafficLengthInMeters": 2191, - "departureTime": "2026-03-04T15:38:14+01:00", - "arrivalTime": "2026-03-04T16:02:57+01:00" + "lengthInMeters": 10982, + "travelTimeInSeconds": 1142, + "trafficDelayInSeconds": 0, + "trafficLengthInMeters": 0, + "departureTime": "2026-03-12T10:44:06+01:00", + "arrivalTime": "2026-03-12T11:03:08+01:00" }, "legs": [ { "summary": { - "lengthInMeters": 11122, - "travelTimeInSeconds": 1483, - "trafficDelayInSeconds": 175, - "trafficLengthInMeters": 2191, - "departureTime": "2026-03-04T15:38:14+01:00", - "arrivalTime": "2026-03-04T16:02:57+01:00" + "lengthInMeters": 10982, + "travelTimeInSeconds": 1142, + "trafficDelayInSeconds": 0, + "trafficLengthInMeters": 0, + "departureTime": "2026-03-12T10:44:06+01:00", + "arrivalTime": "2026-03-12T11:03:08+01:00" }, - "encodedPolyline": "sfbeH_rteAE|DQEy@GQ?wDQFkEH{G?M?sA@kB?_FAeC?o@?[@_@\\Ab@CVAz@CJA@?dBGhAGjAExAGlBEvBKdCKTAfCKLAv@ELA|AGnAGt@ClCKjDQpBIf@BXDPDPBF@ZB?S@SAYAUKi@Go@Cc@BgBBs@Bg@JyAJiAXqBDWNs@TaA\\mAFa@Po@`BwF?YL]FSFSl@iB^kALc@L]Ro@f@yAf@{AFQNe@dAoCdBgEx@qBTi@BITe@L[L_@^}@HSXu@pB}El@eAb@e@f@[h@QZCRAL?j@HRFh@Vf@XLJhAn@lAv@TLtAz@r@`@bAn@pAv@f@X~@j@z@h@vBpA`@VHDFFJFf@X`CzApAh@f@L`@Fz@@f@AXEVEVEhA[h@Yn@e@PQFEJKRWV[PW`@w@t@}AHQN]~BiFP]`AoBh@aADGTa@t@aAt@{@PQJKJGFG@Cd@]XSxDmCf@a@n@o@TY\\g@LQHMJSLUP[f@iAPg@b@yAFODMNi@FS|@qCVaAHUHUn@wBHYh@eBpAkEjBiGfAeDj@yADMFQd@sAf@kAJUh@qAf@eAt@sAn@iALSN[p@kAVc@JOLSj@w@z@}@x@q@pAu@p@_@j@Sl@MLCRCb@E`@?^?L@`ABz@?N@~AFdADJ@rAH`DVpCVrAJd@BfHp@zGl@pAJ|ALnGp@jEh@fBJpAFF?P@N@ZCtC]r@GJCFCLCD?TEVEXGhAYzAg@NGv@]`@QJEvB_AXMVK\\Qb@Qn@QJCNAZC^ENA`@FnBb@xEtA^H^JnCl@z@r@`@Pr@TtBlA~C`Bn@\\xAl@PF`@LrAVlCh@bBLl@BlBJdG\\RDjCHn@?pB?xB?R@`@@GxAC^?ZInBIfCAvC?r@@dD@n@@b@@^D`C?TDxAFbBHdB@VHp@RjAJb@NNH`@VlBFf@PzARhBFd@@LRbBFh@\\nC@FNhAb@lEj@tDPpABTBRZlBTdBXjBn@xEBLDTRpAR~@l@jDj@Qv@I~ER", + "encodedPolyline": "ijbeHqlteAeDOFkEH{G?M?sA@kB?_FAeC?o@?[@_@\\Ab@CVAz@CJA@?dBGhAGjAExAGlBEvBKdCKTAfCKLAv@ELA|AGnAGt@ClCKjDQpBIf@BXDPDPBF@ZB?S@SAYAUKi@Go@Cc@BgBBs@Bg@JyAJiAXqBDWNs@TaA\\mAFa@Po@`BwF?YL]FSFSl@iB^kALc@L]Ro@f@yAf@{AFQNe@dAoCdBgEx@qBTi@BITe@L[L_@^}@HSXu@pB}El@eAb@e@f@[h@QZCRAL?j@HRFh@Vf@XLJhAn@lAv@TLtAz@r@`@bAn@pAv@f@X~@j@z@h@vBpA`@VHDFFJFf@X`CzApAh@f@L`@Fz@@f@AXEVEVEhA[h@Yn@e@PQFEJKRWV[PW`@w@t@}AHQN]~BiFP]`AoBh@aADGTa@t@aAt@{@PQJKJGFG@Cd@]XSxDmCf@a@n@o@TY\\g@LQHMJSLUP[f@iAPg@b@yAFODMNi@FS|@qCVaAHUHUn@wBHYh@eBpAkEjBiGfAeDj@yADMFQd@sAf@kAJUh@qAf@eAt@sAn@iALSN[p@kAVc@JOLSj@w@z@}@x@q@pAu@p@_@j@Sl@MLCRCb@E`@?^?L@`ABz@?N@~AFdADJ@rAH`DVpCVrAJd@BfHp@zGl@pAJ|ALnGp@jEh@fBJpAFF?P@N@ZCtC]r@GJCFCLCD?TEVEXGhAYzAg@NGv@]`@QJEvB_AXMVK\\Qb@Qn@QJCNAZC^ENA`@FnBb@xEtA^H^JnCl@z@r@`@Pr@TtBlA~C`Bn@\\xAl@PF`@LrAVlCh@bBLl@BlBJdG\\RDjCHn@?pB?xB?R@`@@GxAC^?ZInBIfCAvC?r@@dD@n@@b@@^D`C?TDxAFbBHdB@VHp@RjAJb@NNH`@VlBFf@PzARhBFd@@LRbBFh@\\nC@FNhAb@lEj@tDPpABTBRZlBTdBXjBn@xEBLDTRpAR~@l@jDj@Qv@IrEP", "encodedPolylinePrecision": 5 } ], "sections": [ - { - "startPointIndex": 66, - "endPointIndex": 143, - "sectionType": "TRAFFIC", - "simpleCategory": "JAM", - "effectiveSpeedInKmh": 26, - "delayInSeconds": 175, - "magnitudeOfDelay": 1, - "tec": { - "causes": [ - { - "mainCauseCode": 1 - } - ], - "effectCode": 4 - }, - "eventId": "TTL41056710392017000" - }, { "lanes": [ { @@ -190,8 +172,8 @@ "LONG_DASHED", "SINGLE_SOLID" ], - "startPointIndex": 42, - "endPointIndex": 45, + "startPointIndex": 38, + "endPointIndex": 41, "sectionType": "LANES" }, { @@ -213,8 +195,8 @@ "SHORT_DASHED", "SINGLE_SOLID" ], - "startPointIndex": 61, - "endPointIndex": 62, + "startPointIndex": 57, + "endPointIndex": 58, "sectionType": "LANES" }, { @@ -243,8 +225,8 @@ "SHORT_DASHED", "SINGLE_SOLID" ], - "startPointIndex": 74, - "endPointIndex": 75, + "startPointIndex": 70, + "endPointIndex": 71, "sectionType": "LANES" }, { @@ -266,8 +248,8 @@ "LONG_DASHED", "SINGLE_SOLID" ], - "startPointIndex": 265, - "endPointIndex": 266, + "startPointIndex": 261, + "endPointIndex": 262, "sectionType": "LANES" }, { @@ -301,8 +283,8 @@ "SINGLE_SOLID", "SINGLE_SOLID" ], - "startPointIndex": 287, - "endPointIndex": 288, + "startPointIndex": 283, + "endPointIndex": 284, "sectionType": "LANES" }, { @@ -337,8 +319,8 @@ "SHORT_DASHED", "SINGLE_SOLID" ], - "startPointIndex": 302, - "endPointIndex": 304, + "startPointIndex": 298, + "endPointIndex": 300, "sectionType": "LANES" } ], @@ -348,44 +330,26 @@ "routeOffsetInMeters": 0, "travelTimeInSeconds": 0, "point": { - "latitude": 48.18554, - "longitude": 11.57936 + "latitude": 48.18613, + "longitude": 11.57849 }, "pointIndex": 0, "instructionType": "LOCATION_DEPARTURE", - "street": "Vogelhartstraße", + "street": "Silcherstraße", "countryCode": "DEU", "possibleCombineWithNext": false, "drivingSide": "RIGHT", "maneuver": "DEPART", - "message": "Abfahrt von Vogelhartstraße" + "message": "Abfahrt von Silcherstraße" }, { - "routeOffsetInMeters": 71, - "travelTimeInSeconds": 16, - "point": { - "latitude": 48.18557, - "longitude": 11.57841 - }, - "pointIndex": 1, - "instructionType": "TURN", - "street": "Silcherstraße", - "countryCode": "DEU", - "junctionType": "REGULAR", - "turnAngleInDecimalDegrees": 90, - "possibleCombineWithNext": false, - "drivingSide": "RIGHT", - "maneuver": "TURN_RIGHT", - "message": "Biegen Sie rechts ab auf Silcherstraße" - }, - { - "routeOffsetInMeters": 225, - "travelTimeInSeconds": 61, + "routeOffsetInMeters": 92, + "travelTimeInSeconds": 20, "point": { "latitude": 48.18696, "longitude": 11.57857 }, - "pointIndex": 5, + "pointIndex": 1, "instructionType": "TURN", "street": "Schmalkaldener Straße", "countryCode": "DEU", @@ -397,13 +361,13 @@ "message": "Biegen Sie rechts ab auf Schmalkaldener Straße" }, { - "routeOffsetInMeters": 657, - "travelTimeInSeconds": 135, + "routeOffsetInMeters": 524, + "travelTimeInSeconds": 94, "point": { "latitude": 48.18686, "longitude": 11.58437 }, - "pointIndex": 15, + "pointIndex": 11, "instructionType": "TURN", "roadNumbers": [ "B13" @@ -418,13 +382,13 @@ "message": "Biegen Sie rechts ab auf Ingolstädter Straße/B13" }, { - "routeOffsetInMeters": 1719, - "travelTimeInSeconds": 291, + "routeOffsetInMeters": 1587, + "travelTimeInSeconds": 256, "point": { "latitude": 48.17733, "longitude": 11.58503 }, - "pointIndex": 45, + "pointIndex": 41, "instructionType": "TURN", "roadNumbers": [ "B2R" @@ -440,13 +404,13 @@ "combinedMessage": "Biegen Sie links ab auf Schenkendorfstraße/B2R dann bleiben Sie bei Schenkendorfstraße/B2R Richtung Messe / ICM links" }, { - "routeOffsetInMeters": 2074, - "travelTimeInSeconds": 333, + "routeOffsetInMeters": 1941, + "travelTimeInSeconds": 296, "point": { "latitude": 48.17678, "longitude": 11.58957 }, - "pointIndex": 62, + "pointIndex": 58, "instructionType": "TURN", "roadNumbers": [ "B2R" @@ -463,13 +427,13 @@ "combinedMessage": "Bleiben Sie bei Schenkendorfstraße/B2R Richtung Messe / ICM links dann bleiben Sie bei Schenkendorfstraße/B2R Richtung Passau links" }, { - "routeOffsetInMeters": 2425, - "travelTimeInSeconds": 371, + "routeOffsetInMeters": 2293, + "travelTimeInSeconds": 317, "point": { "latitude": 48.17518, "longitude": 11.59363 }, - "pointIndex": 75, + "pointIndex": 71, "instructionType": "TURN", "roadNumbers": [ "B2R" @@ -485,13 +449,13 @@ "message": "Bleiben Sie bei Schenkendorfstraße/B2R Richtung Passau links" }, { - "routeOffsetInMeters": 2780, - "travelTimeInSeconds": 436, + "routeOffsetInMeters": 2648, + "travelTimeInSeconds": 338, "point": { "latitude": 48.17329, "longitude": 11.59747 }, - "pointIndex": 86, + "pointIndex": 82, "instructionType": "DIRECTION_INFO", "roadNumbers": [ "B2R" @@ -505,13 +469,13 @@ "message": "Folgen Sie Isarring/B2R Richtung München-Ost" }, { - "routeOffsetInMeters": 8431, - "travelTimeInSeconds": 1014, + "routeOffsetInMeters": 8299, + "travelTimeInSeconds": 729, "point": { "latitude": 48.13017, "longitude": 11.6154 }, - "pointIndex": 266, + "pointIndex": 262, "instructionType": "TURN", "street": "Ampfingstraße", "countryCode": "DEU", @@ -523,13 +487,13 @@ "message": "Halten Sie sich rechts bei Ampfingstraße" }, { - "routeOffsetInMeters": 9494, - "travelTimeInSeconds": 1197, + "routeOffsetInMeters": 9361, + "travelTimeInSeconds": 879, "point": { "latitude": 48.12089, "longitude": 11.61285 }, - "pointIndex": 288, + "pointIndex": 284, "instructionType": "TURN", "street": "Anzinger Straße", "countryCode": "DEU", @@ -542,13 +506,13 @@ "combinedMessage": "Biegen Sie rechts ab auf Anzinger Straße dann fahren Sie geradeaus weiter auf Sankt-Martin-Straße" }, { - "routeOffsetInMeters": 9990, - "travelTimeInSeconds": 1286, + "routeOffsetInMeters": 9857, + "travelTimeInSeconds": 968, "point": { "latitude": 48.12087, "longitude": 11.60621 }, - "pointIndex": 304, + "pointIndex": 300, "instructionType": "TURN", "street": "Sankt-Martin-Straße", "countryCode": "DEU", @@ -560,13 +524,13 @@ "message": "Fahren Sie geradeaus weiter auf Sankt-Martin-Straße" }, { - "routeOffsetInMeters": 10940, - "travelTimeInSeconds": 1440, + "routeOffsetInMeters": 10807, + "travelTimeInSeconds": 1100, "point": { "latitude": 48.11811, "longitude": 11.59417 }, - "pointIndex": 335, + "pointIndex": 331, "instructionType": "TURN", "street": "Hohenwaldeckstraße", "countryCode": "DEU", @@ -579,13 +543,13 @@ "combinedMessage": "Biegen Sie links ab auf Hohenwaldeckstraße dann sie haben Hohenwaldeckstraße erreicht. Ihr Ziel liegt auf der linken Seite" }, { - "routeOffsetInMeters": 11122, - "travelTimeInSeconds": 1483, + "routeOffsetInMeters": 10982, + "travelTimeInSeconds": 1142, "point": { - "latitude": 48.11649, - "longitude": 11.59421 + "latitude": 48.11655, + "longitude": 11.59422 }, - "pointIndex": 338, + "pointIndex": 334, "instructionType": "LOCATION_ARRIVAL", "street": "Hohenwaldeckstraße", "countryCode": "DEU", @@ -598,27 +562,921 @@ "instructionGroups": [ { "firstInstructionIndex": 0, - "lastInstructionIndex": 3, - "groupMessage": "Abfahrt von Vogelhartstraße. Fahren Sie auf die Ingolstädter Straße/B13", - "groupLengthInMeters": 1719 + "lastInstructionIndex": 2, + "groupMessage": "Abfahrt von Silcherstraße. Fahren Sie auf die Ingolstädter Straße/B13", + "groupLengthInMeters": 1587 }, { - "firstInstructionIndex": 4, - "lastInstructionIndex": 7, + "firstInstructionIndex": 3, + "lastInstructionIndex": 6, "groupMessage": "Fahren Sie auf die Schenkendorfstraße, Isarring/B2R in Richtung Messe / ICM, Passau, München-Ost", "groupLengthInMeters": 6712 }, { - "firstInstructionIndex": 8, - "lastInstructionIndex": 10, + "firstInstructionIndex": 7, + "lastInstructionIndex": 9, "groupMessage": "Fahren Sie auf die Ampfingstraße, Anzinger Straße, Sankt-Martin-Straße", - "groupLengthInMeters": 2509 + "groupLengthInMeters": 2508 }, { - "firstInstructionIndex": 11, - "lastInstructionIndex": 12, + "firstInstructionIndex": 10, + "lastInstructionIndex": 11, "groupMessage": "Fahren Sie zu Ihrem Ziel in der Hohenwaldeckstraße", - "groupLengthInMeters": 182 + "groupLengthInMeters": 175 + } + ] + } + }, + { + "summary": { + "lengthInMeters": 9794, + "travelTimeInSeconds": 1779, + "trafficDelayInSeconds": 0, + "trafficLengthInMeters": 0, + "departureTime": "2026-03-12T10:44:06+01:00", + "arrivalTime": "2026-03-12T11:13:45+01:00", + "deviationDistance": 1587, + "deviationTime": 256, + "deviationPoint": { + "latitude": 48.17733, + "longitude": 11.58503 + } + }, + "legs": [ + { + "summary": { + "lengthInMeters": 9794, + "travelTimeInSeconds": 1779, + "trafficDelayInSeconds": 0, + "trafficLengthInMeters": 0, + "departureTime": "2026-03-12T10:44:06+01:00", + "arrivalTime": "2026-03-12T11:13:45+01:00" + }, + "encodedPolyline": "ijbeHqlteAeDOFkEH{G?M?sA@kB?_FAeC?o@?[@_@\\Ab@CVAz@CJA@?dBGhAGjAExAGlBEvBKdCKTAfCKLAv@ELA|AGnAGt@ClCKjDQpBIf@BXDPDPBF@ZBH@J?P?^Cz@Kv@GbAQ`@KtA[|AMpBKXATAhAGn@Ef@CvBK|@EfBGtBG^Ax@EpBMfDOJArBKnAAVAx@?pC@dDQbAGzDMN?`DQP@P?J?p@@hABVBvCNX@fABh@Bf@AjAFD?n@F|A`@f@VbBn@nBv@fC~@j@N^PzDtAnBr@dA^DBD@tBx@XHhA`@pDtAj@TfA^d@Ph@RrB~@DDFFd@f@HFb@N|@CRBhBp@d@NJDnAb@nBv@h@Rl@TjBt@\\JjC|@jAd@`DnAx@ZvChAfC|@FBFBFBD[NkADYFm@TeBHk@Jy@J{@BMLwAb@uDLiBB_@@S@IBOBWHk@@QHe@BODIBIDEBCHG^EL?TCNFzChA`DnA|Al@~@ZLDF@D@H@HAVGJIHIVYdCmCf@g@tAyAXUXIL@NBZJdAXb@HVB^DrANbAVdCj@`@L^PFDPJXNt@l@HLh@l@LTNXFJPd@Nb@Rh@FTJB??@?BDBFDHFFB@F?f@o@POROrAcANI|CqBHIHKNQLYn@cB|@gCNi@DORk@\\iABGBIXiADa@BUDa@V}BFk@@O?Y?YEwAAWASBMIqBEo@DY@CH_@HINST[X]|@oAv@kArAwBRYbB{CdAmBVa@R]d@s@Zi@FIFIFIDGPWd@}@JBB?H?LAPIjAe@jDuAdAe@TIhAo@tCmB|@g@POpA}@\\Uv@g@RUtBwBREX[NMRQ\\[HQRMVQ|DqBl@e@n@a@p@]n@_@p@]JGnBeAj@tDPpABTBRZlBTdBXjBn@xEBLDTRpAR~@l@jDj@Qv@IrEP", + "encodedPolylinePrecision": 5 + } + ], + "sections": [ + { + "lanes": [ + { + "directions": [ + "LEFT" + ], + "follow": "LEFT" + }, + { + "directions": [ + "LEFT" + ], + "follow": "LEFT" + }, + { + "directions": [ + "STRAIGHT" + ] + }, + { + "directions": [ + "RIGHT" + ] + } + ], + "laneSeparators": [ + "SINGLE_SOLID", + "LONG_DASHED", + "SINGLE_SOLID", + "LONG_DASHED", + "SINGLE_SOLID" + ], + "startPointIndex": 132, + "endPointIndex": 133, + "sectionType": "LANES" + }, + { + "lanes": [ + { + "directions": [ + "LEFT" + ] + }, + { + "directions": [ + "LEFT" + ] + }, + { + "directions": [ + "RIGHT" + ], + "follow": "RIGHT" + } + ], + "laneSeparators": [ + "SINGLE_SOLID", + "LONG_DASHED", + "SHORT_DASHED", + "SINGLE_SOLID" + ], + "startPointIndex": 156, + "endPointIndex": 162, + "sectionType": "LANES" + }, + { + "lanes": [ + { + "directions": [ + "STRAIGHT" + ], + "follow": "STRAIGHT" + }, + { + "directions": [ + "STRAIGHT" + ] + }, + { + "directions": [ + "RIGHT" + ] + } + ], + "laneSeparators": [ + "SINGLE_SOLID", + "LONG_DASHED", + "SHORT_DASHED", + "SINGLE_SOLID" + ], + "startPointIndex": 207, + "endPointIndex": 208, + "sectionType": "LANES" + }, + { + "lanes": [ + { + "directions": [ + "STRAIGHT" + ] + }, + { + "directions": [ + "SLIGHT_RIGHT" + ], + "follow": "SLIGHT_RIGHT" + }, + { + "directions": [ + "SLIGHT_RIGHT" + ], + "follow": "SLIGHT_RIGHT" + } + ], + "laneSeparators": [ + "SINGLE_SOLID", + "SINGLE_SOLID", + "LONG_DASHED", + "SINGLE_SOLID" + ], + "startPointIndex": 247, + "endPointIndex": 248, + "sectionType": "LANES" + }, + { + "lanes": [ + { + "directions": [ + "STRAIGHT" + ] + }, + { + "directions": [ + "RIGHT" + ], + "follow": "RIGHT" + } + ], + "laneSeparators": [ + "SINGLE_SOLID", + "SHORT_DASHED", + "SINGLE_SOLID" + ], + "startPointIndex": 273, + "endPointIndex": 274, + "sectionType": "LANES" + }, + { + "lanes": [ + { + "directions": [ + "LEFT" + ] + }, + { + "directions": [ + "RIGHT" + ], + "follow": "RIGHT" + } + ], + "laneSeparators": [ + "SINGLE_SOLID", + "SHORT_DASHED", + "SINGLE_SOLID" + ], + "startPointIndex": 306, + "endPointIndex": 308, + "sectionType": "LANES" + } + ], + "guidance": { + "instructions": [ + { + "routeOffsetInMeters": 0, + "travelTimeInSeconds": 0, + "point": { + "latitude": 48.18613, + "longitude": 11.57849 + }, + "pointIndex": 0, + "instructionType": "LOCATION_DEPARTURE", + "street": "Silcherstraße", + "countryCode": "DEU", + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "DEPART", + "message": "Abfahrt von Silcherstraße" + }, + { + "routeOffsetInMeters": 92, + "travelTimeInSeconds": 20, + "point": { + "latitude": 48.18696, + "longitude": 11.57857 + }, + "pointIndex": 1, + "instructionType": "TURN", + "street": "Schmalkaldener Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_RIGHT", + "message": "Biegen Sie rechts ab auf Schmalkaldener Straße" + }, + { + "routeOffsetInMeters": 524, + "travelTimeInSeconds": 94, + "point": { + "latitude": 48.18686, + "longitude": 11.58437 + }, + "pointIndex": 11, + "instructionType": "TURN", + "roadNumbers": [ + "B13" + ], + "street": "Ingolstädter Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_RIGHT", + "message": "Biegen Sie rechts ab auf Ingolstädter Straße/B13" + }, + { + "routeOffsetInMeters": 5234, + "travelTimeInSeconds": 931, + "point": { + "latitude": 48.14538, + "longitude": 11.57877 + }, + "pointIndex": 133, + "instructionType": "TURN", + "street": "Von-der-Tann-Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": -90, + "possibleCombineWithNext": true, + "drivingSide": "RIGHT", + "maneuver": "TURN_LEFT", + "message": "Biegen Sie links ab auf Von-der-Tann-Straße", + "combinedMessage": "Biegen Sie links ab auf Von-der-Tann-Straße dann bleiben Sie bei Von-der-Tann-Straße rechts" + }, + { + "routeOffsetInMeters": 5677, + "travelTimeInSeconds": 1002, + "point": { + "latitude": 48.1441, + "longitude": 11.58414 + }, + "pointIndex": 162, + "instructionType": "TURN", + "street": "Von-der-Tann-Straße", + "countryCode": "DEU", + "junctionType": "BIFURCATION", + "turnAngleInDecimalDegrees": 45, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "KEEP_RIGHT", + "message": "Bleiben Sie bei Von-der-Tann-Straße rechts" + }, + { + "routeOffsetInMeters": 6793, + "travelTimeInSeconds": 1177, + "point": { + "latitude": 48.13504, + "longitude": 11.58227 + }, + "pointIndex": 208, + "instructionType": "TURN", + "street": "Isartorplatz", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": -90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_LEFT", + "message": "Biegen Sie links ab auf Isartorplatz" + }, + { + "routeOffsetInMeters": 7483, + "travelTimeInSeconds": 1377, + "point": { + "latitude": 48.13155, + "longitude": 11.58848 + }, + "pointIndex": 248, + "instructionType": "TURN", + "street": "Rosenheimer Straße", + "countryCode": "DEU", + "junctionType": "BIFURCATION", + "turnAngleInDecimalDegrees": 45, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "KEEP_RIGHT", + "message": "Bleiben Sie bei Rosenheimer Straße rechts" + }, + { + "routeOffsetInMeters": 8082, + "travelTimeInSeconds": 1494, + "point": { + "latitude": 48.12822, + "longitude": 11.59436 + }, + "pointIndex": 274, + "instructionType": "TURN", + "street": "Balanstraße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_RIGHT", + "message": "Biegen Sie rechts ab auf Balanstraße" + }, + { + "routeOffsetInMeters": 9145, + "travelTimeInSeconds": 1683, + "point": { + "latitude": 48.11956, + "longitude": 11.60016 + }, + "pointIndex": 308, + "instructionType": "TURN", + "street": "Sankt-Martin-Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_RIGHT", + "message": "Biegen Sie rechts ab auf Sankt-Martin-Straße" + }, + { + "routeOffsetInMeters": 9619, + "travelTimeInSeconds": 1737, + "point": { + "latitude": 48.11811, + "longitude": 11.59417 + }, + "pointIndex": 321, + "instructionType": "TURN", + "street": "Hohenwaldeckstraße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": -90, + "possibleCombineWithNext": true, + "drivingSide": "RIGHT", + "maneuver": "TURN_LEFT", + "message": "Biegen Sie links ab auf Hohenwaldeckstraße", + "combinedMessage": "Biegen Sie links ab auf Hohenwaldeckstraße dann sie haben Hohenwaldeckstraße erreicht. Ihr Ziel liegt auf der linken Seite" + }, + { + "routeOffsetInMeters": 9794, + "travelTimeInSeconds": 1779, + "point": { + "latitude": 48.11655, + "longitude": 11.59422 + }, + "pointIndex": 324, + "instructionType": "LOCATION_ARRIVAL", + "street": "Hohenwaldeckstraße", + "countryCode": "DEU", + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "ARRIVE_LEFT", + "message": "Sie haben Hohenwaldeckstraße erreicht. Ihr Ziel liegt auf der linken Seite" + } + ], + "instructionGroups": [ + { + "firstInstructionIndex": 0, + "lastInstructionIndex": 2, + "groupMessage": "Abfahrt von Silcherstraße. Fahren Sie auf die Ingolstädter Straße/B13", + "groupLengthInMeters": 5234 + }, + { + "firstInstructionIndex": 3, + "lastInstructionIndex": 4, + "groupMessage": "Fahren Sie auf die Von-der-Tann-Straße", + "groupLengthInMeters": 1559 + }, + { + "firstInstructionIndex": 5, + "lastInstructionIndex": 8, + "groupMessage": "Fahren Sie auf die Isartorplatz, Rosenheimer Straße, Balanstraße, Sankt-Martin-Straße", + "groupLengthInMeters": 2826 + }, + { + "firstInstructionIndex": 9, + "lastInstructionIndex": 10, + "groupMessage": "Fahren Sie zu Ihrem Ziel in der Hohenwaldeckstraße", + "groupLengthInMeters": 175 + } + ] + } + }, + { + "summary": { + "lengthInMeters": 10231, + "travelTimeInSeconds": 2210, + "trafficDelayInSeconds": 0, + "trafficLengthInMeters": 0, + "departureTime": "2026-03-12T10:44:06+01:00", + "arrivalTime": "2026-03-12T11:20:56+01:00", + "deviationDistance": 92, + "deviationTime": 20, + "deviationPoint": { + "latitude": 48.18696, + "longitude": 11.57857 + } + }, + "legs": [ + { + "summary": { + "lengthInMeters": 10231, + "travelTimeInSeconds": 2210, + "trafficDelayInSeconds": 0, + "trafficLengthInMeters": 0, + "departureTime": "2026-03-12T10:44:06+01:00", + "arrivalTime": "2026-03-12T11:20:56+01:00" + }, + "encodedPolyline": "ijbeHqlteAeDOO`MRA~@ErB@n@@^??P@BBDBBLBdFR?d@A`@Ab@ChBd@?bC@vDAL?xDDhD@H?zCAHAbB?l@ED?RHBB@B@D?BC|BA|DN?JJj@HLDVFpA`@RDFBHDHB\\LLFD@LDr@JF@d@Bz@Ep@EhAEhBIVCVCZ?hDUTAhAIpAKbDW\\EPAVAp@GbCSf@Cl@E\\CjBQPCfE]j@GxAKlBMRAr@E`AARCpDYdAENAdD[\\Cl@IHC^ORKLAb@ExBSdE[|BYtC[|BUX?JXBDHFJDVF\\DH?j@Cb@?xBIfF[RCt@G^CbDU`E_@`@C]jBQjAi@hC[zAfEbCfCvAlB`AJFf@\\`@TPHjBfAlCzA`Ah@bAj@z@d@jAn@pCzALFdCvArBhAh@ZRJv@d@\\RvBlAXPLHVLNHh@ZdAl@^T|Az@h@Zj@Zh@X`CrA|@d@hAv@`Ad@lBjAXiAj@yBDUPGBATKb@[RI`@M~@Mp@KVEJ?XAJALBLHPVv@bALRLN\\b@FDFJPRXRRJLBj@JLBPBzANZDR@VDL@PBh@F~@H\\F~@Jn@FX@h@EZMrDmBnAo@l@]^Sd@e@\\]PM^UXOp@a@XSHEHETMPGDCHE\\S\\WDGHKHOBUBg@CmA?Q?UBYDQBMHUFGXc@BE@OVw@Pq@j@eC@M?[?[?_@AG?ICUAGEQx@aAr@}@TUz@sANS~@uAp@cAx@mAn@}@dDeFj@y@r@eAHGLKb@w@\\g@\\e@X_@DIPU\\g@T[V_@RY^k@^i@DGNSNUl@aAFKxBkDjAgBd@s@fAaBz@uAb@g@PMdCuBb@Y\\S`Ak@`@ONE@?RAJ@p@L^P^R\\TlAr@n@RT@P?b@KVO\\YNQXYFCLGHEJCh@QNCHEPGDEJUBI`@{AXcADOZi@HOBGRk@\\cBPiATeAVaAf@cB^kA|CuHJ[Fc@@]F}ANaD@YDo@WMGMCGCECSM_Aa@qCkAoIw@kFj@Qv@IrEP", + "encodedPolylinePrecision": 5 + } + ], + "sections": [ + { + "lanes": [ + { + "directions": [ + "LEFT" + ] + }, + { + "directions": [ + "LEFT" + ] + }, + { + "directions": [ + "STRAIGHT" + ], + "follow": "STRAIGHT" + }, + { + "directions": [ + "STRAIGHT" + ], + "follow": "STRAIGHT" + } + ], + "laneSeparators": [ + "SINGLE_SOLID", + "LONG_DASHED", + "SHORT_DASHED", + "LONG_DASHED", + "SINGLE_SOLID" + ], + "startPointIndex": 41, + "endPointIndex": 42, + "sectionType": "LANES" + }, + { + "lanes": [ + { + "directions": [ + "LEFT" + ] + }, + { + "directions": [ + "STRAIGHT" + ], + "follow": "STRAIGHT" + } + ], + "laneSeparators": [ + "SINGLE_SOLID", + "SHORT_DASHED", + "SINGLE_SOLID" + ], + "startPointIndex": 323, + "endPointIndex": 326, + "sectionType": "LANES" + }, + { + "lanes": [ + { + "directions": [ + "LEFT" + ], + "follow": "LEFT" + }, + { + "directions": [ + "STRAIGHT" + ] + } + ], + "laneSeparators": [ + "SINGLE_SOLID", + "SINGLE_SOLID", + "SINGLE_SOLID" + ], + "startPointIndex": 342, + "endPointIndex": 343, + "sectionType": "LANES" + } + ], + "guidance": { + "instructions": [ + { + "routeOffsetInMeters": 0, + "travelTimeInSeconds": 0, + "point": { + "latitude": 48.18613, + "longitude": 11.57849 + }, + "pointIndex": 0, + "instructionType": "LOCATION_DEPARTURE", + "street": "Silcherstraße", + "countryCode": "DEU", + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "DEPART", + "message": "Abfahrt von Silcherstraße" + }, + { + "routeOffsetInMeters": 92, + "travelTimeInSeconds": 20, + "point": { + "latitude": 48.18696, + "longitude": 11.57857 + }, + "pointIndex": 1, + "instructionType": "TURN", + "street": "Schmalkaldener Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": -90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_LEFT", + "message": "Biegen Sie links ab auf Schmalkaldener Straße" + }, + { + "routeOffsetInMeters": 259, + "travelTimeInSeconds": 60, + "point": { + "latitude": 48.18704, + "longitude": 11.57632 + }, + "pointIndex": 2, + "instructionType": "TURN", + "street": "Oberhofer Platz", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": -90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_LEFT", + "message": "Biegen Sie links ab auf Oberhofer Platz" + }, + { + "routeOffsetInMeters": 415, + "travelTimeInSeconds": 99, + "point": { + "latitude": 48.18564, + "longitude": 11.57634 + }, + "pointIndex": 7, + "instructionType": "TURN", + "street": "Bad-Kreuznacher-Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 90, + "possibleCombineWithNext": true, + "drivingSide": "RIGHT", + "maneuver": "TURN_RIGHT", + "message": "Biegen Sie rechts ab auf Bad-Kreuznacher-Straße", + "combinedMessage": "Biegen Sie rechts ab auf Bad-Kreuznacher-Straße dann biegen Sie rechts ab auf Milbertshofener Straße" + }, + { + "routeOffsetInMeters": 565, + "travelTimeInSeconds": 146, + "point": { + "latitude": 48.18437, + "longitude": 11.57606 + }, + "pointIndex": 13, + "instructionType": "TURN", + "street": "Milbertshofener Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 90, + "possibleCombineWithNext": true, + "drivingSide": "RIGHT", + "maneuver": "TURN_RIGHT", + "message": "Biegen Sie rechts ab auf Milbertshofener Straße", + "combinedMessage": "Biegen Sie rechts ab auf Milbertshofener Straße dann biegen Sie links ab auf Christoph-von-Gluck-Platz" + }, + { + "routeOffsetInMeters": 645, + "travelTimeInSeconds": 172, + "point": { + "latitude": 48.18441, + "longitude": 11.57499 + }, + "pointIndex": 17, + "instructionType": "TURN", + "street": "Christoph-von-Gluck-Platz", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": -90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_LEFT", + "message": "Biegen Sie links ab auf Christoph-von-Gluck-Platz" + }, + { + "routeOffsetInMeters": 1369, + "travelTimeInSeconds": 324, + "point": { + "latitude": 48.17904, + "longitude": 11.57328 + }, + "pointIndex": 36, + "instructionType": "TURN", + "street": "Knorrstraße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": -90, + "possibleCombineWithNext": true, + "drivingSide": "RIGHT", + "maneuver": "TURN_LEFT", + "message": "Biegen Sie links ab auf Knorrstraße", + "combinedMessage": "Biegen Sie links ab auf Knorrstraße dann fahren Sie geradeaus weiter auf Belgradstraße" + }, + { + "routeOffsetInMeters": 1480, + "travelTimeInSeconds": 377, + "point": { + "latitude": 48.17808, + "longitude": 11.57293 + }, + "pointIndex": 42, + "instructionType": "TURN", + "street": "Belgradstraße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 0, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "STRAIGHT", + "message": "Fahren Sie geradeaus weiter auf Belgradstraße" + }, + { + "routeOffsetInMeters": 4053, + "travelTimeInSeconds": 832, + "point": { + "latitude": 48.15515, + "longitude": 11.57536 + }, + "pointIndex": 117, + "instructionType": "TURN", + "street": "Georgenstraße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 90, + "possibleCombineWithNext": true, + "drivingSide": "RIGHT", + "maneuver": "TURN_RIGHT", + "message": "Biegen Sie rechts ab auf Georgenstraße", + "combinedMessage": "Biegen Sie rechts ab auf Georgenstraße dann biegen Sie links ab auf Arcisstraße" + }, + { + "routeOffsetInMeters": 4220, + "travelTimeInSeconds": 885, + "point": { + "latitude": 48.15574, + "longitude": 11.5733 + }, + "pointIndex": 121, + "instructionType": "TURN", + "street": "Arcisstraße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": -90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_LEFT", + "message": "Biegen Sie links ab auf Arcisstraße" + }, + { + "routeOffsetInMeters": 5472, + "travelTimeInSeconds": 1149, + "point": { + "latitude": 48.14541, + "longitude": 11.5666 + }, + "pointIndex": 151, + "instructionType": "TURN", + "street": "Katharina-von-Bora-Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 0, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "STRAIGHT", + "message": "Fahren Sie geradeaus weiter auf Katharina-von-Bora-Straße" + }, + { + "routeOffsetInMeters": 5818, + "travelTimeInSeconds": 1212, + "point": { + "latitude": 48.14257, + "longitude": 11.56473 + }, + "pointIndex": 159, + "instructionType": "TURN", + "street": "Sophienstraße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": -90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_LEFT", + "message": "Biegen Sie links ab auf Sophienstraße" + }, + { + "routeOffsetInMeters": 7332, + "travelTimeInSeconds": 1657, + "point": { + "latitude": 48.13184, + "longitude": 11.57056 + }, + "pointIndex": 246, + "instructionType": "TURN", + "street": "Papa-Schmid-Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_RIGHT", + "message": "Biegen Sie rechts ab auf Papa-Schmid-Straße" + }, + { + "routeOffsetInMeters": 9169, + "travelTimeInSeconds": 2028, + "point": { + "latitude": 48.11905, + "longitude": 11.58351 + }, + "pointIndex": 326, + "instructionType": "TURN", + "street": "Sankt-Bonifatius-Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 0, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "STRAIGHT", + "message": "Fahren Sie geradeaus weiter auf Sankt-Bonifatius-Straße" + }, + { + "routeOffsetInMeters": 9712, + "travelTimeInSeconds": 2111, + "point": { + "latitude": 48.11699, + "longitude": 11.58995 + }, + "pointIndex": 343, + "instructionType": "TURN", + "street": "Sankt-Martin-Straße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": -90, + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "TURN_LEFT", + "message": "Biegen Sie links ab auf Sankt-Martin-Straße" + }, + { + "routeOffsetInMeters": 10056, + "travelTimeInSeconds": 2171, + "point": { + "latitude": 48.11811, + "longitude": 11.59417 + }, + "pointIndex": 352, + "instructionType": "TURN", + "street": "Hohenwaldeckstraße", + "countryCode": "DEU", + "junctionType": "REGULAR", + "turnAngleInDecimalDegrees": 90, + "possibleCombineWithNext": true, + "drivingSide": "RIGHT", + "maneuver": "TURN_RIGHT", + "message": "Biegen Sie rechts ab auf Hohenwaldeckstraße", + "combinedMessage": "Biegen Sie rechts ab auf Hohenwaldeckstraße dann sie haben Hohenwaldeckstraße erreicht. Ihr Ziel liegt auf der linken Seite" + }, + { + "routeOffsetInMeters": 10231, + "travelTimeInSeconds": 2210, + "point": { + "latitude": 48.11655, + "longitude": 11.59422 + }, + "pointIndex": 355, + "instructionType": "LOCATION_ARRIVAL", + "street": "Hohenwaldeckstraße", + "countryCode": "DEU", + "possibleCombineWithNext": false, + "drivingSide": "RIGHT", + "maneuver": "ARRIVE_LEFT", + "message": "Sie haben Hohenwaldeckstraße erreicht. Ihr Ziel liegt auf der linken Seite" + } + ], + "instructionGroups": [ + { + "firstInstructionIndex": 0, + "lastInstructionIndex": 5, + "groupMessage": "Abfahrt von Silcherstraße. Fahren Sie auf die Schmalkaldener Straße, Oberhofer Platz, Bad-Kreuznacher-Straße, Milbertshofener Straße, Christoph-von-Gluck-Platz", + "groupLengthInMeters": 1369 + }, + { + "firstInstructionIndex": 6, + "lastInstructionIndex": 7, + "groupMessage": "Fahren Sie auf die Knorrstraße, Belgradstraße", + "groupLengthInMeters": 2684 + }, + { + "firstInstructionIndex": 8, + "lastInstructionIndex": 9, + "groupMessage": "Fahren Sie auf die Georgenstraße, Arcisstraße", + "groupLengthInMeters": 1419 + }, + { + "firstInstructionIndex": 10, + "lastInstructionIndex": 12, + "groupMessage": "Fahren Sie auf die Katharina-von-Bora-Straße, Sophienstraße, Papa-Schmid-Straße", + "groupLengthInMeters": 3697 + }, + { + "firstInstructionIndex": 13, + "lastInstructionIndex": 14, + "groupMessage": "Fahren Sie auf die Sankt-Bonifatius-Straße, Sankt-Martin-Straße", + "groupLengthInMeters": 887 + }, + { + "firstInstructionIndex": 15, + "lastInstructionIndex": 16, + "groupMessage": "Fahren Sie zu Ihrem Ziel in der Hohenwaldeckstraße", + "groupLengthInMeters": 175 } ] }