Add tailwind data type (#79)

* Add tailwind data type

* Preview gust speed as multiple of wind Speed

* Fix gust speed is not propagated
This commit is contained in:
timklge 2025-03-27 19:53:16 +01:00 committed by GitHub
parent 5ee55ee8c5
commit 07c07b052e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 213 additions and 7 deletions

View File

@ -36,6 +36,7 @@ After installing this app on your Karoo and opening it once from the main menu,
- Headwind (graphical, 1x1 field): Shows the headwind direction and speed as a circle with a triangular direction indicator. The speed is shown at the center in your set unit of measurement (default is kilometers per hour if you have set up metric units in your Karoo, otherwise miles per hour). Both direction and speed are relative to the current riding direction by default, i. e., riding directly into a wind of 20 km/h will show a headwind speed of 20 km/h, while riding in the same direction will show -20 km/h. You can change this behavior in the app settings to show the absolute wind direction and speed instead. - Headwind (graphical, 1x1 field): Shows the headwind direction and speed as a circle with a triangular direction indicator. The speed is shown at the center in your set unit of measurement (default is kilometers per hour if you have set up metric units in your Karoo, otherwise miles per hour). Both direction and speed are relative to the current riding direction by default, i. e., riding directly into a wind of 20 km/h will show a headwind speed of 20 km/h, while riding in the same direction will show -20 km/h. You can change this behavior in the app settings to show the absolute wind direction and speed instead.
- Tailwind with riding speed (graphical, 1x1 field): Shows an arrow indicating the current headwind direction next to a label reading your current speed and the speed of the tailwind. If you ride against a headwind of 5 mph, it will show "-5". If you ride in the same direction of a 5 mph wind, it will read "+5". Text and arrow are colored based on the tailwind speed, with red indicating a strong headwind and green indicating a strong tailwind. - Tailwind with riding speed (graphical, 1x1 field): Shows an arrow indicating the current headwind direction next to a label reading your current speed and the speed of the tailwind. If you ride against a headwind of 5 mph, it will show "-5". If you ride in the same direction of a 5 mph wind, it will read "+5". Text and arrow are colored based on the tailwind speed, with red indicating a strong headwind and green indicating a strong tailwind.
- Tailwind (graphical, 1x1 field): Similar to the tailwind and riding speed field, but shows tailwind speed, wind speed and wind gust speed instead of riding speed.
- Weather forecast (graphical, 2x1 field): Shows three columns indicating the current weather conditions (sunny, cloudy, ...), wind direction, precipitation and temperature forecasted for the next three hours. Tap on this widget to cycle through the 12 hour forecast. If you have a route loaded, the forecast widget will show the forecasted weather along points of the route, with an estimated traveled distance per hour of 20 km / 12 miles by default. - Weather forecast (graphical, 2x1 field): Shows three columns indicating the current weather conditions (sunny, cloudy, ...), wind direction, precipitation and temperature forecasted for the next three hours. Tap on this widget to cycle through the 12 hour forecast. If you have a route loaded, the forecast widget will show the forecasted weather along points of the route, with an estimated traveled distance per hour of 20 km / 12 miles by default.
- Current weather (graphical, 1x1 field): Shows current weather conditions (same as forecast widget, but only for the current time). Tap on this widget to open the headwind app with a forecast overview. - Current weather (graphical, 1x1 field): Shows current weather conditions (same as forecast widget, but only for the current time). Tap on this widget to open the headwind app with a forecast overview.
- Additionally, data fields that only show the current data value for headwind speed, humidity, cloud cover, absolute wind speed, absolute wind gust speed, absolute wind direction, rainfall and surface pressure can be added if desired. - Additionally, data fields that only show the current data value for headwind speed, humidity, cloud cover, absolute wind speed, absolute wind gust speed, absolute wind direction, rainfall and surface pressure can be added if desired.

View File

@ -16,6 +16,7 @@ import de.timklge.karooheadwind.datatypes.RelativeHumidityDataType
import de.timklge.karooheadwind.datatypes.SealevelPressureDataType 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.TailwindDataType
import de.timklge.karooheadwind.datatypes.TemperatureDataType import de.timklge.karooheadwind.datatypes.TemperatureDataType
import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType
import de.timklge.karooheadwind.datatypes.UserWindSpeedDataType import de.timklge.karooheadwind.datatypes.UserWindSpeedDataType
@ -85,7 +86,8 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
TemperatureForecastDataType(karooSystem), TemperatureForecastDataType(karooSystem),
PrecipitationForecastDataType(karooSystem), PrecipitationForecastDataType(karooSystem),
WindForecastDataType(karooSystem), WindForecastDataType(karooSystem),
GraphicalForecastDataType(karooSystem) GraphicalForecastDataType(karooSystem),
TailwindDataType(karooSystem, applicationContext)
) )
} }

View File

@ -0,0 +1,193 @@
package de.timklge.karooheadwind.datatypes
import android.content.Context
import android.graphics.BitmapFactory
import android.util.Log
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.DpSize
import androidx.core.content.ContextCompat
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews
import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.R
import de.timklge.karooheadwind.WindDirectionIndicatorSetting
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
import de.timklge.karooheadwind.getRelativeHeadingFlow
import de.timklge.karooheadwind.streamCurrentWeatherData
import de.timklge.karooheadwind.streamDataFlow
import de.timklge.karooheadwind.streamSettings
import de.timklge.karooheadwind.streamUserProfile
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.DataTypeImpl
import io.hammerhead.karooext.internal.ViewEmitter
import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.ShowCustomStreamState
import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.UpdateGraphicConfig
import io.hammerhead.karooext.models.UserProfile
import io.hammerhead.karooext.models.ViewConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
import kotlin.math.cos
import kotlin.math.roundToInt
class TailwindDataType(
private val karooSystem: KarooSystemService,
private val applicationContext: Context
) : DataTypeImpl("karoo-headwind", "tailwind") {
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
private val glance = GlanceRemoteViews()
data class StreamData(val headingResponse: HeadingResponse,
val absoluteWindDirection: Double?,
val windSpeed: Double?,
val settings: HeadwindSettings,
val rideSpeed: Double?,
val gustSpeed: Double?,
val isImperial: Boolean)
private fun previewFlow(profileFlow: Flow<UserProfile>): Flow<StreamData> {
return flow {
val profile = profileFlow.first()
while (true) {
val bearing = (0..360).random().toDouble()
val windSpeed = (0..20).random()
val rideSpeed = (10..40).random().toDouble()
val gustSpeed = windSpeed * ((10..40).random().toDouble() / 10)
val isImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
emit(StreamData(HeadingResponse.Value(bearing), bearing, windSpeed.toDouble(), HeadwindSettings(), rideSpeed, gustSpeed = gustSpeed, isImperial = isImperial))
delay(2_000)
}
}
}
private fun streamSpeedInMs(): Flow<Double> {
return karooSystem.streamDataFlow(DataType.Type.SMOOTHED_3S_AVERAGE_SPEED)
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue ?: 0.0 }
}
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
Log.d(KarooHeadwindExtension.TAG, "Starting headwind direction view with $emitter")
val baseBitmap = BitmapFactory.decodeResource(
context.resources,
R.drawable.arrow_0
)
val configJob = CoroutineScope(Dispatchers.IO).launch {
emitter.onNext(UpdateGraphicConfig(showHeader = false))
awaitCancellation()
}
val flow = if (config.preview) {
previewFlow(karooSystem.streamUserProfile())
} else {
combine(karooSystem.getRelativeHeadingFlow(context), context.streamCurrentWeatherData(), context.streamSettings(karooSystem), karooSystem.streamUserProfile(), streamSpeedInMs()) { headingResponse, weatherData, settings, userProfile, rideSpeedInMs ->
val isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
val absoluteWindDirection = weatherData.firstOrNull()?.data?.current?.windDirection
val windSpeed = weatherData.firstOrNull()?.data?.current?.windSpeed
val gustSpeed = weatherData.firstOrNull()?.data?.current?.windGusts
val rideSpeed = if (isImperial){
rideSpeedInMs * 2.23694
} else {
rideSpeedInMs * 3.6
}
StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, rideSpeed = rideSpeed, isImperial = isImperial, gustSpeed = gustSpeed)
}
}
val viewJob = CoroutineScope(Dispatchers.IO).launch {
emitter.onNext(ShowCustomStreamState("", null))
flow.collect { streamData ->
Log.d(KarooHeadwindExtension.TAG, "Updating tailwind direction view")
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
if (value == null || streamData.absoluteWindDirection == null || streamData.windSpeed == null){
var headingResponse = streamData.headingResponse
if (headingResponse is HeadingResponse.Value && (streamData.absoluteWindDirection == null || streamData.windSpeed == null)){
headingResponse = HeadingResponse.NoWeatherData
}
emitter.updateView(getErrorWidget(glance, context, streamData.settings, headingResponse).remoteViews)
return@collect
}
val windSpeed = streamData.windSpeed
val windDirection = when (streamData.settings.windDirectionIndicatorSetting){
WindDirectionIndicatorSetting.HEADWIND_DIRECTION -> streamData.headingResponse.diff
WindDirectionIndicatorSetting.WIND_DIRECTION -> streamData.absoluteWindDirection + 180
}
val mainText = when (streamData.settings.windDirectionIndicatorTextSetting) {
WindDirectionIndicatorTextSetting.HEADWIND_SPEED -> {
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
headwindSpeed.roundToInt().toString()
val sign = if (headwindSpeed < 0) "+" else {
if (headwindSpeed > 0) "-" else ""
}
"$sign${headwindSpeed.roundToInt().absoluteValue}"
}
WindDirectionIndicatorTextSetting.WIND_SPEED -> windSpeed.roundToInt().toString()
WindDirectionIndicatorTextSetting.NONE -> ""
}
val subtext = "${windSpeed.roundToInt()}-${streamData.gustSpeed?.roundToInt()}"
var dayColor = Color(ContextCompat.getColor(context, R.color.black))
var nightColor = Color(ContextCompat.getColor(context, R.color.white))
if (streamData.settings.windDirectionIndicatorSetting == WindDirectionIndicatorSetting.HEADWIND_DIRECTION) {
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
val windSpeedInKmh = if (streamData.isImperial == true){
headwindSpeed / 2.23694 * 3.6
} else {
headwindSpeed
}
dayColor = interpolateWindColor(windSpeedInKmh, false, context)
nightColor = interpolateWindColor(windSpeedInKmh, true, context)
}
val result = glance.compose(context, DpSize.Unspecified) {
HeadwindDirection(
baseBitmap,
windDirection.roundToInt(),
config.textSize,
mainText,
subtext,
dayColor,
nightColor
)
}
emitter.updateView(result.remoteViews)
}
}
emitter.setCancellable {
Log.d(KarooHeadwindExtension.TAG, "Stopping headwind view with $emitter")
configJob.cancel()
viewJob.cancel()
}
}
}

View File

@ -39,4 +39,6 @@
<string name="userwind_speed">Set wind speed</string> <string name="userwind_speed">Set wind speed</string>
<string name="graphical_forecast">Graphical Forecast</string> <string name="graphical_forecast">Graphical Forecast</string>
<string name="graphical_forecast_description">Current graphical weather forecast</string> <string name="graphical_forecast_description">Current graphical weather forecast</string>
<string name="tailwind">Tailwind</string>
<string name="tailwind_description">Current tailwind, wind speed and gust speed</string>
</resources> </resources>

View File

@ -4,12 +4,6 @@
icon="@drawable/wind" icon="@drawable/wind"
id="karoo-headwind" id="karoo-headwind"
scansDevices="false"> scansDevices="false">
<DataType
description="@string/headwind_description"
displayName="@string/headwind"
graphical="true"
icon="@drawable/wind"
typeId="headwind" />
<DataType <DataType
description="@string/tailwind_and_speed_description" description="@string/tailwind_and_speed_description"
@ -18,6 +12,20 @@
icon="@drawable/wind" icon="@drawable/wind"
typeId="tailwind-and-ride-speed" /> typeId="tailwind-and-ride-speed" />
<DataType
description="@string/tailwind_description"
displayName="@string/tailwind"
graphical="true"
icon="@drawable/wind"
typeId="tailwind" />
<DataType
description="@string/headwind_description"
displayName="@string/headwind"
graphical="true"
icon="@drawable/wind"
typeId="headwind" />
<DataType <DataType
description="@string/weather_description" description="@string/weather_description"
displayName="@string/weather" displayName="@string/weather"