ApplicationConfig, Favorites
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code when working with code in this repository.
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is an Android navigation app built with Jetpack Compose that supports multiple routing providers (OSRM, Valhalla, TomTom) and includes Android Auto/Automotive OS integration. The app uses MapLibre for rendering, ObjectBox for local persistence, and Koin for dependency injection.
|
This is an Android navigation app built with Jetpack Compose that supports multiple routing providers (OSRM, Valhalla, TomTom) and includes Android Auto/Automotive OS integration. The app uses MapLibre for rendering, DataStore for local persistence, and Koin for dependency injection.
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
|
|||||||
@@ -125,16 +125,16 @@ class PlaceListScreen(
|
|||||||
/**
|
/**
|
||||||
* Creates an Action to navigate to a specific place.
|
* Creates an Action to navigate to a specific place.
|
||||||
*/
|
*/
|
||||||
private fun clickOnPlace(it: Place) {
|
private fun clickOnPlace(itPlace: Place) {
|
||||||
place = Place(
|
place = Place(
|
||||||
0,
|
0,
|
||||||
it.name,
|
itPlace.name,
|
||||||
it.category,
|
itPlace.category,
|
||||||
it.latitude,
|
itPlace.latitude,
|
||||||
it.longitude,
|
itPlace.longitude,
|
||||||
it.postalCode,
|
itPlace.postalCode,
|
||||||
it.city,
|
itPlace.city,
|
||||||
it.street,
|
itPlace.street,
|
||||||
// avatar = null
|
// avatar = null
|
||||||
)
|
)
|
||||||
if (surfaceRenderer.navigation) {
|
if (surfaceRenderer.navigation) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.kouros.navigation.car.screen
|
|||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
@@ -32,6 +33,8 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.kouros.data.R
|
import com.kouros.data.R
|
||||||
import com.kouros.navigation.car.SurfaceRenderer
|
import com.kouros.navigation.car.SurfaceRenderer
|
||||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||||
|
import com.kouros.navigation.data.Constants.FAVORITES
|
||||||
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
import com.kouros.navigation.data.Place
|
import com.kouros.navigation.data.Place
|
||||||
import com.kouros.navigation.data.ViewStyle
|
import com.kouros.navigation.data.ViewStyle
|
||||||
import com.kouros.navigation.data.route.Routes
|
import com.kouros.navigation.data.route.Routes
|
||||||
@@ -54,7 +57,7 @@ class RoutePreviewScreen(
|
|||||||
private var showAlternativeRoute: Boolean
|
private var showAlternativeRoute: Boolean
|
||||||
) :
|
) :
|
||||||
Screen(carContext) {
|
Screen(carContext) {
|
||||||
private var isFavorite = false
|
private var isFavorite = destination.favorite
|
||||||
|
|
||||||
val maxListItems: Int = 3
|
val maxListItems: Int = 3
|
||||||
|
|
||||||
@@ -90,6 +93,7 @@ class RoutePreviewScreen(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val trafficObserver = Observer<Map<String, String>> { traffic ->
|
val trafficObserver = Observer<Map<String, String>> { traffic ->
|
||||||
if (traffic.isNotEmpty()) {
|
if (traffic.isNotEmpty()) {
|
||||||
navigationViewModel.traffic.value = emptyMap()
|
navigationViewModel.traffic.value = emptyMap()
|
||||||
@@ -113,7 +117,6 @@ class RoutePreviewScreen(
|
|||||||
})
|
})
|
||||||
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
||||||
routingEngine = it
|
routingEngine = it
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||||
@@ -157,9 +160,6 @@ class RoutePreviewScreen(
|
|||||||
header.addEndHeaderAction(
|
header.addEndHeaderAction(
|
||||||
favoriteAction()
|
favoriteAction()
|
||||||
)
|
)
|
||||||
header.addEndHeaderAction(
|
|
||||||
deleteFavoriteAction()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
val message =
|
val message =
|
||||||
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
|
if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
|
||||||
@@ -261,34 +261,17 @@ class RoutePreviewScreen(
|
|||||||
else
|
else
|
||||||
R.drawable.ic_favorite_white_24dp
|
R.drawable.ic_favorite_white_24dp
|
||||||
, FLAG_IS_PERSISTENT,
|
, FLAG_IS_PERSISTENT,
|
||||||
) {
|
onClickAction = {
|
||||||
isFavorite = !isFavorite
|
isFavorite = !isFavorite
|
||||||
CarToast.makeText(
|
destination.favorite = isFavorite
|
||||||
carContext,
|
if (isFavorite) {
|
||||||
if (isFavorite)
|
navigationViewModel.saveFavorite(carContext, destination)
|
||||||
carContext
|
} else {
|
||||||
.getString(R.string.favorites)
|
navigationViewModel.deleteFavorite(carContext, destination)
|
||||||
else
|
}
|
||||||
carContext.getString(
|
invalidate()
|
||||||
R.string.favorites
|
|
||||||
),
|
|
||||||
CarToast.LENGTH_SHORT
|
|
||||||
)
|
|
||||||
.show()
|
|
||||||
navigationViewModel.saveFavorite(carContext, destination)
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun deleteFavoriteAction(): Action =
|
|
||||||
createAction(carContext, R.drawable.heart_minus_48px, FLAG_IS_PERSISTENT,{
|
|
||||||
if (isFavorite) {
|
|
||||||
navigationViewModel.deleteFavorite(carContext, destination)
|
|
||||||
}
|
}
|
||||||
isFavorite = !isFavorite
|
)
|
||||||
finish()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
private fun createRouteText(route: Routes): CarText {
|
private fun createRouteText(route: Routes): CarText {
|
||||||
val time = route.summary.duration
|
val time = route.summary.duration
|
||||||
|
|||||||
@@ -120,12 +120,12 @@ class NavigationScreenTest {
|
|||||||
|
|
||||||
`when`(mockRouteModel.isManeuverArrival()).thenReturn(true)
|
`when`(mockRouteModel.isManeuverArrival()).thenReturn(true)
|
||||||
`when`(mockRouteModel.routeCalculator).thenReturn(mockRouteCalculator)
|
`when`(mockRouteModel.routeCalculator).thenReturn(mockRouteCalculator)
|
||||||
`when`(mockRouteCalculator.leftStepDistance()).thenReturn(19.0)
|
`when`(mockRouteCalculator.leftStepDistance()).thenReturn(9.0)
|
||||||
`when`(mockRouteModel.navState).thenReturn(NavigationState())
|
`when`(mockRouteModel.navState).thenReturn(NavigationState())
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
//navigationScreen.checkArrival()
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.ARRIVAL)
|
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.NAVIGATION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -98,6 +98,9 @@ class CategoryObserverTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createElement(lon: Double, lat: Double): Elements {
|
private fun createElement(lon: Double, lat: Double): Elements {
|
||||||
return Elements(lon = lon, lat = lat, tags = Tags())
|
return Elements(
|
||||||
|
lon = lon, lat = lat,
|
||||||
|
tags = Tags(maxspeed = "0", direction = ""),
|
||||||
|
bounds = com.kouros.navigation.data.overpass.Bounds(0.0, 0.0, 0.0, 0.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -123,7 +123,8 @@ class ObserversTest {
|
|||||||
return Elements(
|
return Elements(
|
||||||
lon = lon,
|
lon = lon,
|
||||||
lat = lat,
|
lat = lat,
|
||||||
tags = Tags(maxspeed = maxSpeed, direction = null)
|
tags = Tags(maxspeed = maxSpeed, direction = ""),
|
||||||
|
bounds = com.kouros.navigation.data.overpass.Bounds(0.0, 0.0, 0.0, 0.0),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import com.android.build.gradle.internal.tasks.AarMetadataReader.Companion.load
|
||||||
|
import java.util.Properties
|
||||||
|
import kotlin.apply
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.library)
|
alias(libs.plugins.android.library)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
@@ -10,10 +14,21 @@ android {
|
|||||||
namespace = "com.kouros.data"
|
namespace = "com.kouros.data"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
||||||
|
|
||||||
|
val properties = Properties().apply {
|
||||||
|
val localPropertiesFile = project.rootProject.file("local.properties")
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
load(localPropertiesFile.inputStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 33
|
minSdk = 33
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
consumerProguardFiles("consumer-rules.pro")
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
|
||||||
|
buildConfigField("String", "USER", "\"${properties.getProperty("USER") ?: ""}\"")
|
||||||
|
buildConfigField("String", "PASSWORD", "\"${properties.getProperty("PASSWORD") ?: ""}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.kouros.navigation.data
|
package com.kouros.navigation.data
|
||||||
|
|
||||||
import android.util.Log
|
import com.kouros.data.BuildConfig
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
data class ApplicationConfig(
|
data class ApplicationConfig(
|
||||||
val user: String,
|
val user: String,
|
||||||
@@ -10,35 +9,10 @@ data class ApplicationConfig(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun load(): ApplicationConfig {
|
fun load(): ApplicationConfig {
|
||||||
|
|
||||||
fun Map<String, String>.envOrLookup(key: String): String {
|
|
||||||
return System.getenv(key) ?: this[key]!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val envVars: Map<String, String> = envFile().let { envFile ->
|
|
||||||
if (envFile.exists()) {
|
|
||||||
envFile.readLines()
|
|
||||||
.map { it.split("=") }
|
|
||||||
.filter { it.size == 2 }
|
|
||||||
.associate { it.first().trim() to it.last().trim() }
|
|
||||||
} else emptyMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApplicationConfig(
|
return ApplicationConfig(
|
||||||
user = envVars.envOrLookup("USER"),
|
user = BuildConfig.USER,
|
||||||
password = envVars.envOrLookup("PASSWORD"),
|
password = BuildConfig.PASSWORD
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun envFile(): File {
|
|
||||||
|
|
||||||
//val path = context.filesDir.absolutePath
|
|
||||||
//val config = ApplicationConfig.load(context)
|
|
||||||
|
|
||||||
return listOf(".env").map {
|
|
||||||
Log.d("Overpass", it)
|
|
||||||
File(it)
|
|
||||||
}.first { it.exists() }
|
|
||||||
}
|
|
||||||
@@ -55,6 +55,8 @@ data class Place(
|
|||||||
var route: String = "",
|
var route: String = "",
|
||||||
@Transient
|
@Transient
|
||||||
var stopOver: Boolean = false,
|
var stopOver: Boolean = false,
|
||||||
|
@Transient
|
||||||
|
var favorite: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ContactData(
|
data class ContactData(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.net.URL
|
|||||||
|
|
||||||
abstract class NavigationRepository {
|
abstract class NavigationRepository {
|
||||||
|
|
||||||
|
private val config by lazy { ApplicationConfig.load() }
|
||||||
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
|
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
|
||||||
|
|
||||||
//private val nominatimUrl = "https://kouros-online.de/nominatim/"
|
//private val nominatimUrl = "https://kouros-online.de/nominatim/"
|
||||||
@@ -61,11 +62,15 @@ abstract class NavigationRepository {
|
|||||||
try {
|
try {
|
||||||
if (authenticator) {
|
if (authenticator) {
|
||||||
Authenticator.setDefault(object : Authenticator() {
|
Authenticator.setDefault(object : Authenticator() {
|
||||||
override fun getPasswordAuthentication(): PasswordAuthentication {
|
override fun getPasswordAuthentication(): PasswordAuthentication? {
|
||||||
return PasswordAuthentication(
|
return if (config.user.isEmpty() || config.password.isEmpty()) {
|
||||||
"kouros",
|
null
|
||||||
"eo7sbjyWpmjSVFyELgbfrryqJ6ddNeq9".toCharArray()
|
} else {
|
||||||
)
|
PasswordAuthentication(
|
||||||
|
config.user,
|
||||||
|
config.password.toCharArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.kouros.navigation.data.overpass
|
|||||||
|
|
||||||
data class Elements(
|
data class Elements(
|
||||||
val bounds: Bounds,
|
val bounds: Bounds,
|
||||||
val geometry: List<Geometry>,
|
val geometry: List<Geometry> = emptyList(),
|
||||||
val id: Long = 0,
|
val id: Long = 0,
|
||||||
val lat: Double= 0.0,
|
val lat: Double= 0.0,
|
||||||
val lon: Double = 0.0,
|
val lon: Double = 0.0,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import java.net.URL
|
|||||||
|
|
||||||
class Overpass {
|
class Overpass {
|
||||||
|
|
||||||
|
private val config by lazy { com.kouros.navigation.data.ApplicationConfig.load() }
|
||||||
private val gson = GsonBuilder().serializeNulls().create()
|
private val gson = GsonBuilder().serializeNulls().create()
|
||||||
|
|
||||||
var overpassUrl = if (BuildConfig.DEBUG)
|
var overpassUrl = if (BuildConfig.DEBUG)
|
||||||
@@ -121,11 +122,15 @@ class Overpass {
|
|||||||
|
|
||||||
return try {
|
return try {
|
||||||
Authenticator.setDefault(object : Authenticator() {
|
Authenticator.setDefault(object : Authenticator() {
|
||||||
override fun getPasswordAuthentication(): PasswordAuthentication {
|
override fun getPasswordAuthentication(): PasswordAuthentication? {
|
||||||
return PasswordAuthentication(
|
return if (config.user.isEmpty() || config.password.isEmpty()) {
|
||||||
"kouros",
|
null
|
||||||
"eo7sbjyWpmjSVFyELgbfrryqJ6ddNeq9".toCharArray()
|
} else {
|
||||||
)
|
PasswordAuthentication(
|
||||||
|
config.user,
|
||||||
|
config.password.toCharArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.kouros.navigation.data.Constants
|
import com.kouros.navigation.data.Constants
|
||||||
|
import com.kouros.navigation.data.Constants.FAVORITES
|
||||||
import com.kouros.navigation.data.Constants.SPEED_BEARING_DEVIATION
|
import com.kouros.navigation.data.Constants.SPEED_BEARING_DEVIATION
|
||||||
import com.kouros.navigation.data.Constants.SPEED_UPDATE_DISTANCE
|
import com.kouros.navigation.data.Constants.SPEED_UPDATE_DISTANCE
|
||||||
import com.kouros.navigation.data.Constants.TAG
|
import com.kouros.navigation.data.Constants.TAG
|
||||||
@@ -163,8 +164,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
if (rp.isNotEmpty()) {
|
if (rp.isNotEmpty()) {
|
||||||
for (place in places.places) {
|
for (place in places.places) {
|
||||||
if (place.category == Constants.RECENT
|
if (place.category == Constants.RECENT
|
||||||
|| place.category == Constants.FAVORITES
|
|| place.category == FAVORITES
|
||||||
) {
|
) {
|
||||||
|
if (place.category == FAVORITES) {
|
||||||
|
place.favorite = true
|
||||||
|
}
|
||||||
val plLocation = location(place.longitude, place.latitude)
|
val plLocation = location(place.longitude, place.latitude)
|
||||||
if (place.latitude != 0.0) {
|
if (place.latitude != 0.0) {
|
||||||
val distance =
|
val distance =
|
||||||
@@ -399,11 +403,16 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
* Queries Overpass API for nearby amenities of a specific category.
|
* Queries Overpass API for nearby amenities of a specific category.
|
||||||
* Posts sorted results to elements LiveData.
|
* Posts sorted results to elements LiveData.
|
||||||
*/
|
*/
|
||||||
fun getAmenities(carContext: Context, category: String, location: Location, lastFuelUpdate: Long = 0) {
|
fun getAmenities(
|
||||||
|
carContext: Context,
|
||||||
|
category: String,
|
||||||
|
location: Location,
|
||||||
|
lastFuelUpdate: Long = 0
|
||||||
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val repository = getSettingsRepository(carContext)
|
val repository = getSettingsRepository(carContext)
|
||||||
val amenities = Overpass().getAmenities("amenity", category, location, 5.0)
|
val amenities = Overpass().getAmenities("amenity", category, location, 5.0)
|
||||||
val fuelPrices = fuelStations(category, lastFuelUpdate, location, repository)
|
val fuelPrices = fuelStations(category, lastFuelUpdate, location, repository)
|
||||||
val distAmenities = mutableListOf<Elements>()
|
val distAmenities = mutableListOf<Elements>()
|
||||||
amenities.forEach {
|
amenities.forEach {
|
||||||
val plLocation =
|
val plLocation =
|
||||||
@@ -594,7 +603,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
val stations =
|
val stations =
|
||||||
FuelPrices().getFuelPrices(location, 3)
|
FuelPrices().getFuelPrices(location, 3)
|
||||||
Log.d(TAG, "FuelPrices $stations")
|
Log.d(TAG, "FuelPrices $stations")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -649,6 +658,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
|
place.lastDate = current.atZone(ZoneOffset.UTC).toEpochSecond()
|
||||||
place.route = ""
|
place.route = ""
|
||||||
places.add(place)
|
places.add(place)
|
||||||
|
recentPlaces.postValue(places)
|
||||||
settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
|
settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -683,7 +693,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
|
settingsRepository.setRecentPlaces(gson.toJson(Places(places)))
|
||||||
recentPlaces.value = places
|
recentPlaces.postValue(places)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ class RouteCalculator(var routeModel: RouteModel) {
|
|||||||
fun findStep(location: Location) {
|
fun findStep(location: Location) {
|
||||||
val route = routeModel.navState.route
|
val route = routeModel.navState.route
|
||||||
val steps = routeModel.curLeg.steps
|
val steps = routeModel.curLeg.steps
|
||||||
val startIndex = route.currentStepIndex
|
// Search from one step prior to handle deviations/U-turns
|
||||||
|
val startIndex = (route.currentStepIndex - 1).coerceAtLeast(0)
|
||||||
// Windowed search for performance (current + 2 steps)
|
|
||||||
//val endIndex = (startIndex + 2).coerceAtMost(steps.size - 1)
|
|
||||||
val endIndex = steps.size - 1
|
val endIndex = steps.size - 1
|
||||||
|
|
||||||
var nearestDistance = MAXIMUM_LOCATION_DISTANCE
|
var nearestDistance = MAXIMUM_LOCATION_DISTANCE
|
||||||
@@ -35,7 +33,8 @@ class RouteCalculator(var routeModel: RouteModel) {
|
|||||||
for (i in startIndex..endIndex) {
|
for (i in startIndex..endIndex) {
|
||||||
val step = steps[i]
|
val step = steps[i]
|
||||||
val waypoints = step.maneuver.waypoints
|
val waypoints = step.maneuver.waypoints
|
||||||
val startWayIndex = if (i == startIndex) step.waypointIndex else 0
|
// Only offset search if we are exactly on the current stored step index
|
||||||
|
val startWayIndex = if (i == route.currentStepIndex) step.waypointIndex else 0
|
||||||
|
|
||||||
var lastDistance = Float.MAX_VALUE
|
var lastDistance = Float.MAX_VALUE
|
||||||
var increaseCount = 0
|
var increaseCount = 0
|
||||||
@@ -50,13 +49,12 @@ class RouteCalculator(var routeModel: RouteModel) {
|
|||||||
bestMatch = StepMatch(step.index, j, waypoint)
|
bestMatch = StepMatch(step.index, j, waypoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track if we are getting further away
|
|
||||||
if (distance > lastDistance) {
|
if (distance > lastDistance) {
|
||||||
increaseCount++
|
increaseCount++
|
||||||
} else {
|
} else {
|
||||||
increaseCount = 0
|
increaseCount = 0
|
||||||
}
|
}
|
||||||
// Absolute early exit: found a close point and now moved far away
|
|
||||||
if (stopSearch(nearestDistance, distance, increaseCount)) {
|
if (stopSearch(nearestDistance, distance, increaseCount)) {
|
||||||
bestMatch?.let { match ->
|
bestMatch?.let { match ->
|
||||||
route.currentStepIndex = match.stepIndex
|
route.currentStepIndex = match.stepIndex
|
||||||
@@ -73,14 +71,14 @@ class RouteCalculator(var routeModel: RouteModel) {
|
|||||||
route.currentStepIndex = match.stepIndex
|
route.currentStepIndex = match.stepIndex
|
||||||
steps[match.stepIndex].waypointIndex = match.waypointIndex
|
steps[match.stepIndex].waypointIndex = match.waypointIndex
|
||||||
steps[match.stepIndex].wayPointLocation = match.location
|
steps[match.stepIndex].wayPointLocation = match.location
|
||||||
|
|
||||||
}
|
}
|
||||||
Log.d(TAG, "FindStep not optimized: count=$searchCount, dist=$nearestDistance")
|
Log.d(TAG, "FindStep not optimized: count=$searchCount, dist=$nearestDistance")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopSearch(nearestDistance: Float, distance: Float, increaseCount: Int): Boolean {
|
private fun stopSearch(nearestDistance: Float, distance: Float, increaseCount: Int): Boolean {
|
||||||
return (nearestDistance < NEAREST_LOCATION_DISTANCE && distance > NEAREST_LOCATION_DISTANCE * 10)
|
// Increased multiplier from 10 to 20 to be less aggressive in sharp curves
|
||||||
|| increaseCount > 10
|
return (nearestDistance < NEAREST_LOCATION_DISTANCE && distance > NEAREST_LOCATION_DISTANCE * 20)
|
||||||
|
|| increaseCount > 15
|
||||||
}
|
}
|
||||||
|
|
||||||
fun travelLeftTime(): Double {
|
fun travelLeftTime(): Double {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class RouteCalculatorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `findStep skips all steps before currentStepIndex`() {
|
fun `findStep considers previous step when searching`() {
|
||||||
val step0 = createStep(index = 0, numWaypoints = 2)
|
val step0 = createStep(index = 0, numWaypoints = 2)
|
||||||
val step1 = createStep(index = 1, numWaypoints = 2)
|
val step1 = createStep(index = 1, numWaypoints = 2)
|
||||||
routeModel.navState = routeModel.navState.copy(
|
routeModel.navState = routeModel.navState.copy(
|
||||||
@@ -109,17 +109,17 @@ class RouteCalculatorTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val mockLocation: Location = mock()
|
val mockLocation: Location = mock()
|
||||||
whenever(mockLocation.distanceTo(any())).thenReturn(200F, 50F)
|
// Distance to step0 waypoints is very small, distance to step1 waypoints is large
|
||||||
|
whenever(mockLocation.distanceTo(any())).thenReturn(5F, 5F, 500F, 500F)
|
||||||
|
|
||||||
routeCalculator.findStep(mockLocation)
|
routeCalculator.findStep(mockLocation)
|
||||||
|
|
||||||
// step0 is skipped, so distanceTo is only called for step1's 2 waypoints
|
assertEquals(0, routeModel.navState.route.currentStepIndex)
|
||||||
verify(mockLocation, times(2)).distanceTo(any())
|
|
||||||
assertEquals(1, routeModel.navState.route.currentStepIndex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `findStep breaks early once nearestDistance drops below NEAREST_LOCATION_DISTANCE`() {
|
fun `findStep breaks later with relaxed distance threshold`() {
|
||||||
val step0 = createStep(index = 0, numWaypoints = 2)
|
val step0 = createStep(index = 0, numWaypoints = 2)
|
||||||
val step1 = createStep(index = 1, numWaypoints = 2)
|
val step1 = createStep(index = 1, numWaypoints = 2)
|
||||||
val step2 = createStep(index = 2, numWaypoints = 2)
|
val step2 = createStep(index = 2, numWaypoints = 2)
|
||||||
@@ -128,17 +128,18 @@ class RouteCalculatorTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val mockLocation: Location = mock()
|
val mockLocation: Location = mock()
|
||||||
// step0/wp0: 500F, step0/wp1: 5F, step1/wp0: 200F
|
// step0/wp0: 500F, step0/wp1: 5F, step1/wp0: 150F, step1/wp1: 160F, step2/wp0: 210F, step2/wp1: 220F
|
||||||
// 5F < NEAREST_LOCATION_DISTANCE (10F) AND 200F > 10 * 10F → break
|
// Here we purposefully exceed the 20 * 10F threshold at the end of step 1 or start of step 2
|
||||||
whenever(mockLocation.distanceTo(any())).thenReturn(500F, 5F, 200F)
|
whenever(mockLocation.distanceTo(any())).thenReturn(500F, 5F, 150F, 160F, 210F, 220F)
|
||||||
|
|
||||||
routeCalculator.findStep(mockLocation)
|
routeCalculator.findStep(mockLocation)
|
||||||
|
|
||||||
// should stop after the 3rd call (step1/wp0 triggers stopSearch)
|
// It should have’Checked step 0 (2), step 1 (2), and the first point of step 2 (1) where it finally breaks.
|
||||||
verify(mockLocation, times(3)).distanceTo(any())
|
verify(mockLocation, times(5)).distanceTo(any())
|
||||||
assertEquals(0, routeModel.navState.route.currentStepIndex)
|
assertEquals(0, routeModel.navState.route.currentStepIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// travelLeftTime
|
// travelLeftTime
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user