diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f9adde0..047b3c5 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 = 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) 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 610281c..1a8e55a 100644 --- a/app/src/main/java/com/kouros/navigation/model/MockLocation.kt +++ b/app/src/main/java/com/kouros/navigation/model/MockLocation.kt @@ -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) { 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 229b585..bb2e34c 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -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 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 } } } diff --git a/app/src/main/java/com/kouros/navigation/ui/MapView.kt b/app/src/main/java/com/kouros/navigation/ui/MapView.kt index e16b46b..34b663e 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MapView.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MapView.kt @@ -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, diff --git a/automotive/src/main/AndroidManifest.xml b/automotive/src/main/AndroidManifest.xml index 311eae5..cf7c1e5 100644 --- a/automotive/src/main/AndroidManifest.xml +++ b/automotive/src/main/AndroidManifest.xml @@ -8,9 +8,9 @@ + - diff --git a/common/car/build.gradle.kts b/common/car/build.gradle.kts index c865cac..6477aa6 100644 --- a/common/car/build.gradle.kts +++ b/common/car/build.gradle.kts @@ -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")) diff --git a/common/car/src/main/AndroidManifest.xml b/common/car/src/main/AndroidManifest.xml index feea63c..00a818e 100644 --- a/common/car/src/main/AndroidManifest.xml +++ b/common/car/src/main/AndroidManifest.xml @@ -49,6 +49,7 @@ + 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 830c45d..280810c 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 @@ -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,26 +166,23 @@ 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 = NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel) - if (carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) + 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 = 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,7 +222,24 @@ class NavigationSession : Session(), NavigationScreen.Listener { carLocationListener ) } - carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener) + 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) { @@ -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) { 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 48b166a..3fadc2a 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 @@ -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()) 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 ? 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) - bearing( - lastLocation, - location, - cameraPosition.value!!.bearing - ) else { + val bearing = if (carOrientation == 999F) { + if (location.hasBearing()) { + location.bearing.toDouble() + } else { + bearing( + lastLocation, + location, + cameraPosition.value!!.bearing + ) + } + } else { carOrientation.toDouble() } - println("Bearing $bearing") val zoom = if (viewStyle == ViewStyle.VIEW) { calculateZoom(location.speed.toDouble()) } else { diff --git a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt index 96c97cb..f6648a3 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt @@ -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 { @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( @@ -306,7 +297,7 @@ fun DrawNavigationImages( if (speed != null) { CurrentSpeed(width, height, speed, maxSpeed) } - if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) { + if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) { MaxSpeed(width, height, maxSpeed) } //DebugInfo(width, height, routeModel) @@ -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( diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt index 4bd3281..df05a9a 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt @@ -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 -> {} diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/DarkModeSettings.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/DarkModeSettings.kt index e46122f..3e552ae 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/DarkModeSettings.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/DarkModeSettings.kt @@ -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 { 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 8887c85..0bad036 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 @@ -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 @@ -80,7 +83,7 @@ class NavigationScreen( invalidate() } } - val trafficObserver = Observer > { traffic -> + val trafficObserver = Observer> { traffic -> surfaceRenderer.setTrafficData(traffic) invalidate() } @@ -111,6 +114,9 @@ class NavigationScreen( surfaceRenderer.speedCamerasData.value = speedData } + val maxSpeedObserver = Observer { 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 { } @@ -497,7 +504,7 @@ class NavigationScreen( } updateSpeedCamera(location) with(routeModel) { - updateLocation(carContext,location, navigationViewModel) + updateLocation(carContext, location, navigationViewModel) if ((navState.maneuverType == Maneuver.TYPE_DESTINATION || navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT || navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT @@ -540,7 +547,7 @@ class NavigationScreen( val bearingSpeedCamera = if (camera.tags.direction != null) { try { camera.tags.direction!!.toFloat() - } catch ( e: Exception) { + } catch (e: Exception) { 0F } } else { @@ -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 = 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 { diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationSettings.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationSettings.kt index ab584e5..6b310ad 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationSettings.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationSettings.kt @@ -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( diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/PasswordSettings.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/PasswordSettings.kt new file mode 100644 index 0000000..e30cabb --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/PasswordSettings.kt @@ -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() + } +} \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt index 78d444c..41ba89b 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt @@ -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 = 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 = 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() } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt index be05f27..92bc874 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt @@ -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() diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutingSettings.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutingSettings.kt index f139963..7f2fb98 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutingSettings.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutingSettings.kt @@ -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 { 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 2197c6a..3de50f6 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 @@ -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 diff --git a/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt index c2788fe..697cb48 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt @@ -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 by lazy { MutableLiveData() } + /** LiveData containing categorized traffic incidents map */ val traffic: MutableLiveData> by lazy { MutableLiveData() } - + /** LiveData containing a preview route JSON string for route preview screens */ val previewRoute: MutableLiveData by lazy { MutableLiveData() } + /** LiveData containing the most recent place used for navigation */ val recentPlace: MutableLiveData by lazy { MutableLiveData() } + + /** LiveData containing list of recent navigation destinations */ val places: MutableLiveData> by lazy { MutableLiveData() } + /** LiveData containing list of favorite saved places */ val favorites: MutableLiveData> by lazy { MutableLiveData() } + /** LiveData containing search results from Nominatim geocoding */ val searchPlaces: MutableLiveData> by lazy { MutableLiveData() } + /** LiveData containing the best matching place from address search */ val placeLocation: MutableLiveData by lazy { MutableLiveData() } + /** LiveData containing contacts with addresses */ val contactAddress: MutableLiveData> by lazy { MutableLiveData() } + /** LiveData containing POI elements from Overpass API */ val elements: MutableLiveData> by lazy { MutableLiveData() } + /** LiveData containing speed camera locations */ val speedCameras: MutableLiveData> by lazy { MutableLiveData() } + /** LiveData containing current road speed limit */ val maxSpeed: MutableLiveData by lazy { MutableLiveData() } + + /** LiveData containing selected routing engine index */ val routingEngine: MutableLiveData by lazy { MutableLiveData() } + /** LiveData indicating whether permission is granted */ + val permissionGranted: MutableLiveData 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 { val featureCollection = FeatureCollection.fromJson(data) val incidents = mutableMapOf() @@ -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() @@ -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 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 { val results = listOf() try { @@ -511,5 +622,4 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo } return results.toMutableStateList() } - -} \ No newline at end of file +} diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt index 625c5ef..65ca5f4 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteCalculator.kt @@ -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) + // ) } } } 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 4f0ed4f..0f78e87 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 @@ -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) diff --git a/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt index da34480..6e63251 100644 --- a/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt +++ b/common/data/src/main/java/com/kouros/navigation/utils/GeoUtils.kt @@ -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 } 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 f05fff8..e7e3fc7 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 @@ -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 diff --git a/common/data/src/main/res/drawable/local_gas_station_24.xml b/common/data/src/main/res/drawable/local_gas_station_24.xml new file mode 100644 index 0000000..6a49d12 --- /dev/null +++ b/common/data/src/main/res/drawable/local_gas_station_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/data/src/main/res/drawable/local_gas_station_24px.xml b/common/data/src/main/res/drawable/local_gas_station_24px.xml deleted file mode 100644 index 3566c7d..0000000 --- a/common/data/src/main/res/drawable/local_gas_station_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/common/data/src/main/res/drawable/local_gas_station_48px.xml b/common/data/src/main/res/drawable/local_gas_station_48px.xml deleted file mode 100644 index 301dd10..0000000 --- a/common/data/src/main/res/drawable/local_gas_station_48px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/common/data/src/main/res/drawable/local_pharmacy_24px.xml b/common/data/src/main/res/drawable/local_pharmacy_24px.xml deleted file mode 100644 index 17a1640..0000000 --- a/common/data/src/main/res/drawable/local_pharmacy_24px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/common/data/src/main/res/values-de/strings.xml b/common/data/src/main/res/values-de/strings.xml index 695ac1a..97ef1dd 100644 --- a/common/data/src/main/res/values-de/strings.xml +++ b/common/data/src/main/res/values-de/strings.xml @@ -50,5 +50,7 @@ Auto GPS verwenden TomTom\t Optionen + TomTom ApiKey + Verwende Auto Einstellungen diff --git a/common/data/src/main/res/values/strings.xml b/common/data/src/main/res/values/strings.xml index 2c575a1..a4cf39a 100644 --- a/common/data/src/main/res/values/strings.xml +++ b/common/data/src/main/res/values/strings.xml @@ -37,4 +37,5 @@ TomTom\t Options TomTom ApiKey + Use car settings \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b1f6373..4d7596e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }