Diverse Änderungen

This commit is contained in:
Dimitris
2026-02-24 16:29:13 +01:00
parent 71d3d17847
commit e4b539c4e6
31 changed files with 405 additions and 194 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 45
versionName = "0.2.0.45"
versionCode = 49
versionName = "0.2.0.49"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -82,6 +82,7 @@ dependencies {
implementation(libs.koin.androidx.compose)
implementation(libs.maplibre.compose)
//implementation(libs.maplibre.composeMaterial3)
implementation(libs.androidx.app.projected)
implementation(libs.accompanist.permissions)
implementation(project(":common:data"))
@@ -97,7 +98,7 @@ dependencies {
implementation("com.github.ticofab:android-gpx-parser:2.3.1")
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.foundation.layout)
implementation(libs.androidx.compose.foundation.layout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -7,11 +7,16 @@ import android.os.SystemClock
class MockLocation (private var locationManager: LocationManager) {
var curSpeed = 0F
fun setMockLocation(latitude: Double, longitude: Double) {
fun setMockLocation(latitude: Double, longitude: Double, bearing : Float) {
try {
// Set mock location for all providers
setMockLocationForProvider(LocationManager.GPS_PROVIDER, latitude, longitude)
setMockLocationForProvider(LocationManager.NETWORK_PROVIDER, latitude, longitude)
setMockLocationForProvider(LocationManager.GPS_PROVIDER, latitude, longitude, bearing)
setMockLocationForProvider(
LocationManager.NETWORK_PROVIDER,
latitude,
longitude,
bearing
)
} catch (e: NumberFormatException) {
} catch (e: SecurityException) {
} catch (e: Exception) {
@@ -19,7 +24,12 @@ class MockLocation (private var locationManager: LocationManager) {
}
}
private fun setMockLocationForProvider(provider: String, latitude: Double, longitude: Double) {
private fun setMockLocationForProvider(
provider: String,
latitude: Double,
longitude: Double,
bearing: Float
) {
try {
// Check if provider exists
if (!locationManager.allProviders.contains(provider)) {
@@ -52,10 +62,10 @@ class MockLocation (private var locationManager: LocationManager) {
this.speed = 0f
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
this.bearingAccuracyDegrees = 0.0f
this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f
this.bearing = bearing
}
// Set the mock location
locationManager.setTestProviderLocation(provider, mockLocation)
@@ -79,6 +89,7 @@ class MockLocation (private var locationManager: LocationManager) {
this.bearingAccuracyDegrees = 0.0f
this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f
this.bearing = bearing
}
locationManager.setTestProviderLocation(provider, mockLocation)
} catch (ex: Exception) {

View File

@@ -77,19 +77,16 @@ import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.Location
import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() {
val routeData = MutableLiveData("")
val routeModel = RouteModel()
var tilt = 50.0
val useMock = false
val type = 1 // 1 simulate 2 test 3 gpx 4 testSingle
val type = 3 // 1 simulate 2 test 3 gpx 4 testSingle
val stepData: MutableLiveData<StepData> by lazy {
MutableLiveData()
@@ -125,7 +122,6 @@ class MainActivity : ComponentActivity() {
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var mock: MockLocation
private var loadRecentPlaces = false
lateinit var baseStyle: BaseStyle.Json
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) {
@@ -140,7 +136,7 @@ class MainActivity : ComponentActivity() {
if (useMock) {
mock = MockLocation(locationManager)
mock.setMockLocation(
homeVogelhart.latitude, homeVogelhart.longitude
homeVogelhart.latitude, homeVogelhart.longitude, 0F
)
navigationViewModel.route.observe(this, observer)
}
@@ -190,8 +186,9 @@ class MainActivity : ComponentActivity() {
val appViewModel: AppViewModel = appViewModel()
val darkMode by appViewModel.darkMode.collectAsState()
val sheetPeekHeight = 250.dp
baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1)
val baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1)
val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
@@ -240,7 +237,7 @@ class MainActivity : ComponentActivity() {
cameraPosition,
routeData,
tilt,
baseStyle
baseStyle,
)
}
if (!routeModel.isNavigating()) {
@@ -304,6 +301,9 @@ class MainActivity : ComponentActivity() {
fun updateLocation(location: Location?) {
if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) {
val currentLocation = location(location.position.longitude, location.position.latitude)
if (location.bearing != null) {
currentLocation.bearing = location.bearing!!.toFloat()
}
if (routeModel.isNavigating()) {
val snapedLocation =
snapLocation(currentLocation, routeModel.route.maneuverLocations())
@@ -315,7 +315,15 @@ class MainActivity : ComponentActivity() {
}
fun updateLocationInternal(currentLocation: android.location.Location, location: Location?) {
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
if (currentLocation.hasBearing()) {
routeModel.navState = routeModel.navState.copy(routeBearing = currentLocation.bearing)
}
val bearing = if (currentLocation.hasBearing()) {
currentLocation.bearing.toDouble()
} else {
bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
}
with(routeModel) {
if (isNavigating()) {
updateLocation(applicationContext, currentLocation, navigationViewModel)
@@ -348,7 +356,7 @@ class MainActivity : ComponentActivity() {
closeSheet()
routeModel.stopNavigation(applicationContext)
if (useMock) {
mock.setMockLocation(latitude, longitude)
mock.setMockLocation(latitude, longitude, 0F)
}
routeData.value = ""
stepData.value = StepData("", 0.0, 0, 0, 0, 0.0)
@@ -380,14 +388,18 @@ class MainActivity : ComponentActivity() {
fun simulate() {
CoroutineScope(Dispatchers.IO).launch {
var lastLocation = location(0.0, 0.0)
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
val curLocation = location(waypoint[0], waypoint[1])
if (routeModel.isNavigating()) {
val deviation = 0.0
if (index in 0..routeModel.curRoute.waypoints.size) {
mock.setMockLocation(waypoint[1], waypoint[0])
val bearing = lastLocation.bearingTo(curLocation)
mock.setMockLocation(waypoint[1], waypoint[0], bearing)
Thread.sleep(1000)
}
}
lastLocation = curLocation
}
}
}
@@ -416,7 +428,7 @@ class MainActivity : ComponentActivity() {
fun testSingleUpdate(latitude: Double, longitude: Double) {
if (1 == 1) {
mock.setMockLocation(latitude, longitude)
mock.setMockLocation(latitude, longitude, 0F)
} else {
routeModel.updateLocation(
applicationContext,
@@ -425,12 +437,12 @@ class MainActivity : ComponentActivity() {
}
val step = routeModel.currentStep()
val nextStep = routeModel.nextStep()
println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}")
Thread.sleep(1_000)
}
fun gpx(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
var lastLocation = location(0.0, 0.0)
val parser = GPXParser()
val input = context.resources.openRawResource(R.raw.vh)
val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread
@@ -441,18 +453,23 @@ class MainActivity : ComponentActivity() {
segments!!.forEach { seg ->
var lastTime = DateTime.now()
seg!!.trackPoints.forEach { p ->
val curLocation = location(p.longitude, p.latitude)
val ext = p.extensions
val speed: Double?
if (ext != null) {
speed = ext.speed
mock.curSpeed = speed.toFloat()
}
val duration = p.time.millis - lastTime.millis
mock.setMockLocation(p.latitude, p.longitude)
val bearing = lastLocation.bearingTo(curLocation)
println("Bearing $bearing")
mock.setMockLocation(p.latitude, p.longitude, bearing)
if (duration > 0) {
delay(duration / 5)
}
lastTime = p.time
lastLocation = curLocation
}
}
}

View File

@@ -4,26 +4,21 @@ import android.content.Context
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.window.layout.WindowMetricsCalculator
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.data.StepData
import com.kouros.navigation.ui.app.AppViewModel
import com.kouros.navigation.ui.app.appViewModel
import com.kouros.navigation.utils.settingsViewModel
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.LocationTrackingEffect
@@ -64,8 +59,6 @@ fun MapView(
)
)
val rememberBaseStyle = rememberBaseStyle(baseStyle)
val appViewModel: AppViewModel = appViewModel()
val showBuildings by appViewModel.show3D.collectAsState()
@@ -73,9 +66,8 @@ fun MapView(
NavigationInfo(step, nextStep)
Box(contentAlignment = Alignment.Center) {
MapLibre(
applicationContext,
cameraState,
rememberBaseStyle,
baseStyle,
route,
emptyMap(),
ViewStyle.VIEW,

View File

@@ -8,9 +8,9 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.car.permission.CAR_SPEED"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<!-- Various required feature settings for an automotive app. -->
<uses-feature
android:name="android.hardware.type.automotive"
android:required="true" />

View File

@@ -46,7 +46,6 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.ui)
implementation(libs.maplibre.compose)
implementation(libs.androidx.app.projected)
//implementation(libs.maplibre.composeMaterial3)
implementation(project(":common:data"))

View File

@@ -49,6 +49,7 @@
<category android:name="androidx.car.app.category.WEATHER" />
<category android:name="androidx.car.app.category.POI"/>
<category android:name="androidx.car.app.category.NAVIGATION"/>
<category android:name="androidx.car.app.category.FEATURE_CLUSTER"/>
</intent-filter>
</service>
</application>

View File

@@ -13,6 +13,7 @@ import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.Session
import androidx.car.app.connection.CarConnection
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.common.CarValue
import androidx.car.app.hardware.common.OnCarDataAvailableListener
@@ -36,6 +37,7 @@ import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository
@@ -48,7 +50,7 @@ import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import android.Manifest.permission
class NavigationSession : Session(), NavigationScreen.Listener {
@@ -82,14 +84,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
override fun onDestroy(owner: LifecycleOwner) {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
val repository = getSettingsRepository(carContext)
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
if (useCarLocation) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
carSensors.removeCarHardwareLocationListener(carLocationListener)
}
carInfo.removeSpeedListener(carSpeedListener)
removeSensors()
Log.i(TAG, "In onDestroy()")
val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
@@ -139,6 +134,24 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
}
fun onPermissionGranted(permission : Boolean) {
addSensors(routeModel.navState.carConnection)
}
fun onConnectionStateUpdated(connectionState: Int) {
routeModel.navState = routeModel.navState.copy(carConnection = connectionState)
when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
CarConnection.CONNECTION_TYPE_NATIVE -> {
ObjectBox.init(carContext)
navigationScreen.checkPermission("android.car.permission.CAR_SPEED")
} // Automotive OS
CarConnection.CONNECTION_TYPE_PROJECTION -> {
"Connected to Android Auto"
navigationScreen.checkPermission("com.google.android.gms.permission.CAR_SPEED")
}
}
}
override fun onCreateScreen(intent: Intent): Screen {
@@ -153,18 +166,16 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
}
// lifecycleScope.launch {
//}
navigationViewModel = getViewModel(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
navigationViewModel.permissionGranted.observe(this, ::onPermissionGranted)
routeModel = RouteCarModel()
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
navigationScreen =
@@ -172,7 +183,6 @@ class NavigationSession : Session(), NavigationScreen.Listener {
if ( carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED
&& carContext.checkSelfPermission("com.google.android.gms.permission.CAR_SPEED") == PackageManager.PERMISSION_GRANTED
&& !useContacts
|| (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED)
@@ -180,27 +190,24 @@ class NavigationSession : Session(), NavigationScreen.Listener {
requestLocationUpdates()
} else {
// If we do not have the location permission, show the request permission screen.
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission.ACCESS_FINE_LOCATION)
val screenManager =
carContext.getCarService(ScreenManager::class.java)
screenManager
.push(navigationScreen)
return RequestPermissionScreen(
carContext,
mLocationPermissionCheckCallback = {
permissionCheckCallback = {
screenManager.pop()
},
mContactsPermissionCheckCallback = {
screenManager.pop()
}
)
}
addSensors()
return navigationScreen
}
fun addSensors() {
fun addSensors(connectionState: Int) {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
val repository = getSettingsRepository(carContext)
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
@@ -215,8 +222,25 @@ class NavigationSession : Session(), NavigationScreen.Listener {
carLocationListener
)
}
if (connectionState == CarConnection.CONNECTION_TYPE_NATIVE
|| connectionState == CarConnection.CONNECTION_TYPE_PROJECTION) {
carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener)
}
}
fun removeSensors() {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
val repository = getSettingsRepository(carContext)
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
if (useCarLocation) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
carSensors.removeCarHardwareLocationListener(carLocationListener)
}
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_NATIVE
|| routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
carInfo.removeSpeedListener(carSpeedListener)
}
}
override fun onNewIntent(intent: Intent) {
val screenManager = carContext.getCarService(ScreenManager::class.java)
@@ -277,6 +301,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
fun updateLocation(location: Location) {
if (location.hasBearing()) {
routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing)
}
if (routeModel.isNavigating()) {
navigationScreen.updateTrip(location)
if (!routeModel.navState.arrived) {

View File

@@ -29,7 +29,6 @@ import com.kouros.navigation.car.map.DrawNavigationImages
import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.ObjectBox
@@ -79,6 +78,7 @@ class SurfaceRenderer(
val trafficData = MutableLiveData(emptyMap<String, String>())
val speedCamerasData = MutableLiveData("")
val speed = MutableLiveData(0F)
val maxSpeed = MutableLiveData(0)
var viewStyle = ViewStyle.VIEW
lateinit var centerLocation: Location
@@ -175,14 +175,6 @@ class SurfaceRenderer(
}
fun onConnectionStateUpdated(connectionState: Int) {
when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto"
CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext) // Automotive OS
}
}
fun onBaseStyleStateUpdated(style: BaseStyle) {
}
@@ -190,29 +182,24 @@ class SurfaceRenderer(
@Composable
fun MapView() {
//val appViewModel: AppViewModel = appViewModel(viewModelStoreOwner)
//val darkMode by appViewModel.darkMode.collectAsState()
val darkMode = settingsViewModel(carContext, viewModelStoreOwner).darkMode.collectAsState().value
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
val showBuildings = settingsViewModel(carContext, viewModelStoreOwner).show3D.collectAsState().value
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState()
val traffic: Map<String, String> ? by trafficData.observeAsState()
val speedCameras: String? by speedCamerasData.observeAsState()
val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt)
val rememberBaseStyle = rememberBaseStyle(baseStyle)
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
MapLibre(
carContext,
cameraState,
rememberBaseStyle,
baseStyle,
route,
traffic,
viewStyle,
speedCameras,
false
showBuildings
)
ShowPosition(cameraState, position, paddingValues)
}
@@ -226,12 +213,12 @@ class SurfaceRenderer(
val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
val currentSpeed: Float? by speed.observeAsState()
val maxSpeed: Int? by maxSpeed.observeAsState()
val speed: Int? by maxSpeed.observeAsState()
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
DrawNavigationImages(
paddingValues,
currentSpeed,
maxSpeed!!,
speed!!,
width,
height
)
@@ -251,7 +238,6 @@ class SurfaceRenderer(
}
override fun onCreate(owner: LifecycleOwner) {
CarConnection(carContext).type.observe(owner, ::onConnectionStateUpdated)
style.observe(owner, :: onBaseStyleStateUpdated)
Log.i(TAG, "SurfaceRenderer created")
carContext.getCarService(AppManager::class.java)
@@ -281,15 +267,19 @@ class SurfaceRenderer(
fun updateLocation(location: Location) {
synchronized(this) {
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
val bearing = if (carOrientation == 999F)
val bearing = if (carOrientation == 999F) {
if (location.hasBearing()) {
location.bearing.toDouble()
} else {
bearing(
lastLocation,
location,
cameraPosition.value!!.bearing
) else {
)
}
} else {
carOrientation.toDouble()
}
println("Bearing $bearing")
val zoom = if (viewStyle == ViewStyle.VIEW) {
calculateZoom(location.speed.toDouble())
} else {

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.map
import android.content.Context
import android.location.Location
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
@@ -10,8 +9,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -26,18 +23,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.kouros.data.R
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
@@ -89,7 +81,6 @@ fun cameraState(
@Composable
fun MapLibre(
context: Context,
cameraState: CameraState,
baseStyle: BaseStyle.Json,
route: String?,
@@ -229,15 +220,15 @@ fun trafficColor(key: String): Expression<ColorValue> {
@Composable
fun AmenityLayer(routeData: String?) {
if (routeData != null && routeData.isNotEmpty()) {
if (!routeData.isNullOrEmpty()) {
var color = const(Color.Red)
var img = image(painterResource(R.drawable.local_pharmacy_48px), drawAsSdf = true)
if (routeData.contains(Constants.CHARGING_STATION)) {
color = const(Color.Green)
color = const(Color(0xFF054603))
img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true)
} else if (routeData.contains(Constants.FUEL_STATION)) {
color = const(Color.Black)
img = image(painterResource(R.drawable.local_gas_station_48px), drawAsSdf = true)
color = const(Color.Blue)
img = image(painterResource(R.drawable.local_gas_station_24), drawAsSdf = true)
}
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
SymbolLayer(
@@ -261,7 +252,7 @@ fun AmenityLayer(routeData: String?) {
@Composable
fun SpeedCameraLayer(speedCameras: String?) {
if (speedCameras != null && speedCameras.isNotEmpty()) {
if (!speedCameras.isNullOrEmpty()) {
val color = const(Color.Red)
val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras))
SymbolLayer(
@@ -497,14 +488,6 @@ fun DebugInfo(
}
}
@Composable
fun rememberBaseStyle(baseStyle: BaseStyle.Json): BaseStyle.Json {
val rememberBaseStyle by remember() {
mutableStateOf(baseStyle)
}
return rememberBaseStyle
}
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) {
ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(

View File

@@ -76,7 +76,7 @@ class CategoriesScreen(
fun carIcon(context: CarContext, id: String): CarIcon {
val resId = when (id) {
FUEL_STATION -> R.drawable.local_gas_station_48px
FUEL_STATION -> R.drawable.local_gas_station_24
PHARMACY -> R.drawable.local_pharmacy_48px
CHARGING_STATION -> R.drawable.ev_station_48px
else -> {}

View File

@@ -46,7 +46,7 @@ class DarkModeSettings(private val carContext: CarContext) : Screen(carContext)
)
.addItem(
buildRowForTemplate(
R.string.use_telephon_settings,
R.string.use_car_settings,
)
)
.setOnSelectedListener { index: Int ->
@@ -74,14 +74,6 @@ class DarkModeSettings(private val carContext: CarContext) : Screen(carContext)
private fun onSelected(index: Int) {
settingsViewModel.onDarkModeChanged(index)
CarToast.makeText(
carContext,
(carContext
.getString(R.string.display_settings)
+ ":"
+ " " + index), CarToast.LENGTH_LONG
)
.show()
}
private fun buildRowForTemplate(title: Int): Row {

View File

@@ -1,11 +1,14 @@
package com.kouros.navigation.car.screen
import android.Manifest
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.CountDownTimer
import android.os.Handler
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip
@@ -111,6 +114,9 @@ class NavigationScreen(
surfaceRenderer.speedCamerasData.value = speedData
}
val maxSpeedObserver = Observer<Int> { speed ->
surfaceRenderer.maxSpeed.value = speed
}
init {
navigationViewModel.route.observe(this, observer)
@@ -118,6 +124,7 @@ class NavigationScreen(
navigationViewModel.recentPlace.observe(this, recentObserver)
navigationViewModel.placeLocation.observe(this, placeObserver)
navigationViewModel.speedCameras.observe(this, speedObserver)
navigationViewModel.maxSpeed.observe(this, maxSpeedObserver)
lifecycleScope.launch {
getSettingsViewModel(carContext).routingEngine.collect {
}
@@ -552,6 +559,29 @@ class NavigationScreen(
}
}
}
fun checkPermission(permission: String) {
println("Car connection permission: $permission")
if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission)
val screenManager =
carContext.getCarService(ScreenManager::class.java)
screenManager
.push(
RequestPermissionScreen(
carContext,
permissionCheckCallback = {
screenManager.pop()
navigationViewModel.permissionGranted.value = true
},
permissions
)
)
} else {
navigationViewModel.permissionGranted.value = true
}
}
}
enum class NavigationType {

View File

@@ -16,7 +16,10 @@ import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.launch
class NavigationSettings(private val carContext: CarContext, private var navigationViewModel: NavigationViewModel) :
class NavigationSettings(
private val carContext: CarContext,
private var navigationViewModel: NavigationViewModel
) :
Screen(carContext) {
private var motorWayToggleState = false
@@ -79,6 +82,12 @@ class NavigationSettings(private val carContext: CarContext, private var navigat
R.string.routing_engine
)
)
.addItem(
buildRowForScreenTemplate(
PasswordSettings(carContext, navigationViewModel),
R.string.tomtom_api_key
)
)
return ListTemplate.Builder()
.setSingleList(listBuilder.build())
.setHeader(

View File

@@ -0,0 +1,67 @@
package com.kouros.navigation.car.screen
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.InputCallback
import androidx.car.app.model.ParkedOnlyOnClickListener
import androidx.car.app.model.Template
import androidx.car.app.model.signin.InputSignInMethod
import androidx.car.app.model.signin.SignInTemplate
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.launch
class PasswordSettings(
private val carContext: CarContext,
private var navigationViewModel: NavigationViewModel
) : Screen(carContext) {
var errorMessage: String? = null
val settingsViewModel = getSettingsViewModel(carContext)
init {
lifecycleScope.launch {
settingsViewModel.tomTomApiKey.collect {
invalidate()
}
}
}
override fun onGetTemplate(): Template {
val apiKey = settingsViewModel.tomTomApiKey.value
val callback: InputCallback = object : InputCallback {
override fun onInputSubmitted(text: String) {
settingsViewModel.onTomTomApiKeyChanged(text)
//errorMessage = carContext.getString(R.string.tomtom_api_key)
invalidate()
}
}
val builder = InputSignInMethod.Builder(callback)
.setHint(apiKey)
.setInputType(InputSignInMethod.INPUT_TYPE_PASSWORD)
if (errorMessage != null) {
// builder.setErrorMessage(errorMessage)
}
val signInMethod = builder.build()
val pinSignInAction = Action.Builder()
.setTitle(carContext.getString(R.string.stop_action_title))
.setOnClickListener(ParkedOnlyOnClickListener.create {
println("Sign")
invalidate()
})
.build()
return SignInTemplate.Builder(signInMethod)
//.addAction(pinSignInAction)
.setTitle(carContext.getString(R.string.settings_action_title))
.setInstructions(
carContext.getString(R.string.tomtom_api_key)
)
.setHeaderAction(Action.BACK)
.build()
}
}

View File

@@ -14,22 +14,26 @@ import androidx.car.app.model.Template
/** Screen for asking the user to grant location permission. */
class RequestPermissionScreen(
carContext: CarContext,
var mLocationPermissionCheckCallback: LocationPermissionCheckCallback,
var mContactsPermissionCheckCallback: LocationPermissionCheckCallback,
var permissionCheckCallback: PermissionCheckCallback,
//var mContactsPermissionCheckCallback: LocationPermissionCheckCallback,
val permissions: MutableList<String?> = ArrayList()
) : Screen(carContext) {
/** Callback called when the location permission is granted. */
fun interface LocationPermissionCheckCallback {
/** Callback called when the location permission is granted. */
/** Callback called when the permission is granted. */
fun interface PermissionCheckCallback {
/** Callback called when the permission is granted. */
fun onPermissionGranted()
}
override fun onGetTemplate(): Template {
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission.ACCESS_FINE_LOCATION)
permissions.add("com.google.android.gms.permission.CAR_SPEED")
//permissions.add(permission.READ_CONTACTS)
val message = "This app needs access to location and to car speed"
var message = ""
if (permissions.contains(permission.ACCESS_FINE_LOCATION))
message = "This app needs access to location and to car speed"
if (permissions.contains("android.car.permission.CAR_SPEED"))
message = "This app needs access to car speed"
if (permissions.contains("com.google.android.gms.permission.CAR_SPEED"))
message = "This app needs access to car speed"
val listener: OnClickListener = ParkedOnlyOnClickListener.create {
carContext.requestPermissions(
@@ -41,8 +45,8 @@ class RequestPermissionScreen(
CarToast.LENGTH_LONG
).show()
if (!approved!!.isEmpty()) {
mLocationPermissionCheckCallback.onPermissionGranted()
mContactsPermissionCheckCallback.onPermissionGranted()
permissionCheckCallback.onPermissionGranted()
//mContactsPermissionCheckCallback.onPermissionGranted()
finish()
}
}

View File

@@ -79,7 +79,7 @@ class RoutePreviewScreen(
val itemListBuilder = ItemList.Builder()
var i = 0
routeModel.route.routes.forEach { it ->
routeModel.route.routes.forEach { _ ->
itemListBuilder.addItem(createRow(i++, navigateAction))
}
@@ -95,7 +95,7 @@ class RoutePreviewScreen(
.build()
val message =
if (routeModel.isNavigating() && routeModel.curRoute.waypoints!!.isNotEmpty()) {
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
createRouteText(0)
} else {
CarText.Builder("Wait")
@@ -190,7 +190,7 @@ class RoutePreviewScreen(
private fun createRouteText(index: Int): CarText {
val time = routeModel.route.routes[index].summary.duration
val length =
BigDecimal(routeModel.route.routes[index].summary.distance).setScale(
BigDecimal((routeModel.route.routes[index].summary.distance) / 1000).setScale(
1,
RoundingMode.HALF_EVEN
)
@@ -207,7 +207,7 @@ class RoutePreviewScreen(
val titleText = "$index"
return Row.Builder()
.setTitle(route)
.setOnClickListener(OnClickListener { onRouteSelected(index) })
.setOnClickListener { onRouteSelected(index) }
.addText(titleText)
.addAction(action)
.build()

View File

@@ -73,14 +73,6 @@ class RoutingSettings(private val carContext: CarContext, private var navigation
private fun onSelected(index: Int) {
settingsViewModel.onRoutingEngineChanged(index)
navigationViewModel.routingEngine.value = index
CarToast.makeText(
carContext,
(carContext
.getString(R.string.routing_engine)
+ ":"
+ " " + index), CarToast.LENGTH_LONG
)
.show()
}
private fun buildRowForTemplate(title: Int): Row {

View File

@@ -124,7 +124,7 @@ object Constants {
val homeVogelhart = location(11.5793748, 48.185749)
val homeHohenwaldeck = location( 11.594322, 48.1164817)
const val NEXT_STEP_THRESHOLD = 500.0
const val NEXT_STEP_THRESHOLD = 300.0
const val MAXIMAL_SNAP_CORRECTION = 50.0

View File

@@ -33,59 +33,87 @@ import org.maplibre.geojson.FeatureCollection
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* ViewModel for navigation-related data operations.
* Handles route calculation, place search, traffic information, and local data persistence.
*/
class NavigationViewModel(private val repository: NavigationRepository) : ViewModel() {
/** LiveData containing the calculated route JSON string */
val route: MutableLiveData<String> by lazy {
MutableLiveData()
}
/** LiveData containing categorized traffic incidents map */
val traffic: MutableLiveData<Map<String, String>> by lazy {
MutableLiveData()
}
/** LiveData containing a preview route JSON string for route preview screens */
val previewRoute: MutableLiveData<String> by lazy {
MutableLiveData()
}
/** LiveData containing the most recent place used for navigation */
val recentPlace: MutableLiveData<Place> by lazy {
MutableLiveData()
}
/** LiveData containing list of recent navigation destinations */
val places: MutableLiveData<List<Place>> by lazy {
MutableLiveData()
}
/** LiveData containing list of favorite saved places */
val favorites: MutableLiveData<List<Place>> by lazy {
MutableLiveData()
}
/** LiveData containing search results from Nominatim geocoding */
val searchPlaces: MutableLiveData<List<SearchResult>> by lazy {
MutableLiveData()
}
/** LiveData containing the best matching place from address search */
val placeLocation: MutableLiveData<SearchResult> by lazy {
MutableLiveData()
}
/** LiveData containing contacts with addresses */
val contactAddress: MutableLiveData<List<Place>> by lazy {
MutableLiveData()
}
/** LiveData containing POI elements from Overpass API */
val elements: MutableLiveData<List<Elements>> by lazy {
MutableLiveData()
}
/** LiveData containing speed camera locations */
val speedCameras: MutableLiveData<List<Elements>> by lazy {
MutableLiveData()
}
/** LiveData containing current road speed limit */
val maxSpeed: MutableLiveData<Int> by lazy {
MutableLiveData()
}
/** LiveData containing selected routing engine index */
val routingEngine: MutableLiveData<Int> by lazy {
MutableLiveData()
}
/** LiveData indicating whether permission is granted */
val permissionGranted: MutableLiveData<Boolean> by lazy {
MutableLiveData()
}
/**
* Loads the most recent place from ObjectBox and calculates its distance.
* Posts the result to recentPlace LiveData if distance > 1km.
*/
fun loadRecentPlace(location: Location, carOrientation: Float, context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -116,6 +144,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Loads all recent places from ObjectBox and calculates distances.
* Posts the sorted list to places LiveData.
*/
fun loadRecentPlaces(context: Context, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -146,6 +178,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Loads favorite places from ObjectBox and calculates distances.
* Posts the sorted list to favorites LiveData.
*/
fun loadFavorites(context: Context, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -175,6 +211,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Calculates a route between current location and destination.
* Posts the route JSON to route LiveData.
*/
fun loadRoute(
context: Context,
currentLocation: Location,
@@ -198,6 +238,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Fetches traffic incident data and categorizes by severity.
* Posts categorized traffic map to traffic LiveData.
*/
fun loadTraffic(context: Context, currentLocation: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -216,6 +260,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Categorizes traffic incidents by type (queuing, stationary, slow, heavy, roadworks).
* @param data Raw traffic GeoJSON string
* @return Map of incident type to GeoJSON FeatureCollection
*/
private fun rebuildTraffic(data: String): Map<String, String> {
val featureCollection = FeatureCollection.fromJson(data)
val incidents = mutableMapOf<String, String>()
@@ -238,6 +287,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return incidents
}
/**
* Calculates a preview route for route preview screen.
* Posts the route JSON to previewRoute LiveData.
*/
fun loadPreviewRoute(
context: Context,
currentLocation: Location,
@@ -261,6 +314,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Loads device contacts with addresses and converts to Place objects.
* Posts results to contactAddress LiveData.
*/
fun loadContacts(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
val contactList = mutableListOf<Place>()
@@ -288,7 +345,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Finds the closest matching address for a search query.
* Posts the best result to placeLocation LiveData.
*/
fun findAddress(search: String, location: Location) {
var sortedList: List<SearchResult>
viewModelScope.launch(Dispatchers.IO) {
@@ -310,6 +370,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Searches for places using Nominatim geocoding API.
* Posts sorted results to searchPlaces LiveData.
*/
fun searchPlaces(search: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val placesJson = repository.searchPlaces(search, location)
@@ -330,6 +394,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Performs reverse geocoding to get street name from coordinates.
* @return Street name or empty string
*/
fun reverseAddress(location: Location): String {
val address = repository.reverseAddress(location)
val gson = GsonBuilder().serializeNulls().create()
@@ -337,6 +405,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return place.address.road
}
/**
* Queries Overpass API for nearby amenities of a specific category.
* Posts sorted results to elements LiveData.
*/
fun getAmenities(category: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("amenity", category, location, 5.0)
@@ -353,6 +425,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Queries Overpass API for speed cameras within a radius.
* Posts sorted results to speedCameras LiveData.
*/
fun getSpeedCameras(location: Location, radius: Double) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
@@ -369,6 +445,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Queries Overpass API for speed limit on current road using fuzzy matching.
* Posts speed limit to maxSpeed LiveData.
*/
fun getMaxSpeed(location: Location, street: String) {
viewModelScope.launch(Dispatchers.IO) {
val levenshtein = Levenshtein()
@@ -387,11 +467,18 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Saves a place as a favorite in ObjectBox.
*/
fun saveFavorite(place: Place) {
place.category = Constants.FAVORITES
savePlace(place)
}
/**
* Saves a place to recent destinations in ObjectBox.
* Skips fuel stations, charging stations, and pharmacies.
*/
fun saveRecent(place: Place) {
if (place.category == Constants.FUEL_STATION
|| place.category == Constants.CHARGING_STATION
@@ -403,6 +490,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
savePlace(place)
}
/**
* Saves a place to ObjectBox, removing existing duplicates first.
* Updates the timestamp to current time.
*/
private fun savePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -426,16 +517,25 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Deletes a place from favorites in ObjectBox.
*/
fun deleteFavorite(place: Place) {
place.category = Constants.FAVORITES
deletePlace(place)
}
/**
* Deletes a place from recent destinations in ObjectBox.
*/
fun deleteRecent(place: Place) {
place.category = Constants.RECENT
deletePlace(place)
}
/**
* Deletes a place from ObjectBox matching name and category.
*/
fun deletePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -456,6 +556,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Retrieves search filter settings from preferences.
* @return SearchFilter with avoid motorway/tollway flags
*/
fun getSearchFilter(context: Context): SearchFilter {
val repository = getSettingsRepository(context)
val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() }
@@ -463,7 +567,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return SearchFilter(avoidMotorway, avoidTollway)
}
/**
* Loads recent places with calculated distances for Compose state.
* @return SnapshotStateList of recent places with distances
*/
fun loadPlaces2(
context: Context,
location: Location,
@@ -495,6 +602,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return results.toMutableStateList()
}
/**
* Loads recent places as Compose SnapshotStateList.
* @return SnapshotStateList of recent places
*/
fun loadRecentPlace(): SnapshotStateList<Place?> {
val results = listOf<Place>()
try {
@@ -511,5 +622,4 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
return results.toMutableStateList()
}
}

View File

@@ -26,9 +26,9 @@ class RouteCalculator(var routeModel: RouteModel) {
routeModel.navState.route.currentStepIndex = step.index
step.waypointIndex = wayIndex
step.wayPointLocation = location(waypoint[0], waypoint[1])
routeModel.navState = routeModel.navState.copy(
routeBearing = routeModel.navState.lastLocation.bearingTo(location)
)
//routeModel.navState = routeModel.navState.copy(
// routeBearing = routeModel.navState.lastLocation.bearingTo(location)
// )
}
}
}

View File

@@ -2,6 +2,8 @@ package com.kouros.navigation.model
import android.content.Context
import android.location.Location
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
import androidx.car.app.navigation.model.Maneuver
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Place
@@ -33,7 +35,8 @@ open class RouteModel {
val currentLocation: Location = location(0.0, 0.0),
val routeBearing: Float = 0F,
val currentRouteIndex: Int = 0,
val destination: Place = Place()
val destination: Place = Place(),
val carConnection: Int = 0,
)
var navState = NavigationState()
@@ -81,9 +84,8 @@ open class RouteModel {
fun updateLocation(context: Context, curLocation: Location, viewModel: NavigationViewModel) {
navState = navState.copy(currentLocation = curLocation)
routeCalculator.findStep(curLocation)
val repository = getSettingsRepository(context)
val carLocation = runBlocking { repository.carLocationFlow.first() }
if (carLocation) {
if (navState.carConnection == CONNECTION_TYPE_PROJECTION
|| navState.carConnection == CONNECTION_TYPE_NATIVE) {
routeCalculator.updateSpeedLimit(curLocation, viewModel)
}
navState = navState.copy(lastLocation = navState.currentLocation)

View File

@@ -27,6 +27,9 @@ object GeoUtils {
val point = pointFeature.geometry() as Point
newLocation.latitude = point.latitude()
newLocation.longitude = point.longitude()
if (location.hasBearing()) {
newLocation.bearing = location.bearing
}
}
return newLocation
}

View File

@@ -52,7 +52,7 @@ fun calculateZoom(speed: Double?): Double {
}
fun previewZoom(previewDistance: Double): Double {
when (previewDistance) {
when (previewDistance / 1000) {
in 0.0..10.0 -> return 13.5
in 10.0..20.0 -> return 11.5
in 20.0..30.0 -> return 10.5

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.77,7.23l0.01,-0.01 -3.72,-3.72L15,4.56l2.11,2.11c-0.94,0.36 -1.61,1.26 -1.61,2.33 0,1.38 1.12,2.5 2.5,2.5 0.36,0 0.69,-0.08 1,-0.21v7.21c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1L17,14c0,-1.1 -0.9,-2 -2,-2h-1L14,5c0,-1.1 -0.9,-2 -2,-2L6,3c-1.1,0 -2,0.9 -2,2v16h10v-7.5h1.5v5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5L20.5,9c0,-0.69 -0.28,-1.32 -0.73,-1.77zM12,10L6,10L6,5h6v5zM18,10c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M160,840L160,200Q160,167 183.5,143.5Q207,120 240,120L480,120Q513,120 536.5,143.5Q560,167 560,200L560,480L600,480Q633,480 656.5,503.5Q680,527 680,560L680,740Q680,757 691.5,768.5Q703,780 720,780Q737,780 748.5,768.5Q760,757 760,740L760,452Q751,457 741,458.5Q731,460 720,460Q678,460 649,431Q620,402 620,360Q620,328 637.5,302.5Q655,277 684,266L600,182L642,140L790,284Q805,299 812.5,319Q820,339 820,360L820,740Q820,782 791,811Q762,840 720,840Q678,840 649,811Q620,782 620,740L620,540Q620,540 620,540Q620,540 620,540L560,540L560,840L160,840ZM240,400L480,400L480,200Q480,200 480,200Q480,200 480,200L240,200Q240,200 240,200Q240,200 240,200L240,400ZM720,400Q737,400 748.5,388.5Q760,377 760,360Q760,343 748.5,331.5Q737,320 720,320Q703,320 691.5,331.5Q680,343 680,360Q680,377 691.5,388.5Q703,400 720,400ZM240,760L480,760L480,480L240,480L240,760ZM480,760L240,760L240,760L480,760Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.63,468 649.31,482.69Q664,497.37 664,518L664,737Q664,758.68 679.5,773.34Q695,788 717,788Q739,788 754.5,773.34Q770,758.68 770,737L770,442Q759,448 747,451Q735,454 723,454Q683.52,454 656.26,426.74Q629,399.48 629,360Q629,328.39 647,303.19Q665,278 695,270L600,175L636,140L789,293Q803,307 811.5,323.5Q820,340 820,360L820,737Q820,780.26 790.18,810.13Q760.37,840 717.18,840Q674,840 644,810.13Q614,780.26 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM723,404Q741,404 754,391Q767,378 767,360Q767,342 754,329Q741,316 723,316Q705,316 692,329Q679,342 679,360Q679,378 692,391Q705,404 723,404ZM220,780L489,780L489,468L220,468L220,780ZM489,780L220,780L220,780L489,780Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,840L120,760L200,520L120,280L120,200L628,200L686,40L780,74L734,200L840,200L840,280L760,520L840,760L840,840L120,840ZM440,680L520,680L520,560L640,560L640,480L520,480L520,360L440,360L440,480L320,480L320,560L440,560L440,680ZM204,760L756,760L676,520L756,280L204,280L284,520L204,760ZM480,520L480,520L480,520L480,520L480,520L480,520Z"/>
</vector>

View File

@@ -50,5 +50,7 @@
<string name="use_car_location">Auto GPS verwenden</string>
<string name="tomtom">TomTom\t</string>
<string name="options">Optionen</string>
<string name="tomtom_api_key">TomTom ApiKey</string>
<string name="use_car_settings">Verwende Auto Einstellungen</string>
</resources>

View File

@@ -37,4 +37,5 @@
<string name="tomtom">TomTom\t</string>
<string name="options">Options</string>
<string name="tomtom_api_key">TomTom ApiKey</string>
<string name="use_car_settings">Use car settings</string>
</resources>

View File

@@ -38,7 +38,6 @@ material3WindowSizeClass = "1.4.0"
uiGraphics = "1.10.3"
window = "1.5.1"
foundationLayout = "1.10.3"
foundationLayoutVersion = "1.10.3"
datastorePreferences = "1.2.0"
datastoreCore = "1.2.0"
@@ -81,7 +80,6 @@ androidx-app-automotive = { module = "androidx.car.app:app-automotive", version.
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" }
androidx-window = { group = "androidx.window", name = "window", version.ref = "window" }
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
androidx-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayoutVersion" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" }