Testing, Remove ObjectBox
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<StepData> by lazy {
|
||||
MutableLiveData()
|
||||
@@ -95,7 +98,13 @@ class MainActivity : ComponentActivity() {
|
||||
var lastLocation = location(0.0, 0.0)
|
||||
val observer = Observer<String> { 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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -116,7 +116,7 @@ fun MapLibre(
|
||||
|
||||
@Composable
|
||||
fun RouteLayer(routeData: String?, trafficData: Map<String, String>) {
|
||||
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<String, String>) {
|
||||
|
||||
@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<ColorValue> {
|
||||
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,
|
||||
|
||||
@@ -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<String>(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()
|
||||
|
||||
@@ -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<String> { route ->
|
||||
var lastCameraSearch = 0
|
||||
var speedCameras = listOf<Elements>()
|
||||
|
||||
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<Place> { lastPlace ->
|
||||
if (!routeModel.isNavigating()) {
|
||||
recentPlace = lastPlace
|
||||
navigationType = NavigationType.RECENT
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
val trafficObserver = Observer<Map<String, String>> { 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> { 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<String, String>) {
|
||||
surfaceRenderer.setTrafficData(traffic)
|
||||
}
|
||||
|
||||
override fun onPlaceSearchResultReceived(place: Place) {
|
||||
navigateToPlace(place)
|
||||
}
|
||||
|
||||
var lastCameraSearch = 0
|
||||
|
||||
var speedCameras = listOf<Elements>()
|
||||
val speedObserver = Observer<List<Elements>> { cameras ->
|
||||
override fun onSpeedCamerasReceived(cameras: List<Elements>) {
|
||||
speedCameras = cameras
|
||||
val coordinates = mutableListOf<List<Double>>()
|
||||
cameras.forEach {
|
||||
@@ -112,21 +124,12 @@ class NavigationScreen(
|
||||
surfaceRenderer.speedCamerasData.value = speedData
|
||||
}
|
||||
|
||||
val maxSpeedObserver = Observer<Int> { 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String> { 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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<Place>,
|
||||
)
|
||||
|
||||
@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
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -18,4 +18,6 @@ data class NavigationState (
|
||||
val currentRouteIndex: Int = 0,
|
||||
val destination: Place = Place(),
|
||||
val carConnection: Int = 0,
|
||||
var routingEngine: Int = 0,
|
||||
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.RECENT_PLACES]
|
||||
?: ""
|
||||
}
|
||||
|
||||
val favoritesFlow: Flow<String> =
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Place>()
|
||||
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<Place>()
|
||||
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<Elements>()
|
||||
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<Place>()
|
||||
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<Place>()
|
||||
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<Place?> {
|
||||
val results = listOf<Place>()
|
||||
try {
|
||||
val placeBox = boxStore.boxFor(Place::class)
|
||||
val query = placeBox
|
||||
.query(Place_.name.notEqual("").and(Place_.category.equal(Constants.RECENT)))
|
||||
.orderDesc(Place_.lastDate)
|
||||
.build()
|
||||
val results = query.find()
|
||||
query.close()
|
||||
for (place in results) {
|
||||
val plLocation = location(place.longitude, place.latitude)
|
||||
val distance =
|
||||
repository.getRouteDistance(
|
||||
location,
|
||||
plLocation,
|
||||
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<Place?> {
|
||||
val results = listOf<Place>()
|
||||
try {
|
||||
val placeBox = boxStore.boxFor(Place::class)
|
||||
val query = placeBox
|
||||
.query(Place_.name.notEqual("").and(Place_.category.equal(Constants.RECENT)))
|
||||
.orderDesc(Place_.lastDate)
|
||||
.build()
|
||||
val results = query.find()
|
||||
query.close()
|
||||
return results.toMutableStateList()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
fun loadRecentPlace(context: Context): SnapshotStateList<Place?> {
|
||||
val pl = mutableListOf<Place>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ class SettingsRepository(
|
||||
val tomTomApiKeyFlow: Flow<String> =
|
||||
dataStoreManager.tomTomApiKeyFlow
|
||||
|
||||
|
||||
val recentPlacesFlow: Flow<String> =
|
||||
dataStoreManager.recentPlacesFlow
|
||||
|
||||
val favoritesFlow: Flow<String> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,6 @@ object GeoUtils {
|
||||
|
||||
fun decodePolyline(encoded: String, precision: Int = 6): List<List<Double>> {
|
||||
val factor = 10.0.pow(precision)
|
||||
|
||||
var lat = 0
|
||||
var lng = 0
|
||||
val coordinates = mutableListOf<List<Double>>()
|
||||
|
||||
@@ -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<Step>, 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..<waypoints.size-1 = 2..<2, which is empty
|
||||
val step0 = createStep(index = 0, numWaypoints = 3, waypointIndex = 2)
|
||||
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0)))
|
||||
|
||||
val result = routeCalculator.leftStepDistance()
|
||||
|
||||
assertEquals(0.0, result, 0.01)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// travelLeftDistance
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun `travelLeftDistance sums distances of all future steps plus leftStepDistance`() {
|
||||
// step0 at last waypoint → leftStepDistance = 0
|
||||
val step0 = createStep(index = 0, numWaypoints = 2, distance = 100.0, waypointIndex = 1)
|
||||
val step1 = createStep(index = 1, numWaypoints = 2, distance = 200.0)
|
||||
val step2 = createStep(index = 2, numWaypoints = 2, distance = 150.0)
|
||||
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0, step1, step2)))
|
||||
|
||||
val result = routeCalculator.travelLeftDistance()
|
||||
|
||||
// 200 + 150 + 0 = 350
|
||||
assertEquals(350.0, result, 0.01)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `travelLeftDistance returns 0 when on last step at last waypoint`() {
|
||||
val step0 = createStep(index = 0, numWaypoints = 2, distance = 200.0, waypointIndex = 1)
|
||||
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0)))
|
||||
|
||||
val result = routeCalculator.travelLeftDistance()
|
||||
|
||||
assertEquals(0.0, result, 0.01)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// arrivalTime
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun `arrivalTime returns a timestamp roughly travelLeftTime seconds in the future`() {
|
||||
// step0: 2 waypoints at wp0 → 100% of 3600s duration
|
||||
val step0 = createStep(index = 0, numWaypoints = 2, duration = 3600.0, waypointIndex = 0)
|
||||
routeModel.navState = routeModel.navState.copy(route = setupRoute(listOf(step0)))
|
||||
|
||||
val before = System.currentTimeMillis()
|
||||
val result = routeCalculator.arrivalTime()
|
||||
|
||||
assertTrue("Arrival time should be in the future", result > before)
|
||||
val expectedArrival = before + 3_600_000L
|
||||
assertTrue(
|
||||
"Arrival time should be within 1s of expected",
|
||||
result in expectedArrival - 1_000..expectedArrival + 1_000
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<Step>, 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)
|
||||
}
|
||||
}
|
||||
@@ -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<Location>()
|
||||
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<List<Double>>()
|
||||
|
||||
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]))
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user