Preview
This commit is contained in:
@@ -13,8 +13,8 @@ android {
|
||||
applicationId = "com.kouros.navigation"
|
||||
minSdk = 33
|
||||
targetSdk = 36
|
||||
versionCode = 64
|
||||
versionName = "0.2.0.64"
|
||||
versionCode = 66
|
||||
versionName = "0.2.0.66"
|
||||
base.archivesName = "navi-$versionName"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.kouros.navigation.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AppOpsManager
|
||||
import android.location.LocationManager
|
||||
import android.os.Bundle
|
||||
@@ -11,32 +10,22 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.compose.animation.core.animateIntAsState
|
||||
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.BottomSheetScaffold
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -68,6 +57,8 @@ import com.kouros.navigation.model.testSingle
|
||||
import com.kouros.navigation.ui.app.AppViewModel
|
||||
import com.kouros.navigation.ui.app.appViewModel
|
||||
import com.kouros.navigation.ui.navigation.AppNavGraph
|
||||
import com.kouros.navigation.ui.navigation.NavigationSheet
|
||||
import com.kouros.navigation.ui.search.SearchSheet
|
||||
import com.kouros.navigation.ui.theme.NavigationTheme
|
||||
import com.kouros.navigation.utils.GeoUtils.snapLocation
|
||||
import com.kouros.navigation.utils.bearing
|
||||
@@ -80,8 +71,10 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.Location
|
||||
import org.maplibre.compose.location.UserLocationState
|
||||
import org.maplibre.compose.location.rememberDefaultLocationProvider
|
||||
import org.maplibre.compose.location.rememberUserLocationState
|
||||
import org.maplibre.compose.style.BaseStyle
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -121,6 +114,7 @@ class MainActivity : ComponentActivity() {
|
||||
SimulationType.GPX -> gpx(
|
||||
context = applicationContext, mock
|
||||
)
|
||||
|
||||
SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock)
|
||||
}
|
||||
}
|
||||
@@ -178,30 +172,19 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
CheckPermissionScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@Composable
|
||||
fun CheckPermissionScreen() {
|
||||
val permissions = listOf(
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
)
|
||||
PermissionScreen(
|
||||
permissions = permissions,
|
||||
requiredPermissions = listOf(permissions.first()),
|
||||
onGranted = {
|
||||
Application()
|
||||
},
|
||||
CheckPermissionScreen(app = {
|
||||
AppNavGraph(
|
||||
mainActivity = this
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StartScreen(
|
||||
navController: NavHostController) {
|
||||
navController: NavHostController
|
||||
) {
|
||||
|
||||
val appViewModel: AppViewModel = appViewModel()
|
||||
val darkMode by appViewModel.darkMode.collectAsState()
|
||||
@@ -209,6 +192,10 @@ class MainActivity : ComponentActivity() {
|
||||
val locationProvider = rememberDefaultLocationProvider(
|
||||
updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest
|
||||
)
|
||||
val lastRoute by appViewModel.lastRoute.collectAsState()
|
||||
if (lastRoute.isNotEmpty()) {
|
||||
navigationViewModel.route.value = lastRoute
|
||||
}
|
||||
val userLocationState = rememberUserLocationState(locationProvider)
|
||||
if (!useMock) {
|
||||
val locationState = locationProvider.location.collectAsState()
|
||||
@@ -217,86 +204,27 @@ class MainActivity : ComponentActivity() {
|
||||
val step: StepData? by stepData.observeAsState()
|
||||
val nextStep: StepData? by nextStepData.observeAsState()
|
||||
|
||||
var collapsedHeight by remember {
|
||||
mutableIntStateOf(300)
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
fun closeSheet() {
|
||||
scope.launch {
|
||||
collapsedHeight = if (routeModel.isNavigating()) {
|
||||
150
|
||||
} else {
|
||||
300
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp
|
||||
var expandedType by remember {
|
||||
mutableStateOf(ExpandedType.COLLAPSED)
|
||||
}
|
||||
|
||||
val height by animateIntAsState(
|
||||
when (expandedType) {
|
||||
ExpandedType.HALF -> screenHeight / 2
|
||||
ExpandedType.FULL -> screenHeight
|
||||
ExpandedType.COLLAPSED -> collapsedHeight
|
||||
}
|
||||
)
|
||||
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState()
|
||||
NavigationTheme(useDarkTheme = darkMode == 1) {
|
||||
BottomSheetScaffold(
|
||||
scaffoldState = bottomSheetScaffoldState,
|
||||
sheetShape = RoundedCornerShape(
|
||||
bottomStart = 0.dp,
|
||||
bottomEnd = 0.dp,
|
||||
topStart = 12.dp,
|
||||
topEnd = 12.dp
|
||||
),
|
||||
sheetContent = {
|
||||
var isUpdated = false
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(height.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectVerticalDragGestures(
|
||||
onVerticalDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
if (!isUpdated) {
|
||||
expandedType = when {
|
||||
dragAmount < 0 && expandedType == ExpandedType.COLLAPSED -> {
|
||||
ExpandedType.HALF
|
||||
}
|
||||
dragAmount < 0 && expandedType == ExpandedType.HALF -> {
|
||||
ExpandedType.FULL
|
||||
}
|
||||
|
||||
dragAmount > 0 && expandedType == ExpandedType.FULL -> {
|
||||
ExpandedType.HALF
|
||||
}
|
||||
|
||||
dragAmount > 0 && expandedType == ExpandedType.HALF -> {
|
||||
ExpandedType.COLLAPSED
|
||||
}
|
||||
else -> {
|
||||
ExpandedType.FULL
|
||||
}
|
||||
}
|
||||
isUpdated = true
|
||||
}
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||
SheetLayout(
|
||||
map = { _ ->
|
||||
Map(
|
||||
userLocationState, step, nextStep, baseStyle, navController
|
||||
)
|
||||
},
|
||||
onDragEnd = {
|
||||
isUpdated = false
|
||||
}
|
||||
menu = { SheetContent(navController, step, nextStep) },
|
||||
)
|
||||
}
|
||||
) {
|
||||
SheetContent(step, nextStep) { closeSheet() }
|
||||
}
|
||||
},
|
||||
sheetPeekHeight = height.dp
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Map(
|
||||
userLocationState: UserLocationState,
|
||||
step: StepData?,
|
||||
nextStep: StepData?,
|
||||
baseStyle: BaseStyle.Json,
|
||||
navController: NavHostController
|
||||
) {
|
||||
MapView(
|
||||
applicationContext,
|
||||
@@ -312,18 +240,6 @@ class MainActivity : ComponentActivity() {
|
||||
Settings(navController, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Application() {
|
||||
val appViewModel: AppViewModel = appViewModel()
|
||||
val lastRoute by appViewModel.lastRoute.collectAsState()
|
||||
if (lastRoute.isNotEmpty()) {
|
||||
navigationViewModel.route.value = lastRoute
|
||||
}
|
||||
AppNavGraph(this)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Settings(navController: NavController, modifier: Modifier = Modifier) {
|
||||
@@ -347,10 +263,10 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
@Composable
|
||||
fun SheetContent(
|
||||
step: StepData?, nextStep: StepData?, closeSheet: () -> Unit
|
||||
navController: NavHostController, step: StepData?, nextStep: StepData?
|
||||
) {
|
||||
if (!routeModel.isNavigating()) {
|
||||
SearchSheet(applicationContext, navigationViewModel, lastLocation) { closeSheet() }
|
||||
SearchSheet(applicationContext, navController, navigationViewModel, lastLocation) { }
|
||||
} else {
|
||||
if (step != null) {
|
||||
NavigationSheet(
|
||||
@@ -358,7 +274,7 @@ class MainActivity : ComponentActivity() {
|
||||
routeModel,
|
||||
step,
|
||||
nextStep,
|
||||
{ stopNavigation { closeSheet() } },
|
||||
{ stopNavigation {} },
|
||||
{ simulateNavigation() })
|
||||
}
|
||||
}
|
||||
@@ -446,8 +362,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
fun simulateNavigation() {
|
||||
simulate(
|
||||
routeModel = routeModel,
|
||||
mock = mock
|
||||
routeModel = routeModel, mock = mock
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.kouros.navigation.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -18,6 +19,7 @@ import com.kouros.navigation.car.map.NavigationImage
|
||||
import com.kouros.navigation.data.StepData
|
||||
import com.kouros.navigation.ui.app.AppViewModel
|
||||
import com.kouros.navigation.ui.app.appViewModel
|
||||
import com.kouros.navigation.ui.navigation.NavigationInfo
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.rememberCameraState
|
||||
import org.maplibre.compose.location.LocationTrackingEffect
|
||||
@@ -62,6 +64,8 @@ fun MapView(
|
||||
val showBuildings by appViewModel.show3D.collectAsState()
|
||||
val darkMode by appViewModel.darkMode.collectAsState()
|
||||
|
||||
val dark = darkMode == 1 || darkMode == 2 && isSystemInDarkTheme()
|
||||
|
||||
Column {
|
||||
NavigationInfo(step, nextStep)
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
@@ -88,7 +92,7 @@ fun MapView(
|
||||
duration = 1.seconds
|
||||
)
|
||||
}
|
||||
NavigationImage(paddingValues, width, height / 6, "", darkMode)
|
||||
NavigationImage(paddingValues, width, height / 6, "", dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.kouros.navigation.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import androidx.compose.animation.animateContentSize
|
||||
@@ -37,6 +39,25 @@ import androidx.core.net.toUri
|
||||
*
|
||||
* By default it assumes that all [permissions] are required.
|
||||
*/
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@Composable
|
||||
fun CheckPermissionScreen(
|
||||
app: @Composable () -> Unit,
|
||||
) {
|
||||
val permissions = listOf(
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
)
|
||||
PermissionScreen(
|
||||
permissions = permissions,
|
||||
requiredPermissions = listOf(permissions.first()),
|
||||
onGranted = {
|
||||
app()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun PermissionScreen(
|
||||
|
||||
81
app/src/main/java/com/kouros/navigation/ui/SheetLayout.kt
Normal file
81
app/src/main/java/com/kouros/navigation/ui/SheetLayout.kt
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
|
||||
import com.kouros.navigation.ui.MainActivity
|
||||
import com.kouros.navigation.ui.search.SearchScreen
|
||||
import com.kouros.navigation.ui.settings.SettingsRoute
|
||||
|
||||
|
||||
@@ -17,5 +19,7 @@ fun AppNavGraph(mainActivity: MainActivity) {
|
||||
composable("display_settings") { SettingsRoute("display_settings", navController) { navController.popBackStack() } }
|
||||
composable("nav_settings") { SettingsRoute("nav_settings", navController) { navController.popBackStack() } }
|
||||
composable("settings") { SettingsRoute("settings", navController) { navController.popBackStack() } }
|
||||
composable("search") { SearchScreen(navController, navController.context, navigationViewModel, mainActivity.lastLocation) { navController.popBackStack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.kouros.navigation.ui
|
||||
package com.kouros.navigation.ui.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.kouros.navigation.ui
|
||||
package com.kouros.navigation.ui.navigation
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -1,33 +1,35 @@
|
||||
package com.kouros.navigation.ui
|
||||
package com.kouros.navigation.ui.search
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.captionBar
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SearchBar
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -35,119 +37,93 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.PlaceColor
|
||||
import com.kouros.navigation.data.nominatim.SearchResult
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.ui.theme.NavigationTheme
|
||||
import com.kouros.navigation.utils.location
|
||||
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchSheet(
|
||||
applicationContext: Context,
|
||||
viewModel: NavigationViewModel,
|
||||
fun SearchScreen(
|
||||
navController: NavHostController,
|
||||
context: Context,
|
||||
navigationViewModel: NavigationViewModel,
|
||||
location: Location,
|
||||
closeSheet: () -> Unit
|
||||
function: () -> Unit
|
||||
) {
|
||||
val searchResults = mutableListOf<SearchResult>()
|
||||
val recentPlaces = viewModel.places.observeAsState()
|
||||
val search = viewModel.searchPlaces.observeAsState()
|
||||
if (search.value != null) {
|
||||
searchResults.addAll(search.value!!)
|
||||
}
|
||||
val textFieldState = rememberTextFieldState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
) {
|
||||
SearchBar(
|
||||
textFieldState = textFieldState,
|
||||
searchPlaces = emptyList(),
|
||||
searchResults = searchResults,
|
||||
viewModel = viewModel,
|
||||
context = applicationContext,
|
||||
location = location,
|
||||
closeSheet = { closeSheet() }
|
||||
|
||||
NavigationTheme(true) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
stringResource(id = R.string.search_action_title),
|
||||
)
|
||||
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
|
||||
if (recentPlaces.value != null) {
|
||||
val items = listOf(recentPlaces)
|
||||
if (items.isNotEmpty()) {
|
||||
RecentPlaces(
|
||||
recentPlaces.value!!,
|
||||
viewModel,
|
||||
applicationContext,
|
||||
location,
|
||||
closeSheet
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Home(
|
||||
applicationContext: Context,
|
||||
viewModel: NavigationViewModel,
|
||||
location: Location,
|
||||
closeSheet: () -> Unit
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Button(onClick = {
|
||||
val places = viewModel.loadRecentPlace(applicationContext)
|
||||
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
|
||||
viewModel.loadRoute(applicationContext, location, toLocation, 0F)
|
||||
closeSheet()
|
||||
}) {
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = function) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_place_white_24dp),
|
||||
"Home",
|
||||
modifier = Modifier.size(24.dp, 24.dp),
|
||||
painter = painterResource(R.drawable.arrow_back_24px),
|
||||
contentDescription = stringResource(id = R.string.accept_action_title),
|
||||
modifier = Modifier.size(48.dp, 48.dp),
|
||||
)
|
||||
Text("Home")
|
||||
}
|
||||
Button(onClick = {
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_favorite_white_24dp),
|
||||
"Work",
|
||||
modifier = Modifier.size(24.dp, 24.dp),
|
||||
},
|
||||
)
|
||||
Text("Arbeit")
|
||||
},
|
||||
)
|
||||
{ padding ->
|
||||
val scrollState = rememberScrollState()
|
||||
Column(Modifier.padding(top = 50.dp)) {
|
||||
SearchBar(context, navigationViewModel, location)
|
||||
Categories(context, navigationViewModel, location, closeSheet = { })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) {
|
||||
viewModel.searchPlaces(it, location)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
textFieldState: TextFieldState,
|
||||
searchPlaces: List<Place>,
|
||||
searchResults: List<SearchResult>,
|
||||
viewModel: NavigationViewModel,
|
||||
context: Context,
|
||||
navigationViewModel: NavigationViewModel,
|
||||
location: Location,
|
||||
closeSheet: () -> Unit
|
||||
|
||||
) {
|
||||
|
||||
val searchResults = mutableListOf<SearchResult>()
|
||||
val search = navigationViewModel.searchPlaces.observeAsState()
|
||||
if (search.value != null) {
|
||||
searchResults.addAll(search.value!!)
|
||||
}
|
||||
val textFieldState = rememberTextFieldState()
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
SearchBar(
|
||||
windowInsets = WindowInsets.captionBar,
|
||||
colors = SearchBarDefaults.colors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
),
|
||||
inputField = {
|
||||
SearchBarDefaults.InputField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.search_48px),
|
||||
@@ -158,24 +134,64 @@ fun SearchBar(
|
||||
query = textFieldState.text.toString(),
|
||||
onQueryChange = { textFieldState.edit { replace(0, length, it) } },
|
||||
onSearch = {
|
||||
searchPlaces(viewModel, location, it)
|
||||
expanded = false
|
||||
navigationViewModel.searchPlaces(it, location)
|
||||
expanded = true
|
||||
},
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
onExpandedChange = {
|
||||
//expanded = it
|
||||
},
|
||||
placeholder = { Text(context.getString(R.string.search_action_title)) }
|
||||
)
|
||||
},
|
||||
expanded = expanded,
|
||||
onExpandedChange = { },
|
||||
) {
|
||||
if (searchPlaces.isNotEmpty()) {
|
||||
Text(context.getString(R.string.recent_destinations))
|
||||
RecentPlaces(searchPlaces, viewModel, context, location, closeSheet)
|
||||
}
|
||||
if (searchResults.isNotEmpty()) {
|
||||
Text("Search places")
|
||||
SearchPlaces( searchResults, viewModel, context, location, closeSheet)
|
||||
SearchPlaces(searchResults, navigationViewModel, context, location, { })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Categories(
|
||||
applicationContext: Context,
|
||||
viewModel: NavigationViewModel,
|
||||
location: Location,
|
||||
closeSheet: () -> Unit
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
modifier = Modifier.horizontalScroll(scrollState)
|
||||
) {
|
||||
Button(onClick = {
|
||||
val places = viewModel.loadRecentPlace(applicationContext)
|
||||
val toLocation = location(places.first()!!.longitude, places.first()!!.latitude)
|
||||
viewModel.loadRoute(applicationContext, location, toLocation, 0F)
|
||||
closeSheet()
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.local_gas_station_24),
|
||||
applicationContext.getString(R.string.fuel_station),
|
||||
modifier = Modifier.size(24.dp, 24.dp),
|
||||
)
|
||||
}
|
||||
Button(onClick = {
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ev_station_24px),
|
||||
"Work",
|
||||
modifier = Modifier.size(24.dp, 24.dp),
|
||||
)
|
||||
}
|
||||
Button(onClick = {
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.local_pharmacy_24px),
|
||||
"Work",
|
||||
modifier = Modifier.size(24.dp, 24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +209,6 @@ private fun SearchPlaces(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
if (searchResults.isNotEmpty()) {
|
||||
items(searchResults, key = { it.placeId }) { place ->
|
||||
Row {
|
||||
Icon(
|
||||
@@ -227,41 +242,5 @@ private fun SearchPlaces(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecentPlaces(
|
||||
recentPlaces: List<Place>,
|
||||
viewModel: NavigationViewModel,
|
||||
context: Context,
|
||||
location: Location,
|
||||
closeSheet: () -> Unit
|
||||
) {
|
||||
val color = remember { PlaceColor }
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(recentPlaces, key = { it.id }) { place ->
|
||||
Row {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_place_white_24dp),
|
||||
"Navigation",
|
||||
tint = color.copy(alpha = 1f),
|
||||
modifier = Modifier.size(24.dp, 24.dp),
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text("${place.street} ${place.postalCode} ${place.city}") },
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
val toLocation = location(place.longitude, place.latitude)
|
||||
viewModel.loadRoute(context, location, toLocation, 0F)
|
||||
closeSheet()
|
||||
}
|
||||
.fillMaxWidth()
|
||||
)
|
||||
HorizontalDivider(color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
180
app/src/main/java/com/kouros/navigation/ui/search/SearchSheet.kt
Normal file
180
app/src/main/java/com/kouros/navigation/ui/search/SearchSheet.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.kouros.navigation.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val md_theme_light_primary = Color(0xFF825500)
|
||||
val md_theme_light_primary = Color(0xFF6FE5E1)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF291800)
|
||||
@@ -33,7 +33,7 @@ val md_theme_light_surfaceTint = Color(0xFF825500)
|
||||
val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
|
||||
val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
val md_theme_dark_primary = Color(0xFFFFB951)
|
||||
val md_theme_dark_primary = Color(0xFF8CD6EF)
|
||||
val md_theme_dark_onPrimary = Color(0xFF452B00)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF633F00)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
|
||||
|
||||
@@ -102,7 +102,7 @@ fun NavigationTheme(
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = if (useDarkTheme) darkColorScheme() else colorScheme,
|
||||
colorScheme = if (useDarkTheme) DarkColors else colorScheme,
|
||||
typography = typography,
|
||||
content = content,
|
||||
shapes = shapes,
|
||||
|
||||
@@ -33,7 +33,6 @@ import com.kouros.navigation.data.Constants.TILT
|
||||
import com.kouros.navigation.data.Constants.homeVogelhart
|
||||
import com.kouros.navigation.data.RouteEngine
|
||||
import com.kouros.navigation.model.BaseStyleModel
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.utils.bearing
|
||||
import com.kouros.navigation.utils.calculateTilt
|
||||
import com.kouros.navigation.utils.calculateZoom
|
||||
@@ -46,6 +45,7 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.expressions.dsl.zoom
|
||||
import org.maplibre.compose.style.BaseStyle
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
|
||||
@@ -261,6 +261,7 @@ class SurfaceRenderer(
|
||||
val paddingValues = getPaddingValues(height, viewStyle)
|
||||
val cameraState = cameraState(paddingValues, position, tilt)
|
||||
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
|
||||
val dark = darkMode == 1 || darkMode == 2 && carContext.isDarkMode
|
||||
|
||||
MapLibre(
|
||||
cameraState,
|
||||
@@ -271,7 +272,7 @@ class SurfaceRenderer(
|
||||
speedCameras,
|
||||
showBuildings
|
||||
)
|
||||
ShowPosition(cameraState, position, paddingValues, darkMode)
|
||||
ShowPosition(cameraState, position, paddingValues, dark)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,18 +284,18 @@ class SurfaceRenderer(
|
||||
cameraState: CameraState,
|
||||
position: CameraPosition?,
|
||||
paddingValues: PaddingValues,
|
||||
darkMode: Int
|
||||
darkMode: Boolean
|
||||
) {
|
||||
val cameraDuration =
|
||||
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
|
||||
val currentSpeed: Float? by speed.observeAsState()
|
||||
val speed: Int? by maxSpeed.observeAsState()
|
||||
val maximumSpeed: Int? by maxSpeed.observeAsState()
|
||||
val streetName: String? by street.observeAsState()
|
||||
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
|
||||
DrawNavigationImages(
|
||||
paddingValues,
|
||||
currentSpeed,
|
||||
speed!!,
|
||||
maximumSpeed!!,
|
||||
width,
|
||||
height,
|
||||
streetName,
|
||||
@@ -340,7 +341,7 @@ class SurfaceRenderer(
|
||||
updateCameraPosition(
|
||||
cameraPosition.value!!.bearing,
|
||||
newZoom,
|
||||
cameraPosition.value!!.target,
|
||||
cameraPosition.value!!.target, tilt
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -379,7 +380,7 @@ class SurfaceRenderer(
|
||||
updateCameraPosition(
|
||||
bearing,
|
||||
zoom,
|
||||
Position(location.longitude, location.latitude)
|
||||
Position(location.longitude, location.latitude), tilt
|
||||
)
|
||||
lastBearing = cameraPosition.value!!.bearing
|
||||
lastLocation = location
|
||||
@@ -387,11 +388,19 @@ class SurfaceRenderer(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets route data for active navigation and switches to VIEW mode.
|
||||
*/
|
||||
fun setRouteData() {
|
||||
routeData.value = routeModel.curRoute.routeGeoJson
|
||||
viewStyle = ViewStyle.VIEW
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates camera position with new bearing, zoom, and target.
|
||||
* Posts update to LiveData for UI observation.
|
||||
*/
|
||||
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) {
|
||||
private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position, tilt: Double) {
|
||||
synchronized(this) {
|
||||
cameraPosition.postValue(
|
||||
cameraPosition.value!!.copy(
|
||||
@@ -408,9 +417,15 @@ class SurfaceRenderer(
|
||||
/**
|
||||
* Sets route data for active navigation and switches to VIEW mode.
|
||||
*/
|
||||
fun setRouteData() {
|
||||
routeData.value = routeModel.curRoute.routeGeoJson
|
||||
fun clearRouteData() {
|
||||
routeData.value = ""
|
||||
viewStyle = ViewStyle.VIEW
|
||||
cameraPosition.postValue(
|
||||
cameraPosition.value!!.copy(
|
||||
zoom = 16.0
|
||||
)
|
||||
)
|
||||
tilt = TILT
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,18 +439,21 @@ class SurfaceRenderer(
|
||||
* Sets up route preview mode with overview camera position.
|
||||
* Calculates appropriate zoom based on route distance.
|
||||
*/
|
||||
fun setPreviewRouteData(routeModel: RouteModel) {
|
||||
fun setPreviewRouteData(routeModel: RouteCarModel) {
|
||||
viewStyle = ViewStyle.PREVIEW
|
||||
with(routeModel) {
|
||||
routeData.value = curRoute.routeGeoJson
|
||||
centerLocation = curRoute.centerLocation
|
||||
previewDistance = curRoute.summary.distance
|
||||
}
|
||||
tilt = 0.0
|
||||
updateCameraPosition(
|
||||
0.0,
|
||||
previewZoom(previewDistance),
|
||||
Position(centerLocation.longitude, centerLocation.latitude)
|
||||
previewZoom(centerLocation, previewDistance),
|
||||
Position(centerLocation.longitude, centerLocation.latitude), 0.0
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -448,7 +466,7 @@ class SurfaceRenderer(
|
||||
updateCameraPosition(
|
||||
0.0,
|
||||
14.0,
|
||||
target = Position(location.longitude, location.latitude)
|
||||
target = Position(location.longitude, location.latitude), tilt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -19,6 +20,7 @@ import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.scale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.drawText
|
||||
@@ -33,6 +35,8 @@ import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.NavigationColor
|
||||
import com.kouros.navigation.data.RouteColor
|
||||
import com.kouros.navigation.data.SpeedColor
|
||||
import com.kouros.navigation.utils.isMetricSystem
|
||||
import com.kouros.navigation.utils.location
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.camera.rememberCameraState
|
||||
@@ -59,6 +63,7 @@ import org.maplibre.compose.sources.Source
|
||||
import org.maplibre.compose.sources.getBaseSource
|
||||
import org.maplibre.compose.sources.rememberGeoJsonSource
|
||||
import org.maplibre.compose.style.BaseStyle
|
||||
import org.maplibre.spatialk.geojson.BoundingBox
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
|
||||
|
||||
@@ -296,7 +301,7 @@ fun DrawNavigationImages(
|
||||
width: Int,
|
||||
height: Int,
|
||||
streetName: String?,
|
||||
darkMode: Int,
|
||||
darkMode: Boolean,
|
||||
) {
|
||||
NavigationImage(padding, width, height, streetName, darkMode)
|
||||
if (speed != null) {
|
||||
@@ -314,7 +319,7 @@ fun NavigationImage(
|
||||
width: Int,
|
||||
height: Int,
|
||||
streetName: String?,
|
||||
darkMode: Int
|
||||
darkMode: Boolean
|
||||
) {
|
||||
|
||||
val imageSize = (height / 8)
|
||||
@@ -323,9 +328,9 @@ fun NavigationImage(
|
||||
val textMeasurerStreet = rememberTextMeasurer()
|
||||
val street = streetName.toString()
|
||||
val styleStreet = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (darkMode == 1) Color.White else navigationColor,
|
||||
color = if (darkMode) Color.White else navigationColor,
|
||||
)
|
||||
val textLayoutStreet = remember(street) {
|
||||
textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis)
|
||||
@@ -348,28 +353,31 @@ fun NavigationImage(
|
||||
.size(imageSize.dp, imageSize.dp)
|
||||
.scale(scaleX = 1f, scaleY = 0.7f),
|
||||
)
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.size((width / 5).dp, (height/4).dp)
|
||||
.size(textLayoutStreet.size.width.dp, textLayoutStreet.size.height.dp * 6 )
|
||||
) {
|
||||
if (!streetName.isNullOrEmpty()) {
|
||||
if (street.isNotEmpty()) {
|
||||
val topLeftX = center.x - textLayoutStreet.size.width / 2
|
||||
val topLeftY = center.y + textLayoutStreet.size.height
|
||||
drawRoundRect(
|
||||
topLeft = Offset(
|
||||
x = center.x - textLayoutStreet.size.width / 2 ,
|
||||
y = center.y + textLayoutStreet.size.height,
|
||||
x = topLeftX ,
|
||||
y = topLeftY,
|
||||
),
|
||||
color = if (darkMode == 1) navigationColor else Color.White,
|
||||
color = if (darkMode) navigationColor else Color.White,
|
||||
cornerRadius = CornerRadius(x = 10f, y = 10f),
|
||||
)
|
||||
drawText(
|
||||
textMeasurer = textMeasurerStreet,
|
||||
text = streetName,
|
||||
text = street,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = styleStreet,
|
||||
topLeft = Offset(
|
||||
x = center.x - textLayoutStreet.size.width / 2,
|
||||
y = center.y + textLayoutStreet.size.height + 10,
|
||||
x = topLeftX,
|
||||
y = topLeftY + 10,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -384,6 +392,7 @@ private fun CurrentSpeed(
|
||||
curSpeed: Float,
|
||||
maxSpeed: Int
|
||||
) {
|
||||
|
||||
val radius = 34
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -395,9 +404,11 @@ private fun CurrentSpeed(
|
||||
) {
|
||||
val textMeasurerSpeed = rememberTextMeasurer()
|
||||
val textMeasurerKm = rememberTextMeasurer()
|
||||
val speed = (curSpeed * 3.6).toInt().toString()
|
||||
|
||||
val kmh = "km/h"
|
||||
val speed = if (isMetricSystem()) (curSpeed * 3.6).toInt().toString() else (curSpeed * 3.6 * 0.6214).toInt().toString()
|
||||
|
||||
val kmh = if (isMetricSystem()) "km/h" else "mph"
|
||||
|
||||
val styleSpeed = TextStyle(
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -433,7 +444,7 @@ private fun CurrentSpeed(
|
||||
)
|
||||
drawText(
|
||||
textMeasurer = textMeasurerKm,
|
||||
text = "km/h",
|
||||
text = kmh,
|
||||
style = styleKm,
|
||||
topLeft = Offset(
|
||||
x = center.x - textLayoutKm.size.width / 2,
|
||||
|
||||
@@ -17,6 +17,7 @@ class Simulation {
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
updateLocation: (Location) -> Unit
|
||||
) {
|
||||
if (routeModel.navState.route.isRouteValid()) {
|
||||
val points = routeModel.curRoute.waypoints
|
||||
if (points.isEmpty()) return
|
||||
simulationJob?.cancel()
|
||||
@@ -36,13 +37,14 @@ class Simulation {
|
||||
curBearing = lastLocation.bearingTo(fakeLocation)
|
||||
// Update your app's state as if a real GPS update occurred
|
||||
updateLocation(fakeLocation)
|
||||
// Wait before moving to the next point (e.g., every 2 seconds)
|
||||
// Wait before moving to the next point (e.g., every 1 second)
|
||||
delay(1000)
|
||||
lastLocation = fakeLocation
|
||||
}
|
||||
routeModel.stopNavigation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopSimulation() {
|
||||
simulationJob?.cancel()
|
||||
|
||||
@@ -116,7 +116,8 @@ class CategoryScreen(
|
||||
} else {
|
||||
row.addText("${(it.distance / 1000).round(1)} km")
|
||||
}
|
||||
if (category == Constants.CHARGING_STATION) {
|
||||
if (category == CHARGING_STATION) {
|
||||
if (it.tags.socketType2 != null)
|
||||
row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
|
||||
} else {
|
||||
row.addText(carText("${it.tags.openingHours}"))
|
||||
@@ -134,7 +135,7 @@ class CategoryScreen(
|
||||
setResult(
|
||||
Place(
|
||||
name = name,
|
||||
category = Constants.CHARGING_STATION,
|
||||
category = CHARGING_STATION,
|
||||
latitude = it.lat,
|
||||
longitude = it.lon
|
||||
)
|
||||
|
||||
@@ -377,7 +377,6 @@ class NavigationScreen(
|
||||
*/
|
||||
private fun stopAction(): Action {
|
||||
return Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.stop_action_title))
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
@@ -531,6 +530,8 @@ class NavigationScreen(
|
||||
* Pushes the search screen and handles the search result.
|
||||
*/
|
||||
private fun startSearchScreen() {
|
||||
navigationViewModel.recentPlaces.value = emptyList()
|
||||
navigationViewModel.previewRoute.value = ""
|
||||
screenManager
|
||||
.pushForResult(
|
||||
SearchScreen(
|
||||
@@ -558,16 +559,22 @@ class NavigationScreen(
|
||||
* Loads a route to the specified place and sets it as the destination.
|
||||
*/
|
||||
fun navigateToPlace(place: Place) {
|
||||
val preview = navigationViewModel.previewRoute.value
|
||||
navigationType = NavigationType.VIEW
|
||||
val location = location(place.longitude, place.latitude)
|
||||
navigationViewModel.saveRecent(carContext, place)
|
||||
currentNavigationLocation = location
|
||||
if (preview.isNullOrEmpty()) {
|
||||
navigationViewModel.loadRoute(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
location,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
} else {
|
||||
routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
|
||||
navigationViewModel.route.value = preview
|
||||
}
|
||||
routeModel.navState = routeModel.navState.copy(destination = place)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.asLiveData
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
@@ -27,58 +26,52 @@ import com.kouros.navigation.data.Constants.RECENT
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
|
||||
class PlaceListScreen(
|
||||
private val carContext: CarContext,
|
||||
private val surfaceRenderer: SurfaceRenderer,
|
||||
private val category: String,
|
||||
private val navigationViewModel: NavigationViewModel
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
private val places: List<Place>
|
||||
) : Screen(carContext) {
|
||||
|
||||
var places = listOf<Place>()
|
||||
|
||||
val observer = Observer<List<Place>> { newPlaces ->
|
||||
places = newPlaces
|
||||
invalidate()
|
||||
val routeModel = RouteCarModel()
|
||||
|
||||
var place = Place()
|
||||
|
||||
val previewObserver = Observer<String> { route ->
|
||||
if (route.isNotEmpty()) {
|
||||
val repository = getSettingsRepository(carContext)
|
||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
||||
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
||||
routeModel.startNavigation(route)
|
||||
surfaceRenderer.setPreviewRouteData(routeModel)
|
||||
screenManager
|
||||
.pushForResult(
|
||||
RoutePreviewScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
place,
|
||||
navigationViewModel,
|
||||
routeModel = routeModel
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val observerAddress = Observer<List<Place>> { newContacts ->
|
||||
places = newContacts
|
||||
invalidate()
|
||||
}
|
||||
|
||||
init {
|
||||
if (category == RECENT) {
|
||||
navigationViewModel.places.observe(this, observer)
|
||||
}
|
||||
if (category == CONTACTS) {
|
||||
navigationViewModel.contactAddress.observe(this, observerAddress)
|
||||
}
|
||||
if (category == FAVORITES) {
|
||||
navigationViewModel.favorites.observe(this, observer)
|
||||
}
|
||||
loadPlaces()
|
||||
}
|
||||
|
||||
fun loadPlaces() {
|
||||
if (category == RECENT) {
|
||||
navigationViewModel.loadRecentPlaces(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
if (category == CONTACTS) {
|
||||
navigationViewModel.loadContacts(carContext)
|
||||
}
|
||||
if (category == FAVORITES) {
|
||||
navigationViewModel.loadFavorites(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
navigationViewModel.previewRoute.observe(this, previewObserver)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -94,7 +87,7 @@ class PlaceListScreen(
|
||||
// .setImage(contactIcon(it.avatar, it.category))
|
||||
.setTitle("$street ${it.city}")
|
||||
.setOnClickListener {
|
||||
val place = Place(
|
||||
place = Place(
|
||||
0,
|
||||
it.name,
|
||||
it.category,
|
||||
@@ -105,20 +98,13 @@ class PlaceListScreen(
|
||||
it.street,
|
||||
// avatar = null
|
||||
)
|
||||
screenManager
|
||||
.pushForResult(
|
||||
RoutePreviewScreen(
|
||||
val location = location(place.longitude, place.latitude)
|
||||
navigationViewModel.loadPreviewRoute(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
place,
|
||||
navigationViewModel
|
||||
surfaceRenderer.lastLocation,
|
||||
location,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (category != CONTACTS) {
|
||||
row.addText(SpannableString(" ").apply {
|
||||
@@ -184,4 +170,10 @@ class PlaceListScreen(
|
||||
}
|
||||
return CarIcon.Builder(IconCompat.createWithContentUri(avatar)).build()
|
||||
}
|
||||
|
||||
fun loadPlaces() {
|
||||
if (category == CONTACTS) {
|
||||
navigationViewModel.loadContacts(carContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.kouros.navigation.car.screen
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import android.text.SpannableString
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.Action.FLAG_DEFAULT
|
||||
import androidx.car.app.model.ActionStrip
|
||||
@@ -20,88 +22,79 @@ import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.navigation.model.MapController
|
||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||
import androidx.car.app.versioning.CarAppApiLevels
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.navigation.NavigationUtils
|
||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.route.Routes
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import kotlin.math.min
|
||||
|
||||
/** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */
|
||||
class RoutePreviewScreen(
|
||||
carContext: CarContext,
|
||||
private var surfaceRenderer: SurfaceRenderer,
|
||||
private var destination: Place,
|
||||
private val navigationViewModel: NavigationViewModel
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
private val routeModel: RouteCarModel
|
||||
) :
|
||||
Screen(carContext) {
|
||||
private var isFavorite = false
|
||||
|
||||
val routeModel = RouteCarModel()
|
||||
|
||||
val maxListItems: Int = 3
|
||||
val navigationUtils = NavigationUtils(carContext)
|
||||
val observer = Observer<String> { route ->
|
||||
if (route.isNotEmpty()) {
|
||||
val repository = getSettingsRepository(carContext)
|
||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
||||
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
||||
routeModel.startNavigation(route)
|
||||
surfaceRenderer.setPreviewRouteData(routeModel)
|
||||
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
navigationViewModel.previewRoute.observe(this, observer)
|
||||
val location = location(destination.longitude, destination.latitude)
|
||||
navigationViewModel.loadPreviewRoute(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
location,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val navigateActionIcon: CarIcon = CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext, R.drawable.navigation_48px
|
||||
)
|
||||
).build()
|
||||
val navigateAction = Action.Builder()
|
||||
.setFlags(FLAG_DEFAULT)
|
||||
.setIcon(navigateActionIcon)
|
||||
.setOnClickListener { this.onNavigate() }
|
||||
.build()
|
||||
|
||||
|
||||
val itemListBuilder = ItemList.Builder()
|
||||
var i = 0
|
||||
routeModel.route.routes.forEach { _ ->
|
||||
itemListBuilder.addItem(createRow(i++, navigateAction))
|
||||
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
|
||||
val listLimit = min(
|
||||
maxListItems,
|
||||
carContext.getCarService(ConstraintManager::class.java)
|
||||
.getContentLimit(
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
|
||||
)
|
||||
)
|
||||
var index = 0
|
||||
routeModel.route.routes.forEach { route ->
|
||||
itemListBuilder.addItem(createRow(route, index++))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val header = Header.Builder()
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.setTitle(carContext.getString(R.string.route_preview))
|
||||
.addEndHeaderAction(
|
||||
|
||||
if (routeModel.route.routes.size == 1) {
|
||||
header.addEndHeaderAction(
|
||||
favoriteAction()
|
||||
)
|
||||
.addEndHeaderAction(
|
||||
header.addEndHeaderAction(
|
||||
deleteFavoriteAction()
|
||||
)
|
||||
.build()
|
||||
|
||||
}
|
||||
val message =
|
||||
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
|
||||
createRouteText(0)
|
||||
createRouteText(routeModel.route.routes.first())
|
||||
} else {
|
||||
CarText.Builder("Wait")
|
||||
.build()
|
||||
@@ -110,7 +103,7 @@ class RoutePreviewScreen(
|
||||
val timer = object : CountDownTimer(5000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
onNavigate()
|
||||
onNavigate(0)
|
||||
}
|
||||
}
|
||||
timer.start()
|
||||
@@ -118,18 +111,27 @@ class RoutePreviewScreen(
|
||||
|
||||
val content = if (routeModel.route.routes.size > 1) {
|
||||
ListTemplate.Builder()
|
||||
.setHeader(header)
|
||||
.setHeader(header.build())
|
||||
.setSingleList(itemListBuilder.build())
|
||||
.build()
|
||||
} else {
|
||||
val navigateActionIcon: CarIcon = CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext, R.drawable.navigation_48px
|
||||
)
|
||||
).build()
|
||||
val navigateAction = Action.Builder()
|
||||
.setFlags(FLAG_DEFAULT)
|
||||
.setIcon(navigateActionIcon)
|
||||
.setOnClickListener { this.onNavigate(0) }
|
||||
.build()
|
||||
MessageTemplate.Builder(
|
||||
message
|
||||
)
|
||||
.setHeader(header)
|
||||
.setHeader(header.build())
|
||||
.addAction(navigateAction)
|
||||
.setLoading(message.toString() == "Wait")
|
||||
.build()
|
||||
|
||||
}
|
||||
return MapWithContentTemplate.Builder()
|
||||
.setContentTemplate(content)
|
||||
@@ -192,10 +194,10 @@ class RoutePreviewScreen(
|
||||
)
|
||||
.build()
|
||||
|
||||
private fun createRouteText(index: Int): CarText {
|
||||
val time = routeModel.route.routes[index].summary.duration
|
||||
private fun createRouteText(route: Routes): CarText {
|
||||
val time = route.summary.duration
|
||||
val length =
|
||||
BigDecimal((routeModel.route.routes[index].summary.distance) / 1000).setScale(
|
||||
BigDecimal((route.summary.distance) / 1000).setScale(
|
||||
1,
|
||||
RoundingMode.HALF_EVEN
|
||||
)
|
||||
@@ -207,18 +209,43 @@ class RoutePreviewScreen(
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createRow(index: Int, action: Action): Row {
|
||||
val route = createRouteText(index)
|
||||
val titleText = "$index"
|
||||
return Row.Builder()
|
||||
.setTitle(route)
|
||||
.setOnClickListener { onRouteSelected(index) }
|
||||
.addText(titleText)
|
||||
.addAction(action)
|
||||
private fun createRow(route: Routes, index: Int): Row {
|
||||
val navigateActionIcon: CarIcon = CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext, R.drawable.navigation_48px
|
||||
)
|
||||
).build()
|
||||
val navigateAction = Action.Builder()
|
||||
.setFlags(FLAG_DEFAULT)
|
||||
.setIcon(navigateActionIcon)
|
||||
.setOnClickListener { this.onNavigate(index) }
|
||||
.build()
|
||||
|
||||
val routeText = createRouteText(route)
|
||||
var street = ""
|
||||
var maxDistance = 0.0
|
||||
routeModel.route.routes[index].legs.first().steps.forEach {
|
||||
if (it.distance > maxDistance) {
|
||||
maxDistance = it.distance
|
||||
street = it.street
|
||||
}
|
||||
}
|
||||
val delay = (route.summary.trafficDelay / 60).toInt().toString()
|
||||
|
||||
val row = Row.Builder()
|
||||
.setTitle(routeText)
|
||||
.setOnClickListener { onRouteSelected(index) }
|
||||
.addText(street)
|
||||
.addAction(navigateAction)
|
||||
|
||||
if (route.summary.trafficDelay > 60) {
|
||||
row.addText("$delay min")
|
||||
}
|
||||
return row.build()
|
||||
}
|
||||
|
||||
private fun onNavigate() {
|
||||
private fun onNavigate(index: Int) {
|
||||
destination.routeIndex = index
|
||||
setResult(destination)
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.ViewStyle
|
||||
import com.kouros.navigation.data.Category
|
||||
import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.Constants.CATEGORIES
|
||||
import com.kouros.navigation.data.Constants.FAVORITES
|
||||
import com.kouros.navigation.data.Constants.RECENT
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.nominatim.SearchResult
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
@@ -30,11 +32,13 @@ class SearchScreen(
|
||||
|
||||
var isSearchComplete: Boolean = false
|
||||
|
||||
var category = ""
|
||||
|
||||
var categories: List<Category> = listOf(
|
||||
Category(id = Constants.RECENT, name = carContext.getString(R.string.recent_destinations)),
|
||||
Category(id = RECENT, name = carContext.getString(R.string.recent_destinations)),
|
||||
// Category(id = Constants.CONTACTS, name = carContext.getString(R.string.contacts)),
|
||||
Category(id = Constants.CATEGORIES, name = carContext.getString(R.string.category_title)),
|
||||
Category(id = Constants.FAVORITES, name = carContext.getString(R.string.favorites))
|
||||
Category(id = CATEGORIES, name = carContext.getString(R.string.category_title)),
|
||||
Category(id = FAVORITES, name = carContext.getString(R.string.favorites))
|
||||
)
|
||||
|
||||
lateinit var searchResult: List<SearchResult>
|
||||
@@ -44,8 +48,50 @@ class SearchScreen(
|
||||
invalidate()
|
||||
}
|
||||
|
||||
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
|
||||
if (newPlaces.isNotEmpty()) {
|
||||
screenManager
|
||||
.pushForResult(
|
||||
PlaceListScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
RECENT,
|
||||
navigationViewModel,
|
||||
newPlaces
|
||||
)
|
||||
) { obj: Any? ->
|
||||
surfaceRenderer.clearRouteData()
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val observerFavorites = Observer<List<Place>> { newPlaces ->
|
||||
screenManager
|
||||
.pushForResult(
|
||||
PlaceListScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
FAVORITES,
|
||||
navigationViewModel,
|
||||
newPlaces
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
navigationViewModel.searchPlaces.observe(this, observer)
|
||||
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
||||
navigationViewModel.favorites.observe(this, observerFavorites)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -62,7 +108,7 @@ class SearchScreen(
|
||||
.setTitle(it.name)
|
||||
.setImage(categoryIcon(it.id))
|
||||
.setOnClickListener {
|
||||
if (it.id == Constants.CATEGORIES) {
|
||||
if (it.id == CATEGORIES) {
|
||||
screenManager
|
||||
.pushForResult(
|
||||
CategoriesScreen(
|
||||
@@ -78,19 +124,19 @@ class SearchScreen(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
screenManager
|
||||
.pushForResult(
|
||||
PlaceListScreen(
|
||||
if (it.id == RECENT) {
|
||||
navigationViewModel.loadRecentPlaces(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
it.id,
|
||||
navigationViewModel
|
||||
surfaceRenderer.lastLocation,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
if (it.id == FAVORITES) {
|
||||
navigationViewModel.loadFavorites(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,12 +169,14 @@ class SearchScreen(
|
||||
|
||||
fun categoryIcon(category: String?): CarIcon {
|
||||
val resId: Int = when (category) {
|
||||
Constants.RECENT -> {
|
||||
RECENT -> {
|
||||
R.drawable.ic_place_white_24dp
|
||||
}
|
||||
Constants.FAVORITES -> {
|
||||
|
||||
FAVORITES -> {
|
||||
R.drawable.ic_favorite_white_24dp
|
||||
}
|
||||
|
||||
else -> {
|
||||
R.drawable.navigation_48px
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ data class Place(
|
||||
var street: String? = null,
|
||||
var distance: Float = 0F,
|
||||
//var avatar: Uri? = null,
|
||||
var lastDate: Long = 0
|
||||
var lastDate: Long = 0,
|
||||
var routeIndex: Int = 0
|
||||
)
|
||||
|
||||
data class ContactData(
|
||||
|
||||
@@ -23,12 +23,14 @@ data class Route(
|
||||
val routeEngine: Int,
|
||||
val routes: List<com.kouros.navigation.data.route.Routes>,
|
||||
var currentStepIndex: Int = 0,
|
||||
var routeIndex : Int = 0,
|
||||
) {
|
||||
|
||||
data class Builder(
|
||||
var routeEngine: Int = 0,
|
||||
var summary: Summary = Summary(),
|
||||
var routes: List<com.kouros.navigation.data.route.Routes> = emptyList(),
|
||||
var routeIndex : Int = 0
|
||||
) {
|
||||
|
||||
fun routeType(routeEngine: Int) = apply { this.routeEngine = routeEngine }
|
||||
@@ -38,6 +40,8 @@ data class Route(
|
||||
}
|
||||
fun routeEngine(routeEngine: Int) = apply { this.routeEngine = routeEngine }
|
||||
|
||||
fun routeIndex(routeIndex: Int) = apply { this.routeIndex = routeIndex}
|
||||
|
||||
fun route(route: String) = apply {
|
||||
if (route.isNotEmpty() && route != "[]") {
|
||||
val gson = GsonBuilder().serializeNulls().create()
|
||||
@@ -68,6 +72,7 @@ data class Route(
|
||||
return Route(
|
||||
routeEngine = this.routeEngine,
|
||||
routes = this.routes,
|
||||
routeIndex = this.routeIndex
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,6 +80,7 @@ data class Route(
|
||||
return Route(
|
||||
routeEngine = 0,
|
||||
routes = emptyList(),
|
||||
routeIndex = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -82,7 +88,7 @@ data class Route(
|
||||
|
||||
fun legs(): List<Leg> {
|
||||
return if (routes.isNotEmpty()) {
|
||||
routes.first().legs
|
||||
routes[routeIndex].legs
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ class TomTomRepository : NavigationRepository() {
|
||||
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
|
||||
"&instructionsType=text&language=$language§ionType=lanes" +
|
||||
"&routeRepresentation=encodedPolyline" +
|
||||
"&maxAlternatives=2" +
|
||||
"&vehicleEngineType=combustion$filter&key=$tomtomApiKey"
|
||||
return fetchUrl(
|
||||
url,
|
||||
|
||||
@@ -3,10 +3,8 @@ package com.kouros.navigation.model
|
||||
//import com.kouros.navigation.data.Preferences.boxStore
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -58,7 +56,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
}
|
||||
|
||||
/** LiveData containing list of recent navigation destinations */
|
||||
val places: MutableLiveData<List<Place>> by lazy {
|
||||
val recentPlaces: MutableLiveData<List<Place>> by lazy {
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
@@ -144,11 +142,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
val settingsRepository = getSettingsRepository(context)
|
||||
val rp = settingsRepository.recentPlacesFlow.first()
|
||||
val gson = GsonBuilder().serializeNulls().create()
|
||||
val recentPlaces = gson.fromJson(rp, Places::class.java)
|
||||
val places = gson.fromJson(rp, Places::class.java)
|
||||
val pl = mutableListOf<Place>()
|
||||
var id: Long = 0
|
||||
if (rp.isNotEmpty()) {
|
||||
for (place in recentPlaces.places) {
|
||||
for (place in places.places) {
|
||||
if (place.category.equals(Constants.RECENT)) {
|
||||
val plLocation = location(place.longitude, place.latitude)
|
||||
if (place.latitude != 0.0) {
|
||||
@@ -167,7 +165,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
}
|
||||
}
|
||||
}
|
||||
places.postValue(pl)
|
||||
recentPlaces.postValue(pl)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ open class RouteModel {
|
||||
route = Route.Builder()
|
||||
.routeEngine(navState.routingEngine)
|
||||
.route(routeString)
|
||||
.routeIndex(navState.currentRouteIndex)
|
||||
.build()
|
||||
)
|
||||
if (hasLegs()) {
|
||||
@@ -42,7 +43,7 @@ open class RouteModel {
|
||||
}
|
||||
|
||||
fun hasLegs(): Boolean {
|
||||
return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty()
|
||||
return navState.route.routes.isNotEmpty() && navState.route.routes[navState.currentRouteIndex].legs.isNotEmpty()
|
||||
}
|
||||
|
||||
fun stopNavigation() {
|
||||
|
||||
@@ -9,8 +9,11 @@ import com.kouros.navigation.data.osrm.OsrmRepository
|
||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||
import com.kouros.navigation.data.valhalla.ValhallaRepository
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.lang.Math.toDegrees
|
||||
import java.lang.Math.toRadians
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
@@ -19,6 +22,8 @@ import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.Duration
|
||||
@@ -53,14 +58,35 @@ fun calculateZoom(speed: Double?): Double {
|
||||
return zoom
|
||||
}
|
||||
|
||||
fun previewZoom(previewDistance: Double): Double {
|
||||
when (previewDistance / 1000) {
|
||||
in 0.0..10.0 -> return 13.5
|
||||
in 10.0..20.0 -> return 11.5
|
||||
in 20.0..30.0 -> return 10.5
|
||||
fun previewZoom(centerLocation: Location, previewDistance: Double): Double {
|
||||
return calculateZoomFromBoundingBox(centerLocation, previewDistance / 1000)
|
||||
}
|
||||
return 9.5
|
||||
|
||||
|
||||
fun calculateZoomFromBoundingBox(centerLocation: Location, previewDistance: Double): Double {
|
||||
|
||||
val earthRadius = 6371.0
|
||||
val maxLat = centerLocation.latitude + toDegrees(previewDistance / earthRadius)
|
||||
val minLat = centerLocation.latitude - toDegrees(previewDistance / earthRadius)
|
||||
val maxLon = centerLocation.longitude + toDegrees(previewDistance / earthRadius / cos(toRadians(centerLocation.latitude)))
|
||||
val minLon = centerLocation.longitude - toDegrees(previewDistance / earthRadius / cos(toRadians(centerLocation.latitude)))
|
||||
|
||||
var zoomLevel: Double
|
||||
val latDiff = maxLat - minLat
|
||||
val lngDiff = maxLon - minLon
|
||||
|
||||
val maxDiff = if(lngDiff > latDiff) lngDiff else latDiff
|
||||
|
||||
if (maxDiff < 360 / 2.0.pow(20.0)) {
|
||||
zoomLevel = 21.0
|
||||
} else {
|
||||
zoomLevel = (-1 * ((ln(maxDiff) / ln(2.0)) - (ln(360.0) / ln(2.0))));
|
||||
if (zoomLevel < 1)
|
||||
zoomLevel = 1.0;
|
||||
}
|
||||
return zoomLevel + 1.2
|
||||
}
|
||||
|
||||
|
||||
fun calculateTilt(newZoom: Double, tilt: Double): Double =
|
||||
if (newZoom < 13) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user