* Prototype simplified weather forecast along route * Reset last known route progression on route change * Add forecast distance per hour setting * Only decode response as gzip if gzipped * Bump version to 1.3
214 lines
8.3 KiB
Kotlin
214 lines
8.3 KiB
Kotlin
package de.timklge.karooheadwind
|
|
|
|
import android.content.Context
|
|
import android.util.Log
|
|
import androidx.datastore.preferences.core.edit
|
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
import com.mapbox.geojson.LineString
|
|
import com.mapbox.turf.TurfConstants
|
|
import com.mapbox.turf.TurfMeasurement
|
|
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
|
import de.timklge.karooheadwind.screens.HeadwindSettings
|
|
import de.timklge.karooheadwind.screens.HeadwindStats
|
|
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
|
|
import de.timklge.karooheadwind.screens.WindUnit
|
|
import io.hammerhead.karooext.KarooSystemService
|
|
import io.hammerhead.karooext.models.DataType
|
|
import io.hammerhead.karooext.models.OnNavigationState
|
|
import io.hammerhead.karooext.models.StreamState
|
|
import io.hammerhead.karooext.models.UserProfile
|
|
import kotlinx.coroutines.channels.awaitClose
|
|
import kotlinx.coroutines.channels.trySendBlocking
|
|
import kotlinx.coroutines.flow.Flow
|
|
import kotlinx.coroutines.flow.callbackFlow
|
|
import kotlinx.coroutines.flow.collect
|
|
import kotlinx.coroutines.flow.combine
|
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
import kotlinx.coroutines.flow.filter
|
|
import kotlinx.coroutines.flow.first
|
|
import kotlinx.coroutines.flow.flow
|
|
import kotlinx.coroutines.flow.map
|
|
import kotlinx.serialization.Serializable
|
|
import kotlinx.serialization.encodeToString
|
|
import kotlinx.serialization.json.Json
|
|
|
|
|
|
val jsonWithUnknownKeys = Json { ignoreUnknownKeys = true }
|
|
|
|
val settingsKey = stringPreferencesKey("settings")
|
|
val widgetSettingsKey = stringPreferencesKey("widgetSettings")
|
|
val currentDataKey = stringPreferencesKey("currentForecasts")
|
|
val statsKey = stringPreferencesKey("stats")
|
|
val lastKnownPositionKey = stringPreferencesKey("lastKnownPosition")
|
|
|
|
suspend fun saveSettings(context: Context, settings: HeadwindSettings) {
|
|
context.dataStore.edit { t ->
|
|
t[settingsKey] = Json.encodeToString(settings)
|
|
}
|
|
}
|
|
|
|
suspend fun saveWidgetSettings(context: Context, settings: HeadwindWidgetSettings) {
|
|
context.dataStore.edit { t ->
|
|
t[widgetSettingsKey] = Json.encodeToString(settings)
|
|
}
|
|
}
|
|
|
|
suspend fun saveStats(context: Context, stats: HeadwindStats) {
|
|
context.dataStore.edit { t ->
|
|
t[statsKey] = Json.encodeToString(stats)
|
|
}
|
|
}
|
|
|
|
@Serializable
|
|
data class WeatherDataResponse(val data: OpenMeteoCurrentWeatherResponse, val requestedPosition: GpsCoordinates)
|
|
|
|
suspend fun saveCurrentData(context: Context, forecast: List<WeatherDataResponse>) {
|
|
context.dataStore.edit { t ->
|
|
t[currentDataKey] = Json.encodeToString(forecast)
|
|
}
|
|
}
|
|
|
|
suspend fun saveLastKnownPosition(context: Context, gpsCoordinates: GpsCoordinates) {
|
|
Log.i(KarooHeadwindExtension.TAG, "Saving last known position: $gpsCoordinates")
|
|
|
|
try {
|
|
context.dataStore.edit { t ->
|
|
t[lastKnownPositionKey] = Json.encodeToString(gpsCoordinates)
|
|
}
|
|
} catch(e: Throwable){
|
|
Log.e(KarooHeadwindExtension.TAG, "Failed to save last known position", e)
|
|
}
|
|
}
|
|
|
|
fun Context.streamWidgetSettings(): Flow<HeadwindWidgetSettings> {
|
|
return dataStore.data.map { settingsJson ->
|
|
try {
|
|
if (settingsJson.contains(widgetSettingsKey)){
|
|
jsonWithUnknownKeys.decodeFromString<HeadwindWidgetSettings>(settingsJson[widgetSettingsKey]!!)
|
|
} else {
|
|
jsonWithUnknownKeys.decodeFromString<HeadwindWidgetSettings>(HeadwindWidgetSettings.defaultWidgetSettings)
|
|
}
|
|
} catch(e: Throwable){
|
|
Log.e(KarooHeadwindExtension.TAG, "Failed to read widget preferences", e)
|
|
jsonWithUnknownKeys.decodeFromString<HeadwindWidgetSettings>(HeadwindWidgetSettings.defaultWidgetSettings)
|
|
}
|
|
}.distinctUntilChanged()
|
|
}
|
|
|
|
fun Context.streamSettings(karooSystemService: KarooSystemService): Flow<HeadwindSettings> {
|
|
return dataStore.data.map { settingsJson ->
|
|
try {
|
|
if (settingsJson.contains(settingsKey)){
|
|
jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(settingsJson[settingsKey]!!)
|
|
} else {
|
|
val defaultSettings = jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(HeadwindSettings.defaultSettings)
|
|
|
|
val preferredUnits = karooSystemService.streamUserProfile().first().preferredUnit
|
|
|
|
defaultSettings.copy(
|
|
windUnit = if (preferredUnits.distance == UserProfile.PreferredUnit.UnitType.METRIC) WindUnit.KILOMETERS_PER_HOUR else WindUnit.MILES_PER_HOUR,
|
|
)
|
|
}
|
|
} catch(e: Throwable){
|
|
Log.e(KarooHeadwindExtension.TAG, "Failed to read preferences", e)
|
|
jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(HeadwindSettings.defaultSettings)
|
|
}
|
|
}.distinctUntilChanged()
|
|
}
|
|
|
|
data class UpcomingRoute(val distanceAlongRoute: Double, val routePolyline: LineString, val routeLength: Double)
|
|
|
|
fun KarooSystemService.streamUpcomingRoute(): Flow<UpcomingRoute?> {
|
|
val distanceToDestinationStream = flow {
|
|
emit(null)
|
|
|
|
streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION)
|
|
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
|
|
.filter { it != 0.0 } // FIXME why is 0 sometimes emitted if no route is loaded?
|
|
.collect { emit(it) }
|
|
}
|
|
|
|
var lastKnownDistanceAlongRoute = 0.0
|
|
var lastKnownRoutePolyline: LineString? = null
|
|
|
|
val navigationStateStream = streamNavigationState()
|
|
.map { it.state as? OnNavigationState.NavigationState.NavigatingRoute }
|
|
.map { navigationState ->
|
|
navigationState?.let { LineString.fromPolyline(it.routePolyline, 5) }
|
|
}
|
|
.combine(distanceToDestinationStream) { routePolyline, distanceToDestination ->
|
|
if (routePolyline != null){
|
|
val length = TurfMeasurement.length(routePolyline, TurfConstants.UNIT_METERS)
|
|
if (routePolyline != lastKnownRoutePolyline){
|
|
lastKnownDistanceAlongRoute = 0.0
|
|
}
|
|
val distanceAlongRoute = distanceToDestination?.let { toDest -> length - toDest } ?: lastKnownDistanceAlongRoute
|
|
lastKnownDistanceAlongRoute = distanceAlongRoute
|
|
lastKnownRoutePolyline = routePolyline
|
|
|
|
UpcomingRoute(distanceAlongRoute, routePolyline, length)
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
|
|
return navigationStateStream
|
|
}
|
|
|
|
fun Context.streamStats(): Flow<HeadwindStats> {
|
|
return dataStore.data.map { statsJson ->
|
|
try {
|
|
jsonWithUnknownKeys.decodeFromString<HeadwindStats>(
|
|
statsJson[statsKey] ?: HeadwindStats.defaultStats
|
|
)
|
|
} catch(e: Throwable){
|
|
Log.e(KarooHeadwindExtension.TAG, "Failed to read stats", e)
|
|
jsonWithUnknownKeys.decodeFromString<HeadwindStats>(HeadwindStats.defaultStats)
|
|
}
|
|
}.distinctUntilChanged()
|
|
}
|
|
|
|
suspend fun Context.getLastKnownPosition(): GpsCoordinates? {
|
|
val settingsJson = dataStore.data.first()
|
|
|
|
try {
|
|
val lastKnownPositionString = settingsJson[lastKnownPositionKey] ?: return null
|
|
val lastKnownPosition = jsonWithUnknownKeys.decodeFromString<GpsCoordinates>(
|
|
lastKnownPositionString
|
|
)
|
|
|
|
return lastKnownPosition
|
|
} catch(e: Throwable){
|
|
Log.e(KarooHeadwindExtension.TAG, "Failed to read last known position", e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
|
|
return callbackFlow {
|
|
val listenerId = addConsumer { userProfile: UserProfile ->
|
|
trySendBlocking(userProfile)
|
|
}
|
|
awaitClose {
|
|
removeConsumer(listenerId)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun Context.streamCurrentWeatherData(): Flow<List<WeatherDataResponse>> {
|
|
return dataStore.data.map { settingsJson ->
|
|
try {
|
|
val data = settingsJson[currentDataKey]
|
|
data?.let { d -> jsonWithUnknownKeys.decodeFromString<List<WeatherDataResponse>>(d) } ?: emptyList()
|
|
} catch (e: Throwable) {
|
|
Log.e(KarooHeadwindExtension.TAG, "Failed to read weather data", e)
|
|
emptyList()
|
|
}
|
|
}.distinctUntilChanged().map { response ->
|
|
response.filter { forecast ->
|
|
forecast.data.current.time * 1000 >= System.currentTimeMillis() - (1000 * 60 * 60 * 12)
|
|
}
|
|
}
|
|
}
|