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

View File

@@ -13,8 +13,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 64 versionCode = 66
versionName = "0.2.0.64" versionName = "0.2.0.66"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -1,7 +1,6 @@
package com.kouros.navigation.ui package com.kouros.navigation.ui
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.app.AppOpsManager import android.app.AppOpsManager
import android.location.LocationManager import android.location.LocationManager
import android.os.Bundle import android.os.Bundle
@@ -11,32 +10,22 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -68,6 +57,8 @@ import com.kouros.navigation.model.testSingle
import com.kouros.navigation.ui.app.AppViewModel import com.kouros.navigation.ui.app.AppViewModel
import com.kouros.navigation.ui.app.appViewModel import com.kouros.navigation.ui.app.appViewModel
import com.kouros.navigation.ui.navigation.AppNavGraph import com.kouros.navigation.ui.navigation.AppNavGraph
import com.kouros.navigation.ui.navigation.NavigationSheet
import com.kouros.navigation.ui.search.SearchSheet
import com.kouros.navigation.ui.theme.NavigationTheme import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.bearing
@@ -80,8 +71,10 @@ import kotlinx.coroutines.runBlocking
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.location.DesiredAccuracy import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.Location import org.maplibre.compose.location.Location
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.location.rememberDefaultLocationProvider import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -121,6 +114,7 @@ class MainActivity : ComponentActivity() {
SimulationType.GPX -> gpx( SimulationType.GPX -> gpx(
context = applicationContext, mock context = applicationContext, mock
) )
SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock) SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock)
} }
} }
@@ -178,30 +172,19 @@ class MainActivity : ComponentActivity() {
} }
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
CheckPermissionScreen() CheckPermissionScreen(app = {
} AppNavGraph(
} mainActivity = this
@SuppressLint("MissingPermission")
@Composable
fun CheckPermissionScreen() {
val permissions = listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
)
PermissionScreen(
permissions = permissions,
requiredPermissions = listOf(permissions.first()),
onGranted = {
Application()
},
) )
})
}
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun StartScreen( fun StartScreen(
navController: NavHostController) { navController: NavHostController
) {
val appViewModel: AppViewModel = appViewModel() val appViewModel: AppViewModel = appViewModel()
val darkMode by appViewModel.darkMode.collectAsState() val darkMode by appViewModel.darkMode.collectAsState()
@@ -209,6 +192,10 @@ class MainActivity : ComponentActivity() {
val locationProvider = rememberDefaultLocationProvider( val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest
) )
val lastRoute by appViewModel.lastRoute.collectAsState()
if (lastRoute.isNotEmpty()) {
navigationViewModel.route.value = lastRoute
}
val userLocationState = rememberUserLocationState(locationProvider) val userLocationState = rememberUserLocationState(locationProvider)
if (!useMock) { if (!useMock) {
val locationState = locationProvider.location.collectAsState() val locationState = locationProvider.location.collectAsState()
@@ -217,86 +204,27 @@ class MainActivity : ComponentActivity() {
val step: StepData? by stepData.observeAsState() val step: StepData? by stepData.observeAsState()
val nextStep: StepData? by nextStepData.observeAsState() val nextStep: StepData? by nextStepData.observeAsState()
var collapsedHeight by remember {
mutableIntStateOf(300)
}
val scope = rememberCoroutineScope()
fun closeSheet() {
scope.launch {
collapsedHeight = if (routeModel.isNavigating()) {
150
} else {
300
}
}
}
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp
var expandedType by remember {
mutableStateOf(ExpandedType.COLLAPSED)
}
val height by animateIntAsState(
when (expandedType) {
ExpandedType.HALF -> screenHeight / 2
ExpandedType.FULL -> screenHeight
ExpandedType.COLLAPSED -> collapsedHeight
}
)
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState()
NavigationTheme(useDarkTheme = darkMode == 1) { NavigationTheme(useDarkTheme = darkMode == 1) {
BottomSheetScaffold( CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
scaffoldState = bottomSheetScaffoldState, SheetLayout(
sheetShape = RoundedCornerShape( map = { _ ->
bottomStart = 0.dp, Map(
bottomEnd = 0.dp, userLocationState, step, nextStep, baseStyle, navController
topStart = 12.dp, )
topEnd = 12.dp
),
sheetContent = {
var isUpdated = false
Box(
Modifier
.fillMaxWidth()
.height(height.dp)
.pointerInput(Unit) {
detectVerticalDragGestures(
onVerticalDrag = { change, dragAmount ->
change.consume()
if (!isUpdated) {
expandedType = when {
dragAmount < 0 && expandedType == ExpandedType.COLLAPSED -> {
ExpandedType.HALF
}
dragAmount < 0 && expandedType == ExpandedType.HALF -> {
ExpandedType.FULL
}
dragAmount > 0 && expandedType == ExpandedType.FULL -> {
ExpandedType.HALF
}
dragAmount > 0 && expandedType == ExpandedType.HALF -> {
ExpandedType.COLLAPSED
}
else -> {
ExpandedType.FULL
}
}
isUpdated = true
}
}, },
onDragEnd = { menu = { SheetContent(navController, step, nextStep) },
isUpdated = false
}
) )
} }
) {
SheetContent(step, nextStep) { closeSheet() }
} }
}, }
sheetPeekHeight = height.dp
@Composable
private fun Map(
userLocationState: UserLocationState,
step: StepData?,
nextStep: StepData?,
baseStyle: BaseStyle.Json,
navController: NavHostController
) { ) {
MapView( MapView(
applicationContext, applicationContext,
@@ -312,18 +240,6 @@ class MainActivity : ComponentActivity() {
Settings(navController, modifier = Modifier.fillMaxWidth()) Settings(navController, modifier = Modifier.fillMaxWidth())
} }
} }
}
}
@Composable
fun Application() {
val appViewModel: AppViewModel = appViewModel()
val lastRoute by appViewModel.lastRoute.collectAsState()
if (lastRoute.isNotEmpty()) {
navigationViewModel.route.value = lastRoute
}
AppNavGraph(this)
}
@Composable @Composable
fun Settings(navController: NavController, modifier: Modifier = Modifier) { fun Settings(navController: NavController, modifier: Modifier = Modifier) {
@@ -347,10 +263,10 @@ class MainActivity : ComponentActivity() {
@Composable @Composable
fun SheetContent( fun SheetContent(
step: StepData?, nextStep: StepData?, closeSheet: () -> Unit navController: NavHostController, step: StepData?, nextStep: StepData?
) { ) {
if (!routeModel.isNavigating()) { if (!routeModel.isNavigating()) {
SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() } SearchSheet(applicationContext, navController, navigationViewModel, lastLocation) { }
} else { } else {
if (step != null) { if (step != null) {
NavigationSheet( NavigationSheet(
@@ -358,7 +274,7 @@ class MainActivity : ComponentActivity() {
routeModel, routeModel,
step, step,
nextStep, nextStep,
{ stopNavigation { closeSheet() } }, { stopNavigation {} },
{ simulateNavigation() }) { simulateNavigation() })
} }
} }
@@ -446,8 +362,7 @@ class MainActivity : ComponentActivity() {
fun simulateNavigation() { fun simulateNavigation() {
simulate( simulate(
routeModel = routeModel, routeModel = routeModel, mock = mock
mock = mock
) )
} }

View File

@@ -1,6 +1,7 @@
package com.kouros.navigation.ui package com.kouros.navigation.ui
import android.content.Context import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -18,6 +19,7 @@ import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.ui.app.AppViewModel import com.kouros.navigation.ui.app.AppViewModel
import com.kouros.navigation.ui.app.appViewModel import com.kouros.navigation.ui.app.appViewModel
import com.kouros.navigation.ui.navigation.NavigationInfo
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.LocationTrackingEffect import org.maplibre.compose.location.LocationTrackingEffect
@@ -62,6 +64,8 @@ fun MapView(
val showBuildings by appViewModel.show3D.collectAsState() val showBuildings by appViewModel.show3D.collectAsState()
val darkMode by appViewModel.darkMode.collectAsState() val darkMode by appViewModel.darkMode.collectAsState()
val dark = darkMode == 1 || darkMode == 2 && isSystemInDarkTheme()
Column { Column {
NavigationInfo(step, nextStep) NavigationInfo(step, nextStep)
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
@@ -88,7 +92,7 @@ fun MapView(
duration = 1.seconds duration = 1.seconds
) )
} }
NavigationImage(paddingValues, width, height / 6, "", darkMode) NavigationImage(paddingValues, width, height / 6, "", dark)
} }
} }
} }

View File

@@ -1,5 +1,7 @@
package com.kouros.navigation.ui package com.kouros.navigation.ui
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.provider.Settings import android.provider.Settings
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
@@ -37,6 +39,25 @@ import androidx.core.net.toUri
* *
* By default it assumes that all [permissions] are required. * By default it assumes that all [permissions] are required.
*/ */
@SuppressLint("MissingPermission")
@Composable
fun CheckPermissionScreen(
app: @Composable () -> Unit,
) {
val permissions = listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
)
PermissionScreen(
permissions = permissions,
requiredPermissions = listOf(permissions.first()),
onGranted = {
app()
},
)
}
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun PermissionScreen( fun PermissionScreen(

View File

@@ -0,0 +1,81 @@
package com.kouros.navigation.ui
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SheetLayout(
map: @Composable (PaddingValues) -> Unit,
menu: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
val sheetState = rememberBottomSheetScaffoldState()
BottomSheetScaffold(
sheetPeekHeight = 180.dp,
scaffoldState = sheetState,
sheetSwipeEnabled = true,
sheetDragHandle = {
ExpandCollapseButton(
sheetState.bottomSheetState.targetValue == SheetValue.Expanded,
onExpand = { sheetState.bottomSheetState.expand() },
onCollapse = { sheetState.bottomSheetState.partialExpand() },
modifier = Modifier.fillMaxWidth(),
)
},
sheetContent = {
Box(
modifier =
Modifier.background(BottomSheetDefaults.ContainerColor)
.consumeWindowInsets(PaddingValues(top = 56.dp))
.requiredHeight(500.dp)
) {
menu()
}
},
modifier = modifier,
) { padding ->
map(padding)
}
}
@Composable
private fun ExpandCollapseButton(
expanded: Boolean,
onExpand: suspend () -> Unit,
onCollapse: suspend () -> Unit,
modifier: Modifier = Modifier,
) {
val degrees by animateFloatAsState(targetValue = if (expanded) 180f else 0f)
val coroutineScope = rememberCoroutineScope()
IconButton(
modifier = modifier,
onClick = { coroutineScope.launch { if (expanded) onCollapse() else onExpand() } },
) {
Icon(
painter = painterResource(com.kouros.android.cars.carappservice.R.drawable.keyboard_arrow_up_24px),
contentDescription = if (expanded) "Collapse" else "Expand",
modifier = Modifier.rotate(degrees),
)
}
}

View File

@@ -4,7 +4,9 @@ import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.ui.MainActivity import com.kouros.navigation.ui.MainActivity
import com.kouros.navigation.ui.search.SearchScreen
import com.kouros.navigation.ui.settings.SettingsRoute import com.kouros.navigation.ui.settings.SettingsRoute
@@ -17,5 +19,7 @@ fun AppNavGraph(mainActivity: MainActivity) {
composable("display_settings") { SettingsRoute("display_settings", navController) { navController.popBackStack() } } composable("display_settings") { SettingsRoute("display_settings", navController) { navController.popBackStack() } }
composable("nav_settings") { SettingsRoute("nav_settings", navController) { navController.popBackStack() } } composable("nav_settings") { SettingsRoute("nav_settings", navController) { navController.popBackStack() } }
composable("settings") { SettingsRoute("settings", navController) { navController.popBackStack() } } composable("settings") { SettingsRoute("settings", navController) { navController.popBackStack() } }
composable("search") { SearchScreen(navController, navController.context, navigationViewModel, mainActivity.lastLocation) { navController.popBackStack() }
}
} }
} }

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation.ui package com.kouros.navigation.ui.navigation
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation.ui package com.kouros.navigation.ui.navigation
import android.content.Context import android.content.Context
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement

View File

@@ -1,33 +1,35 @@
package com.kouros.navigation.ui package com.kouros.navigation.ui.search
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.captionBar
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -35,119 +37,93 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.PlaceColor import com.kouros.navigation.data.PlaceColor
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SearchSheet( fun SearchScreen(
applicationContext: Context, navController: NavHostController,
viewModel: NavigationViewModel, context: Context,
navigationViewModel: NavigationViewModel,
location: Location, location: Location,
closeSheet: () -> Unit function: () -> Unit
) { ) {
val searchResults = mutableListOf<SearchResult>()
val recentPlaces = viewModel.places.observeAsState()
val search = viewModel.searchPlaces.observeAsState()
if (search.value != null) {
searchResults.addAll(search.value!!)
}
val textFieldState = rememberTextFieldState()
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
SearchBar(
textFieldState = textFieldState,
searchPlaces = emptyList(),
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location,
closeSheet = { closeSheet() }
NavigationTheme(true) {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
stringResource(id = R.string.search_action_title),
) )
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() }) },
if (recentPlaces.value != null) { navigationIcon = {
val items = listOf(recentPlaces) IconButton(onClick = function) {
if (items.isNotEmpty()) {
RecentPlaces(
recentPlaces.value!!,
viewModel,
applicationContext,
location,
closeSheet
)
}
}
}
}
@Composable
fun Home(
applicationContext: Context,
viewModel: NavigationViewModel,
location: Location,
closeSheet: () -> Unit
) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Button(onClick = {
val places = viewModel.loadRecentPlace(applicationContext)
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
viewModel.loadRoute(applicationContext, location, toLocation, 0F)
closeSheet()
}) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_place_white_24dp), painter = painterResource(R.drawable.arrow_back_24px),
"Home", contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(24.dp, 24.dp), modifier = Modifier.size(48.dp, 48.dp),
) )
Text("Home")
} }
Button(onClick = { },
}) {
Icon(
painter = painterResource(id = R.drawable.ic_favorite_white_24dp),
"Work",
modifier = Modifier.size(24.dp, 24.dp),
) )
Text("Arbeit") },
)
{ padding ->
val scrollState = rememberScrollState()
Column(Modifier.padding(top = 50.dp)) {
SearchBar(context, navigationViewModel, location)
Categories(context, navigationViewModel, location, closeSheet = { })
} }
} }
} }
private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) {
viewModel.searchPlaces(it, location)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SearchBar( fun SearchBar(
textFieldState: TextFieldState,
searchPlaces: List<Place>,
searchResults: List<SearchResult>,
viewModel: NavigationViewModel,
context: Context, context: Context,
navigationViewModel: NavigationViewModel,
location: Location, location: Location,
closeSheet: () -> Unit
) { ) {
val searchResults = mutableListOf<SearchResult>()
val search = navigationViewModel.searchPlaces.observeAsState()
if (search.value != null) {
searchResults.addAll(search.value!!)
}
val textFieldState = rememberTextFieldState()
var expanded by rememberSaveable { mutableStateOf(false) } var expanded by rememberSaveable { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
SearchBar( SearchBar(
windowInsets = WindowInsets.captionBar,
colors = SearchBarDefaults.colors( colors = SearchBarDefaults.colors(
containerColor = MaterialTheme.colorScheme.secondaryContainer containerColor = MaterialTheme.colorScheme.secondaryContainer
), ),
inputField = { inputField = {
SearchBarDefaults.InputField( SearchBarDefaults.InputField(
modifier = Modifier.focusRequester(focusRequester),
leadingIcon = { leadingIcon = {
Icon( Icon(
painter = painterResource(id = R.drawable.search_48px), painter = painterResource(id = R.drawable.search_48px),
@@ -158,24 +134,64 @@ fun SearchBar(
query = textFieldState.text.toString(), query = textFieldState.text.toString(),
onQueryChange = { textFieldState.edit { replace(0, length, it) } }, onQueryChange = { textFieldState.edit { replace(0, length, it) } },
onSearch = { onSearch = {
searchPlaces(viewModel, location, it) navigationViewModel.searchPlaces(it, location)
expanded = false expanded = true
}, },
expanded = expanded, expanded = expanded,
onExpandedChange = { expanded = it }, onExpandedChange = {
//expanded = it
},
placeholder = { Text(context.getString(R.string.search_action_title)) } placeholder = { Text(context.getString(R.string.search_action_title)) }
) )
}, },
expanded = expanded, expanded = expanded,
onExpandedChange = { }, onExpandedChange = { },
) { ) {
if (searchPlaces.isNotEmpty()) {
Text(context.getString(R.string.recent_destinations))
RecentPlaces(searchPlaces, viewModel, context, location, closeSheet)
}
if (searchResults.isNotEmpty()) { if (searchResults.isNotEmpty()) {
Text("Search places") SearchPlaces(searchResults, navigationViewModel, context, location, { })
SearchPlaces( searchResults, viewModel, context, location, closeSheet) }
}
}
@Composable
fun Categories(
applicationContext: Context,
viewModel: NavigationViewModel,
location: Location,
closeSheet: () -> Unit
) {
val scrollState = rememberScrollState()
Row(
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier.horizontalScroll(scrollState)
) {
Button(onClick = {
val places = viewModel.loadRecentPlace(applicationContext)
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
viewModel.loadRoute(applicationContext, location, toLocation, 0F)
closeSheet()
}) {
Icon(
painter = painterResource(id = R.drawable.local_gas_station_24),
applicationContext.getString(R.string.fuel_station),
modifier = Modifier.size(24.dp, 24.dp),
)
}
Button(onClick = {
}) {
Icon(
painter = painterResource(id = R.drawable.ev_station_24px),
"Work",
modifier = Modifier.size(24.dp, 24.dp),
)
}
Button(onClick = {
}) {
Icon(
painter = painterResource(id = R.drawable.local_pharmacy_24px),
"Work",
modifier = Modifier.size(24.dp, 24.dp),
)
} }
} }
} }
@@ -193,7 +209,6 @@ private fun SearchPlaces(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
if (searchResults.isNotEmpty()) {
items(searchResults, key = { it.placeId }) { place -> items(searchResults, key = { it.placeId }) { place ->
Row { Row {
Icon( Icon(
@@ -227,41 +242,5 @@ private fun SearchPlaces(
} }
} }
} }
}
@Composable
private fun RecentPlaces(
recentPlaces: List<Place>,
viewModel: NavigationViewModel,
context: Context,
location: Location,
closeSheet: () -> Unit
) {
val color = remember { PlaceColor }
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(recentPlaces, key = { it.id }) { place ->
Row {
Icon(
painter = painterResource(id = R.drawable.ic_place_white_24dp),
"Navigation",
tint = color.copy(alpha = 1f),
modifier = Modifier.size(24.dp, 24.dp),
)
ListItem(
headlineContent = { Text("${place.street} ${place.postalCode} ${place.city}") },
modifier = Modifier
.clickable {
val toLocation = location(place.longitude, place.latitude)
viewModel.loadRoute(context, location, toLocation, 0F)
closeSheet()
}
.fillMaxWidth()
)
HorizontalDivider(color = Color.Gray)
}
}
}
}

View File

@@ -0,0 +1,180 @@
package com.kouros.navigation.ui.search
import android.content.Context
import android.location.Location
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.kouros.data.R
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.PlaceColor
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.location
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchSheet(
applicationContext: Context,
navController: NavHostController,
viewModel: NavigationViewModel,
location: Location,
closeSheet: () -> Unit
) {
val recentPlaces = viewModel.recentPlaces.observeAsState()
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
SearchBar(
modifier = Modifier.clickable {
navController.navigate("search")
},
colors = SearchBarDefaults.colors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
),
expanded = false,
onExpandedChange = { navController.navigate("search") },
inputField = {
SearchBarDefaults.InputField(
enabled = false,
modifier = Modifier.clickable {
navController.navigate("search")
},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.search_48px),
applicationContext.getString(R.string.search_action_title),
modifier = Modifier.size(24.dp, 24.dp),
)
},
query = applicationContext.getString(R.string.search_action_title),
onQueryChange = { },
onSearch = {
},
expanded = false,
onExpandedChange = { },
placeholder = { applicationContext.getString(R.string.search_action_title) }
)
},
) {
}
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
if (recentPlaces.value != null) {
val items = listOf(recentPlaces)
if (items.isNotEmpty()) {
Column(Modifier.padding(all = 10.dp)) {
Text(applicationContext.getString(R.string.recent_destinations))
RecentPlaces(
recentPlaces.value!!,
viewModel,
applicationContext,
location,
closeSheet
)
}
}
}
}
}
@Composable
fun Home(
applicationContext: Context,
viewModel: NavigationViewModel,
location: Location,
closeSheet: () -> Unit
) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Button(onClick = {
val places = viewModel.loadRecentPlace(applicationContext)
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
viewModel.loadRoute(applicationContext, location, toLocation, 0F)
closeSheet()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_place_white_24dp),
"Home",
modifier = Modifier.size(24.dp, 24.dp),
)
Text("Home")
}
Button(onClick = {
}) {
Icon(
painter = painterResource(id = R.drawable.ic_favorite_white_24dp),
"Work",
modifier = Modifier.size(24.dp, 24.dp),
)
Text("Arbeit")
}
}
}
@Composable
private fun RecentPlaces(
recentPlaces: List<Place>,
viewModel: NavigationViewModel,
context: Context,
location: Location,
closeSheet: () -> Unit
) {
val color = remember { PlaceColor }
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(recentPlaces, key = { it.id }) { place ->
Row {
Icon(
painter = painterResource(id = R.drawable.ic_place_white_24dp),
"Navigation",
tint = color.copy(alpha = 1f),
modifier = Modifier.size(24.dp, 24.dp),
)
ListItem(
headlineContent = { Text("${place.street} ${place.postalCode} ${place.city}") },
modifier = Modifier
.clickable {
val toLocation = location(place.longitude, place.latitude)
viewModel.loadRoute(context, location, toLocation, 0F)
closeSheet()
}
.fillMaxWidth()
)
HorizontalDivider(color = Color.Gray)
}
}
}
}

View File

@@ -2,7 +2,7 @@ package com.kouros.navigation.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFF825500) val md_theme_light_primary = Color(0xFF6FE5E1)
val md_theme_light_onPrimary = Color(0xFFFFFFFF) val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFDDB3) val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
val md_theme_light_onPrimaryContainer = Color(0xFF291800) val md_theme_light_onPrimaryContainer = Color(0xFF291800)
@@ -33,7 +33,7 @@ val md_theme_light_surfaceTint = Color(0xFF825500)
val md_theme_light_outlineVariant = Color(0xFFD3C4B4) val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
val md_theme_light_scrim = Color(0xFF000000) val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFFFFB951) val md_theme_dark_primary = Color(0xFF8CD6EF)
val md_theme_dark_onPrimary = Color(0xFF452B00) val md_theme_dark_onPrimary = Color(0xFF452B00)
val md_theme_dark_primaryContainer = Color(0xFF633F00) val md_theme_dark_primaryContainer = Color(0xFF633F00)
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3) val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)

View File

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

View File

@@ -33,7 +33,6 @@ import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.homeVogelhart import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.model.BaseStyleModel import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt import com.kouros.navigation.utils.calculateTilt
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
@@ -46,6 +45,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
@@ -261,6 +261,7 @@ class SurfaceRenderer(
val paddingValues = getPaddingValues(height, viewStyle) val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt) val cameraState = cameraState(paddingValues, position, tilt)
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode) val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
val dark = darkMode == 1 || darkMode == 2 && carContext.isDarkMode
MapLibre( MapLibre(
cameraState, cameraState,
@@ -271,7 +272,7 @@ class SurfaceRenderer(
speedCameras, speedCameras,
showBuildings showBuildings
) )
ShowPosition(cameraState, position, paddingValues, darkMode) ShowPosition(cameraState, position, paddingValues, dark)
} }
/** /**
@@ -283,18 +284,18 @@ class SurfaceRenderer(
cameraState: CameraState, cameraState: CameraState,
position: CameraPosition?, position: CameraPosition?,
paddingValues: PaddingValues, paddingValues: PaddingValues,
darkMode: Int darkMode: Boolean
) { ) {
val cameraDuration = val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing) duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
val currentSpeed: Float? by speed.observeAsState() val currentSpeed: Float? by speed.observeAsState()
val speed: Int? by maxSpeed.observeAsState() val maximumSpeed: Int? by maxSpeed.observeAsState()
val streetName: String? by street.observeAsState() val streetName: String? by street.observeAsState()
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) { if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
DrawNavigationImages( DrawNavigationImages(
paddingValues, paddingValues,
currentSpeed, currentSpeed,
speed!!, maximumSpeed!!,
width, width,
height, height,
streetName, streetName,
@@ -340,7 +341,7 @@ class SurfaceRenderer(
updateCameraPosition( updateCameraPosition(
cameraPosition.value!!.bearing, cameraPosition.value!!.bearing,
newZoom, newZoom,
cameraPosition.value!!.target, cameraPosition.value!!.target, tilt
) )
} }
} }
@@ -379,7 +380,7 @@ class SurfaceRenderer(
updateCameraPosition( updateCameraPosition(
bearing, bearing,
zoom, zoom,
Position(location.longitude, location.latitude) Position(location.longitude, location.latitude), tilt
) )
lastBearing = cameraPosition.value!!.bearing lastBearing = cameraPosition.value!!.bearing
lastLocation = location lastLocation = location
@@ -387,11 +388,19 @@ class SurfaceRenderer(
} }
} }
/**
* Sets route data for active navigation and switches to VIEW mode.
*/
fun setRouteData() {
routeData.value = routeModel.curRoute.routeGeoJson
viewStyle = ViewStyle.VIEW
}
/** /**
* Updates camera position with new bearing, zoom, and target. * Updates camera position with new bearing, zoom, and target.
* Posts update to LiveData for UI observation. * Posts update to LiveData for UI observation.
*/ */
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) { private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position, tilt: Double) {
synchronized(this) { synchronized(this) {
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
@@ -408,9 +417,15 @@ class SurfaceRenderer(
/** /**
* Sets route data for active navigation and switches to VIEW mode. * Sets route data for active navigation and switches to VIEW mode.
*/ */
fun setRouteData() { fun clearRouteData() {
routeData.value = routeModel.curRoute.routeGeoJson routeData.value = ""
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = 16.0
)
)
tilt = TILT
} }
/** /**
@@ -424,18 +439,21 @@ class SurfaceRenderer(
* Sets up route preview mode with overview camera position. * Sets up route preview mode with overview camera position.
* Calculates appropriate zoom based on route distance. * Calculates appropriate zoom based on route distance.
*/ */
fun setPreviewRouteData(routeModel: RouteModel) { fun setPreviewRouteData(routeModel: RouteCarModel) {
viewStyle = ViewStyle.PREVIEW viewStyle = ViewStyle.PREVIEW
with(routeModel) { with(routeModel) {
routeData.value = curRoute.routeGeoJson routeData.value = curRoute.routeGeoJson
centerLocation = curRoute.centerLocation centerLocation = curRoute.centerLocation
previewDistance = curRoute.summary.distance previewDistance = curRoute.summary.distance
} }
tilt = 0.0
updateCameraPosition( updateCameraPosition(
0.0, 0.0,
previewZoom(previewDistance), previewZoom(centerLocation, previewDistance),
Position(centerLocation.longitude, centerLocation.latitude) Position(centerLocation.longitude, centerLocation.latitude), 0.0
) )
} }
/** /**
@@ -448,7 +466,7 @@ class SurfaceRenderer(
updateCameraPosition( updateCameraPosition(
0.0, 0.0,
14.0, 14.0,
target = Position(location.longitude, location.latitude) target = Position(location.longitude, location.latitude), tilt
) )
} }
} }

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -19,6 +20,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
@@ -33,6 +35,8 @@ import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationColor import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.utils.isMetricSystem
import com.kouros.navigation.utils.location
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
@@ -59,6 +63,7 @@ import org.maplibre.compose.sources.Source
import org.maplibre.compose.sources.getBaseSource import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.sources.rememberGeoJsonSource import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.BoundingBox
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
@@ -296,7 +301,7 @@ fun DrawNavigationImages(
width: Int, width: Int,
height: Int, height: Int,
streetName: String?, streetName: String?,
darkMode: Int, darkMode: Boolean,
) { ) {
NavigationImage(padding, width, height, streetName, darkMode) NavigationImage(padding, width, height, streetName, darkMode)
if (speed != null) { if (speed != null) {
@@ -314,7 +319,7 @@ fun NavigationImage(
width: Int, width: Int,
height: Int, height: Int,
streetName: String?, streetName: String?,
darkMode: Int darkMode: Boolean
) { ) {
val imageSize = (height / 8) val imageSize = (height / 8)
@@ -323,9 +328,9 @@ fun NavigationImage(
val textMeasurerStreet = rememberTextMeasurer() val textMeasurerStreet = rememberTextMeasurer()
val street = streetName.toString() val street = streetName.toString()
val styleStreet = TextStyle( val styleStreet = TextStyle(
fontSize = 14.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = if (darkMode == 1) Color.White else navigationColor, color = if (darkMode) Color.White else navigationColor,
) )
val textLayoutStreet = remember(street) { val textLayoutStreet = remember(street) {
textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis) textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis)
@@ -348,28 +353,31 @@ fun NavigationImage(
.size(imageSize.dp, imageSize.dp) .size(imageSize.dp, imageSize.dp)
.scale(scaleX = 1f, scaleY = 0.7f), .scale(scaleX = 1f, scaleY = 0.7f),
) )
Canvas( Canvas(
modifier = Modifier modifier = Modifier
.size((width / 5).dp, (height/4).dp) .size(textLayoutStreet.size.width.dp, textLayoutStreet.size.height.dp * 6 )
) { ) {
if (!streetName.isNullOrEmpty()) { if (street.isNotEmpty()) {
val topLeftX = center.x - textLayoutStreet.size.width / 2
val topLeftY = center.y + textLayoutStreet.size.height
drawRoundRect( drawRoundRect(
topLeft = Offset( topLeft = Offset(
x = center.x - textLayoutStreet.size.width / 2 , x = topLeftX ,
y = center.y + textLayoutStreet.size.height, y = topLeftY,
), ),
color = if (darkMode == 1) navigationColor else Color.White, color = if (darkMode) navigationColor else Color.White,
cornerRadius = CornerRadius(x = 10f, y = 10f), cornerRadius = CornerRadius(x = 10f, y = 10f),
) )
drawText( drawText(
textMeasurer = textMeasurerStreet, textMeasurer = textMeasurerStreet,
text = streetName, text = street,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
style = styleStreet, style = styleStreet,
topLeft = Offset( topLeft = Offset(
x = center.x - textLayoutStreet.size.width / 2, x = topLeftX,
y = center.y + textLayoutStreet.size.height + 10, y = topLeftY + 10,
) )
) )
} }
@@ -384,6 +392,7 @@ private fun CurrentSpeed(
curSpeed: Float, curSpeed: Float,
maxSpeed: Int maxSpeed: Int
) { ) {
val radius = 34 val radius = 34
Box( Box(
modifier = Modifier modifier = Modifier
@@ -395,9 +404,11 @@ private fun CurrentSpeed(
) { ) {
val textMeasurerSpeed = rememberTextMeasurer() val textMeasurerSpeed = rememberTextMeasurer()
val textMeasurerKm = rememberTextMeasurer() val textMeasurerKm = rememberTextMeasurer()
val speed = (curSpeed * 3.6).toInt().toString()
val kmh = "km/h" val speed = if (isMetricSystem()) (curSpeed * 3.6).toInt().toString() else (curSpeed * 3.6 * 0.6214).toInt().toString()
val kmh = if (isMetricSystem()) "km/h" else "mph"
val styleSpeed = TextStyle( val styleSpeed = TextStyle(
fontSize = 22.sp, fontSize = 22.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -433,7 +444,7 @@ private fun CurrentSpeed(
) )
drawText( drawText(
textMeasurer = textMeasurerKm, textMeasurer = textMeasurerKm,
text = "km/h", text = kmh,
style = styleKm, style = styleKm,
topLeft = Offset( topLeft = Offset(
x = center.x - textLayoutKm.size.width / 2, x = center.x - textLayoutKm.size.width / 2,

View File

@@ -17,6 +17,7 @@ class Simulation {
lifecycleScope: LifecycleCoroutineScope, lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit updateLocation: (Location) -> Unit
) { ) {
if (routeModel.navState.route.isRouteValid()) {
val points = routeModel.curRoute.waypoints val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return if (points.isEmpty()) return
simulationJob?.cancel() simulationJob?.cancel()
@@ -36,13 +37,14 @@ class Simulation {
curBearing = lastLocation.bearingTo(fakeLocation) curBearing = lastLocation.bearingTo(fakeLocation)
// Update your app's state as if a real GPS update occurred // Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation) updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 2 seconds) // Wait before moving to the next point (e.g., every 1 second)
delay(1000) delay(1000)
lastLocation = fakeLocation lastLocation = fakeLocation
} }
routeModel.stopNavigation() routeModel.stopNavigation()
} }
} }
}
fun stopSimulation() { fun stopSimulation() {
simulationJob?.cancel() simulationJob?.cancel()

View File

@@ -116,7 +116,8 @@ class CategoryScreen(
} else { } else {
row.addText("${(it.distance / 1000).round(1)} km") row.addText("${(it.distance / 1000).round(1)} km")
} }
if (category == Constants.CHARGING_STATION) { if (category == CHARGING_STATION) {
if (it.tags.socketType2 != null)
row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}") row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
} else { } else {
row.addText(carText("${it.tags.openingHours}")) row.addText(carText("${it.tags.openingHours}"))
@@ -134,7 +135,7 @@ class CategoryScreen(
setResult( setResult(
Place( Place(
name = name, name = name,
category = Constants.CHARGING_STATION, category = CHARGING_STATION,
latitude = it.lat, latitude = it.lat,
longitude = it.lon longitude = it.lon
) )

View File

@@ -377,7 +377,6 @@ class NavigationScreen(
*/ */
private fun stopAction(): Action { private fun stopAction(): Action {
return Action.Builder() return Action.Builder()
.setTitle(carContext.getString(R.string.stop_action_title))
.setIcon( .setIcon(
CarIcon.Builder( CarIcon.Builder(
IconCompat.createWithResource( IconCompat.createWithResource(
@@ -531,6 +530,8 @@ class NavigationScreen(
* Pushes the search screen and handles the search result. * Pushes the search screen and handles the search result.
*/ */
private fun startSearchScreen() { private fun startSearchScreen() {
navigationViewModel.recentPlaces.value = emptyList()
navigationViewModel.previewRoute.value = ""
screenManager screenManager
.pushForResult( .pushForResult(
SearchScreen( SearchScreen(
@@ -558,16 +559,22 @@ class NavigationScreen(
* Loads a route to the specified place and sets it as the destination. * Loads a route to the specified place and sets it as the destination.
*/ */
fun navigateToPlace(place: Place) { fun navigateToPlace(place: Place) {
val preview = navigationViewModel.previewRoute.value
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
val location = location(place.longitude, place.latitude) val location = location(place.longitude, place.latitude)
navigationViewModel.saveRecent(carContext, place) navigationViewModel.saveRecent(carContext, place)
currentNavigationLocation = location currentNavigationLocation = location
if (preview.isNullOrEmpty()) {
navigationViewModel.loadRoute( navigationViewModel.loadRoute(
carContext, carContext,
surfaceRenderer.lastLocation, surfaceRenderer.lastLocation,
location, location,
surfaceRenderer.carOrientation surfaceRenderer.carOrientation
) )
} else {
routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
navigationViewModel.route.value = preview
}
routeModel.navState = routeModel.navState.copy(destination = place) routeModel.navState = routeModel.navState.copy(destination = place)
invalidate() invalidate()
} }

View File

@@ -17,7 +17,6 @@ import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
@@ -27,58 +26,52 @@ import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
class PlaceListScreen( class PlaceListScreen(
private val carContext: CarContext, private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer, private val surfaceRenderer: SurfaceRenderer,
private val category: String, private val category: String,
private val navigationViewModel: NavigationViewModel private val navigationViewModel: NavigationViewModel,
private val places: List<Place>
) : Screen(carContext) { ) : Screen(carContext) {
var places = listOf<Place>()
val observer = Observer<List<Place>> { newPlaces -> val routeModel = RouteCarModel()
places = newPlaces
invalidate() var place = Place()
val previewObserver = Observer<String> { route ->
if (route.isNotEmpty()) {
val repository = getSettingsRepository(carContext)
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel)
screenManager
.pushForResult(
RoutePreviewScreen(
carContext,
surfaceRenderer,
place,
navigationViewModel,
routeModel = routeModel
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
} }
val observerAddress = Observer<List<Place>> { newContacts ->
places = newContacts
invalidate()
} }
init { init {
if (category == RECENT) {
navigationViewModel.places.observe(this, observer)
}
if (category == CONTACTS) {
navigationViewModel.contactAddress.observe(this, observerAddress)
}
if (category == FAVORITES) {
navigationViewModel.favorites.observe(this, observer)
}
loadPlaces() loadPlaces()
} navigationViewModel.previewRoute.observe(this, previewObserver)
fun loadPlaces() {
if (category == RECENT) {
navigationViewModel.loadRecentPlaces(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
}
if (category == CONTACTS) {
navigationViewModel.loadContacts(carContext)
}
if (category == FAVORITES) {
navigationViewModel.loadFavorites(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
}
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -94,7 +87,7 @@ class PlaceListScreen(
// .setImage(contactIcon(it.avatar, it.category)) // .setImage(contactIcon(it.avatar, it.category))
.setTitle("$street ${it.city}") .setTitle("$street ${it.city}")
.setOnClickListener { .setOnClickListener {
val place = Place( place = Place(
0, 0,
it.name, it.name,
it.category, it.category,
@@ -105,20 +98,13 @@ class PlaceListScreen(
it.street, it.street,
// avatar = null // avatar = null
) )
screenManager val location = location(place.longitude, place.latitude)
.pushForResult( navigationViewModel.loadPreviewRoute(
RoutePreviewScreen(
carContext, carContext,
surfaceRenderer, surfaceRenderer.lastLocation,
place, location,
navigationViewModel surfaceRenderer.carOrientation
) )
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
} }
if (category != CONTACTS) { if (category != CONTACTS) {
row.addText(SpannableString(" ").apply { row.addText(SpannableString(" ").apply {
@@ -184,4 +170,10 @@ class PlaceListScreen(
} }
return CarIcon.Builder(IconCompat.createWithContentUri(avatar)).build() return CarIcon.Builder(IconCompat.createWithContentUri(avatar)).build()
} }
fun loadPlaces() {
if (category == CONTACTS) {
navigationViewModel.loadContacts(carContext)
}
}
} }

View File

@@ -2,10 +2,12 @@ package com.kouros.navigation.car.screen
import android.os.CountDownTimer import android.os.CountDownTimer
import android.text.SpannableString import android.text.SpannableString
import androidx.activity.OnBackPressedCallback
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip import androidx.car.app.model.ActionStrip
@@ -20,88 +22,79 @@ import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Lifecycle
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationUtils import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import kotlin.math.min
/** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */ /** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */
class RoutePreviewScreen( class RoutePreviewScreen(
carContext: CarContext, carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
private var destination: Place, private var destination: Place,
private val navigationViewModel: NavigationViewModel private val navigationViewModel: NavigationViewModel,
private val routeModel: RouteCarModel
) : ) :
Screen(carContext) { Screen(carContext) {
private var isFavorite = false private var isFavorite = false
val routeModel = RouteCarModel() val maxListItems: Int = 3
val navigationUtils = NavigationUtils(carContext) val navigationUtils = NavigationUtils(carContext)
val observer = Observer<String> { route ->
if (route.isNotEmpty()) { private val backPressedCallback = object : OnBackPressedCallback(false) {
val repository = getSettingsRepository(carContext) override fun handleOnBackPressed() {
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel)
invalidate() invalidate()
} }
} }
init { init {
navigationViewModel.previewRoute.observe(this, observer) carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback)
val location = location(destination.longitude, destination.latitude)
navigationViewModel.loadPreviewRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val navigateActionIcon: CarIcon = CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.navigation_48px
)
).build()
val navigateAction = Action.Builder()
.setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate() }
.build()
val itemListBuilder = ItemList.Builder() val itemListBuilder = ItemList.Builder()
var i = 0 if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
routeModel.route.routes.forEach { _ -> val listLimit = min(
itemListBuilder.addItem(createRow(i++, navigateAction)) maxListItems,
carContext.getCarService(ConstraintManager::class.java)
.getContentLimit(
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
)
)
var index = 0
routeModel.route.routes.forEach { route ->
itemListBuilder.addItem(createRow(route, index++))
} }
}
val header = Header.Builder() val header = Header.Builder()
.setStartHeaderAction(Action.BACK) .setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.route_preview)) .setTitle(carContext.getString(R.string.route_preview))
.addEndHeaderAction(
if (routeModel.route.routes.size == 1) {
header.addEndHeaderAction(
favoriteAction() favoriteAction()
) )
.addEndHeaderAction( header.addEndHeaderAction(
deleteFavoriteAction() deleteFavoriteAction()
) )
.build() }
val message = val message =
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) { if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
createRouteText(0) createRouteText(routeModel.route.routes.first())
} else { } else {
CarText.Builder("Wait") CarText.Builder("Wait")
.build() .build()
@@ -110,7 +103,7 @@ class RoutePreviewScreen(
val timer = object : CountDownTimer(5000, 1000) { val timer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
onNavigate() onNavigate(0)
} }
} }
timer.start() timer.start()
@@ -118,18 +111,27 @@ class RoutePreviewScreen(
val content = if (routeModel.route.routes.size > 1) { val content = if (routeModel.route.routes.size > 1) {
ListTemplate.Builder() ListTemplate.Builder()
.setHeader(header) .setHeader(header.build())
.setSingleList(itemListBuilder.build()) .setSingleList(itemListBuilder.build())
.build() .build()
} else { } else {
val navigateActionIcon: CarIcon = CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.navigation_48px
)
).build()
val navigateAction = Action.Builder()
.setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate(0) }
.build()
MessageTemplate.Builder( MessageTemplate.Builder(
message message
) )
.setHeader(header) .setHeader(header.build())
.addAction(navigateAction) .addAction(navigateAction)
.setLoading(message.toString() == "Wait") .setLoading(message.toString() == "Wait")
.build() .build()
} }
return MapWithContentTemplate.Builder() return MapWithContentTemplate.Builder()
.setContentTemplate(content) .setContentTemplate(content)
@@ -192,10 +194,10 @@ class RoutePreviewScreen(
) )
.build() .build()
private fun createRouteText(index: Int): CarText { private fun createRouteText(route: Routes): CarText {
val time = routeModel.route.routes[index].summary.duration val time = route.summary.duration
val length = val length =
BigDecimal((routeModel.route.routes[index].summary.distance) / 1000).setScale( BigDecimal((route.summary.distance) / 1000).setScale(
1, 1,
RoundingMode.HALF_EVEN RoundingMode.HALF_EVEN
) )
@@ -207,18 +209,43 @@ class RoutePreviewScreen(
.build() .build()
} }
private fun createRow(index: Int, action: Action): Row { private fun createRow(route: Routes, index: Int): Row {
val route = createRouteText(index) val navigateActionIcon: CarIcon = CarIcon.Builder(
val titleText = "$index" IconCompat.createWithResource(
return Row.Builder() carContext, R.drawable.navigation_48px
.setTitle(route) )
.setOnClickListener { onRouteSelected(index) } ).build()
.addText(titleText) val navigateAction = Action.Builder()
.addAction(action) .setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate(index) }
.build() .build()
val routeText = createRouteText(route)
var street = ""
var maxDistance = 0.0
routeModel.route.routes[index].legs.first().steps.forEach {
if (it.distance > maxDistance) {
maxDistance = it.distance
street = it.street
}
}
val delay = (route.summary.trafficDelay / 60).toInt().toString()
val row = Row.Builder()
.setTitle(routeText)
.setOnClickListener { onRouteSelected(index) }
.addText(street)
.addAction(navigateAction)
if (route.summary.trafficDelay > 60) {
row.addText("$delay min")
}
return row.build()
} }
private fun onNavigate() { private fun onNavigate(index: Int) {
destination.routeIndex = index
setResult(destination) setResult(destination)
finish() finish()
} }

View File

@@ -16,7 +16,9 @@ import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Category import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants.CATEGORIES
import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
@@ -30,11 +32,13 @@ class SearchScreen(
var isSearchComplete: Boolean = false var isSearchComplete: Boolean = false
var category = ""
var categories: List<Category> = listOf( var categories: List<Category> = listOf(
Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)), Category(id = RECENT, name = carContext.getString(R.string.recent_destinations)),
// Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)), // Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)), Category(id = CATEGORIES, name = carContext.getString(R.string.category_title)),
Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites)) Category(id = FAVORITES, name = carContext.getString(R.string.favorites))
) )
lateinit var searchResult: List<SearchResult> lateinit var searchResult: List<SearchResult>
@@ -44,8 +48,50 @@ class SearchScreen(
invalidate() invalidate()
} }
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
if (newPlaces.isNotEmpty()) {
screenManager
.pushForResult(
PlaceListScreen(
carContext,
surfaceRenderer,
RECENT,
navigationViewModel,
newPlaces
)
) { obj: Any? ->
surfaceRenderer.clearRouteData()
if (obj != null) {
setResult(obj)
finish()
}
}
}
}
val observerFavorites = Observer<List<Place>> { newPlaces ->
screenManager
.pushForResult(
PlaceListScreen(
carContext,
surfaceRenderer,
FAVORITES,
navigationViewModel,
newPlaces
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
init { init {
navigationViewModel.searchPlaces.observe(this, observer) navigationViewModel.searchPlaces.observe(this, observer)
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
navigationViewModel.favorites.observe(this, observerFavorites)
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -62,7 +108,7 @@ class SearchScreen(
.setTitle(it.name) .setTitle(it.name)
.setImage(categoryIcon(it.id)) .setImage(categoryIcon(it.id))
.setOnClickListener { .setOnClickListener {
if (it.id == Constants.CATEGORIES) { if (it.id == CATEGORIES) {
screenManager screenManager
.pushForResult( .pushForResult(
CategoriesScreen( CategoriesScreen(
@@ -78,19 +124,19 @@ class SearchScreen(
} }
} }
} else { } else {
screenManager if (it.id == RECENT) {
.pushForResult( navigationViewModel.loadRecentPlaces(
PlaceListScreen(
carContext, carContext,
surfaceRenderer, surfaceRenderer.lastLocation,
it.id, surfaceRenderer.carOrientation
navigationViewModel
) )
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
} }
if (it.id == FAVORITES) {
navigationViewModel.loadFavorites(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
} }
} }
} }
@@ -123,12 +169,14 @@ class SearchScreen(
fun categoryIcon(category: String?): CarIcon { fun categoryIcon(category: String?): CarIcon {
val resId: Int = when (category) { val resId: Int = when (category) {
Constants.RECENT -> { RECENT -> {
R.drawable.ic_place_white_24dp R.drawable.ic_place_white_24dp
} }
Constants.FAVORITES -> {
FAVORITES -> {
R.drawable.ic_favorite_white_24dp R.drawable.ic_favorite_white_24dp
} }
else -> { else -> {
R.drawable.navigation_48px R.drawable.navigation_48px
} }

View File

@@ -43,7 +43,8 @@ data class Place(
var street: String? = null, var street: String? = null,
var distance: Float = 0F, var distance: Float = 0F,
//var avatar: Uri? = null, //var avatar: Uri? = null,
var lastDate: Long = 0 var lastDate: Long = 0,
var routeIndex: Int = 0
) )
data class ContactData( data class ContactData(

View File

@@ -23,12 +23,14 @@ data class Route(
val routeEngine: Int, val routeEngine: Int,
val routes: List<com.kouros.navigation.data.route.Routes>, val routes: List<com.kouros.navigation.data.route.Routes>,
var currentStepIndex: Int = 0, var currentStepIndex: Int = 0,
var routeIndex : Int = 0,
) { ) {
data class Builder( data class Builder(
var routeEngine: Int = 0, var routeEngine: Int = 0,
var summary: Summary = Summary(), var summary: Summary = Summary(),
var routes: List<com.kouros.navigation.data.route.Routes> = emptyList(), var routes: List<com.kouros.navigation.data.route.Routes> = emptyList(),
var routeIndex : Int = 0
) { ) {
fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine } fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine }
@@ -38,6 +40,8 @@ data class Route(
} }
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine } fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
fun routeIndex(routeIndex: Int) = apply { this.routeIndex = routeIndex}
fun route(route: String) = apply { fun route(route: String) = apply {
if (route.isNotEmpty() && route != "[]") { if (route.isNotEmpty() && route != "[]") {
val gson = GsonBuilder().serializeNulls().create() val gson = GsonBuilder().serializeNulls().create()
@@ -68,6 +72,7 @@ data class Route(
return Route( return Route(
routeEngine = this.routeEngine, routeEngine = this.routeEngine,
routes = this.routes, routes = this.routes,
routeIndex = this.routeIndex
) )
} }
@@ -75,6 +80,7 @@ data class Route(
return Route( return Route(
routeEngine = 0, routeEngine = 0,
routes = emptyList(), routes = emptyList(),
routeIndex = 0
) )
} }
} }
@@ -82,7 +88,7 @@ data class Route(
fun legs(): List<Leg> { fun legs(): List<Leg> {
return if (routes.isNotEmpty()) { return if (routes.isNotEmpty()) {
routes.first().legs routes[routeIndex].legs
} else { } else {
emptyList() emptyList()
} }

View File

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

View File

@@ -3,10 +3,8 @@ package com.kouros.navigation.model
//import com.kouros.navigation.data.Preferences.boxStore //import com.kouros.navigation.data.Preferences.boxStore
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.util.Log
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -58,7 +56,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
/** LiveData containing list of recent navigation destinations */ /** LiveData containing list of recent navigation destinations */
val places: MutableLiveData<List<Place>> by lazy { val recentPlaces: MutableLiveData<List<Place>> by lazy {
MutableLiveData() MutableLiveData()
} }
@@ -144,11 +142,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val settingsRepository = getSettingsRepository(context) val settingsRepository = getSettingsRepository(context)
val rp = settingsRepository.recentPlacesFlow.first() val rp = settingsRepository.recentPlacesFlow.first()
val gson = GsonBuilder().serializeNulls().create() val gson = GsonBuilder().serializeNulls().create()
val recentPlaces = gson.fromJson(rp, Places::class.java) val places = gson.fromJson(rp, Places::class.java)
val pl = mutableListOf<Place>() val pl = mutableListOf<Place>()
var id: Long = 0 var id: Long = 0
if (rp.isNotEmpty()) { if (rp.isNotEmpty()) {
for (place in recentPlaces.places) { for (place in places.places) {
if (place.category.equals(Constants.RECENT)) { if (place.category.equals(Constants.RECENT)) {
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
if (place.latitude != 0.0) { if (place.latitude != 0.0) {
@@ -167,7 +165,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
} }
places.postValue(pl) recentPlaces.postValue(pl)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }

View File

@@ -34,6 +34,7 @@ open class RouteModel {
route = Route.Builder() route = Route.Builder()
.routeEngine(navState.routingEngine) .routeEngine(navState.routingEngine)
.route(routeString) .route(routeString)
.routeIndex(navState.currentRouteIndex)
.build() .build()
) )
if (hasLegs()) { if (hasLegs()) {
@@ -42,7 +43,7 @@ open class RouteModel {
} }
fun hasLegs(): Boolean { fun hasLegs(): Boolean {
return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty() return navState.route.routes.isNotEmpty() && navState.route.routes[navState.currentRouteIndex].legs.isNotEmpty()
} }
fun stopNavigation() { fun stopNavigation() {

View File

@@ -9,8 +9,11 @@ import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.lang.Math.toDegrees
import java.lang.Math.toRadians
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -19,6 +22,8 @@ import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.cos
import kotlin.math.ln
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.time.Duration import kotlin.time.Duration
@@ -53,14 +58,35 @@ fun calculateZoom(speed: Double?): Double {
return zoom return zoom
} }
fun previewZoom(previewDistance: Double): Double { fun previewZoom(centerLocation: Location, previewDistance: Double): Double {
when (previewDistance / 1000) { return calculateZoomFromBoundingBox(centerLocation, previewDistance / 1000)
in 0.0..10.0 -> return 13.5
in 10.0..20.0 -> return 11.5
in 20.0..30.0 -> return 10.5
} }
return 9.5
fun calculateZoomFromBoundingBox(centerLocation: Location, previewDistance: Double): Double {
val earthRadius = 6371.0
val maxLat = centerLocation.latitude + toDegrees(previewDistance / earthRadius)
val minLat = centerLocation.latitude - toDegrees(previewDistance / earthRadius)
val maxLon = centerLocation.longitude + toDegrees(previewDistance / earthRadius / cos(toRadians(centerLocation.latitude)))
val minLon = centerLocation.longitude - toDegrees(previewDistance / earthRadius / cos(toRadians(centerLocation.latitude)))
var zoomLevel: Double
val latDiff = maxLat - minLat
val lngDiff = maxLon - minLon
val maxDiff = if(lngDiff > latDiff) lngDiff else latDiff
if (maxDiff < 360 / 2.0.pow(20.0)) {
zoomLevel = 21.0
} else {
zoomLevel = (-1 * ((ln(maxDiff) / ln(2.0)) - (ln(360.0) / ln(2.0))));
if (zoomLevel < 1)
zoomLevel = 1.0;
} }
return zoomLevel + 1.2
}
fun calculateTilt(newZoom: Double, tilt: Double): Double = fun calculateTilt(newZoom: Double, tilt: Double): Double =
if (newZoom < 13) { if (newZoom < 13) {

File diff suppressed because it is too large Load Diff