Compare commits
No commits in common. "ef5e980de38750a2dad37b8ae1074b030a2b80b2" and "d1b6f2c525886d0360e505a52139ce8c29cb66a0" have entirely different histories.
ef5e980de3
...
d1b6f2c525
@ -23,9 +23,11 @@ After installing this app on your Karoo and opening it once from the main menu,
|
||||
|
||||
- Headwind (graphical, 1x1 field): Shows the headwind direction and speed as a circle with a triangular direction indicator. The speed is shown at the center in your set unit of measurement (default is kilometers per hour if you have set up metric units in your Karoo, otherwise miles per hour). Both direction and speed are relative to the current riding direction by default, i. e., riding directly into a wind of 20 km/h will show a headwind speed of 20 km/h, while riding in the same direction will show -20 km/h.
|
||||
- Tailwind with riding speed (graphical, 1x1 field): Shows an arrow indicating the current headwind direction next to a label reading your current speed and the speed of the tailwind. If you ride against a headwind of 5 mph, it will show "-5". If you ride in the same direction of a 5 mph wind, it will read "+5". Text and arrow are colored based on the tailwind speed, with red indicating a strong headwind and green indicating a strong tailwind.
|
||||
- Tailwind (graphical, 1x1 field): Similar to the tailwind and riding speed field, but shows tailwind speed, wind speed and wind gust speed instead of riding speed.
|
||||
- Wind direction and speed (graphical, 1x1 field): Similar to the tailwind data field, but shows the absolute wind speed and gust speed instead.
|
||||
- Wind forecast / Temperature Forecast / Precipitation forecast (graphical, 2x1 field): Line graphs showing the forecasted wind speeds, temperature or precipitation for the next 12 hours if no route is loaded. If a route is loaded, forecasts along the route will be used instead of the current location.
|
||||
- Weather forecast (graphical, 2x1 field): Shows three columns indicating the current weather conditions (sunny, cloudy, ...), wind direction, precipitation and temperature forecasted for the next three hours. Tap on this widget to cycle through the 12 hour forecast. If you have a route loaded, the forecast widget will show the forecasted weather along points of the route, with an estimated traveled distance per hour of 20 km / 12 miles by default. If placed in a 1x1 datafield, only the current weather conditions are shown.
|
||||
- Weather forecast (graphical, 2x1 field): Shows three columns indicating the current weather conditions (sunny, cloudy, ...), wind direction, precipitation and temperature forecasted for the next three hours. Tap on this widget to cycle through the 12 hour forecast. If you have a route loaded, the forecast widget will show the forecasted weather along points of the route, with an estimated traveled distance per hour of 20 km / 12 miles by default.
|
||||
- Current weather (graphical, 1x1 field): Shows current weather conditions (same as forecast widget, but only for the current time).
|
||||
- Relative grade (numerical): Shows the relative grade. The relative grade is calculated by estimating the force of the headwind, and then calculating the gradient you would need to ride at to experience this resistance if there was no wind. Example: If you are riding on an actual gradient of 2 %, face a headwind of 18 km/h while riding at 29 km/h, the relative grade will be shown as 5.2 % (with 3.2 % added to the actual grade due to the headwind).
|
||||
- Relative elevation gain (numerical): Shows the relative elegation gain. The relative elevation gain is calculated using the relative grade and is an estimation of how much climbing would have been equivalent to the headwind you faced during the ride.
|
||||
- Additionally, data fields that only show the current data value for headwind speed, humidity, cloud cover, absolute wind speed, absolute wind gust speed, absolute wind direction, rainfall and surface pressure can be added if desired.
|
||||
|
||||
@ -5,7 +5,6 @@ import android.util.Log
|
||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||
import de.timklge.karooheadwind.util.signedAngleDifference
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@ -91,8 +90,7 @@ suspend fun KarooSystemService.updateLastKnownGps(context: Context) {
|
||||
|
||||
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> {
|
||||
/* return flow {
|
||||
// emit(GpsCoordinates(52.5164069,13.3784))
|
||||
emit(GpsCoordinates(32.46,-111.524))
|
||||
emit(GpsCoordinates(52.5164069,13.3784))
|
||||
awaitCancellation()
|
||||
} */
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ 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
|
||||
@ -16,9 +17,11 @@ 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.TemperatureForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType
|
||||
import de.timklge.karooheadwind.datatypes.TemperatureDataType
|
||||
import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType
|
||||
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
|
||||
@ -65,12 +68,14 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
||||
listOf(
|
||||
HeadwindDirectionDataType(karooSystem, applicationContext),
|
||||
TailwindAndRideSpeedDataType(karooSystem, applicationContext),
|
||||
WeatherDataType(karooSystem, applicationContext),
|
||||
WeatherForecastDataType(karooSystem),
|
||||
HeadwindSpeedDataType(karooSystem, applicationContext),
|
||||
RelativeHumidityDataType(karooSystem, applicationContext),
|
||||
CloudCoverDataType(karooSystem, applicationContext),
|
||||
WindGustsDataType(karooSystem, applicationContext),
|
||||
WindSpeedDataType(karooSystem, applicationContext),
|
||||
TemperatureDataType(karooSystem, applicationContext),
|
||||
WindDirectionDataType(karooSystem, applicationContext),
|
||||
WindDirectionAndSpeedDataType(karooSystem, applicationContext),
|
||||
PrecipitationDataType(karooSystem, applicationContext),
|
||||
@ -79,6 +84,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
||||
TemperatureForecastDataType(karooSystem),
|
||||
PrecipitationForecastDataType(karooSystem),
|
||||
WindForecastDataType(karooSystem),
|
||||
GraphicalForecastDataType(karooSystem),
|
||||
WindDirectionAndSpeedDataType(karooSystem, applicationContext),
|
||||
RelativeGradeDataType(karooSystem, applicationContext),
|
||||
RelativeElevationGainDataType(karooSystem, applicationContext),
|
||||
|
||||
@ -25,9 +25,9 @@ class CycleHoursAction : ActionCallback {
|
||||
|
||||
var hourOffset = currentSettings.currentForecastHourOffset + 3
|
||||
val requestedPositions = forecastData?.data?.size
|
||||
val requestedHours = forecastData?.data?.firstOrNull()?.forecasts?.size?.coerceAtMost(6)
|
||||
val requestedHours = forecastData?.data?.firstOrNull()?.forecasts?.size
|
||||
|
||||
if (forecastData == null || requestedHours == null || requestedPositions == null || (requestedPositions == 1 && hourOffset >= requestedHours) || (requestedPositions in 2..hourOffset)) {
|
||||
if (forecastData == null || requestedHours == null || requestedPositions == null || hourOffset >= requestedHours || (requestedPositions in 2..hourOffset)) {
|
||||
hourOffset = 0
|
||||
}
|
||||
|
||||
|
||||
@ -38,7 +38,6 @@ import de.timklge.karooheadwind.streamUserProfile
|
||||
import de.timklge.karooheadwind.streamWidgetSettings
|
||||
import de.timklge.karooheadwind.throttle
|
||||
import de.timklge.karooheadwind.util.celciusInUserUnit
|
||||
import de.timklge.karooheadwind.util.getTimeFormatter
|
||||
import de.timklge.karooheadwind.util.millimetersInUserUnit
|
||||
import de.timklge.karooheadwind.util.msInUserUnit
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
@ -90,6 +89,10 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
private val glance = GlanceRemoteViews()
|
||||
|
||||
companion object {
|
||||
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault())
|
||||
}
|
||||
|
||||
data class StreamData(val data: WeatherDataResponse?, val settings: SettingsAndProfile,
|
||||
val widgetSettings: HeadwindWidgetSettings? = null,
|
||||
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null, val isVisible: Boolean)
|
||||
@ -253,7 +256,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
|
||||
if (!config.preview) modifier = modifier.clickable(onClick = actionRunCallback<CycleHoursAction>())
|
||||
|
||||
Row(modifier = modifier, horizontalAlignment = Alignment.Horizontal.CenterHorizontally) {
|
||||
Row(modifier = modifier, horizontalAlignment = Alignment.Horizontal.Start) {
|
||||
val hourOffset = widgetSettings?.currentForecastHourOffset ?: 0
|
||||
val positionOffset = if (allData?.data?.size == 1) 0 else hourOffset
|
||||
|
||||
@ -267,9 +270,6 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
}
|
||||
|
||||
for (baseIndex in hourOffset..hourOffset + 2) {
|
||||
// Only show first value if placed in a 1x1 grid cell
|
||||
if (baseIndex > 0 && config.gridSize.first == 30) break
|
||||
|
||||
val positionIndex = if (allData?.data?.size == 1) 0 else baseIndex
|
||||
|
||||
if (allData?.data?.getOrNull(positionIndex) == null) break
|
||||
@ -301,22 +301,13 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
|
||||
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) {
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode)
|
||||
val unixTime = data.current.time
|
||||
val formattedTime = getTimeFormatter(context).format(Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalTime())
|
||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()))
|
||||
val formattedTime =
|
||||
timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedDate =
|
||||
getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
||||
|
||||
RenderWidget(
|
||||
@ -341,7 +332,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
||||
val weatherData = data?.forecasts?.getOrNull(baseIndex)
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(weatherData?.weatherCode ?: 0)
|
||||
val unixTime = data?.forecasts?.getOrNull(baseIndex)?.time ?: 0
|
||||
val formattedTime = getTimeFormatter(context).format(Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalTime())
|
||||
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
val hasNewDate = formattedDate != previousDate || baseIndex == 0
|
||||
|
||||
|
||||
@ -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.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.TextStyle
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.weatherprovider.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?,
|
||||
isNight: 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, isNight)),
|
||||
contentDescription = "Current weather information",
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
isNight: Boolean,
|
||||
) {
|
||||
GraphicalForecast(
|
||||
current = current,
|
||||
distance = distance,
|
||||
timeLabel = timeLabel,
|
||||
isImperial = isImperial,
|
||||
isNight = isNight
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -25,7 +25,6 @@ import de.timklge.karooheadwind.streamUpcomingRoute
|
||||
import de.timklge.karooheadwind.streamUserProfile
|
||||
import de.timklge.karooheadwind.streamWidgetSettings
|
||||
import de.timklge.karooheadwind.throttle
|
||||
import de.timklge.karooheadwind.util.getTimeFormatter
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
||||
@ -50,6 +49,7 @@ 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.abs
|
||||
import kotlin.math.ceil
|
||||
@ -59,6 +59,10 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
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,
|
||||
val widgetSettings: HeadwindWidgetSettings? = null,
|
||||
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null, val isVisible: Boolean)
|
||||
@ -78,7 +82,7 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
||||
val settingsAndProfile = settingsAndProfileStream.firstOrNull()
|
||||
|
||||
while (true) {
|
||||
val data = (0..<12).map { index ->
|
||||
val data = (0..<10).map { index ->
|
||||
val timeAtFullHour = Instant.now().truncatedTo(ChronoUnit.HOURS).epochSecond
|
||||
|
||||
val weatherData = (0..<12).map {
|
||||
@ -224,13 +228,7 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
||||
val result = glance.compose(context, DpSize.Unspecified) {
|
||||
val data = buildList {
|
||||
for(i in 0..<12){
|
||||
val isRouteLoaded = if (config.preview){
|
||||
true
|
||||
} else {
|
||||
upcomingRoute != null
|
||||
}
|
||||
|
||||
val locationData = if (isRouteLoaded){
|
||||
val locationData = if (upcomingRoute != null){
|
||||
allData?.data?.getOrNull(i)
|
||||
} else {
|
||||
allData?.data?.firstOrNull()
|
||||
@ -248,11 +246,6 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
||||
|
||||
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(
|
||||
time = time,
|
||||
distance = locationData?.coords?.distanceAlongRoute?.toFloat(),
|
||||
@ -265,7 +258,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 startTime = data.firstOrNull()?.time
|
||||
val time = startTime?.plus(floor(x).toLong(), ChronoUnit.HOURS)
|
||||
val timeLabel = getTimeFormatter(context).format(time?.atZone(ZoneId.systemDefault())?.toLocalTime())
|
||||
val timeLabel = timeFormatter.format(time)
|
||||
val beforeData = data.getOrNull(floor(x).toInt().coerceAtLeast(0))
|
||||
val afterData = data.getOrNull(ceil(x).toInt().coerceAtMost(data.size - 1))
|
||||
|
||||
@ -274,9 +267,9 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
||||
val end = (afterData?.distance ?: upcomingRoute?.routeLength?.toFloat()) ?: 0.0f
|
||||
val distance = start + (end - start) * (x - floor(x))
|
||||
val distanceLabel = if (settingsAndProfile.isImperial) {
|
||||
"${(distance * 0.000621371).toInt()}"
|
||||
"${(distance * 0.000621371).toInt()}mi"
|
||||
} else {
|
||||
"${(distance / 1000).toInt()}"
|
||||
"${(distance / 1000).toInt()}km"
|
||||
}
|
||||
return@drawLineGraph distanceLabel
|
||||
} else {
|
||||
|
||||
@ -19,7 +19,7 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph
|
||||
}
|
||||
|
||||
val precipitationPropagation = lineData.map { data ->
|
||||
(data.weatherData.precipitationProbability?.coerceAtMost(99.0)) ?: 0.0 // Max 99 % so that the label doesn't take up too much space
|
||||
data.weatherData.precipitationProbability ?: 0.0
|
||||
}
|
||||
|
||||
return setOf(
|
||||
|
||||
@ -15,6 +15,7 @@ import de.timklge.karooheadwind.HeadingResponse
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.R
|
||||
import de.timklge.karooheadwind.datatypes.TailwindDataType.StreamData
|
||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import de.timklge.karooheadwind.streamDataFlow
|
||||
@ -74,17 +75,6 @@ class TailwindAndRideSpeedDataType(
|
||||
) : DataTypeImpl("karoo-headwind", "tailwind-and-ride-speed") {
|
||||
private val glance = GlanceRemoteViews()
|
||||
|
||||
data class StreamData(
|
||||
val headingResponse: HeadingResponse,
|
||||
val absoluteWindDirection: Double?,
|
||||
val windSpeed: Double?,
|
||||
val settings: HeadwindSettings,
|
||||
val rideSpeed: Double? = null,
|
||||
val gustSpeed: Double? = null,
|
||||
val isImperial: Boolean = false,
|
||||
val isVisible: Boolean = true
|
||||
)
|
||||
|
||||
private fun previewFlow(profileFlow: Flow<UserProfile>): Flow<StreamData> {
|
||||
return flow {
|
||||
val profile = profileFlow.first()
|
||||
|
||||
@ -0,0 +1,192 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||
import androidx.glance.appwidget.GlanceRemoteViews
|
||||
import de.timklge.karooheadwind.HeadingResponse
|
||||
import de.timklge.karooheadwind.HeadwindSettings
|
||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||
import de.timklge.karooheadwind.R
|
||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import de.timklge.karooheadwind.streamDataFlow
|
||||
import de.timklge.karooheadwind.streamDatatypeIsVisible
|
||||
import de.timklge.karooheadwind.streamSettings
|
||||
import de.timklge.karooheadwind.streamUserProfile
|
||||
import de.timklge.karooheadwind.throttle
|
||||
import de.timklge.karooheadwind.util.msInUserUnit
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||
import io.hammerhead.karooext.internal.ViewEmitter
|
||||
import io.hammerhead.karooext.models.DataType
|
||||
import io.hammerhead.karooext.models.ShowCustomStreamState
|
||||
import io.hammerhead.karooext.models.StreamState
|
||||
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
||||
import io.hammerhead.karooext.models.UserProfile
|
||||
import io.hammerhead.karooext.models.ViewConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class TailwindDataType(
|
||||
private val karooSystem: KarooSystemService,
|
||||
private val applicationContext: Context
|
||||
) : DataTypeImpl("karoo-headwind", "tailwind") {
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
private val glance = GlanceRemoteViews()
|
||||
|
||||
data class StreamData(val headingResponse: HeadingResponse,
|
||||
val absoluteWindDirection: Double?,
|
||||
val windSpeed: Double?,
|
||||
val settings: HeadwindSettings,
|
||||
val rideSpeed: Double?,
|
||||
val gustSpeed: Double?,
|
||||
val isImperial: Boolean,
|
||||
val isVisible: Boolean)
|
||||
|
||||
private fun previewFlow(profileFlow: Flow<UserProfile>): Flow<StreamData> {
|
||||
return flow {
|
||||
val profile = profileFlow.first()
|
||||
|
||||
while (true) {
|
||||
val bearing = (0..360).random().toDouble()
|
||||
val windSpeed = (0..10).random()
|
||||
val rideSpeed = (5..10).random().toDouble()
|
||||
val gustSpeed = windSpeed * ((10..40).random().toDouble() / 10)
|
||||
val isImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||
|
||||
emit(StreamData(HeadingResponse.Value(bearing), bearing, windSpeed.toDouble(), HeadwindSettings(), rideSpeed, gustSpeed = gustSpeed, isImperial = isImperial, isVisible = true))
|
||||
|
||||
delay(2_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun streamSpeedInMs(): Flow<Double> {
|
||||
return karooSystem.streamDataFlow(DataType.Type.SMOOTHED_3S_AVERAGE_SPEED)
|
||||
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue ?: 0.0 }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Starting headwind direction view with $emitter")
|
||||
|
||||
val baseBitmap = BitmapFactory.decodeResource(
|
||||
context.resources,
|
||||
R.drawable.arrow_0
|
||||
)
|
||||
|
||||
val configJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
emitter.onNext(UpdateGraphicConfig(showHeader = false))
|
||||
awaitCancellation()
|
||||
}
|
||||
|
||||
val flow = if (config.preview) {
|
||||
previewFlow(karooSystem.streamUserProfile())
|
||||
} else {
|
||||
combine(karooSystem.getRelativeHeadingFlow(context),
|
||||
context.streamCurrentWeatherData(karooSystem),
|
||||
context.streamSettings(karooSystem),
|
||||
karooSystem.streamUserProfile(),
|
||||
streamSpeedInMs(),
|
||||
karooSystem.streamDatatypeIsVisible(dataTypeId)
|
||||
) { data ->
|
||||
val headingResponse = data[0] as HeadingResponse
|
||||
val weatherData = data[1] as? WeatherData
|
||||
val settings = data[2] as HeadwindSettings
|
||||
val userProfile = data[3] as UserProfile
|
||||
val rideSpeedInMs = data[4] as Double
|
||||
val isVisible = data[5] as Boolean
|
||||
|
||||
val isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||
val absoluteWindDirection = weatherData?.windDirection
|
||||
val windSpeed = weatherData?.windSpeed
|
||||
val gustSpeed = weatherData?.windGusts
|
||||
|
||||
StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, rideSpeed = rideSpeedInMs, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible)
|
||||
}
|
||||
}
|
||||
|
||||
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
emitter.onNext(ShowCustomStreamState("", null))
|
||||
|
||||
val refreshRate = karooSystem.getRefreshRateInMilliseconds(context)
|
||||
flow.filter { it.isVisible }.throttle(refreshRate).collect { streamData ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Updating tailwind direction view")
|
||||
|
||||
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
||||
if (value == null || streamData.absoluteWindDirection == null || streamData.windSpeed == null){
|
||||
var headingResponse = streamData.headingResponse
|
||||
|
||||
if (headingResponse is HeadingResponse.Value && (streamData.absoluteWindDirection == null || streamData.windSpeed == null)){
|
||||
headingResponse = HeadingResponse.NoWeatherData
|
||||
}
|
||||
|
||||
emitter.updateView(getErrorWidget(glance, context, streamData.settings, headingResponse).remoteViews)
|
||||
|
||||
return@collect
|
||||
}
|
||||
|
||||
val windSpeed = streamData.windSpeed
|
||||
val windDirection = streamData.headingResponse.diff
|
||||
|
||||
val mainText = let {
|
||||
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||
headwindSpeed.roundToInt().toString()
|
||||
|
||||
val sign = if (headwindSpeed < 0) "+" else {
|
||||
if (headwindSpeed > 0) "-" else ""
|
||||
}
|
||||
|
||||
val headwindSpeedUserUnit = msInUserUnit(headwindSpeed, streamData.isImperial)
|
||||
|
||||
"$sign${headwindSpeedUserUnit.roundToInt().absoluteValue}"
|
||||
}
|
||||
|
||||
val windSpeedUserUnit = msInUserUnit(windSpeed, streamData.isImperial)
|
||||
val gustSpeedUserUnit = msInUserUnit(streamData.gustSpeed ?: 0.0, streamData.isImperial)
|
||||
|
||||
val subtext = "${windSpeedUserUnit.roundToInt()}-${gustSpeedUserUnit.roundToInt()}"
|
||||
|
||||
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||
val windSpeedInKmh = headwindSpeed * 3.6
|
||||
|
||||
val result = glance.compose(context, DpSize.Unspecified) {
|
||||
HeadwindDirection(
|
||||
baseBitmap,
|
||||
windDirection.roundToInt(),
|
||||
config.textSize,
|
||||
mainText,
|
||||
subtext,
|
||||
interpolateWindColor(windSpeedInKmh, false, context),
|
||||
interpolateWindColor(windSpeedInKmh, true, context),
|
||||
wideMode = config.gridSize.first == 60,
|
||||
preview = config.preview,
|
||||
)
|
||||
}
|
||||
|
||||
emitter.updateView(result.remoteViews)
|
||||
}
|
||||
}
|
||||
emitter.setCancellable {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Stopping headwind view with $emitter")
|
||||
configJob.cancel()
|
||||
viewJob.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.models.DataType
|
||||
import io.hammerhead.karooext.models.UserProfile
|
||||
|
||||
class TemperatureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "temperature"){
|
||||
override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
|
||||
return data.temperature
|
||||
}
|
||||
|
||||
override fun getFormatDataType(): String? {
|
||||
return DataType.Type.TEMPERATURE
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
package de.timklge.karooheadwind.datatypes
|
||||
|
||||
import android.content.Context
|
||||
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
|
||||
import androidx.glance.layout.Box
|
||||
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.R
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.getHeadingFlow
|
||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||
import de.timklge.karooheadwind.streamDatatypeIsVisible
|
||||
import de.timklge.karooheadwind.streamSettings
|
||||
import de.timklge.karooheadwind.streamUserProfile
|
||||
import de.timklge.karooheadwind.throttle
|
||||
import de.timklge.karooheadwind.util.celciusInUserUnit
|
||||
import de.timklge.karooheadwind.util.millimetersInUserUnit
|
||||
import de.timklge.karooheadwind.util.msInUserUnit
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import io.hammerhead.karooext.KarooSystemService
|
||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||
import io.hammerhead.karooext.internal.Emitter
|
||||
import io.hammerhead.karooext.internal.ViewEmitter
|
||||
import io.hammerhead.karooext.models.DataPoint
|
||||
import io.hammerhead.karooext.models.DataType
|
||||
import io.hammerhead.karooext.models.ShowCustomStreamState
|
||||
import io.hammerhead.karooext.models.StreamState
|
||||
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
||||
import io.hammerhead.karooext.models.UserProfile
|
||||
import io.hammerhead.karooext.models.ViewConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||
class WeatherDataType(
|
||||
private val karooSystem: KarooSystemService,
|
||||
private val applicationContext: Context
|
||||
) : DataTypeImpl("karoo-headwind", "weather") {
|
||||
private val glance = GlanceRemoteViews()
|
||||
|
||||
companion object {
|
||||
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault())
|
||||
}
|
||||
|
||||
override fun startStream(emitter: Emitter<StreamState>) {
|
||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
||||
val currentWeatherData = applicationContext.streamCurrentWeatherData(karooSystem)
|
||||
|
||||
currentWeatherData.collect { data ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data?.weatherCode}")
|
||||
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to (data?.weatherCode?.toDouble() ?: 0.0)))))
|
||||
}
|
||||
}
|
||||
emitter.setCancellable {
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
data class StreamData(val data: WeatherData?, val settings: HeadwindSettings,
|
||||
val profile: UserProfile? = null, val headingResponse: HeadingResponse? = null,
|
||||
val isVisible: Boolean)
|
||||
|
||||
private fun previewFlow(): Flow<StreamData> = flow {
|
||||
while (true){
|
||||
emit(StreamData(
|
||||
WeatherData(
|
||||
Instant.now().epochSecond, 0.0,
|
||||
20, 50.0, 3.0, 0.0, 1013.25, 980.0, 5.0, 30.0, 10.0,
|
||||
WeatherInterpretation.getKnownWeatherCodes().random(), isForecast = false,
|
||||
isNight = listOf(true, false).random()
|
||||
), HeadwindSettings(), isVisible = true))
|
||||
|
||||
delay(5_000)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Starting weather 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 dataFlow = if (config.preview){
|
||||
previewFlow()
|
||||
} else {
|
||||
combine(
|
||||
context.streamCurrentWeatherData(karooSystem),
|
||||
context.streamSettings(karooSystem),
|
||||
karooSystem.streamUserProfile(),
|
||||
karooSystem.getHeadingFlow(context),
|
||||
karooSystem.streamDatatypeIsVisible(dataTypeId)
|
||||
) { data, settings, profile, heading, isVisible ->
|
||||
StreamData(data, settings, profile, heading, isVisible)
|
||||
}
|
||||
}
|
||||
|
||||
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
emitter.onNext(ShowCustomStreamState("", null))
|
||||
|
||||
val refreshRate = karooSystem.getRefreshRateInMilliseconds(context)
|
||||
|
||||
dataFlow.filter { it.isVisible }.throttle(refreshRate).collect { (data, settings, userProfile, headingResponse) ->
|
||||
Log.d(KarooHeadwindExtension.TAG, "Updating weather view")
|
||||
|
||||
if (data == null){
|
||||
emitter.updateView(getErrorWidget(glance, context, settings, headingResponse).remoteViews)
|
||||
|
||||
return@collect
|
||||
}
|
||||
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(data.weatherCode)
|
||||
val formattedTime = timeFormatter.format(Instant.ofEpochSecond(data.time))
|
||||
val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(data.time))
|
||||
|
||||
val result = glance.compose(context, DpSize.Unspecified) {
|
||||
var modifier = GlanceModifier.fillMaxSize()
|
||||
// TODO reenable once swipes are no longer interpreted as clicks if (!config.preview) modifier = modifier.clickable(onClick = actionStartActivity<MainActivity>())
|
||||
|
||||
Box(modifier = modifier, contentAlignment = Alignment.CenterEnd) {
|
||||
Weather(
|
||||
baseBitmap,
|
||||
current = interpretation,
|
||||
windBearing = data.windDirection.roundToInt(),
|
||||
windSpeed = msInUserUnit(data.windSpeed, userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||
windGusts = msInUserUnit(data.windGusts, userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||
precipitation = millimetersInUserUnit(data.precipitation, userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL),
|
||||
precipitationProbability = null,
|
||||
temperature = celciusInUserUnit(data.temperature, userProfile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||
timeLabel = formattedTime,
|
||||
rowAlignment = when (config.alignment){
|
||||
ViewConfig.Alignment.LEFT -> Alignment.Horizontal.Start
|
||||
ViewConfig.Alignment.CENTER -> Alignment.Horizontal.CenterHorizontally
|
||||
ViewConfig.Alignment.RIGHT -> Alignment.Horizontal.End
|
||||
},
|
||||
dateLabel = formattedDate,
|
||||
singleDisplay = true,
|
||||
isImperial = userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL,
|
||||
isNight = data.isNight,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
emitter.updateView(result.remoteViews)
|
||||
}
|
||||
}
|
||||
emitter.setCancellable {
|
||||
Log.d(KarooHeadwindExtension.TAG, "Stopping headwind view with $emitter")
|
||||
configJob.cancel()
|
||||
viewJob.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,6 @@ import android.graphics.Path
|
||||
import androidx.annotation.ColorInt
|
||||
import kotlin.math.abs
|
||||
import androidx.core.graphics.createBitmap
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class LineGraphBuilder(val context: Context) {
|
||||
enum class YAxis {
|
||||
@ -55,7 +54,7 @@ class LineGraphBuilder(val context: Context) {
|
||||
if (lines.isEmpty() || lines.all { it.dataPoints.isEmpty() }) {
|
||||
val emptyPaint = Paint().apply {
|
||||
color = primaryTextColor
|
||||
textSize = 30f // Increased from 24f
|
||||
textSize = 24f // Increased from 20f
|
||||
textAlign = Align.CENTER
|
||||
isAntiAlias = true
|
||||
}
|
||||
@ -64,8 +63,8 @@ class LineGraphBuilder(val context: Context) {
|
||||
}
|
||||
|
||||
val marginTop = 10f
|
||||
val marginBottom = 55f // Increased from 40f
|
||||
var marginRight = 25f // Increased from 20f // Made var, default updated
|
||||
val marginBottom = 40f // Increased from 30f
|
||||
var marginRight = 20f // Increased from 5f // Made var
|
||||
|
||||
var dataMinX = Float.MAX_VALUE
|
||||
var dataMaxX = Float.MIN_VALUE
|
||||
@ -102,7 +101,7 @@ class LineGraphBuilder(val context: Context) {
|
||||
if (!hasData) {
|
||||
val emptyPaint = Paint().apply {
|
||||
color = primaryTextColor
|
||||
textSize = 60f // Increased from 48f
|
||||
textSize = 48f // Increased from 40f
|
||||
textAlign = Align.CENTER
|
||||
isAntiAlias = true
|
||||
}
|
||||
@ -112,7 +111,7 @@ class LineGraphBuilder(val context: Context) {
|
||||
|
||||
// Dynamically calculate marginLeft based on Y-axis label widths
|
||||
val yAxisLabelPaint = Paint().apply {
|
||||
textSize = 40f // Increased from 32f
|
||||
textSize = 32f
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
@ -121,18 +120,26 @@ class LineGraphBuilder(val context: Context) {
|
||||
val yLabelStringsLeft = mutableListOf<String>()
|
||||
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)
|
||||
if (abs(dataMaxYLeft - dataMinYLeft) < 0.0001f) {
|
||||
yLabelStringsLeft.add(dataMinYLeft.roundToInt().toString())
|
||||
yLabelStringsLeft.add(
|
||||
String.format(
|
||||
java.util.Locale.getDefault(),
|
||||
"%.0f",
|
||||
dataMinYLeft
|
||||
)
|
||||
)
|
||||
} else {
|
||||
for (i in 0..numYTicksForCalc) {
|
||||
val value = dataMinYLeft + ((dataMaxYLeft - dataMinYLeft) / numYTicksForCalc) * i
|
||||
yLabelStringsLeft.add(value.roundToInt().toString())
|
||||
val value =
|
||||
dataMinYLeft + ((dataMaxYLeft - dataMinYLeft) / numYTicksForCalc) * i
|
||||
yLabelStringsLeft.add(
|
||||
String.format(
|
||||
java.util.Locale.getDefault(),
|
||||
"%.0f",
|
||||
value
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,8 +149,8 @@ class LineGraphBuilder(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
val yAxisTextRightToAxisGap = 18f // Increased from 15f
|
||||
val canvasEdgePadding = 3f // Increased from 5f
|
||||
val yAxisTextRightToAxisGap = 15f // Current gap used: graphLeft - 15f
|
||||
val canvasEdgePadding = 5f // Desired padding from the canvas edge
|
||||
|
||||
val dynamicMarginLeft =
|
||||
if (hasLeftYAxisData) maxLabelWidthLeft + yAxisTextRightToAxisGap + canvasEdgePadding else canvasEdgePadding
|
||||
@ -154,18 +161,25 @@ class LineGraphBuilder(val context: Context) {
|
||||
val yLabelStringsRight = mutableListOf<String>()
|
||||
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) {
|
||||
yLabelStringsRight.add(dataMinYRight.roundToInt().toString())
|
||||
yLabelStringsRight.add(
|
||||
String.format(
|
||||
java.util.Locale.getDefault(),
|
||||
"%.0f",
|
||||
dataMinYRight
|
||||
)
|
||||
)
|
||||
} else {
|
||||
for (i in 0..numYTicksForCalc) {
|
||||
val value = dataMinYRight + ((dataMaxYRight - dataMinYRight) / numYTicksForCalc) * i
|
||||
yLabelStringsRight.add(value.roundToInt().toString())
|
||||
val value =
|
||||
dataMinYRight + ((dataMaxYRight - dataMinYRight) / numYTicksForCalc) * i
|
||||
yLabelStringsRight.add(
|
||||
String.format(
|
||||
java.util.Locale.getDefault(),
|
||||
"%.0f",
|
||||
value
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,13 +199,12 @@ class LineGraphBuilder(val context: Context) {
|
||||
val graphBottom = height - marginBottom
|
||||
val graphRight = width - marginRight // Define graphRight for clarity
|
||||
|
||||
|
||||
// Legend properties
|
||||
val legendTextSize = 25f
|
||||
val legendTextSize = 26f // Increased from 22f
|
||||
val legendTextColor = primaryTextColor
|
||||
val legendPadding = 5f
|
||||
val legendEntryHeight = 30f
|
||||
val legendColorBoxSize = 24f
|
||||
val legendEntryHeight = 30f // Increased from 25f
|
||||
val legendColorBoxSize = 24f // Increased from 20f
|
||||
val legendTextMargin = 5f
|
||||
|
||||
var effectiveMinX = dataMinX
|
||||
@ -340,7 +353,7 @@ class LineGraphBuilder(val context: Context) {
|
||||
|
||||
val textPaint = Paint().apply {
|
||||
color = primaryTextColor
|
||||
textSize = 40f // Increased from 32f
|
||||
textSize = 32f // Increased from 28f
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
@ -391,7 +404,7 @@ class LineGraphBuilder(val context: Context) {
|
||||
// Draw faint horizontal grid line
|
||||
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
|
||||
canvas.drawText(
|
||||
value.roundToInt().toString(),
|
||||
String.format(java.util.Locale.getDefault(), "%.0f", value),
|
||||
graphLeft - 15f,
|
||||
yPos + (textPaint.textSize / 3),
|
||||
textPaint
|
||||
@ -404,7 +417,7 @@ class LineGraphBuilder(val context: Context) {
|
||||
// Draw faint horizontal grid line
|
||||
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
|
||||
canvas.drawText(
|
||||
dataMinYLeft.roundToInt().toString(),
|
||||
String.format(java.util.Locale.getDefault(), "%.0f", dataMinYLeft),
|
||||
graphLeft - 15f,
|
||||
yPos + (textPaint.textSize / 3),
|
||||
textPaint
|
||||
@ -431,7 +444,7 @@ class LineGraphBuilder(val context: Context) {
|
||||
// Draw faint horizontal grid line
|
||||
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
|
||||
canvas.drawText(
|
||||
value.roundToInt().toString(),
|
||||
String.format(java.util.Locale.getDefault(), "%.0f", value),
|
||||
graphRight + 15f,
|
||||
yPos + (textPaint.textSize / 3),
|
||||
textPaint
|
||||
@ -450,7 +463,7 @@ class LineGraphBuilder(val context: Context) {
|
||||
// Draw faint horizontal grid line
|
||||
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
|
||||
canvas.drawText(
|
||||
dataMinYRight.roundToInt().toString(),
|
||||
String.format(java.util.Locale.getDefault(), "%.0f", dataMinYRight),
|
||||
graphRight + 15f,
|
||||
yPos + (textPaint.textSize / 3),
|
||||
textPaint
|
||||
@ -486,20 +499,20 @@ class LineGraphBuilder(val context: Context) {
|
||||
}
|
||||
|
||||
textPaint.textAlign = Align.CENTER
|
||||
val numXTicks = if (gridHeight > 15) 2 else 1
|
||||
val numXTicks = if (gridHeight > 15) 3 else 1
|
||||
if (abs(dataMaxX - dataMinX) > 0.0001f) {
|
||||
for (i in 0..numXTicks) {
|
||||
val value = dataMinX + ((dataMaxX - dataMinX) / numXTicks) * i
|
||||
val xPos = mapX(value)
|
||||
if (xPos >= graphLeft - 5f && xPos <= graphLeft + graphWidth + 5f) {
|
||||
canvas.drawLine(xPos, graphBottom - 5f, xPos, graphBottom + 5f, axisPaint)
|
||||
canvas.drawText(labelProvider(value), xPos, graphBottom + 40f, textPaint)
|
||||
canvas.drawText(labelProvider(value), xPos, graphBottom + 30f, textPaint)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val xPos = mapX(dataMinX)
|
||||
canvas.drawLine(xPos, graphBottom - 5f, xPos, graphBottom + 5f, axisPaint)
|
||||
canvas.drawText(labelProvider(dataMinX), xPos, graphBottom + 40f, textPaint)
|
||||
canvas.drawText(labelProvider(dataMinX), xPos, graphBottom + 30f, textPaint)
|
||||
}
|
||||
|
||||
textPaint.textAlign = Align.CENTER
|
||||
|
||||
@ -27,6 +27,9 @@ import de.timklge.karooheadwind.HeadwindStats
|
||||
import de.timklge.karooheadwind.R
|
||||
import de.timklge.karooheadwind.ServiceStatusSingleton
|
||||
import de.timklge.karooheadwind.TemperatureUnit
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
import de.timklge.karooheadwind.datatypes.ForecastDataType
|
||||
import de.timklge.karooheadwind.datatypes.WeatherDataType.Companion.timeFormatter
|
||||
import de.timklge.karooheadwind.datatypes.getShortDateFormatter
|
||||
import de.timklge.karooheadwind.getGpsCoordinateFlow
|
||||
import de.timklge.karooheadwind.streamCurrentForecastWeatherData
|
||||
@ -35,15 +38,12 @@ import de.timklge.karooheadwind.streamStats
|
||||
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||
import de.timklge.karooheadwind.streamUserProfile
|
||||
import de.timklge.karooheadwind.util.celciusInUserUnit
|
||||
import de.timklge.karooheadwind.util.getTimeFormatter
|
||||
import de.timklge.karooheadwind.util.millimetersInUserUnit
|
||||
import de.timklge.karooheadwind.util.msInUserUnit
|
||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||
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.temporal.ChronoUnit
|
||||
import kotlin.math.roundToInt
|
||||
@ -105,7 +105,7 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
||||
|
||||
val requestedWeatherPosition = forecastData?.data?.firstOrNull()?.coords
|
||||
|
||||
val formattedTime = currentWeatherData?.let { getTimeFormatter(ctx).format(Instant.ofEpochSecond(it.time).atZone(ZoneId.systemDefault()).toLocalTime()) }
|
||||
val formattedTime = currentWeatherData?.let { timeFormatter.format(Instant.ofEpochSecond(it.time)) }
|
||||
val formattedDate = currentWeatherData?.let { getShortDateFormatter().format(Instant.ofEpochSecond(it.time)) }
|
||||
|
||||
if (karooConnected == true && currentWeatherData != null) {
|
||||
@ -226,7 +226,7 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
||||
val weatherData = data?.forecasts?.getOrNull(index)
|
||||
val interpretation = WeatherInterpretation.fromWeatherCode(weatherData?.weatherCode ?: 0)
|
||||
val unixTime = weatherData?.time ?: 0
|
||||
val formattedForecastTime = getTimeFormatter(ctx).format(Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalTime())
|
||||
val formattedForecastTime = ForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime))
|
||||
val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime))
|
||||
|
||||
WeatherWidget(
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,8 @@
|
||||
<string name="surfacePressure_description">Atmospheric pressure at surface in configured unit</string>
|
||||
<string name="sealevelPressure">Sealevel pressure</string>
|
||||
<string name="sealevelPressure_description">Atmospheric pressure at sea level in configured unit</string>
|
||||
<string name="weather">Weather</string>
|
||||
<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>
|
||||
@ -31,8 +33,14 @@
|
||||
<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>
|
||||
<string name="tailwind">Tailwind</string>
|
||||
<string name="tailwind_description">Current tailwind, wind speed and gust speed</string>
|
||||
<string name="relativeGrade">Relative Grade</string>
|
||||
<string name="relativeGrade_description">Perceived grade in percent</string>
|
||||
<string name="relativeElevationGain">Relative Elevation Gain</string>
|
||||
|
||||
@ -12,6 +12,13 @@
|
||||
icon="@drawable/wind"
|
||||
typeId="tailwind-and-ride-speed" />
|
||||
|
||||
<DataType
|
||||
description="@string/tailwind_description"
|
||||
displayName="@string/tailwind"
|
||||
graphical="true"
|
||||
icon="@drawable/wind"
|
||||
typeId="tailwind" />
|
||||
|
||||
<DataType
|
||||
description="@string/headwind_description"
|
||||
displayName="@string/headwind"
|
||||
@ -20,11 +27,11 @@
|
||||
typeId="headwind" />
|
||||
|
||||
<DataType
|
||||
description="@string/windDirectionAndSpeed_description"
|
||||
displayName="@string/windDirectionAndSpeed"
|
||||
graphical="false"
|
||||
description="@string/weather_description"
|
||||
displayName="@string/weather"
|
||||
graphical="true"
|
||||
icon="@drawable/wind"
|
||||
typeId="windDirectionAndSpeed" />
|
||||
typeId="weather" />
|
||||
|
||||
<DataType
|
||||
description="@string/weather_forecast_description"
|
||||
@ -54,6 +61,13 @@
|
||||
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"
|
||||
@ -61,6 +75,13 @@
|
||||
icon="@drawable/wind"
|
||||
typeId="headwindSpeed" />
|
||||
|
||||
<DataType
|
||||
description="@string/windDirectionAndSpeed_description"
|
||||
displayName="@string/windDirectionAndSpeed"
|
||||
graphical="false"
|
||||
icon="@drawable/wind"
|
||||
typeId="windDirectionAndSpeed" />
|
||||
|
||||
<DataType
|
||||
description="@string/relativeHumidity_description"
|
||||
displayName="@string/relativeHumidity"
|
||||
@ -117,6 +138,13 @@
|
||||
icon="@drawable/ic_cloud"
|
||||
typeId="sealevelPressure" />
|
||||
|
||||
<DataType
|
||||
description="@string/temperature_description"
|
||||
displayName="@string/temperature"
|
||||
graphical="false"
|
||||
icon="@drawable/thermometer"
|
||||
typeId="temperature" />
|
||||
|
||||
<DataType
|
||||
description="@string/relativeGrade_description"
|
||||
displayName="@string/relativeGrade"
|
||||
|
||||
BIN
preview0.png
BIN
preview0.png
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 39 KiB |
Loading…
x
Reference in New Issue
Block a user