App DrawItem and Search

This commit is contained in:
Dimitris
2025-12-09 15:52:27 +01:00
parent f70ca6e8fe
commit aeca6ff237
25 changed files with 868 additions and 654 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 4
versionName = "0.1.3.3"
versionCode = 6
versionName = "0.1.3.6"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -91,6 +91,8 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3.window.size.class1)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.window)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)

View File

@@ -6,6 +6,7 @@ import android.os.SystemClock
class MockLocation (private var locationManager: LocationManager) {
var curSpeed = 0F
fun setMockLocation(latitude: Double, longitude: Double) {
try {
// Set mock location for all providers
@@ -48,7 +49,7 @@ class MockLocation (private var locationManager: LocationManager) {
this.longitude = longitude
this.altitude = 0.0
this.accuracy = 1.0f
this.speed = 0F
this.speed = 0f
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
@@ -56,7 +57,6 @@ class MockLocation (private var locationManager: LocationManager) {
this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f
}
// Set the mock location
locationManager.setTestProviderLocation(provider, mockLocation)
@@ -72,6 +72,7 @@ class MockLocation (private var locationManager: LocationManager) {
this.longitude = longitude
this.altitude = 0.0
this.accuracy = 1.0f
this.speed = 0f
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
@@ -79,7 +80,6 @@ class MockLocation (private var locationManager: LocationManager) {
this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f
}
locationManager.setTestProviderLocation(provider, mockLocation)
} catch (ex: Exception) {
ex.printStackTrace()

View File

@@ -1,10 +1,9 @@
package com.kouros.navigation.ui
import NavigationSheet
import android.Manifest
import android.annotation.SuppressLint
import android.app.AppOpsManager
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.Bundle
import android.os.Process
@@ -12,100 +11,93 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.Card
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import androidx.window.layout.WindowMetricsCalculator
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.MainApplication
import com.kouros.navigation.car.BuildingLayer
import com.kouros.navigation.car.PuckState
import com.kouros.navigation.car.RouteLayer
import com.kouros.navigation.car.map.BuildingLayer
import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.car.map.RouteLayer
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.location
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.LocationTrackingEffect
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() {
private val LOCATION_PERMISSION_REQUEST_CODE = 1001
private val CONTACTS_PERMISSION_REQUEST_CODE = 1002
val routeData = MutableLiveData("")
val vieModel = ViewModel(NavigationRepository())
val viewModel = ViewModel(NavigationRepository())
val routeModel = RouteModel()
var tilt = 50.0
val useMock = true
val instruction: MutableLiveData<StepData> by lazy {
MutableLiveData<StepData>()
}
var lastLocation = Location(LocationManager.GPS_PROVIDER)
var lastLocation = location(0.0, 0.0)
val observer = Observer<String> { newRoute ->
routeModel.startNavigation(newRoute)
routeData.value = routeModel.route.routeGeoJson
println("Start simulating $newRoute")
simulate()
}
val cameraPosition = MutableLiveData(
CameraPosition(
zoom = 15.0,
@@ -113,153 +105,93 @@ class MainActivity : ComponentActivity() {
)
)
private lateinit var locationManager: LocationManager
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var mock: MockLocation
private var loadRecentPlaces = false
init {
vieModel.route.observe(this, observer)
viewModel.route.observe(this, observer)
}
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkLocationPermissions()
if (MainApplication.useContacts) {
checkContactsPermissions()
if (useMock) {
checkMockLocationEnabled()
}
// checkMockLocationEnabled()
enableEdgeToEdge()
setContent {
if ((checkPermissionForLocation() && !MainApplication.useContacts)
|| (checkPermissionForLocation() && MainApplication.useContacts && checkPermissionForContact())) {
Content()
} else {
}
}
}
@Composable
fun Content() {
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
mock = MockLocation(locationManager)
mock.setMockLocation(
Constants.homeLocation.latitude,
Constants.homeLocation.longitude
)
if (useMock) {
mock = MockLocation(locationManager)
mock.setMockLocation(
Constants.homeLocation.latitude,
Constants.homeLocation.longitude
)
}
enableEdgeToEdge()
setContent {
CheckPermissionScreen()
}
}
val scope = rememberCoroutineScope()
@SuppressLint("AutoboxingStateCreation")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Content() {
val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
var simulationText by remember { mutableStateOf("Start Simulation") }
val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds,
desiredAccuracy = DesiredAccuracy.Highest
)
val userLocationState = rememberUserLocationState(locationProvider)
val locationState = locationProvider.location.collectAsState()
updateLocation(locationState.value)
var latitude by remember { mutableDoubleStateOf(0.0) }
if (locationState.value != null) {
latitude = locationState.value!!.position.latitude
}
val step: StepData? by instruction.observeAsState()
NavigationTheme {
ModalNavigationDrawer(
drawerContent = {
ModalDrawerSheet {
Text("Drawer title", modifier = Modifier.Companion.padding(16.dp))
HorizontalDivider()
NavigationDrawerItem(
label = { Text(text = "Drawer Item") },
selected = false,
onClick = { /*TODO*/ }
)
}
BottomSheetScaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
gesturesEnabled = false
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = {
Text(simulationText)
},
icon = { SegmentedButtonDefaults.Icon(true) },
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Starte Navigation")
}
if (!routeModel.isNavigating()) {
tilt = 60.0
vieModel.loadRoute(
applicationContext,
lastLocation,
Constants.home2Location
)
simulationText = "Stop Simulation"
} else {
tilt = 0.0
routeModel.stopNavigation()
routeData.value = ""
println("stopNavigation")
simulationText = "Start Simulation"
}
}
)
}
) { innerPadding ->
Column(modifier = Modifier.Companion.padding(innerPadding)) {
//CheckPermission()
Map()
}
scaffoldState = scaffoldState,
sheetPeekHeight = 128.dp,
sheetContent = {
SheetContent(latitude, step)
},
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center,
) {
Map(userLocationState, step)
}
}
}
}
@SuppressLint("PermissionLaunchedDuringComposition")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CheckPermission() {
var rationaleState by remember {
mutableStateOf<RationaleState?>(null)
}
@OptIn(ExperimentalPermissionsApi::class)
val fineLocationPermissionState = rememberMultiplePermissionsState(
listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
//Manifest.permission.READ_CONTACTS,
),
)
if (fineLocationPermissionState.allPermissionsGranted) {
Map()
}
// Show rationale dialog when needed
rationaleState?.run { PermissionRationaleDialog(rationaleState = this) }
PermissionRequestButton(
isGranted = fineLocationPermissionState.allPermissionsGranted,
title = "Precise location access",
) {
if (fineLocationPermissionState.shouldShowRationale) {
rationaleState = RationaleState(
"Request Precise Location",
"In order to use this feature please grant access by accepting " + "the location permission dialog." + "\n\nWould you like to continue?",
) { proceed ->
if (proceed) {
fineLocationPermissionState.launchMultiplePermissionRequest()
}
rationaleState = null
}
} else {
fineLocationPermissionState.launchMultiplePermissionRequest()
}
fun SheetContent(locationState: Double, step: StepData?) {
if (!routeModel.isNavigating()) {
SearchSheet(applicationContext, viewModel, lastLocation)
} else {
NavigationSheet( routeModel, step, { simulate() })
}
// to recomposite SheetContent !
Text("State $locationState")
}
@Composable
fun NavigationInfo(step: StepData?) {
Card {
@@ -269,7 +201,6 @@ class MainActivity : ComponentActivity() {
contentDescription = stringResource(id = R.string.accept_action_title)
)
if (step != null) {
Text(text = step.bearing.toString(), fontSize = 25.sp)
Text(text = step.instruction, fontSize = 25.sp)
}
}
@@ -277,32 +208,24 @@ class MainActivity : ComponentActivity() {
}
@Composable
fun Map() {
val step: StepData? by instruction.observeAsState()
fun Map(userLocationState: UserLocationState, step: StepData?) {
Column {
//SimpleSearchBar()
if (step != null) {
NavigationInfo(step)
}
MapView()
MapView(userLocationState)
}
}
@Composable
fun MapView() {
val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds,
desiredAccuracy = DesiredAccuracy.Highest
)
val userLocationState = rememberUserLocationState(locationProvider)
val locationState = locationProvider.location.collectAsState()
updateLocation(locationState.value)
if (locationState.value != null && lastLocation.latitude == 0.0) {
lastLocation.latitude = locationState.value?.position!!.latitude
lastLocation.longitude = locationState.value?.position!!.longitude
}
val position: CameraPosition? by cameraPosition.observeAsState()
fun MapView(userLocationState: UserLocationState) {
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
val width = metrics.bounds.width()
val height = metrics.bounds.height()
val paddingValues = PaddingValues(start = 0.dp, top = 350.dp)
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState()
val cameraState =
rememberCameraState(
@@ -315,124 +238,71 @@ class MainActivity : ComponentActivity() {
zoom = 15.0,
)
)
MaplibreMap(
cameraState = cameraState,
baseStyle = BaseStyle.Uri(Constants.STYLE),
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!NavigationUtils.getBooleanKeyValue(
context = applicationContext,
Constants.SHOW_THREED_BUILDING
)
) {
BuildingLayer(tiles)
}
RouteLayer(route, "")
}
val location = Location(LocationManager.GPS_PROVIDER)
if (userLocationState.location != null) {
location.longitude = userLocationState.location!!.position.longitude
location.latitude = userLocationState.location!!.position.latitude
PuckState(cameraState, userLocationState)
}
}
LocationTrackingEffect(
locationState = userLocationState,
) {
cameraState.animateTo(
finalPosition = CameraPosition(
bearing = position!!.bearing,
zoom = position!!.zoom,
target = position!!.target,
tilt = tilt,
padding = PaddingValues(start = 0.dp, top = 350.dp)
Box (contentAlignment = Alignment.Center) {
MaplibreMap(
options = MapOptions(
ornamentOptions =
OrnamentOptions(isScaleBarEnabled = false)
),
duration = 1.seconds
)
cameraState = cameraState,
baseStyle = BaseStyle.Uri(Constants.STYLE),
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!NavigationUtils.getBooleanKeyValue(
context = applicationContext,
Constants.SHOW_THREED_BUILDING
)
) {
BuildingLayer(tiles)
}
RouteLayer(route, "", position!!.zoom)
}
if (userLocationState.location != null) {
///PuckState(cameraState, userLocationState)
}
}
LocationTrackingEffect(
locationState = userLocationState,
) {
cameraState.animateTo(
finalPosition = CameraPosition(
bearing = position!!.bearing,
zoom = position!!.zoom,
target = position!!.target,
tilt = tilt,
padding = paddingValues
),
duration = 1.seconds
)
}
NavigationImage(paddingValues, width, height /6, "")
}
}
fun updateLocation(location: org.maplibre.compose.location.Location?) {
if (1 == 1)
return
if (location != null) {
if (location != null
&& lastLocation.latitude != location.position.latitude
&& lastLocation.longitude != location.position.longitude) {
if (routeModel.isNavigating()) {
routeModel.updateLocation(lastLocation)
instruction.value = routeModel.currentStep()
}
val currentLocation = location(location.position.longitude, location.position.latitude)
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
val zoom = calculateZoom(location.speed)
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = zoom,
target = location.position
target = location.position,
bearing = bearing
),
)
}
}
private fun checkLocationPermissions() {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (MainApplication.useContacts) {
permissions.add(Manifest.permission.READ_CONTACTS)
}
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
LOCATION_PERMISSION_REQUEST_CODE
)
}
}
fun checkPermissionForLocation(): Boolean {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (MainApplication.useContacts) {
permissions.add(Manifest.permission.READ_CONTACTS)
}
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
return permissionsToRequest.isEmpty()
}
fun checkPermissionForContact(): Boolean {
val permissions = arrayOf(
Manifest.permission.READ_CONTACTS,
)
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
return permissionsToRequest.isEmpty()
}
private fun checkContactsPermissions() {
val permissions = arrayOf(
Manifest.permission.READ_CONTACTS,
)
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
CONTACTS_PERMISSION_REQUEST_CODE
)
lastLocation = currentLocation
if (!loadRecentPlaces) {
viewModel.loadRecentPlaces(applicationContext, lastLocation)
loadRecentPlaces = true
}
}
}
@@ -460,18 +330,27 @@ class MainActivity : ComponentActivity() {
}
}
@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 = {
Content()
},
)
}
@OptIn(DelicateCoroutinesApi::class)
fun simulate() = GlobalScope.async {
for ((i, loc) in routeModel.route.waypoints.withIndex()) {
if (routeModel.isNavigating()) {
lastLocation.longitude = loc[0]
lastLocation.latitude = loc[1]
if (i == 20) {
mock.setMockLocation(loc[1] , loc[0])
} else {
mock.setMockLocation(loc[1], loc[0])
}
delay(500L) //
mock.setMockLocation(loc[1], loc[0])
delay(1000L) //
}
}
}

View File

@@ -0,0 +1,53 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel
import kotlinx.coroutines.Deferred
@Composable
fun NavigationSheet(
routeModel: RouteModel,
step: StepData?,
simulate: () -> Unit
) {
Column {
//Text("${routeModel.travelLeftTime()}")
if (step != null)
Text("${step.leftDistance / 1000} km")
HorizontalDivider()
Row() {
if (routeModel.isNavigating()) {
Button(onClick = {
routeModel.stopNavigation()
}) {
Icon(
painter = painterResource(id = R.drawable.ic_close_white_24dp),
"Stop",
modifier = Modifier.size(24.dp, 24.dp),
)
}
Button(onClick = {
simulate()
}) {
Icon(
painter = painterResource(id = R.drawable.assistant_navigation_48px),
"Simulate",
modifier = Modifier.size(24.dp, 24.dp),
)
}
}
}
}
}

View File

@@ -0,0 +1,198 @@
package com.kouros.navigation.ui
import android.content.Intent
import android.provider.Settings
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import androidx.core.net.toUri
/**
* [PermissionScreen] that takes a list of permissions and only calls [onGranted] when
* all the [requiredPermissions] are granted.
*
* By default it assumes that all [permissions] are required.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PermissionScreen(
modifier: Modifier = Modifier,
permissions: List<String>,
requiredPermissions: List<String> = permissions,
description: String? = null,
contentAlignment: Alignment = Alignment.TopStart,
onGranted: @Composable BoxScope.(List<String>) -> Unit,
) {
val context = LocalContext.current
var errorText by remember {
mutableStateOf("")
}
val permissionState = rememberMultiplePermissionsState(permissions = permissions) { map ->
val rejectedPermissions = map.filterValues { !it }.keys
errorText = if (rejectedPermissions.none { it in requiredPermissions }) {
""
} else {
"${rejectedPermissions.joinToString()} required for the sample"
}
}
val allRequiredPermissionsGranted =
permissionState.revokedPermissions.none { it.permission in requiredPermissions }
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = if (allRequiredPermissionsGranted) {
contentAlignment
} else {
Alignment.Center
},
) {
if (allRequiredPermissionsGranted) {
onGranted(
permissionState.permissions
.filter { it.status.isGranted }
.map { it.permission },
)
} else {
PermissionScreen(
permissionState,
description,
errorText,
)
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
data = "package:${context.packageName}".toUri()
}
context.startActivity(intent)
},
) {
Text("App settings")
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun PermissionScreen(
state: MultiplePermissionsState,
description: String?,
errorText: String,
) {
var showRationale by remember(state) {
mutableStateOf(false)
}
val permissions = remember(state.revokedPermissions) {
state.revokedPermissions.joinToString("\n") {
" - " + it.permission.removePrefix("android.permission.")
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Sample requires permission/s:",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp),
)
Text(
text = permissions,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
)
if (description != null) {
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
)
}
Button(
onClick = {
if (state.shouldShowRationale) {
showRationale = true
} else {
state.launchMultiplePermissionRequest()
}
},
) {
Text(text = "Grant permissions")
}
if (errorText.isNotBlank()) {
Text(
text = errorText,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(16.dp),
)
}
}
if (showRationale) {
AlertDialog(
onDismissRequest = {
showRationale = false
},
title = {
Text(text = "Permissions required by the navigation app")
},
text = {
Text(text = "The navigation app requires the following permissions to work:\n $permissions")
},
confirmButton = {
TextButton(
onClick = {
showRationale = false
state.launchMultiplePermissionRequest()
},
) {
Text("Continue")
}
},
dismissButton = {
TextButton(
onClick = {
showRationale = false
},
) {
Text("Dismiss")
}
},
)
}
}

View File

@@ -1,130 +0,0 @@
package com.kouros.navigation.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
/**
* Simple screen that manages the location permission state
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun Permissions(text: String, rationale: String, locationState: PermissionState) {
Permissions(
text = text,
rationale = rationale,
locationState = rememberMultiplePermissionsState(
permissions = listOf(
locationState.permission
)
)
)
}
/**
* Simple screen that manages the location permission state
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun Permissions(text: String, rationale: String, locationState: MultiplePermissionsState) {
var showRationale by remember(locationState) {
mutableStateOf(false)
}
if (showRationale) {
PermissionRationaleDialog(rationaleState = RationaleState(
title = "Permission Access",
rationale = rationale,
onRationaleReply = { proceed ->
if (proceed) {
locationState.launchMultiplePermissionRequest()
}
showRationale = false
}
))
}
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
PermissionRequestButton(isGranted = false, title = text) {
if (locationState.shouldShowRationale) {
showRationale = true
} else {
locationState.launchMultiplePermissionRequest()
}
}
}
}
/**
* A button that shows the title or the request permission action.
*/
@Composable
fun PermissionRequestButton(isGranted: Boolean, title: String, onClick: () -> Unit) {
if (isGranted) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Icon(Icons.Outlined.CheckCircle, title, modifier = Modifier.size(48.dp))
Spacer(Modifier.size(10.dp))
Text(text = title, modifier = Modifier.background(Color.Transparent))
}
} else {
Button(onClick = onClick) {
Text("Request $title")
}
}
}
/**
* Simple AlertDialog that displays the given rationale state
*/
@Composable
fun PermissionRationaleDialog(rationaleState: RationaleState) {
AlertDialog(onDismissRequest = { rationaleState.onRationaleReply(false) }, title = {
Text(text = rationaleState.title)
}, text = {
Text(text = rationaleState.rationale)
}, confirmButton = {
TextButton(onClick = {
rationaleState.onRationaleReply(true)
}) {
Text("Continue")
}
}, dismissButton = {
TextButton(onClick = {
rationaleState.onRationaleReply(false)
}) {
Text("Dismiss")
}
})
}
data class RationaleState(
val title: String,
val rationale: String,
val onRationaleReply: (proceed: Boolean) -> Unit,
)

View File

@@ -1,79 +0,0 @@
package com.kouros.navigation.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SimpleSearchBar(
textFieldState: TextFieldState,
onSearch: (String) -> Unit,
searchResults: List<String>,
modifier: Modifier = Modifier
) {
// Controls expansion state of the search bar
var expanded by rememberSaveable { mutableStateOf(false) }
Box(
modifier
.fillMaxSize()
.semantics { isTraversalGroup = true }
) {
SearchBar(
modifier = Modifier
.align(Alignment.TopCenter)
.semantics { traversalIndex = 0f },
inputField = {
SearchBarDefaults.InputField(
query = textFieldState.text.toString(),
onQueryChange = { textFieldState.edit { replace(0, length, it) } },
onSearch = {
onSearch(textFieldState.text.toString())
expanded = false
},
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text("Search") }
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
) {
// Display search results in a scrollable column
Column(Modifier.verticalScroll(rememberScrollState())) {
searchResults.forEach { result ->
ListItem(
headlineContent = { Text(result) },
modifier = Modifier
.clickable {
textFieldState.edit { replace(0, length, result) }
expanded = false
}
.fillMaxWidth()
)
}
}
}
}
}

View File

@@ -0,0 +1,217 @@
package com.kouros.navigation.ui
import android.content.Context
import android.location.Location
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.unit.dp
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.PlaceColor
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.location
@Composable
fun SearchSheet(applicationContext: Context, viewModel: ViewModel, location: Location) {
val searchResults = mutableListOf<SearchResult>()
val recentPlaces = viewModel.places.observeAsState()
val search = viewModel.searchPlaces.observeAsState()
if (search.value != null) {
searchResults.addAll(search.value!!)
}
if (searchResults.isNotEmpty()) {
val textFieldState = rememberTextFieldState()
val items = listOf(searchResults)
if (items.isNotEmpty()) {
SearchBar(
textFieldState = textFieldState,
searchPlaces = recentPlaces.value!!,
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location
)
}
}
if (recentPlaces.value != null) {
val textFieldState = rememberTextFieldState()
val items = listOf(recentPlaces)
if (items.isNotEmpty()) {
SearchBar(
textFieldState = textFieldState,
searchPlaces = recentPlaces.value!!,
searchResults = searchResults,
viewModel = viewModel,
context = applicationContext,
location = location
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
textFieldState: TextFieldState,
searchPlaces: List<Place>,
searchResults: List<SearchResult>,
modifier: Modifier = Modifier,
viewModel: ViewModel,
context: Context,
location: Location,
) {
var expanded by rememberSaveable { mutableStateOf(true) }
Box(
modifier
.fillMaxSize()
.semantics { isTraversalGroup = true }
) {
SearchBar(
modifier = Modifier
.align(Alignment.TopCenter)
.semantics { traversalIndex = 0f },
inputField = {
SearchBarDefaults.InputField(
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_search_black36dp),
"Search",
modifier = Modifier.size(24.dp, 24.dp),
)
},
query = textFieldState.text.toString(),
onQueryChange = { textFieldState.edit { replace(0, length, it) } },
onSearch = {
searchPlaces(viewModel, location, it)
expanded = false
},
expanded = expanded,
onExpandedChange = { expanded = it },
placeholder = { Text("Suchen") }
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
) {
if (searchPlaces.isNotEmpty()) {
Text("Recent places")
RecentPlaces(searchPlaces, viewModel, context, location)
}
if (searchResults.isNotEmpty()) {
Text("Search places")
SearchPlaces(searchResults, viewModel, context, location)
}
}
}
}
private fun searchPlaces(viewModel: ViewModel, location: Location, it: String) {
viewModel.searchPlaces(it, location)
}
@Composable
private fun SearchPlaces(
searchResults: List<SearchResult>,
viewModel: ViewModel,
context: Context,
location: Location,
) {
val color = remember { PlaceColor }
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (searchResults.isNotEmpty()) {
items(searchResults, key = { it.placeId }) { place ->
Row {
Icon(
painter = painterResource(id = R.drawable.ic_place_white_24dp),
"Navigation",
tint = color.copy(alpha = 1f),
modifier = Modifier.size(24.dp, 24.dp),
)
ListItem(
headlineContent = { Text("${place.address.road} ${place.address.postcode}") },
modifier = Modifier
.clickable {
val toLocation =
location(place.lon.toDouble(), place.lat.toDouble())
viewModel.loadRoute(context, location, toLocation)
}
.fillMaxWidth()
)
HorizontalDivider(color = Color.Gray)
}
}
}
}
}
@Composable
private fun RecentPlaces(
recentPlaces: List<Place>,
viewModel: ViewModel,
context: Context,
location: Location
) {
val color = remember { PlaceColor }
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.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}") },
modifier = Modifier
.clickable {
val toLocation = location(place.longitude, place.latitude)
viewModel.loadRoute(context, location, toLocation)
}
.fillMaxWidth()
)
HorizontalDivider(color = Color.Gray)
}
}
}
}

View File

@@ -40,7 +40,7 @@ fun NavigationTheme(
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
dynamicColor -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}

View File

@@ -151,8 +151,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
updateLocation(location)
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
/* minTimeMs= */ 500,
/* minDistanceM= */ 0f,
/* minTimeMs= */ 1000,
/* minDistanceM= */ 5f,
mLocationListener
)
}

View File

@@ -18,6 +18,8 @@ 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
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
@@ -25,6 +27,11 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.kouros.navigation.car.map.BuildingLayer
import com.kouros.navigation.car.map.DrawImage
import com.kouros.navigation.car.map.RouteLayer
import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
@@ -33,9 +40,12 @@ import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.previewZoom
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.sources.getBaseSource
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
@@ -77,7 +87,7 @@ class SurfaceRenderer(
lateinit var mapView: ComposeView
var panView = false
val tilt = 55.0
var tilt = 55.0
var previewDistance = 0.0
val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback {
@@ -164,11 +174,12 @@ class SurfaceRenderer(
}
fun onConnectionStateUpdated(connectionState: Int) {
when(connectionState) {
when (connectionState) {
CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext)
}
}
@Composable
fun MapView() {
val stateWidth = visibleArea.observeAsState()
@@ -178,21 +189,29 @@ class SurfaceRenderer(
val paddingValues = getPaddingValues(width - stateWidth.value!!.width(), height, preview)
val cameraState = cameraState(paddingValues, position, tilt)
val baseStyle =if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
Constants.STYLE
)
MaplibreMap(
cameraState = cameraState,
baseStyle = baseStyle,
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!getBooleanKeyValue(context = carContext, SHOW_THREED_BUILDING)) {
BuildingLayer(tiles)
}
RouteLayer(route, previewRoute)
}
//Puck(cameraState, lastLocation)
val baseStyle = remember {
mutableStateOf(BaseStyle.Uri(Constants.STYLE))
}
baseStyle.value =
(if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
Constants.STYLE
))
MaplibreMap(
options = MapOptions(
ornamentOptions =
OrnamentOptions(isScaleBarEnabled = false)),
cameraState = cameraState,
baseStyle = baseStyle.value
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!getBooleanKeyValue(context = carContext, SHOW_THREED_BUILDING)) {
BuildingLayer(tiles)
}
RouteLayer(route, previewRoute, position!!.zoom)
}
//Puck(cameraState, lastLocation)
}
ShowPosition(cameraState, position, paddingValues)
}
@@ -215,7 +234,7 @@ class SurfaceRenderer(
}
} else {
bearing = 0.0
zoom = previewZoom()
zoom = previewZoom(previewDistance)
target = Position(centerLocation.longitude, centerLocation.latitude)
localTilt = 0.0
}
@@ -261,11 +280,19 @@ class SurfaceRenderer(
} else {
cameraPosition.value!!.zoom + 1.0
}
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = newZoom,
target = cameraPosition.value!!.target
)
tilt = if (newZoom < 13) {
0.0
} else {
if (tilt == 0.0) {
55.0
} else {
tilt
}
}
updateCameraPosition(
cameraPosition.value!!.bearing,
newZoom,
cameraPosition.value!!.target,
)
}
}
@@ -273,7 +300,7 @@ class SurfaceRenderer(
fun updateLocation(location: Location) {
synchronized(this) {
if (!preview) {
val bearing = bearing(lastLocation, location)
val bearing = bearing(lastLocation, location, cameraPosition.value!!.bearing)
val zoom = if (!panView) {
calculateZoom(location.speed.toDouble())
} else {
@@ -287,10 +314,9 @@ class SurfaceRenderer(
lastBearing = cameraPosition.value!!.bearing
lastLocation = location
} else {
val zoom = previewZoom()
updateCameraPosition(
0.0,
zoom,
previewZoom(previewDistance),
Position(centerLocation.longitude, centerLocation.latitude)
)
}
@@ -302,7 +328,7 @@ class SurfaceRenderer(
cameraPosition.value!!.copy(
bearing = bearing,
zoom = zoom,
tilt = 0.0,
tilt = tilt,
padding = getPaddingValues(width - visibleArea.value!!.width(), height, preview),
target = target
)
@@ -323,28 +349,6 @@ class SurfaceRenderer(
previewDistance = routeModel.route.distance
}
private fun previewZoom(): Double {
when (previewDistance) {
in 0.0..10.0 -> {
return 13.0
}
in 10.0..20.0 -> {
return 11.0
}
in 20.0..30.0 -> {
return 10.0
}
}
return 9.0
}
fun setPreViewDistance(): Double {
return previewDistance
}
companion
object {
private const val TAG = "MapRenderer"

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation.car
package com.kouros.navigation.car.map
import android.location.Location
import androidx.compose.runtime.Composable

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation.car
package com.kouros.navigation.car.map
import android.location.Location
import androidx.compose.foundation.Canvas
@@ -17,9 +17,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
@@ -67,7 +66,8 @@ fun cameraState(
}
@Composable
fun RouteLayer(routeData: String?, previewRoute: String?) {
fun RouteLayer(routeData: String?, previewRoute: String?, zoom: Double) {
val width = zoom - 2
if (routeData!!.isNotEmpty()) {
val routes =
rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
@@ -75,13 +75,13 @@ fun RouteLayer(routeData: String?, previewRoute: String?) {
id = "routes-casing",
source = routes,
color = const(Color.White),
width = const(16.dp),
width = const((width+2).dp),
)
LineLayer(
id = "routes",
source = routes,
color = const(RouteColor),
width = const(14.dp),
width = const(width.dp),
)
}
if (previewRoute!!.isNotEmpty()) {
@@ -122,10 +122,7 @@ fun DrawImage(padding: PaddingValues, location: Location, width: Int, height: In
@Composable
fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: String) {
val imageSize = (height/6)
println("Image Size: $imageSize")
val vector = ImageVector.vectorResource(id = R.drawable.assistant_navigation_48px)
val color = remember { NavigationColor }
BadgedBox(
modifier = Modifier
@@ -134,19 +131,18 @@ fun NavigationImage(padding: PaddingValues, width: Int, height: Int, street: Str
Badge()
}
) {
Canvas(modifier =Modifier
.size(imageSize.dp, imageSize.dp)) {
scale(scaleX = 1f, scaleY = 0.7f) {
drawCircle(Color.DarkGray.copy(alpha = 0.2f))
}
}
Icon(
painter = painterResource(id = R.drawable.navigation),
"Navigation",
tint = color,
tint = color.copy(alpha = 1f),
modifier = Modifier.size(imageSize.dp, imageSize.dp),
)
// Icon(
// modifier = Modifier.size(72.dp, 72.dp),
// imageVector = vector,
// contentDescription = "Navigation",
// tint = color
// )
if (street.isNotEmpty())
Text(text = street)
}

View File

@@ -81,7 +81,7 @@ class RouteCarModel() : RouteModel() {
val maneuverType = maneuver.type
val routing = routingData(maneuverType, carContext)
var text = ""
val distanceLeft = leftStepDistance() * 1000
val distanceLeft = leftStepDistance()
when (distanceLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
@@ -178,7 +178,7 @@ class RouteCarModel() : RouteModel() {
|| maneuverType == ManeuverType.DestinationLeft.value
}
fun travelEstimate(carContext: CarContext): TravelEstimate {
fun travelEstimate(): TravelEstimate {
val timeLeft = travelLeftTime()
// Calculate the time to destination from the current time.
val nowUtcMillis = System.currentTimeMillis()

View File

@@ -29,6 +29,7 @@ import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.car.NavigationCarAppService
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel
@@ -102,7 +103,7 @@ class NavigationScreen(
.setNavigationInfo(
getRoutingInfo()
)
.setDestinationTravelEstimate(routeModel.travelEstimate(carContext))
.setDestinationTravelEstimate(routeModel.travelEstimate())
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.setBackgroundColor(CarColor.GREEN)
@@ -182,27 +183,11 @@ class NavigationScreen(
return builder.build()
}
fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder()
.setNavigationInfo(
MessageInfo.Builder(
carContext.getString(R.string.new_route)
)
.setText(routeModel.destination.street.toString())
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.navigation_48px
)
)
.build()
)
.build()
)
.setBackgroundColor(CarColor.SECONDARY)
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.setBackgroundColor(CarColor.GREEN)
.build()
}
@@ -447,8 +432,12 @@ class NavigationScreen(
}
fun updateTrip(location: Location) {
val start = System.currentTimeMillis()
routeModel.updateLocation(location)
if (routeModel.maneuverType == Maneuver.TYPE_DESTINATION && routeModel.leftStepDistance() * 1000 < 25.0) {
val end = System.currentTimeMillis()
println("Time ${end-start}")
if (routeModel.maneuverType == Maneuver.TYPE_DESTINATION
&& routeModel.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) {
routeModel.arrived = true
routeModel.stopNavigation()
}

View File

@@ -2,11 +2,8 @@ package com.kouros.navigation.car.screen
import android.location.Location
import android.net.Uri
import android.os.Build
import android.text.Spannable
import android.text.SpannableString
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
@@ -25,7 +22,6 @@ import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.ViewModel
@@ -66,7 +62,7 @@ class PlaceListScreen(
fun loadPlaces() {
if (category == Constants.RECENT) {
viewModel.loadPlaces(carContext, location)
viewModel.loadRecentPlaces(carContext, location)
}
if (category == Constants.CONTACTS) {
viewModel.loadContacts(carContext, location)

View File

@@ -2,14 +2,17 @@ package com.kouros.navigation.car.screen
import android.annotation.SuppressLint
import android.location.Location
import android.net.Uri
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon
import androidx.car.app.model.ItemList
import androidx.car.app.model.Row
import androidx.car.app.model.SearchTemplate
import androidx.car.app.model.SearchTemplate.SearchCallback
import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.car.SurfaceRenderer
@@ -61,6 +64,7 @@ class SearchScreen(
itemListBuilder.addItem(
Row.Builder()
.setTitle(it.name)
.setImage(categoryIcon(it.id))
.setOnClickListener {
screenManager
.pushForResult(
@@ -104,6 +108,25 @@ class SearchScreen(
.build()
}
fun categoryIcon(category: String?): CarIcon {
val resId : Int = when (category) {
Constants.RECENT -> {
R.drawable.ic_place_white_24dp
}
Constants.FAVORITES -> {
R.drawable.ic_favorite_white_24dp
}
else -> {
R.drawable.navigation
}
}
return CarIcon.Builder(
IconCompat.createWithResource(
carContext, resId
)
).build()
}
@SuppressLint("DefaultLocale")
fun doSearch(searchItemListBuilder: ItemList.Builder) {
searchResult.forEach {

View File

@@ -2,8 +2,10 @@ package com.kouros.navigation.data
import androidx.compose.ui.graphics.Color
val NavigationColor = Color(0xFF368605)
val NavigationColor = Color(0xFF052186)
val RouteColor = Color(0xFF5582D0)
val SpeedColor = Color(0xFF262525)
val SpeedColor = Color(0xFF262525)
val PlaceColor = Color(0xFF868005)

View File

@@ -62,7 +62,6 @@ data class ContactData(
data class StepData (
var instruction: String,
var leftDistance: Double,
var bearing: Double
)
@@ -138,8 +137,6 @@ object Constants {
const val STYLE: String = "https://kouros-online.de/liberty.json"
const val STYLE_DARK: String = "https://kouros-online.de/liberty_night.json"
//const val STYLE: String = "https://tiles.openfreemap.org/styles/liberty"
const val TAG: String = "Navigation"
const val CONTACTS: String = "Contacts"
@@ -175,6 +172,8 @@ object Constants {
const val MAXIMAL_ROUTE_DEVIATION = 100.0
const val DESTINATION_ARRIVAL_DISTANCE = 20.0
}

View File

@@ -39,11 +39,11 @@ class NavigationRepository {
// motorway, trunk, primary, secondary, tertiary, unclassified, residential, service_other.
// exclude_toll
fun getRoute(currentLocation: Location, location: Location, SearchFilter: SearchFilter): String {
fun getRoute(currentLocation: Location, location: Location, searchFilter: SearchFilter): String {
SearchFilter
val vLocation = listOf(
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = SearchFilter),
Locations(lat = location.latitude, lon = location.longitude, search_filter = SearchFilter)
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = searchFilter),
Locations(lat = location.latitude, lon = location.longitude, search_filter = searchFilter)
)
val valhallaLocation = ValhallaLocation(
locations = vLocation,

View File

@@ -47,9 +47,9 @@
"source": "openmaptiles",
"source-layer": "park",
"paint": {
"fill-color": "#d8e8c8",
"fill-color": "rgba(78, 100, 56, 1)",
"fill-opacity": 0.7,
"fill-outline-color": "rgba(95, 208, 100, 1)"
"fill-outline-color": "rgba(39, 78, 40, 1)"
}
},
{
@@ -101,7 +101,7 @@
"filter": ["==", ["get", "class"], "grass"],
"paint": {
"fill-antialias": false,
"fill-color": "rgba(176, 213, 154, 1)",
"fill-color": "rgba(21, 32, 15, 1)",
"fill-opacity": 0.3
}
},
@@ -153,7 +153,7 @@
"source": "openmaptiles",
"source-layer": "landuse",
"filter": ["==", ["get", "class"], "cemetery"],
"paint": {"fill-color": "hsl(75,37%,81%)"}
"paint": {"fill-color": "rgba(65, 74, 92, 1)"}
},
{
"id": "landuse_hospital",
@@ -169,7 +169,7 @@
"source": "openmaptiles",
"source-layer": "landuse",
"filter": ["==", ["get", "class"], "school"],
"paint": {"fill-color": "rgb(236,238,204)"}
"paint": {"fill-color": "rgba(65, 74, 92, 1)"}
},
{
"id": "waterway_tunnel",
@@ -905,7 +905,6 @@
],
"layout": {"line-cap": "round", "line-join": "round"},
"paint": {
"line-color": "#cfcdca",
"line-width": [
"interpolate",
["exponential", 1.2],
@@ -916,7 +915,8 @@
4,
20,
11
]
],
"line-color": "rgba(65, 74, 92, 1)"
}
},
{
@@ -975,7 +975,6 @@
],
"layout": {"line-cap": "round", "line-join": "round"},
"paint": {
"line-color": "#cfcdca",
"line-opacity": ["interpolate", ["linear"], ["zoom"], 12, 0, 12.5, 1],
"line-width": [
"interpolate",
@@ -989,7 +988,8 @@
4,
20,
20
]
],
"line-color": "rgba(65, 74, 92, 1)"
}
},
{
@@ -1095,7 +1095,7 @@
],
"layout": {"line-join": "round"},
"paint": {
"line-color": "hsl(0,0%,100%)",
"line-color": "rgba(65, 74, 92, 1)",
"line-dasharray": [1, 0.7],
"line-width": [
"interpolate",
@@ -1219,7 +1219,6 @@
],
"layout": {"line-cap": "round", "line-join": "round"},
"paint": {
"line-color": "rgba(195, 190, 190, 1)",
"line-width": [
"interpolate",
["exponential", 1.2],
@@ -1230,7 +1229,8 @@
2.5,
20,
18
]
],
"line-color": "rgba(65, 74, 92, 1)"
}
},
{

View File

@@ -30,8 +30,6 @@ open class RouteModel() {
var distanceToStepEnd = 0F
var bearing = 0F
var beginIndex = 0
var endIndex = 0
@@ -57,7 +55,7 @@ open class RouteModel() {
val currentDistance: Double
/** Returns the current [Step] with information such as the cue text and images. */
get() {
return ((leftStepDistance() * 1000).roundToInt().toDouble() / 10.0).roundToInt() * 10.0
return ((leftStepDistance()).roundToInt().toDouble() / 10.0).roundToInt() * 10.0
}
fun updateLocation(location: Location) {
@@ -86,14 +84,7 @@ open class RouteModel() {
route.pointLocations[currentShapeIndex].longitude(),
route.pointLocations[currentShapeIndex].latitude()
)
// if (currentShapeIndex < route.pointLocations.size) {
// val nextLocation = location(
// route.pointLocations[currentShapeIndex + 1].latitude(),
// route.pointLocations[currentShapeIndex + 1].longitude()
// )
// bearing = curLocation.bearingTo(nextLocation)
// }
val distanceStepLeft = leftStepDistance() * 1000
val distanceStepLeft = leftStepDistance()
when (distanceStepLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
if (route.currentManeuverIndex < route.maneuvers.size) {
@@ -104,7 +95,7 @@ open class RouteModel() {
}
}
}
return StepData(text, distanceStepLeft, bearing.toDouble())
return StepData(text, distanceStepLeft)
}
/** Calculates the index in a maneuver. */
@@ -174,16 +165,17 @@ open class RouteModel() {
return timeLeft
}
/** Returns the current [Step] left distance in km. */
/** Returns the current [Step] left distance in m. */
fun leftStepDistance(): Double {
val maneuver = route.currentManeuver()
var leftDistance = maneuver.length
if (endIndex > 0) {
leftDistance = (distanceToStepEnd / 1000).toDouble()
leftDistance = distanceToStepEnd.toDouble()
}
return leftDistance
return leftDistance * 1000
}
/** Returns the left distance in km. */
fun travelLeftDistance(): Double {
var leftDistance = 0.0
for (i in route.currentManeuverIndex + 1..<route.maneuvers.size) {

View File

@@ -3,11 +3,14 @@ package com.kouros.navigation.model
import android.content.Context
import android.location.Geocoder
import android.location.Location
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.gson.GsonBuilder
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Locations
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.ObjectBox.boxStore
import com.kouros.navigation.data.Place
@@ -64,20 +67,21 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val results = query.find()
query.close()
for (place in results) {
val plLocation = location(place.longitude,place.latitude)
// val distance = repository.getRouteDistance(location, plLocation)
val plLocation = location(place.longitude, place.latitude)
//val distance = repository.getRouteDistance(location, plLocation, SearchFilter())
//place.distance = distance.toFloat()
if (place.distance == 0F) {
recentPlace.postValue(place)
return@launch
}
//if (place.distance == 0F) {
recentPlace.postValue(place)
return@launch
//}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun loadPlaces(context: Context, location: Location) {
fun loadRecentPlaces(context: Context, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
try {
val placeBox = boxStore.boxFor(Place::class)
@@ -89,7 +93,8 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
query.close()
for (place in results) {
val plLocation = location(place.longitude, place.latitude)
val distance = repository.getRouteDistance(location, plLocation, getSearchFilter(context))
val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context))
place.distance = distance.toFloat()
}
places.postValue(results)
@@ -110,8 +115,9 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
val results = query.find()
query.close()
for (place in results) {
val plLocation = location(place.longitude, place.latitude )
val distance = repository.getRouteDistance(location, plLocation, getSearchFilter(context))
val plLocation = location(place.longitude, place.latitude)
val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context))
place.distance = distance.toFloat()
}
favorites.postValue(results)
@@ -124,7 +130,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun loadRoute(context: Context, currentLocation: Location, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
try {
route.postValue(repository.getRoute(currentLocation, location, getSearchFilter(context)))
route.postValue(
repository.getRoute(
currentLocation,
location,
getSearchFilter(context)
)
)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -134,7 +146,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun loadPreviewRoute(context: Context, currentLocation: Location, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
try {
previewRoute.postValue(repository.getRoute(currentLocation, location, getSearchFilter(context)))
previewRoute.postValue(
repository.getRoute(
currentLocation,
location,
getSearchFilter(context)
)
)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -155,9 +173,13 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
) {
for (adr in it) {
if (addressLines.size > 1) {
val plLocation = location( adr.longitude, adr.latitude)
val plLocation = location(adr.longitude, adr.latitude)
val distance =
repository.getRouteDistance(currentLocation, plLocation, getSearchFilter(context))
repository.getRouteDistance(
currentLocation,
plLocation,
getSearchFilter(context)
)
contactList.add(
Place(
id = address.contactId,
@@ -201,7 +223,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun reverseAddress(location: Location ): String {
fun reverseAddress(location: Location): String {
val address = repository.reverseAddress(location)
val gson = GsonBuilder().serializeNulls().create()
val place = gson.fromJson(address, SearchResult::class.java)
@@ -213,16 +235,20 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
place.category = Constants.FAVORITES
savePlace(place)
}
fun saveRecent(place: Place) {
place.category = Constants.RECENT
savePlace(place)
}
fun savePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) {
try {
val placeBox = boxStore.boxFor(Place::class)
val query = placeBox
.query(Place_.name.equal(place.name!!).and(Place_.category.equal(place.category!!)))
.query(
Place_.name.equal(place.name!!).and(Place_.category.equal(place.category!!))
)
.build()
val results = query.find()
query.close()
@@ -247,12 +273,15 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
place.category = Constants.RECENT
deletePlace(place)
}
fun deletePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) {
try {
val placeBox = boxStore.boxFor(Place::class)
val query = placeBox
.query(Place_.name.equal(place.name!!).and(Place_.category.equal(place.category!!)))
.query(
Place_.name.equal(place.name!!).and(Place_.category.equal(place.category!!))
)
.build()
val results = query.find()
query.close()
@@ -267,11 +296,11 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun getSearchFilter(context: Context): SearchFilter {
val avoidMotorway = NavigationUtils.getBooleanKeyValue(
val avoidMotorway = NavigationUtils.getBooleanKeyValue(
context = context,
Constants.AVOID_MOTORWAY
)
val avoidTollway = NavigationUtils.getBooleanKeyValue(
val avoidTollway = NavigationUtils.getBooleanKeyValue(
context = context,
Constants.AVOID_TOLLWAY
)
@@ -280,5 +309,28 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
.avoidTollway(avoidTollway)
.build()
}
}
fun loadPlaces2(context: Context, location: Location): SnapshotStateList<Place?> {
val results = listOf<Place>()
try {
val placeBox = boxStore.boxFor(Place::class)
val query = placeBox
.query(Place_.name.notEqual("").and(Place_.category.equal(Constants.RECENT)))
.orderDesc(Place_.lastDate)
.build()
val results = query.find()
query.close()
for (place in results) {
val plLocation = location(place.longitude, place.latitude)
val distance =
repository.getRouteDistance(location, plLocation, getSearchFilter(context))
place.distance = distance.toFloat()
}
} catch (e: Exception) {
e.printStackTrace()
}
return results.toMutableStateList()
}
}

View File

@@ -11,7 +11,6 @@ import com.kouros.navigation.data.GeoJsonLineString
import kotlinx.serialization.json.Json
import org.maplibre.geojson.Point
import org.maplibre.turf.TurfMisc
import org.maplibre.turf.TurfMeasurement
import java.lang.Math.toDegrees
import java.lang.Math.toRadians
import kotlin.math.asin
@@ -151,20 +150,38 @@ object NavigationUtils {
fun calculateZoom(speed: Double?): Double {
if (speed == null) {
return 18.0
return 17.0
}
val zoom = when (speed.toInt()) {
val speedKmh = (speed * 3.6).toInt()
val zoom = when (speedKmh) {
in 0..10 -> 17.0
in 11..20 -> 17.0
in 21..30 -> 17.0
in 31..40 -> 16.0
in 41..50 -> 15.0
in 51..60 -> 14.0
else -> 14
in 11..30 -> 16.0
in 31..50 -> 16.0
in 51..60 -> 15.0
else -> 15
}
return zoom.toDouble()
}
fun bearing(fromLocation: Location, toLocation: Location ) : Double {
fun previewZoom(previewDistance: Double): Double {
when (previewDistance) {
in 0.0..10.0 -> {
return 13.0
}
in 10.0..20.0 -> {
return 11.0
}
in 20.0..30.0 -> {
return 10.0
}
}
return 9.0
}
fun bearing(fromLocation: Location, toLocation: Location, oldBearing: Double) : Double {
val distance = fromLocation.distanceTo(toLocation)
if (distance < 1.0) {
return oldBearing
}
val bearing = fromLocation.bearingTo(toLocation).toInt().toDouble()
return bearing
}

View File

@@ -35,6 +35,8 @@ navigationCompose = "2.9.6"
uiToolingPreview = "1.9.5"
uiTooling = "1.9.5"
material3WindowSizeClass = "1.4.0"
uiGraphics = "1.10.0"
window = "1.5.1"
[libraries]
@@ -73,6 +75,8 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
androidx-compose-material3-window-size-class1 = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3WindowSizeClass" }
androidx-app-automotive = { module = "androidx.car.app:app-automotive", version.ref = "androidx-car" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" }
androidx-window = { group = "androidx.window", name = "window", version.ref = "window" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }