diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 778fbe2..26a9e67 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/kouros/navigation/model/MockLocation.kt b/app/src/main/java/com/kouros/navigation/model/MockLocation.kt index 5c54fcd..fcc4724 100644 --- a/app/src/main/java/com/kouros/navigation/model/MockLocation.kt +++ b/app/src/main/java/com/kouros/navigation/model/MockLocation.kt @@ -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() diff --git a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt index b46edf9..849406d 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -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 by lazy { MutableLiveData() } - var lastLocation = Location(LocationManager.GPS_PROVIDER) + var lastLocation = location(0.0, 0.0) val observer = Observer { 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(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) // } } } diff --git a/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt b/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt new file mode 100755 index 0000000..2df7018 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt @@ -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), + ) + } + } + } + } +} + + diff --git a/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt b/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt new file mode 100644 index 0000000..2ca8709 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/ui/PermissionScreen.kt @@ -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, + requiredPermissions: List = permissions, + description: String? = null, + contentAlignment: Alignment = Alignment.TopStart, + onGranted: @Composable BoxScope.(List) -> 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") + } + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/ui/Permissions.kt b/app/src/main/java/com/kouros/navigation/ui/Permissions.kt deleted file mode 100644 index 705234f..0000000 --- a/app/src/main/java/com/kouros/navigation/ui/Permissions.kt +++ /dev/null @@ -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, -) \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/ui/SearchBar.kt b/app/src/main/java/com/kouros/navigation/ui/SearchBar.kt deleted file mode 100644 index f73d188..0000000 --- a/app/src/main/java/com/kouros/navigation/ui/SearchBar.kt +++ /dev/null @@ -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, - 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() - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt b/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt new file mode 100644 index 0000000..d80ef23 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt @@ -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() + 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, + searchResults: List, + 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, + 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, + 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) + } + } + } +} diff --git a/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt b/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt index 2653667..662a949 100644 --- a/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt +++ b/app/src/main/java/com/kouros/navigation/ui/theme/Theme.kt @@ -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) } diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt index 72f0857..9af282a 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt @@ -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 ) } diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index c5c7f9d..9cb9482 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -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" diff --git a/common/car/src/main/java/com/kouros/navigation/car/LocationPuck.kt b/common/car/src/main/java/com/kouros/navigation/car/map/LocationPuck.kt similarity index 99% rename from common/car/src/main/java/com/kouros/navigation/car/LocationPuck.kt rename to common/car/src/main/java/com/kouros/navigation/car/map/LocationPuck.kt index 2d8939e..6dc5cdf 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/LocationPuck.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/map/LocationPuck.kt @@ -1,4 +1,4 @@ -package com.kouros.navigation.car +package com.kouros.navigation.car.map import android.location.Location import androidx.compose.runtime.Composable diff --git a/common/car/src/main/java/com/kouros/navigation/car/MapView.kt b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt similarity index 92% rename from common/car/src/main/java/com/kouros/navigation/car/MapView.kt rename to common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt index 0816689..d560258 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/MapView.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt @@ -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) } diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt index 288a489..1344a5a 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt @@ -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() diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt index 858786c..b71d260 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt @@ -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() } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt index 76d7c14..6108f27 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt @@ -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) diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt index ca519af..1e763bc 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt @@ -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 { diff --git a/common/data/src/main/java/com/kouros/navigation/data/Color.kt b/common/data/src/main/java/com/kouros/navigation/data/Color.kt index eaf2d4c..82225a2 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Color.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Color.kt @@ -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) \ No newline at end of file +val SpeedColor = Color(0xFF262525) + +val PlaceColor = Color(0xFF868005) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/Data.kt b/common/data/src/main/java/com/kouros/navigation/data/Data.kt index 1c373cf..1ba89aa 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Data.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Data.kt @@ -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 + } diff --git a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt index 87cf7ec..99da2e9 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt @@ -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, diff --git a/common/data/src/main/java/com/kouros/navigation/data/styles/liberty_night.json b/common/data/src/main/java/com/kouros/navigation/data/styles/liberty_night.json index e900e6b..b8c29cc 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/styles/liberty_night.json +++ b/common/data/src/main/java/com/kouros/navigation/data/styles/liberty_night.json @@ -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)" } }, { diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt index b0b6ee3..3265233 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt @@ -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.. 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 { + val results = listOf() + 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() + } + +} \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt index c7fa1fd..27cce56 100644 --- a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt +++ b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt @@ -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 } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 934646d..411fbe0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }