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) { 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 { return dataStore.data.map { settingsJson -> try { if (settingsJson.contains(widgetSettingsKey)){ jsonWithUnknownKeys.decodeFromString(settingsJson[widgetSettingsKey]!!) } else { jsonWithUnknownKeys.decodeFromString(HeadwindWidgetSettings.defaultWidgetSettings) } } catch(e: Throwable){ Log.e(KarooHeadwindExtension.TAG, "Failed to read widget preferences", e) jsonWithUnknownKeys.decodeFromString(HeadwindWidgetSettings.defaultWidgetSettings) } }.distinctUntilChanged() } fun Context.streamSettings(karooSystemService: KarooSystemService): Flow { return dataStore.data.map { settingsJson -> try { if (settingsJson.contains(settingsKey)){ jsonWithUnknownKeys.decodeFromString(settingsJson[settingsKey]!!) } else { val defaultSettings = jsonWithUnknownKeys.decodeFromString(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.defaultSettings) } }.distinctUntilChanged() } data class UpcomingRoute(val distanceAlongRoute: Double, val routePolyline: LineString, val routeLength: Double) fun KarooSystemService.streamUpcomingRoute(): Flow { 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 { return dataStore.data.map { statsJson -> try { jsonWithUnknownKeys.decodeFromString( statsJson[statsKey] ?: HeadwindStats.defaultStats ) } catch(e: Throwable){ Log.e(KarooHeadwindExtension.TAG, "Failed to read stats", e) jsonWithUnknownKeys.decodeFromString(HeadwindStats.defaultStats) } }.distinctUntilChanged() } suspend fun Context.getLastKnownPosition(): GpsCoordinates? { val settingsJson = dataStore.data.first() try { val lastKnownPositionString = settingsJson[lastKnownPositionKey] ?: return null val lastKnownPosition = jsonWithUnknownKeys.decodeFromString( lastKnownPositionString ) return lastKnownPosition } catch(e: Throwable){ Log.e(KarooHeadwindExtension.TAG, "Failed to read last known position", e) return null } } fun KarooSystemService.streamUserProfile(): Flow { return callbackFlow { val listenerId = addConsumer { userProfile: UserProfile -> trySendBlocking(userProfile) } awaitClose { removeConsumer(listenerId) } } } fun Context.streamCurrentWeatherData(): Flow> { return dataStore.data.map { settingsJson -> try { val data = settingsJson[currentDataKey] data?.let { d -> jsonWithUnknownKeys.decodeFromString>(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) } } }