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
@@ -136,9 +136,9 @@ object Constants {
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
@@ -159,6 +159,8 @@ object Constants {
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"
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.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.kouros.navigation.data.EngineType
@@ -59,6 +60,10 @@ class DataStoreManager(private val context: Context) {
val ALTERNATIVE_ROUTES = booleanPreferencesKey("AlternativeRoutes")
val LAST_FUEL_PRICES = longPreferencesKey("LastFuelPrices")
val FUEL_PRICES = stringPreferencesKey("FuelPrices")
}
// Read values
@@ -151,6 +156,18 @@ class DataStoreManager(private val context: Context) {
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
suspend fun setShow3D(enabled: Boolean) {
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.util.Log
import com.google.gson.GsonBuilder
import com.kouros.data.BuildConfig
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.overpass.Amenity
@@ -13,6 +14,7 @@ import java.net.HttpURLConnection
import java.net.PasswordAuthentication
import java.net.URL
const val tankerKoenigUrl = "https://creativecommons.tankerkoenig.de/json/list.php?"
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 apiKey = "&apikey=fc9900c9-8f02-4b28-990d-dc6067228c59"
val useLocal = BuildConfig.DEBUG
class FuelPrices : NavigationRepository() {
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(
url,
@@ -1,5 +1,10 @@
package com.kouros.navigation.data.fuel
import com.kouros.navigation.data.Place
data class Stations(
val stations: List<Station>,
)
data class Station(
val brand: String,
val diesel: Double,
@@ -9,6 +9,9 @@ data class Elements(
val lon: Double = 0.0,
val tags: Tags,
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_UPDATE_DISTANCE
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.Place
import com.kouros.navigation.data.Places
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.Station
import com.kouros.navigation.data.fuel.Stations
import com.kouros.navigation.data.nominatim.Search
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.ElementSearch
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Overpass
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.countryCodeSpeedLimit
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.isNumeric
import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.settingsViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -273,26 +280,25 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val incidents = mutableMapOf<String, String>()
val queuing = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Queuing traffic") }
incidents["queuing"] = FeatureCollection.fromFeatures(queuing).toJson()
incidents["queuing"] = transferCoordinates(queuing)
val stationary = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Stationary traffic") }
incidents["stationary"] = FeatureCollection.fromFeatures(stationary).toJson()
incidents["stationary"] = transferCoordinates(stationary)
val slow = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Slow traffic") }
incidents["slow"] = FeatureCollection.fromFeatures(slow).toJson()
incidents["slow"] = transferCoordinates(slow)
val heavy = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Heavy traffic") }
incidents["heavy"] = FeatureCollection.fromFeatures(heavy).toJson()
incidents["heavy"] = transferCoordinates(heavy)
val roadworks = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Roadworks") }
incidents["roadworks"] = FeatureCollection.fromFeatures(roadworks).toJson()
incidents["roadworks"] = transferCoordinates(roadworks)
val laneClosed = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Lane closed") }
incidents["lane"] = FeatureCollection.fromFeatures(laneClosed).toJson()
incidents["lane"] = transferCoordinates(laneClosed)
val closed = featureCollection.features()!!
.filter { it.properties()!!.get("events").toString().contains("Closed") }
incidents["closed"] = FeatureCollection.fromFeatures(closed).toJson()
incidents["closed"] = transferCoordinates(closed)
return incidents
}
@@ -391,15 +397,30 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
* Queries Overpass API for nearby amenities of a specific category.
* 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) {
val repository = getSettingsRepository(carContext)
var fuelPrices: List<Station>
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>()
amenities.forEach {
val plLocation =
location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
if (category == Constants.FUEL_STATION) {
addFuelPrice(it, fuelPrices, location)
}
distAmenities.add(it)
}
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.
* Posts sorted results to speedCameras LiveData.
@@ -1,12 +1,10 @@
package com.kouros.navigation.model
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_PROJECTION
import androidx.car.app.navigation.model.Maneuver
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.Route
import com.kouros.navigation.data.StepData
@@ -170,7 +168,7 @@ open class RouteModel {
/**
* Checks for arrival
*/
fun isArrival(): Boolean {
fun isManeuverArrival(): Boolean {
return navState.maneuverType == Maneuver.TYPE_DESTINATION
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
@@ -105,6 +105,18 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
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) {
viewModelScope.launch { repository.setShow3D(enabled) }
}
@@ -165,4 +177,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
fun onAlternativeRoutes(enabled: Boolean) {
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 com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.data.fuel.FuelPrices
import kotlinx.coroutines.flow.Flow
class SettingsRepository(
@@ -54,6 +55,13 @@ class SettingsRepository(
val alternativeRoutesFlow: Flow<Boolean> =
dataStoreManager.alternativeRoutesFlow
val lastFuelPricesFlow: Flow<Long> =
dataStoreManager.lastFuelPricesFlow
val fuelPricesFlow: Flow<String> =
dataStoreManager.fuelPricesFlow
suspend fun setShow3D(enabled: Boolean) {
dataStoreManager.setShow3D(enabled)
}
@@ -117,4 +125,12 @@ class SettingsRepository(
suspend fun setAlternativeRoutes(enabled: Boolean) {
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.util.Log
import com.kouros.navigation.data.Constants.TAG
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import org.maplibre.geojson.FeatureCollection
@@ -17,8 +16,11 @@ import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc
import java.lang.Math.toDegrees
import java.lang.Math.toRadians
import kotlin.math.asin
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
object GeoUtils {
@@ -185,4 +187,64 @@ object GeoUtils {
&& location.longitude <= topRightLon
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))
}
}