DataStore Manager

This commit is contained in:
Dimitris
2026-02-20 07:20:04 +01:00
parent 65ff41995d
commit ebd97cf1c9
65 changed files with 7975 additions and 13716 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 38
versionName = "0.2.0.38"
versionCode = 42
versionName = "0.2.0.42"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -4,7 +4,7 @@ import android.app.Application
import android.content.Context
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.di.appModule
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
@@ -15,7 +15,7 @@ class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
ObjectBox.init(this);
ObjectBox.init(this)
appContext = applicationContext
navigationViewModel = getViewModel(appContext!!)
startKoin {
@@ -30,6 +30,6 @@ class MainApplication : Application() {
var appContext: Context? = null
private set
lateinit var navigationViewModel : ViewModel
lateinit var navigationViewModel : NavigationViewModel
}
}

View File

@@ -1,18 +1,19 @@
package com.kouros.navigation.di
import com.kouros.navigation.data.NavigationRepository
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.BaseStyleModel
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModel
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
import kotlin.math.sin
val appModule = module {
viewModelOf(::ViewModel)
viewModelOf(::NavigationViewModel)
viewModelOf(::SettingsViewModel)
singleOf(::ValhallaRepository)
singleOf(::OsrmRepository)
singleOf(::TomTomRepository)

View File

@@ -43,26 +43,27 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.kouros.data.R
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.repository.SettingsRepository
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.theme.NavigationTheme
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx
@@ -70,7 +71,9 @@ import io.ticofab.androidgpxparser.parser.domain.TrackSegment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.joda.time.DateTime
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.location.DesiredAccuracy
@@ -83,12 +86,13 @@ import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() {
val routeData = MutableLiveData("")
val routeModel = RouteModel()
var tilt = 50.0
val useMock = false
val type = 3 // simulate 2 test 3 gpx 4 testSingle
val useMock = true
val type = 3 // 1 simulate 2 test 3 gpx 4 testSingle
var currentIndex = 0
val stepData: MutableLiveData<StepData> by lazy {
@@ -134,8 +138,7 @@ class MainActivity : ComponentActivity() {
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val darkModeSettings = getIntKeyValue(applicationContext, Constants.DARK_MODE_SETTINGS)
baseStyle = BaseStyleModel().readStyle(applicationContext, darkModeSettings, false)
if (useMock) {
checkMockLocationEnabled()
}
@@ -150,6 +153,11 @@ class MainActivity : ComponentActivity() {
navigationViewModel.route.observe(this, observer)
}
}
lifecycleScope.launch {
getSettingsViewModel(applicationContext).routingEngine.collect {
}
}
enableEdgeToEdge()
setContent {
CheckPermissionScreen()
@@ -186,9 +194,13 @@ class MainActivity : ComponentActivity() {
@Composable
fun StartScreen(
navController: NavHostController
) {
val appViewModel: AppViewModel = appViewModel()
val darkMode by appViewModel.darkMode.collectAsState()
baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1)
val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
@@ -212,7 +224,7 @@ class MainActivity : ComponentActivity() {
sheetPeekHeightState.value = 128.dp
}
}
NavigationTheme {
NavigationTheme (useDarkTheme = darkMode == 1) {
BottomSheetScaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
@@ -249,13 +261,12 @@ class MainActivity : ComponentActivity() {
@Composable
fun App() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "startScreen") {
composable("startScreen") { StartScreen(navController) }
composable("settings") { SettingsScreen(navController) { navController.popBackStack() } }
composable("display_settings") { DisplayScreenSettings(applicationContext) { navController.popBackStack() } }
composable("nav_settings") { NavigationScreenSettings(applicationContext) { navController.popBackStack() } }
}
//val lastRoute = NavigationUtils.getStringKeyValue(applicationContext, Constants.LAST_ROUTE)
//if (lastRoute!!.isNotEmpty()) {
// routeModel.startNavigation(lastRoute, applicationContext)
// routeData.value = routeModel.curRoute.routeGeoJson
//}
AppNavGraph(applicationContext = applicationContext, this)
}
@Composable
@@ -285,14 +296,16 @@ class MainActivity : ComponentActivity() {
if (!routeModel.isNavigating()) {
SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() }
} else {
if (step != null && nextStep != null) {
NavigationSheet(
applicationContext,
routeModel,
step!!,
nextStep!!,
step,
nextStep,
{ stopNavigation { closeSheet() } },
{ simulateNavigation() })
}
}
// For recomposition!
Text("$locationState", fontSize = 12.sp)
}
@@ -303,17 +316,18 @@ class MainActivity : ComponentActivity() {
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
with(routeModel) {
if (isNavigating()) {
updateLocation(currentLocation, navigationViewModel)
updateLocation(applicationContext,currentLocation, navigationViewModel)
stepData.value = currentStep()
nextStepData.value = nextStep()
if (navState.maneuverType in 39..42 && routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) {
// stopNavigation()
navState.copy(arrived = true)
navState = navState.copy(arrived = true)
routeData.value = ""
}
}
}
val zoom = calculateZoom(location.speed)
//val zoom = calculateZoom(location.speed)
val zoom = 16.0
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = zoom, target = location.position, bearing = bearing
@@ -331,7 +345,7 @@ class MainActivity : ComponentActivity() {
val latitude = routeModel.curRoute.waypoints[0][1]
val longitude = routeModel.curRoute.waypoints[0][0]
closeSheet()
routeModel.stopNavigation()
routeModel.stopNavigation(applicationContext)
if (useMock) {
mock.setMockLocation(latitude, longitude)
}
@@ -370,7 +384,7 @@ class MainActivity : ComponentActivity() {
val deviation = 0.0
if (index in 0..routeModel.curRoute.waypoints.size) {
mock.setMockLocation(waypoint[1], waypoint[0])
Thread.sleep(500)
Thread.sleep(1000)
}
}
}
@@ -381,7 +395,7 @@ class MainActivity : ComponentActivity() {
for ((index, step) in routeModel.curLeg.steps.withIndex()) {
//if (index in 3..3) {
for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) {
routeModel.updateLocation(
routeModel.updateLocation(applicationContext,
location(waypoint[0], waypoint[1]), navigationViewModel
)
val step = routeModel.currentStep()
@@ -402,7 +416,7 @@ class MainActivity : ComponentActivity() {
if (1 == 1) {
mock.setMockLocation(latitude, longitude)
} else {
routeModel.updateLocation(
routeModel.updateLocation(applicationContext,
location(longitude, latitude), navigationViewModel
)
}

View File

@@ -5,18 +5,24 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.window.layout.WindowMetricsCalculator
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.tomtom.TrafficData
import com.kouros.navigation.ui.app.AppViewModel
import com.kouros.navigation.ui.app.appViewModel
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.LocationTrackingEffect
@@ -58,6 +64,10 @@ fun MapView(
)
val rememberBaseStyle = rememberBaseStyle(baseStyle)
val appViewModel: AppViewModel = appViewModel()
val showBuildings by appViewModel.threedBuilding.collectAsState()
Column {
NavigationInfo(step, nextStep)
Box(contentAlignment = Alignment.Center) {
@@ -67,7 +77,9 @@ fun MapView(
rememberBaseStyle,
route,
emptyMap(),
ViewStyle.VIEW
ViewStyle.VIEW,
speedCameras = "",
showBuildings
)
LocationTrackingEffect(
locationState = userLocationState,
@@ -87,6 +99,3 @@ fun MapView(
}
}
}

View File

@@ -2,10 +2,15 @@ package com.kouros.navigation.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -17,39 +22,43 @@ import com.kouros.data.R
import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.round
@Composable
fun NavigationInfo(step: StepData?, nextStep: StepData?) {
if (step != null && step.instruction.isNotEmpty()) {
Card(modifier = Modifier.padding(top = 60.dp)) {
Column() {
Row {
ElevatedCard(
elevation = CardDefaults.cardElevation(
defaultElevation = 6.dp
), modifier = Modifier
.padding(top = 60.dp)
.fillMaxWidth()
) {
Column {
Icon(
painter = painterResource(step.icon),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
if (step.currentManeuverType == 46
|| step.currentManeuverType == 45) {
Text(text ="Exit ${step.exitNumber}", fontSize = 20.sp)
|| step.currentManeuverType == 45
) {
Text(text = "Exit ${step.exitNumber}", fontSize = 18.sp)
}
Column {
Row {
if (step.leftStepDistance < 1000) {
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 25.sp)
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 24.sp, color = MaterialTheme.colorScheme.primary)
} else {
Text(
text = "${(step.leftStepDistance / 1000).round(1)} km",
fontSize = 25.sp
fontSize = 24.sp,
color = MaterialTheme.colorScheme.primary
)
}
Text(text = step.instruction, fontSize = 20.sp)
}
if (nextStep != null && step.icon != nextStep.icon) {
Icon(
painter = painterResource(nextStep.icon),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
Spacer(
modifier = Modifier.padding(5.dp)
)
}
Text(text = step.instruction, fontSize = 24.sp, color = MaterialTheme.colorScheme.primary)
}
}
}

View File

@@ -34,7 +34,7 @@ fun NavigationSheet(
val distance = (step.leftDistance / 1000).round(1)
if (step.lane.isNotEmpty()) {
routeModel.navState.iconMapper.addLanes( step)
// routeModel.navState.iconMapper.addLanes( step)
}
Column {

View File

@@ -4,10 +4,8 @@ import android.content.Context
import android.location.Location
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -29,25 +27,21 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.dp
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.ViewModel
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.location
@Composable
fun SearchSheet(
applicationContext: Context,
viewModel: ViewModel,
viewModel: NavigationViewModel,
location: Location,
closeSheet: () -> Unit
) {
@@ -88,7 +82,7 @@ fun SearchSheet(
@Composable
fun Home(
applicationContext: Context,
viewModel: ViewModel,
viewModel: NavigationViewModel,
location: Location,
closeSheet: () -> Unit
) {
@@ -125,7 +119,7 @@ fun SearchBar(
searchPlaces: List<Place>,
searchResults: List<SearchResult>,
modifier: Modifier = Modifier,
viewModel: ViewModel,
viewModel: NavigationViewModel,
context: Context,
location: Location,
closeSheet: () -> Unit
@@ -166,14 +160,14 @@ fun SearchBar(
}
}
private fun searchPlaces(viewModel: ViewModel, location: Location, it: String) {
private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) {
viewModel.searchPlaces(it, location)
}
@Composable
private fun SearchPlaces(
searchResults: List<SearchResult>,
viewModel: ViewModel,
viewModel: NavigationViewModel,
context: Context,
location: Location,
closeSheet: () -> Unit
@@ -222,7 +216,7 @@ private fun SearchPlaces(
@Composable
private fun RecentPlaces(
recentPlaces: List<Place>,
viewModel: ViewModel,
viewModel: NavigationViewModel,
context: Context,
location: Location,
closeSheet: () -> Unit

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -19,7 +20,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.alorma.compose.settings.ui.SettingsMenuLink
import com.kouros.data.R
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -53,34 +53,16 @@ fun SettingsScreen(navController: NavHostController, navigateBack: () -> Unit) {
.verticalScroll(scrollState)
.padding(top = padding.calculateTopPadding()),
) {
SettingsMenuLink(
title = { Text(text = stringResource(R.string.display_settings)) },
modifier = Modifier,
enabled = true,
onClick = { navController.navigate("display_settings")},
icon = {
Icon(
painter = painterResource(R.drawable.ic_place_white_24dp),
contentDescription = stringResource(id = R.string.display_settings),
modifier = Modifier.size(48.dp, 48.dp),
)
Column(modifier = Modifier.padding(16.dp)) {
Button(onClick = { navController.navigate("display_settings") }) {
Text(stringResource(R.string.display_settings))
}
Button(onClick = { navController.navigate("nav_settings") }) {
Text(stringResource(R.string.navigation_settings))
}
)
SettingsMenuLink(
title = { Text(text = stringResource(R.string.navigation_settings)) },
modifier = Modifier,
enabled = true,
onClick = { navController.navigate("nav_settings")},
icon = {
Icon(
painter = painterResource(R.drawable.navigation_24px),
contentDescription = stringResource(id = R.string.navigation_settings),
modifier = Modifier.size(48.dp, 48.dp),
)
}
)
}
}
}

View File

@@ -0,0 +1,27 @@
package com.kouros.navigation.ui.app
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.kouros.navigation.repository.SettingsRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
class AppViewModel(
settingsRepository: SettingsRepository
) : ViewModel() {
val darkMode = settingsRepository.darkModeFlow
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
0
)
val threedBuilding = settingsRepository.threedBuildingFlow
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
false
)
}

View File

@@ -0,0 +1,29 @@
package com.kouros.navigation.ui.app
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.repository.SettingsRepository
@Composable
fun appViewModel(): AppViewModel {
val context = LocalContext.current
val dataStoreManager = remember { DataStoreManager(context) }
val repository = remember { SettingsRepository(dataStoreManager) }
return viewModel(
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return AppViewModel(repository) as T
}
}
)
}

View File

@@ -0,0 +1,59 @@
package com.kouros.navigation.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
@Composable
fun RadioButtonSingleSelection(
modifier: Modifier = Modifier,
selectedOption: Int,
radioOptions: List<String>,
onClick: (Int) -> Unit,
) {
Column(modifier.selectableGroup()) {
for ((index, text) in radioOptions.withIndex()) {
Row(
Modifier
.fillMaxWidth()
.height(56.dp)
.selectable(
selected = (index == selectedOption),
onClick = {
onClick(index)
},
role = Role.RadioButton
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (index == selectedOption),
onClick = null
)
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.kouros.navigation.ui.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun SectionTitle(title: String) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp)
)
}

View File

@@ -0,0 +1,29 @@
package com.kouros.navigation.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun SettingItem(
title: String,
value: String? = null
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = title)
value?.let {
Text(text = it, color = MaterialTheme.colorScheme.primary)
}
}
}

View File

@@ -0,0 +1,37 @@
package com.kouros.navigation.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun SettingSwitch(
title: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = title)
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
uncheckedThumbColor = Color.White
)
)
}
}

View File

@@ -0,0 +1,52 @@
package com.kouros.navigation.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.kouros.navigation.data.NavigationThemeColor
@Composable
fun ThemeColorPicker(
selectedColor: Long,
onColorSelected: (Long) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
NavigationThemeColor.entries.forEach { themeColor ->
val isSelected = selectedColor == themeColor.color
Box(
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(Color(themeColor.color))
.border(
width = if (isSelected) 3.dp else 0.dp,
color = if (isSelected) MaterialTheme.colorScheme.onSurface else Color.Transparent,
shape = CircleShape
)
.clickable {
onColorSelected(themeColor.color)
}
)
}
}
}

View File

@@ -0,0 +1,34 @@
package com.kouros.navigation.ui.navigation
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.kouros.navigation.ui.MainActivity
import com.kouros.navigation.ui.settings.NavigationScreenSettings
import com.kouros.navigation.ui.SettingsScreen
import com.kouros.navigation.ui.settings.DisplaySettings
import com.kouros.navigation.ui.settings.SettingsRoute
@Composable
fun AppNavGraph(applicationContext: Context, mainActivity: MainActivity) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "startScreen") {
composable("startScreen") { mainActivity.StartScreen(navController) }
composable("settings") { SettingsScreen(navController) { navController.popBackStack() } }
composable("settingsxx") {
SettingsRoute("display_settings", navController) { navController.popBackStack() }
}
// old
//composable("display_settings") { DisplaySettings(applicationContext) { navController.popBackStack() } }
//composable("nav_settings") { NavigationScreenSettings(applicationContext) { navController.popBackStack() } }
// new
composable("display_settings") { SettingsRoute("display_settings", navController) { navController.popBackStack() } }
composable("nav_settings") { SettingsRoute("nav_settings", navController) { navController.popBackStack() } }
}
}

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation.ui
package com.kouros.navigation.ui.settings
import android.content.Context
import androidx.compose.foundation.layout.Arrangement
@@ -32,13 +32,11 @@ import com.alorma.compose.settings.ui.SettingsRadioButton
import com.alorma.compose.settings.ui.base.internal.LocalSettingsTileColors
import com.alorma.compose.settings.ui.base.internal.SettingsTileDefaults
import com.kouros.data.R
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.getSettingsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DisplayScreenSettings(context: Context, navigateBack: () -> Unit) {
fun DisplaySettings(context: Context, navigateBack: () -> Unit) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
@@ -75,13 +73,11 @@ fun DisplayScreenSettings(context: Context, navigateBack: () -> Unit) {
@Composable
private fun DisplaySettings(context: Context) {
val settingsViewModel = getSettingsViewModel(context)
Section(title = "Anzeige") {
val state = remember {
mutableStateOf(
NavigationUtils.getBooleanKeyValue(
context,
SHOW_THREED_BUILDING
)
settingsViewModel.threedBuilding.value
)
}
SettingsCheckbox(
@@ -89,17 +85,14 @@ private fun DisplaySettings(context: Context) {
title = { Text(text = stringResource(R.string.threed_building)) },
onCheckedChange = {
state.value = it
NavigationUtils.setBooleanKeyValue(context, it, SHOW_THREED_BUILDING)
settingsViewModel.onThreedBuildingChanged(it)
},
)
}
Section(title = "Dunkles Design") {
val state = remember {
mutableIntStateOf(
NavigationUtils.getIntKeyValue(
context,
DARK_MODE_SETTINGS
)
settingsViewModel.darkMode.value
)
}
DarkModeData(context).darkDesign.forEach { sampleItem ->
@@ -108,7 +101,7 @@ private fun DisplaySettings(context: Context) {
title = { Text(text = sampleItem.title) },
onClick = {
state.intValue = sampleItem.key
NavigationUtils.setIntKeyValue(context, state.intValue, DARK_MODE_SETTINGS)
settingsViewModel.onDarkModeChanged(state.intValue)
},
)
}

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation.ui
package com.kouros.navigation.ui.settings
import android.content.Context
import androidx.compose.foundation.layout.Column
@@ -25,10 +25,10 @@ import com.alorma.compose.settings.ui.SettingsCheckbox
import com.alorma.compose.settings.ui.SettingsRadioButton
import com.kouros.data.R
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.getSettingsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -68,13 +68,11 @@ fun NavigationScreenSettings(context: Context, navigateBack: () -> Unit) {
@Composable
private fun NavigationSettings(context: Context) {
val settingsViewModel = getSettingsViewModel(context)
Section(title = stringResource(id = R.string.options)) {
val avoidMotorwayState = remember {
mutableStateOf(
NavigationUtils.getBooleanKeyValue(
context,
Constants.AVOID_MOTORWAY
)
settingsViewModel.avoidMotorway.value
)
}
SettingsCheckbox(
@@ -82,16 +80,13 @@ private fun NavigationSettings(context: Context) {
title = { Text(text = stringResource(id = R.string.avoid_highways_row_title)) },
onCheckedChange = {
avoidMotorwayState.value = it
NavigationUtils.setBooleanKeyValue(context, it, Constants.AVOID_MOTORWAY)
settingsViewModel.onAvoidMotorway(it)
},
)
val avoidTollwayState = remember {
mutableStateOf(
NavigationUtils.getBooleanKeyValue(
context,
Constants.AVOID_TOLLWAY
)
settingsViewModel.avoidTollway.value
)
}
SettingsCheckbox(
@@ -99,16 +94,13 @@ private fun NavigationSettings(context: Context) {
title = { Text(text = stringResource(id = R.string.avoid_tolls_row_title)) },
onCheckedChange = {
avoidTollwayState.value = it
NavigationUtils.setBooleanKeyValue(context, it, Constants.AVOID_TOLLWAY)
settingsViewModel.onAvoidTollway(it)
},
)
val carLocationState = remember {
mutableStateOf(
NavigationUtils.getBooleanKeyValue(
context,
Constants.CAR_LOCATION
)
settingsViewModel.carLocation.value
)
}
SettingsCheckbox(
@@ -116,7 +108,7 @@ private fun NavigationSettings(context: Context) {
title = { Text(text = stringResource(id = R.string.use_car_location)) },
onCheckedChange = {
carLocationState.value = it
NavigationUtils.setBooleanKeyValue(context, it, Constants.CAR_LOCATION)
settingsViewModel.onCarLocation(it)
},
)
}
@@ -124,10 +116,7 @@ private fun NavigationSettings(context: Context) {
Section(title = stringResource(id = R.string.routing_engine)) {
val state = remember {
mutableIntStateOf(
NavigationUtils.getIntKeyValue(
context,
ROUTING_ENGINE
)
settingsViewModel.routingEngine.value
)
}
RoutingEngineData.engines.forEach { sampleItem ->
@@ -136,7 +125,7 @@ private fun NavigationSettings(context: Context) {
title = { Text(text = sampleItem.title) },
onClick = {
state.intValue = sampleItem.key
NavigationUtils.setIntKeyValue(context, state.intValue, ROUTING_ENGINE)
settingsViewModel.onRoutingEngineChanged(state.intValue)
},
)
}

View File

@@ -0,0 +1,37 @@
package com.kouros.navigation.ui.settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
@Composable
fun SettingsRoute(route: String, navController: NavHostController, function: () -> Boolean) {
val context = LocalContext.current
val dataStoreManager = remember { DataStoreManager(context) }
val repository = remember { SettingsRepository(dataStoreManager) }
val viewModel: SettingsViewModel = viewModel(
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return SettingsViewModel(repository) as T
}
}
)
if (route == "display_settings") {
SettingsScreen(viewModel = viewModel)
}
if (route == "nav_settings") {
SettingsScreen(viewModel = viewModel)
}
///DisplaySettings(context, viewModel, navController.popBackStack())
}

View File

@@ -0,0 +1,151 @@
package com.kouros.navigation.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.kouros.data.R
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.ui.components.RadioButtonSingleSelection
import com.kouros.navigation.ui.components.SectionTitle
import com.kouros.navigation.ui.components.SettingSwitch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(viewModel: SettingsViewModel) {
val darkMode by viewModel.darkMode.collectAsState()
val threedBuilding by viewModel.threedBuilding.collectAsState()
val avoidMotorway by viewModel.avoidMotorway.collectAsState()
val avoidTollway by viewModel.avoidTollway.collectAsState()
val carLocation by viewModel.carLocation.collectAsState()
val routingEngine by viewModel.routingEngine.collectAsState()
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
stringResource(id = R.string.display_settings),
)
},
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),
// )
// }
},
)
},
)
{ padding ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = 20.dp)
.verticalScroll(scrollState)
) {
Text(
text = stringResource(R.string.settings_action_title),
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(24.dp))
// Appearance
SectionTitle(stringResource(R.string.threed_building))
SettingSwitch(
title = stringResource(R.string.threed_building),
checked = threedBuilding,
onCheckedChange = viewModel::onThreedBuildingChanged
)
SectionTitle(stringResource(R.string.dark_mode))
val radioOptions = listOf(
stringResource(R.string.off_action_title),
stringResource(R.string.on_action_title),
stringResource(R.string.use_telephon_settings)
)
RadioButtonSingleSelection(
modifier = Modifier.padding(),
selectedOption = darkMode,
radioOptions = radioOptions,
onClick = viewModel::onDarkModeChanged
)
// Appearance
SectionTitle(stringResource(R.string.navigation_settings))
SettingSwitch(
title = stringResource(R.string.avoid_highways_row_title),
checked = avoidMotorway,
onCheckedChange = viewModel::onAvoidMotorway
)
SettingSwitch(
title = stringResource(R.string.avoid_tolls_row_title),
checked = avoidTollway,
onCheckedChange = viewModel::onAvoidTollway
)
SettingSwitch(
title = stringResource(R.string.use_car_location),
checked = carLocation,
onCheckedChange = viewModel::onCarLocation
)
SectionTitle(stringResource(R.string.routing_engine))
val routingEngineOptions = listOf(
stringResource(R.string.valhalla),
stringResource(R.string.osrm),
stringResource(R.string.tomtom)
)
RadioButtonSingleSelection(
modifier = Modifier.padding(),
selectedOption = routingEngine,
radioOptions = routingEngineOptions,
onClick = viewModel::onRoutingEngineChanged
)
}
}
}

View File

@@ -102,7 +102,7 @@ fun NavigationTheme(
}
MaterialTheme(
colorScheme = colorScheme,
colorScheme = if (useDarkTheme) darkColorScheme() else colorScheme,
typography = typography,
content = content,
shapes = shapes,

View File

@@ -56,6 +56,7 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.androidx.compose.ui.text)
implementation(libs.play.services.location)
implementation(libs.androidx.datastore.core)
androidTestImplementation(libs.androidx.junit)
testImplementation(libs.junit)
}

View File

@@ -2,6 +2,7 @@ package com.kouros.navigation.car
import android.Manifest
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@@ -26,6 +27,9 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.lifecycleScope
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.car.screen.RequestPermissionScreen
@@ -35,13 +39,20 @@ import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.datastore.DataStoreManager
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.ViewModel
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class NavigationSession : Session(), NavigationScreen.Listener {
@@ -54,7 +65,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
lateinit var surfaceRenderer: SurfaceRenderer
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
val repository = getSettingsRepository(carContext)
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
if (!useCarLocation) {
updateLocation(location!!)
}
@@ -75,7 +87,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
override fun onDestroy(owner: LifecycleOwner) {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
val repository = getSettingsRepository(carContext)
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
if (useCarLocation) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
carSensors.removeCarHardwareLocationListener(carLocationListener)
@@ -88,8 +101,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
}
lateinit var navigationViewModel: ViewModel
lateinit var navigationViewModel: NavigationViewModel
lateinit var viewModelStoreOwner : ViewModelStoreOwner
val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
OnCarDataAvailableListener { data ->
if (data.location.status == CarValue.STATUS_SUCCESS) {
@@ -123,22 +137,39 @@ class NavigationSession : Session(), NavigationScreen.Listener {
fun onRoutingEngineStateUpdated(routeEngine : Int) {
navigationViewModel = when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
else -> ViewModel(TomTomRepository())
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
else -> NavigationViewModel(TomTomRepository())
}
}
override fun onCreateScreen(intent: Intent): Screen {
viewModelStoreOwner = object : ViewModelStoreOwner {
override val viewModelStore = ViewModelStore()
}
lifecycleScope.launch {
try {
awaitCancellation()
} finally {
viewModelStoreOwner.viewModelStore.clear()
}
}
// lifecycleScope.launch {
//}
navigationViewModel = getViewModel(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
routeModel = RouteCarModel()
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel)
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
navigationScreen =
NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
@@ -175,7 +206,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
fun addSensors() {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
val useCarLocation = getBooleanKeyValue(carContext, CAR_LOCATION)
val repository = getSettingsRepository(carContext)
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
if (useCarLocation) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL,
@@ -269,8 +301,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
}
override fun stopNavigation() {
routeModel.stopNavigation()
override fun stopNavigation(context: CarContext) {
routeModel.stopNavigation(context)
}

View File

@@ -14,6 +14,7 @@ import androidx.car.app.connection.CarConnection
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.ComposeView
@@ -21,6 +22,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.kouros.navigation.car.map.DrawNavigationImages
@@ -29,21 +31,23 @@ import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.tomtom.TrafficData
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.duration
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.previewZoom
import com.kouros.navigation.utils.settingsViewModel
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.style.BaseStyle
@@ -52,7 +56,8 @@ import org.maplibre.spatialk.geojson.Position
class SurfaceRenderer(
private var carContext: CarContext, lifecycle: Lifecycle,
private var routeModel: RouteCarModel
private var routeModel: RouteCarModel,
private var viewModelStoreOwner: ViewModelStoreOwner
) : DefaultLifecycleObserver {
var lastLocation = location(0.0, 0.0)
@@ -187,9 +192,12 @@ class SurfaceRenderer(
@Composable
fun MapView() {
val darkMode = getIntKeyValue(carContext, Constants.DARK_MODE_SETTINGS)
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
//val appViewModel: AppViewModel = appViewModel(viewModelStoreOwner)
//val darkMode by appViewModel.darkMode.collectAsState()
val darkMode = settingsViewModel(carContext, viewModelStoreOwner).darkMode.collectAsState().value
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState()
val traffic: Map<String, String> ? by trafficData.observeAsState()
@@ -197,7 +205,17 @@ class SurfaceRenderer(
val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt)
val rememberBaseStyle = rememberBaseStyle(baseStyle)
MapLibre(carContext, cameraState, rememberBaseStyle, route, traffic, viewStyle, speedCameras)
MapLibre(
carContext,
cameraState,
rememberBaseStyle,
route,
traffic,
viewStyle,
speedCameras,
false
)
ShowPosition(cameraState, position, paddingValues)
}
@@ -339,7 +357,8 @@ class SurfaceRenderer(
}
fun updateCarLocation(location: Location) {
val routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
val repository = getSettingsRepository(carContext)
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
if (routingEngine == RouteEngine.OSRM.ordinal) {
updateLocation(location)
}

View File

@@ -26,15 +26,18 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.kouros.data.R
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
@@ -92,7 +95,8 @@ fun MapLibre(
route: String?,
traffic: Map<String, String>?,
viewStyle: ViewStyle,
speedCameras: String? = ""
speedCameras: String? = "",
showBuildings: Boolean
) {
MaplibreMap(
options = MapOptions(
@@ -103,7 +107,7 @@ fun MapLibre(
baseStyle = baseStyle
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!getBooleanKeyValue(context = context, SHOW_THREED_BUILDING)) {
if (!showBuildings) {
BuildingLayer(tiles)
}
if (viewStyle == ViewStyle.AMENITY_VIEW) {
@@ -553,3 +557,4 @@ fun PuckState(cameraState: CameraState, userLocationState: UserLocationState) {
)
}

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.screen
import android.location.Location
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
@@ -14,17 +13,16 @@ import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.model.NavigationViewModel
class CategoriesScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
private val viewModel: ViewModel,
private val navigationViewModel: NavigationViewModel,
) : Screen(carContext) {
var categories: List<Category> = listOf(
@@ -48,7 +46,7 @@ class CategoriesScreen(
carContext,
surfaceRenderer,
it.id,
viewModel
navigationViewModel
)
) { obj: Any? ->
if (obj != null) {

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.screen
import android.location.Location
import androidx.annotation.DrawableRes
import androidx.car.app.CarContext
import androidx.car.app.Screen
@@ -19,11 +18,10 @@ import androidx.lifecycle.Observer
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationMessage
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.round
@@ -33,7 +31,7 @@ class CategoryScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
private val category: String,
private val viewModel: ViewModel,
private val navigationViewModel: NavigationViewModel,
) : Screen(carContext) {
var elements = listOf<Elements>()
@@ -57,8 +55,8 @@ class CategoryScreen(
}
init {
viewModel.elements.observe(this, observer)
viewModel.getAmenities(category, surfaceRenderer.lastLocation)
navigationViewModel.elements.observe(this, observer)
navigationViewModel.getAmenities(category, surfaceRenderer.lastLocation)
}
@@ -130,7 +128,7 @@ class CategoryScreen(
row.addAction(
Action.Builder()
.setOnClickListener {
viewModel.loadRoute(
navigationViewModel.loadRoute(
carContext,
currentLocation = surfaceRenderer.lastLocation,
location(it.lon!!, it.lat!!),

View File

@@ -10,23 +10,27 @@ import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.data.Constants.DARK_MODE_SETTINGS
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.launch
class DarkModeSettings(private val carContext: CarContext) : Screen(carContext) {
private var darkModeSettings = 0
val settingsViewModel = getSettingsViewModel(carContext)
init {
darkModeSettings = getIntKeyValue(carContext, DARK_MODE_SETTINGS)
lifecycleScope.launch {
settingsViewModel.darkMode.collect {
invalidate()
}
}
}
override fun onGetTemplate(): Template {
darkModeSettings = settingsViewModel.darkMode.value
val templateBuilder = ListTemplate.Builder()
val radioList =
ItemList.Builder()
@@ -52,10 +56,12 @@ class DarkModeSettings(private val carContext: CarContext) : Screen(carContext)
.build()
return templateBuilder
.addSectionedList(SectionedItemList.create(
.addSectionedList(
SectionedItemList.create(
radioList,
carContext.getString(R.string.dark_mode)
))
)
)
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.dark_mode))
@@ -67,7 +73,7 @@ class DarkModeSettings(private val carContext: CarContext) : Screen(carContext)
private fun onSelected(index: Int) {
setIntKeyValue(carContext, index, DARK_MODE_SETTINGS)
settingsViewModel.onDarkModeChanged(index)
CarToast.makeText(
carContext,
(carContext

View File

@@ -6,33 +6,34 @@ import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.OnClickListener
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.launch
class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
private var buildingToggleState = false
val settingsViewModel = getSettingsViewModel(carContext)
init {
buildingToggleState = getBooleanKeyValue(carContext, SHOW_THREED_BUILDING)
lifecycleScope.launch {
settingsViewModel.threedBuilding.collect {
invalidate()
}
}
}
override fun onGetTemplate(): Template {
buildingToggleState = settingsViewModel.threedBuilding.value
val listBuilder = ItemList.Builder()
val buildingToggle: Toggle =
Toggle.Builder { checked: Boolean ->
if (checked) {
setBooleanKeyValue(carContext, true, SHOW_THREED_BUILDING)
} else {
setBooleanKeyValue(carContext, false, SHOW_THREED_BUILDING)
}
settingsViewModel.onThreedBuildingChanged(checked)
buildingToggleState = !buildingToggleState
}.setChecked(buildingToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.threed_building, buildingToggle))

View File

@@ -22,6 +22,7 @@ import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.RoutingInfo
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
@@ -29,11 +30,18 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.absoluteValue
@@ -43,21 +51,21 @@ class NavigationScreen(
private var surfaceRenderer: SurfaceRenderer,
private var routeModel: RouteCarModel,
private var listener: Listener,
private val viewModel: ViewModel
private val navigationViewModel: NavigationViewModel
) :
Screen(carContext) {
/** A listener for navigation start and stop signals. */
interface Listener {
/** Stops navigation. */
fun stopNavigation()
fun stopNavigation(context: CarContext)
}
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
var recentPlace = Place()
var navigationType = NavigationType.VIEW
var lastTrafficDate = LocalDateTime.of(1960, 6, 21, 0, 0)
var lastTrafficDate: LocalDateTime? = LocalDateTime.of(1960, 6, 21, 0, 0)
val observer = Observer<String> { route ->
if (route.isNotEmpty()) {
navigationType = NavigationType.NAVIGATION
@@ -99,7 +107,7 @@ class NavigationScreen(
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
cameras.forEach {
coordinates.add(listOf(it.lon!!, it.lat!!))
coordinates.add(listOf(it.lon, it.lat))
}
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCamerasData.value = speedData
@@ -107,11 +115,15 @@ class NavigationScreen(
init {
viewModel.route.observe(this, observer)
viewModel.traffic.observe(this, trafficObserver);
viewModel.recentPlace.observe(this, recentObserver)
viewModel.placeLocation.observe(this, placeObserver)
viewModel.speedCameras.observe(this, speedObserver)
navigationViewModel.route.observe(this, observer)
navigationViewModel.traffic.observe(this, trafficObserver);
navigationViewModel.recentPlace.observe(this, recentObserver)
navigationViewModel.placeLocation.observe(this, placeObserver)
navigationViewModel.speedCameras.observe(this, speedObserver)
lifecycleScope.launch {
getSettingsViewModel(carContext).routingEngine.collect {
}
}
}
override fun onGetTemplate(): Template {
@@ -306,7 +318,7 @@ class NavigationScreen(
)
.setOnClickListener {
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
viewModel.loadRoute(
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
navigateTo,
@@ -349,7 +361,7 @@ class NavigationScreen(
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
.setOnClickListener {
screenManager.push(SettingsScreen(carContext, viewModel))
screenManager.push(SettingsScreen(carContext, navigationViewModel))
}
.build()
}
@@ -411,13 +423,13 @@ class NavigationScreen(
SearchScreen(
carContext,
surfaceRenderer,
viewModel
navigationViewModel
)
) { obj: Any? ->
if (obj != null) {
val place = obj as Place
if (place.longitude == 0.0) {
viewModel.findAddress(
navigationViewModel.findAddress(
"${obj.city} ${obj.street}},",
currentNavigationLocation
)
@@ -432,9 +444,9 @@ class NavigationScreen(
fun navigateToPlace(place: Place) {
navigationType = NavigationType.VIEW
val location = location(place.longitude, place.latitude)
viewModel.saveRecent(place)
navigationViewModel.saveRecent(place)
currentNavigationLocation = location
viewModel.loadRoute(
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
@@ -446,7 +458,7 @@ class NavigationScreen(
fun stopNavigation() {
navigationType = NavigationType.VIEW
listener.stopNavigation()
listener.stopNavigation(carContext)
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
invalidate()
@@ -470,7 +482,7 @@ class NavigationScreen(
fun reRoute(destination: Place) {
val dest = location(destination.longitude, destination.latitude)
viewModel.loadRoute(
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
dest,
@@ -480,14 +492,14 @@ class NavigationScreen(
fun updateTrip(location: Location) {
val current = LocalDateTime.now(ZoneOffset.UTC)
val duration = java.time.Duration.between(current, lastTrafficDate)
val duration = Duration.between(current, lastTrafficDate)
if (duration.abs().seconds > 360) {
lastTrafficDate = current
viewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
updateSpeedCamera(location)
with(routeModel) {
updateLocation(location, viewModel)
updateLocation(carContext,location, navigationViewModel)
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
@@ -506,7 +518,7 @@ class NavigationScreen(
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
viewModel.getSpeedCameras(location, 5.0)
navigationViewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)

View File

@@ -9,16 +9,14 @@ import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
import com.kouros.navigation.data.Constants.AVOID_TOLLWAY
import com.kouros.navigation.data.Constants.CAR_LOCATION
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.launch
class NavigationSettings(private val carContext: CarContext, private var viewModel: ViewModel) :
class NavigationSettings(private val carContext: CarContext, private var navigationViewModel: NavigationViewModel) :
Screen(carContext) {
private var motorWayToggleState = false
@@ -27,25 +25,25 @@ class NavigationSettings(private val carContext: CarContext, private var viewMod
private var carLocationToggleState = false
val settingsViewModel = getSettingsViewModel(carContext)
init {
motorWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
tollWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
carLocationToggleState = getBooleanKeyValue(carContext, CAR_LOCATION)
lifecycleScope.launch {
settingsViewModel.avoidTollway.collect {
invalidate()
}
}
}
override fun onGetTemplate(): Template {
motorWayToggleState = settingsViewModel.avoidMotorway.value
tollWayToggleState = settingsViewModel.avoidTollway.value
carLocationToggleState = settingsViewModel.carLocation.value
val listBuilder = ItemList.Builder()
val highwayToggle: Toggle =
Toggle.Builder { checked: Boolean ->
if (checked) {
setBooleanKeyValue(carContext, true, AVOID_MOTORWAY)
} else {
setBooleanKeyValue(carContext, false, AVOID_MOTORWAY)
}
settingsViewModel.onAvoidMotorway(checked)
motorWayToggleState = !motorWayToggleState
}.setChecked(motorWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_highways_row_title, highwayToggle))
@@ -53,22 +51,14 @@ class NavigationSettings(private val carContext: CarContext, private var viewMod
// Tollway
val tollwayToggle: Toggle =
Toggle.Builder { checked: Boolean ->
if (checked) {
setBooleanKeyValue(carContext, true, AVOID_TOLLWAY)
} else {
setBooleanKeyValue(carContext, false, AVOID_TOLLWAY)
}
settingsViewModel.onAvoidTollway(checked)
tollWayToggleState = !tollWayToggleState
}.setChecked(tollWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
val carLocationToggle: Toggle =
Toggle.Builder { checked: Boolean ->
if (checked) {
setBooleanKeyValue(carContext, true, CAR_LOCATION)
} else {
setBooleanKeyValue(carContext, false, CAR_LOCATION)
}
settingsViewModel.onCarLocation(checked)
carLocationToggleState = !carLocationToggleState
}.setChecked(carLocationToggleState).build()
@@ -81,7 +71,7 @@ class NavigationSettings(private val carContext: CarContext, private var viewMod
listBuilder.addItem(
buildRowForScreenTemplate(
RoutingSettings(carContext, viewModel),
RoutingSettings(carContext, navigationViewModel),
R.string.routing_engine
)
)

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.screen
import android.location.Location
import android.net.Uri
import android.text.Spannable
import android.text.SpannableString
@@ -25,14 +24,14 @@ import com.kouros.navigation.data.Constants.CONTACTS
import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.model.NavigationViewModel
class PlaceListScreen(
private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer,
private val category: String,
private val viewModel: ViewModel
private val navigationViewModel: NavigationViewModel
) : Screen(carContext) {
var places = listOf<Place>()
@@ -49,30 +48,30 @@ class PlaceListScreen(
init {
if (category == RECENT) {
viewModel.places.observe(this, observer)
navigationViewModel.places.observe(this, observer)
}
if (category == CONTACTS) {
viewModel.contactAddress.observe(this, observerAddress)
navigationViewModel.contactAddress.observe(this, observerAddress)
}
if (category == FAVORITES) {
viewModel.favorites.observe(this, observer)
navigationViewModel.favorites.observe(this, observer)
}
loadPlaces()
}
fun loadPlaces() {
if (category == RECENT) {
viewModel.loadRecentPlaces(
navigationViewModel.loadRecentPlaces(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
}
if (category == CONTACTS) {
viewModel.loadContacts(carContext)
navigationViewModel.loadContacts(carContext)
}
if (category == FAVORITES) {
viewModel.loadFavorites(
navigationViewModel.loadFavorites(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
@@ -110,7 +109,7 @@ class PlaceListScreen(
carContext,
surfaceRenderer,
place,
viewModel
navigationViewModel
)
) { obj: Any? ->
if (obj != null) {
@@ -163,7 +162,7 @@ class PlaceListScreen(
)
)
.setOnClickListener {
viewModel.deletePlace(place)
navigationViewModel.deletePlace(place)
CarToast.makeText(
carContext,
R.string.recent_Item_deleted, CarToast.LENGTH_LONG

View File

@@ -6,7 +6,6 @@ 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
@@ -29,7 +28,7 @@ import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationMessage
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.location
import java.math.BigDecimal
import java.math.RoundingMode
@@ -39,7 +38,7 @@ class RoutePreviewScreen(
carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer,
private var destination: Place,
private val viewModel: ViewModel
private val navigationViewModel: NavigationViewModel
) :
Screen(carContext) {
private var isFavorite = false
@@ -56,9 +55,9 @@ class RoutePreviewScreen(
}
init {
viewModel.previewRoute.observe(this, observer)
navigationViewModel.previewRoute.observe(this, observer)
val location = location(destination.longitude, destination.latitude)
viewModel.loadPreviewRoute(
navigationViewModel.loadPreviewRoute(
carContext,
surfaceRenderer.lastLocation,
location,
@@ -164,7 +163,7 @@ class RoutePreviewScreen(
CarToast.LENGTH_SHORT
)
.show()
viewModel.saveFavorite(destination)
navigationViewModel.saveFavorite(destination)
invalidate()
}
.build()
@@ -172,7 +171,7 @@ class RoutePreviewScreen(
private fun deleteFavoriteAction(): Action = Action.Builder()
.setOnClickListener {
if (isFavorite) {
viewModel.deleteFavorite(destination)
navigationViewModel.deleteFavorite(destination)
}
isFavorite = !isFavorite
finish()

View File

@@ -10,28 +10,28 @@ import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
import com.kouros.navigation.data.Constants.CAR_LOCATION
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setIntKeyValue
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.launch
class RoutingSettings(private val carContext: CarContext, private var viewModel: ViewModel) : Screen(carContext) {
class RoutingSettings(private val carContext: CarContext, private var navigationViewModel: NavigationViewModel) : Screen(carContext) {
private var routingEngine = RouteEngine.OSRM.ordinal
val settingsViewModel = getSettingsViewModel(carContext)
init {
routingEngine = getIntKeyValue(carContext, ROUTING_ENGINE)
lifecycleScope.launch {
settingsViewModel.routingEngine.collect {
invalidate()
}
}
}
override fun onGetTemplate(): Template {
routingEngine = settingsViewModel.routingEngine.value
val templateBuilder = ListTemplate.Builder()
val radioList =
ItemList.Builder()
@@ -71,8 +71,8 @@ class RoutingSettings(private val carContext: CarContext, private var viewModel:
}
private fun onSelected(index: Int) {
setIntKeyValue(carContext, index, ROUTING_ENGINE)
viewModel.routingEngine.value = index
settingsViewModel.onRoutingEngineChanged(index)
navigationViewModel.routingEngine.value = index
CarToast.makeText(
carContext,
(carContext

View File

@@ -1,7 +1,6 @@
package com.kouros.navigation.car.screen
import android.annotation.SuppressLint
import android.location.Location
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
@@ -16,18 +15,17 @@ import androidx.lifecycle.Observer
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.model.NavigationViewModel
class SearchScreen(
carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer,
private val viewModel: ViewModel,
private val navigationViewModel: NavigationViewModel,
) : Screen(carContext) {
var isSearchComplete: Boolean = false
@@ -47,7 +45,7 @@ class SearchScreen(
}
init {
viewModel.searchPlaces.observe(this, observer)
navigationViewModel.searchPlaces.observe(this, observer)
}
override fun onGetTemplate(): Template {
@@ -71,7 +69,7 @@ class SearchScreen(
CategoriesScreen(
carContext,
surfaceRenderer,
viewModel
navigationViewModel
)
) { obj: Any? ->
surfaceRenderer.viewStyle = ViewStyle.VIEW
@@ -87,7 +85,7 @@ class SearchScreen(
carContext,
surfaceRenderer,
it.id,
viewModel
navigationViewModel
)
) { obj: Any? ->
if (obj != null) {
@@ -115,7 +113,7 @@ class SearchScreen(
object : SearchCallback {
override fun onSearchSubmitted(searchTerm: String) {
isSearchComplete = true
viewModel.searchPlaces(searchTerm, surfaceRenderer.lastLocation)
navigationViewModel.searchPlaces(searchTerm, surfaceRenderer.lastLocation)
}
})
.setHeaderAction(Action.BACK)

View File

@@ -24,12 +24,12 @@ import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import com.kouros.data.R
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.model.NavigationViewModel
/** A screen demonstrating selectable lists. */
class SettingsScreen(
carContext: CarContext,
private var viewModel: ViewModel,
private var navigationViewModel: NavigationViewModel,
) : Screen(carContext) {
override fun onGetTemplate(): Template {
@@ -42,7 +42,7 @@ class SettingsScreen(
)
listBuilder.addItem(
buildRowForTemplate(
NavigationSettings(carContext, viewModel),
NavigationSettings(carContext, navigationViewModel),
R.string.navigation_settings
)
)

View File

@@ -2,7 +2,7 @@ package com.kouros.navigation.car
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.model.NavigationViewModel
import org.junit.Test
/**
@@ -12,7 +12,7 @@ import org.junit.Test
class ViewModelTest {
val repo = ValhallaRepository()
val viewModel = ViewModel(repo)
val navigationViewModel = NavigationViewModel(repo)
val model = RouteModel()
@Test

View File

@@ -5,6 +5,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "2.2.21"
kotlin("kapt")
}
@@ -19,6 +20,10 @@ android {
consumerProguardFiles("consumer-rules.pro")
}
buildFeatures {
compose = true
}
buildTypes {
release {
isMinifyEnabled = false
@@ -40,6 +45,10 @@ android {
}
dependencies {
val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
@@ -49,6 +58,7 @@ dependencies {
implementation(libs.koin.compose.viewmodel)
implementation(libs.androidx.car.app)
implementation(libs.android.sdk.turf)
implementation(libs.androidx.compose.runtime)
// objectbox
@@ -56,6 +66,8 @@ dependencies {
implementation(libs.androidx.material3)
annotationProcessor(libs.objectbox.processor)
implementation(libs.androidx.datastore.preferences)
implementation(libs.kotlinx.serialization.json)
implementation(libs.maplibre.compose)
implementation("androidx.compose.material:material-icons-extended:1.7.8")

View File

@@ -139,7 +139,9 @@ object Constants {
const val CAR_LOCATION = "CarLocation"
const val ROUTING_ENGINE = "RoutingEngine"
const val NEXT_STEP_THRESHOLD = 120.0
const val LAST_ROUTE = "LastRoute"
const val NEXT_STEP_THRESHOLD = 500.0
const val MAXIMAL_SNAP_CORRECTION = 50.0
@@ -157,3 +159,12 @@ object Constants {
enum class RouteEngine {
VALHALLA, OSRM, TOMTOM, GRAPHHOPPER
}
enum class NavigationThemeColor(val color: Long) {
RED(0xFFD32F2F),
ORANGE(0xFFF57C00),
YELLOW(0xFFFBC02D),
GREEN(0xFF388E3C),
BLUE(0xFF1976D2),
PURPLE(0xFF7B1FA2)
}

View File

@@ -18,7 +18,11 @@ package com.kouros.navigation.data
import android.content.Context
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.data.R
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.osrm.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import java.net.Authenticator
@@ -50,10 +54,18 @@ abstract class NavigationRepository {
searchFilter: SearchFilter,
context: Context
): Double {
val route = getRoute(context, currentLocation, location, carOrientation, searchFilter)
val routeModel = RouteModel()
routeModel.startNavigation(route, context)
return routeModel.curRoute.summary.distance
val osrm = OsrmRepository()
val route = osrm.getRoute(context, currentLocation, location, carOrientation, searchFilter)
val gson = GsonBuilder().serializeNulls().create()
val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
if (osrmJson.routes.isEmpty()) {
return 0.0
}
return osrmJson.routes.first().distance
// return osrmJson.destinations.first().distance?.toDouble() ?: 0.0
///val routeModel = RouteModel()
//routeModel.startNavigation(route, context)
//return routeModel.curRoute.summary.distance
}
fun searchPlaces(search: String, location: Location): String {

View File

@@ -99,8 +99,8 @@ data class Route(
}
}
fun nextStep(steps : Int): Step {
val nextIndex = currentStepIndex + steps
fun nextStep(add: Int): Step {
val nextIndex = currentStepIndex + add
return if (isRouteValid() && nextIndex < legs().first().steps.size) {
legs().first().steps[nextIndex]
} else {

View File

@@ -0,0 +1,111 @@
package com.kouros.navigation.data.datastore
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
private const val DATASTORE_NAME = "navigation_settings"
/**
* Central manager for app settings using DataStore
*/
class DataStoreManager(private val context: Context) {
companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
}
// Keys
private object PreferencesKeys {
val THREED_BUILDING = booleanPreferencesKey("Show3D")
val DARK_MODE = intPreferencesKey("DarkMode")
val AVOID_MOTORWAY = booleanPreferencesKey("AvoidMotorway")
val AVOID_TOLLWAY = booleanPreferencesKey("AvoidTollway")
val CAR_LOCATION = booleanPreferencesKey("CarLocation")
val ROUTING_ENGINE = intPreferencesKey("RoutingEngine")
}
// Read values
val threeDBuildingFlow: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.THREED_BUILDING] == true
}
val darkModeFlow: Flow<Int> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.DARK_MODE]
?: 0
}
val avoidMotorwayFlow: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.AVOID_MOTORWAY] == true
}
val avoidTollwayFlow: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.AVOID_TOLLWAY] == true
}
val useCarLocationFlow: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.CAR_LOCATION] == true
}
val routingEngineFlow: Flow<Int> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.ROUTING_ENGINE]
?: 0
}
// Save values
suspend fun setThreedBuilding(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.THREED_BUILDING] = enabled
}
}
suspend fun setDarkMode(mode: Int) {
context.dataStore.edit { prefs ->
prefs[PreferencesKeys.DARK_MODE] = mode
}
}
suspend fun setAvoidMotorway(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.AVOID_MOTORWAY] = enabled
}
}
suspend fun setAvoidTollway(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.AVOID_TOLLWAY] = enabled
}
}
suspend fun setCarLocation(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.CAR_LOCATION] = enabled
}
}
suspend fun setRoutingEngine(mode: Int) {
context.dataStore.edit { prefs ->
prefs[PreferencesKeys.ROUTING_ENGINE] = mode
}
}
}

View File

@@ -5,7 +5,9 @@ import android.location.Location
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
private const val routeUrl = "https://kouros-online.de/osrm/route/v1/driving/"
//private const val routeUrl = "https://kouros-online.de/osrm/route/v1/driving/"
private const val routeUrl = "https://router.project-osrm.org/route/v1/driving/"
class OsrmRepository : NavigationRepository() {
override fun getRoute(
@@ -23,7 +25,7 @@ class OsrmRepository : NavigationRepository() {
if (searchFilter.avoidTollway) {
exclude = "$exclude&exclude=toll"
}
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true&alternatives=0"
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true&alternatives=false"
return fetchUrl(routeUrl + routeLocation + exclude, true)
}

View File

@@ -10,6 +10,7 @@ import java.net.URL
class Overpass {
//val overpassUrl = "https://overpass.kumi.systems/api/interpreter"
//val overpassUrl = "https://overpass-api.de/api"
val overpassUrl = "https://kouros-online.de/overpass/interpreter"
@@ -43,13 +44,13 @@ class Overpass {
val boundingBox = getBoundingBox(location.latitude, location.longitude, radius)
val httpURLConnection = URL(overpassUrl).openConnection() as HttpURLConnection
httpURLConnection.requestMethod = "POST"
// node["highway"="speed_camera"]
// node[amenity=$category]
httpURLConnection.setDoOutput(true);
httpURLConnection.setRequestProperty(
"Accept",
"application/json"
)
// node["highway"="speed_camera"]
// node[amenity=$category]
httpURLConnection.setDoOutput(true);
// define search query
val searchQuery = """
|[out:json];
@@ -79,7 +80,7 @@ class Overpass {
}
} catch (e: Exception) {
println("Speed $e")
}
return emptyList()
}

View File

@@ -10,15 +10,14 @@ import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
private const val routeUrl = "https://api.tomtom.com/routing/1/calculateRoute/"
val tomtomApiKey = "678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
const val tomtomApiKey = "678k5v6940cSXXIS5oD92qIrDgW3RBZ3"
val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incidentDetails"
const val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incidentDetails"
private val tomtomFields =
private const val tomtomFields =
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
const val useAsset = false
const val useAsset = true
class TomTomRepository : NavigationRepository() {
override fun getRoute(

View File

@@ -61,9 +61,7 @@ class TomTomRoute {
route.sections?.forEach { section ->
val lanes = mutableListOf<Lane>()
var startIndex = 0
if (section.startPointIndex <= instruction.pointIndex - 3
&& instruction.pointIndex <= section.endPointIndex
) {
section.lanes?.forEach { itLane ->
val lane = Lane(
location(
@@ -77,7 +75,7 @@ class TomTomRoute {
lanes.add(lane)
}
intersections.add(Intersection(waypoints[startIndex], lanes))
}
}
allIntersections.addAll(intersections)
stepDistance = route.guidance.instructions[index].routeOffsetInMeters - stepDistance
@@ -166,13 +164,23 @@ class TomTomRoute {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_RIGHT
}
"ROUNDABOUT_RIGHT" -> {
"ROUNDABOUT_RIGHT", "ROUNDABOUT_CROSS" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CCW
}
"ROUNDABOUT_LEFT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_ROUNDABOUT_ENTER_CW
}
"MAKE_UTURN" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_U_TURN_LEFT
}
"ENTER_MOTORWAY" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_MERGE_LEFT
}
"TAKE_EXIT" -> {
newType = androidx.car.app.navigation.model.Maneuver.TYPE_TURN_SHARP_RIGHT
}
}
return newType
}

View File

@@ -66,30 +66,19 @@ class IconMapper() {
currentTurnIcon = R.drawable.ic_roundabout_ccw
}
Maneuver.TYPE_U_TURN_LEFT -> {
currentTurnIcon = R.drawable.ic_turn_u_turn_left
}
Maneuver.TYPE_U_TURN_RIGHT -> {
currentTurnIcon = R.drawable.ic_turn_u_turn_right
}
Maneuver.TYPE_MERGE_LEFT -> {
currentTurnIcon = R.drawable.ic_turn_merge_symmetrical
}
}
return currentTurnIcon
}
fun addLanes(stepData: StepData) : Int {
stepData.lane.forEach {
if (it.indications.isNotEmpty() && it.valid) {
Collections.sort<String>(it.indications)
var direction = ""
it.indications.forEach { it2 ->
direction = if (direction.isEmpty()) {
it2.trim()
} else {
"${direction}_${it2.trim()}"
}
}
val laneDirection = addLanes(direction, stepData)
return laneDirection
}
}
return 0
}
fun addLanes(direction: String, stepData: StepData): Int {
val laneDirection = when (direction.lowercase(Locale.getDefault())) {
"left_straight" -> {
@@ -112,6 +101,8 @@ class IconMapper() {
"straight" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_STRAIGHT -> LaneDirection.SHAPE_STRAIGHT
Maneuver.TYPE_KEEP_LEFT -> LaneDirection.SHAPE_STRAIGHT
Maneuver.TYPE_KEEP_RIGHT -> LaneDirection.SHAPE_STRAIGHT
else
-> LaneDirection.SHAPE_UNKNOWN
}
@@ -136,7 +127,8 @@ class IconMapper() {
"left_slight", "slight_left" -> {
when (stepData.currentManeuverType) {
Maneuver.TYPE_TURN_SLIGHT_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
Maneuver.TYPE_TURN_NORMAL_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
Maneuver.TYPE_KEEP_LEFT -> LaneDirection.SHAPE_SLIGHT_LEFT
else
-> LaneDirection.SHAPE_UNKNOWN
}

View File

@@ -2,6 +2,7 @@ package com.kouros.navigation.model
import android.content.Context
import android.location.Location
import androidx.car.app.CarContext
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.MutableLiveData
@@ -18,21 +19,21 @@ import com.kouros.navigation.data.nominatim.Search
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.data.tomtom.Features
import com.kouros.navigation.data.tomtom.Traffic
import com.kouros.navigation.data.tomtom.TrafficData
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.Levenshtein
import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.maplibre.geojson.FeatureCollection
import java.time.LocalDateTime
import java.time.ZoneOffset
class ViewModel(private val repository: NavigationRepository) : ViewModel() {
class NavigationViewModel(private val repository: NavigationRepository) : ViewModel() {
val route: MutableLiveData<String> by lazy {
MutableLiveData()
@@ -457,14 +458,9 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
fun getSearchFilter(context: Context): SearchFilter {
val avoidMotorway = NavigationUtils.getBooleanKeyValue(
context = context,
Constants.AVOID_MOTORWAY
)
val avoidTollway = NavigationUtils.getBooleanKeyValue(
context = context,
Constants.AVOID_TOLLWAY
)
val repository = getSettingsRepository(context)
val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() }
val avoidTollway = runBlocking { repository.avoidTollwayFlow.first() }
return SearchFilter(avoidMotorway, avoidTollway)
}

View File

@@ -90,7 +90,7 @@ class RouteCalculator(var routeModel: RouteModel) {
return nowUtcMillis + timeToDestinationMillis
}
fun updateSpeedLimit(location: Location, viewModel: ViewModel) {
fun updateSpeedLimit(location: Location, viewModel: NavigationViewModel) {
if (routeModel.isNavigating()) {
// speed limit
val distance = lastSpeedLocation.distanceTo(location)

View File

@@ -4,15 +4,19 @@ import android.content.Context
import android.location.Location
import androidx.car.app.navigation.model.Maneuver
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.data.route.Lane
import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.utils.NavigationUtils.getIntKeyValue
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlin.math.absoluteValue
open class RouteModel {
@@ -46,15 +50,17 @@ open class RouteModel {
get() = navState.route.routes[navState.currentRouteIndex].legs.first()
fun startNavigation(routeString: String, context: Context) {
val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
val repository = getSettingsRepository(context)
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
navState = navState.copy(
route = Route.Builder()
.routeEngine(routeEngine)
.routeEngine(routingEngine)
.route(routeString)
.build()
)
if (hasLegs()) {
navState = navState.copy(navigating = true)
//NavigationUtils.setStringKeyValue(context, routeString, LAST_ROUTE)
}
}
@@ -62,22 +68,80 @@ open class RouteModel {
return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty()
}
fun stopNavigation() {
fun stopNavigation(context: Context) {
navState = navState.copy(
route = Route.Builder().buildEmpty(),
navigating = false,
arrived = false,
maneuverType = Maneuver.TYPE_UNKNOWN
)
//NavigationUtils.setStringKeyValue(context, "", LAST_ROUTE)
}
fun updateLocation(curLocation: Location, viewModel: ViewModel) {
fun updateLocation(context: Context, curLocation: Location, viewModel: NavigationViewModel) {
navState = navState.copy(currentLocation = curLocation)
routeCalculator.findStep(curLocation)
val repository = getSettingsRepository(context)
val carLocation = runBlocking { repository.carLocationFlow.first() }
if (carLocation) {
routeCalculator.updateSpeedLimit(curLocation, viewModel)
}
navState = navState.copy(lastLocation = navState.currentLocation)
}
fun currentStep(): StepData {
val distanceToNextStep = routeCalculator.leftStepDistance()
// Determine the maneuver type and corresponding icon
val currentStep = navState.route.nextStep(0)
val streetName = if (distanceToNextStep > NEXT_STEP_THRESHOLD) {
currentStep.street
} else {
currentStep.maneuver.street
}
val curManeuverType = if (distanceToNextStep > NEXT_STEP_THRESHOLD) {
Maneuver.TYPE_STRAIGHT
} else {
currentStep.maneuver.type
}
val exitNumber = currentStep.maneuver.exit
val maneuverIcon = navState.iconMapper.maneuverIcon(curManeuverType)
navState = navState.copy(maneuverType = curManeuverType)
// Construct and return the final StepData object
return StepData(
instruction = streetName,
leftStepDistance = distanceToNextStep,
currentManeuverType = navState.maneuverType,
icon = maneuverIcon,
arrivalTime = routeCalculator.arrivalTime(),
leftDistance = routeCalculator.travelLeftDistance(),
lane = currentLanes(),
exitNumber = exitNumber
)
}
fun nextStep(): StepData {
val distanceToNextStep = routeCalculator.leftStepDistance()
val step = navState.route.nextStep(1)
val streetName = if (distanceToNextStep < NEXT_STEP_THRESHOLD) {
step.maneuver.street
} else {
step.street
}
val maneuverType = step.maneuver.type
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
// Construct and return the final StepData object
return StepData(
instruction = streetName,
leftStepDistance = distanceToNextStep,
currentManeuverType = maneuverType,
icon = maneuverIcon,
arrivalTime = routeCalculator.arrivalTime(),
leftDistance = routeCalculator.travelLeftDistance(),
exitNumber = step.maneuver.exit
)
}
private fun currentLanes(): List<Lane> {
var lanes = emptyList<Lane>()
if (navState.route.legs().isNotEmpty()) {
@@ -87,7 +151,7 @@ open class RouteModel {
navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
val sectionBearing =
navState.lastLocation.bearingTo(location(it.location[0], it.location[1]))
if (distance < 500 && (navState.routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue < 10) {
if (distance < NEXT_STEP_THRESHOLD && (navState.routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue < 10) {
lanes = it.lane
}
}
@@ -96,61 +160,6 @@ open class RouteModel {
return lanes
}
fun currentStep(): StepData {
val distanceToNextStep = routeCalculator.leftStepDistance()
// Determine the maneuver type and corresponding icon
val currentStep = navState.route.nextStep(0)
// Safely get the street name from the maneuver
val streetName = currentStep.maneuver.street
val curManeuverType = currentStep.maneuver.type
val exitNumber = currentStep.maneuver.exit
val maneuverIcon = navState.iconMapper.maneuverIcon(curManeuverType)
navState = navState.copy(maneuverType = curManeuverType)
val lanes = currentLanes()
// Construct and return the final StepData object
return StepData(
streetName,
distanceToNextStep,
navState.maneuverType,
maneuverIcon,
routeCalculator.arrivalTime(),
routeCalculator.travelLeftDistance(),
lanes,
exitNumber
)
}
fun nextStep(): StepData {
val step = navState.route.nextStep(1)
val maneuverType = step.maneuver.type
val distanceLeft = routeCalculator.leftStepDistance()
var text = ""
when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
}
else -> {
if (step.street.isNotEmpty()) {
text = step.street
}
}
}
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
// Construct and return the final StepData object
return StepData(
text,
distanceLeft,
maneuverType,
maneuverIcon,
routeCalculator.arrivalTime(),
routeCalculator.travelLeftDistance(),
listOf(Lane(location(0.0, 0.0), valid = false, indications = emptyList())),
step.maneuver.exit
)
}
fun isNavigating(): Boolean {
return navState.navigating
}

View File

@@ -0,0 +1,72 @@
package com.kouros.navigation.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.kouros.navigation.repository.SettingsRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class SettingsViewModel(private val repository: SettingsRepository) : ViewModel() {
val threedBuilding = repository.threedBuildingFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
false
)
val darkMode = repository.darkModeFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
0
)
val avoidMotorway = repository.avoidMotorwayFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
false
)
val avoidTollway = repository.avoidTollwayFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
false
)
val carLocation = repository.carLocationFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
false
)
val routingEngine = repository.routingEngineFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
0
)
fun onThreedBuildingChanged(enabled: Boolean) {
viewModelScope.launch { repository.setThreedBuilding(enabled) }
}
fun onDarkModeChanged(mode: Int) {
viewModelScope.launch { repository.setDarkMode(mode) }
}
fun onAvoidMotorway(enabled: Boolean) {
viewModelScope.launch { repository.setAvoidMotorway(enabled) }
}
fun onAvoidTollway(enabled: Boolean) {
viewModelScope.launch { repository.setAvoidTollway(enabled) }
}
fun onCarLocation(enabled: Boolean) {
viewModelScope.launch { repository.setCarLocation(enabled) }
}
fun onRoutingEngineChanged(mode: Int) {
viewModelScope.launch { repository.setRoutingEngine(mode) }
}
}

View File

@@ -0,0 +1,52 @@
package com.kouros.navigation.repository
import com.kouros.navigation.data.datastore.DataStoreManager
import kotlinx.coroutines.flow.Flow
class SettingsRepository(
private val dataStoreManager: DataStoreManager
) {
val threedBuildingFlow: Flow<Boolean> =
dataStoreManager.threeDBuildingFlow
val darkModeFlow: Flow<Int> =
dataStoreManager.darkModeFlow
val avoidMotorwayFlow: Flow<Boolean> =
dataStoreManager.avoidMotorwayFlow
val avoidTollwayFlow: Flow<Boolean> =
dataStoreManager.avoidTollwayFlow
val carLocationFlow: Flow<Boolean> =
dataStoreManager.useCarLocationFlow
val routingEngineFlow: Flow<Int> =
dataStoreManager.routingEngineFlow
suspend fun setThreedBuilding(enabled: Boolean) {
dataStoreManager.setThreedBuilding(enabled)
}
suspend fun setDarkMode(mode: Int) {
dataStoreManager.setDarkMode(mode)
}
suspend fun setAvoidMotorway(enabled: Boolean) {
dataStoreManager.setAvoidMotorway(enabled)
}
suspend fun setAvoidTollway(enabled: Boolean) {
dataStoreManager.setAvoidTollway(enabled)
}
suspend fun setCarLocation(enabled: Boolean) {
dataStoreManager.setCarLocation(enabled)
}
suspend fun setRoutingEngine(mode: Int) {
dataStoreManager.setRoutingEngine(mode)
}
}

View File

@@ -3,14 +3,20 @@ package com.kouros.navigation.utils
import android.content.Context
import android.location.Location
import android.location.LocationManager
import androidx.car.app.CarContext
import androidx.core.content.edit
import com.kouros.navigation.data.Constants.ROUTING_ENGINE
import com.kouros.navigation.data.Constants.SHARED_PREF_KEY
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.datastore.DataStoreManager
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.ViewModel
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
@@ -26,58 +32,13 @@ import kotlin.time.Duration.Companion.seconds
object NavigationUtils {
fun getViewModel(context: Context): ViewModel {
val routeEngine = getIntKeyValue(context = context, ROUTING_ENGINE)
fun getViewModel(context: Context): NavigationViewModel {
val repository = getSettingsRepository(context)
val routeEngine = runBlocking { repository.routingEngineFlow.first() }
return when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> ViewModel(ValhallaRepository())
RouteEngine.OSRM.ordinal -> ViewModel(OsrmRepository())
else -> ViewModel(TomTomRepository())
}
}
fun getBooleanKeyValue(context: Context, key: String): Boolean {
return context
.getSharedPreferences(
SHARED_PREF_KEY,
Context.MODE_PRIVATE
)
.getBoolean(key, false)
}
fun setBooleanKeyValue(context: Context, `val`: Boolean, key: String) {
context
.getSharedPreferences(
SHARED_PREF_KEY,
Context.MODE_PRIVATE
)
.edit {
putBoolean(
key, `val`
)
apply()
}
}
fun getIntKeyValue(context: Context, key: String, default: Int = 0): Int {
return context
.getSharedPreferences(
SHARED_PREF_KEY,
Context.MODE_PRIVATE
)
.getInt(key, default)
}
fun setIntKeyValue(context: Context, `val`: Int, key: String) {
context
.getSharedPreferences(
SHARED_PREF_KEY,
Context.MODE_PRIVATE
)
.edit {
putInt(
key, `val`
)
apply()
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
else -> NavigationViewModel(TomTomRepository())
}
}
}
@@ -90,8 +51,8 @@ fun calculateZoom(speed: Double?): Double {
val zoom = when (speedKmh) {
in 0..10 -> 18.0
in 11..30 -> 17.5
in 31..50 -> 17.0
in 61..70 -> 16.5
in 31..65 -> 17.0
in 66..70 -> 16.5
else -> 16.0
}
return zoom

View File

@@ -0,0 +1,45 @@
package com.kouros.navigation.utils
import android.content.Context
import androidx.car.app.CarContext
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
@Composable
fun settingsViewModel(context: CarContext, viewModelStoreOwner: ViewModelStoreOwner) : SettingsViewModel {
val dataStoreManager = remember { DataStoreManager(context) }
val repository = remember { SettingsRepository(dataStoreManager) }
val settingsViewModel: SettingsViewModel = viewModel(
viewModelStoreOwner,
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return SettingsViewModel(repository) as T
}
}
)
return settingsViewModel
}
fun getSettingsViewModel(context: Context) : SettingsViewModel {
val dataStoreManager = DataStoreManager(context)
val repository = SettingsRepository(dataStoreManager)
return SettingsViewModel( repository)
}
fun getSettingsRepository(context: Context) : SettingsRepository {
val dataStore = DataStoreManager(context)
return SettingsRepository(dataStore)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -13,33 +13,34 @@ junitVersion = "1.3.0"
espressoCore = "3.7.0"
kotlinxSerializationJson = "1.10.0"
lifecycleRuntimeKtx = "2.10.0"
composeBom = "2026.01.01"
composeBom = "2026.02.00"
appcompat = "1.7.1"
material = "1.13.0"
carApp = "1.7.0"
androidx-car = "1.7.0"
objectboxKotlin = "5.1.0"
objectboxProcessor = "5.0.1"
objectboxKotlin = "5.2.0"
objectboxProcessor = "5.2.0"
ui = "1.10.0"
material3 = "1.4.0"
runtimeLivedata = "1.10.2"
foundation = "1.10.0"
runtimeLivedata = "1.10.3"
foundation = "1.10.3"
maplibre-composeMaterial3 = "0.12.2"
maplibre-compose = "0.12.1"
playServicesLocation = "21.3.0"
runtime = "1.10.2"
runtime = "1.10.3"
accompanist = "0.37.3"
uiVersion = "1.10.2"
uiText = "1.10.2"
uiVersion = "1.10.3"
uiText = "1.10.3"
navigationCompose = "2.9.7"
uiToolingPreview = "1.10.2"
uiTooling = "1.10.2"
uiToolingPreview = "1.10.3"
uiTooling = "1.10.3"
material3WindowSizeClass = "1.4.0"
uiGraphics = "1.10.2"
uiGraphics = "1.10.3"
window = "1.5.1"
foundationLayout = "1.10.2"
foundationLayoutVersion = "1.10.2"
foundationLayout = "1.10.3"
foundationLayoutVersion = "1.10.3"
datastorePreferences = "1.2.0"
datastoreCore = "1.2.0"
[libraries]
android-sdk-turf = { module = "org.maplibre.gl:android-sdk-turf", version.ref = "androidSdkTurf" }
@@ -81,6 +82,8 @@ androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graph
androidx-window = { group = "androidx.window", name = "window", version.ref = "window" }
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
androidx-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayoutVersion" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }