From a468529ca4d8c15ae4f88e870062e3fbc6c3727e Mon Sep 17 00:00:00 2001 From: Dimitris Date: Sat, 28 Feb 2026 13:10:48 +0100 Subject: [PATCH] Testing, Remove ObjectBox --- app/build.gradle.kts | 26 +- .../com/kouros/navigation/MainApplication.kt | 2 - .../com/kouros/navigation/model/Simulation.kt | 4 - .../com/kouros/navigation/ui/MainActivity.kt | 20 +- .../com/kouros/navigation/ui/SearchSheet.kt | 6 +- automotive/build.gradle.kts | 10 +- build.gradle.kts | 4 +- common/car/build.gradle.kts | 20 +- .../com/kouros/navigation/car/GeoUtilsTest.kt | 44 +++ .../kouros/navigation/car/RouteModelTest.kt | 144 ++++++++++ .../navigation/car/NavigationSession.kt | 24 +- .../kouros/navigation/car/SurfaceRenderer.kt | 5 +- .../com/kouros/navigation/car/map/MapView.kt | 30 +- .../car/navigation/RouteCarModel.kt | 20 +- .../navigation/car/screen/NavigationScreen.kt | 100 +++---- .../navigation/car/screen/PlaceListScreen.kt | 6 +- .../car/screen/RoutePreviewScreen.kt | 13 +- .../com/kouros/navigation/car/UnitTest.kt | 33 --- common/data/build.gradle.kts | 29 +- .../java/com/kouros/navigation/data/Color.kt | 4 +- .../java/com/kouros/navigation/data/Data.kt | 15 +- .../navigation/data/NavigationRepository.kt | 2 + .../kouros/navigation/data/NavigationState.kt | 2 + .../com/kouros/navigation/data/ObjectBox.kt | 22 -- .../data/datastore/DataStoreManager.kt | 29 ++ .../data/tomtom/TomTomRepository.kt | 9 +- .../navigation/model/NavigationViewModel.kt | 264 ++++++++---------- .../com/kouros/navigation/model/RouteModel.kt | 61 ++-- .../repository/SettingsRepository.kt | 15 + .../com/kouros/navigation/utils/GeoUtils.kt | 1 - .../navigation/model/RouteCalculatorTest.kt | 247 ++++++++++++++++ .../kouros/navigation/model/RouteModelTest.kt | 109 ++++++++ .../kouros/navigation/utils/GeoUtilsTest.kt | 205 ++++++++++++++ gradle/libs.versions.toml | 46 +-- settings.gradle.kts | 8 + 35 files changed, 1148 insertions(+), 431 deletions(-) create mode 100644 common/car/src/androidTest/java/com/kouros/navigation/car/GeoUtilsTest.kt create mode 100644 common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt delete mode 100644 common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt delete mode 100644 common/data/src/main/java/com/kouros/navigation/data/ObjectBox.kt create mode 100644 common/data/src/test/java/com/kouros/navigation/model/RouteCalculatorTest.kt create mode 100644 common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt create mode 100644 common/data/src/test/java/com/kouros/navigation/utils/GeoUtilsTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 43f52e6..624852a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,9 +2,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) -} + } android { namespace = "com.kouros.navigation" @@ -14,8 +13,8 @@ android { applicationId = "com.kouros.navigation" minSdk = 33 targetSdk = 36 - versionCode = 50 - versionName = "0.2.0.50" + versionCode = 56 + versionName = "0.2.0.56" base.archivesName = "navi-$versionName" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -37,7 +36,11 @@ android { buildTypes { release { + // Enables code-related app optimization. isMinifyEnabled = false + // Enables resource shrinking. + isShrinkResources = false + proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -59,14 +62,10 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlin { - compilerOptions { - jvmTarget = JvmTarget.JVM_11 - } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } + buildFeatures { compose = true } @@ -81,8 +80,7 @@ dependencies { implementation(libs.androidx.runtime.livedata) 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")) @@ -95,7 +93,7 @@ dependencies { implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.window) implementation(libs.androidx.compose.foundation.layout) - implementation("com.github.ticofab:android-gpx-parser:2.3.1") + implementation(libs.android.gpx.parser) implementation(libs.androidx.navigation.compose) implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.compose.foundation.layout) diff --git a/app/src/main/java/com/kouros/navigation/MainApplication.kt b/app/src/main/java/com/kouros/navigation/MainApplication.kt index ac8694c..832c2c7 100644 --- a/app/src/main/java/com/kouros/navigation/MainApplication.kt +++ b/app/src/main/java/com/kouros/navigation/MainApplication.kt @@ -2,7 +2,6 @@ package com.kouros.navigation import android.app.Application import android.content.Context -import com.kouros.navigation.data.ObjectBox import com.kouros.navigation.di.appModule import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.utils.NavigationUtils.getViewModel @@ -15,7 +14,6 @@ class MainApplication : Application() { override fun onCreate() { super.onCreate() - ObjectBox.init(this) appContext = applicationContext navigationViewModel = getViewModel(appContext!!) startKoin { diff --git a/app/src/main/java/com/kouros/navigation/model/Simulation.kt b/app/src/main/java/com/kouros/navigation/model/Simulation.kt index a674c09..54fea0d 100644 --- a/app/src/main/java/com/kouros/navigation/model/Simulation.kt +++ b/app/src/main/java/com/kouros/navigation/model/Simulation.kt @@ -36,7 +36,6 @@ fun test(applicationContext: Context, routeModel: RouteModel) { for ((index, step) in routeModel.curLeg.steps.withIndex()) { for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) { routeModel.updateLocation( - applicationContext, location(waypoint[0], waypoint[1]), navigationViewModel ) val step = routeModel.currentStep() @@ -81,7 +80,6 @@ fun testSingleUpdate( mock.setMockLocation(latitude, longitude, 0F) } else { routeModel.updateLocation( - applicationContext, location(longitude, latitude), navigationViewModel ) } @@ -110,10 +108,8 @@ fun gpx(context: Context, mock: MockLocation) { speed = ext.speed mock.curSpeed = speed.toFloat() } - val duration = p.time.millis - lastTime.millis val bearing = lastLocation.bearingTo(curLocation) - println("Bearing $bearing") mock.setMockLocation(p.latitude, p.longitude, bearing) if (duration > 0) { delay(duration / 5) 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 75fa00c..50e338a 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -66,9 +66,12 @@ import com.kouros.navigation.ui.navigation.AppNavGraph import com.kouros.navigation.ui.theme.NavigationTheme import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.bearing +import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.location +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.location.DesiredAccuracy import org.maplibre.compose.location.Location @@ -83,7 +86,7 @@ class MainActivity : ComponentActivity() { val routeModel = RouteModel() var tilt = 50.0 val useMock = false - val type = SimulationType.GPX + val type = SimulationType.SIMULATE val stepData: MutableLiveData by lazy { MutableLiveData() @@ -95,7 +98,13 @@ class MainActivity : ComponentActivity() { var lastLocation = location(0.0, 0.0) val observer = Observer { newRoute -> if (newRoute.isNotEmpty()) { - routeModel.startNavigation(newRoute, applicationContext) + val repository = getSettingsRepository(applicationContext) + val routingEngine = runBlocking { repository.routingEngineFlow.first() } + routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) + routeModel.startNavigation(newRoute) + if (routeModel.hasLegs()) { + getSettingsViewModel(applicationContext).onLastRouteChanged(newRoute) + } routeData.value = routeModel.curRoute.routeGeoJson if (useMock) { when (type) { @@ -189,7 +198,7 @@ class MainActivity : ComponentActivity() { val scaffoldState = rememberBottomSheetScaffoldState() val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() - val sheetPeekHeight = 250.dp + val sheetPeekHeight = 180.dp val sheetPeekHeightState = remember { mutableStateOf(sheetPeekHeight) } val locationProvider = rememberDefaultLocationProvider( @@ -324,7 +333,7 @@ class MainActivity : ComponentActivity() { with(routeModel) { if (isNavigating()) { - updateLocation(applicationContext, currentLocation, navigationViewModel) + updateLocation( currentLocation, navigationViewModel) stepData.value = currentStep() nextStepData.value = nextStep() if (navState.maneuverType in 39..42 && routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE) { @@ -352,7 +361,8 @@ class MainActivity : ComponentActivity() { val latitude = routeModel.curRoute.waypoints[0][1] val longitude = routeModel.curRoute.waypoints[0][0] closeSheet() - routeModel.stopNavigation(applicationContext) + routeModel.stopNavigation() + getSettingsViewModel(applicationContext).onLastRouteChanged("") if (useMock) { mock.setMockLocation(latitude, longitude, 0F) } diff --git a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt b/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt index eeb8f11..1ffabae 100644 --- a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt +++ b/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt @@ -97,7 +97,7 @@ fun Home( ) { Row(horizontalArrangement = Arrangement.SpaceBetween) { Button(onClick = { - val places = viewModel.loadRecentPlace() + val places = viewModel.loadRecentPlace(applicationContext) val toLocation = location(places.first()!!.longitude, places.first()!!.latitude) viewModel.loadRoute(applicationContext, location, toLocation, 0F) closeSheet() @@ -168,7 +168,7 @@ fun SearchBar( } if (searchResults.isNotEmpty()) { Text("Search places") - SearchPlaces(searchResults, viewModel, context, location, closeSheet) + SearchPlaces( searchResults, viewModel, context, location, closeSheet) } } } @@ -211,7 +211,7 @@ private fun SearchPlaces( city = place.address.city, street = place.address.road ) - viewModel.saveRecent(pl) + viewModel.saveRecent(context,pl) val toLocation = location(place.lon.toDouble(), place.lat.toDouble()) viewModel.loadRoute(context, location, toLocation, 0F) diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index a879b46..342e444 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) } android { @@ -31,13 +30,8 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlin { - compilerOptions { - jvmTarget = JvmTarget.JVM_11 - } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } } diff --git a/build.gradle.kts b/build.gradle.kts index c40487e..e00027f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,17 +2,15 @@ plugins { alias(libs.plugins.android.application) apply false - alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.android.library) apply false } buildscript { - val objectboxVersion by extra("5.0.1") // For KTS build scripts + dependencies { classpath(libs.gradle) - classpath("io.objectbox:objectbox-gradle-plugin:$objectboxVersion") } } diff --git a/common/car/build.gradle.kts b/common/car/build.gradle.kts index 6477aa6..abce27b 100644 --- a/common/car/build.gradle.kts +++ b/common/car/build.gradle.kts @@ -1,8 +1,5 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) } @@ -26,13 +23,8 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlin { - compilerOptions { - jvmTarget = JvmTarget.JVM_11 - } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } buildFeatures { compose = true @@ -46,8 +38,7 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.ui) implementation(libs.maplibre.compose) - //implementation(libs.maplibre.composeMaterial3) - + implementation(libs.androidx.app.projected) implementation(project(":common:data")) implementation(libs.androidx.runtime.livedata) implementation(libs.androidx.compose.foundation) @@ -56,6 +47,11 @@ dependencies { implementation(libs.androidx.compose.ui.text) implementation(libs.play.services.location) implementation(libs.androidx.datastore.core) + implementation(libs.androidx.monitor) androidTestImplementation(libs.androidx.junit) testImplementation(libs.junit) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + androidTestImplementation(libs.androidx.runner) + androidTestImplementation(libs.androidx.rules) } \ No newline at end of file diff --git a/common/car/src/androidTest/java/com/kouros/navigation/car/GeoUtilsTest.kt b/common/car/src/androidTest/java/com/kouros/navigation/car/GeoUtilsTest.kt new file mode 100644 index 0000000..960c9ed --- /dev/null +++ b/common/car/src/androidTest/java/com/kouros/navigation/car/GeoUtilsTest.kt @@ -0,0 +1,44 @@ +package com.kouros.navigation.car + +import android.location.Location +import android.location.LocationManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.kouros.navigation.utils.GeoUtils +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.maplibre.geojson.Point + +@RunWith(AndroidJUnit4::class) +class GeoUtilsTest { + + @Test + fun snapLocation() { + val location = Location(LocationManager.GPS_PROVIDER) + location.latitude = 48.18600 + location.longitude = 11.57844 + + val stepCoordinates = listOf( + Point.fromLngLat(11.57841, 48.18557), + Point.fromLngLat(11.57844, 48.18566), + Point.fromLngLat(11.57848, 48.18595), + Point.fromLngLat(11.57848, 48.18604), + Point.fromLngLat(11.57857, 48.18696), + ) + val result = GeoUtils.snapLocation(location, stepCoordinates) + assertEquals(result.latitude, 48.18599999996868, 0.0001) + + } + + @Test + fun `createCenterLocation calculates center of GeoJSON`() { + val geoJson = + """{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[11.0,48.0]},"properties":{}},{"type":"Feature","geometry":{"type":"Point","coordinates":[11.1,48.1]},"properties":{}}]}""" + + val result = GeoUtils.createCenterLocation(geoJson) + + // Center should be roughly halfway between the two points + assertEquals(48.05, result.latitude, 0.01) + assertEquals(11.05, result.longitude, 0.01) + } +} \ No newline at end of file diff --git a/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt b/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt new file mode 100644 index 0000000..41b23a5 --- /dev/null +++ b/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt @@ -0,0 +1,144 @@ +package com.kouros.navigation.car + +import android.location.Location +import android.location.LocationManager +import androidx.car.app.navigation.model.Maneuver +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.kouros.data.R +import com.kouros.navigation.data.Constants.homeHohenwaldeck +import com.kouros.navigation.data.Constants.homeVogelhart +import com.kouros.navigation.data.RouteEngine +import com.kouros.navigation.data.tomtom.TomTomRepository +import com.kouros.navigation.model.NavigationViewModel +import com.kouros.navigation.model.RouteModel +import com.kouros.navigation.utils.getSettingsRepository +import com.kouros.navigation.utils.location +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* +import org.junit.Before +import kotlin.collections.forEach + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class RouteModelTest { + + val routeModel = RouteModel() + val location = Location(LocationManager.GPS_PROVIDER) + + @Before + fun setup() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + val repository = getSettingsRepository(appContext) + runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) } + val routeJson = appContext.resources.openRawResource(R.raw.tomom_routing) + val routeJsonString = routeJson.bufferedReader().use { it.readText() } + assertNotEquals("", routeJsonString) + routeModel.navState = routeModel.navState.copy(routingEngine = RouteEngine.TOMTOM.ordinal) + routeModel.startNavigation(routeJsonString) + } + + @Test + fun checkRoute() { + assertEquals(true, routeModel.isNavigating()) + assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0) + assertEquals(routeModel.curRoute.summary.duration, 1148.0, 10.0) + } + + @Test + fun checkDeparture() { + location.latitude = 48.185569 + location.longitude = 11.579034 + routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) + val stepData = routeModel.currentStep() + assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) + assertEquals(stepData.instruction, "Silcherstraße") + assertEquals(stepData.leftStepDistance, 70.0, 1.0) + val nextStepData = routeModel.nextStep() + assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) + assertEquals(nextStepData.instruction, "Schmalkaldener Straße") + } + + + @Test + fun checkSchmalkadener20() { + location.latitude = 48.187057 + location.longitude = 11.576652 + routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) + val stepData = routeModel.currentStep() + assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) + assertEquals(stepData.instruction, "Schmalkaldener Straße") + assertEquals(stepData.leftStepDistance, 0.0, 1.0) + val nextStepData = routeModel.nextStep() + assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) + assertEquals(nextStepData.instruction, "Ingolstädter Straße") + } + + @Test + fun checkIngol() { + location.latitude = 48.180555 + location.longitude = 11.585125 + routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) + val stepData = routeModel.currentStep() + assertEquals(stepData.currentManeuverType, Maneuver.TYPE_STRAIGHT) + assertEquals(stepData.instruction, "Ingolstädter Straße") + assertEquals(stepData.leftStepDistance, 350.0, 1.0) + val nextStepData = routeModel.nextStep() + assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT) + assertEquals(nextStepData.instruction, "Schenkendorfstraße") + } + + @Test + fun checkIngol2() { + location.latitude = 48.179286 + location.longitude = 11.585258 + location.bearing = 180.0F + routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) + val stepData = routeModel.currentStep() + assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT) + assertEquals(stepData.instruction, "Schenkendorfstraße") + assertEquals(stepData.leftStepDistance, 240.0, 1.0) + assertEquals(stepData.lane.size, 4) + assertEquals(stepData.lane.first().valid, true) + assertEquals(stepData.lane.last().valid, false) + val nextStepData = routeModel.nextStep() + assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_KEEP_LEFT) + assertEquals(nextStepData.instruction, "Schenkendorfstraße") + } + + @Test + fun checkDestination() { + location.latitude = homeHohenwaldeck.latitude + location.longitude = homeHohenwaldeck.longitude + routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) + val stepData = routeModel.nextStep() + assertEquals(stepData.currentManeuverType, Maneuver.TYPE_DESTINATION_LEFT) + } + + @Test + fun simulate() { + for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) { + val curLocation = location(waypoint[0], waypoint[1]) + if (routeModel.isNavigating()) { + if (index in 0..routeModel.curRoute.waypoints.size) { + routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository())) + val stepData = routeModel.currentStep() + val nextData = routeModel.nextStep() + println("${stepData.leftStepDistance} : ${stepData.instruction} ${stepData.currentManeuverType} : ${nextData.instruction} ${nextData.currentManeuverType}") + } + } + } + } +} \ No newline at end of file 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 246e4f5..cc2fec8 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 @@ -26,7 +26,6 @@ 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 @@ -119,19 +118,26 @@ class NavigationSession : Session(), NavigationScreen.Listener { */ fun onConnectionStateUpdated(connectionState: Int) { routeModel.navState = routeModel.navState.copy(carConnection = connectionState) - if (::carSensorManager.isInitialized) { - carSensorManager.updateConnectionState(connectionState) - } when (connectionState) { CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit CarConnection.CONNECTION_TYPE_NATIVE -> { - ObjectBox.init(carContext) navigationScreen.checkPermission("android.car.permission.CAR_SPEED") + if (carContext.checkSelfPermission("android.car.permission.CAR_SPEED") == PackageManager.PERMISSION_GRANTED) { + if (::carSensorManager.isInitialized) { + carSensorManager.updateConnectionState(connectionState) + } + } } CarConnection.CONNECTION_TYPE_PROJECTION -> { navigationScreen.checkPermission("com.google.android.gms.permission.CAR_SPEED") + if (carContext.checkSelfPermission("com.google.android.gms.permission.CAR_SPEED") == PackageManager.PERMISSION_GRANTED) { + if (::carSensorManager.isInitialized) { + carSensorManager.updateConnectionState(connectionState) + } + } } } + } /** @@ -318,13 +324,11 @@ class NavigationSession : Session(), NavigationScreen.Listener { * Snaps location to route and checks for deviation requiring reroute. */ private fun handleNavigationLocation(location: Location) { + routeModel.navState = routeModel.navState.copy(travelMessage = "${location.latitude} ${location.longitude}") navigationScreen.updateTrip(location) - if (routeModel.navState.arrived) return - val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations()) val distance = location.distanceTo(snappedLocation) - when { distance > MAXIMAL_ROUTE_DEVIATION -> { navigationScreen.calculateNewRoute(routeModel.navState.destination) @@ -342,8 +346,8 @@ class NavigationSession : Session(), NavigationScreen.Listener { * Stops active navigation and clears route state. * Called when user exits navigation or arrives at destination. */ - override fun stopNavigation(context: CarContext) { - routeModel.stopNavigation(context) + override fun stopNavigation() { + routeModel.stopNavigation() } 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 0f9464b..429e736 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 @@ -10,7 +10,6 @@ import androidx.car.app.AppManager import androidx.car.app.CarContext import androidx.car.app.SurfaceCallback import androidx.car.app.SurfaceContainer -import androidx.car.app.connection.CarConnection import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -31,7 +30,6 @@ import com.kouros.navigation.car.map.cameraState import com.kouros.navigation.car.map.getPaddingValues import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.data.Constants.homeVogelhart -import com.kouros.navigation.data.ObjectBox import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.model.BaseStyleModel import com.kouros.navigation.model.RouteModel @@ -292,7 +290,8 @@ class SurfaceRenderer( currentSpeed, speed!!, width, - height + height, + 0.0 ) } LaunchedEffect(position, viewStyle) { 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 f6648a3..f8d1ac6 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 @@ -116,7 +116,7 @@ fun MapLibre( @Composable fun RouteLayer(routeData: String?, trafficData: Map) { - if (routeData != null && routeData.isNotEmpty()) { + if (!routeData.isNullOrEmpty()) { val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) LineLayer( id = "routes-casing", @@ -184,7 +184,7 @@ fun RouteLayer(routeData: String?, trafficData: Map) { @Composable fun RouteLayerPoint(routeData: String?) { - if (routeData != null && routeData.isNotEmpty()) { + if (!routeData.isNullOrEmpty()) { val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) val img = image(painterResource(R.drawable.ic_favorite_filled_white_24dp), drawAsSdf = true) SymbolLayer( @@ -209,11 +209,11 @@ fun RouteLayerPoint(routeData: String?) { fun trafficColor(key: String): Expression { when (key) { - "queuing" -> return const(Color(0xFFD24417)) + "queuing" -> return const(Color(0xFFC46E53)) "stationary" -> return const(Color(0xFFFF0000)) "heavy" -> return const(Color(0xFF6B0404)) - "slow" -> return const(Color(0xFFC41F1F)) - "roadworks" -> return const(Color(0xFF7A631A)) + "slow" -> return const(Color(0xFFBD2525)) + "roadworks" -> return const(Color(0xFF725A0F)) } return const(Color.Blue) } @@ -291,7 +291,8 @@ fun DrawNavigationImages( speed: Float?, maxSpeed: Int, width: Int, - height: Int + height: Int, + lat: Double? ) { NavigationImage(padding, width, height) if (speed != null) { @@ -300,26 +301,26 @@ fun DrawNavigationImages( if (speed != null && maxSpeed > 0 && (speed * 3.6) > maxSpeed) { MaxSpeed(width, height, maxSpeed) } - //DebugInfo(width, height, routeModel) + //DebugInfo(width, height, lat!!) } @Composable fun NavigationImage(padding: PaddingValues, width: Int, height: Int) { val imageSize = (height / 8) - val color = remember { NavigationColor } + val navigationColor = remember { NavigationColor } Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) { Canvas( modifier = Modifier .size(imageSize.dp, imageSize.dp) ) { scale(scaleX = 1f, scaleY = 0.7f) { - drawCircle(Color.DarkGray.copy(alpha = 0.3f)) + drawCircle(navigationColor.copy(alpha = 0.3f)) } } Icon( painter = painterResource(id = R.drawable.navigation_48px), "Navigation", - tint = color.copy(alpha = 0.7f), + tint = navigationColor.copy(alpha = 0.7f), modifier = Modifier .size(imageSize.dp, imageSize.dp) .scale(scaleX = 1f, scaleY = 0.7f), @@ -453,7 +454,7 @@ private fun MaxSpeed( fun DebugInfo( width: Int, height: Int, - routeModel: RouteModel, + latitude: Double, ) { Box( @@ -465,19 +466,18 @@ fun DebugInfo( contentAlignment = Alignment.CenterStart ) { val textMeasurerLocation = rememberTextMeasurer() - val location = routeModel.navState.currentLocation.latitude.toString() val styleSpeed = TextStyle( fontSize = 26.sp, fontWeight = FontWeight.Bold, color = Color.Black, ) - val textLayoutLocation = remember(location) { - textMeasurerLocation.measure(location, styleSpeed) + val textLayoutLocation = remember(latitude.toString()) { + textMeasurerLocation.measure(latitude.toString(), styleSpeed) } Canvas(modifier = Modifier.fillMaxSize()) { drawText( textMeasurer = textMeasurerLocation, - text = location, + text = latitude.toString(), style = styleSpeed, topLeft = Offset( x = center.x - textLayoutLocation.size.width / 2, diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt index a8df211..94d68be 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt @@ -1,18 +1,3 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package com.kouros.navigation.car.navigation import android.location.Location @@ -138,9 +123,9 @@ class RouteCarModel() : RouteModel() { var laneImageAdded = false stepData.lane.forEach { if (it.indications.isNotEmpty() && it.valid) { - Collections.sort(it.indications) + val sorted = it.indications.sorted() var direction = "" - it.indications.forEach { it2 -> + sorted.forEach { it2 -> direction = if (direction.isEmpty()) { it2.trim() } else { @@ -212,7 +197,6 @@ class RouteCarModel() : RouteModel() { .addAction(dismissAction).setCallback(object : AlertCallback { override fun onCancel(reason: Int) { } - override fun onDismiss() { } }).build() 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 aaeb8e6..cec17b9 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 @@ -24,22 +24,25 @@ import androidx.car.app.navigation.model.MessageInfo import androidx.car.app.navigation.model.NavigationTemplate import androidx.car.app.navigation.model.RoutingInfo import androidx.core.graphics.drawable.IconCompat -import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.kouros.data.R import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.car.navigation.RouteCarModel +import com.kouros.navigation.car.screen.observers.NavigationObserverCallback +import com.kouros.navigation.car.screen.observers.NavigationObserverManager import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Place -import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.utils.GeoUtils +import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.location +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import java.time.Duration import java.time.LocalDateTime import java.time.ZoneOffset @@ -51,58 +54,67 @@ class NavigationScreen( private var routeModel: RouteCarModel, private var listener: Listener, private val navigationViewModel: NavigationViewModel -) : - Screen(carContext) { +) : Screen(carContext), NavigationObserverCallback { /** A listener for navigation start and stop signals. */ interface Listener { /** Stops navigation. */ - fun stopNavigation(context: CarContext) + fun stopNavigation() } + val backGroundColor = CarColor.BLUE + var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER) var recentPlace = Place() var navigationType = NavigationType.VIEW var lastTrafficDate: LocalDateTime? = LocalDateTime.of(1960, 6, 21, 0, 0) - val observer = Observer { route -> + var lastCameraSearch = 0 + var speedCameras = listOf() + + val observerManager = NavigationObserverManager(navigationViewModel, this) + + init { + observerManager.attachAllObservers(this) + lifecycleScope.launch { + getSettingsViewModel(carContext).routingEngine.collect { + } + } + } + + // NavigationObserverCallback implementations + override fun onRouteReceived(route: String) { if (route.isNotEmpty()) { + val repository = getSettingsRepository(carContext) + val routingEngine = runBlocking { repository.routingEngineFlow.first() } + routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) navigationType = NavigationType.NAVIGATION - routeModel.startNavigation(route, carContext) + routeModel.startNavigation(route) + if (routeModel.hasLegs()) { + getSettingsViewModel(carContext).onLastRouteChanged(route) + } surfaceRenderer.setRouteData() invalidate() } } - val recentObserver = Observer { lastPlace -> - if (!routeModel.isNavigating()) { - recentPlace = lastPlace - navigationType = NavigationType.RECENT - invalidate() - } - } - val trafficObserver = Observer> { traffic -> - surfaceRenderer.setTrafficData(traffic) + override fun isNavigating(): Boolean = routeModel.isNavigating() + + override fun onRecentPlaceReceived(place: Place) { + recentPlace = place + navigationType = NavigationType.RECENT invalidate() } - val placeObserver = Observer { searchResult -> - val place = Place( - name = searchResult.displayName, - street = searchResult.address.road, - city = searchResult.address.city, - latitude = searchResult.lat.toDouble(), - longitude = searchResult.lon.toDouble(), - category = Constants.CONTACTS, - postalCode = searchResult.address.postcode - ) + override fun onTrafficReceived(traffic: Map) { + surfaceRenderer.setTrafficData(traffic) + } + + override fun onPlaceSearchResultReceived(place: Place) { navigateToPlace(place) } - var lastCameraSearch = 0 - - var speedCameras = listOf() - val speedObserver = Observer> { cameras -> + override fun onSpeedCamerasReceived(cameras: List) { speedCameras = cameras val coordinates = mutableListOf>() cameras.forEach { @@ -112,21 +124,12 @@ class NavigationScreen( surfaceRenderer.speedCamerasData.value = speedData } - val maxSpeedObserver = Observer { speed -> + override fun onMaxSpeedReceived(speed: Int) { surfaceRenderer.maxSpeed.value = speed } - init { - navigationViewModel.route.observe(this, observer) - navigationViewModel.traffic.observe(this, trafficObserver); - 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 { - } - } + override fun invalidateScreen() { + invalidate() } override fun onGetTemplate(): Template { @@ -151,7 +154,7 @@ class NavigationScreen( .setDestinationTravelEstimate(routeModel.travelEstimate(carContext)) .setActionStrip(actionStripBuilder.build()) .setMapActionStrip(mapActionStripBuilder().build()) - .setBackgroundColor(CarColor.GREEN) + .setBackgroundColor(backGroundColor) .build() } @@ -208,7 +211,7 @@ class NavigationScreen( ) .build() ) - .setBackgroundColor(CarColor.GREEN) + .setBackgroundColor(CarColor.SECONDARY) .setActionStrip(actionStripBuilder.build()) .setMapActionStrip(mapActionStripBuilder().build()) .build() @@ -243,7 +246,7 @@ class NavigationScreen( return NavigationTemplate.Builder() .setNavigationInfo(RoutingInfo.Builder().setLoading(true).build()) .setActionStrip(actionStripBuilder.build()) - .setBackgroundColor(CarColor.GREEN) + .setBackgroundColor(backGroundColor) .build() } @@ -447,7 +450,7 @@ class NavigationScreen( fun navigateToPlace(place: Place) { navigationType = NavigationType.VIEW val location = location(place.longitude, place.latitude) - navigationViewModel.saveRecent(place) + navigationViewModel.saveRecent(carContext, place) currentNavigationLocation = location navigationViewModel.loadRoute( carContext, @@ -461,7 +464,7 @@ class NavigationScreen( fun stopNavigation() { navigationType = NavigationType.VIEW - listener.stopNavigation(carContext) + listener.stopNavigation() surfaceRenderer.routeData.value = "" lastCameraSearch = 0 invalidate() @@ -502,7 +505,7 @@ class NavigationScreen( } updateSpeedCamera(location) with(routeModel) { - updateLocation(carContext, location, navigationViewModel) + updateLocation( location, navigationViewModel) if ((navState.maneuverType == Maneuver.TYPE_DESTINATION || navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT || navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT @@ -510,6 +513,7 @@ class NavigationScreen( && routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE ) { stopNavigation() + getSettingsViewModel(carContext).onLastRouteChanged("") navState = navState.copy(arrived = true) surfaceRenderer.routeData.value = "" navigationType = NavigationType.ARRIVAL diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt index ee4c544..ec61a09 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt @@ -89,7 +89,7 @@ class PlaceListScreen( "" } val row = Row.Builder() - .setImage(contactIcon(it.avatar, it.category)) + // .setImage(contactIcon(it.avatar, it.category)) .setTitle("$street ${it.city}") .setOnClickListener { val place = Place( @@ -101,7 +101,7 @@ class PlaceListScreen( it.postalCode, it.city, it.street, - avatar = null + // avatar = null ) screenManager .pushForResult( @@ -162,7 +162,7 @@ class PlaceListScreen( ) ) .setOnClickListener { - navigationViewModel.deletePlace(place) + navigationViewModel.deletePlace(carContext, place) CarToast.makeText( carContext, R.string.recent_Item_deleted, CarToast.LENGTH_LONG 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 92bc874..ab3de4d 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 @@ -29,7 +29,11 @@ import com.kouros.navigation.car.navigation.NavigationMessage import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.data.Place import com.kouros.navigation.model.NavigationViewModel +import com.kouros.navigation.utils.getSettingsRepository +import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.location +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import java.math.BigDecimal import java.math.RoundingMode @@ -48,7 +52,10 @@ class RoutePreviewScreen( val navigationMessage = NavigationMessage(carContext) val observer = Observer { route -> if (route.isNotEmpty()) { - routeModel.startNavigation(route, carContext) + val repository = getSettingsRepository(carContext) + val routingEngine = runBlocking { repository.routingEngineFlow.first() } + routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) + routeModel.startNavigation(route) surfaceRenderer.setPreviewRouteData(routeModel) invalidate() } @@ -163,7 +170,7 @@ class RoutePreviewScreen( CarToast.LENGTH_SHORT ) .show() - navigationViewModel.saveFavorite(destination) + navigationViewModel.saveFavorite(carContext, destination) invalidate() } .build() @@ -171,7 +178,7 @@ class RoutePreviewScreen( private fun deleteFavoriteAction(): Action = Action.Builder() .setOnClickListener { if (isFavorite) { - navigationViewModel.deleteFavorite(destination) + navigationViewModel.deleteFavorite(carContext,destination) } isFavorite = !isFavorite finish() diff --git a/common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt b/common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt deleted file mode 100644 index 8020d3b..0000000 --- a/common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.kouros.navigation.car - -import com.kouros.navigation.data.valhalla.ValhallaRepository -import com.kouros.navigation.model.RouteModel -import com.kouros.navigation.model.NavigationViewModel -import org.junit.Test - -/** - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ViewModelTest { - - val repo = ValhallaRepository() - val navigationViewModel = NavigationViewModel(repo) - val model = RouteModel() - - @Test - fun routeViewModelTest() { - -// val fromLocation = Location(LocationManager.GPS_PROVIDER) -// fromLocation.isMock = true -// fromLocation.latitude = homeLocation.latitude -// fromLocation.longitude = homeLocation.longitude -// val toLocation = Location(LocationManager.GPS_PROVIDER) -// toLocation.isMock = true -// toLocation.latitude = home2Location.latitude -// toLocation.longitude = home2Location.longitude -// -// val route = repo.getRoute(fromLocation, toLocation, SearchFilter()) - //model.startNavigation(route) - } -} diff --git a/common/data/build.gradle.kts b/common/data/build.gradle.kts index a6b9b65..173f769 100644 --- a/common/data/build.gradle.kts +++ b/common/data/build.gradle.kts @@ -1,14 +1,9 @@ -import org.gradle.kotlin.dsl.annotationProcessor -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - - plugins { alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) kotlin("plugin.serialization") version "2.2.21" - kotlin("kapt") -} + alias(libs.plugins.kotlin.kapt) + } android { namespace = "com.kouros.data" @@ -37,9 +32,9 @@ android { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlin { - compilerOptions { - jvmTarget = JvmTarget.JVM_11 + testOptions { + unitTests { + isReturnDefaultValues = true } } } @@ -60,20 +55,20 @@ dependencies { implementation(libs.android.sdk.turf) implementation(libs.androidx.compose.runtime) - - // objectbox - implementation(libs.objectbox.kotlin) implementation(libs.androidx.material3) - annotationProcessor(libs.objectbox.processor) implementation(libs.androidx.datastore.preferences) implementation(libs.kotlinx.serialization.json) implementation(libs.maplibre.compose) - implementation("androidx.compose.material:material-icons-extended:1.7.8") + implementation(libs.androidx.compose.material.icons.extended) testImplementation(libs.junit) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) -} + androidTestImplementation(libs.androidx.runner) + androidTestImplementation(libs.androidx.rules) -apply(plugin = "io.objectbox") + +} diff --git a/common/data/src/main/java/com/kouros/navigation/data/Color.kt b/common/data/src/main/java/com/kouros/navigation/data/Color.kt index 4b4ffa7..7c27b77 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Color.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Color.kt @@ -2,9 +2,9 @@ package com.kouros.navigation.data import androidx.compose.ui.graphics.Color -val NavigationColor = Color(0xFF0730B2) +val NavigationColor = Color(0xFF16BBB6) -val RouteColor = Color(0xFF5582D0) +val RouteColor = Color(0xFF7B06E1) val SpeedColor = Color(0xFF262525) 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 3de50f6..ee0e87f 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 @@ -16,13 +16,9 @@ package com.kouros.navigation.data -import android.location.Location -import android.location.LocationManager import android.net.Uri import com.kouros.navigation.data.route.Lane import com.kouros.navigation.utils.location -import io.objectbox.annotation.Entity -import io.objectbox.annotation.Id import kotlinx.serialization.Serializable data class Category( @@ -30,9 +26,13 @@ data class Category( val name: String, ) -@Entity + +data class Places( + val places: List, +) + +@Serializable data class Place( - @Id var id: Long = 0, var name: String? = null, var category: String? = null, @@ -42,8 +42,7 @@ data class Place( var city: String? = null, var street: String? = null, var distance: Float = 0F, - @Transient - var avatar: Uri? = null, + //var avatar: Uri? = null, var lastDate: Long = 0 ) diff --git a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt index 24219e2..2f5df1d 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt @@ -53,6 +53,8 @@ abstract class NavigationRepository { carOrientation: Float, context: Context ): Double { + if (currentLocation.latitude == 0.0) + return 0.0 val osrm = OsrmRepository() val route = osrm.getRoute(context, currentLocation, location, carOrientation, SearchFilter()) val gson = GsonBuilder().serializeNulls().create() diff --git a/common/data/src/main/java/com/kouros/navigation/data/NavigationState.kt b/common/data/src/main/java/com/kouros/navigation/data/NavigationState.kt index d924f08..483c399 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/NavigationState.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/NavigationState.kt @@ -18,4 +18,6 @@ data class NavigationState ( val currentRouteIndex: Int = 0, val destination: Place = Place(), val carConnection: Int = 0, + var routingEngine: Int = 0, + ) \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/ObjectBox.kt b/common/data/src/main/java/com/kouros/navigation/data/ObjectBox.kt deleted file mode 100644 index 888e53f..0000000 --- a/common/data/src/main/java/com/kouros/navigation/data/ObjectBox.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.kouros.navigation.data - -import android.content.Context -import com.kouros.navigation.data.MyObjectBox -import io.objectbox.BoxStore - -/** - * Singleton to keep BoxStore reference. - */ -object ObjectBox { - - lateinit var boxStore: BoxStore - private set - - fun init(context: Context) { - try { - boxStore = MyObjectBox.builder().androidContext(context.applicationContext).build() - } catch (e: Exception) { - println(e.message) - } - } -} \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt b/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt index ec97d57..a904885 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt @@ -42,6 +42,11 @@ class DataStoreManager(private val context: Context) { val LAST_ROUTE = stringPreferencesKey("LastRoute") val TOMTOM_APIKEY = stringPreferencesKey("TomTomApiKey") + + val RECENT_PLACES = stringPreferencesKey("RecentPlaces") + + val FAVORITES = stringPreferencesKey("Favorites") + } // Read values @@ -89,6 +94,18 @@ class DataStoreManager(private val context: Context) { ?: "" } + val recentPlacesFlow: Flow = + context.dataStore.data.map { preferences -> + preferences[PreferencesKeys.RECENT_PLACES] + ?: "" + } + + val favoritesFlow: Flow = + context.dataStore.data.map { preferences -> + preferences[PreferencesKeys.FAVORITES] + ?: "" + } + // Save values suspend fun setShow3D(enabled: Boolean) { context.dataStore.edit { preferences -> @@ -137,4 +154,16 @@ class DataStoreManager(private val context: Context) { prefs[PreferencesKeys.TOMTOM_APIKEY] = apiKey } } + + suspend fun setRecentPlaces(apiKey: String) { + context.dataStore.edit { prefs -> + prefs[PreferencesKeys.RECENT_PLACES] = apiKey + } + } + + suspend fun setFavorites(apiKey: String) { + context.dataStore.edit { prefs -> + prefs[PreferencesKeys.FAVORITES] = apiKey + } + } } diff --git a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt index 16cec5b..7ea1064 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt @@ -33,6 +33,13 @@ class TomTomRepository : NavigationRepository() { val routeJsonString = routeJson.bufferedReader().use { it.readText() } return routeJsonString } + var filter = "" + if (searchFilter.avoidMotorway) { + filter = "$filter&avoid=motorways" + } + if (searchFilter.avoidTollway) { + filter = "$filter&avoid=tollRoads" + } val repository = getSettingsRepository(context) val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() } val url = @@ -42,7 +49,7 @@ class TomTomRepository : NavigationRepository() { "&vehicleMaxSpeed=120&vehicleCommercial=false" + "&instructionsType=text&language=en-GB§ionType=lanes" + "&routeRepresentation=encodedPolyline" + - "&vehicleEngineType=combustion&key=$tomtomApiKey" + "&vehicleEngineType=combustion$filter&key=$tomtomApiKey" return fetchUrl( url, false 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 d751b48..ca53ba8 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 @@ -1,30 +1,27 @@ package com.kouros.navigation.model +//import com.kouros.navigation.data.Preferences.boxStore import android.content.Context import android.location.Location -import androidx.car.app.CarContext import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.platform.isDebugInspectorInfoEnabled import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.gson.GsonBuilder import com.kouros.navigation.data.Constants import com.kouros.navigation.data.NavigationRepository -import com.kouros.navigation.data.ObjectBox.boxStore import com.kouros.navigation.data.Place -import com.kouros.navigation.data.Place_ +import com.kouros.navigation.data.Places import com.kouros.navigation.data.SearchFilter import com.kouros.navigation.data.nominatim.Search import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Overpass import com.kouros.navigation.utils.Levenshtein -import com.kouros.navigation.utils.NavigationUtils import com.kouros.navigation.utils.getSettingsRepository -import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.location -import io.objectbox.kotlin.boxFor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -111,20 +108,18 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo /** - * Loads the most recent place from ObjectBox and calculates its distance. + * Loads the most recent place from Preferences 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 { - val placeBox = boxStore.boxFor(Place::class) - val query = placeBox - .query(Place_.name.notEqual("")) - .orderDesc(Place_.lastDate) - .build() - val results = query.find() - query.close() - for (place in results) { + val settingsRepository = getSettingsRepository(context) + val recentPlaces = settingsRepository.recentPlacesFlow.first() + val gson = GsonBuilder().serializeNulls().create() + val places = gson.fromJson(recentPlaces, Places::class.java) + val place = places.places.minByOrNull { it.lastDate.dec() } + if (place != null) { val plLocation = location(place.longitude, place.latitude) val distance = repository.getRouteDistance( location, @@ -145,33 +140,39 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo } /** - * Loads all recent places from ObjectBox and calculates distances. + * Loads all recent places from Preferences and calculates distances. * Posts the sorted list to places LiveData. */ fun loadRecentPlaces(context: Context, location: Location, carOrientation: Float) { viewModelScope.launch(Dispatchers.IO) { try { - val placeBox = boxStore.boxFor(Place::class) - val query = placeBox - .query(Place_.name.notEqual("").and(Place_.category.equal(Constants.RECENT))) - .orderDesc(Place_.lastDate) - .build() - val results = query.find() - query.close() - for (place in results) { - val plLocation = location(place.longitude, place.latitude) - if (place.latitude != 0.0) { - val distance = - repository.getRouteDistance( - location, - plLocation, - carOrientation, - context - ) - place.distance = distance.toFloat() + val settingsRepository = getSettingsRepository(context) + val rp = settingsRepository.recentPlacesFlow.first() + val gson = GsonBuilder().serializeNulls().create() + val recentPlaces = gson.fromJson(rp, Places::class.java) + val pl = mutableListOf() + var id : Long = 0 + if (rp.isNotEmpty()) { + for (place in recentPlaces.places) { + if (place.category.equals(Constants.RECENT)) { + val plLocation = location(place.longitude, place.latitude) + if (place.latitude != 0.0) { + val distance = + repository.getRouteDistance( + location, + plLocation, + carOrientation, + context + ) + place.distance = distance.toFloat() + place.id = id + id += 1 + } + pl.add(place) + } } } - places.postValue(results) + places.postValue(pl) } catch (e: Exception) { e.printStackTrace() } @@ -179,32 +180,36 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo } /** - * Loads favorite places from ObjectBox and calculates distances. + * Loads favorite places from Preferences and calculates distances. * Posts the sorted list to favorites LiveData. */ fun loadFavorites(context: Context, location: Location, carOrientation: Float) { viewModelScope.launch(Dispatchers.IO) { try { - val placeBox = boxStore.boxFor(Place::class) - val query = placeBox - .query(Place_.name.notEqual("").and(Place_.category.equal(Constants.FAVORITES))) - .orderDesc(Place_.lastDate) - .build() - val results = query.find() - query.close() - for (place in results) { - val plLocation = location(place.longitude, place.latitude) - val distance = - repository.getRouteDistance( - location, - plLocation, - carOrientation, - - context - ) - place.distance = distance.toFloat() + val settingsRepository = getSettingsRepository(context) + val rp = settingsRepository.recentPlacesFlow.first() + val gson = GsonBuilder().serializeNulls().create() + val recentPlaces = gson.fromJson(rp, Places::class.java) + val pl = mutableListOf() + if (rp.isNotEmpty()) { + for (place in recentPlaces.places) { + if (place.category.equals(Constants.FAVORITES)) { + val plLocation = location(place.longitude, place.latitude) + if (place.latitude != 0.0) { + val distance = + repository.getRouteDistance( + location, + plLocation, + carOrientation, + context + ) + place.distance = distance.toFloat() + } + pl.add(place) + } + } } - favorites.postValue(results) + favorites.postValue(pl) } catch (e: Exception) { e.printStackTrace() } @@ -335,7 +340,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo Constants.CONTACTS, street = addressLines[0], city = addressLines[1], - avatar = address.avatar, + //avatar = address.avatar, longitude = 0.0, latitude = 0.0, distance = 0F, @@ -417,7 +422,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo val distAmenities = mutableListOf() amenities.forEach { val plLocation = - location(longitude = it.lon!!, latitude = it.lat!!) + location(longitude = it.lon, latitude = it.lat) val distance = plLocation.distanceTo(location) it.distance = distance.toDouble() distAmenities.add(it) @@ -470,18 +475,18 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo } /** - * Saves a place as a favorite in ObjectBox. + * Saves a place as a favorite in Preferences. */ - fun saveFavorite(place: Place) { + fun saveFavorite(context: Context, place: Place) { place.category = Constants.FAVORITES - savePlace(place) + savePlace(context, place) } /** - * Saves a place to recent destinations in ObjectBox. + * Saves a place to recent destinations in Preferences. * Skips fuel stations, charging stations, and pharmacies. */ - fun saveRecent(place: Place) { + fun saveRecent(context: Context, place: Place) { if (place.category == Constants.FUEL_STATION || place.category == Constants.CHARGING_STATION || place.category == Constants.PHARMACY @@ -489,30 +494,36 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo return } place.category = Constants.RECENT - savePlace(place) + savePlace(context, place) } /** - * Saves a place to ObjectBox, removing existing duplicates first. + * Saves a place to Preferences, removing existing duplicates first. * Updates the timestamp to current time. */ - private fun savePlace(place: Place) { + private fun savePlace(context: Context, place: Place) { viewModelScope.launch(Dispatchers.IO) { try { - val placeBox = boxStore.boxFor(Place::class) - val query = placeBox - .query( - Place_.name.equal(place.name!!).and(Place_.category.equal(place.category!!)) - ) - .build() - val results = query.find() - query.close() - if (results.isNotEmpty()) { - placeBox.remove(results.first()) + val places = mutableListOf() + val gson = GsonBuilder().serializeNulls().create() + val settingsRepository = getSettingsRepository(context) + val rp = settingsRepository.recentPlacesFlow.first() + var id : Long = 0 + if (rp.isNotEmpty()) { + val recentPlaces = + gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate } + for (curPlace in recentPlaces) { + if (curPlace.name != place.name || curPlace.category != place.category) { + curPlace.id = id + places.add(curPlace) + id += 1 + } + } } val current = LocalDateTime.now(ZoneOffset.UTC) place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond() - placeBox.put(place) + places.add(place) + settingsRepository.setRecentPlaces(gson.toJson(Places(places))) } catch (e: Exception) { e.printStackTrace() } @@ -520,37 +531,40 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo } /** - * Deletes a place from favorites in ObjectBox. + * Deletes a place from favorites in Preferences. */ - fun deleteFavorite(place: Place) { + fun deleteFavorite(context: Context, place: Place) { place.category = Constants.FAVORITES - deletePlace(place) + deletePlace(context, place) } /** - * Deletes a place from recent destinations in ObjectBox. + * Deletes a place from recent destinations in Preferences. */ - fun deleteRecent(place: Place) { + fun deleteRecent(context: Context, place: Place) { place.category = Constants.RECENT - deletePlace(place) + deletePlace(context, place) } /** - * Deletes a place from ObjectBox matching name and category. + * Deletes a place from Preferences matching name and category. */ - fun deletePlace(place: Place) { + fun deletePlace(context: Context, place: Place) { viewModelScope.launch(Dispatchers.IO) { try { - val placeBox = boxStore.boxFor(Place::class) - val query = placeBox - .query( - Place_.name.equal(place.name!!).and(Place_.category.equal(place.category!!)) - ) - .build() - val results = query.find() - query.close() - if (results.isNotEmpty()) { - placeBox.remove(results.first()) + val gson = GsonBuilder().serializeNulls().create() + val settingsRepository = getSettingsRepository(context) + val rp = settingsRepository.recentPlacesFlow.first() + val places = mutableListOf() + if (rp.isNotEmpty()) { + val recentPlaces = + gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate } + for (curPlace in recentPlaces) { + if (curPlace.name != place.name || curPlace.category != place.category) { + places.add(curPlace) + } + } + settingsRepository.setRecentPlaces(gson.toJson(Places(places))) } } catch (e: Exception) { e.printStackTrace() @@ -569,59 +583,23 @@ 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, - carOrientation: Float - ): SnapshotStateList { - val results = listOf() - try { - val placeBox = boxStore.boxFor(Place::class) - val query = placeBox - .query(Place_.name.notEqual("").and(Place_.category.equal(Constants.RECENT))) - .orderDesc(Place_.lastDate) - .build() - val results = query.find() - query.close() - for (place in results) { - val plLocation = location(place.longitude, place.latitude) - val distance = - repository.getRouteDistance( - location, - plLocation, - carOrientation, - context - ) - place.distance = distance.toFloat() - } - } catch (e: Exception) { - e.printStackTrace() - } - return results.toMutableStateList() - } - /** * Loads recent places as Compose SnapshotStateList. * @return SnapshotStateList of recent places */ - fun loadRecentPlace(): SnapshotStateList { - val results = listOf() - try { - val placeBox = boxStore.boxFor(Place::class) - val query = placeBox - .query(Place_.name.notEqual("").and(Place_.category.equal(Constants.RECENT))) - .orderDesc(Place_.lastDate) - .build() - val results = query.find() - query.close() - return results.toMutableStateList() - } catch (e: Exception) { - e.printStackTrace() + fun loadRecentPlace(context: Context): SnapshotStateList { + val pl = mutableListOf() + val settingsRepository = getSettingsRepository(context) + val rp = runBlocking { settingsRepository.recentPlacesFlow.first()} + if (rp.isNotEmpty()) { + val gson = GsonBuilder().serializeNulls().create() + val recentPlaces = gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate } + for (place in recentPlaces) { + if (place.category == Constants.RECENT) { + pl.add(place) + } + } } - return results.toMutableStateList() + return pl.toMutableStateList() } } 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 474bb32..d8bc116 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 @@ -1,25 +1,17 @@ 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.NavigationState -import com.kouros.navigation.data.Place import com.kouros.navigation.data.Route import com.kouros.navigation.data.StepData -import com.kouros.navigation.data.datastore.DataStoreManager import com.kouros.navigation.data.route.Lane import com.kouros.navigation.data.route.Leg import com.kouros.navigation.data.route.Routes -import com.kouros.navigation.repository.SettingsRepository -import com.kouros.navigation.utils.getSettingsRepository -import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.location -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking import kotlin.math.absoluteValue open class RouteModel { @@ -37,40 +29,40 @@ open class RouteModel { val curLeg: Leg get() = navState.route.routes[navState.currentRouteIndex].legs.first() - fun startNavigation(routeString: String, context: Context) { - val repository = getSettingsRepository(context) - val routingEngine = runBlocking { repository.routingEngineFlow.first() } + fun startNavigation(routeString: String) { navState = navState.copy( route = Route.Builder() - .routeEngine(routingEngine) + .routeEngine(navState.routingEngine) .route(routeString) .build() ) if (hasLegs()) { navState = navState.copy(navigating = true) - getSettingsViewModel(context).onLastRouteChanged(routeString) } } - private fun hasLegs(): Boolean { + fun hasLegs(): Boolean { return navState.route.routes.isNotEmpty() && navState.route.routes[0].legs.isNotEmpty() } - fun stopNavigation(context: Context) { + fun stopNavigation() { navState = navState.copy( route = Route.Builder().buildEmpty(), navigating = false, arrived = false, maneuverType = Maneuver.TYPE_UNKNOWN ) - getSettingsViewModel(context).onLastRouteChanged("") } - fun updateLocation(context: Context, curLocation: Location, viewModel: NavigationViewModel) { + fun updateLocation(curLocation: Location, viewModel: NavigationViewModel) { + if (curLocation.hasBearing()) { + navState = navState.copy(routeBearing = curLocation.bearing) + } navState = navState.copy(currentLocation = curLocation) routeCalculator.findStep(curLocation) if (navState.carConnection == CONNECTION_TYPE_PROJECTION - || navState.carConnection == CONNECTION_TYPE_NATIVE) { + || navState.carConnection == CONNECTION_TYPE_NATIVE + ) { routeCalculator.updateSpeedLimit(curLocation, viewModel) } navState = navState.copy(lastLocation = navState.currentLocation) @@ -80,15 +72,11 @@ open class RouteModel { val distanceToNextStep = routeCalculator.leftStepDistance() // Determine the maneuver type and corresponding icon val currentStep = navState.route.nextStep(0) - val streetName = if (distanceToNextStep > NEXT_STEP_THRESHOLD) { - currentStep.street - } else { - currentStep.maneuver.street - } - val curManeuverType = if (distanceToNextStep > NEXT_STEP_THRESHOLD) { - Maneuver.TYPE_STRAIGHT - } else { - currentStep.maneuver.type + var streetName = currentStep.maneuver.street + var curManeuverType = currentStep.maneuver.type + if (distanceToNextStep > NEXT_STEP_THRESHOLD) { + streetName = currentStep.street + curManeuverType = Maneuver.TYPE_STRAIGHT } val exitNumber = currentStep.maneuver.exit val maneuverIcon = navState.iconMapper.maneuverIcon(curManeuverType) @@ -108,13 +96,14 @@ open class RouteModel { fun nextStep(): StepData { val distanceToNextStep = routeCalculator.leftStepDistance() - val step = navState.route.nextStep(1) - val streetName = if (distanceToNextStep < NEXT_STEP_THRESHOLD) { - step.maneuver.street - } else { - step.street + val currentStep = navState.route.nextStep(0) + val nextStep = navState.route.nextStep(1) + var streetName = nextStep.street + var maneuverType = currentStep.maneuver.type + if (distanceToNextStep < NEXT_STEP_THRESHOLD) { + streetName = nextStep.maneuver.street + maneuverType = nextStep.maneuver.type } - val maneuverType = step.maneuver.type val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType) // Construct and return the final StepData object @@ -125,7 +114,7 @@ open class RouteModel { icon = maneuverIcon, arrivalTime = routeCalculator.arrivalTime(), leftDistance = routeCalculator.travelLeftDistance(), - exitNumber = step.maneuver.exit + exitNumber = nextStep.maneuver.exit ) } @@ -138,7 +127,9 @@ open class RouteModel { navState.lastLocation.distanceTo(location(it.location[0], it.location[1])) val sectionBearing = navState.lastLocation.bearingTo(location(it.location[0], it.location[1])) - if (distance < NEXT_STEP_THRESHOLD && (navState.routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue < 10) { + val bearingDeviation = + (navState.routeBearing.absoluteValue - sectionBearing.absoluteValue).absoluteValue + if (distance < NEXT_STEP_THRESHOLD && bearingDeviation < 10) { lanes = it.lane } } diff --git a/common/data/src/main/java/com/kouros/navigation/repository/SettingsRepository.kt b/common/data/src/main/java/com/kouros/navigation/repository/SettingsRepository.kt index 0c1f864..815f2fb 100644 --- a/common/data/src/main/java/com/kouros/navigation/repository/SettingsRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/repository/SettingsRepository.kt @@ -29,6 +29,14 @@ class SettingsRepository( val tomTomApiKeyFlow: Flow = dataStoreManager.tomTomApiKeyFlow + + val recentPlacesFlow: Flow = + dataStoreManager.recentPlacesFlow + + val favoritesFlow: Flow = + dataStoreManager.favoritesFlow + + suspend fun setShow3D(enabled: Boolean) { dataStoreManager.setShow3D(enabled) } @@ -61,4 +69,11 @@ class SettingsRepository( dataStoreManager.setTomtomApiKey(apiKey) } + suspend fun setRecentPlaces(places: String) { + dataStoreManager.setRecentPlaces(places) + } + + suspend fun setFavorites(favorites: String) { + dataStoreManager.setFavorites(favorites) + } } \ No newline at end of file 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 6e63251..49eb74d 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 @@ -36,7 +36,6 @@ object GeoUtils { fun decodePolyline(encoded: String, precision: Int = 6): List> { val factor = 10.0.pow(precision) - var lat = 0 var lng = 0 val coordinates = mutableListOf>() diff --git a/common/data/src/test/java/com/kouros/navigation/model/RouteCalculatorTest.kt b/common/data/src/test/java/com/kouros/navigation/model/RouteCalculatorTest.kt new file mode 100644 index 0000000..80e1149 --- /dev/null +++ b/common/data/src/test/java/com/kouros/navigation/model/RouteCalculatorTest.kt @@ -0,0 +1,247 @@ +package com.kouros.navigation.model + +import android.location.Location +import com.kouros.navigation.data.Route +import com.kouros.navigation.data.route.Leg +import com.kouros.navigation.data.route.Maneuver +import com.kouros.navigation.data.route.Routes +import com.kouros.navigation.data.route.Step +import com.kouros.navigation.data.route.Summary +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class RouteCalculatorTest { + + private lateinit var routeModel: RouteModel + private lateinit var routeCalculator: RouteCalculator + + @Before + fun setup() { + routeModel = RouteModel() + routeCalculator = RouteCalculator(routeModel) + } + + // ---------------------------------------------------------- + // Helpers + // ---------------------------------------------------------- + + /** + * Creates a Step with [numWaypoints] evenly spaced waypoints. + * The Maneuver location is mocked to avoid Android stub issues. + */ + private fun createStep( + index: Int, + numWaypoints: Int = 2, + duration: Double = 60.0, + distance: Double = 200.0, + waypointIndex: Int = 0, + ): Step { + val waypoints = (0 until numWaypoints).map { i -> + listOf(11.0 + index * 0.01 + i * 0.001, 48.0) + } + return Step( + index = index, + waypointIndex = waypointIndex, + maneuver = Maneuver(waypoints = waypoints, location = mock()), + duration = duration, + distance = distance, + ) + } + + private fun setupRoute(steps: List, currentStepIndex: Int = 0): Route { + val leg = Leg(steps = steps) + val routes = Routes( + legs = listOf(leg), + summary = Summary(), + routeGeoJson = "", + waypoints = emptyList(), + ) + return Route(routeEngine = 1, routes = listOf(routes), currentStepIndex = currentStepIndex) + } + + // ---------------------------------------------------------- + // findStep + // ---------------------------------------------------------- + + @Test + fun `findStep updates currentStepIndex to step containing the nearest waypoint`() { + val step0 = createStep(index = 0, numWaypoints = 2) + val step1 = createStep(index = 1, numWaypoints = 2) + routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1))) + + val mockLocation: Location = mock() + // step0/wp0: 500F, step0/wp1: 400F, step1/wp0: 300F, step1/wp1: 8F + whenever(mockLocation.distanceTo(any())).thenReturn(500F, 400F, 300F, 8F) + + routeCalculator.findStep(mockLocation) + + assertEquals(1, routeModel.navState.route.currentStepIndex) + } + + @Test + fun `findStep updates waypointIndex to the nearest waypoint within the step`() { + val step0 = createStep(index = 0, numWaypoints = 3) + routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0))) + + val mockLocation: Location = mock() + // wp0: 100F, wp1: 30F (nearest), wp2: 80F + whenever(mockLocation.distanceTo(any())).thenReturn(100F, 30F, 80F) + + routeCalculator.findStep(mockLocation) + + assertEquals(1, step0.waypointIndex) + } + + @Test + fun `findStep skips all steps before currentStepIndex`() { + val step0 = createStep(index = 0, numWaypoints = 2) + val step1 = createStep(index = 1, numWaypoints = 2) + routeModel.navState = routeModel.navState.copy( + route = setupRoute(listOf(step0, step1), currentStepIndex = 1) + ) + + val mockLocation: Location = mock() + whenever(mockLocation.distanceTo(any())).thenReturn(200F, 50F) + + routeCalculator.findStep(mockLocation) + + // step0 is skipped, so distanceTo is only called for step1's 2 waypoints + verify(mockLocation, times(2)).distanceTo(any()) + assertEquals(1, routeModel.navState.route.currentStepIndex) + } + + @Test + fun `findStep breaks early once nearestDistance drops below NEAREST_LOCATION_DISTANCE`() { + val step0 = createStep(index = 0, numWaypoints = 2) + val step1 = createStep(index = 1, numWaypoints = 2) + val step2 = createStep(index = 2, numWaypoints = 2) + routeModel.navState = routeModel.navState.copy( + route = setupRoute(listOf(step0, step1, step2)) + ) + + val mockLocation: Location = mock() + // step0/wp0: 500F, step0/wp1: 5F — 5F < NEAREST_LOCATION_DISTANCE (10F) → break + whenever(mockLocation.distanceTo(any())).thenReturn(500F, 5F) + + routeCalculator.findStep(mockLocation) + + // step1 and step2 are never evaluated + verify(mockLocation, times(2)).distanceTo(any()) + assertEquals(0, routeModel.navState.route.currentStepIndex) + } + + // ---------------------------------------------------------- + // travelLeftTime + // ---------------------------------------------------------- + + @Test + fun `travelLeftTime sums future step durations plus full current step duration at first waypoint`() { + // waypointIndex=0, waypoints=2 → percent = 100*(2-0)/2 = 100 → time = 60s + val step0 = createStep(index = 0, numWaypoints = 2, duration = 60.0, waypointIndex = 0) + val step1 = createStep(index = 1, numWaypoints = 2, duration = 120.0) + val step2 = createStep(index = 2, numWaypoints = 2, duration = 90.0) + routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1, step2))) + + val result = routeCalculator.travelLeftTime() + + // future: 120 + 90 = 210; current: 60 → total: 270 + assertEquals(270.0, result, 0.01) + } + + @Test + fun `travelLeftTime uses proportional duration based on remaining waypoints in current step`() { + // waypointIndex=2, waypoints=4 → percent = 100*(4-2)/4 = 50 → time = 80*50/100 = 40s + val step0 = createStep(index = 0, numWaypoints = 4, duration = 80.0, waypointIndex = 2) + val step1 = createStep(index = 1, numWaypoints = 2, duration = 40.0) + routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1))) + + val result = routeCalculator.travelLeftTime() + + // future: 40; current: 40 → total: 80 + assertEquals(80.0, result, 0.01) + } + + @Test + fun `travelLeftTime returns only future steps when at last step`() { + val step0 = createStep(index = 0, numWaypoints = 2, duration = 60.0, waypointIndex = 1) + routeModel.navState = routeModel.navState.copy( + route = setupRoute(listOf(step0), currentStepIndex = 0) + ) + + val result = routeCalculator.travelLeftTime() + + // no future steps; waypointIndex=1, waypoints=2 → percent = 100*(2-1)/2 = 50 → 30s + assertEquals(30.0, result, 0.01) + } + + // ---------------------------------------------------------- + // leftStepDistance + // ---------------------------------------------------------- + + @Test + fun `leftStepDistance returns 0 when waypointIndex is at the last position`() { + // Loop range: waypointIndex.. before) + val expectedArrival = before + 3_600_000L + assertTrue( + "Arrival time should be within 1s of expected", + result in expectedArrival - 1_000..expectedArrival + 1_000 + ) + } +} diff --git a/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt b/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt new file mode 100644 index 0000000..12c5fb9 --- /dev/null +++ b/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt @@ -0,0 +1,109 @@ +package com.kouros.navigation.model + +import android.location.Location +import com.kouros.navigation.data.Route +import com.kouros.navigation.data.RouteEngine +import com.kouros.navigation.data.route.Leg +import com.kouros.navigation.data.route.Maneuver +import com.kouros.navigation.data.route.Routes +import com.kouros.navigation.data.route.Step +import com.kouros.navigation.data.route.Summary +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class RouteModelTest { + + val route = + "{\"formatVersion\":\"0.0.12\",\"report\":{\"effectiveSettings\":[{\"key\":\"avoid\",\"value\":\"unpavedRoads\"},{\"key\":\"computeBestOrder\",\"value\":\"false\"},{\"key\":\"computeTollAmounts\",\"value\":\"none\"},{\"key\":\"computeTravelTimeFor\",\"value\":\"none\"},{\"key\":\"contentType\",\"value\":\"json\"},{\"key\":\"departAt\",\"value\":\"2026-02-26T11:12:31.436Z\"},{\"key\":\"guidanceVersion\",\"value\":\"1\"},{\"key\":\"includeTollPaymentTypes\",\"value\":\"none\"},{\"key\":\"instructionsType\",\"value\":\"text\"},{\"key\":\"language\",\"value\":\"en-GB\"},{\"key\":\"locations\",\"value\":\"48.18575,11.57937:48.18440,11.58298\"},{\"key\":\"maxAlternatives\",\"value\":\"0\"},{\"key\":\"maxPathAlternatives\",\"value\":\"0\"},{\"key\":\"routeRepresentation\",\"value\":\"encodedPolyline\"},{\"key\":\"routeType\",\"value\":\"eco\"},{\"key\":\"sectionType\",\"value\":\"traffic\"},{\"key\":\"sectionType\",\"value\":\"lanes\"},{\"key\":\"traffic\",\"value\":\"true\"},{\"key\":\"travelMode\",\"value\":\"car\"},{\"key\":\"vehicleAxleWeight\",\"value\":\"0\"},{\"key\":\"vehicleCommercial\",\"value\":\"false\"},{\"key\":\"vehicleEngineType\",\"value\":\"combustion\"},{\"key\":\"vehicleHeading\",\"value\":\"90\"},{\"key\":\"vehicleHeight\",\"value\":\"0.00\"},{\"key\":\"vehicleLength\",\"value\":\"0.00\"},{\"key\":\"vehicleMaxSpeed\",\"value\":\"120\"},{\"key\":\"vehicleNumberOfAxles\",\"value\":\"0\"},{\"key\":\"vehicleWeight\",\"value\":\"0\"},{\"key\":\"vehicleWidth\",\"value\":\"0.00\"}]},\"routes\":[{\"summary\":{\"lengthInMeters\":904,\"travelTimeInSeconds\":183,\"trafficDelayInSeconds\":0,\"trafficLengthInMeters\":0,\"departureTime\":\"2026-02-26T12:12:31+01:00\",\"arrivalTime\":\"2026-02-26T12:15:34+01:00\"},\"legs\":[{\"summary\":{\"lengthInMeters\":904,\"travelTimeInSeconds\":183,\"trafficDelayInSeconds\":0,\"trafficLengthInMeters\":0,\"departureTime\":\"2026-02-26T12:12:31+01:00\",\"arrivalTime\":\"2026-02-26T12:15:34+01:00\"},\"encodedPolyline\":\"sfbeH_rteAE|DM|K?P@BBDBBLBdFRBqABw@FkDD_BD_CDkD@uA?oB?{@C_C?YA}@IiEY?\",\"encodedPolylinePrecision\":5}],\"guidance\":{\"instructions\":[{\"routeOffsetInMeters\":0,\"travelTimeInSeconds\":0,\"point\":{\"latitude\":48.18554,\"longitude\":11.57936},\"pointIndex\":0,\"instructionType\":\"LOCATION_DEPARTURE\",\"street\":\"Vogelhartstraße\",\"countryCode\":\"DEU\",\"possibleCombineWithNext\":false,\"drivingSide\":\"RIGHT\",\"maneuver\":\"DEPART\",\"message\":\"Leave from Vogelhartstraße\"},{\"routeOffsetInMeters\":375,\"travelTimeInSeconds\":87,\"point\":{\"latitude\":48.18437,\"longitude\":11.57606},\"pointIndex\":8,\"instructionType\":\"TURN\",\"street\":\"Milbertshofener Straße\",\"countryCode\":\"DEU\",\"junctionType\":\"REGULAR\",\"turnAngleInDecimalDegrees\":-90,\"possibleCombineWithNext\":false,\"drivingSide\":\"RIGHT\",\"maneuver\":\"TURN_LEFT\",\"message\":\"Turn left onto Milbertshofener Straße\"},{\"routeOffsetInMeters\":890,\"travelTimeInSeconds\":168,\"point\":{\"latitude\":48.18427,\"longitude\":11.58297},\"pointIndex\":21,\"instructionType\":\"TURN\",\"street\":\"Bad-Soden-Straße\",\"countryCode\":\"DEU\",\"junctionType\":\"REGULAR\",\"turnAngleInDecimalDegrees\":-90,\"possibleCombineWithNext\":true,\"drivingSide\":\"RIGHT\",\"maneuver\":\"TURN_LEFT\",\"message\":\"Turn left onto Bad-Soden-Straße\",\"combinedMessage\":\"Turn left onto Bad-Soden-Straße then you have arrived at Bad-Soden-Straße\"},{\"routeOffsetInMeters\":904,\"travelTimeInSeconds\":183,\"point\":{\"latitude\":48.1844,\"longitude\":11.58297},\"pointIndex\":22,\"instructionType\":\"LOCATION_ARRIVAL\",\"street\":\"Bad-Soden-Straße\",\"countryCode\":\"DEU\",\"possibleCombineWithNext\":false,\"drivingSide\":\"RIGHT\",\"maneuver\":\"ARRIVE\",\"message\":\"You have arrived at Bad-Soden-Straße\"}],\"instructionGroups\":[{\"firstInstructionIndex\":0,\"lastInstructionIndex\":3,\"groupMessage\":\"Leave from Vogelhartstraße. Take the Milbertshofener Straße. Continue to your destination at Bad-Soden-Straße\",\"groupLengthInMeters\":904}]}}]}" + private lateinit var routeModel: RouteModel + + @Before + fun setup() { + routeModel = RouteModel() + routeModel.navState = routeModel.navState.copy(routingEngine = RouteEngine.TOMTOM.ordinal) + routeModel.startNavigation(route) + } + + private fun createStep( + index: Int, + numWaypoints: Int = 2, + duration: Double = 60.0, + distance: Double = 200.0, + waypointIndex: Int = 0, + ): Step { + val waypoints = (0 until numWaypoints).map { i -> + listOf(11.0 + index * 0.01 + i * 0.001, 48.0) + } + return Step( + index = index, + waypointIndex = waypointIndex, + maneuver = Maneuver(waypoints = waypoints, location = mock()), + duration = duration, + distance = distance, + ) + } + + private fun setupRoute(steps: List, currentStepIndex: Int = 0): Route { + val leg = Leg(steps = steps) + val routes = Routes( + legs = listOf(leg), + summary = Summary(), + routeGeoJson = "", + waypoints = emptyList(), + ) + return Route(routeEngine = 2, routes = listOf(routes), currentStepIndex = currentStepIndex) + } + + @Test + fun `hasLegs returns true when route has legs`() { + val step0 = createStep(index = 0, numWaypoints = 2) + val step1 = createStep(index = 1, numWaypoints = 2) + routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1))) + val result = routeModel.hasLegs() + assert(result) + } + + @Test + fun `startNavigation updates route and sets navigating to true `() { + assert(routeModel.navState.navigating) + } + + @Test + fun `updateLocation updates currentLocation and lastLocation `() { + val routeCalculator: RouteCalculator = mock() + val mockLocation: Location = mock() + whenever(mockLocation.latitude).thenReturn(48.18554) + whenever(mockLocation.longitude).thenReturn(11.57936) + whenever(mockLocation.bearing).thenReturn(90F) + whenever(mockLocation.hasBearing()).thenReturn(true) + doNothing().`when`(routeCalculator).findStep(mockLocation) + routeModel.updateLocation(mockLocation, mock()) + assert(routeModel.navState.currentLocation.latitude == 48.18554) + assert(routeModel.navState.currentLocation.longitude == 11.57936) + } + + @Test + fun `currentStep returns StepData `() { + val stepData = routeModel.currentStep() + assert(stepData.leftStepDistance == 0.0) + assert(stepData.instruction == "Milbertshofener Straße") + } + + @Test + fun `nextStep returns StepData `() { + routeModel.currentStep() + val stepData = routeModel.nextStep() + assert(stepData.leftStepDistance == 0.0) + assert(stepData.instruction == "Bad-Soden-Straße") + } + + @Test + fun `stopNavigation updates route and sets navigating to false `() { + routeModel.stopNavigation() + assert(!routeModel.navState.navigating) + } +} \ No newline at end of file diff --git a/common/data/src/test/java/com/kouros/navigation/utils/GeoUtilsTest.kt b/common/data/src/test/java/com/kouros/navigation/utils/GeoUtilsTest.kt new file mode 100644 index 0000000..326270f --- /dev/null +++ b/common/data/src/test/java/com/kouros/navigation/utils/GeoUtilsTest.kt @@ -0,0 +1,205 @@ +package com.kouros.navigation.utils + +import android.location.Location +import org.junit.Assert.* +import org.junit.Test +import org.maplibre.geojson.Point +import org.maplibre.turf.TurfMisc +import org.mockito.Mockito.mock +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + + +class GeoUtilsTest { + private fun createLocation(lat: Double, lng: Double, bearing: Float? = null): Location { + val location = mock() + whenever(location.latitude).thenReturn(lat) + whenever(location.longitude).thenReturn(lng) + whenever(location.hasBearing()).thenReturn(bearing != null) + bearing?.let { whenever(location.bearing).thenReturn(it) } + + return location + } + + @Test + fun `snapLocation snaps to nearest point on line`() { + + val location = createLocation(48.0, 11.0) + val stepCoordinates = listOf( + Point.fromLngLat(11.0, 48.0), + Point.fromLngLat(11.001, 48.0), + Point.fromLngLat(11.002, 48.0) + ) + + + val result = GeoUtils.snapLocation(location, stepCoordinates) + assertEquals(0.0, result.latitude, 0.001) + assertEquals(0.0, result.longitude, 0.001) + } + + @Test + fun `snapLocation preserves bearing when available`() { + val location = createLocation(48.0, 11.0, bearing = 90f) + val stepCoordinates = listOf( + Point.fromLngLat(11.0, 48.0), + Point.fromLngLat(11.001, 48.0) + ) + + val result = GeoUtils.snapLocation(location, stepCoordinates) + + assertEquals(0f, result.bearing, 0.001f) + } + + @Test + fun `snapLocation returns original location when coordinates has less than 2 points`() { + val location = createLocation(48.0, 11.0) + val stepCoordinates = listOf(Point.fromLngLat(11.0, 48.0)) + + val result = GeoUtils.snapLocation(location, stepCoordinates) + + assertEquals(0.0, result.latitude, 0.001) + assertEquals(0.0, result.longitude, 0.001) + } + + @Test + fun `decodePolyline decodes simple polyline correctly`() { + val encoded = + "sfbeH_rteAE|DQEy@GQ?wDQFkEH{G?M?sA@kB?_FAeC?o@?[@_@\\Ab@CVAz@CJA@?dBGhAGjAExAGlBEvBKdCKTAfCKLAv@ELA|AGnAGt@ClCKjDQpBIf@BXDPDPBF@ZB?S@SAYAUKi@Go@Cc@BgBBs@Bg@JyAJiAXqBDWNs@TaA\\mAFa@Po@`BwF?YL]FSFSl@iB^kALc@L]Ro@f@yAf@{AFQNe@dAoCdBgEx@qBTi@BITe@L[L_@^}@HSXu@pB}El@eAb@e@f@[h@QZCRAL?j@HRFh@Vf@XLJhAn@lAv@TLtAz@r@`@bAn@pAv@f@X~@j@z@h@vBpA`@VHDFFJFf@X`CzApAh@f@L`@Fz@@f@AXEVEVEhA[h@Yn@e@PQFEJKRWV[PW`@w@t@}AHQN]~BiFP]`AoBh@aADGTa@t@aAt@{@PQJKJGFG@Cd@]XSxDmCf@a@n@o@TY\\g@LQHMJSLUP[f@iAPg@b@yAFODMNi@FS|@qCVaAHUHUn@wBHYh@eBpAkEjBiGfAeDj@yADMFQd@sAf@kAJUh@qAf@eAt@sAn@iALSN[p@kAVc@JOLSj@w@z@}@x@q@pAu@p@_@j@Sl@MLCRCb@E`@?^?L@`ABz@?N@~AFdADJ@rAH`DVpCVrAJd@BfHp@zGl@pAJ|ALnGp@jEh@fBJpAFF?P@N@ZCtC]r@GJCFCLCD?TEVEXGhAYzAg@NGv@]`@QJEvB_AXMVK\\Qb@Qn@QJCNAZC^ENA`@FnBb@xEtA^H^JnCl@z@r@`@Pr@TtBlA~C`Bn@\\xAl@PF`@LrAVlCh@bBLl@BlBJdG\\RDjCHn@?pB?xB?R@`@@GxAC^?ZInBIfCAvC?r@@dD@n@@b@@^D`C?TDxAFbBHdB@VHp@RjAJb@NNH`@VlBFf@PzARhBFd@@LRbBFh@\\nC@FNhAb@lEj@tDPpABTBRZlBTdBXjBn@xEBLDTRpAR~@l@jDj@Qv@IrEP" + val result = GeoUtils.decodePolyline(encoded, 5) + assertEquals(339, result.size) + + assertEquals(11.58204, result[10][0], 0.001) + assertEquals(48.18686, result[10][1], 0.001) + + assertEquals(11.59979, result[100][0], 0.001) + assertEquals(48.17076, result[100][1], 0.001) + + } + + @Test + fun `decodePolyline returns empty list for empty string`() { + val result = GeoUtils.decodePolyline("") + + assertTrue(result.isEmpty()) + } + + @Test + fun `createLineStringCollection creates valid GeoJSON`() { + val coordinates = listOf( + listOf(11.0, 48.0), + listOf(11.1, 48.1), + listOf(11.2, 48.2) + ) + + val result = GeoUtils.createLineStringCollection(coordinates) + + assertTrue(result.contains("LineString")) + assertTrue(result.contains("11.0")) + assertTrue(result.contains("48.0")) + assertTrue(result.contains("FeatureCollection")) + } + + @Test + fun `createPointCollection creates valid GeoJSON with category`() { + val coordinates = listOf( + listOf(11.0, 48.0), + listOf(11.1, 48.1) + ) + val category = "TestCategory" + + val result = GeoUtils.createPointCollection(coordinates, category) + + assertTrue(result.contains("Point")) + assertTrue(result.contains("TestCategory")) + assertTrue(result.contains("FeatureCollection")) + } + + @Test + fun `createPointCollection handles empty coordinates`() { + val coordinates = emptyList>() + + val result = GeoUtils.createPointCollection(coordinates, "Empty") + + assertTrue(result.contains("FeatureCollection")) + } + + @Test + fun `calculateSquareRadius returns correct bounding box`() { + val lat = 48.0 + val lng = 11.0 + val radius = 1.0 // 1 km + + val result = GeoUtils.calculateSquareRadius(lat, lng, radius) + + // Result should be in format: lngMin,latMin,lngMax,latMax + val parts = result.split(",") + assertEquals(4, parts.size) + + val lngMin = parts[0].toDouble() + val latMin = parts[1].toDouble() + val lngMax = parts[2].toDouble() + val latMax = parts[3].toDouble() + + assertTrue(latMin < lat) + assertTrue(latMax > lat) + assertTrue(lngMin < lng) + assertTrue(lngMax > lng) + assertTrue(latMax - latMin > 0) + assertTrue(lngMax - lngMin > 0) + } + + @Test + fun `getBoundingBox returns correct format`() { + val lat = 48.0 + val lon = 11.0 + val radius = 1.0 // 1 km + + val result = GeoUtils.getBoundingBox(lat, lon, radius) + + // Result should be in format: minLat,minLon,maxLat,maxLon + val parts = result.split(",") + assertEquals(4, parts.size) + + val minLat = parts[0].toDouble() + val minLon = parts[1].toDouble() + val maxLat = parts[2].toDouble() + val maxLon = parts[3].toDouble() + + assertTrue(minLat < lat) + assertTrue(maxLat > lat) + assertTrue(minLon < lon) + assertTrue(maxLon > lon) + } + + @Test + fun `calculateSquareRadius and getBoundingBox produce different formats`() { + val lat = 48.0 + val lng = 11.0 + val radius = 1.0 + + val squareRadius = GeoUtils.calculateSquareRadius(lat, lng, radius) + val boundingBox = GeoUtils.getBoundingBox(lat, lng, radius) + + // Both should have 4 comma-separated values + assertEquals(4, squareRadius.split(",").size) + assertEquals(4, boundingBox.split(",").size) + + // But the order should be different + assertNotEquals(squareRadius, boundingBox) + } + + @Test + fun `calculateSquareRadius scales with radius`() { + val lat = 48.0 + val lng = 11.0 + + val smallRadius = GeoUtils.calculateSquareRadius(lat, lng, 1.0) + val largeRadius = GeoUtils.calculateSquareRadius(lat, lng, 10.0) + + val smallParts = smallRadius.split(",").map { it.toDouble() } + val largeParts = largeRadius.split(",").map { it.toDouble() } + + // Larger radius should produce larger bounding box + assertTrue((largeParts[3] - largeParts[1]) > (smallParts[3] - smallParts[1])) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d7596e..8dec5b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,8 @@ [versions] agp = "9.0.1" +androidGpxParser = "2.3.1" androidSdkTurf = "6.0.1" +datastore = "1.2.0" gradle = "9.0.1" koinAndroid = "4.1.1" koinAndroidxCompose = "4.1.1" @@ -13,38 +15,45 @@ junitVersion = "1.3.0" espressoCore = "3.7.0" kotlinxSerializationJson = "1.10.0" lifecycleRuntimeKtx = "2.10.0" -composeBom = "2026.02.00" +composeBom = "2026.02.01" appcompat = "1.7.1" material = "1.13.0" carApp = "1.7.0" androidx-car = "1.7.0" -objectboxKotlin = "5.2.0" -objectboxProcessor = "5.2.0" -ui = "1.10.0" +materialIconsExtended = "1.7.8" +mockitoCore = "5.21.0" +mockitoKotlin = "6.2.3" +rules = "1.7.0" +runner = "1.7.0" material3 = "1.4.0" -runtimeLivedata = "1.10.3" -foundation = "1.10.3" -maplibre-composeMaterial3 = "0.12.2" +runtimeLivedata = "1.10.4" +foundation = "1.10.4" maplibre-compose = "0.12.1" playServicesLocation = "21.3.0" -runtime = "1.10.3" +runtime = "1.10.4" accompanist = "0.37.3" -uiVersion = "1.10.3" -uiText = "1.10.3" +uiVersion = "1.10.4" +uiText = "1.10.4" navigationCompose = "2.9.7" -uiToolingPreview = "1.10.3" -uiTooling = "1.10.3" +uiToolingPreview = "1.10.4" +uiTooling = "1.10.4" material3WindowSizeClass = "1.4.0" -uiGraphics = "1.10.3" +uiGraphics = "1.10.4" window = "1.5.1" -foundationLayout = "1.10.3" +foundationLayout = "1.10.4" datastorePreferences = "1.2.0" datastoreCore = "1.2.0" +monitor = "1.8.0" [libraries] +android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" } android-sdk-turf = { module = "org.maplibre.gl:android-sdk-turf", version.ref = "androidSdkTurf" } androidx-app-projected = { module = "androidx.car.app:app-projected" } +androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } +androidx-rules = { module = "androidx.test:rules", version.ref = "rules" } +androidx-runner = { module = "androidx.test:runner", version.ref = "runner" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } @@ -60,10 +69,9 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinCore" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" } -objectbox-kotlin = { module = "io.objectbox:objectbox-kotlin", version.ref = "objectboxKotlin" } -objectbox-processor = { module = "io.objectbox:objectbox-processor", version.ref = "objectboxProcessor" } +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" } +mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" } -maplibre-composeMaterial3 = { module = "org.maplibre.compose:maplibre-compose-material3", version = "maplibre-composeMaterial3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } @@ -82,10 +90,12 @@ androidx-window = { group = "androidx.window", name = "window", version.ref = "w androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" } +androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "monitor" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } android-library = { id = "com.android.library", version.ref = "agp" } - +kotlin-kapt = { id = "com.android.legacy-kapt", version.ref = "agp" } +android-protobuf = {id = "com.google.protobuf" } diff --git a/settings.gradle.kts b/settings.gradle.kts index a28c095..5dee68a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,14 @@ pluginManagement { mavenCentral() gradlePluginPortal() } + resolutionStrategy { + eachPlugin { + // Map the plugin ID to the Maven artifact + if (requested.id.id == "io.objectbox") { + useModule("io.objectbox:objectbox-gradle-plugin:${requested.version}") + } + } + } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)