Crop timespan to 6 hours in forecast views, use am / pm time format if appropriate (#144)
Some checks failed
Build / build (push) Has been cancelled

* Crop timespan to 6 hours in forecast views, use am / pm time format if appropriate

* Also crop timeframe in hourly forecast widget
This commit is contained in:
timklge 2025-06-09 14:14:40 +02:00 committed by GitHub
parent 4952d8fbf4
commit ef5e980de3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 69 additions and 58 deletions

View File

@ -5,6 +5,7 @@ import android.util.Log
import de.timklge.karooheadwind.datatypes.GpsCoordinates import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.util.signedAngleDifference import de.timklge.karooheadwind.util.signedAngleDifference
import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.KarooSystemService
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -90,7 +91,8 @@ suspend fun KarooSystemService.updateLastKnownGps(context: Context) {
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> { fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> {
/* return flow { /* return flow {
emit(GpsCoordinates(52.5164069,13.3784)) // emit(GpsCoordinates(52.5164069,13.3784))
emit(GpsCoordinates(32.46,-111.524))
awaitCancellation() awaitCancellation()
} */ } */

View File

@ -25,9 +25,9 @@ class CycleHoursAction : ActionCallback {
var hourOffset = currentSettings.currentForecastHourOffset + 3 var hourOffset = currentSettings.currentForecastHourOffset + 3
val requestedPositions = forecastData?.data?.size val requestedPositions = forecastData?.data?.size
val requestedHours = forecastData?.data?.firstOrNull()?.forecasts?.size val requestedHours = forecastData?.data?.firstOrNull()?.forecasts?.size?.coerceAtMost(6)
if (forecastData == null || requestedHours == null || requestedPositions == null || hourOffset >= requestedHours || (requestedPositions in 2..hourOffset)) { if (forecastData == null || requestedHours == null || requestedPositions == null || (requestedPositions == 1 && hourOffset >= requestedHours) || (requestedPositions in 2..hourOffset)) {
hourOffset = 0 hourOffset = 0
} }

View File

@ -38,6 +38,7 @@ import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.streamWidgetSettings import de.timklge.karooheadwind.streamWidgetSettings
import de.timklge.karooheadwind.throttle import de.timklge.karooheadwind.throttle
import de.timklge.karooheadwind.util.celciusInUserUnit import de.timklge.karooheadwind.util.celciusInUserUnit
import de.timklge.karooheadwind.util.getTimeFormatter
import de.timklge.karooheadwind.util.millimetersInUserUnit import de.timklge.karooheadwind.util.millimetersInUserUnit
import de.timklge.karooheadwind.util.msInUserUnit import de.timklge.karooheadwind.util.msInUserUnit
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
@ -89,10 +90,6 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
@OptIn(ExperimentalGlanceRemoteViewsApi::class) @OptIn(ExperimentalGlanceRemoteViewsApi::class)
private val glance = GlanceRemoteViews() private val glance = GlanceRemoteViews()
companion object {
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault())
}
data class StreamData(val data: WeatherDataResponse?, val settings: SettingsAndProfile, data class StreamData(val data: WeatherDataResponse?, val settings: SettingsAndProfile,
val widgetSettings: HeadwindWidgetSettings? = null, val widgetSettings: HeadwindWidgetSettings? = null,
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null, val isVisible: Boolean) val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null, val isVisible: Boolean)
@ -304,13 +301,22 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
val isCurrent = baseIndex == 0 && positionIndex == 0 val isCurrent = baseIndex == 0 && positionIndex == 0
val time = if (isCurrent && data?.current != null) {
Instant.ofEpochSecond(data.current.time)
} else {
Instant.ofEpochSecond(data?.forecasts?.getOrNull(baseIndex)?.time ?: 0)
}
if (time.isBefore(Instant.now().minus(1, ChronoUnit.HOURS)) || (upcomingRoute == null && time.isAfter(Instant.now().plus(6, ChronoUnit.HOURS)))) {
Log.d(KarooHeadwindExtension.TAG, "Skipping forecast data for time $time as it is in the past or too close to now")
continue
}
if (isCurrent && data?.current != null) { if (isCurrent && data?.current != null) {
val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode) val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
val unixTime = data.current.time val unixTime = data.current.time
val formattedTime = val formattedTime = getTimeFormatter(context).format(Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalTime())
timeFormatter.format(Instant.ofEpochSecond(unixTime)) val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()))
val formattedDate =
getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
val hasNewDate = formattedDate != previousDate || baseIndex == 0 val hasNewDate = formattedDate != previousDate || baseIndex == 0
RenderWidget( RenderWidget(
@ -335,7 +341,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
val weatherData = data?.forecasts?.getOrNull(baseIndex) val weatherData = data?.forecasts?.getOrNull(baseIndex)
val interpretation = WeatherInterpretation.fromWeatherCode(weatherData?.weatherCode ?: 0) val interpretation = WeatherInterpretation.fromWeatherCode(weatherData?.weatherCode ?: 0)
val unixTime = data?.forecasts?.getOrNull(baseIndex)?.time ?: 0 val unixTime = data?.forecasts?.getOrNull(baseIndex)?.time ?: 0
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime)) val formattedTime = getTimeFormatter(context).format(Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalTime())
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
val hasNewDate = formattedDate != previousDate || baseIndex == 0 val hasNewDate = formattedDate != previousDate || baseIndex == 0

View File

@ -25,6 +25,7 @@ import de.timklge.karooheadwind.streamUpcomingRoute
import de.timklge.karooheadwind.streamUserProfile import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.streamWidgetSettings import de.timklge.karooheadwind.streamWidgetSettings
import de.timklge.karooheadwind.throttle import de.timklge.karooheadwind.throttle
import de.timklge.karooheadwind.util.getTimeFormatter
import de.timklge.karooheadwind.weatherprovider.WeatherData import de.timklge.karooheadwind.weatherprovider.WeatherData
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
@ -49,7 +50,6 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.ceil import kotlin.math.ceil
@ -59,10 +59,6 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
@OptIn(ExperimentalGlanceRemoteViewsApi::class) @OptIn(ExperimentalGlanceRemoteViewsApi::class)
private val glance = GlanceRemoteViews() private val glance = GlanceRemoteViews()
companion object {
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.of("UTC"))
}
data class StreamData(val data: WeatherDataResponse?, val settings: SettingsAndProfile, data class StreamData(val data: WeatherDataResponse?, val settings: SettingsAndProfile,
val widgetSettings: HeadwindWidgetSettings? = null, val widgetSettings: HeadwindWidgetSettings? = null,
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null, val isVisible: Boolean) val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null, val isVisible: Boolean)
@ -252,6 +248,11 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
val time = Instant.ofEpochSecond(data.time) val time = Instant.ofEpochSecond(data.time)
if (time.isBefore(Instant.now().minus(1, ChronoUnit.HOURS)) || (locationData?.coords?.distanceAlongRoute == null && time.isAfter(Instant.now().plus(6, ChronoUnit.HOURS)))) {
Log.d(KarooHeadwindExtension.TAG, "Skipping forecast data for time $time as it is in the past or too close to now")
continue
}
add(LineData( add(LineData(
time = time, time = time,
distance = locationData?.coords?.distanceAlongRoute?.toFloat(), distance = locationData?.coords?.distanceAlongRoute?.toFloat(),
@ -264,7 +265,7 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
val bitmap = LineGraphBuilder(context).drawLineGraph(config.viewSize.first, config.viewSize.second, config.gridSize.first, config.gridSize.second, pointData) { x -> val bitmap = LineGraphBuilder(context).drawLineGraph(config.viewSize.first, config.viewSize.second, config.gridSize.first, config.gridSize.second, pointData) { x ->
val startTime = data.firstOrNull()?.time val startTime = data.firstOrNull()?.time
val time = startTime?.plus(floor(x).toLong(), ChronoUnit.HOURS) val time = startTime?.plus(floor(x).toLong(), ChronoUnit.HOURS)
val timeLabel = timeFormatter.format(time) val timeLabel = getTimeFormatter(context).format(time?.atZone(ZoneId.systemDefault())?.toLocalTime())
val beforeData = data.getOrNull(floor(x).toInt().coerceAtLeast(0)) val beforeData = data.getOrNull(floor(x).toInt().coerceAtLeast(0))
val afterData = data.getOrNull(ceil(x).toInt().coerceAtMost(data.size - 1)) val afterData = data.getOrNull(ceil(x).toInt().coerceAtMost(data.size - 1))

View File

@ -11,6 +11,7 @@ import android.graphics.Path
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import kotlin.math.abs import kotlin.math.abs
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
import kotlin.math.roundToInt
class LineGraphBuilder(val context: Context) { class LineGraphBuilder(val context: Context) {
enum class YAxis { enum class YAxis {
@ -120,26 +121,18 @@ class LineGraphBuilder(val context: Context) {
val yLabelStringsLeft = mutableListOf<String>() val yLabelStringsLeft = mutableListOf<String>()
val numYTicksForCalc = 2 // As used later for drawing Y-axis ticks val numYTicksForCalc = 2 // As used later for drawing Y-axis ticks
val minRange = numYTicksForCalc.toFloat()
if (dataMaxYLeft - dataMinYLeft < minRange) {
dataMaxYLeft += minRange - (dataMaxYLeft - dataMinYLeft)
}
// Determine Y-axis label strings (mirrors logic from where labels are drawn) // Determine Y-axis label strings (mirrors logic from where labels are drawn)
if (abs(dataMaxYLeft - dataMinYLeft) < 0.0001f) { if (abs(dataMaxYLeft - dataMinYLeft) < 0.0001f) {
yLabelStringsLeft.add( yLabelStringsLeft.add(dataMinYLeft.roundToInt().toString())
String.format(
java.util.Locale.getDefault(),
"%.0f",
dataMinYLeft
)
)
} else { } else {
for (i in 0..numYTicksForCalc) { for (i in 0..numYTicksForCalc) {
val value = val value = dataMinYLeft + ((dataMaxYLeft - dataMinYLeft) / numYTicksForCalc) * i
dataMinYLeft + ((dataMaxYLeft - dataMinYLeft) / numYTicksForCalc) * i yLabelStringsLeft.add(value.roundToInt().toString())
yLabelStringsLeft.add(
String.format(
java.util.Locale.getDefault(),
"%.0f",
value
)
)
} }
} }
@ -161,25 +154,18 @@ class LineGraphBuilder(val context: Context) {
val yLabelStringsRight = mutableListOf<String>() val yLabelStringsRight = mutableListOf<String>()
val numYTicksForCalc = 2 // As used later for drawing Y-axis ticks val numYTicksForCalc = 2 // As used later for drawing Y-axis ticks
// Adjust Y-axis range based on numYTicksForCalc.
val minRange = numYTicksForCalc.toFloat()
if (dataMaxYRight - dataMinYRight < minRange) {
dataMaxYRight += minRange - (dataMaxYRight - dataMinYRight)
}
if (abs(dataMaxYRight - dataMinYRight) < 0.0001f) { if (abs(dataMaxYRight - dataMinYRight) < 0.0001f) {
yLabelStringsRight.add( yLabelStringsRight.add(dataMinYRight.roundToInt().toString())
String.format(
java.util.Locale.getDefault(),
"%.0f",
dataMinYRight
)
)
} else { } else {
for (i in 0..numYTicksForCalc) { for (i in 0..numYTicksForCalc) {
val value = val value = dataMinYRight + ((dataMaxYRight - dataMinYRight) / numYTicksForCalc) * i
dataMinYRight + ((dataMaxYRight - dataMinYRight) / numYTicksForCalc) * i yLabelStringsRight.add(value.roundToInt().toString())
yLabelStringsRight.add(
String.format(
java.util.Locale.getDefault(),
"%.0f",
value
)
)
} }
} }
@ -405,7 +391,7 @@ class LineGraphBuilder(val context: Context) {
// Draw faint horizontal grid line // Draw faint horizontal grid line
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint) canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
canvas.drawText( canvas.drawText(
String.format(java.util.Locale.getDefault(), "%.0f", value), value.roundToInt().toString(),
graphLeft - 15f, graphLeft - 15f,
yPos + (textPaint.textSize / 3), yPos + (textPaint.textSize / 3),
textPaint textPaint
@ -418,7 +404,7 @@ class LineGraphBuilder(val context: Context) {
// Draw faint horizontal grid line // Draw faint horizontal grid line
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint) canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
canvas.drawText( canvas.drawText(
String.format(java.util.Locale.getDefault(), "%.0f", dataMinYLeft), dataMinYLeft.roundToInt().toString(),
graphLeft - 15f, graphLeft - 15f,
yPos + (textPaint.textSize / 3), yPos + (textPaint.textSize / 3),
textPaint textPaint
@ -445,7 +431,7 @@ class LineGraphBuilder(val context: Context) {
// Draw faint horizontal grid line // Draw faint horizontal grid line
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint) canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
canvas.drawText( canvas.drawText(
String.format(java.util.Locale.getDefault(), "%.0f", value), value.roundToInt().toString(),
graphRight + 15f, graphRight + 15f,
yPos + (textPaint.textSize / 3), yPos + (textPaint.textSize / 3),
textPaint textPaint
@ -464,7 +450,7 @@ class LineGraphBuilder(val context: Context) {
// Draw faint horizontal grid line // Draw faint horizontal grid line
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint) canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
canvas.drawText( canvas.drawText(
String.format(java.util.Locale.getDefault(), "%.0f", dataMinYRight), dataMinYRight.roundToInt().toString(),
graphRight + 15f, graphRight + 15f,
yPos + (textPaint.textSize / 3), yPos + (textPaint.textSize / 3),
textPaint textPaint
@ -500,7 +486,7 @@ class LineGraphBuilder(val context: Context) {
} }
textPaint.textAlign = Align.CENTER textPaint.textAlign = Align.CENTER
val numXTicks = if (gridHeight > 15) 3 else 1 val numXTicks = if (gridHeight > 15) 2 else 1
if (abs(dataMaxX - dataMinX) > 0.0001f) { if (abs(dataMaxX - dataMinX) > 0.0001f) {
for (i in 0..numXTicks) { for (i in 0..numXTicks) {
val value = dataMinX + ((dataMaxX - dataMinX) / numXTicks) * i val value = dataMinX + ((dataMaxX - dataMinX) / numXTicks) * i

View File

@ -27,7 +27,6 @@ import de.timklge.karooheadwind.HeadwindStats
import de.timklge.karooheadwind.R import de.timklge.karooheadwind.R
import de.timklge.karooheadwind.ServiceStatusSingleton import de.timklge.karooheadwind.ServiceStatusSingleton
import de.timklge.karooheadwind.TemperatureUnit import de.timklge.karooheadwind.TemperatureUnit
import de.timklge.karooheadwind.datatypes.ForecastDataType
import de.timklge.karooheadwind.datatypes.getShortDateFormatter import de.timklge.karooheadwind.datatypes.getShortDateFormatter
import de.timklge.karooheadwind.getGpsCoordinateFlow import de.timklge.karooheadwind.getGpsCoordinateFlow
import de.timklge.karooheadwind.streamCurrentForecastWeatherData import de.timklge.karooheadwind.streamCurrentForecastWeatherData
@ -36,6 +35,7 @@ import de.timklge.karooheadwind.streamStats
import de.timklge.karooheadwind.streamUpcomingRoute import de.timklge.karooheadwind.streamUpcomingRoute
import de.timklge.karooheadwind.streamUserProfile import de.timklge.karooheadwind.streamUserProfile
import de.timklge.karooheadwind.util.celciusInUserUnit import de.timklge.karooheadwind.util.celciusInUserUnit
import de.timklge.karooheadwind.util.getTimeFormatter
import de.timklge.karooheadwind.util.millimetersInUserUnit import de.timklge.karooheadwind.util.millimetersInUserUnit
import de.timklge.karooheadwind.util.msInUserUnit import de.timklge.karooheadwind.util.msInUserUnit
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
@ -43,6 +43,7 @@ import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.UserProfile import io.hammerhead.karooext.models.UserProfile
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -104,7 +105,7 @@ fun WeatherScreen(onFinish: () -> Unit) {
val requestedWeatherPosition = forecastData?.data?.firstOrNull()?.coords val requestedWeatherPosition = forecastData?.data?.firstOrNull()?.coords
val formattedTime = currentWeatherData?.let { ForecastDataType.timeFormatter.format(Instant.ofEpochSecond(it.time)) } val formattedTime = currentWeatherData?.let { getTimeFormatter(ctx).format(Instant.ofEpochSecond(it.time).atZone(ZoneId.systemDefault()).toLocalTime()) }
val formattedDate = currentWeatherData?.let { getShortDateFormatter().format(Instant.ofEpochSecond(it.time)) } val formattedDate = currentWeatherData?.let { getShortDateFormatter().format(Instant.ofEpochSecond(it.time)) }
if (karooConnected == true && currentWeatherData != null) { if (karooConnected == true && currentWeatherData != null) {
@ -225,7 +226,7 @@ fun WeatherScreen(onFinish: () -> Unit) {
val weatherData = data?.forecasts?.getOrNull(index) val weatherData = data?.forecasts?.getOrNull(index)
val interpretation = WeatherInterpretation.fromWeatherCode(weatherData?.weatherCode ?: 0) val interpretation = WeatherInterpretation.fromWeatherCode(weatherData?.weatherCode ?: 0)
val unixTime = weatherData?.time ?: 0 val unixTime = weatherData?.time ?: 0
val formattedForecastTime = ForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime)) val formattedForecastTime = getTimeFormatter(ctx).format(Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalTime())
val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
WeatherWidget( WeatherWidget(

View File

@ -0,0 +1,15 @@
package de.timklge.karooheadwind.util
import android.content.Context
import android.text.format.DateFormat
import java.time.format.DateTimeFormatter
fun getTimeFormatter(context: Context): DateTimeFormatter {
val is24HourFormat = DateFormat.is24HourFormat(context)
return if (is24HourFormat) {
DateTimeFormatter.ofPattern("HH:mm")
} else {
DateTimeFormatter.ofPattern("h a")
}
}