This commit is contained in:
Dimitris
2026-03-12 15:34:34 +01:00
parent 61ce09f393
commit 619ceb9f83
28 changed files with 1815 additions and 634 deletions

View File

@@ -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"
}

View File

@@ -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
@@ -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()
}
}
@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()
},
CheckPermissionScreen(app = {
AppNavGraph(
mainActivity = this
)
})
}
}
@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,86 +204,27 @@ 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
}
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
SheetLayout(
map = { _ ->
Map(
userLocationState, step, nextStep, baseStyle, navController
)
},
onDragEnd = {
isUpdated = false
}
menu = { SheetContent(navController, step, nextStep) },
)
}
) {
SheetContent(step, nextStep) { closeSheet() }
}
},
sheetPeekHeight = height.dp
}
@Composable
private fun Map(
userLocationState: UserLocationState,
step: StepData?,
nextStep: StepData?,
baseStyle: BaseStyle.Json,
navController: NavHostController
) {
MapView(
applicationContext,
@@ -312,18 +240,6 @@ class MainActivity : ComponentActivity() {
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
}
AppNavGraph(this)
}
@Composable
fun Settings(navController: NavController, modifier: Modifier = Modifier) {
@@ -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
)
}

View File

@@ -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)
}
}
}

View File

@@ -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(

View File

@@ -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),
)
}
}

View File

@@ -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() }
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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<SearchResult>()
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() }
NavigationTheme(true) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
stringResource(id = R.string.search_action_title),
)
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
if (recentPlaces.value != null) {
val items = listOf(recentPlaces)
if (items.isNotEmpty()) {
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()
}) {
},
navigationIcon = {
IconButton(onClick = function) {
Icon(
painter = painterResource(id = R.drawable.ic_place_white_24dp),
"Home",
modifier = Modifier.size(24.dp, 24.dp),
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.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")
},
)
{ padding ->
val scrollState = rememberScrollState()
Column(Modifier.padding(top = 50.dp)) {
SearchBar(context, navigationViewModel, location)
Categories(context, navigationViewModel, location, closeSheet = { })
}
}
}
private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) {
viewModel.searchPlaces(it, location)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
textFieldState: TextFieldState,
searchPlaces: List<Place>,
searchResults: List<SearchResult>,
viewModel: NavigationViewModel,
context: Context,
navigationViewModel: NavigationViewModel,
location: Location,
closeSheet: () -> Unit
) {
val searchResults = mutableListOf<SearchResult>()
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,7 +209,6 @@ 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(
@@ -227,41 +242,5 @@ private fun SearchPlaces(
}
}
}
}
@Composable
private fun RecentPlaces(
recentPlaces: List<Place>,
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)
}
}
}
}

View File

@@ -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<Place>,
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)
}
}
}
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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
)
}
}

View File

@@ -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,

View File

@@ -17,6 +17,7 @@ class Simulation {
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
if (routeModel.navState.route.isRouteValid()) {
val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return
simulationJob?.cancel()
@@ -36,13 +37,14 @@ class Simulation {
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)
// Wait before moving to the next point (e.g., every 1 second)
delay(1000)
lastLocation = fakeLocation
}
routeModel.stopNavigation()
}
}
}
fun stopSimulation() {
simulationJob?.cancel()

View File

@@ -116,7 +116,8 @@ class CategoryScreen(
} else {
row.addText("${(it.distance / 1000).round(1)} km")
}
if (category == Constants.CHARGING_STATION) {
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
)

View File

@@ -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
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()
}

View File

@@ -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<Place>
) : Screen(carContext) {
var places = listOf<Place>()
val observer = Observer<List<Place>> { newPlaces ->
places = newPlaces
invalidate()
val routeModel = RouteCarModel()
var place = Place()
val previewObserver = Observer<String> { 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()
}
}
}
val observerAddress = Observer<List<Place>> { newContacts ->
places = newContacts
invalidate()
}
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 {
@@ -94,7 +87,7 @@ class PlaceListScreen(
// .setImage(contactIcon(it.avatar, it.category))
.setTitle("$street ${it.city}")
.setOnClickListener {
val place = Place(
place = Place(
0,
it.name,
it.category,
@@ -105,20 +98,13 @@ class PlaceListScreen(
it.street,
// avatar = null
)
screenManager
.pushForResult(
RoutePreviewScreen(
val location = location(place.longitude, place.latitude)
navigationViewModel.loadPreviewRoute(
carContext,
surfaceRenderer,
place,
navigationViewModel
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
if (category != CONTACTS) {
row.addText(SpannableString(" ").apply {
@@ -184,4 +170,10 @@ class PlaceListScreen(
}
return CarIcon.Builder(IconCompat.createWithContentUri(avatar)).build()
}
fun loadPlaces() {
if (category == CONTACTS) {
navigationViewModel.loadContacts(carContext)
}
}
}

View File

@@ -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<String> { 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)
@@ -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()
}

View File

@@ -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<Category> = listOf(
Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)),
Category(id = 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 = CATEGORIES, name = carContext.getString(R.string.category_title)),
Category(id = FAVORITES, name = carContext.getString(R.string.favorites))
)
lateinit var searchResult: List<SearchResult>
@@ -44,8 +48,50 @@ class SearchScreen(
invalidate()
}
val observerRecentPlaces = Observer<List<Place>> { 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<List<Place>> { 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,19 +124,19 @@ class SearchScreen(
}
}
} else {
screenManager
.pushForResult(
PlaceListScreen(
if (it.id == RECENT) {
navigationViewModel.loadRecentPlaces(
carContext,
surfaceRenderer,
it.id,
navigationViewModel
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
if (it.id == FAVORITES) {
navigationViewModel.loadFavorites(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
}
}
}
@@ -123,12 +169,14 @@ class SearchScreen(
fun categoryIcon(category: String?): CarIcon {
val resId: Int = when (category) {
Constants.RECENT -> {
RECENT -> {
R.drawable.ic_place_white_24dp
}
Constants.FAVORITES -> {
FAVORITES -> {
R.drawable.ic_favorite_white_24dp
}
else -> {
R.drawable.navigation_48px
}

View File

@@ -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(

View File

@@ -23,12 +23,14 @@ data class Route(
val routeEngine: Int,
val routes: List<com.kouros.navigation.data.route.Routes>,
var currentStepIndex: Int = 0,
var routeIndex : Int = 0,
) {
data class Builder(
var routeEngine: Int = 0,
var summary: Summary = Summary(),
var routes: List<com.kouros.navigation.data.route.Routes> = 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<Leg> {
return if (routes.isNotEmpty()) {
routes.first().legs
routes[routeIndex].legs
} else {
emptyList()
}

View File

@@ -58,6 +58,7 @@ class TomTomRepository : NavigationRepository() {
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
"&instructionsType=text&language=$language&sectionType=lanes" +
"&routeRepresentation=encodedPolyline" +
"&maxAlternatives=2" +
"&vehicleEngineType=combustion$filter&key=$tomtomApiKey"
return fetchUrl(
url,

View File

@@ -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<List<Place>> by lazy {
val recentPlaces: MutableLiveData<List<Place>> 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<Place>()
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()
}

View File

@@ -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() {

View File

@@ -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
@@ -53,14 +58,35 @@ 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
fun previewZoom(centerLocation: Location, previewDistance: Double): Double {
return calculateZoomFromBoundingBox(centerLocation, previewDistance / 1000)
}
return 9.5
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) {

File diff suppressed because it is too large Load Diff