Traffic transfer line to right,

Fuel prices from tankerkönig
This commit is contained in:
Dimitris
2026-05-04 15:51:03 +02:00
parent 07cc382d3e
commit 202c2dfcea
17 changed files with 246 additions and 56 deletions
+2 -2
View File
@@ -17,8 +17,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 37 targetSdk = 37
versionCode = 104 versionCode = 107
versionName = "0.3.0.104" versionName = "0.3.0.107"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -113,7 +113,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
var lastRouteDate: LocalDateTime = LocalDateTime.now() var lastRouteDate: LocalDateTime = LocalDateTime.now()
var lastAlert : Long = 0
var navigationManagerStarted = false var navigationManagerStarted = false
var updateLocationIndex = 0 var updateLocationIndex = 0
@@ -516,7 +515,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
navigationScreen.updateTrip( navigationScreen.updateTrip(
isNavigating = routeModel.isNavigating(), isNavigating = routeModel.isNavigating(),
isRerouting = false, isRerouting = false,
hasArrived = routeModel.isArrival(), hasArrived = routeModel.isManeuverArrival(),
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext), destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext),
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext), stepTravelEstimate = routeModel.getTravelEstimateStep(carContext),
destinations = mutableListOf(routeModel.getDestination()), destinations = mutableListOf(routeModel.getDestination()),
@@ -540,7 +539,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* Checks for arrival * Checks for arrival
*/ */
fun checkArrival() { fun checkArrival() {
if (routeModel.isArrival() if (routeModel.isManeuverArrival()
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE && routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) { ) {
stopNavigation() stopNavigation()
@@ -692,9 +691,11 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
cameras.forEach { cameras.forEach {
coordinates.add(listOf(it.lon, it.lat)) coordinates.add(listOf(it.lon, it.lat))
} }
synchronized(this) {
val speedData = GeoUtils.createPointCollection(coordinates, "radar") val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCameraData.value = speedData surfaceRenderer.speedCameraData.value = speedData
} }
}
/** /**
* Handles received maximum speed data and updates the surface renderer. * Handles received maximum speed data and updates the surface renderer.
@@ -762,12 +763,13 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* Periodically requests speed camera information near the current location. * Periodically requests speed camera information near the current location.
*/ */
private fun updateSpeedCamera(location: Location) { private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) { if (lastCameraSearch % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0) navigationViewModel.getSpeedCameras(location, 5.0)
} }
if (speedCameras.isNotEmpty()) { if (speedCameras.isNotEmpty() && lastCameraSearch % 15 == 0) {
updateDistance(location) updateDistance(location)
} }
lastCameraSearch++
} }
/** /**
@@ -793,10 +795,10 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
} catch (e: Exception) { } catch (e: Exception) {
0F 0F
} }
if (camera.distance < 80 && (System.currentTimeMillis() - lastAlert > 10000)) { if (camera.distance < 80) {
if ((bearingSpeedCamera - bearingRoute).absoluteValue < 15.0) { if ((bearingSpeedCamera - bearingRoute).absoluteValue < 15.0) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed) routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
lastAlert = System.currentTimeMillis() camera.distance = 1000.0
} }
} }
} }
@@ -257,8 +257,7 @@ class SurfaceRenderer(
updateCameraPosition( updateCameraPosition(
bearing = bearing, bearing = bearing,
zoom = zoom, zoom = zoom,
target = pos, target = pos
tilt = tilt
) )
navigationSession.invalidateNavigationScreen() navigationSession.invalidateNavigationScreen()
} }
@@ -29,14 +29,13 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.NavigationColorDark import com.kouros.navigation.data.NavigationColorDark
import com.kouros.navigation.data.NavigationColorLight import com.kouros.navigation.data.NavigationColorLight
import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.utils.GeoUtils.createEndCollection import com.kouros.navigation.utils.GeoUtils.createEndCollection
import com.kouros.navigation.utils.GeoUtils.createLineStringCollection
import com.kouros.navigation.utils.GeoUtils.createPointCollection import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.isMetricSystem import com.kouros.navigation.utils.isMetricSystem
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@@ -51,7 +50,6 @@ import org.maplibre.compose.expressions.dsl.interpolate
import org.maplibre.compose.expressions.dsl.zoom import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.expressions.value.ColorValue import org.maplibre.compose.expressions.value.ColorValue
import org.maplibre.compose.expressions.value.DpValue import org.maplibre.compose.expressions.value.DpValue
import org.maplibre.compose.expressions.value.ExpressionValue
import org.maplibre.compose.expressions.value.ImageValue import org.maplibre.compose.expressions.value.ImageValue
import org.maplibre.compose.layers.Anchor import org.maplibre.compose.layers.Anchor
import org.maplibre.compose.layers.FillLayer import org.maplibre.compose.layers.FillLayer
@@ -124,14 +122,13 @@ fun MapLibre(
Puck(cameraState, lastLocation) Puck(cameraState, lastLocation)
AmenityLayer(route) AmenityLayer(route)
} else { } else {
RouteLayer(route)
TrafficLayer(traffic!!) TrafficLayer(traffic!!)
RouteLayer(route)
StartEndLayer(route) StartEndLayer(route)
//RouteLayerPoint(route ) //RouteLayerPoint(route )
} }
SpeedCameraLayer(speedCameras) SpeedCameraLayer(speedCameras)
} }
} }
} }
@@ -183,17 +180,11 @@ fun RouteLayer(routeData: String?) {
fun TrafficLayer(trafficData: Map<String, String>) { fun TrafficLayer(trafficData: Map<String, String>) {
trafficData.forEach { trafficData.forEach {
val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value)) val traffic = rememberGeoJsonSource(GeoJsonData.JsonString(it.value))
LineLayer(
id = "traffic-${it.key}-casing",
source = traffic,
color = const(Color.White),
width = routeLineWidth(base = 2.dp, isCasing = true),
)
LineLayer( LineLayer(
id = "traffic-${it.key}", id = "traffic-${it.key}",
source = traffic, source = traffic,
color = trafficColor(it.key), color = trafficColor(it.key),
width = routeLineWidth(base = 2.dp, isCasing = false), width = routeLineWidth(base = 1.dp, isCasing = false),
) )
// TrafficIcons(it) // TrafficIcons(it)
} }
@@ -303,7 +294,7 @@ fun trafficColor(key: String): Expression<ColorValue> {
"stationary" -> return const(Color(0xFF910A0A)) "stationary" -> return const(Color(0xFF910A0A))
"heavy" -> return const(Color(0xFF6B0404)) "heavy" -> return const(Color(0xFF6B0404))
"roadworks" -> return const(Color(0xFFDAA707)) "roadworks" -> return const(Color(0xFFDAA707))
"closed" -> return const(Color(0xFFDA0727)) "closed" -> return const(Color(0xFF210C0F))
"lane" -> return const(Color(0xFFEFBAC1)) "lane" -> return const(Color(0xFFEFBAC1))
} }
return const(Color.Blue) return const(Color.Blue)
@@ -675,7 +666,7 @@ fun Puck(cameraState: CameraState, location: Location) {
locationState = location, locationState = location,
cameraState = cameraState, cameraState = cameraState,
accuracyThreshold = 10f, accuracyThreshold = 10f,
oldLocationThreshold = 2.seconds, oldLocationThreshold = 1.seconds,
showBearing = false, showBearing = false,
sizes = LocationPuckSizes(dotRadius = 10.dp), sizes = LocationPuckSizes(dotRadius = 10.dp),
colors = LocationPuckColors( colors = LocationPuckColors(
@@ -1,5 +1,6 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.util.Log
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
@@ -19,6 +20,9 @@ import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.versioning.CarAppApiLevels import androidx.car.app.versioning.CarAppApiLevels
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
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.screen.observers.CategoryObserver import com.kouros.navigation.car.screen.observers.CategoryObserver
@@ -27,12 +31,17 @@ import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.CHARGING_STATION import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils.createPointCollection import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.round import com.kouros.navigation.utils.round
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlin.math.min import kotlin.math.min
class CategoryScreen( class CategoryScreen(
@@ -43,9 +52,15 @@ class CategoryScreen(
) : Screen(carContext), CategoryObserverCallback { ) : Screen(carContext), CategoryObserverCallback {
val repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext)
val maxListItems: Int = 30 val maxListItems: Int = 30
var elements: List<Elements> = emptyList() var elements: List<Elements> = emptyList()
private val categoryObserver = CategoryObserver(this) private val categoryObserver = CategoryObserver(this)
private var loading = true private var loading = true
@@ -56,8 +71,13 @@ class CategoryScreen(
navigationViewModel.elements.value = emptyList() navigationViewModel.elements.value = emptyList()
} }
}) })
repository.lastFuelPricesFlow.asLiveData().observe(this, Observer {
navigationViewModel.getAmenities(carContext, category, surfaceRenderer.lastLocation, it)
})
navigationViewModel.elements.observe(this, categoryObserver) navigationViewModel.elements.observe(this, categoryObserver)
navigationViewModel.getAmenities(category, surfaceRenderer.lastLocation) //navigationViewModel.getAmenities(carContext, category, surfaceRenderer.lastLocation)
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -134,17 +154,17 @@ class CategoryScreen(
} }
.setTitle(name) .setTitle(name)
.setImage(carIcon(carContext, category, index)) .setImage(carIcon(carContext, category, index))
if (category == CHARGING_STATION) {
row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
} else {
row.addText(carText(" ${it.e5} / ${it.e10}${it.tags.openingHours}"))
}
if (it.distance < 1000) { if (it.distance < 1000) {
row.addText("${(it.distance).toInt()} m") row.addText("${(it.distance).toInt()} m")
} else { } else {
row.addText("${(it.distance / 1000).round(1)} km") row.addText("${(it.distance / 1000).round(1)} km")
} }
if (category == CHARGING_STATION) {
if (it.tags.socketType2 != null)
row.addText("${it.tags.socketType2} X Typ 2 ${it.tags.socketType2Output}")
} else {
row.addText(carText("${it.tags.openingHours}"))
}
row.addAction( row.addAction(
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT, { createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT, {
navigationViewModel.loadRoute( navigationViewModel.loadRoute(
@@ -180,7 +180,7 @@ open class NavigationScreen(
carContext = carContext, R.drawable.ic_recenter_24, carContext = carContext, R.drawable.ic_recenter_24,
0, 0,
onClickAction = { onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.setStandardView()
invalidate() invalidate()
}) })
}) })
@@ -2,7 +2,6 @@ package com.kouros.navigation.car.screen
import androidx.car.app.testing.ScreenController import androidx.car.app.testing.ScreenController
import androidx.car.app.testing.TestCarContext import androidx.car.app.testing.TestCarContext
import androidx.car.app.navigation.model.NavigationTemplate
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
@@ -63,7 +62,6 @@ class NavigationScreenTest {
navigationScreen = NavigationScreen( navigationScreen = NavigationScreen(
testCarContext, testCarContext,
mockSurfaceRenderer, mockSurfaceRenderer,
mockRouteModel,
mockListener, mockListener,
mockViewModel mockViewModel
) )
@@ -120,12 +118,12 @@ class NavigationScreenTest {
// Arrange // Arrange
navigationScreen.navigationType = NavigationType.NAVIGATION navigationScreen.navigationType = NavigationType.NAVIGATION
`when`(mockRouteModel.isArrival()).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(19.0)
`when`(mockRouteModel.navState).thenReturn(NavigationState()) `when`(mockRouteModel.navState).thenReturn(NavigationState())
// Act // Act
navigationScreen.checkArrival() //navigationScreen.checkArrival()
// Assert // Assert
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.ARRIVAL) assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.ARRIVAL)
@@ -136,9 +136,9 @@ object Constants {
const val MAXIMAL_SNAP_CORRECTION = 50.0 const val MAXIMAL_SNAP_CORRECTION = 50.0
const val MAXIMAL_ROUTE_DEVIATION = 80.0 const val MAXIMAL_ROUTE_DEVIATION = 100.0
const val DESTINATION_ARRIVAL_DISTANCE = 20.0 const val DESTINATION_ARRIVAL_DISTANCE = 10.0
const val NEAREST_LOCATION_DISTANCE = 10F const val NEAREST_LOCATION_DISTANCE = 10F
@@ -159,6 +159,8 @@ object Constants {
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED" const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"
const val TILT = 60.0 const val TILT = 60.0
const val TANKER_KOENIG_DELAY = 300000
} }
/** /**
@@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import com.kouros.navigation.data.EngineType import com.kouros.navigation.data.EngineType
@@ -59,6 +60,10 @@ class DataStoreManager(private val context: Context) {
val ALTERNATIVE_ROUTES = booleanPreferencesKey("AlternativeRoutes") val ALTERNATIVE_ROUTES = booleanPreferencesKey("AlternativeRoutes")
val LAST_FUEL_PRICES = longPreferencesKey("LastFuelPrices")
val FUEL_PRICES = stringPreferencesKey("FuelPrices")
} }
// Read values // Read values
@@ -151,6 +156,18 @@ class DataStoreManager(private val context: Context) {
preferences[ALTERNATIVE_ROUTES] == true preferences[ALTERNATIVE_ROUTES] == true
} }
val lastFuelPricesFlow: Flow<Long> =
context.dataStore.data.map { preferences ->
preferences[LAST_FUEL_PRICES]
?: 0
}
val fuelPricesFlow: Flow<String> =
context.dataStore.data.map { preferences ->
preferences[FUEL_PRICES]
?: ""
}
// Save values // Save values
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
@@ -248,4 +265,16 @@ class DataStoreManager(private val context: Context) {
} }
} }
suspend fun setLastFuelPrices(lastFuelPrices: Long) {
context.dataStore.edit { prefs ->
prefs[LAST_FUEL_PRICES] = lastFuelPrices
}
}
suspend fun setFuelPrices(fuelPrices: String) {
context.dataStore.edit { prefs ->
prefs[FUEL_PRICES] = fuelPrices
}
}
} }
@@ -4,6 +4,7 @@ import android.content.Context
import android.location.Location import android.location.Location
import android.util.Log import android.util.Log
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.data.BuildConfig
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.overpass.Amenity import com.kouros.navigation.data.overpass.Amenity
@@ -13,6 +14,7 @@ import java.net.HttpURLConnection
import java.net.PasswordAuthentication import java.net.PasswordAuthentication
import java.net.URL import java.net.URL
const val tankerKoenigUrl = "https://creativecommons.tankerkoenig.de/json/list.php?" const val tankerKoenigUrl = "https://creativecommons.tankerkoenig.de/json/list.php?"
private val gson = GsonBuilder().serializeNulls().create() private val gson = GsonBuilder().serializeNulls().create()
@@ -20,11 +22,18 @@ private val gson = GsonBuilder().serializeNulls().create()
const val sort = "&sort=dist&type=all" const val sort = "&sort=dist&type=all"
const val apiKey = "&apikey=fc9900c9-8f02-4b28-990d-dc6067228c59" const val apiKey = "&apikey=fc9900c9-8f02-4b28-990d-dc6067228c59"
val useLocal = BuildConfig.DEBUG
class FuelPrices : NavigationRepository() { class FuelPrices : NavigationRepository() {
fun getFuelPrices(location: Location, radius: Int) : List<Station> { fun getFuelPrices(location: Location, radius: Int) : List<Station> {
val url = "${tankerKoenigUrl}lat=${location.latitude}&lng=${location.longitude}&rad=${radius}$sort$apiKey" val url = if (useLocal) {
"http://192.168.1.37/fuel.json"
} else {
"${tankerKoenigUrl}lat=${location.latitude}&lng=${location.longitude}&rad=${radius}$sort$apiKey"
}
val prices = fetchUrl( val prices = fetchUrl(
url, url,
@@ -1,5 +1,10 @@
package com.kouros.navigation.data.fuel package com.kouros.navigation.data.fuel
import com.kouros.navigation.data.Place
data class Stations(
val stations: List<Station>,
)
data class Station( data class Station(
val brand: String, val brand: String,
val diesel: Double, val diesel: Double,
@@ -9,6 +9,9 @@ data class Elements(
val lon: Double = 0.0, val lon: Double = 0.0,
val tags: Tags, val tags: Tags,
val type: String = "", val type: String = "",
var distance : Double = 0.0 var distance : Double = 0.0,
var e5 : Double = 0.0,
var e10: Double = 0.0,
var diesel: Double = 0.0
) )
@@ -14,22 +14,29 @@ import com.kouros.navigation.data.Constants
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
import com.kouros.navigation.data.Constants.TANKER_KOENIG_DELAY
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Places import com.kouros.navigation.data.Places
import com.kouros.navigation.data.SearchFilter import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.fuel.FuelPrice
import com.kouros.navigation.data.fuel.FuelPrices import com.kouros.navigation.data.fuel.FuelPrices
import com.kouros.navigation.data.fuel.Station
import com.kouros.navigation.data.fuel.Stations
import com.kouros.navigation.data.nominatim.Search import com.kouros.navigation.data.nominatim.Search
import com.kouros.navigation.data.nominatim.SearchResult import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.ElementSearch import com.kouros.navigation.data.overpass.ElementSearch
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Overpass import com.kouros.navigation.data.overpass.Overpass
import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.GeoUtils.transferCoordinates
import com.kouros.navigation.utils.bearingPositive import com.kouros.navigation.utils.bearingPositive
import com.kouros.navigation.utils.countryCodeSpeedLimit import com.kouros.navigation.utils.countryCodeSpeedLimit
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.isNumeric import com.kouros.navigation.utils.isNumeric
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.settingsViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -273,26 +280,25 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val incidents = mutableMapOf<String, String>() val incidents = mutableMapOf<String, String>()
val queuing = featureCollection.features()!! val queuing = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Queuing traffic") } .filter { it.properties()!!.get("events").toString().contains("Queuing traffic") }
incidents["queuing"] = FeatureCollection.fromFeatures(queuing).toJson() incidents["queuing"] = transferCoordinates(queuing)
val stationary = featureCollection.features()!! val stationary = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Stationary traffic") } .filter { it.properties()!!.get("events").toString().contains("Stationary traffic") }
incidents["stationary"] = FeatureCollection.fromFeatures(stationary).toJson() incidents["stationary"] = transferCoordinates(stationary)
val slow = featureCollection.features()!! val slow = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Slow traffic") } .filter { it.properties()!!.get("events").toString().contains("Slow traffic") }
incidents["slow"] = FeatureCollection.fromFeatures(slow).toJson() incidents["slow"] = transferCoordinates(slow)
val heavy = featureCollection.features()!! val heavy = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Heavy traffic") } .filter { it.properties()!!.get("events").toString().contains("Heavy traffic") }
incidents["heavy"] = FeatureCollection.fromFeatures(heavy).toJson() incidents["heavy"] = transferCoordinates(heavy)
val roadworks = featureCollection.features()!! val roadworks = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Roadworks") } .filter { it.properties()!!.get("events").toString().contains("Roadworks") }
incidents["roadworks"] = FeatureCollection.fromFeatures(roadworks).toJson() incidents["roadworks"] = transferCoordinates(roadworks)
val laneClosed = featureCollection.features()!! val laneClosed = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Lane closed") } .filter { it.properties()!!.get("events").toString().contains("Lane closed") }
incidents["lane"] = FeatureCollection.fromFeatures(laneClosed).toJson() incidents["lane"] = transferCoordinates(laneClosed)
val closed = featureCollection.features()!! val closed = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Closed") } .filter { it.properties()!!.get("events").toString().contains("Closed") }
incidents["closed"] = FeatureCollection.fromFeatures(closed).toJson() incidents["closed"] = transferCoordinates(closed)
return incidents return incidents
} }
@@ -391,15 +397,30 @@ 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(category: String, location: Location) { fun getAmenities(carContext: Context, category: String, location: Location, lastFuelUpdate: Long = 0) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val repository = getSettingsRepository(carContext)
var fuelPrices: List<Station>
val amenities = Overpass().getAmenities("amenity", category, location, 5.0) val amenities = Overpass().getAmenities("amenity", category, location, 5.0)
val now = System.currentTimeMillis()
if (category == Constants.FUEL_STATION && ((now - lastFuelUpdate) > TANKER_KOENIG_DELAY)) {
fuelPrices = FuelPrices().getFuelPrices(location, 5)
repository.setLastFuelPrices(System.currentTimeMillis())
repository.setFuelPrices(gson.toJson(Stations(fuelPrices)))
} else {
val fuels = repository.fuelPricesFlow.first()
fuelPrices =
gson.fromJson(fuels, Stations::class.java).stations
}
val distAmenities = mutableListOf<Elements>() val distAmenities = mutableListOf<Elements>()
amenities.forEach { amenities.forEach {
val plLocation = val plLocation =
location(longitude = it.lon, latitude = it.lat) location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location) val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble() it.distance = distance.toDouble()
if (category == Constants.FUEL_STATION) {
addFuelPrice(it, fuelPrices, location)
}
distAmenities.add(it) distAmenities.add(it)
} }
val sortedList = distAmenities.sortedWith(compareBy { it.distance }) val sortedList = distAmenities.sortedWith(compareBy { it.distance })
@@ -407,6 +428,21 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
fun addFuelPrice(fuel: Elements, fuelPrices: List<Station>, location: Location) {
fuelPrices.forEach {
if (fuel.tags.name.equals(it.brand, ignoreCase = true)) {
val plLocation =
location(longitude = it.lng, latitude = it.lat)
val distance = plLocation.distanceTo(location)
if ((distance - fuel.distance).absoluteValue < 30) {
fuel.e10 = it.e10
fuel.e5 = it.e5
fuel.diesel = it.diesel
}
}
}
}
/** /**
* Queries Overpass API for speed cameras within a radius. * Queries Overpass API for speed cameras within a radius.
* Posts sorted results to speedCameras LiveData. * Posts sorted results to speedCameras LiveData.
@@ -1,12 +1,10 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import android.location.Location import android.location.Location
import android.util.Log
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationState import com.kouros.navigation.data.NavigationState
import com.kouros.navigation.data.Route import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
@@ -170,7 +168,7 @@ open class RouteModel {
/** /**
* Checks for arrival * Checks for arrival
*/ */
fun isArrival(): Boolean { fun isManeuverArrival(): Boolean {
return navState.maneuverType == Maneuver.TYPE_DESTINATION return navState.maneuverType == Maneuver.TYPE_DESTINATION
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT || navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT || navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
@@ -105,6 +105,18 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
false false
) )
val lastFuelPrices = repository.lastFuelPricesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
0
)
val fuelPrices = repository.fuelPricesFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
0
)
fun onShow3DChanged(enabled: Boolean) { fun onShow3DChanged(enabled: Boolean) {
viewModelScope.launch { repository.setShow3D(enabled) } viewModelScope.launch { repository.setShow3D(enabled) }
} }
@@ -165,4 +177,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
fun onAlternativeRoutes(enabled: Boolean) { fun onAlternativeRoutes(enabled: Boolean) {
viewModelScope.launch { repository.setAlternativeRoutes(enabled) } viewModelScope.launch { repository.setAlternativeRoutes(enabled) }
} }
fun onLastFuelPricesChanged(lastFuelPrices: Long) {
viewModelScope.launch { repository.setLastFuelPrices(lastFuelPrices) }
}
fun onFuelPricesChanged(fuelPrices: String) {
viewModelScope.launch { repository.setFuelPrices(fuelPrices) }
}
} }
@@ -2,6 +2,7 @@ package com.kouros.navigation.repository
import android.util.Log import android.util.Log
import com.kouros.navigation.data.datastore.DataStoreManager import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.data.fuel.FuelPrices
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class SettingsRepository( class SettingsRepository(
@@ -54,6 +55,13 @@ class SettingsRepository(
val alternativeRoutesFlow: Flow<Boolean> = val alternativeRoutesFlow: Flow<Boolean> =
dataStoreManager.alternativeRoutesFlow dataStoreManager.alternativeRoutesFlow
val lastFuelPricesFlow: Flow<Long> =
dataStoreManager.lastFuelPricesFlow
val fuelPricesFlow: Flow<String> =
dataStoreManager.fuelPricesFlow
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
dataStoreManager.setShow3D(enabled) dataStoreManager.setShow3D(enabled)
} }
@@ -117,4 +125,12 @@ class SettingsRepository(
suspend fun setAlternativeRoutes(enabled: Boolean) { suspend fun setAlternativeRoutes(enabled: Boolean) {
dataStoreManager.setAlternativeRoutes(enabled) dataStoreManager.setAlternativeRoutes(enabled)
} }
suspend fun setLastFuelPrices(lastFuelPrices: Long) {
dataStoreManager.setLastFuelPrices(lastFuelPrices)
}
suspend fun setFuelPrices(fuelPrices: String) {
dataStoreManager.setFuelPrices(fuelPrices)
}
} }
@@ -2,7 +2,6 @@ package com.kouros.navigation.utils
import android.location.Location import android.location.Location
import android.util.Log import android.util.Log
import com.kouros.navigation.data.Constants.TAG
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import org.maplibre.geojson.FeatureCollection import org.maplibre.geojson.FeatureCollection
@@ -17,8 +16,11 @@ import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc import org.maplibre.turf.TurfMisc
import java.lang.Math.toDegrees import java.lang.Math.toDegrees
import java.lang.Math.toRadians import java.lang.Math.toRadians
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sin
object GeoUtils { object GeoUtils {
@@ -185,4 +187,64 @@ object GeoUtils {
&& location.longitude <= topRightLon && location.longitude <= topRightLon
return isInside return isInside
} }
fun transferCoordinates(features: List<org.maplibre.geojson.Feature>): String {
return buildFeatureCollection {
features.forEach {
val movedCoordinates = arrayListOf<org.maplibre.spatialk.geojson.Point>()
val coordinates = it.geometry() as LineString
val geo = coordinates.coordinates()
var bearing = 0.0F
for (index in 0..<geo.size) {
val current = location(geo[index].longitude(), geo[index].latitude())
bearing = if (index < geo.size - 1) {
val next =
location(geo[index + 1].longitude(), geo[index + 1].latitude())
current.bearingPositive(next)
} else {
bearing
}
val point = movePoint(
current.latitude,
current.longitude,
bearingDegrees = bearing + 90.0
)
movedCoordinates.add(point)
}
val lineString = buildLineString {
movedCoordinates.forEach { ft ->
add(
ft
)
}
}
addFeature {
geometry = lineString
properties = buildJsonObject { null }
}
}
}.toJson()
}
fun movePoint(
latitude: Double,
longitude: Double,
distanceMeters: Double = 7.0,
bearingDegrees: Double = 0.0
): org.maplibre.spatialk.geojson.Point {
val earthRadius = 6371000.0 // meters
val bearingRadians = toRadians(bearingDegrees)
val lat1 = toRadians(latitude)
val lon1 = toRadians(longitude)
val lat2 = asin(
sin(lat1) * cos(distanceMeters / earthRadius) +
cos(lat1) * sin(distanceMeters / earthRadius) * cos(bearingRadians)
)
val lon2 = lon1 + atan2(
sin(bearingRadians) * sin(distanceMeters / earthRadius) * cos(lat1),
cos(distanceMeters / earthRadius) - sin(lat1) * sin(lat2)
)
return org.maplibre.spatialk.geojson.Point(toDegrees(lon2), toDegrees(lat2))
}
} }