This commit is contained in:
Dimitris
2026-03-21 12:24:53 +01:00
parent ada878b23c
commit d1968cfa68
45 changed files with 1121 additions and 935 deletions

View File

@@ -130,6 +130,8 @@ object Constants {
const val TRAFFIC_UPDATE = 300
const val ROUTE_UPDATE = 60
const val INSTRUCTION_DISTANCE = 50
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"

View File

@@ -1,29 +1,7 @@
/*
* Copyright 2023 Google LLC
*
* 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.data
import android.content.Context
import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.data.R
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.osrm.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import java.net.Authenticator
import java.net.HttpURLConnection
@@ -41,7 +19,7 @@ abstract class NavigationRepository {
abstract fun getRoute(
context: Context,
currentLocation: Location,
location: Location,
destination: Location,
carOrientation: Float,
searchFilter: SearchFilter
): String
@@ -59,7 +37,7 @@ abstract class NavigationRepository {
}
fun searchPlaces(search: String, location: Location): String {
val box = calculateSquareRadius(location.latitude, location.longitude, 100.0)
val box = calculateSquareRadius(location.latitude, location.longitude, 800.0)
val viewbox = "&bounded=1&viewbox=${box}"
return fetchUrl(
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",

View File

@@ -53,6 +53,8 @@ class DataStoreManager(private val context: Context) {
val TRAFFIC = booleanPreferencesKey("Traffic")
val TRIP_SUGGESTION = booleanPreferencesKey("TripSuggestion")
}
// Read values
@@ -129,6 +131,11 @@ class DataStoreManager(private val context: Context) {
preferences[PreferencesKeys.TRAFFIC] == true
}
val tripSuggestionFlow: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.TRIP_SUGGESTION] == true
}
// Save values
suspend fun setShow3D(enabled: Boolean) {
context.dataStore.edit { preferences ->
@@ -207,4 +214,10 @@ class DataStoreManager(private val context: Context) {
preferences[PreferencesKeys.TRAFFIC] = enabled
}
}
suspend fun setTripSuggestion(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.TRIP_SUGGESTION] = enabled
}
}
}

View File

@@ -12,4 +12,5 @@ data class Step(
val distance: Double = 0.0,
val street : String = "",
val intersection: List<Intersection> = mutableListOf(),
val countryCode : String = ""
)

View File

@@ -19,9 +19,9 @@ const val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incident
private const val tomtomFields =
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
const val useAsset = false
const val useLocal = false
const val useAssetTraffic = false
const val useLocalTraffic = false
class TomTomRepository : NavigationRepository() {
@@ -32,12 +32,11 @@ class TomTomRepository : NavigationRepository() {
carOrientation: Float,
searchFilter: SearchFilter
): String {
if (useAsset) {
val resourceId: Int = context.resources
.getIdentifier("tomtom_routing", "raw", context.packageName)
val routeJson = context.resources.openRawResource(resourceId)
val routeJsonString = routeJson.bufferedReader().use { it.readText() }
return routeJsonString
if (useLocal) {
return fetchUrl(
"https://kouros-online.de/tomtom_routing.json",
false
)
}
var filter = ""
if (searchFilter.avoidMotorway) {
@@ -76,11 +75,11 @@ class TomTomRepository : NavigationRepository() {
return ""
}
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
return if (useAssetTraffic) {
val resourceId: Int = context.resources
.getIdentifier("tomtom_traffic", "raw", context.packageName)
val trafficJson = context.resources.openRawResource(resourceId)
trafficJson.bufferedReader().use { it.readText() }
return if (useLocalTraffic) {
fetchUrl(
"https://kouros-online.de/tomtom_traffic.json",
false
)
} else {
val trafficResult = fetchUrl(
"$tomtomTrafficUrl?key=$tomtomApiKey&bbox=$bbox&fields=$tomtomFields&language=en-GB&timeValidityFilter=present",

View File

@@ -62,7 +62,9 @@ class TomTomRoute {
lastPointIndex = instruction.pointIndex
val intersections = mutableListOf<Intersection>()
route.sections?.forEach { section ->
if (section.sectionType == "LANES" && section.startPointIndex <= lastPointIndex && section.endPointIndex >= lastPointIndex) {
if (section.sectionType == "LANES" && section.startPointIndex <= lastPointIndex
&& section.endPointIndex >= lastPointIndex
) {
val lanes = mutableListOf<Lane>()
var startIndex = 0
var lastLane: Lane? = null
@@ -87,25 +89,25 @@ class TomTomRoute {
lastLane = lane
}
intersections.add(Intersection(waypoints[startIndex], lanes))
}
stepDistance =
route.guidance.instructions[index].routeOffsetInMeters - stepDistance
stepDuration =
route.guidance.instructions[index].travelTimeInSeconds - stepDuration
val step = Step(
index = stepIndex,
street = street,
distance = stepDistance,
duration = stepDuration,
maneuver = maneuver,
intersection = intersections
)
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
steps.add(step)
stepIndex += 1
}
stepDistance =
route.guidance.instructions[index].routeOffsetInMeters - stepDistance
stepDuration =
route.guidance.instructions[index].travelTimeInSeconds - stepDuration
val step = Step(
index = stepIndex,
street = street,
distance = stepDistance,
duration = stepDuration,
maneuver = maneuver,
intersection = intersections,
countryCode = lastInstruction.countryCode
)
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
steps.add(step)
stepIndex += 1
}
legs.add(Leg(steps))
val routeGeoJson = createLineStringCollection(waypoints)

View File

@@ -16,7 +16,7 @@ import com.kouros.navigation.data.StepData
import java.util.Collections
import java.util.Locale
class IconMapper() {
class IconMapper {
fun maneuverIcon(routeManeuverType: Int): Int {
var currentTurnIcon = R.drawable.ic_turn_name_change

View File

@@ -5,6 +5,8 @@ import android.content.Context
import android.location.Location
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -60,11 +62,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
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()
@@ -147,7 +144,8 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
var id: Long = 0
if (rp.isNotEmpty()) {
for (place in places.places) {
if (place.category.equals(Constants.RECENT)) {
if (place.category.equals(Constants.RECENT)
|| place.category.equals(Constants.FAVORITES)) {
val plLocation = location(place.longitude, place.latitude)
if (place.latitude != 0.0) {
val distance =
@@ -172,43 +170,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
/**
* 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 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(pl)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/**
* Calculates a route between current location and destination.
* Posts the route JSON to route LiveData.
@@ -216,7 +177,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
fun loadRoute(
context: Context,
currentLocation: Location,
location: Location,
destination: Location,
carOrientation: Float
) {
viewModelScope.launch(Dispatchers.IO) {
@@ -225,7 +186,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
repository.getRoute(
context,
currentLocation,
location,
destination,
carOrientation,
getSearchFilter(context)
)
@@ -296,7 +257,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
currentLocation: Location,
location: Location,
carOrientation: Float
) {
): String? {
viewModelScope.launch(Dispatchers.IO) {
try {
previewRoute.postValue(
@@ -312,8 +273,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
e.printStackTrace()
}
}
return previewRoute.value
}
/**
* Loads device contacts with addresses and converts to Place objects.
* Posts results to contactAddress LiveData.

View File

@@ -13,7 +13,7 @@ import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.data.route.Step
import com.kouros.navigation.utils.location
import kotlin.math.absoluteValue
open class RouteModel {
@@ -33,6 +33,9 @@ open class RouteModel {
val currentStep: Step
get() = navState.route.nextStep(0)
val steps: List<Step>
get() = curLeg.steps
fun startNavigation(routeString: String) {
navState = navState.copy(
route = Route.Builder()
@@ -145,7 +148,20 @@ open class RouteModel {
)
}
/**
* Checks for navigating
*/
fun isNavigating(): Boolean {
return navState.navigating
}
/**
* Checks for arrival
*/
fun isArrival(): Boolean {
return navState.maneuverType == Maneuver.TYPE_DESTINATION
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT
}
}

View File

@@ -90,6 +90,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
false
)
val tripSuggestion = repository.tripSuggestionFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
false
)
fun onShow3DChanged(enabled: Boolean) {
viewModelScope.launch { repository.setShow3D(enabled) }
}
@@ -138,4 +144,8 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
fun onTraffic(enabled: Boolean) {
viewModelScope.launch { repository.setTraffic(enabled) }
}
fun onTripSuggestion(enabled: Boolean) {
viewModelScope.launch { repository.setTripSuggestion(enabled) }
}
}

View File

@@ -44,6 +44,9 @@ class SettingsRepository(
val trafficFlow: Flow<Boolean> =
dataStoreManager.trafficFlow
val tripSuggestionFlow: Flow<Boolean> =
dataStoreManager.tripSuggestionFlow
suspend fun setShow3D(enabled: Boolean) {
dataStoreManager.setShow3D(enabled)
}
@@ -95,4 +98,8 @@ class SettingsRepository(
suspend fun setTraffic(enabled: Boolean) {
dataStoreManager.setTraffic(enabled)
}
suspend fun setTripSuggestion(enabled: Boolean) {
dataStoreManager.setTripSuggestion(enabled)
}
}

View File

@@ -0,0 +1,11 @@
<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"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M504,480L320,296L376,240L616,480L376,720L320,664L504,480Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<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="M70,880Q58,880 49,871.5Q40,863 40,850L40,517L128,308Q133,295 144,287.5Q155,280 169,280L552,280Q566,280 577,287.5Q588,295 593,308L680,517L680,850Q680,863 671.5,871.5Q663,880 650,880L609,880Q597,880 588,871.5Q579,863 579,850L579,800L141,800L141,850Q141,863 132.5,871.5Q124,880 111,880L70,880ZM130,458L591,458L542,340L179,340L130,458ZM246.5,664Q261,649 261,629Q261,608 246.5,593Q232,578 211,578Q190,578 175,593Q160,608 160,629Q160,649 175,664Q190,679 211,679Q232,679 246.5,664ZM545.5,664Q560,649 560,629Q560,608 545.5,593Q531,578 510,578Q489,578 474,593Q459,608 459,629Q459,649 474,664Q489,679 510,679Q531,679 545.5,664ZM740,757L740,413L659,220L237,220L251,188Q256,175 267,167.5Q278,160 292,160L669,160Q683,160 694.5,167.5Q706,175 711,188L800,401L800,727Q800,740 791.5,748.5Q783,757 770,757L740,757ZM860,634L860,290L780,100L360,100L374,68Q379,55 390,47.5Q401,40 415,40L790,40Q804,40 815.5,47.5Q827,55 832,68L920,278L920,604Q920,617 911.5,625.5Q903,634 890,634L860,634Z"/>
</vector>

View File

@@ -65,4 +65,5 @@
<string name="no_categories">Keine Kategorien</string>
<string name="general">Allgemein</string>
<string name="traffic">Verkehr anzeigen</string>
<string name="trip_suggestion">Fahrten-Vorschläge</string>
</resources>

View File

@@ -49,4 +49,5 @@
<string name="no_categories">Δεν υπάρχουν κατηγορίες</string>
<string name="general">Γενικά</string>
<string name="traffic">Εμφάνιση κίνησης</string>
<string name="trip_suggestion">Προτάσεις διαδρομής</string>
</resources>

View File

@@ -49,4 +49,5 @@
<string name="no_categories">Brak kategorii do wyświetlenia</string>
<string name="general">Ogólne</string>
<string name="traffic">Pokaż natężenie ruchu</string>
<string name="trip_suggestion">Sugestie dotyczące podróży</string>
</resources>

View File

@@ -52,4 +52,5 @@
<string name="no_categories">No categories to show</string>
<string name="general">General</string>
<string name="traffic">Show traffic</string>
<string name="trip_suggestion">Trip suggestions</string>
</resources>

View File

@@ -86,21 +86,6 @@ class RouteModelTest {
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()