diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt index fbd8cbb..7f8b202 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt @@ -163,7 +163,8 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") { add(gps) var currentPosition = positionOnRoute + calculatedDistanceToNextFullHour - var lastRequestedPosition = currentPosition + var lastRequestedPosition = positionOnRoute + while (currentPosition < upcomingRoute.routeLength && size < 10) { val point = TurfMeasurement.along( upcomingRoute.routePolyline, @@ -182,7 +183,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") { currentPosition += distancePerHour } - if (upcomingRoute.routeLength > lastRequestedPosition + 5_000) { + if (upcomingRoute.routeLength > lastRequestedPosition + 1_000) { val point = TurfMeasurement.along( upcomingRoute.routePolyline, upcomingRoute.routeLength, diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt index f85dbdc..ac7b5f8 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt @@ -5,6 +5,8 @@ import android.graphics.BitmapFactory import android.util.Log import androidx.compose.ui.unit.DpSize import androidx.glance.GlanceModifier +import androidx.glance.action.actionStartActivity +import androidx.glance.action.clickable import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi import androidx.glance.appwidget.GlanceRemoteViews import androidx.glance.layout.Alignment @@ -13,6 +15,7 @@ import androidx.glance.layout.fillMaxSize import de.timklge.karooheadwind.HeadingResponse import de.timklge.karooheadwind.HeadwindSettings import de.timklge.karooheadwind.KarooHeadwindExtension +import de.timklge.karooheadwind.MainActivity import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse import de.timklge.karooheadwind.OpenMeteoData import de.timklge.karooheadwind.TemperatureUnit @@ -43,7 +46,6 @@ import kotlinx.coroutines.launch import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle import kotlin.math.roundToInt @OptIn(ExperimentalGlanceRemoteViewsApi::class) @@ -124,11 +126,13 @@ class WeatherDataType( val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode) val formattedTime = timeFormatter.format(Instant.ofEpochSecond(data.current.time)) - val formattedDate = Instant.ofEpochSecond(data.current.time).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate( - FormatStyle.SHORT)) + val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(data.current.time)) val result = glance.compose(context, DpSize.Unspecified) { - Box(modifier = GlanceModifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + var modifier = GlanceModifier.fillMaxSize() + if (!config.preview) modifier = modifier.clickable(onClick = actionStartActivity()) + + Box(modifier = modifier, contentAlignment = Alignment.CenterEnd) { Weather( baseBitmap, current = interpretation, diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt index d31d1f8..918aee7 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt @@ -165,7 +165,7 @@ class WeatherForecastDataType( var previousDate: String? = let { val unixTime = allData.getOrNull(positionOffset)?.data?.forecastData?.time?.getOrNull(hourOffset) - val formattedDate = unixTime?.let { Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) } + val formattedDate = unixTime?.let { getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) } formattedDate } @@ -182,8 +182,6 @@ class WeatherForecastDataType( 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}" } - Log.d(KarooHeadwindExtension.TAG, "Distance along route ${positionIndex}: $position") - if (baseIndex > hourOffset) { Spacer( modifier = GlanceModifier.fillMaxHeight().background( @@ -192,6 +190,8 @@ class WeatherForecastDataType( ) } + Log.d(KarooHeadwindExtension.TAG, "Distance along route ${positionIndex}: $position") + val distanceFromCurrent = upcomingRoute?.distanceAlongRoute?.let { currentDistanceAlongRoute -> distanceAlongRoute?.minus(currentDistanceAlongRoute) } @@ -202,7 +202,7 @@ class WeatherForecastDataType( val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode) val unixTime = data.current.time val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime)) - val formattedDate = Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) + val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) val hasNewDate = formattedDate != previousDate || baseIndex == 0 Weather( @@ -225,7 +225,7 @@ class WeatherForecastDataType( 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 = Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) + val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) val hasNewDate = formattedDate != previousDate || baseIndex == 0 Weather( diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt index 95e3435..5e6226e 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt @@ -24,7 +24,6 @@ import androidx.glance.layout.padding import androidx.glance.layout.width import androidx.glance.layout.wrapContentWidth import androidx.glance.preview.ExperimentalGlancePreviewApi -import androidx.glance.preview.Preview import androidx.glance.text.FontFamily import androidx.glance.text.FontWeight import androidx.glance.text.Text @@ -33,9 +32,19 @@ import androidx.glance.text.TextStyle import de.timklge.karooheadwind.R import de.timklge.karooheadwind.TemperatureUnit import de.timklge.karooheadwind.WeatherInterpretation +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Locale import kotlin.math.absoluteValue import kotlin.math.ceil +fun getShortDateFormatter(): DateTimeFormatter = DateTimeFormatter.ofPattern( + when (Locale.getDefault().country) { + "US" -> "MM/dd" + else -> "dd.MM" + } +).withZone(ZoneId.systemDefault()) + fun getWeatherIcon(interpretation: WeatherInterpretation): Int { return when (interpretation){ WeatherInterpretation.CLEAR -> R.drawable.bx_clear @@ -49,7 +58,6 @@ fun getWeatherIcon(interpretation: WeatherInterpretation): Int { } @OptIn(ExperimentalGlancePreviewApi::class) -@Preview(widthDp = 200, heightDp = 150) @Composable fun Weather( baseBitmap: Bitmap, @@ -69,7 +77,7 @@ fun Weather( isImperial: Boolean? ) { - val fontSize = 14f + val fontSize = if (singleDisplay) 19f else 14f Column(modifier = if (singleDisplay) GlanceModifier.fillMaxSize().padding(1.dp) else GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) { Row(modifier = GlanceModifier.defaultWeight().wrapContentWidth(), horizontalAlignment = rowAlignment, verticalAlignment = Alignment.CenterVertically) { @@ -130,7 +138,7 @@ fun Weather( } Image( - modifier = GlanceModifier.height(16.dp).width(12.dp).padding(1.dp), + modifier = if (singleDisplay) GlanceModifier.height(20.dp).width(16.dp) else GlanceModifier.height(16.dp).width(12.dp).padding(1.dp), provider = ImageProvider(R.drawable.thermometer), contentDescription = "Temperature", contentScale = ContentScale.Fit, @@ -163,14 +171,13 @@ fun Weather( Spacer(modifier = GlanceModifier.width(5.dp)) Image( - modifier = GlanceModifier.height(16.dp).width(12.dp).padding(1.dp), + 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)), contentDescription = "Current wind direction", contentScale = ContentScale.Fit, colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White)) ) - Text( text = "$windSpeed,${windGusts}", style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(fontSize, TextUnitType.Sp)) diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt index 207fbfc..88ed319 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt @@ -26,12 +26,12 @@ 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.datatypes.getShortDateFormatter import de.timklge.karooheadwind.getGpsCoordinateFlow import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamStats @@ -41,10 +41,7 @@ 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 @@ -91,15 +88,10 @@ fun WeatherScreen(onFinish: () -> Unit) { 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)) - } + val formattedDate = currentWeatherData?.let { getShortDateFormatter().format(Instant.ofEpochSecond(currentWeatherData.current.time)) } if (karooConnected == true && currentWeatherData != null) { WeatherWidget( - dateLabel = formattedDate, - timeLabel = formattedTime, baseBitmap = baseBitmap, current = WeatherInterpretation.fromWeatherCode(currentWeatherData.current.weatherCode), windBearing = currentWeatherData.current.windDirection.roundToInt(), @@ -108,10 +100,11 @@ fun WeatherScreen(onFinish: () -> Unit) { 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, + timeLabel = formattedTime, + dateLabel = formattedDate, distance = requestedWeatherPosition?.let { l -> location?.distanceTo(l)?.times(1000) }, includeDistanceLabel = false, + isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL, ) } @@ -208,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 formattedForecastDate = Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) + val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) WeatherWidget( baseBitmap, @@ -217,15 +210,14 @@ fun WeatherScreen(onFinish: () -> Unit) { 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 + includeDistanceLabel = true, + precipitationProbability = data?.forecastData?.precipitationProbability?.get(index) ?: 0, + isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL ) } diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt index 47f9b7b..ba3f5b6 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt @@ -23,7 +23,6 @@ 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 @@ -47,122 +46,129 @@ fun WeatherWidget( 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 - ) - ) - } + val fontSize = 20.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 - ) + Row( + modifier = Modifier.fillMaxWidth().padding(5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + if (dateLabel != null) { + Text( + text = dateLabel, + style = TextStyle( + fontSize = fontSize ) - } - - 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 - ) - ) + 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 } - // 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 - ) + Text( + text = text, + style = TextStyle( + fontSize = fontSize ) - } + ) + } - // 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) + if (timeLabel != null) { + Text( + text = timeLabel, + style = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = fontSize ) - - Spacer(modifier = Modifier.width(4.dp)) - - Text( - text = "$windSpeed,$windGusts", - style = TextStyle( - fontSize = 14.sp - ) - ) - } + ) } } + + // Weather icon (larger) + Icon( + painter = painterResource(id = getWeatherIcon(current)), + contentDescription = "Current weather", + modifier = Modifier.size(72.dp) + ) + + Column(horizontalAlignment = Alignment.End) { + // 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 "" + + Icon( + painter = painterResource(id = R.drawable.droplet_regular), + contentDescription = "Precipitation", + modifier = Modifier.size(18.dp) + ) + + Text( + text = "${precipitationProbabilityLabel}${ceil(precipitation).toInt()}", + style = TextStyle( + fontSize = fontSize + ) + ) + } + + // 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(20.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = "$windSpeed,$windGusts", + style = TextStyle( + fontSize = fontSize + ) + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/droplet_regular.png b/app/src/main/res/drawable/droplet_regular.png new file mode 100644 index 0000000..1e579f7 Binary files /dev/null and b/app/src/main/res/drawable/droplet_regular.png differ