diff --git a/.github/workflows/release_comment.yml b/.github/workflows/release_comment.yml new file mode 100644 index 0000000..64d4ff5 --- /dev/null +++ b/.github/workflows/release_comment.yml @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index db69e73..de9ef91 100644 --- a/README.md +++ b/README.md @@ -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. - 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. +- 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. - 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. diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt index 27f9de9..c07e389 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt @@ -7,6 +7,7 @@ import com.mapbox.turf.TurfMeasurement import de.timklge.karooheadwind.datatypes.CloudCoverDataType import de.timklge.karooheadwind.datatypes.GpsCoordinates import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType +import de.timklge.karooheadwind.datatypes.HeadwindForecastDataType import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType import de.timklge.karooheadwind.datatypes.PrecipitationDataType 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.TailwindAndRideSpeedDataType import de.timklge.karooheadwind.datatypes.TemperatureDataType -import de.timklge.karooheadwind.datatypes.UviDataType import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType +import de.timklge.karooheadwind.datatypes.UviDataType import de.timklge.karooheadwind.datatypes.WeatherForecastDataType import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataTypeCircle @@ -81,6 +82,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS TemperatureForecastDataType(karooSystem), PrecipitationForecastDataType(karooSystem), WindForecastDataType(karooSystem), + HeadwindForecastDataType(karooSystem), WindDirectionAndSpeedDataType(karooSystem, applicationContext), RelativeGradeDataType(karooSystem, applicationContext), RelativeElevationGainDataType(karooSystem, applicationContext), diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindForecastDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindForecastDataType.kt new file mode 100644 index 0000000..d5ac8fe --- /dev/null +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/HeadwindForecastDataType.kt @@ -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, + 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.. + 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 + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/LineGraphForecastDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/LineGraphForecastDataType.kt index 331e23d..9dcd3db 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/LineGraphForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/LineGraphForecastDataType.kt @@ -2,6 +2,7 @@ package de.timklge.karooheadwind.datatypes import android.content.Context import android.graphics.Canvas +import android.graphics.Color import android.util.Log import androidx.compose.ui.unit.DpSize import androidx.core.graphics.createBitmap @@ -16,6 +17,7 @@ import de.timklge.karooheadwind.HeadingResponse import de.timklge.karooheadwind.HeadwindSettings import de.timklge.karooheadwind.HeadwindWidgetSettings import de.timklge.karooheadwind.KarooHeadwindExtension +import de.timklge.karooheadwind.R import de.timklge.karooheadwind.UpcomingRoute import de.timklge.karooheadwind.WeatherDataProvider import de.timklge.karooheadwind.getHeadingFlow @@ -58,6 +60,11 @@ import kotlin.math.ceil import kotlin.math.floor abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) { + sealed class LineGraphForecastData { + data class LineData(val data: Set) : LineGraphForecastData() + data class Error(val message: String) : LineGraphForecastData() + } + @OptIn(ExperimentalGlanceRemoteViewsApi::class) private val glance = GlanceRemoteViews() @@ -75,7 +82,7 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer upcomingRoute: UpcomingRoute?, isPreview: Boolean, context: Context - ): Set + ): LineGraphForecastData private fun previewFlow(settingsAndProfileStream: Flow): Flow = flow { @@ -275,30 +282,41 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer context ) - 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 beforeData = data.getOrNull(floor(x).toInt().coerceAtLeast(0)) - val afterData = data.getOrNull(ceil(x).toInt().coerceAtMost(data.size - 1)) + 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 time = startTime?.plus(floor(x).toLong(), ChronoUnit.HOURS) + val timeLabel = getTimeFormatter(context).format(time?.atZone(ZoneId.systemDefault())?.toLocalTime()) + val beforeData = data.getOrNull(floor(x).toInt().coerceAtLeast(0)) + val afterData = data.getOrNull(ceil(x).toInt().coerceAtMost(data.size - 1)) - if (beforeData?.distance != null || afterData?.distance != null) { - val start = beforeData?.distance ?: 0.0f - 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()}" - } else { - "${(distance / 1000).toInt()}" + if (beforeData?.distance != null || afterData?.distance != null) { + val start = beforeData?.distance ?: 0.0f + 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()}" + } else { + "${(distance / 1000).toInt()}" + } + return@drawLineGraph distanceLabel + } else { + timeLabel + } } - return@drawLineGraph distanceLabel - } else { - timeLabel - } - } - Box(modifier = GlanceModifier.fillMaxSize()){ - Image(ImageProvider(bitmap), "Forecast", modifier = GlanceModifier.fillMaxSize()) + Box(modifier = GlanceModifier.fillMaxSize()){ + Image(ImageProvider(bitmap), "Forecast", modifier = GlanceModifier.fillMaxSize()) + } + } + + is LineGraphForecastData.Error -> { + emitter.onNext(ShowCustomStreamState(pointData.message, null)) + Box(modifier = GlanceModifier.fillMaxSize()){ + + } + } } } diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/PrecipitationForecastDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/PrecipitationForecastDataType.kt index 481ba7a..936c415 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/PrecipitationForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/PrecipitationForecastDataType.kt @@ -12,7 +12,7 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph upcomingRoute: UpcomingRoute?, isPreview: Boolean, context: Context - ): Set { + ): LineGraphForecastData { val precipitationPoints = lineData.map { data -> if (isImperial) { // 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 } - return setOf( + return LineGraphForecastData.LineData(setOf( LineGraphBuilder.Line( dataPoints = precipitationPoints.mapIndexed { index, value -> LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat()) @@ -42,7 +42,7 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph label = "%", yAxis = LineGraphBuilder.YAxis.RIGHT ) - ) + )) } } \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TemperatureForecastDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TemperatureForecastDataType.kt index 45959b8..e6a8054 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TemperatureForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TemperatureForecastDataType.kt @@ -12,7 +12,7 @@ class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphFo upcomingRoute: UpcomingRoute?, isPreview: Boolean, context: Context - ): Set { + ): LineGraphForecastData { val linePoints = lineData.map { data -> if (isImperial) { 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( dataPoints = linePoints.mapIndexed { index, value -> LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat()) @@ -29,7 +29,7 @@ class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphFo color = android.graphics.Color.RED, label = if (!isImperial) "°C" else "°F", ) - ) + )) } } diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WindForecastDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WindForecastDataType.kt index 8399242..561ca5d 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WindForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WindForecastDataType.kt @@ -29,7 +29,7 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD upcomingRoute: UpcomingRoute?, isPreview: Boolean, context: Context - ): Set { + ): LineGraphForecastData { val windPoints = lineData.map { data -> if (isImperial) { // 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 { - if (upcomingRoute != null || isPreview){ - (0.. - 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 { + return LineGraphForecastData.LineData(buildSet { add(LineGraphBuilder.Line( dataPoints = gustPoints.mapIndexed { index, value -> LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat()) @@ -134,25 +62,6 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD color = Color.GRAY, 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 - } - } \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 73309bc..ffd6cda 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,6 +5,7 @@ #ffffff #000000 + #808080 #00ff00 #ff9930 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3bae51b..f49866b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,8 @@ Current hourly temperature forecast Wind Forecast Current hourly wind forecast + Headwind Forecast + Current hourly headwind forecast for loaded route Precipitation Forecast Current hourly precipitation forecast Temperature diff --git a/app/src/main/res/xml/extension_info.xml b/app/src/main/res/xml/extension_info.xml index 72dbe70..ffc15c1 100644 --- a/app/src/main/res/xml/extension_info.xml +++ b/app/src/main/res/xml/extension_info.xml @@ -61,6 +61,13 @@ icon="@drawable/wind" typeId="windForecast" /> + +