Add weather view to main menu (#54)
* Add initial weather widget to main menu * Always forecast position with calculated distance * Fix manifest * Replace exit button with back button
This commit is contained in:
parent
de009454b8
commit
ee206a11c9
@ -8,6 +8,8 @@ This extension for Karoo devices adds a graphical data field that shows the curr
|
|||||||
|
|
||||||
Compatible with Karoo 2 and Karoo 3 devices.
|
Compatible with Karoo 2 and Karoo 3 devices.
|
||||||
|
|
||||||
|
<a href="https://www.buymeacoffee.com/timklge" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|||||||
@ -7,5 +7,5 @@
|
|||||||
"latestVersionCode": 14,
|
"latestVersionCode": 14,
|
||||||
"developer": "timklge",
|
"developer": "timklge",
|
||||||
"description": "Provides headwind direction, wind speed and other weather data fields",
|
"description": "Provides headwind direction, wind speed and other weather data fields",
|
||||||
"releaseNotes": "Forecast weather along route in fixed intervals if route is loaded",
|
"releaseNotes": "* Forecast weather along route in fixed intervals if route is loaded\n* Show current weather in app menu"
|
||||||
}
|
}
|
||||||
@ -8,10 +8,6 @@ import com.mapbox.geojson.LineString
|
|||||||
import com.mapbox.turf.TurfConstants
|
import com.mapbox.turf.TurfConstants
|
||||||
import com.mapbox.turf.TurfMeasurement
|
import com.mapbox.turf.TurfMeasurement
|
||||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
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.KarooSystemService
|
||||||
import io.hammerhead.karooext.models.DataType
|
import io.hammerhead.karooext.models.DataType
|
||||||
import io.hammerhead.karooext.models.OnNavigationState
|
import io.hammerhead.karooext.models.OnNavigationState
|
||||||
@ -21,16 +17,13 @@ import kotlinx.coroutines.channels.awaitClose
|
|||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
|
||||||
@ -102,7 +95,8 @@ fun Context.streamSettings(karooSystemService: KarooSystemService): Flow<Headwin
|
|||||||
if (settingsJson.contains(settingsKey)){
|
if (settingsJson.contains(settingsKey)){
|
||||||
jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(settingsJson[settingsKey]!!)
|
jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(settingsJson[settingsKey]!!)
|
||||||
} else {
|
} else {
|
||||||
val defaultSettings = jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(HeadwindSettings.defaultSettings)
|
val defaultSettings = jsonWithUnknownKeys.decodeFromString<HeadwindSettings>(
|
||||||
|
HeadwindSettings.defaultSettings)
|
||||||
|
|
||||||
val preferredUnits = karooSystemService.streamUserProfile().first().preferredUnit
|
val preferredUnits = karooSystemService.streamUserProfile().first().preferredUnit
|
||||||
|
|
||||||
@ -138,6 +132,7 @@ fun KarooSystemService.streamUpcomingRoute(): Flow<UpcomingRoute?> {
|
|||||||
navigationState?.let { LineString.fromPolyline(it.routePolyline, 5) }
|
navigationState?.let { LineString.fromPolyline(it.routePolyline, 5) }
|
||||||
}
|
}
|
||||||
.combine(distanceToDestinationStream) { routePolyline, distanceToDestination ->
|
.combine(distanceToDestinationStream) { routePolyline, distanceToDestination ->
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Route polyline size: ${routePolyline?.coordinates()?.size}, distance to destination: $distanceToDestination")
|
||||||
if (routePolyline != null){
|
if (routePolyline != null){
|
||||||
val length = TurfMeasurement.length(routePolyline, TurfConstants.UNIT_METERS)
|
val length = TurfMeasurement.length(routePolyline, TurfConstants.UNIT_METERS)
|
||||||
if (routePolyline != lastKnownRoutePolyline){
|
if (routePolyline != lastKnownRoutePolyline){
|
||||||
|
|||||||
@ -1,67 +1,15 @@
|
|||||||
package de.timklge.karooheadwind
|
package de.timklge.karooheadwind
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.TextUnitType
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.datastore.preferences.core.edit
|
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
||||||
import androidx.glance.GlanceModifier
|
|
||||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
|
||||||
import androidx.glance.appwidget.GlanceRemoteViews
|
|
||||||
import androidx.glance.appwidget.RemoteViewsCompositionResult
|
|
||||||
import androidx.glance.color.ColorProvider
|
|
||||||
import androidx.glance.layout.Alignment
|
|
||||||
import androidx.glance.layout.Box
|
|
||||||
import androidx.glance.layout.fillMaxSize
|
|
||||||
import androidx.glance.layout.padding
|
|
||||||
import androidx.glance.text.Text
|
|
||||||
import androidx.glance.text.TextAlign
|
|
||||||
import androidx.glance.text.TextStyle
|
|
||||||
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.PrecipitationUnit
|
|
||||||
import de.timklge.karooheadwind.screens.TemperatureUnit
|
|
||||||
import de.timklge.karooheadwind.screens.WindUnit
|
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.models.DataType
|
|
||||||
import io.hammerhead.karooext.models.HttpResponseState
|
|
||||||
import io.hammerhead.karooext.models.OnHttpResponse
|
|
||||||
import io.hammerhead.karooext.models.OnLocationChanged
|
import io.hammerhead.karooext.models.OnLocationChanged
|
||||||
import io.hammerhead.karooext.models.OnNavigationState
|
import io.hammerhead.karooext.models.OnNavigationState
|
||||||
import io.hammerhead.karooext.models.OnStreamState
|
import io.hammerhead.karooext.models.OnStreamState
|
||||||
import io.hammerhead.karooext.models.StreamState
|
import io.hammerhead.karooext.models.StreamState
|
||||||
import io.hammerhead.karooext.models.UserProfile
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.emitAll
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.scan
|
|
||||||
import kotlinx.coroutines.flow.single
|
|
||||||
import kotlinx.coroutines.flow.timeout
|
|
||||||
import kotlinx.coroutines.time.debounce
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.time.Duration
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
|
fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
|
||||||
return callbackFlow {
|
return callbackFlow {
|
||||||
|
|||||||
@ -91,7 +91,10 @@ suspend fun KarooSystemService.updateLastKnownGps(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> {
|
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> {
|
||||||
// return flowOf(GpsCoordinates(52.5164069,13.3784))
|
/* return flow {
|
||||||
|
emit(GpsCoordinates(52.5164069,13.3784))
|
||||||
|
awaitCancellation()
|
||||||
|
} */
|
||||||
|
|
||||||
val initialFlow = flow {
|
val initialFlow = flow {
|
||||||
val lastKnownPosition = context.getLastKnownPosition()
|
val lastKnownPosition = context.getLastKnownPosition()
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
package de.timklge.karooheadwind
|
||||||
|
|
||||||
|
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
enum class WindUnit(val id: String, val label: String, val unitDisplay: String){
|
||||||
|
KILOMETERS_PER_HOUR("kmh", "Kilometers (km/h)", "km/h"),
|
||||||
|
METERS_PER_SECOND("ms", "Meters (m/s)", "m/s"),
|
||||||
|
MILES_PER_HOUR("mph", "Miles (mph)", "mph"),
|
||||||
|
KNOTS("kn", "Knots (kn)", "kn")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PrecipitationUnit(val id: String, val label: String, val unitDisplay: String){
|
||||||
|
MILLIMETERS("mm", "Millimeters (mm)", "mm"),
|
||||||
|
INCH("inch", "Inch", "in")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WindDirectionIndicatorTextSetting(val id: String, val label: String){
|
||||||
|
HEADWIND_SPEED("headwind-speed", "Headwind speed"),
|
||||||
|
WIND_SPEED("absolute-wind-speed", "Absolute wind speed"),
|
||||||
|
NONE("none", "None")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WindDirectionIndicatorSetting(val id: String, val label: String){
|
||||||
|
HEADWIND_DIRECTION("headwind-direction", "Headwind"),
|
||||||
|
WIND_DIRECTION("wind-direction", "Absolute wind direction"),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TemperatureUnit(val id: String, val label: String, val unitDisplay: String){
|
||||||
|
CELSIUS("celsius", "Celsius (°C)", "°C"),
|
||||||
|
FAHRENHEIT("fahrenheit", "Fahrenheit (°F)", "°F")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class RoundLocationSetting(val id: String, val label: String, val km: Int){
|
||||||
|
KM_1("1 km", "1 km", 1),
|
||||||
|
KM_2("2 km", "2 km", 2),
|
||||||
|
KM_5("5 km", "5 km", 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HeadwindWidgetSettings(
|
||||||
|
val currentForecastHourOffset: Int = 0
|
||||||
|
){
|
||||||
|
companion object {
|
||||||
|
val defaultWidgetSettings = Json.encodeToString(HeadwindWidgetSettings())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HeadwindStats(
|
||||||
|
val lastSuccessfulWeatherRequest: Long? = null,
|
||||||
|
val lastSuccessfulWeatherPosition: GpsCoordinates? = null,
|
||||||
|
val failedWeatherRequest: Long? = null,
|
||||||
|
){
|
||||||
|
companion object {
|
||||||
|
val defaultStats = Json.encodeToString(HeadwindStats())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HeadwindSettings(
|
||||||
|
val windUnit: WindUnit = WindUnit.KILOMETERS_PER_HOUR,
|
||||||
|
val welcomeDialogAccepted: Boolean = false,
|
||||||
|
val windDirectionIndicatorTextSetting: WindDirectionIndicatorTextSetting = WindDirectionIndicatorTextSetting.HEADWIND_SPEED,
|
||||||
|
val windDirectionIndicatorSetting: WindDirectionIndicatorSetting = WindDirectionIndicatorSetting.HEADWIND_DIRECTION,
|
||||||
|
val roundLocationTo: RoundLocationSetting = RoundLocationSetting.KM_2,
|
||||||
|
val forecastedKmPerHour: Int = 20,
|
||||||
|
val forecastedMilesPerHour: Int = 12,
|
||||||
|
){
|
||||||
|
companion object {
|
||||||
|
val defaultSettings = Json.encodeToString(HeadwindSettings())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getForecastMetersPerHour(isImperial: Boolean): Int {
|
||||||
|
return if (isImperial) forecastedMilesPerHour * 1609 else forecastedKmPerHour * 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,13 +7,13 @@ import com.mapbox.turf.TurfConstants
|
|||||||
import com.mapbox.turf.TurfMeasurement
|
import com.mapbox.turf.TurfMeasurement
|
||||||
import de.timklge.karooheadwind.datatypes.CloudCoverDataType
|
import de.timklge.karooheadwind.datatypes.CloudCoverDataType
|
||||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||||
|
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
|
||||||
import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType
|
import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType
|
||||||
import de.timklge.karooheadwind.datatypes.PrecipitationDataType
|
import de.timklge.karooheadwind.datatypes.PrecipitationDataType
|
||||||
import de.timklge.karooheadwind.datatypes.RelativeHumidityDataType
|
import de.timklge.karooheadwind.datatypes.RelativeHumidityDataType
|
||||||
|
import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
|
||||||
import de.timklge.karooheadwind.datatypes.SurfacePressureDataType
|
import de.timklge.karooheadwind.datatypes.SurfacePressureDataType
|
||||||
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
|
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
|
||||||
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
|
|
||||||
import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
|
|
||||||
import de.timklge.karooheadwind.datatypes.TemperatureDataType
|
import de.timklge.karooheadwind.datatypes.TemperatureDataType
|
||||||
import de.timklge.karooheadwind.datatypes.UserWindSpeedDataType
|
import de.timklge.karooheadwind.datatypes.UserWindSpeedDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherDataType
|
import de.timklge.karooheadwind.datatypes.WeatherDataType
|
||||||
@ -21,9 +21,6 @@ import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
|||||||
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindGustsDataType
|
import de.timklge.karooheadwind.datatypes.WindGustsDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindSpeedDataType
|
import de.timklge.karooheadwind.datatypes.WindSpeedDataType
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
|
||||||
import de.timklge.karooheadwind.screens.HeadwindStats
|
|
||||||
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
|
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.KarooExtension
|
import io.hammerhead.karooext.extension.KarooExtension
|
||||||
import io.hammerhead.karooext.models.UserProfile
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
@ -47,7 +44,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.Locale
|
|
||||||
import java.util.zip.GZIPInputStream
|
import java.util.zip.GZIPInputStream
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@ -159,27 +155,46 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") {
|
|||||||
ChronoUnit.MILLIS.between(startOfHour, now)
|
ChronoUnit.MILLIS.between(startOfHour, now)
|
||||||
}
|
}
|
||||||
val msToNextFullHour = (1_000 * 60 * 60) - msSinceFullHour
|
val msToNextFullHour = (1_000 * 60 * 60) - msSinceFullHour
|
||||||
val calculatedDistanceToNextFullHour = (msToNextFullHour / (1_000.0 * 60 * 60)) * distancePerHour
|
val calculatedDistanceToNextFullHour = ((msToNextFullHour / (1_000.0 * 60 * 60)) * distancePerHour).coerceIn(0.0, distancePerHour)
|
||||||
val distanceToNextFullHour = if (calculatedDistanceToNextFullHour > 5_000) calculatedDistanceToNextFullHour else distancePerHour
|
|
||||||
|
|
||||||
Log.d(TAG, "Minutes to next full hour: ${msToNextFullHour / 1000 / 60}, Distance to next full hour: ${(distanceToNextFullHour / 1000).roundToInt()}km (calculated: ${(calculatedDistanceToNextFullHour / 1000).roundToInt()}km)")
|
Log.d(TAG, "Minutes to next full hour: ${msToNextFullHour / 1000 / 60}, Distance to next full hour: ${(calculatedDistanceToNextFullHour / 1000).roundToInt()}km")
|
||||||
|
|
||||||
requestedGpsCoordinates = buildList {
|
requestedGpsCoordinates = buildList {
|
||||||
add(gps)
|
add(gps)
|
||||||
|
|
||||||
var currentPosition = positionOnRoute + distanceToNextFullHour
|
var currentPosition = positionOnRoute + calculatedDistanceToNextFullHour
|
||||||
var lastRequestedPosition = currentPosition
|
var lastRequestedPosition = currentPosition
|
||||||
while (currentPosition < upcomingRoute.routeLength && size < 10){
|
while (currentPosition < upcomingRoute.routeLength && size < 10) {
|
||||||
val point = TurfMeasurement.along(upcomingRoute.routePolyline, currentPosition, TurfConstants.UNIT_METERS)
|
val point = TurfMeasurement.along(
|
||||||
add(GpsCoordinates(point.latitude(), point.longitude(), distanceAlongRoute = currentPosition))
|
upcomingRoute.routePolyline,
|
||||||
|
currentPosition,
|
||||||
|
TurfConstants.UNIT_METERS
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
GpsCoordinates(
|
||||||
|
point.latitude(),
|
||||||
|
point.longitude(),
|
||||||
|
distanceAlongRoute = currentPosition
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
lastRequestedPosition = currentPosition
|
lastRequestedPosition = currentPosition
|
||||||
currentPosition += distancePerHour
|
currentPosition += distancePerHour
|
||||||
}
|
}
|
||||||
|
|
||||||
if (upcomingRoute.routeLength > lastRequestedPosition + 5_000){
|
if (upcomingRoute.routeLength > lastRequestedPosition + 5_000) {
|
||||||
val point = TurfMeasurement.along(upcomingRoute.routePolyline, upcomingRoute.routeLength, TurfConstants.UNIT_METERS)
|
val point = TurfMeasurement.along(
|
||||||
add(GpsCoordinates(point.latitude(), point.longitude(), distanceAlongRoute = upcomingRoute.routeLength))
|
upcomingRoute.routePolyline,
|
||||||
|
upcomingRoute.routeLength,
|
||||||
|
TurfConstants.UNIT_METERS
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
GpsCoordinates(
|
||||||
|
point.latitude(),
|
||||||
|
point.longitude(),
|
||||||
|
distanceAlongRoute = upcomingRoute.routeLength
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -2,9 +2,6 @@ package de.timklge.karooheadwind
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
|
||||||
import de.timklge.karooheadwind.screens.PrecipitationUnit
|
|
||||||
import de.timklge.karooheadwind.screens.TemperatureUnit
|
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.models.HttpResponseState
|
import io.hammerhead.karooext.models.HttpResponseState
|
||||||
import io.hammerhead.karooext.models.OnHttpResponse
|
import io.hammerhead.karooext.models.OnHttpResponse
|
||||||
|
|||||||
@ -7,12 +7,11 @@ import androidx.compose.ui.unit.DpSize
|
|||||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||||
import androidx.glance.appwidget.GlanceRemoteViews
|
import androidx.glance.appwidget.GlanceRemoteViews
|
||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
|
import de.timklge.karooheadwind.WindDirectionIndicatorSetting
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
|
||||||
import de.timklge.karooheadwind.screens.WindDirectionIndicatorSetting
|
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamDataFlow
|
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
@ -30,8 +29,6 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,6 @@ fun getArrowBitmapByBearing(baseBitmap: Bitmap, bearing: Int): Bitmap {
|
|||||||
|
|
||||||
canvas.save()
|
canvas.save()
|
||||||
canvas.scale((bitmap.width / baseBitmap.width.toFloat()), (bitmap.height / baseBitmap.height.toFloat()), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat())
|
canvas.scale((bitmap.width / baseBitmap.width.toFloat()), (bitmap.height / baseBitmap.height.toFloat()), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat())
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Drawing arrow at $bearingRounded")
|
|
||||||
canvas.rotate(bearingRounded.toFloat(), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat())
|
canvas.rotate(bearingRounded.toFloat(), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat())
|
||||||
canvas.drawBitmap(baseBitmap, ((bitmap.width - baseBitmap.width) / 2).toFloat(), ((bitmap.height - baseBitmap.height) / 2).toFloat(), paint)
|
canvas.drawBitmap(baseBitmap, ((bitmap.width - baseBitmap.width) / 2).toFloat(), ((bitmap.height - baseBitmap.height) / 2).toFloat(), paint)
|
||||||
canvas.restore()
|
canvas.restore()
|
||||||
|
|||||||
@ -2,9 +2,9 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
|||||||
@ -12,12 +12,12 @@ import androidx.core.graphics.ColorUtils
|
|||||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||||
import androidx.glance.appwidget.GlanceRemoteViews
|
import androidx.glance.appwidget.GlanceRemoteViews
|
||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.R
|
import de.timklge.karooheadwind.R
|
||||||
|
import de.timklge.karooheadwind.WindDirectionIndicatorSetting
|
||||||
|
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
|
||||||
import de.timklge.karooheadwind.screens.WindDirectionIndicatorSetting
|
|
||||||
import de.timklge.karooheadwind.screens.WindDirectionIndicatorTextSetting
|
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamDataFlow
|
import de.timklge.karooheadwind.streamDataFlow
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
|
|||||||
@ -2,10 +2,10 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||||
|
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
|
||||||
import de.timklge.karooheadwind.screens.WindDirectionIndicatorTextSetting
|
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
|||||||
@ -20,8 +20,8 @@ import androidx.glance.text.Text
|
|||||||
import androidx.glance.text.TextAlign
|
import androidx.glance.text.TextAlign
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
|
||||||
|
|
||||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings: HeadwindSettings?, headingResponse: HeadingResponse?): RemoteViewsCompositionResult {
|
suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings: HeadwindSettings?, headingResponse: HeadingResponse?): RemoteViewsCompositionResult {
|
||||||
|
|||||||
@ -11,13 +11,13 @@ import androidx.glance.layout.Alignment
|
|||||||
import androidx.glance.layout.Box
|
import androidx.glance.layout.Box
|
||||||
import androidx.glance.layout.fillMaxSize
|
import androidx.glance.layout.fillMaxSize
|
||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||||
import de.timklge.karooheadwind.OpenMeteoData
|
import de.timklge.karooheadwind.OpenMeteoData
|
||||||
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
import de.timklge.karooheadwind.getHeadingFlow
|
import de.timklge.karooheadwind.getHeadingFlow
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
|
||||||
import de.timklge.karooheadwind.screens.TemperatureUnit
|
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import de.timklge.karooheadwind.streamUserProfile
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
@ -83,7 +83,8 @@ class WeatherDataType(
|
|||||||
0.0, 0.0, "Europe/Berlin", 30.0, 0,
|
0.0, 0.0, "Europe/Berlin", 30.0, 0,
|
||||||
|
|
||||||
null
|
null
|
||||||
), HeadwindSettings()))
|
), HeadwindSettings()
|
||||||
|
))
|
||||||
|
|
||||||
delay(5_000)
|
delay(5_000)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,17 +20,17 @@ import androidx.glance.layout.fillMaxHeight
|
|||||||
import androidx.glance.layout.fillMaxSize
|
import androidx.glance.layout.fillMaxSize
|
||||||
import androidx.glance.layout.width
|
import androidx.glance.layout.width
|
||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
|
import de.timklge.karooheadwind.HeadwindWidgetSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||||
import de.timklge.karooheadwind.OpenMeteoData
|
import de.timklge.karooheadwind.OpenMeteoData
|
||||||
import de.timklge.karooheadwind.OpenMeteoForecastData
|
import de.timklge.karooheadwind.OpenMeteoForecastData
|
||||||
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.UpcomingRoute
|
import de.timklge.karooheadwind.UpcomingRoute
|
||||||
import de.timklge.karooheadwind.WeatherDataResponse
|
import de.timklge.karooheadwind.WeatherDataResponse
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
import de.timklge.karooheadwind.getHeadingFlow
|
import de.timklge.karooheadwind.getHeadingFlow
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
|
||||||
import de.timklge.karooheadwind.screens.HeadwindWidgetSettings
|
|
||||||
import de.timklge.karooheadwind.screens.TemperatureUnit
|
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import de.timklge.karooheadwind.streamUpcomingRoute
|
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||||
|
|||||||
@ -31,8 +31,8 @@ import androidx.glance.text.Text
|
|||||||
import androidx.glance.text.TextAlign
|
import androidx.glance.text.TextAlign
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.timklge.karooheadwind.R
|
import de.timklge.karooheadwind.R
|
||||||
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
import de.timklge.karooheadwind.screens.TemperatureUnit
|
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,6 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.MenuAnchorType
|
import androidx.compose.material3.MenuAnchorType
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|||||||
@ -1,135 +1,44 @@
|
|||||||
package de.timklge.karooheadwind.screens
|
package de.timklge.karooheadwind.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
|
||||||
import androidx.compose.material.icons.filled.Done
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.FilledTonalButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
import de.timklge.karooheadwind.R
|
||||||
import de.timklge.karooheadwind.getGpsCoordinateFlow
|
|
||||||
import de.timklge.karooheadwind.saveSettings
|
import de.timklge.karooheadwind.saveSettings
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import de.timklge.karooheadwind.streamStats
|
|
||||||
import de.timklge.karooheadwind.streamUserProfile
|
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.models.UserProfile
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.time.temporal.ChronoUnit
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
enum class WindUnit(val id: String, val label: String, val unitDisplay: String){
|
|
||||||
KILOMETERS_PER_HOUR("kmh", "Kilometers (km/h)", "km/h"),
|
|
||||||
METERS_PER_SECOND("ms", "Meters (m/s)", "m/s"),
|
|
||||||
MILES_PER_HOUR("mph", "Miles (mph)", "mph"),
|
|
||||||
KNOTS("kn", "Knots (kn)", "kn")
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class PrecipitationUnit(val id: String, val label: String, val unitDisplay: String){
|
|
||||||
MILLIMETERS("mm", "Millimeters (mm)", "mm"),
|
|
||||||
INCH("inch", "Inch", "in")
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class WindDirectionIndicatorTextSetting(val id: String, val label: String){
|
|
||||||
HEADWIND_SPEED("headwind-speed", "Headwind speed"),
|
|
||||||
WIND_SPEED("absolute-wind-speed", "Absolute wind speed"),
|
|
||||||
NONE("none", "None")
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class WindDirectionIndicatorSetting(val id: String, val label: String){
|
|
||||||
HEADWIND_DIRECTION("headwind-direction", "Headwind"),
|
|
||||||
WIND_DIRECTION("wind-direction", "Absolute wind direction"),
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class TemperatureUnit(val id: String, val label: String, val unitDisplay: String){
|
|
||||||
CELSIUS("celsius", "Celsius (°C)", "°C"),
|
|
||||||
FAHRENHEIT("fahrenheit", "Fahrenheit (°F)", "°F")
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class RoundLocationSetting(val id: String, val label: String, val km: Int){
|
|
||||||
KM_1("1 km", "1 km", 1),
|
|
||||||
KM_2("2 km", "2 km", 2),
|
|
||||||
KM_5("5 km", "5 km", 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class HeadwindSettings(
|
|
||||||
val windUnit: WindUnit = WindUnit.KILOMETERS_PER_HOUR,
|
|
||||||
val welcomeDialogAccepted: Boolean = false,
|
|
||||||
val windDirectionIndicatorTextSetting: WindDirectionIndicatorTextSetting = WindDirectionIndicatorTextSetting.HEADWIND_SPEED,
|
|
||||||
val windDirectionIndicatorSetting: WindDirectionIndicatorSetting = WindDirectionIndicatorSetting.HEADWIND_DIRECTION,
|
|
||||||
val roundLocationTo: RoundLocationSetting = RoundLocationSetting.KM_2,
|
|
||||||
val forecastedKmPerHour: Int = 20,
|
|
||||||
val forecastedMilesPerHour: Int = 12,
|
|
||||||
){
|
|
||||||
companion object {
|
|
||||||
val defaultSettings = Json.encodeToString(HeadwindSettings())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getForecastMetersPerHour(isImperial: Boolean): Int {
|
|
||||||
return if (isImperial) forecastedMilesPerHour * 1609 else forecastedKmPerHour * 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class HeadwindWidgetSettings(
|
|
||||||
val currentForecastHourOffset: Int = 0
|
|
||||||
){
|
|
||||||
companion object {
|
|
||||||
val defaultWidgetSettings = Json.encodeToString(HeadwindWidgetSettings())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class HeadwindStats(
|
|
||||||
val lastSuccessfulWeatherRequest: Long? = null,
|
|
||||||
val lastSuccessfulWeatherPosition: GpsCoordinates? = null,
|
|
||||||
val failedWeatherRequest: Long? = null,
|
|
||||||
){
|
|
||||||
companion object {
|
|
||||||
val defaultStats = Json.encodeToString(HeadwindStats())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(onFinish: () -> Unit) {
|
fun MainScreen(onFinish: () -> Unit) {
|
||||||
var karooConnected by remember { mutableStateOf(false) }
|
var karooConnected by remember { mutableStateOf(false) }
|
||||||
@ -137,30 +46,20 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val karooSystem = remember { KarooSystemService(ctx) }
|
val karooSystem = remember { KarooSystemService(ctx) }
|
||||||
|
|
||||||
var selectedWindUnit by remember { mutableStateOf(WindUnit.KILOMETERS_PER_HOUR) }
|
|
||||||
var welcomeDialogVisible by remember { mutableStateOf(false) }
|
var welcomeDialogVisible by remember { mutableStateOf(false) }
|
||||||
var selectedWindDirectionIndicatorTextSetting by remember { mutableStateOf(WindDirectionIndicatorTextSetting.HEADWIND_SPEED) }
|
var tabIndex by remember { mutableIntStateOf(0) }
|
||||||
var selectedWindDirectionIndicatorSetting by remember { mutableStateOf(WindDirectionIndicatorSetting.HEADWIND_DIRECTION) }
|
|
||||||
var selectedRoundLocationSetting by remember { mutableStateOf(RoundLocationSetting.KM_2) }
|
|
||||||
var forecastKmPerHour by remember { mutableStateOf("20") }
|
|
||||||
var forecastMilesPerHour by remember { mutableStateOf("12") }
|
|
||||||
|
|
||||||
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
|
val tabs = listOf("Weather", "Settings")
|
||||||
val stats by ctx.streamStats().collectAsStateWithLifecycle(HeadwindStats())
|
|
||||||
val location by karooSystem.getGpsCoordinateFlow(ctx).collectAsStateWithLifecycle(null)
|
|
||||||
|
|
||||||
var savedDialogVisible by remember { mutableStateOf(false) }
|
DisposableEffect(Unit) {
|
||||||
var exitDialogVisible by remember { mutableStateOf(false) }
|
onDispose {
|
||||||
|
karooSystem.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
ctx.streamSettings(karooSystem).collect { settings ->
|
ctx.streamSettings(karooSystem).collect { settings ->
|
||||||
selectedWindUnit = settings.windUnit
|
|
||||||
welcomeDialogVisible = !settings.welcomeDialogAccepted
|
welcomeDialogVisible = !settings.welcomeDialogAccepted
|
||||||
selectedWindDirectionIndicatorTextSetting = settings.windDirectionIndicatorTextSetting
|
|
||||||
selectedWindDirectionIndicatorSetting = settings.windDirectionIndicatorSetting
|
|
||||||
selectedRoundLocationSetting = settings.roundLocationTo
|
|
||||||
forecastKmPerHour = settings.forecastedKmPerHour.toString()
|
|
||||||
forecastMilesPerHour = settings.forecastedMilesPerHour.toString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,153 +69,32 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Column(modifier = Modifier
|
Column(modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.background)) {
|
.background(MaterialTheme.colorScheme.background)) {
|
||||||
TopAppBar(title = { Text("Headwind") })
|
|
||||||
Column(modifier = Modifier
|
|
||||||
.padding(5.dp)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
|
||||||
|
|
||||||
val windDirectionIndicatorSettingDropdownOptions = WindDirectionIndicatorSetting.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
val windDirectionIndicatorSettingSelection by remember(selectedWindDirectionIndicatorSetting) {
|
TabRow(selectedTabIndex = tabIndex) {
|
||||||
mutableStateOf(windDirectionIndicatorSettingDropdownOptions.find { option -> option.id == selectedWindDirectionIndicatorSetting.id }!!)
|
tabs.forEachIndexed { index, title ->
|
||||||
}
|
Tab(text = { Text(title) },
|
||||||
Dropdown(label = "Wind direction indicator", options = windDirectionIndicatorSettingDropdownOptions, selected = windDirectionIndicatorSettingSelection) { selectedOption ->
|
selected = tabIndex == index,
|
||||||
selectedWindDirectionIndicatorSetting = WindDirectionIndicatorSetting.entries.find { unit -> unit.id == selectedOption.id }!!
|
onClick = { tabIndex = index }
|
||||||
}
|
|
||||||
|
|
||||||
val windDirectionIndicatorTextSettingDropdownOptions = WindDirectionIndicatorTextSetting.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
|
|
||||||
val windDirectionIndicatorTextSettingSelection by remember(selectedWindDirectionIndicatorTextSetting) {
|
|
||||||
mutableStateOf(windDirectionIndicatorTextSettingDropdownOptions.find { option -> option.id == selectedWindDirectionIndicatorTextSetting.id }!!)
|
|
||||||
}
|
|
||||||
Dropdown(label = "Text on headwind indicator", options = windDirectionIndicatorTextSettingDropdownOptions, selected = windDirectionIndicatorTextSettingSelection) { selectedOption ->
|
|
||||||
selectedWindDirectionIndicatorTextSetting = WindDirectionIndicatorTextSetting.entries.find { unit -> unit.id == selectedOption.id }!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val windSpeedUnitDropdownOptions = WindUnit.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
|
|
||||||
val windSpeedUnitInitialSelection by remember(selectedWindUnit) {
|
|
||||||
mutableStateOf(windSpeedUnitDropdownOptions.find { option -> option.id == selectedWindUnit.id }!!)
|
|
||||||
}
|
|
||||||
Dropdown(label = "Wind Speed Unit", options = windSpeedUnitDropdownOptions, selected = windSpeedUnitInitialSelection) { selectedOption ->
|
|
||||||
selectedWindUnit = WindUnit.entries.find { unit -> unit.id == selectedOption.id }!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val roundLocationDropdownOptions = RoundLocationSetting.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
|
|
||||||
val roundLocationInitialSelection by remember(selectedRoundLocationSetting) {
|
|
||||||
mutableStateOf(roundLocationDropdownOptions.find { option -> option.id == selectedRoundLocationSetting.id }!!)
|
|
||||||
}
|
|
||||||
Dropdown(label = "Round Location", options = roundLocationDropdownOptions, selected = roundLocationInitialSelection) { selectedOption ->
|
|
||||||
selectedRoundLocationSetting = RoundLocationSetting.entries.find { unit -> unit.id == selectedOption.id }!!
|
|
||||||
}
|
|
||||||
|
|
||||||
if (profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL){
|
|
||||||
OutlinedTextField(value = forecastMilesPerHour, modifier = Modifier.fillMaxWidth(),
|
|
||||||
onValueChange = { forecastMilesPerHour = it },
|
|
||||||
label = { Text("Forecast Distance per Hour") },
|
|
||||||
suffix = { Text("mi") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
|
||||||
singleLine = true
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
OutlinedTextField(value = forecastKmPerHour, modifier = Modifier.fillMaxWidth(),
|
|
||||||
onValueChange = { forecastKmPerHour = it },
|
|
||||||
label = { Text("Forecast Distance per Hour") },
|
|
||||||
suffix = { Text("km") },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
|
||||||
singleLine = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
FilledTonalButton(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(50.dp), onClick = {
|
|
||||||
val newSettings = HeadwindSettings(windUnit = selectedWindUnit,
|
|
||||||
welcomeDialogAccepted = true,
|
|
||||||
windDirectionIndicatorSetting = selectedWindDirectionIndicatorSetting,
|
|
||||||
windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting,
|
|
||||||
roundLocationTo = selectedRoundLocationSetting,
|
|
||||||
forecastedMilesPerHour = forecastMilesPerHour.toIntOrNull()?.coerceIn(3, 30) ?: 12,
|
|
||||||
forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20)
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
saveSettings(ctx, newSettings)
|
|
||||||
savedDialogVisible = true
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Default.Done, contentDescription = "Save")
|
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
|
||||||
Text("Save")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!karooConnected){
|
|
||||||
Text(modifier = Modifier.padding(5.dp), text = "Could not read device status. Is your Karoo updated?")
|
|
||||||
}
|
|
||||||
|
|
||||||
val lastPosition = location?.let { l -> stats.lastSuccessfulWeatherPosition?.distanceTo(l) }
|
|
||||||
val lastPositionDistanceStr = lastPosition?.let { dist -> " (${dist.roundToInt()} km away)" } ?: ""
|
|
||||||
|
|
||||||
if (stats.failedWeatherRequest != null && (stats.lastSuccessfulWeatherRequest == null || stats.failedWeatherRequest!! > stats.lastSuccessfulWeatherRequest!!)){
|
|
||||||
val successfulTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.lastSuccessfulWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalTime().truncatedTo(
|
|
||||||
ChronoUnit.SECONDS)
|
|
||||||
val successfulDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.lastSuccessfulWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalDate()
|
|
||||||
val lastTryTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.failedWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalTime().truncatedTo(
|
|
||||||
ChronoUnit.SECONDS)
|
|
||||||
val lastTryDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.failedWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalDate()
|
|
||||||
|
|
||||||
val successStr = if(lastPosition != null) " Last data received at $successfulDate ${successfulTime}${lastPositionDistanceStr}." else ""
|
|
||||||
Text(modifier = Modifier.padding(5.dp), text = "Failed to update weather data; last try at $lastTryDate ${lastTryTime}.${successStr}")
|
|
||||||
} else if(stats.lastSuccessfulWeatherRequest != null){
|
|
||||||
val localDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.lastSuccessfulWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalDate()
|
|
||||||
val localTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(stats.lastSuccessfulWeatherRequest ?: 0), ZoneOffset.systemDefault()).toLocalTime().truncatedTo(
|
|
||||||
ChronoUnit.SECONDS)
|
|
||||||
|
|
||||||
Text(modifier = Modifier.padding(5.dp), text = "Last weather data received at $localDate ${localTime}${lastPositionDistanceStr}")
|
|
||||||
} else {
|
|
||||||
Text(modifier = Modifier.padding(5.dp), text = "No weather data received yet, waiting for GPS fix...")
|
|
||||||
}
|
|
||||||
|
|
||||||
FilledTonalButton(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(50.dp), onClick = {
|
|
||||||
exitDialogVisible = true
|
|
||||||
}) {
|
|
||||||
Icon(Icons.AutoMirrored.Default.ExitToApp, contentDescription = "Exit")
|
|
||||||
Spacer(modifier = Modifier.width(5.dp))
|
|
||||||
Text("Exit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exitDialogVisible) {
|
|
||||||
AlertDialog(onDismissRequest = { exitDialogVisible = false },
|
|
||||||
confirmButton = { Button(onClick = {
|
|
||||||
onFinish()
|
|
||||||
}) { Text("Yes") } },
|
|
||||||
dismissButton = { Button(onClick = {
|
|
||||||
exitDialogVisible = false
|
|
||||||
}) { Text("No") } },
|
|
||||||
text = { Text("Do you really want to exit?") }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
when (tabIndex) {
|
||||||
if (savedDialogVisible){
|
0 -> WeatherScreen(onFinish)
|
||||||
AlertDialog(onDismissRequest = { savedDialogVisible = false },
|
1 -> SettingsScreen(onFinish)
|
||||||
confirmButton = { Button(onClick = {
|
}
|
||||||
savedDialogVisible = false
|
}
|
||||||
}) { Text("OK") } },
|
|
||||||
text = { Text("Settings saved successfully.") }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (welcomeDialogVisible){
|
if (welcomeDialogVisible){
|
||||||
AlertDialog(onDismissRequest = { },
|
AlertDialog(onDismissRequest = { },
|
||||||
confirmButton = { Button(onClick = {
|
confirmButton = { Button(onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
saveSettings(ctx, HeadwindSettings(windUnit = selectedWindUnit,
|
saveSettings(ctx, HeadwindSettings(welcomeDialogAccepted = true))
|
||||||
welcomeDialogAccepted = true))
|
|
||||||
}
|
}
|
||||||
}) { Text("OK") } },
|
}) { Text("OK") } },
|
||||||
text = {
|
text = {
|
||||||
@ -334,4 +112,15 @@ fun MainScreen(onFinish: () -> Unit) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.back),
|
||||||
|
contentDescription = "Back",
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(bottom = 10.dp)
|
||||||
|
.size(54.dp)
|
||||||
|
.clickable { onFinish() }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,217 @@
|
|||||||
|
package de.timklge.karooheadwind.screens
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
|
import de.timklge.karooheadwind.RoundLocationSetting
|
||||||
|
import de.timklge.karooheadwind.WindDirectionIndicatorSetting
|
||||||
|
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
|
||||||
|
import de.timklge.karooheadwind.WindUnit
|
||||||
|
import de.timklge.karooheadwind.saveSettings
|
||||||
|
import de.timklge.karooheadwind.streamSettings
|
||||||
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(onFinish: () -> Unit) {
|
||||||
|
var karooConnected by remember { mutableStateOf(false) }
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
val karooSystem = remember { KarooSystemService(ctx) }
|
||||||
|
|
||||||
|
var selectedWindUnit by remember { mutableStateOf(WindUnit.KILOMETERS_PER_HOUR) }
|
||||||
|
var selectedWindDirectionIndicatorTextSetting by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
WindDirectionIndicatorTextSetting.HEADWIND_SPEED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var selectedWindDirectionIndicatorSetting by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
WindDirectionIndicatorSetting.HEADWIND_DIRECTION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var selectedRoundLocationSetting by remember { mutableStateOf(RoundLocationSetting.KM_2) }
|
||||||
|
var forecastKmPerHour by remember { mutableStateOf("20") }
|
||||||
|
var forecastMilesPerHour by remember { mutableStateOf("12") }
|
||||||
|
|
||||||
|
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
ctx.streamSettings(karooSystem).collect { settings ->
|
||||||
|
selectedWindUnit = settings.windUnit
|
||||||
|
selectedWindDirectionIndicatorTextSetting = settings.windDirectionIndicatorTextSetting
|
||||||
|
selectedWindDirectionIndicatorSetting = settings.windDirectionIndicatorSetting
|
||||||
|
selectedRoundLocationSetting = settings.roundLocationTo
|
||||||
|
forecastKmPerHour = settings.forecastedKmPerHour.toString()
|
||||||
|
forecastMilesPerHour = settings.forecastedMilesPerHour.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
karooSystem.connect { connected ->
|
||||||
|
karooConnected = connected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
karooSystem.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateSettings(){
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Saving settings")
|
||||||
|
|
||||||
|
val newSettings = HeadwindSettings(
|
||||||
|
windUnit = selectedWindUnit,
|
||||||
|
welcomeDialogAccepted = true,
|
||||||
|
windDirectionIndicatorSetting = selectedWindDirectionIndicatorSetting,
|
||||||
|
windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting,
|
||||||
|
roundLocationTo = selectedRoundLocationSetting,
|
||||||
|
forecastedMilesPerHour = forecastMilesPerHour.toIntOrNull()?.coerceIn(3, 30) ?: 12,
|
||||||
|
forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20
|
||||||
|
)
|
||||||
|
|
||||||
|
saveSettings(ctx, newSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
runBlocking {
|
||||||
|
updateSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler {
|
||||||
|
coroutineScope.launch {
|
||||||
|
updateSettings()
|
||||||
|
onFinish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(5.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
val windDirectionIndicatorSettingDropdownOptions =
|
||||||
|
WindDirectionIndicatorSetting.entries.toList()
|
||||||
|
.map { unit -> DropdownOption(unit.id, unit.label) }
|
||||||
|
val windDirectionIndicatorSettingSelection by remember(selectedWindDirectionIndicatorSetting) {
|
||||||
|
mutableStateOf(windDirectionIndicatorSettingDropdownOptions.find { option -> option.id == selectedWindDirectionIndicatorSetting.id }!!)
|
||||||
|
}
|
||||||
|
Dropdown(
|
||||||
|
label = "Wind direction indicator",
|
||||||
|
options = windDirectionIndicatorSettingDropdownOptions,
|
||||||
|
selected = windDirectionIndicatorSettingSelection
|
||||||
|
) { selectedOption ->
|
||||||
|
selectedWindDirectionIndicatorSetting =
|
||||||
|
WindDirectionIndicatorSetting.entries.find { unit -> unit.id == selectedOption.id }!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val windDirectionIndicatorTextSettingDropdownOptions =
|
||||||
|
WindDirectionIndicatorTextSetting.entries.toList()
|
||||||
|
.map { unit -> DropdownOption(unit.id, unit.label) }
|
||||||
|
val windDirectionIndicatorTextSettingSelection by remember(
|
||||||
|
selectedWindDirectionIndicatorTextSetting
|
||||||
|
) {
|
||||||
|
mutableStateOf(windDirectionIndicatorTextSettingDropdownOptions.find { option -> option.id == selectedWindDirectionIndicatorTextSetting.id }!!)
|
||||||
|
}
|
||||||
|
Dropdown(
|
||||||
|
label = "Text on headwind indicator",
|
||||||
|
options = windDirectionIndicatorTextSettingDropdownOptions,
|
||||||
|
selected = windDirectionIndicatorTextSettingSelection
|
||||||
|
) { selectedOption ->
|
||||||
|
selectedWindDirectionIndicatorTextSetting =
|
||||||
|
WindDirectionIndicatorTextSetting.entries.find { unit -> unit.id == selectedOption.id }!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val windSpeedUnitDropdownOptions =
|
||||||
|
WindUnit.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
|
||||||
|
val windSpeedUnitInitialSelection by remember(selectedWindUnit) {
|
||||||
|
mutableStateOf(windSpeedUnitDropdownOptions.find { option -> option.id == selectedWindUnit.id }!!)
|
||||||
|
}
|
||||||
|
Dropdown(
|
||||||
|
label = "Wind Speed Unit",
|
||||||
|
options = windSpeedUnitDropdownOptions,
|
||||||
|
selected = windSpeedUnitInitialSelection
|
||||||
|
) { selectedOption ->
|
||||||
|
selectedWindUnit = WindUnit.entries.find { unit -> unit.id == selectedOption.id }!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val roundLocationDropdownOptions = RoundLocationSetting.entries.toList()
|
||||||
|
.map { unit -> DropdownOption(unit.id, unit.label) }
|
||||||
|
val roundLocationInitialSelection by remember(selectedRoundLocationSetting) {
|
||||||
|
mutableStateOf(roundLocationDropdownOptions.find { option -> option.id == selectedRoundLocationSetting.id }!!)
|
||||||
|
}
|
||||||
|
Dropdown(
|
||||||
|
label = "Round Location",
|
||||||
|
options = roundLocationDropdownOptions,
|
||||||
|
selected = roundLocationInitialSelection
|
||||||
|
) { selectedOption ->
|
||||||
|
selectedRoundLocationSetting =
|
||||||
|
RoundLocationSetting.entries.find { unit -> unit.id == selectedOption.id }!!
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = forecastMilesPerHour, modifier = Modifier.fillMaxWidth(),
|
||||||
|
onValueChange = { forecastMilesPerHour = it },
|
||||||
|
label = { Text("Forecast Distance per Hour") },
|
||||||
|
suffix = { Text("mi") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = forecastKmPerHour, modifier = Modifier.fillMaxWidth(),
|
||||||
|
onValueChange = { forecastKmPerHour = it },
|
||||||
|
label = { Text("Forecast Distance per Hour") },
|
||||||
|
suffix = { Text("km") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!karooConnected) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(5.dp),
|
||||||
|
text = "Could not read device status. Is your Karoo updated?"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.padding(30.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,234 @@
|
|||||||
|
package de.timklge.karooheadwind.screens
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import de.timklge.karooheadwind.HeadwindStats
|
||||||
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
|
import de.timklge.karooheadwind.PrecipitationUnit
|
||||||
|
import de.timklge.karooheadwind.R
|
||||||
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
|
import de.timklge.karooheadwind.datatypes.WeatherDataType.Companion.timeFormatter
|
||||||
|
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
||||||
|
import de.timklge.karooheadwind.getGpsCoordinateFlow
|
||||||
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
|
import de.timklge.karooheadwind.streamStats
|
||||||
|
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||||
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.FormatStyle
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WeatherScreen(onFinish: () -> Unit) {
|
||||||
|
var karooConnected by remember { mutableStateOf<Boolean?>(null) }
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
val karooSystem = remember { KarooSystemService(ctx) }
|
||||||
|
|
||||||
|
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
|
||||||
|
val stats by ctx.streamStats().collectAsStateWithLifecycle(HeadwindStats())
|
||||||
|
val location by karooSystem.getGpsCoordinateFlow(ctx).collectAsStateWithLifecycle(null)
|
||||||
|
val weatherData by ctx.streamCurrentWeatherData().collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
|
val baseBitmap = BitmapFactory.decodeResource(
|
||||||
|
ctx.resources,
|
||||||
|
R.drawable.arrow_0
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
karooSystem.connect { connected ->
|
||||||
|
karooConnected = connected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
karooSystem.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(5.dp)) {
|
||||||
|
if (karooConnected == false) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(5.dp),
|
||||||
|
text = "Could not read device status. Is your Karoo updated?"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentWeatherData = weatherData.firstOrNull()?.data
|
||||||
|
val requestedWeatherPosition = weatherData.firstOrNull()?.requestedPosition
|
||||||
|
|
||||||
|
val formattedTime = currentWeatherData?.let { timeFormatter.format(Instant.ofEpochSecond(currentWeatherData.current.time)) }
|
||||||
|
val formattedDate = currentWeatherData?.let { Instant.ofEpochSecond(currentWeatherData.current.time).atZone(ZoneId.systemDefault()).toLocalDate().format(
|
||||||
|
DateTimeFormatter.ofLocalizedDate(
|
||||||
|
FormatStyle.SHORT))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (karooConnected == true && currentWeatherData != null) {
|
||||||
|
WeatherWidget(
|
||||||
|
dateLabel = formattedDate,
|
||||||
|
timeLabel = formattedTime,
|
||||||
|
baseBitmap = baseBitmap,
|
||||||
|
current = WeatherInterpretation.fromWeatherCode(currentWeatherData.current.weatherCode),
|
||||||
|
windBearing = currentWeatherData.current.windDirection.roundToInt(),
|
||||||
|
windSpeed = currentWeatherData.current.windSpeed.roundToInt(),
|
||||||
|
windGusts = currentWeatherData.current.windGusts.roundToInt(),
|
||||||
|
precipitation = currentWeatherData.current.precipitation,
|
||||||
|
temperature = currentWeatherData.current.temperature.toInt(),
|
||||||
|
temperatureUnit = if(profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||||
|
isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL,
|
||||||
|
precipitationUnit = if (profile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH,
|
||||||
|
distance = requestedWeatherPosition?.let { l -> location?.distanceTo(l)?.times(1000) },
|
||||||
|
includeDistanceLabel = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val lastPosition = location?.let { l -> stats.lastSuccessfulWeatherPosition?.distanceTo(l) }
|
||||||
|
val lastPositionDistanceStr =
|
||||||
|
lastPosition?.let { dist -> " (${dist.roundToInt()} km away)" } ?: ""
|
||||||
|
|
||||||
|
if (stats.failedWeatherRequest != null && (stats.lastSuccessfulWeatherRequest == null || stats.failedWeatherRequest!! > stats.lastSuccessfulWeatherRequest!!)) {
|
||||||
|
val successfulTime = LocalDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(
|
||||||
|
stats.lastSuccessfulWeatherRequest ?: 0
|
||||||
|
), ZoneOffset.systemDefault()
|
||||||
|
).toLocalTime().truncatedTo(
|
||||||
|
ChronoUnit.SECONDS
|
||||||
|
)
|
||||||
|
val successfulDate = LocalDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(
|
||||||
|
stats.lastSuccessfulWeatherRequest ?: 0
|
||||||
|
), ZoneOffset.systemDefault()
|
||||||
|
).toLocalDate()
|
||||||
|
val lastTryTime = LocalDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(stats.failedWeatherRequest ?: 0),
|
||||||
|
ZoneOffset.systemDefault()
|
||||||
|
).toLocalTime().truncatedTo(
|
||||||
|
ChronoUnit.SECONDS
|
||||||
|
)
|
||||||
|
val lastTryDate = LocalDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(stats.failedWeatherRequest ?: 0),
|
||||||
|
ZoneOffset.systemDefault()
|
||||||
|
).toLocalDate()
|
||||||
|
|
||||||
|
val successStr =
|
||||||
|
if (lastPosition != null) " Last data received at $successfulDate ${successfulTime}${lastPositionDistanceStr}." else ""
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(5.dp),
|
||||||
|
text = "Failed to update weather data; last try at $lastTryDate ${lastTryTime}.${successStr}"
|
||||||
|
)
|
||||||
|
} else if (stats.lastSuccessfulWeatherRequest != null) {
|
||||||
|
val localDate = LocalDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(
|
||||||
|
stats.lastSuccessfulWeatherRequest ?: 0
|
||||||
|
), ZoneOffset.systemDefault()
|
||||||
|
).toLocalDate()
|
||||||
|
val localTime = LocalDateTime.ofInstant(
|
||||||
|
Instant.ofEpochMilli(
|
||||||
|
stats.lastSuccessfulWeatherRequest ?: 0
|
||||||
|
), ZoneOffset.systemDefault()
|
||||||
|
).toLocalTime().truncatedTo(
|
||||||
|
ChronoUnit.SECONDS
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(5.dp),
|
||||||
|
text = "Last weather data received at $localDate ${localTime}${lastPositionDistanceStr}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(5.dp),
|
||||||
|
text = "No weather data received yet, waiting for GPS fix..."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val upcomingRoute by karooSystem.streamUpcomingRoute().collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
for (index in 1..12){
|
||||||
|
val positionIndex = if (weatherData.size == 1) 0 else index
|
||||||
|
|
||||||
|
if (weatherData.getOrNull(positionIndex) == null) break
|
||||||
|
if (index >= (weatherData.getOrNull(positionIndex)?.data?.forecastData?.weatherCode?.size ?: 0)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
val data = weatherData.getOrNull(positionIndex)?.data
|
||||||
|
val distanceAlongRoute = weatherData.getOrNull(positionIndex)?.requestedPosition?.distanceAlongRoute
|
||||||
|
val position = weatherData.getOrNull(positionIndex)?.requestedPosition?.let { "${(it.distanceAlongRoute?.div(1000.0))?.toInt()} at ${it.lat}, ${it.lon}" }
|
||||||
|
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Distance along route index ${positionIndex}: $position")
|
||||||
|
|
||||||
|
if (index > 1) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
Color.Black
|
||||||
|
)
|
||||||
|
.height(1.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val distanceFromCurrent = upcomingRoute?.distanceAlongRoute?.let { currentDistanceAlongRoute ->
|
||||||
|
distanceAlongRoute?.minus(currentDistanceAlongRoute)
|
||||||
|
}
|
||||||
|
|
||||||
|
val interpretation = WeatherInterpretation.fromWeatherCode(data?.forecastData?.weatherCode?.get(index) ?: 0)
|
||||||
|
val unixTime = data?.forecastData?.time?.get(index) ?: 0
|
||||||
|
val formattedForecastTime = WeatherForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||||
|
val formattedForecastDate = Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||||
|
|
||||||
|
WeatherWidget(
|
||||||
|
baseBitmap,
|
||||||
|
current = interpretation,
|
||||||
|
windBearing = data?.forecastData?.windDirection?.get(index)?.roundToInt() ?: 0,
|
||||||
|
windSpeed = data?.forecastData?.windSpeed?.get(index)?.roundToInt() ?: 0,
|
||||||
|
windGusts = data?.forecastData?.windGusts?.get(index)?.roundToInt() ?: 0,
|
||||||
|
precipitation = data?.forecastData?.precipitation?.get(index) ?: 0.0,
|
||||||
|
precipitationProbability = data?.forecastData?.precipitationProbability?.get(index) ?: 0,
|
||||||
|
temperature = data?.forecastData?.temperature?.get(index)?.roundToInt() ?: 0,
|
||||||
|
temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||||
|
timeLabel = formattedForecastTime,
|
||||||
|
dateLabel = formattedForecastDate,
|
||||||
|
distance = distanceFromCurrent,
|
||||||
|
isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL,
|
||||||
|
precipitationUnit = if (profile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH,
|
||||||
|
includeDistanceLabel = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.padding(30.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
package de.timklge.karooheadwind.screens
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import de.timklge.karooheadwind.PrecipitationUnit
|
||||||
|
import de.timklge.karooheadwind.R
|
||||||
|
import de.timklge.karooheadwind.TemperatureUnit
|
||||||
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
|
import de.timklge.karooheadwind.datatypes.getArrowBitmapByBearing
|
||||||
|
import de.timklge.karooheadwind.datatypes.getWeatherIcon
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WeatherWidget(
|
||||||
|
baseBitmap: Bitmap,
|
||||||
|
current: WeatherInterpretation,
|
||||||
|
windBearing: Int,
|
||||||
|
windSpeed: Int,
|
||||||
|
windGusts: Int,
|
||||||
|
precipitation: Double,
|
||||||
|
temperature: Int,
|
||||||
|
temperatureUnit: TemperatureUnit,
|
||||||
|
timeLabel: String? = null,
|
||||||
|
dateLabel: String? = null,
|
||||||
|
distance: Double? = null,
|
||||||
|
includeDistanceLabel: Boolean = false,
|
||||||
|
precipitationProbability: Int? = null,
|
||||||
|
precipitationUnit: PrecipitationUnit,
|
||||||
|
isImperial: Boolean
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
if (dateLabel != null) {
|
||||||
|
Text(
|
||||||
|
text = dateLabel,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance != null) {
|
||||||
|
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||||
|
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||||
|
val text = if (includeDistanceLabel){
|
||||||
|
if(distanceInUserUnit > 0){
|
||||||
|
"In $label"
|
||||||
|
} else {
|
||||||
|
"$label ago"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
label
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeLabel != null) {
|
||||||
|
Text(
|
||||||
|
text = timeLabel,
|
||||||
|
style = TextStyle(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather icon (larger)
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = getWeatherIcon(current)),
|
||||||
|
contentDescription = "Current weather",
|
||||||
|
modifier = Modifier.size(72.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
// Temperature (larger)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.thermometer),
|
||||||
|
contentDescription = "Temperature",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${temperature}${temperatureUnit.unitDisplay}",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precipitation
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
) {
|
||||||
|
val precipitationProbabilityLabel =
|
||||||
|
if (precipitationProbability != null) "${precipitationProbability}% " else ""
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${precipitationProbabilityLabel}${ceil(precipitation).toInt()}${precipitationUnit.unitDisplay}",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wind
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
bitmap = getArrowBitmapByBearing(baseBitmap, windBearing).asImageBitmap(),
|
||||||
|
colorFilter = ColorFilter.tint(Color.Black),
|
||||||
|
contentDescription = "Wind direction",
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "$windSpeed,$windGusts",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/src/main/res/drawable/back.png
Normal file
BIN
app/src/main/res/drawable/back.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Loading…
x
Reference in New Issue
Block a user