Diverse Änderungen

This commit is contained in:
Dimitris
2026-02-24 16:29:13 +01:00
parent 71d3d17847
commit e4b539c4e6
31 changed files with 405 additions and 194 deletions

View File

@@ -124,7 +124,7 @@ object Constants {
val homeVogelhart = location(11.5793748, 48.185749)
val homeHohenwaldeck = location( 11.594322, 48.1164817)
const val NEXT_STEP_THRESHOLD = 500.0
const val NEXT_STEP_THRESHOLD = 300.0
const val MAXIMAL_SNAP_CORRECTION = 50.0

View File

@@ -33,59 +33,87 @@ import org.maplibre.geojson.FeatureCollection
import java.time.LocalDateTime
import java.time.ZoneOffset
/**
* ViewModel for navigation-related data operations.
* Handles route calculation, place search, traffic information, and local data persistence.
*/
class NavigationViewModel(private val repository: NavigationRepository) : ViewModel() {
/** LiveData containing the calculated route JSON string */
val route: MutableLiveData<String> by lazy {
MutableLiveData()
}
/** LiveData containing categorized traffic incidents map */
val traffic: MutableLiveData<Map<String, String>> by lazy {
MutableLiveData()
}
/** LiveData containing a preview route JSON string for route preview screens */
val previewRoute: MutableLiveData<String> by lazy {
MutableLiveData()
}
/** LiveData containing the most recent place used for navigation */
val recentPlace: MutableLiveData<Place> by lazy {
MutableLiveData()
}
/** LiveData containing list of recent navigation destinations */
val places: MutableLiveData<List<Place>> by lazy {
MutableLiveData()
}
/** LiveData containing list of favorite saved places */
val favorites: MutableLiveData<List<Place>> by lazy {
MutableLiveData()
}
/** LiveData containing search results from Nominatim geocoding */
val searchPlaces: MutableLiveData<List<SearchResult>> by lazy {
MutableLiveData()
}
/** LiveData containing the best matching place from address search */
val placeLocation: MutableLiveData<SearchResult> by lazy {
MutableLiveData()
}
/** LiveData containing contacts with addresses */
val contactAddress: MutableLiveData<List<Place>> by lazy {
MutableLiveData()
}
/** LiveData containing POI elements from Overpass API */
val elements: MutableLiveData<List<Elements>> by lazy {
MutableLiveData()
}
/** LiveData containing speed camera locations */
val speedCameras: MutableLiveData<List<Elements>> by lazy {
MutableLiveData()
}
/** LiveData containing current road speed limit */
val maxSpeed: MutableLiveData<Int> by lazy {
MutableLiveData()
}
/** LiveData containing selected routing engine index */
val routingEngine: MutableLiveData<Int> by lazy {
MutableLiveData()
}
/** LiveData indicating whether permission is granted */
val permissionGranted: MutableLiveData<Boolean> by lazy {
MutableLiveData()
}
/**
* Loads the most recent place from ObjectBox and calculates its distance.
* Posts the result to recentPlace LiveData if distance > 1km.
*/
fun loadRecentPlace(location: Location, carOrientation: Float, context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -116,6 +144,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Loads all recent places from ObjectBox and calculates distances.
* Posts the sorted list to places LiveData.
*/
fun loadRecentPlaces(context: Context, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -146,6 +178,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Loads favorite places from ObjectBox and calculates distances.
* Posts the sorted list to favorites LiveData.
*/
fun loadFavorites(context: Context, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -175,6 +211,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Calculates a route between current location and destination.
* Posts the route JSON to route LiveData.
*/
fun loadRoute(
context: Context,
currentLocation: Location,
@@ -198,6 +238,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Fetches traffic incident data and categorizes by severity.
* Posts categorized traffic map to traffic LiveData.
*/
fun loadTraffic(context: Context, currentLocation: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -216,6 +260,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Categorizes traffic incidents by type (queuing, stationary, slow, heavy, roadworks).
* @param data Raw traffic GeoJSON string
* @return Map of incident type to GeoJSON FeatureCollection
*/
private fun rebuildTraffic(data: String): Map<String, String> {
val featureCollection = FeatureCollection.fromJson(data)
val incidents = mutableMapOf<String, String>()
@@ -238,6 +287,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return incidents
}
/**
* Calculates a preview route for route preview screen.
* Posts the route JSON to previewRoute LiveData.
*/
fun loadPreviewRoute(
context: Context,
currentLocation: Location,
@@ -261,6 +314,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Loads device contacts with addresses and converts to Place objects.
* Posts results to contactAddress LiveData.
*/
fun loadContacts(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
val contactList = mutableListOf<Place>()
@@ -288,7 +345,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Finds the closest matching address for a search query.
* Posts the best result to placeLocation LiveData.
*/
fun findAddress(search: String, location: Location) {
var sortedList: List<SearchResult>
viewModelScope.launch(Dispatchers.IO) {
@@ -310,6 +370,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Searches for places using Nominatim geocoding API.
* Posts sorted results to searchPlaces LiveData.
*/
fun searchPlaces(search: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val placesJson = repository.searchPlaces(search, location)
@@ -330,6 +394,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Performs reverse geocoding to get street name from coordinates.
* @return Street name or empty string
*/
fun reverseAddress(location: Location): String {
val address = repository.reverseAddress(location)
val gson = GsonBuilder().serializeNulls().create()
@@ -337,6 +405,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return place.address.road
}
/**
* Queries Overpass API for nearby amenities of a specific category.
* Posts sorted results to elements LiveData.
*/
fun getAmenities(category: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("amenity", category, location, 5.0)
@@ -353,6 +425,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Queries Overpass API for speed cameras within a radius.
* Posts sorted results to speedCameras LiveData.
*/
fun getSpeedCameras(location: Location, radius: Double) {
viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
@@ -369,6 +445,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Queries Overpass API for speed limit on current road using fuzzy matching.
* Posts speed limit to maxSpeed LiveData.
*/
fun getMaxSpeed(location: Location, street: String) {
viewModelScope.launch(Dispatchers.IO) {
val levenshtein = Levenshtein()
@@ -387,11 +467,18 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Saves a place as a favorite in ObjectBox.
*/
fun saveFavorite(place: Place) {
place.category = Constants.FAVORITES
savePlace(place)
}
/**
* Saves a place to recent destinations in ObjectBox.
* Skips fuel stations, charging stations, and pharmacies.
*/
fun saveRecent(place: Place) {
if (place.category == Constants.FUEL_STATION
|| place.category == Constants.CHARGING_STATION
@@ -403,6 +490,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
savePlace(place)
}
/**
* Saves a place to ObjectBox, removing existing duplicates first.
* Updates the timestamp to current time.
*/
private fun savePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -426,16 +517,25 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Deletes a place from favorites in ObjectBox.
*/
fun deleteFavorite(place: Place) {
place.category = Constants.FAVORITES
deletePlace(place)
}
/**
* Deletes a place from recent destinations in ObjectBox.
*/
fun deleteRecent(place: Place) {
place.category = Constants.RECENT
deletePlace(place)
}
/**
* Deletes a place from ObjectBox matching name and category.
*/
fun deletePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -456,6 +556,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* Retrieves search filter settings from preferences.
* @return SearchFilter with avoid motorway/tollway flags
*/
fun getSearchFilter(context: Context): SearchFilter {
val repository = getSettingsRepository(context)
val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() }
@@ -463,7 +567,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return SearchFilter(avoidMotorway, avoidTollway)
}
/**
* Loads recent places with calculated distances for Compose state.
* @return SnapshotStateList of recent places with distances
*/
fun loadPlaces2(
context: Context,
location: Location,
@@ -495,6 +602,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return results.toMutableStateList()
}
/**
* Loads recent places as Compose SnapshotStateList.
* @return SnapshotStateList of recent places
*/
fun loadRecentPlace(): SnapshotStateList<Place?> {
val results = listOf<Place>()
try {
@@ -511,5 +622,4 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
return results.toMutableStateList()
}
}
}

View File

@@ -26,9 +26,9 @@ class RouteCalculator(var routeModel: RouteModel) {
routeModel.navState.route.currentStepIndex = step.index
step.waypointIndex = wayIndex
step.wayPointLocation = location(waypoint[0], waypoint[1])
routeModel.navState = routeModel.navState.copy(
routeBearing = routeModel.navState.lastLocation.bearingTo(location)
)
//routeModel.navState = routeModel.navState.copy(
// routeBearing = routeModel.navState.lastLocation.bearingTo(location)
// )
}
}
}

View File

@@ -2,6 +2,8 @@ package com.kouros.navigation.model
import android.content.Context
import android.location.Location
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
import androidx.car.app.navigation.model.Maneuver
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Place
@@ -33,7 +35,8 @@ open class RouteModel {
val currentLocation: Location = location(0.0, 0.0),
val routeBearing: Float = 0F,
val currentRouteIndex: Int = 0,
val destination: Place = Place()
val destination: Place = Place(),
val carConnection: Int = 0,
)
var navState = NavigationState()
@@ -81,9 +84,8 @@ open class RouteModel {
fun updateLocation(context: Context, curLocation: Location, viewModel: NavigationViewModel) {
navState = navState.copy(currentLocation = curLocation)
routeCalculator.findStep(curLocation)
val repository = getSettingsRepository(context)
val carLocation = runBlocking { repository.carLocationFlow.first() }
if (carLocation) {
if (navState.carConnection == CONNECTION_TYPE_PROJECTION
|| navState.carConnection == CONNECTION_TYPE_NATIVE) {
routeCalculator.updateSpeedLimit(curLocation, viewModel)
}
navState = navState.copy(lastLocation = navState.currentLocation)

View File

@@ -27,6 +27,9 @@ object GeoUtils {
val point = pointFeature.geometry() as Point
newLocation.latitude = point.latitude()
newLocation.longitude = point.longitude()
if (location.hasBearing()) {
newLocation.bearing = location.bearing
}
}
return newLocation
}

View File

@@ -52,7 +52,7 @@ fun calculateZoom(speed: Double?): Double {
}
fun previewZoom(previewDistance: Double): Double {
when (previewDistance) {
when (previewDistance / 1000) {
in 0.0..10.0 -> return 13.5
in 10.0..20.0 -> return 11.5
in 20.0..30.0 -> return 10.5

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.77,7.23l0.01,-0.01 -3.72,-3.72L15,4.56l2.11,2.11c-0.94,0.36 -1.61,1.26 -1.61,2.33 0,1.38 1.12,2.5 2.5,2.5 0.36,0 0.69,-0.08 1,-0.21v7.21c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1L17,14c0,-1.1 -0.9,-2 -2,-2h-1L14,5c0,-1.1 -0.9,-2 -2,-2L6,3c-1.1,0 -2,0.9 -2,2v16h10v-7.5h1.5v5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5L20.5,9c0,-0.69 -0.28,-1.32 -0.73,-1.77zM12,10L6,10L6,5h6v5zM18,10c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M160,840L160,200Q160,167 183.5,143.5Q207,120 240,120L480,120Q513,120 536.5,143.5Q560,167 560,200L560,480L600,480Q633,480 656.5,503.5Q680,527 680,560L680,740Q680,757 691.5,768.5Q703,780 720,780Q737,780 748.5,768.5Q760,757 760,740L760,452Q751,457 741,458.5Q731,460 720,460Q678,460 649,431Q620,402 620,360Q620,328 637.5,302.5Q655,277 684,266L600,182L642,140L790,284Q805,299 812.5,319Q820,339 820,360L820,740Q820,782 791,811Q762,840 720,840Q678,840 649,811Q620,782 620,740L620,540Q620,540 620,540Q620,540 620,540L560,540L560,840L160,840ZM240,400L480,400L480,200Q480,200 480,200Q480,200 480,200L240,200Q240,200 240,200Q240,200 240,200L240,400ZM720,400Q737,400 748.5,388.5Q760,377 760,360Q760,343 748.5,331.5Q737,320 720,320Q703,320 691.5,331.5Q680,343 680,360Q680,377 691.5,388.5Q703,400 720,400ZM240,760L480,760L480,480L240,480L240,760ZM480,760L240,760L240,760L480,760Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.63,468 649.31,482.69Q664,497.37 664,518L664,737Q664,758.68 679.5,773.34Q695,788 717,788Q739,788 754.5,773.34Q770,758.68 770,737L770,442Q759,448 747,451Q735,454 723,454Q683.52,454 656.26,426.74Q629,399.48 629,360Q629,328.39 647,303.19Q665,278 695,270L600,175L636,140L789,293Q803,307 811.5,323.5Q820,340 820,360L820,737Q820,780.26 790.18,810.13Q760.37,840 717.18,840Q674,840 644,810.13Q614,780.26 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM723,404Q741,404 754,391Q767,378 767,360Q767,342 754,329Q741,316 723,316Q705,316 692,329Q679,342 679,360Q679,378 692,391Q705,404 723,404ZM220,780L489,780L489,468L220,468L220,780ZM489,780L220,780L220,780L489,780Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,840L120,760L200,520L120,280L120,200L628,200L686,40L780,74L734,200L840,200L840,280L760,520L840,760L840,840L120,840ZM440,680L520,680L520,560L640,560L640,480L520,480L520,360L440,360L440,480L320,480L320,560L440,560L440,680ZM204,760L756,760L676,520L756,280L204,280L284,520L204,760ZM480,520L480,520L480,520L480,520L480,520L480,520Z"/>
</vector>

View File

@@ -50,5 +50,7 @@
<string name="use_car_location">Auto GPS verwenden</string>
<string name="tomtom">TomTom\t</string>
<string name="options">Optionen</string>
<string name="tomtom_api_key">TomTom ApiKey</string>
<string name="use_car_settings">Verwende Auto Einstellungen</string>
</resources>

View File

@@ -37,4 +37,5 @@
<string name="tomtom">TomTom\t</string>
<string name="options">Options</string>
<string name="tomtom_api_key">TomTom ApiKey</string>
<string name="use_car_settings">Use car settings</string>
</resources>