Testing, Remove ObjectBox

This commit is contained in:
Dimitris
2026-02-28 13:10:48 +01:00
parent eb6d3e4ef7
commit a468529ca4
35 changed files with 1148 additions and 431 deletions
@@ -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&sectionType=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>>()