fix #156, fix #151: Split wind forecast datafield into headwind forecast and wind / gust forecast (#161)

This commit is contained in:
timklge 2025-07-07 20:29:36 +02:00 committed by GitHub
parent 5169048143
commit 109c002533
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 264 additions and 123 deletions

54
.github/workflows/release_comment.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: Comment on Fixed Issues/PRs on Release
on:
push:
tags:
- '*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to run the workflow for'
required: false
default: ''
jobs:
comment-on-fixed:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for all tags and branches
- name: Find closed issues/PRs and comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Use the input tag if provided, otherwise use the tag from the push event
if [ -n "${{ github.event.inputs.tag }}" ]; then
RELEASE_TAG="${{ github.event.inputs.tag }}"
else
RELEASE_TAG="${{ github.ref }}"
# Remove the 'refs/tags/' part to get the tag name
RELEASE_TAG="${RELEASE_TAG#refs/tags/}"
fi
# Get the previous tag. If there is no previous tag, this will be empty.
PREVIOUS_TAG=$(git tag --sort=-v:refname | grep -v "$RELEASE_TAG" | head -n 1)
# Get the commit range
if [ -z "$PREVIOUS_TAG" ]; then
# If there is no previous tag, get all commits up to the current tag
COMMIT_RANGE="$RELEASE_TAG"
else
COMMIT_RANGE="$PREVIOUS_TAG..$RELEASE_TAG"
fi
# Find the commits in this release
COMMITS=$(git log "$COMMIT_RANGE" --pretty=format:"%B")
# Extract issues/PRs closed (simple regex, can be improved)
echo "$COMMITS" | grep -oE "#[0-9]+" | sort -u | while read ISSUE; do
ISSUE_NUMBER="${ISSUE//#/}"
COMMENT="This issue/pr has been fixed in release ${RELEASE_TAG} :tada:"
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
done
shell: bash

View File

@ -25,6 +25,7 @@ After installing this app on your Karoo and opening it once from the main menu,
- Tailwind with riding speed (graphical, 1x1 field): Shows an arrow indicating the current headwind direction next to a label reading your current speed and the speed of the tailwind. If you ride against a headwind of 5 mph, it will show "-5". If you ride in the same direction of a 5 mph wind, it will read "+5". Text and arrow are colored based on the tailwind speed, with red indicating a strong headwind and green indicating a strong tailwind. - Tailwind with riding speed (graphical, 1x1 field): Shows an arrow indicating the current headwind direction next to a label reading your current speed and the speed of the tailwind. If you ride against a headwind of 5 mph, it will show "-5". If you ride in the same direction of a 5 mph wind, it will read "+5". Text and arrow are colored based on the tailwind speed, with red indicating a strong headwind and green indicating a strong tailwind.
- Wind direction and speed (graphical, 1x1 field): Similar to the tailwind data field, but shows the absolute wind speed and gust speed instead. The "circular" variant uses the same circular graphics as the headwind indicator instead. - Wind direction and speed (graphical, 1x1 field): Similar to the tailwind data field, but shows the absolute wind speed and gust speed instead. The "circular" variant uses the same circular graphics as the headwind indicator 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. - 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.
- Headwind forecast (graphical, 2x1 field): Shows the forecasted headwind speed if a route is loaded.
- 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. If placed in a 1x1 datafield, only the current weather conditions are shown.
- 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 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. - 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.

View File

@ -7,6 +7,7 @@ import com.mapbox.turf.TurfMeasurement
import de.timklge.karooheadwind.datatypes.CloudCoverDataType import de.timklge.karooheadwind.datatypes.CloudCoverDataType
import de.timklge.karooheadwind.datatypes.GpsCoordinates import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
import de.timklge.karooheadwind.datatypes.HeadwindForecastDataType
import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType
import de.timklge.karooheadwind.datatypes.PrecipitationDataType import de.timklge.karooheadwind.datatypes.PrecipitationDataType
import de.timklge.karooheadwind.datatypes.PrecipitationForecastDataType import de.timklge.karooheadwind.datatypes.PrecipitationForecastDataType
@ -17,8 +18,8 @@ import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
import de.timklge.karooheadwind.datatypes.SurfacePressureDataType import de.timklge.karooheadwind.datatypes.SurfacePressureDataType
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
import de.timklge.karooheadwind.datatypes.TemperatureDataType import de.timklge.karooheadwind.datatypes.TemperatureDataType
import de.timklge.karooheadwind.datatypes.UviDataType
import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType
import de.timklge.karooheadwind.datatypes.UviDataType
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataTypeCircle import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataTypeCircle
@ -81,6 +82,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
TemperatureForecastDataType(karooSystem), TemperatureForecastDataType(karooSystem),
PrecipitationForecastDataType(karooSystem), PrecipitationForecastDataType(karooSystem),
WindForecastDataType(karooSystem), WindForecastDataType(karooSystem),
HeadwindForecastDataType(karooSystem),
WindDirectionAndSpeedDataType(karooSystem, applicationContext), WindDirectionAndSpeedDataType(karooSystem, applicationContext),
RelativeGradeDataType(karooSystem, applicationContext), RelativeGradeDataType(karooSystem, applicationContext),
RelativeElevationGainDataType(karooSystem, applicationContext), RelativeElevationGainDataType(karooSystem, applicationContext),

View File

@ -0,0 +1,147 @@
package de.timklge.karooheadwind.datatypes
import android.content.Context
import android.util.Log
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.content.ContextCompat
import com.mapbox.turf.TurfConstants
import com.mapbox.turf.TurfMeasurement
import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.R
import de.timklge.karooheadwind.UpcomingRoute
import de.timklge.karooheadwind.lerpWeather
import de.timklge.karooheadwind.screens.LineGraphBuilder
import de.timklge.karooheadwind.screens.isNightMode
import de.timklge.karooheadwind.util.signedAngleDifference
import io.hammerhead.karooext.KarooSystemService
import kotlin.math.ceil
import kotlin.math.cos
import kotlin.math.floor
fun interpolateWindLineColor(windSpeedInKmh: Double, night: Boolean, context: Context): androidx.compose.ui.graphics.Color {
val default = Color(ContextCompat.getColor(context, R.color.gray))
val green = Color(ContextCompat.getColor(context, R.color.green))
val red = Color(ContextCompat.getColor(context, R.color.red))
val orange = Color(ContextCompat.getColor(context, R.color.orange))
return when {
windSpeedInKmh <= -10 -> green
windSpeedInKmh >= 15 -> red
windSpeedInKmh in -10.0..0.0 -> interpolateColor(green, default, -10.0, 0.0, windSpeedInKmh)
windSpeedInKmh in 0.0..10.0 -> interpolateColor(default, orange, 0.0, 10.0, windSpeedInKmh)
else -> interpolateColor(orange, red, 10.0, 15.0, windSpeedInKmh)
}
}
class HeadwindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastDataType(karooSystem, "headwindForecast") {
override fun getLineData(
lineData: List<LineData>,
isImperial: Boolean,
upcomingRoute: UpcomingRoute?,
isPreview: Boolean,
context: Context
): LineGraphForecastData {
if (upcomingRoute == null && !isPreview){
return LineGraphForecastData.Error("No route loaded")
}
val windPoints = lineData.map { data ->
if (isImperial) { // Convert m/s to mph
data.weatherData.windSpeed * 2.23694 // Convert m/s to mph
} else { // Convert m/s to km/h
data.weatherData.windSpeed * 3.6 // Convert m/s to km/h
}
}
val headwindPoints = try {
if (upcomingRoute != null){
(0..<HEADWIND_SAMPLE_COUNT).mapNotNull { i ->
val t = i / HEADWIND_SAMPLE_COUNT.toDouble()
if (isPreview) {
// Use a sine wave for headwind preview speed
val headwindSpeed = 10f * kotlin.math.sin(i * Math.PI * 2 / HEADWIND_SAMPLE_COUNT).toFloat()
return@mapNotNull LineGraphBuilder.DataPoint(x = i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()),
y = headwindSpeed)
}
val beforeLineData = lineData.getOrNull(floor((lineData.size) * t).toInt().coerceAtLeast(0)) ?: lineData.firstOrNull()
val afterLineData = lineData.getOrNull(ceil((lineData.size) * t).toInt().coerceAtLeast(0)) ?: lineData.lastOrNull()
if (beforeLineData?.weatherData == null || afterLineData?.weatherData == null || beforeLineData.distance == null
|| afterLineData.distance == null || beforeLineData == afterLineData) return@mapNotNull null
val dt = remap(t.toFloat(),
floor(lineData.size * t).toFloat() / lineData.size,
ceil(lineData.size * t).toFloat() / lineData.size,
0.0f, 1.0f
).toDouble()
val interpolatedWeather = lerpWeather(beforeLineData.weatherData, afterLineData.weatherData, dt)
val beforeDistanceAlongRoute = beforeLineData.distance
val afterDistanceAlongRoute = afterLineData.distance
val distanceAlongRoute = (beforeDistanceAlongRoute + (afterDistanceAlongRoute - beforeDistanceAlongRoute) * dt).coerceIn(0.0, upcomingRoute.routeLength)
val coordsAlongRoute = try {
TurfMeasurement.along(upcomingRoute.routePolyline, distanceAlongRoute, TurfConstants.UNIT_METERS)
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error getting coordinates along route", e)
return@mapNotNull null
}
val nextCoordsAlongRoute = try {
TurfMeasurement.along(upcomingRoute.routePolyline, distanceAlongRoute + 5, TurfConstants.UNIT_METERS)
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error getting next coordinates along route", e)
return@mapNotNull null
}
val bearingAlongRoute = try {
TurfMeasurement.bearing(coordsAlongRoute, nextCoordsAlongRoute)
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error calculating bearing along route", e)
return@mapNotNull null
}
val windBearing = interpolatedWeather.windDirection + 180
val diff = signedAngleDifference(bearingAlongRoute, windBearing)
val headwindSpeed = cos( (diff + 180) * Math.PI / 180.0) * interpolatedWeather.windSpeed
val headwindSpeedInUserUnit = if (isImperial) {
headwindSpeed * 2.23694 // Convert m/s to mph
} else {
headwindSpeed * 3.6 // Convert m/s to km/h
}
LineGraphBuilder.DataPoint(
x = i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()),
y = headwindSpeedInUserUnit.toFloat()
)
}
} else {
emptyList()
}
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error calculating headwind points", e)
emptyList()
}
return LineGraphForecastData.LineData(buildSet {
if (headwindPoints.isNotEmpty()) {
add(LineGraphBuilder.Line(
dataPoints = headwindPoints,
color = android.graphics.Color.BLACK,
label = "Head", // if (!isImperial) "Headwind km/h" else "Headwind mph",
drawCircles = false,
colorFunc = { headwindSpeed ->
val headwindSpeedInKmh = headwindSpeed * 3.6 // Convert m/s to km/h
interpolateWindLineColor(headwindSpeedInKmh, isNightMode(context), context).toArgb()
},
alpha = 255
))
}
})
}
companion object {
const val HEADWIND_SAMPLE_COUNT = 70
}
}

View File

@ -2,6 +2,7 @@ package de.timklge.karooheadwind.datatypes
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color
import android.util.Log import android.util.Log
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
@ -16,6 +17,7 @@ import de.timklge.karooheadwind.HeadingResponse
import de.timklge.karooheadwind.HeadwindSettings import de.timklge.karooheadwind.HeadwindSettings
import de.timklge.karooheadwind.HeadwindWidgetSettings import de.timklge.karooheadwind.HeadwindWidgetSettings
import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.R
import de.timklge.karooheadwind.UpcomingRoute import de.timklge.karooheadwind.UpcomingRoute
import de.timklge.karooheadwind.WeatherDataProvider import de.timklge.karooheadwind.WeatherDataProvider
import de.timklge.karooheadwind.getHeadingFlow import de.timklge.karooheadwind.getHeadingFlow
@ -58,6 +60,11 @@ import kotlin.math.ceil
import kotlin.math.floor import kotlin.math.floor
abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) { abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) {
sealed class LineGraphForecastData {
data class LineData(val data: Set<LineGraphBuilder.Line>) : LineGraphForecastData()
data class Error(val message: String) : LineGraphForecastData()
}
@OptIn(ExperimentalGlanceRemoteViewsApi::class) @OptIn(ExperimentalGlanceRemoteViewsApi::class)
private val glance = GlanceRemoteViews() private val glance = GlanceRemoteViews()
@ -75,7 +82,7 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
upcomingRoute: UpcomingRoute?, upcomingRoute: UpcomingRoute?,
isPreview: Boolean, isPreview: Boolean,
context: Context context: Context
): Set<LineGraphBuilder.Line> ): LineGraphForecastData
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> = private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> =
flow { flow {
@ -275,7 +282,9 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
context context
) )
val bitmap = LineGraphBuilder(context).drawLineGraph(config.viewSize.first, config.viewSize.second, config.gridSize.first, config.gridSize.second, pointData) { x -> when (pointData) {
is LineGraphForecastData.LineData -> {
val bitmap = LineGraphBuilder(context).drawLineGraph(config.viewSize.first, config.viewSize.second, config.gridSize.first, config.gridSize.second, pointData.data) { 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 = getTimeFormatter(context).format(time?.atZone(ZoneId.systemDefault())?.toLocalTime()) val timeLabel = getTimeFormatter(context).format(time?.atZone(ZoneId.systemDefault())?.toLocalTime())
@ -302,6 +311,15 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
} }
} }
is LineGraphForecastData.Error -> {
emitter.onNext(ShowCustomStreamState(pointData.message, null))
Box(modifier = GlanceModifier.fillMaxSize()){
}
}
}
}
emitter.updateView(result.remoteViews) emitter.updateView(result.remoteViews)
} }
} }

View File

@ -12,7 +12,7 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph
upcomingRoute: UpcomingRoute?, upcomingRoute: UpcomingRoute?,
isPreview: Boolean, isPreview: Boolean,
context: Context context: Context
): Set<LineGraphBuilder.Line> { ): LineGraphForecastData {
val precipitationPoints = lineData.map { data -> val precipitationPoints = lineData.map { data ->
if (isImperial) { // Convert mm to inches if (isImperial) { // Convert mm to inches
data.weatherData.precipitation * 0.0393701 // Convert mm to inches data.weatherData.precipitation * 0.0393701 // Convert mm to inches
@ -25,7 +25,7 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph
(data.weatherData.precipitationProbability?.coerceAtMost(99.0)) ?: 0.0 // Max 99 % so that the label doesn't take up too much space (data.weatherData.precipitationProbability?.coerceAtMost(99.0)) ?: 0.0 // Max 99 % so that the label doesn't take up too much space
} }
return setOf( return LineGraphForecastData.LineData(setOf(
LineGraphBuilder.Line( LineGraphBuilder.Line(
dataPoints = precipitationPoints.mapIndexed { index, value -> dataPoints = precipitationPoints.mapIndexed { index, value ->
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat()) LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
@ -42,7 +42,7 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph
label = "%", label = "%",
yAxis = LineGraphBuilder.YAxis.RIGHT yAxis = LineGraphBuilder.YAxis.RIGHT
) )
) ))
} }
} }

View File

@ -12,7 +12,7 @@ class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphFo
upcomingRoute: UpcomingRoute?, upcomingRoute: UpcomingRoute?,
isPreview: Boolean, isPreview: Boolean,
context: Context context: Context
): Set<LineGraphBuilder.Line> { ): LineGraphForecastData {
val linePoints = lineData.map { data -> val linePoints = lineData.map { data ->
if (isImperial) { if (isImperial) {
data.weatherData.temperature * 9 / 5 + 32 // Convert Celsius to Fahrenheit data.weatherData.temperature * 9 / 5 + 32 // Convert Celsius to Fahrenheit
@ -21,7 +21,7 @@ class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphFo
} }
} }
return setOf( return LineGraphForecastData.LineData(setOf(
LineGraphBuilder.Line( LineGraphBuilder.Line(
dataPoints = linePoints.mapIndexed { index, value -> dataPoints = linePoints.mapIndexed { index, value ->
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat()) LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
@ -29,7 +29,7 @@ class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphFo
color = android.graphics.Color.RED, color = android.graphics.Color.RED,
label = if (!isImperial) "°C" else "°F", label = if (!isImperial) "°C" else "°F",
) )
) ))
} }
} }

View File

@ -29,7 +29,7 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
upcomingRoute: UpcomingRoute?, upcomingRoute: UpcomingRoute?,
isPreview: Boolean, isPreview: Boolean,
context: Context context: Context
): Set<LineGraphBuilder.Line> { ): LineGraphForecastData {
val windPoints = lineData.map { data -> val windPoints = lineData.map { data ->
if (isImperial) { // Convert m/s to mph if (isImperial) { // Convert m/s to mph
data.weatherData.windSpeed * 2.23694 // Convert m/s to mph data.weatherData.windSpeed * 2.23694 // Convert m/s to mph
@ -46,79 +46,7 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
} }
} }
val headwindPoints = try { return LineGraphForecastData.LineData(buildSet {
if (upcomingRoute != null || isPreview){
(0..<HEADWIND_SAMPLE_COUNT).mapNotNull { i ->
val t = i / HEADWIND_SAMPLE_COUNT.toDouble()
if (isPreview) {
// Use a sine wave for headwind preview speed
val headwindSpeed = 10f * kotlin.math.sin(i * Math.PI * 2 / HEADWIND_SAMPLE_COUNT).toFloat()
val headwindSpeedInKmh = headwindSpeed * 3.6 // Convert m/s to km/h
return@mapNotNull LineGraphBuilder.DataPoint(x = i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()),
y = headwindSpeed)
}
if (upcomingRoute == null) return@mapNotNull null
val beforeLineData = lineData.getOrNull(floor((lineData.size) * t).toInt().coerceAtLeast(0)) ?: lineData.firstOrNull()
val afterLineData = lineData.getOrNull(ceil((lineData.size) * t).toInt().coerceAtLeast(0)) ?: lineData.lastOrNull()
if (beforeLineData?.weatherData == null || afterLineData?.weatherData == null || beforeLineData.distance == null
|| afterLineData.distance == null || beforeLineData == afterLineData) return@mapNotNull null
val dt = remap(t.toFloat(),
floor(lineData.size * t).toFloat() / lineData.size,
ceil(lineData.size * t).toFloat() / lineData.size,
0.0f, 1.0f
).toDouble()
val interpolatedWeather = lerpWeather(beforeLineData.weatherData, afterLineData.weatherData, dt)
val beforeDistanceAlongRoute = beforeLineData.distance
val afterDistanceAlongRoute = afterLineData.distance
val distanceAlongRoute = (beforeDistanceAlongRoute + (afterDistanceAlongRoute - beforeDistanceAlongRoute) * dt).coerceIn(0.0, upcomingRoute.routeLength)
val coordsAlongRoute = try {
TurfMeasurement.along(upcomingRoute.routePolyline, distanceAlongRoute, TurfConstants.UNIT_METERS)
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error getting coordinates along route", e)
return@mapNotNull null
}
val nextCoordsAlongRoute = try {
TurfMeasurement.along(upcomingRoute.routePolyline, distanceAlongRoute + 5, TurfConstants.UNIT_METERS)
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error getting next coordinates along route", e)
return@mapNotNull null
}
val bearingAlongRoute = try {
TurfMeasurement.bearing(coordsAlongRoute, nextCoordsAlongRoute)
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error calculating bearing along route", e)
return@mapNotNull null
}
val windBearing = interpolatedWeather.windDirection + 180
val diff = signedAngleDifference(bearingAlongRoute, windBearing)
val headwindSpeed = cos( (diff + 180) * Math.PI / 180.0) * interpolatedWeather.windSpeed
val headwindSpeedInUserUnit = if (isImperial) {
headwindSpeed * 2.23694 // Convert m/s to mph
} else {
headwindSpeed * 3.6 // Convert m/s to km/h
}
LineGraphBuilder.DataPoint(
x = i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()),
y = headwindSpeedInUserUnit.toFloat()
)
}
} else {
emptyList()
}
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error calculating headwind points", e)
emptyList()
}
return buildSet {
add(LineGraphBuilder.Line( add(LineGraphBuilder.Line(
dataPoints = gustPoints.mapIndexed { index, value -> dataPoints = gustPoints.mapIndexed { index, value ->
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat()) LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
@ -134,25 +62,6 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
color = Color.GRAY, color = Color.GRAY,
label = "Wind" // if (!isImperial) "Wind km/h" else "Wind mph", label = "Wind" // if (!isImperial) "Wind km/h" else "Wind mph",
)) ))
})
if (headwindPoints.isNotEmpty()) {
add(LineGraphBuilder.Line(
dataPoints = headwindPoints,
color = if (isNightMode(context)) Color.WHITE else Color.BLACK,
label = "Head", // if (!isImperial) "Headwind km/h" else "Headwind mph",
drawCircles = false,
colorFunc = { headwindSpeed ->
val headwindSpeedInKmh = headwindSpeed * 3.6 // Convert m/s to km/h
interpolateWindColor(headwindSpeedInKmh, isNightMode(context), context).toArgb()
},
alpha = 255
))
} }
} }
}
companion object {
const val HEADWIND_SAMPLE_COUNT = 50
}
}

View File

@ -5,6 +5,7 @@
<color name="white">#ffffff</color> <color name="white">#ffffff</color>
<color name="black">#000000</color> <color name="black">#000000</color>
<color name="gray">#808080</color>
<color name="green">#00ff00</color> <color name="green">#00ff00</color>
<color name="orange">#ff9930</color> <color name="orange">#ff9930</color>

View File

@ -29,6 +29,8 @@
<string name="temperature_forecast_description">Current hourly temperature forecast</string> <string name="temperature_forecast_description">Current hourly temperature forecast</string>
<string name="wind_forecast">Wind Forecast</string> <string name="wind_forecast">Wind Forecast</string>
<string name="wind_forecast_description">Current hourly wind forecast</string> <string name="wind_forecast_description">Current hourly wind forecast</string>
<string name="headwind_forecast">Headwind Forecast</string>
<string name="headwind_forecast_description">Current hourly headwind forecast for loaded route</string>
<string name="precipitation_forecast">Precipitation Forecast</string> <string name="precipitation_forecast">Precipitation Forecast</string>
<string name="precipitation_forecast_description">Current hourly precipitation forecast</string> <string name="precipitation_forecast_description">Current hourly precipitation forecast</string>
<string name="temperature">Temperature</string> <string name="temperature">Temperature</string>

View File

@ -61,6 +61,13 @@
icon="@drawable/wind" icon="@drawable/wind"
typeId="windForecast" /> typeId="windForecast" />
<DataType
description="@string/headwind_forecast_description"
displayName="@string/headwind_forecast"
graphical="true"
icon="@drawable/wind"
typeId="headwindForecast" />
<DataType <DataType
description="@string/headwind_speed_description" description="@string/headwind_speed_description"
displayName="@string/headwind_speed" displayName="@string/headwind_speed"