parent
4cd6d27aa2
commit
aaeb1204bf
@ -15,8 +15,8 @@ android {
|
||||
applicationId = "de.timklge.karooheadwind"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 14
|
||||
versionName = "1.3"
|
||||
versionCode = 15
|
||||
versionName = "1.3.1"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
"packageName": "de.timklge.karooheadwind",
|
||||
"iconUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/karoo-headwind.png",
|
||||
"latestApkUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/app-release.apk",
|
||||
"latestVersion": "1.3",
|
||||
"latestVersionCode": 14,
|
||||
"latestVersion": "1.3.1",
|
||||
"latestVersionCode": 15,
|
||||
"developer": "timklge",
|
||||
"description": "Provides headwind direction, wind speed and other weather data fields",
|
||||
"releaseNotes": "* Forecast weather along route in fixed intervals if route is loaded\n* Show current weather in app menu"
|
||||
"releaseNotes": "* Forecast weather along route in fixed intervals if route is loaded\n* Show current weather in app menu\n*Add individual forecast fields"
|
||||
}
|
||||
@ -69,6 +69,7 @@ data class HeadwindSettings(
|
||||
val forecastedKmPerHour: Int = 20,
|
||||
val forecastedMilesPerHour: Int = 12,
|
||||
val lastUpdateRequested: Long? = null,
|
||||
val showDistanceInForecast: Boolean = true,
|
||||
){
|
||||
companion object {
|
||||
val defaultSettings = Json.encodeToString(HeadwindSettings())
|
||||
|
||||
@ -7,18 +7,22 @@ import com.mapbox.turf.TurfConstants
|
||||
import com.mapbox.turf.TurfMeasurement
|
||||
import de.timklge.karooheadwind.datatypes.CloudCoverDataType
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import de.timklge.karooheadwind.datatypes.GraphicalForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
|
||||
import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType
|
||||
import de.timklge.karooheadwind.datatypes.PrecipitationDataType
|
||||
import de.timklge.karooheadwind.datatypes.PrecipitationForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.RelativeHumidityDataType
|
||||
import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
|
||||
import de.timklge.karooheadwind.datatypes.SurfacePressureDataType
|
||||
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
|
||||
import de.timklge.karooheadwind.datatypes.TemperatureDataType
|
||||
import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.UserWindSpeedDataType
|
||||
import de.timklge.karooheadwind.datatypes.WeatherDataType
|
||||
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
||||
import de.timklge.karooheadwind.datatypes.WindForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.WindGustsDataType
|
||||
import de.timklge.karooheadwind.datatypes.WindSpeedDataType
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
@ -50,7 +54,7 @@ import kotlin.math.roundToInt
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") {
|
||||
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3.1") {
|
||||
companion object {
|
||||
const val TAG = "karoo-headwind"
|
||||
}
|
||||
@ -66,7 +70,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") {
|
||||
TailwindAndRideSpeedDataType(karooSystem, applicationContext),
|
||||
HeadwindSpeedDataType(karooSystem, applicationContext),
|
||||
WeatherDataType(karooSystem, applicationContext),
|
||||
WeatherForecastDataType(karooSystem, applicationContext),
|
||||
WeatherForecastDataType(karooSystem),
|
||||
HeadwindSpeedDataType(karooSystem, applicationContext),
|
||||
RelativeHumidityDataType(applicationContext),
|
||||
CloudCoverDataType(applicationContext),
|
||||
@ -77,7 +81,11 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") {
|
||||
PrecipitationDataType(applicationContext),
|
||||
SurfacePressureDataType(applicationContext),
|
||||
SealevelPressureDataType(applicationContext),
|
||||
UserWindSpeedDataType(karooSystem, applicationContext)
|
||||
UserWindSpeedDataType(karooSystem, applicationContext),
|
||||
TemperatureForecastDataType(karooSystem),
|
||||
PrecipitationForecastDataType(karooSystem),
|
||||
WindForecastDataType(karooSystem),
|
||||
GraphicalForecastDataType(karooSystem)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,359 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||
import androidx.glance.appwidget.GlanceRemoteViews
|
||||
import androidx.glance.appwidget.action.actionRunCallback
|
||||
import androidx.glance.background
|
||||
import androidx.glance.color.ColorProvider
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.width
|
||||
import de.timklge.karooheadwind.HeadingResponse
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.HeadwindWidgetSettings
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.OpenMeteoData
|
||||
import de.timklge.karooheadwind.OpenMeteoForecastData
|
||||
import de.timklge.karooheadwind.R
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.UpcomingRoute
|
||||
import de.timklge.karooheadwind.WeatherDataResponse
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.getHeadingFlow
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import de.timklge.karooheadwind.streamSettings
|
||||
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||
import de.timklge.karooheadwind.streamUserProfile
|
||||
import de.timklge.karooheadwind.streamWidgetSettings
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||
import io.hammerhead.karooext.internal.ViewEmitter
|
||||
import io.hammerhead.karooext.models.ShowCustomStreamState
|
||||
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.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
abstract class ForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) {
|
||||
@Composable
|
||||
abstract fun RenderWidget(arrowBitmap: Bitmap,
|
||||
current: WeatherInterpretation,
|
||||
windBearing: Int,
|
||||
windSpeed: Int,
|
||||
windGusts: Int,
|
||||
precipitation: Double,
|
||||
precipitationProbability: Int?,
|
||||
temperature: Int,
|
||||
temperatureUnit: TemperatureUnit,
|
||||
timeLabel: String,
|
||||
dateLabel: String?,
|
||||
distance: Double?,
|
||||
isImperial: Boolean)
|
||||
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
private val glance = GlanceRemoteViews()
|
||||
|
||||
companion object {
|
||||
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault())
|
||||
}
|
||||
|
||||
data class StreamData(val data: List<WeatherDataResponse>?, val settings: SettingsAndProfile,
|
||||
val widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null,
|
||||
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null)
|
||||
|
||||
data class SettingsAndProfile(val settings: HeadwindSettings, val isImperial: Boolean)
|
||||
|
||||
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> =
|
||||
flow {
|
||||
val settingsAndProfile = settingsAndProfileStream.firstOrNull()
|
||||
|
||||
while (true) {
|
||||
val data = (0..<10).map { index ->
|
||||
val timeAtFullHour = Instant.now().truncatedTo(ChronoUnit.HOURS).epochSecond
|
||||
val forecastTimes = (0..<12).map { timeAtFullHour + it * 60 * 60 }
|
||||
val forecastTemperatures = (0..<12).map { 20.0 + (-20..20).random() }
|
||||
val forecastPrecipitationPropability = (0..<12).map { (0..100).random() }
|
||||
val forecastPrecipitation = (0..<12).map { 0.0 + (0..10).random() }
|
||||
val forecastWeatherCodes =
|
||||
(0..<12).map { WeatherInterpretation.getKnownWeatherCodes().random() }
|
||||
val forecastWindSpeed = (0..<12).map { 0.0 + (0..10).random() }
|
||||
val forecastWindDirection = (0..<12).map { 0.0 + (0..360).random() }
|
||||
val forecastWindGusts = (0..<12).map { 0.0 + (0..10).random() }
|
||||
val weatherData = OpenMeteoCurrentWeatherResponse(
|
||||
OpenMeteoData(
|
||||
Instant.now().epochSecond,
|
||||
0,
|
||||
20.0,
|
||||
50,
|
||||
3.0,
|
||||
0,
|
||||
1013.25,
|
||||
980.0,
|
||||
15.0,
|
||||
30.0,
|
||||
30.0,
|
||||
WeatherInterpretation.getKnownWeatherCodes().random()
|
||||
),
|
||||
0.0, 0.0, "Europe/Berlin", 30.0, 0,
|
||||
|
||||
OpenMeteoForecastData(
|
||||
forecastTimes,
|
||||
forecastTemperatures,
|
||||
forecastPrecipitationPropability,
|
||||
forecastPrecipitation,
|
||||
forecastWeatherCodes,
|
||||
forecastWindSpeed,
|
||||
forecastWindDirection,
|
||||
forecastWindGusts
|
||||
)
|
||||
)
|
||||
|
||||
val distancePerHour =
|
||||
settingsAndProfile?.settings?.getForecastMetersPerHour(settingsAndProfile.isImperial)
|
||||
?.toDouble() ?: 0.0
|
||||
val gpsCoords =
|
||||
GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour)
|
||||
|
||||
WeatherDataResponse(weatherData, gpsCoords)
|
||||
}
|
||||
|
||||
|
||||
emit(
|
||||
StreamData(
|
||||
data,
|
||||
SettingsAndProfile(
|
||||
HeadwindSettings(),
|
||||
settingsAndProfile?.isImperial == true
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
delay(5_000)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Starting weather forecast view with $emitter")
|
||||
val configJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
emitter.onNext(UpdateGraphicConfig(showHeader = false))
|
||||
awaitCancellation()
|
||||
}
|
||||
|
||||
val baseBitmap = BitmapFactory.decodeResource(
|
||||
context.resources,
|
||||
R.drawable.arrow_0
|
||||
)
|
||||
|
||||
val settingsAndProfileStream = context.streamSettings(karooSystem).combine(karooSystem.streamUserProfile()) { settings, userProfile ->
|
||||
SettingsAndProfile(settings = settings, isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)
|
||||
}
|
||||
|
||||
val dataFlow = if (config.preview){
|
||||
previewFlow(settingsAndProfileStream)
|
||||
} else {
|
||||
combine(
|
||||
context.streamCurrentWeatherData(),
|
||||
settingsAndProfileStream,
|
||||
context.streamWidgetSettings(),
|
||||
karooSystem.getHeadingFlow(context),
|
||||
karooSystem.streamUpcomingRoute()
|
||||
) { weatherData, settings, widgetSettings, heading, upcomingRoute ->
|
||||
StreamData(
|
||||
data = weatherData,
|
||||
settings = settings,
|
||||
widgetSettings = widgetSettings,
|
||||
headingResponse = heading,
|
||||
upcomingRoute = upcomingRoute
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
emitter.onNext(ShowCustomStreamState("", null))
|
||||
|
||||
dataFlow.collect { (allData, settingsAndProfile, widgetSettings, userProfile, headingResponse, upcomingRoute) ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")
|
||||
|
||||
if (allData == null){
|
||||
emitter.updateView(
|
||||
getErrorWidget(
|
||||
glance,
|
||||
context,
|
||||
settingsAndProfile.settings,
|
||||
headingResponse
|
||||
).remoteViews)
|
||||
|
||||
return@collect
|
||||
}
|
||||
|
||||
val result = glance.compose(context, DpSize.Unspecified) {
|
||||
var modifier = GlanceModifier.fillMaxSize()
|
||||
|
||||
if (!config.preview) modifier = modifier.clickable(onClick = actionRunCallback<CycleHoursAction>())
|
||||
|
||||
Row(modifier = modifier, horizontalAlignment = Alignment.Horizontal.Start) {
|
||||
val hourOffset = widgetSettings?.currentForecastHourOffset ?: 0
|
||||
val positionOffset = if (allData.size == 1) 0 else hourOffset
|
||||
|
||||
var previousDate: String? = let {
|
||||
val unixTime =
|
||||
allData.getOrNull(positionOffset)?.data?.forecastData?.time?.getOrNull(
|
||||
hourOffset
|
||||
)
|
||||
val formattedDate = unixTime?.let {
|
||||
getShortDateFormatter().format(
|
||||
Instant.ofEpochSecond(unixTime)
|
||||
)
|
||||
}
|
||||
|
||||
formattedDate
|
||||
}
|
||||
|
||||
for (baseIndex in hourOffset..hourOffset + 2) {
|
||||
val positionIndex = if (allData.size == 1) 0 else baseIndex
|
||||
|
||||
if (allData.getOrNull(positionIndex) == null) break
|
||||
if (baseIndex >= (allData.getOrNull(positionOffset)?.data?.forecastData?.weatherCode?.size
|
||||
?: 0)
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
val data = allData.getOrNull(positionIndex)?.data
|
||||
val distanceAlongRoute =
|
||||
allData.getOrNull(positionIndex)?.requestedPosition?.distanceAlongRoute
|
||||
val position =
|
||||
allData.getOrNull(positionIndex)?.requestedPosition?.let {
|
||||
"${
|
||||
(it.distanceAlongRoute?.div(1000.0))?.toInt()
|
||||
} at ${it.lat}, ${it.lon}"
|
||||
}
|
||||
|
||||
if (baseIndex > hourOffset) {
|
||||
Spacer(
|
||||
modifier = GlanceModifier.fillMaxHeight().background(
|
||||
ColorProvider(Color.Black, Color.White)
|
||||
).width(1.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Log.d(
|
||||
KarooHeadwindExtension.TAG,
|
||||
"Distance along route ${positionIndex}: $position"
|
||||
)
|
||||
|
||||
val distanceFromCurrent =
|
||||
upcomingRoute?.distanceAlongRoute?.let { currentDistanceAlongRoute ->
|
||||
distanceAlongRoute?.minus(currentDistanceAlongRoute)
|
||||
}
|
||||
|
||||
val isCurrent = baseIndex == 0 && positionIndex == 0
|
||||
|
||||
if (isCurrent && data?.current != null) {
|
||||
val interpretation =
|
||||
WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
||||
val unixTime = data.current.time
|
||||
val formattedTime =
|
||||
timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedDate =
|
||||
getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
||||
|
||||
RenderWidget(
|
||||
arrowBitmap = baseBitmap,
|
||||
current = interpretation,
|
||||
windBearing = data.current.windDirection.roundToInt(),
|
||||
windSpeed = data.current.windSpeed.roundToInt(),
|
||||
windGusts = data.current.windGusts.roundToInt(),
|
||||
precipitation = data.current.precipitation,
|
||||
precipitationProbability = null,
|
||||
temperature = data.current.temperature.roundToInt(),
|
||||
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||
timeLabel = formattedTime,
|
||||
dateLabel = if (hasNewDate) formattedDate else null,
|
||||
distance = null,
|
||||
isImperial = userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||
)
|
||||
|
||||
previousDate = formattedDate
|
||||
} else {
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(
|
||||
data?.forecastData?.weatherCode?.get(baseIndex) ?: 0
|
||||
)
|
||||
val unixTime = data?.forecastData?.time?.get(baseIndex) ?: 0
|
||||
val formattedTime =
|
||||
timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedDate =
|
||||
getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
||||
|
||||
RenderWidget(
|
||||
arrowBitmap = baseBitmap,
|
||||
current = interpretation,
|
||||
windBearing = data?.forecastData?.windDirection?.get(baseIndex)
|
||||
?.roundToInt() ?: 0,
|
||||
windSpeed = data?.forecastData?.windSpeed?.get(baseIndex)
|
||||
?.roundToInt() ?: 0,
|
||||
windGusts = data?.forecastData?.windGusts?.get(baseIndex)
|
||||
?.roundToInt() ?: 0,
|
||||
precipitation = data?.forecastData?.precipitation?.get(baseIndex)
|
||||
?: 0.0,
|
||||
precipitationProbability = data?.forecastData?.precipitationProbability?.get(
|
||||
baseIndex
|
||||
) ?: 0,
|
||||
temperature = data?.forecastData?.temperature?.get(baseIndex)
|
||||
?.roundToInt() ?: 0,
|
||||
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||
timeLabel = formattedTime,
|
||||
dateLabel = if (hasNewDate) formattedDate else null,
|
||||
distance = if (settingsAndProfile.settings.showDistanceInForecast) distanceFromCurrent else null,
|
||||
isImperial = settingsAndProfile.isImperial
|
||||
)
|
||||
|
||||
previousDate = formattedDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emitter.updateView(result.remoteViews)
|
||||
}
|
||||
}
|
||||
emitter.setCancellable {
|
||||
Log.d(
|
||||
KarooHeadwindExtension.TAG,
|
||||
"Stopping headwind weather forecast view with $emitter"
|
||||
)
|
||||
configJob.cancel()
|
||||
viewJob.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.ColorFilter
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.color.ColorProvider
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.ContentScale
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.layout.wrapContentWidth
|
||||
import androidx.glance.text.FontFamily
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun GraphicalForecast(
|
||||
current: WeatherInterpretation,
|
||||
distance: Double? = null,
|
||||
timeLabel: String? = null,
|
||||
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
||||
isImperial: Boolean?
|
||||
) {
|
||||
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
||||
Row(modifier = GlanceModifier.defaultWeight().wrapContentWidth(), horizontalAlignment = rowAlignment, verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
modifier = GlanceModifier.defaultWeight().wrapContentWidth().padding(1.dp),
|
||||
provider = ImageProvider(getWeatherIcon(current)),
|
||||
contentDescription = "Current weather information",
|
||||
contentScale = ContentScale.Fit,
|
||||
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
||||
)
|
||||
}
|
||||
|
||||
if (distance != null && isImperial != null){
|
||||
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||
val text = if(distanceInUserUnit > 0){
|
||||
"In $label"
|
||||
} else {
|
||||
"$label ago"
|
||||
}
|
||||
|
||||
if (distanceInUserUnit != 0){
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = text,
|
||||
style = TextStyle(
|
||||
color = ColorProvider(Color.Black, Color.White),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (timeLabel != null){
|
||||
Text(
|
||||
text = timeLabel,
|
||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GraphicalForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "graphicalForecast") {
|
||||
@Composable
|
||||
override fun RenderWidget(
|
||||
arrowBitmap: Bitmap,
|
||||
current: WeatherInterpretation,
|
||||
windBearing: Int,
|
||||
windSpeed: Int,
|
||||
windGusts: Int,
|
||||
precipitation: Double,
|
||||
precipitationProbability: Int?,
|
||||
temperature: Int,
|
||||
temperatureUnit: TemperatureUnit,
|
||||
timeLabel: String,
|
||||
dateLabel: String?,
|
||||
distance: Double?,
|
||||
isImperial: Boolean
|
||||
) {
|
||||
GraphicalForecast(
|
||||
current = current,
|
||||
distance = distance,
|
||||
timeLabel = timeLabel,
|
||||
isImperial = isImperial
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.color.ColorProvider
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.text.FontFamily
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun PrecipitationForecast(
|
||||
precipitation: Int,
|
||||
precipitationProbability: Int?,
|
||||
distance: Double? = null,
|
||||
timeLabel: String? = null,
|
||||
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
||||
isImperial: Boolean?
|
||||
) {
|
||||
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
||||
Row(modifier = GlanceModifier.defaultWeight().fillMaxWidth(), horizontalAlignment = rowAlignment, verticalAlignment = Alignment.CenterVertically) {
|
||||
val precipitationProbabilityText = if (precipitationProbability != null) "${precipitationProbability}% " else ""
|
||||
val precipitationText = if (precipitation > 0) precipitation.toString() else ""
|
||||
Text(
|
||||
text = "${precipitationProbabilityText}${precipitationText}",
|
||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(24f, TextUnitType.Sp), textAlign = TextAlign.Center)
|
||||
)
|
||||
}
|
||||
|
||||
if (distance != null && isImperial != null){
|
||||
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||
val text = if(distanceInUserUnit > 0){
|
||||
"In $label"
|
||||
} else {
|
||||
"$label ago"
|
||||
}
|
||||
|
||||
if (distanceInUserUnit != 0){
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = text,
|
||||
style = TextStyle(
|
||||
color = ColorProvider(Color.Black, Color.White),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (timeLabel != null){
|
||||
Text(
|
||||
text = timeLabel,
|
||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PrecipitationForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "precipitationForecast") {
|
||||
@Composable
|
||||
override fun RenderWidget(
|
||||
arrowBitmap: Bitmap,
|
||||
current: WeatherInterpretation,
|
||||
windBearing: Int,
|
||||
windSpeed: Int,
|
||||
windGusts: Int,
|
||||
precipitation: Double,
|
||||
precipitationProbability: Int?,
|
||||
temperature: Int,
|
||||
temperatureUnit: TemperatureUnit,
|
||||
timeLabel: String,
|
||||
dateLabel: String?,
|
||||
distance: Double?,
|
||||
isImperial: Boolean
|
||||
) {
|
||||
PrecipitationForecast(
|
||||
precipitation = precipitation.roundToInt().coerceAtLeast(0),
|
||||
precipitationProbability = precipitationProbability,
|
||||
distance = distance,
|
||||
timeLabel = timeLabel,
|
||||
isImperial = isImperial
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.color.ColorProvider
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.text.FontFamily
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun TemperatureForecast(
|
||||
temperature: Int,
|
||||
temperatureUnit: TemperatureUnit,
|
||||
distance: Double? = null,
|
||||
timeLabel: String? = null,
|
||||
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
||||
isImperial: Boolean?
|
||||
) {
|
||||
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
||||
Row(modifier = GlanceModifier.defaultWeight().fillMaxWidth(), horizontalAlignment = rowAlignment, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "${temperature}${temperatureUnit.unitDisplay}",
|
||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(28f, TextUnitType.Sp), textAlign = TextAlign.Center)
|
||||
)
|
||||
}
|
||||
|
||||
if (distance != null && isImperial != null){
|
||||
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||
val text = if(distanceInUserUnit > 0){
|
||||
"In $label"
|
||||
} else {
|
||||
"$label ago"
|
||||
}
|
||||
|
||||
if (distanceInUserUnit != 0){
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = text,
|
||||
style = TextStyle(
|
||||
color = ColorProvider(Color.Black, Color.White),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (timeLabel != null){
|
||||
Text(
|
||||
text = timeLabel,
|
||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TemperatureForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "temperatureForecast") {
|
||||
@Composable
|
||||
override fun RenderWidget(
|
||||
arrowBitmap: Bitmap,
|
||||
current: WeatherInterpretation,
|
||||
windBearing: Int,
|
||||
windSpeed: Int,
|
||||
windGusts: Int,
|
||||
precipitation: Double,
|
||||
precipitationProbability: Int?,
|
||||
temperature: Int,
|
||||
temperatureUnit: TemperatureUnit,
|
||||
timeLabel: String,
|
||||
dateLabel: String?,
|
||||
distance: Double?,
|
||||
isImperial: Boolean
|
||||
) {
|
||||
TemperatureForecast(
|
||||
temperature = temperature,
|
||||
temperatureUnit = temperatureUnit,
|
||||
distance = distance,
|
||||
timeLabel = timeLabel,
|
||||
isImperial = isImperial
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,262 +1,43 @@
|
||||
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.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||
import androidx.glance.appwidget.GlanceRemoteViews
|
||||
import androidx.glance.appwidget.action.actionRunCallback
|
||||
import androidx.glance.background
|
||||
import androidx.glance.color.ColorProvider
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.width
|
||||
import de.timklge.karooheadwind.HeadingResponse
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.HeadwindWidgetSettings
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||
import de.timklge.karooheadwind.OpenMeteoData
|
||||
import de.timklge.karooheadwind.OpenMeteoForecastData
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.runtime.Composable
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.UpcomingRoute
|
||||
import de.timklge.karooheadwind.WeatherDataResponse
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.getHeadingFlow
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import de.timklge.karooheadwind.streamSettings
|
||||
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||
import de.timklge.karooheadwind.streamUserProfile
|
||||
import de.timklge.karooheadwind.streamWidgetSettings
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||
import io.hammerhead.karooext.internal.ViewEmitter
|
||||
import io.hammerhead.karooext.models.ShowCustomStreamState
|
||||
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.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
class WeatherForecastDataType(
|
||||
private val karooSystem: KarooSystemService,
|
||||
private val applicationContext: Context
|
||||
) : DataTypeImpl("karoo-headwind", "weatherForecast") {
|
||||
private val glance = GlanceRemoteViews()
|
||||
|
||||
companion object {
|
||||
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault())
|
||||
}
|
||||
|
||||
data class StreamData(val data: List<WeatherDataResponse>?, val settings: SettingsAndProfile,
|
||||
val widgetSettings: HeadwindWidgetSettings? = null, val profile: UserProfile? = null,
|
||||
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null)
|
||||
|
||||
data class SettingsAndProfile(val settings: HeadwindSettings, val isImperial: Boolean)
|
||||
|
||||
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> = flow {
|
||||
val settingsAndProfile = settingsAndProfileStream.firstOrNull()
|
||||
|
||||
while (true){
|
||||
val data = (0..<10).map { index ->
|
||||
val timeAtFullHour = Instant.now().truncatedTo(ChronoUnit.HOURS).epochSecond
|
||||
val forecastTimes = (0..<12).map { timeAtFullHour + it * 60 * 60 }
|
||||
val forecastTemperatures = (0..<12).map { 20.0 + (-20..20).random() }
|
||||
val forecastPrecipitationPropability = (0..<12).map { (0..100).random() }
|
||||
val forecastPrecipitation = (0..<12).map { 0.0 + (0..10).random() }
|
||||
val forecastWeatherCodes = (0..<12).map { WeatherInterpretation.getKnownWeatherCodes().random() }
|
||||
val forecastWindSpeed = (0..<12).map { 0.0 + (0..10).random() }
|
||||
val forecastWindDirection = (0..<12).map { 0.0 + (0..360).random() }
|
||||
val forecastWindGusts = (0..<12).map { 0.0 + (0..10).random() }
|
||||
val weatherData = OpenMeteoCurrentWeatherResponse(
|
||||
OpenMeteoData(Instant.now().epochSecond, 0, 20.0, 50, 3.0, 0, 1013.25, 980.0, 15.0, 30.0, 30.0, WeatherInterpretation.getKnownWeatherCodes().random()),
|
||||
0.0, 0.0, "Europe/Berlin", 30.0, 0,
|
||||
|
||||
OpenMeteoForecastData(forecastTimes, forecastTemperatures, forecastPrecipitationPropability,
|
||||
forecastPrecipitation, forecastWeatherCodes, forecastWindSpeed, forecastWindDirection,
|
||||
forecastWindGusts)
|
||||
)
|
||||
|
||||
val distancePerHour = settingsAndProfile?.settings?.getForecastMetersPerHour(settingsAndProfile.isImperial)?.toDouble() ?: 0.0
|
||||
val gpsCoords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour)
|
||||
|
||||
WeatherDataResponse(weatherData, gpsCoords)
|
||||
}
|
||||
|
||||
|
||||
emit(
|
||||
StreamData(data, SettingsAndProfile(HeadwindSettings(), settingsAndProfile?.isImperial == true))
|
||||
)
|
||||
|
||||
delay(5_000)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Starting weather forecast view with $emitter")
|
||||
val configJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
emitter.onNext(UpdateGraphicConfig(showHeader = false))
|
||||
awaitCancellation()
|
||||
}
|
||||
|
||||
val baseBitmap = BitmapFactory.decodeResource(
|
||||
context.resources,
|
||||
de.timklge.karooheadwind.R.drawable.arrow_0
|
||||
class WeatherForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "weatherForecast") {
|
||||
@Composable
|
||||
override fun RenderWidget(
|
||||
arrowBitmap: Bitmap,
|
||||
current: WeatherInterpretation,
|
||||
windBearing: Int,
|
||||
windSpeed: Int,
|
||||
windGusts: Int,
|
||||
precipitation: Double,
|
||||
precipitationProbability: Int?,
|
||||
temperature: Int,
|
||||
temperatureUnit: TemperatureUnit,
|
||||
timeLabel: String,
|
||||
dateLabel: String?,
|
||||
distance: Double?,
|
||||
isImperial: Boolean
|
||||
) {
|
||||
Weather(
|
||||
arrowBitmap = arrowBitmap,
|
||||
current = current,
|
||||
windBearing = windBearing,
|
||||
windSpeed = windSpeed,
|
||||
windGusts = windGusts,
|
||||
precipitation = precipitation,
|
||||
precipitationProbability = precipitationProbability,
|
||||
temperature = temperature,
|
||||
temperatureUnit = temperatureUnit,
|
||||
timeLabel = timeLabel,
|
||||
dateLabel = dateLabel,
|
||||
distance = distance,
|
||||
isImperial = isImperial
|
||||
)
|
||||
|
||||
val settingsAndProfileStream = context.streamSettings(karooSystem).combine(karooSystem.streamUserProfile()) { settings, userProfile ->
|
||||
SettingsAndProfile(settings = settings, isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)
|
||||
}
|
||||
|
||||
val dataFlow = if (config.preview){
|
||||
previewFlow(settingsAndProfileStream)
|
||||
} else {
|
||||
combine(context.streamCurrentWeatherData(),
|
||||
settingsAndProfileStream,
|
||||
context.streamWidgetSettings(),
|
||||
karooSystem.getHeadingFlow(context),
|
||||
karooSystem.streamUpcomingRoute()) { weatherData, settings, widgetSettings, heading, upcomingRoute ->
|
||||
StreamData(data = weatherData, settings = settings, widgetSettings = widgetSettings, headingResponse = heading, upcomingRoute = upcomingRoute)
|
||||
}
|
||||
}
|
||||
|
||||
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
emitter.onNext(ShowCustomStreamState("", null))
|
||||
|
||||
dataFlow.collect { (allData, settingsAndProfile, widgetSettings, userProfile, headingResponse, upcomingRoute) ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")
|
||||
|
||||
if (allData == null){
|
||||
emitter.updateView(getErrorWidget(glance, context, settingsAndProfile.settings, headingResponse).remoteViews)
|
||||
|
||||
return@collect
|
||||
}
|
||||
|
||||
val result = glance.compose(context, DpSize.Unspecified) {
|
||||
var modifier = GlanceModifier.fillMaxSize()
|
||||
|
||||
if (!config.preview) modifier = modifier.clickable(onClick = actionRunCallback<CycleHoursAction>())
|
||||
|
||||
Row(modifier = modifier, horizontalAlignment = Alignment.Horizontal.Start) {
|
||||
val hourOffset = widgetSettings?.currentForecastHourOffset ?: 0
|
||||
val positionOffset = if (allData.size == 1) 0 else hourOffset
|
||||
|
||||
var previousDate: String? = let {
|
||||
val unixTime = allData.getOrNull(positionOffset)?.data?.forecastData?.time?.getOrNull(hourOffset)
|
||||
val formattedDate = unixTime?.let { getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) }
|
||||
|
||||
formattedDate
|
||||
}
|
||||
|
||||
for (baseIndex in hourOffset..hourOffset + 2){
|
||||
val positionIndex = if (allData.size == 1) 0 else baseIndex
|
||||
|
||||
if (allData.getOrNull(positionIndex) == null) break
|
||||
if (baseIndex >= (allData.getOrNull(positionOffset)?.data?.forecastData?.weatherCode?.size ?: 0)) {
|
||||
break
|
||||
}
|
||||
|
||||
val data = allData.getOrNull(positionIndex)?.data
|
||||
val distanceAlongRoute = allData.getOrNull(positionIndex)?.requestedPosition?.distanceAlongRoute
|
||||
val position = allData.getOrNull(positionIndex)?.requestedPosition?.let { "${(it.distanceAlongRoute?.div(1000.0))?.toInt()} at ${it.lat}, ${it.lon}" }
|
||||
|
||||
if (baseIndex > hourOffset) {
|
||||
Spacer(
|
||||
modifier = GlanceModifier.fillMaxHeight().background(
|
||||
ColorProvider(Color.Black, Color.White)
|
||||
).width(1.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Log.d(KarooHeadwindExtension.TAG, "Distance along route ${positionIndex}: $position")
|
||||
|
||||
val distanceFromCurrent = upcomingRoute?.distanceAlongRoute?.let { currentDistanceAlongRoute ->
|
||||
distanceAlongRoute?.minus(currentDistanceAlongRoute)
|
||||
}
|
||||
|
||||
val isCurrent = baseIndex == 0 && positionIndex == 0
|
||||
|
||||
if (isCurrent && data?.current != null){
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
||||
val unixTime = data.current.time
|
||||
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
||||
|
||||
Weather(
|
||||
baseBitmap,
|
||||
current = interpretation,
|
||||
windBearing = data.current.windDirection.roundToInt(),
|
||||
windSpeed = data.current.windSpeed.roundToInt(),
|
||||
windGusts = data.current.windGusts.roundToInt(),
|
||||
precipitation = data.current.precipitation,
|
||||
precipitationProbability = null,
|
||||
temperature = data.current.temperature.roundToInt(),
|
||||
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||
timeLabel = formattedTime,
|
||||
dateLabel = if (hasNewDate) formattedDate else null,
|
||||
isImperial = userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||
)
|
||||
|
||||
previousDate = formattedDate
|
||||
} else {
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(data?.forecastData?.weatherCode?.get(baseIndex) ?: 0)
|
||||
val unixTime = data?.forecastData?.time?.get(baseIndex) ?: 0
|
||||
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
||||
|
||||
Weather(
|
||||
baseBitmap,
|
||||
current = interpretation,
|
||||
windBearing = data?.forecastData?.windDirection?.get(baseIndex)?.roundToInt() ?: 0,
|
||||
windSpeed = data?.forecastData?.windSpeed?.get(baseIndex)?.roundToInt() ?: 0,
|
||||
windGusts = data?.forecastData?.windGusts?.get(baseIndex)?.roundToInt() ?: 0,
|
||||
precipitation = data?.forecastData?.precipitation?.get(baseIndex) ?: 0.0,
|
||||
precipitationProbability = data?.forecastData?.precipitationProbability?.get(baseIndex) ?: 0,
|
||||
temperature = data?.forecastData?.temperature?.get(baseIndex)?.roundToInt() ?: 0,
|
||||
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||
timeLabel = formattedTime,
|
||||
dateLabel = if (hasNewDate) formattedDate else null,
|
||||
distance = distanceFromCurrent,
|
||||
isImperial = settingsAndProfile.isImperial
|
||||
)
|
||||
|
||||
previousDate = formattedDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emitter.updateView(result.remoteViews)
|
||||
}
|
||||
}
|
||||
emitter.setCancellable {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Stopping headwind weather forecast view with $emitter")
|
||||
configJob.cancel()
|
||||
viewJob.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -60,7 +60,7 @@ fun getWeatherIcon(interpretation: WeatherInterpretation): Int {
|
||||
@OptIn(ExperimentalGlancePreviewApi::class)
|
||||
@Composable
|
||||
fun Weather(
|
||||
baseBitmap: Bitmap,
|
||||
arrowBitmap: Bitmap,
|
||||
current: WeatherInterpretation,
|
||||
windBearing: Int,
|
||||
windSpeed: Int,
|
||||
@ -172,7 +172,7 @@ fun Weather(
|
||||
|
||||
Image(
|
||||
modifier = if (singleDisplay) GlanceModifier.height(20.dp).width(16.dp) else GlanceModifier.height(16.dp).width(12.dp).padding(1.dp),
|
||||
provider = ImageProvider(getArrowBitmapByBearing(baseBitmap, windBearing + 180)),
|
||||
provider = ImageProvider(getArrowBitmapByBearing(arrowBitmap, windBearing + 180)),
|
||||
contentDescription = "Current wind direction",
|
||||
contentScale = ContentScale.Fit,
|
||||
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
||||
|
||||
@ -0,0 +1,120 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.glance.ColorFilter
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.Image
|
||||
import androidx.glance.ImageProvider
|
||||
import androidx.glance.color.ColorProvider
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.ContentScale
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.text.FontFamily
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun WindForecast(
|
||||
arrowBitmap: Bitmap,
|
||||
windBearing: Int,
|
||||
windSpeed: Int,
|
||||
gustSpeed: Int,
|
||||
distance: Double? = null,
|
||||
timeLabel: String? = null,
|
||||
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
||||
isImperial: Boolean?
|
||||
) {
|
||||
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
||||
Image(
|
||||
modifier = GlanceModifier.defaultWeight().fillMaxWidth(),
|
||||
provider = ImageProvider(getArrowBitmapByBearing(arrowBitmap, windBearing + 180)),
|
||||
contentDescription = "Current wind direction",
|
||||
contentScale = ContentScale.Fit,
|
||||
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${windSpeed}-${gustSpeed}",
|
||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp), textAlign = TextAlign.Center)
|
||||
)
|
||||
|
||||
if (distance != null && isImperial != null){
|
||||
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
||||
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
||||
val text = if(distanceInUserUnit > 0){
|
||||
"In $label"
|
||||
} else {
|
||||
"$label ago"
|
||||
}
|
||||
|
||||
if (distanceInUserUnit != 0){
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = text,
|
||||
style = TextStyle(
|
||||
color = ColorProvider(Color.Black, Color.White),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (timeLabel != null){
|
||||
Text(
|
||||
text = timeLabel,
|
||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WindForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "windForecast") {
|
||||
@Composable
|
||||
override fun RenderWidget(
|
||||
arrowBitmap: Bitmap,
|
||||
current: WeatherInterpretation,
|
||||
windBearing: Int,
|
||||
windSpeed: Int,
|
||||
windGusts: Int,
|
||||
precipitation: Double,
|
||||
precipitationProbability: Int?,
|
||||
temperature: Int,
|
||||
temperatureUnit: TemperatureUnit,
|
||||
timeLabel: String,
|
||||
dateLabel: String?,
|
||||
distance: Double?,
|
||||
isImperial: Boolean
|
||||
) {
|
||||
WindForecast(
|
||||
arrowBitmap = arrowBitmap,
|
||||
windBearing = windBearing,
|
||||
windSpeed = windSpeed,
|
||||
gustSpeed = windGusts,
|
||||
distance = distance,
|
||||
timeLabel = timeLabel,
|
||||
isImperial = isImperial
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -4,13 +4,16 @@ 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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@ -21,6 +24,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
@ -62,6 +66,7 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
||||
var selectedRoundLocationSetting by remember { mutableStateOf(RoundLocationSetting.KM_3) }
|
||||
var forecastKmPerHour by remember { mutableStateOf("20") }
|
||||
var forecastMilesPerHour by remember { mutableStateOf("12") }
|
||||
var showDistanceInForecast by remember { mutableStateOf(true) }
|
||||
|
||||
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
|
||||
|
||||
@ -73,6 +78,7 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
||||
selectedRoundLocationSetting = settings.roundLocationTo
|
||||
forecastKmPerHour = settings.forecastedKmPerHour.toString()
|
||||
forecastMilesPerHour = settings.forecastedMilesPerHour.toString()
|
||||
showDistanceInForecast = settings.showDistanceInForecast
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,7 +104,8 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
||||
windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting,
|
||||
roundLocationTo = selectedRoundLocationSetting,
|
||||
forecastedMilesPerHour = forecastMilesPerHour.toIntOrNull()?.coerceIn(3, 30) ?: 12,
|
||||
forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20
|
||||
forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20,
|
||||
showDistanceInForecast = showDistanceInForecast
|
||||
)
|
||||
|
||||
saveSettings(ctx, newSettings)
|
||||
@ -205,6 +212,12 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Switch(checked = showDistanceInForecast, onCheckedChange = { showDistanceInForecast = it})
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text("Show Distance in Forecast")
|
||||
}
|
||||
|
||||
if (!karooConnected) {
|
||||
Text(
|
||||
modifier = Modifier.padding(5.dp),
|
||||
|
||||
@ -29,6 +29,7 @@ import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.R
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.datatypes.ForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.WeatherDataType.Companion.timeFormatter
|
||||
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.getShortDateFormatter
|
||||
@ -200,7 +201,7 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
||||
|
||||
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 formattedForecastTime = ForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
|
||||
WeatherWidget(
|
||||
|
||||
@ -25,10 +25,18 @@
|
||||
<string name="weather_description">Current weather conditions</string>
|
||||
<string name="weather_forecast">Weather Forecast</string>
|
||||
<string name="weather_forecast_description">Current hourly weather forecast</string>
|
||||
<string name="temperature_forecast">Temperature Forecast</string>
|
||||
<string name="temperature_forecast_description">Current hourly temperature forecast</string>
|
||||
<string name="wind_forecast">Wind Forecast</string>
|
||||
<string name="wind_forecast_description">Current hourly wind forecast</string>
|
||||
<string name="precipitation_forecast">Precipitation Forecast</string>
|
||||
<string name="precipitation_forecast_description">Current hourly precipitation forecast</string>
|
||||
<string name="headwind_speed">Headwind speed</string>
|
||||
<string name="headwind_speed_description">Current headwind speed</string>
|
||||
<string name="temperature">Temperature</string>
|
||||
<string name="temperature_description">Current temperature in configured unit</string>
|
||||
<string name="userwind_speed_description">Current headwind or wind speed based on user setting</string>
|
||||
<string name="userwind_speed">Set wind speed</string>
|
||||
<string name="graphical_forecast">Graphical Forecast</string>
|
||||
<string name="graphical_forecast_description">Current graphical weather forecast</string>
|
||||
</resources>
|
||||
@ -32,6 +32,34 @@
|
||||
icon="@drawable/wind"
|
||||
typeId="weatherForecast" />
|
||||
|
||||
<DataType
|
||||
description="@string/temperature_forecast_description"
|
||||
displayName="@string/temperature_forecast"
|
||||
graphical="true"
|
||||
icon="@drawable/wind"
|
||||
typeId="temperatureForecast" />
|
||||
|
||||
<DataType
|
||||
description="@string/precipitation_forecast_description"
|
||||
displayName="@string/precipitation_forecast"
|
||||
graphical="true"
|
||||
icon="@drawable/wind"
|
||||
typeId="precipitationForecast" />
|
||||
|
||||
<DataType
|
||||
description="@string/wind_forecast_description"
|
||||
displayName="@string/wind_forecast"
|
||||
graphical="true"
|
||||
icon="@drawable/wind"
|
||||
typeId="windForecast" />
|
||||
|
||||
<DataType
|
||||
description="@string/graphical_forecast_description"
|
||||
displayName="@string/graphical_forecast"
|
||||
graphical="true"
|
||||
icon="@drawable/wind"
|
||||
typeId="graphicalForecast" />
|
||||
|
||||
<DataType
|
||||
description="@string/headwind_speed_description"
|
||||
displayName="@string/headwind_speed"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user