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 764f2a2..a3c434c 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/LineGraphForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/LineGraphForecastDataType.kt @@ -1,8 +1,10 @@ package de.timklge.karooheadwind.datatypes import android.content.Context +import android.graphics.Canvas import android.util.Log import androidx.compose.ui.unit.DpSize +import androidx.core.graphics.createBitmap import androidx.glance.GlanceModifier import androidx.glance.Image import androidx.glance.ImageProvider @@ -71,7 +73,8 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer lineData: List, isImperial: Boolean, upcomingRoute: UpcomingRoute?, - isPreview: Boolean + isPreview: Boolean, + context: Context ): Set private fun previewFlow(settingsAndProfileStream: Flow): Flow = @@ -265,8 +268,10 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer data, settingsAndProfile.isImperialTemperature, upcomingRoute, - config.preview + config.preview, + 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) 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 e66d9a4..481ba7a 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/PrecipitationForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/PrecipitationForecastDataType.kt @@ -1,5 +1,6 @@ package de.timklge.karooheadwind.datatypes +import android.content.Context import de.timklge.karooheadwind.UpcomingRoute import de.timklge.karooheadwind.screens.LineGraphBuilder import io.hammerhead.karooext.KarooSystemService @@ -9,7 +10,8 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph lineData: List, isImperial: Boolean, upcomingRoute: UpcomingRoute?, - isPreview: Boolean + isPreview: Boolean, + context: Context ): Set { val precipitationPoints = lineData.map { data -> if (isImperial) { // Convert mm to inches diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TailwindAndRideSpeedDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TailwindAndRideSpeedDataType.kt index a2a4279..9b7be3e 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TailwindAndRideSpeedDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TailwindAndRideSpeedDataType.kt @@ -16,6 +16,7 @@ import de.timklge.karooheadwind.HeadwindSettings import de.timklge.karooheadwind.KarooHeadwindExtension import de.timklge.karooheadwind.R import de.timklge.karooheadwind.getRelativeHeadingFlow +import de.timklge.karooheadwind.screens.isNightMode import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamDataFlow import de.timklge.karooheadwind.streamDatatypeIsVisible @@ -199,14 +200,10 @@ class TailwindAndRideSpeedDataType( "$sign${headwindSpeedUserUnit.roundToInt().absoluteValue} ${windSpeedUserUnit.roundToInt()}${gustSpeedAddon}" } - - var dayColor = Color(ContextCompat.getColor(context, R.color.black)) - var nightColor = Color(ContextCompat.getColor(context, R.color.white)) - val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed val windSpeedInKmh = headwindSpeed * 3.6 - dayColor = interpolateWindColor(windSpeedInKmh, false, context) - nightColor = interpolateWindColor(windSpeedInKmh, true, context) + val dayColor = interpolateWindColor(windSpeedInKmh, false, context) + val nightColor = interpolateWindColor(windSpeedInKmh, true, context) val result = glance.compose(context, DpSize.Unspecified) { HeadwindDirection( 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 901d5e4..45959b8 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TemperatureForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/TemperatureForecastDataType.kt @@ -1,5 +1,6 @@ package de.timklge.karooheadwind.datatypes +import android.content.Context import de.timklge.karooheadwind.UpcomingRoute import de.timklge.karooheadwind.screens.LineGraphBuilder import io.hammerhead.karooext.KarooSystemService @@ -9,7 +10,8 @@ class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphFo lineData: List, isImperial: Boolean, upcomingRoute: UpcomingRoute?, - isPreview: Boolean + isPreview: Boolean, + context: Context ): Set { val linePoints = lineData.map { data -> if (isImperial) { 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 3d6d89f..8399242 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WindForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WindForecastDataType.kt @@ -1,12 +1,16 @@ package de.timklge.karooheadwind.datatypes +import android.content.Context +import android.graphics.Color import android.util.Log +import androidx.compose.ui.graphics.toArgb import com.mapbox.turf.TurfConstants import com.mapbox.turf.TurfMeasurement import de.timklge.karooheadwind.KarooHeadwindExtension 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 @@ -23,7 +27,8 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD lineData: List, isImperial: Boolean, upcomingRoute: UpcomingRoute?, - isPreview: Boolean + isPreview: Boolean, + context: Context ): Set { val windPoints = lineData.map { data -> if (isImperial) { // Convert m/s to mph @@ -47,16 +52,21 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD val t = i / HEADWIND_SAMPLE_COUNT.toDouble() if (isPreview) { - return@mapNotNull LineGraphBuilder.DataPoint(i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()), (-10f) + (20f * kotlin.random.Random.nextFloat())) + // 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()) ?: lineData.firstOrNull() - val afterLineData = lineData.getOrNull(ceil(lineData.size * t).toInt()) ?: lineData.lastOrNull() + 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) return@mapNotNull null + || afterLineData.distance == null || beforeLineData == afterLineData) return@mapNotNull null val dt = remap(t.toFloat(), floor(lineData.size * t).toFloat() / lineData.size, @@ -95,7 +105,10 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD headwindSpeed * 3.6 // Convert m/s to km/h } - LineGraphBuilder.DataPoint(i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()), headwindSpeedInUserUnit.toFloat()) + LineGraphBuilder.DataPoint( + x = i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()), + y = headwindSpeedInUserUnit.toFloat() + ) } } else { emptyList() @@ -106,34 +119,40 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD } return buildSet { - add(LineGraphBuilder.Line( - dataPoints = windPoints.mapIndexed { index, value -> - LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat()) - }, - color = android.graphics.Color.GRAY, - label = "Wind" // if (!isImperial) "Wind km/h" else "Wind mph", - )) add(LineGraphBuilder.Line( dataPoints = gustPoints.mapIndexed { index, value -> LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat()) }, - color = android.graphics.Color.DKGRAY, + color = Color.DKGRAY, label = "Gust" // if (!isImperial) "Gust km/h" else "Gust mph", )) + add(LineGraphBuilder.Line( + dataPoints = windPoints.mapIndexed { index, value -> + LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat()) + }, + color = Color.GRAY, + label = "Wind" // if (!isImperial) "Wind km/h" else "Wind mph", + )) + if (headwindPoints.isNotEmpty()) { add(LineGraphBuilder.Line( dataPoints = headwindPoints, - color = android.graphics.Color.MAGENTA, - label = "Headwind", // if (!isImperial) "Headwind km/h" else "Headwind mph", - drawCircles = false + 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 = 30 + const val HEADWIND_SAMPLE_COUNT = 50 } } \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/LineGraph.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/LineGraph.kt index 35abe20..a16b76e 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/screens/LineGraph.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/LineGraph.kt @@ -9,30 +9,33 @@ import android.graphics.Paint import android.graphics.Paint.Align import android.graphics.Path import androidx.annotation.ColorInt -import kotlin.math.abs import androidx.core.graphics.createBitmap +import kotlin.math.abs +import kotlin.math.floor import kotlin.math.roundToInt +fun isNightMode(context: Context): Boolean { + val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return nightModeFlags == Configuration.UI_MODE_NIGHT_YES +} + class LineGraphBuilder(val context: Context) { enum class YAxis { LEFT, RIGHT } - data class DataPoint(val x: Float, val y: Float) + data class DataPoint(val x: Float, val y: Float) // color field removed data class Line( - val dataPoints: List, + val dataPoints: List, // DataPoint type is now the new one @ColorInt val color: Int, val label: String? = null, val yAxis: YAxis = YAxis.LEFT, // Default to left Y-axis - val drawCircles: Boolean = true // Default to true + val drawCircles: Boolean = true, // Default to true + val colorFunc: ((Float) -> Int)? = null, // Optional color function for dynamic colors, + val alpha: Int = 80 ) - private fun isNightMode(): Boolean { - val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - return nightModeFlags == Configuration.UI_MODE_NIGHT_YES - } - fun drawLineGraph( width: Int, height: Int, @@ -41,10 +44,9 @@ class LineGraphBuilder(val context: Context) { lines: Set, labelProvider: ((Float) -> String) ): Bitmap { - val isNightMode = isNightMode() - val bitmap = createBitmap(width, height) val canvas = Canvas(bitmap) + val isNightMode = isNightMode(context) val backgroundColor = if (isNightMode) Color.BLACK else Color.WHITE val primaryTextColor = if (isNightMode) Color.WHITE else Color.BLACK @@ -287,7 +289,7 @@ class LineGraphBuilder(val context: Context) { if (!hasRightYAxisData || abs(effectiveMaxYRight - effectiveMinYRight) < 0.0001f) 1f else (effectiveMaxYRight - effectiveMinYRight) fun mapX(originalX: Float): Float { - return graphLeft + ((originalX - effectiveMinX) / rangeX) * graphWidth + return floor(graphLeft + ((originalX - effectiveMinX) / rangeX) * graphWidth) } fun mapYLeft(originalY: Float): Float { @@ -347,35 +349,113 @@ class LineGraphBuilder(val context: Context) { for (line in lines) { if (line.dataPoints.isEmpty()) continue - linePaint.color = line.color - val path = Path() - val firstPoint = line.dataPoints.first() val mapY = if (line.yAxis == YAxis.LEFT) ::mapYLeft else ::mapYRight - path.moveTo(mapX(firstPoint.x), mapY(firstPoint.y)) - if (line.drawCircles) { - canvas.drawCircle( - mapX(firstPoint.x), - mapY(firstPoint.y), - 8f, - linePaint.apply { style = Paint.Style.FILL }) - } - linePaint.style = Paint.Style.STROKE - + // Draw area between line and X axis, colorized per segment (match line colorization) + val zeroY = mapY((if (line.yAxis == YAxis.LEFT) effectiveMinYLeft else effectiveMinYRight).coerceAtLeast(0f)) for (i in 1 until line.dataPoints.size) { - val point = line.dataPoints[i] - path.lineTo(mapX(point.x), mapY(point.y)) - if (line.drawCircles) { - canvas.drawCircle( - mapX(point.x), - mapY(point.y), - 8f, - linePaint.apply { style = Paint.Style.FILL }) + val prev = line.dataPoints[i - 1] + val curr = line.dataPoints[i] + if (line.colorFunc != null) { + val N = 4 // Number of sub-segments (tweak for smoothness/performance) + val adjustment = 0.5f // Pixel adjustment to help close seams + + for (j in 0 until N) { + val t0 = j / N.toFloat() + val t1 = (j + 1) / N.toFloat() + val x0 = prev.x + (curr.x - prev.x) * t0 + val y0 = prev.y + (curr.y - prev.y) * t0 + val x1 = prev.x + (curr.x - prev.x) * t1 + val y1 = prev.y + (curr.y - prev.y) * t1 + val color0 = line.colorFunc.invoke(y0) + + val mappedX0 = mapX(x0) + val mappedY0 = mapY(y0) + val mappedX1 = mapX(x1) + val mappedY1 = mapY(y1) + + // Extend the right edge of internal sub-segments to create an overlap + val rightEdgeX = if (j < N - 1) mappedX1 + adjustment else mappedX1 + + val areaPath = Path().apply { + moveTo(mappedX0, mappedY0) + lineTo(rightEdgeX, mappedY1) + lineTo(rightEdgeX, zeroY) + lineTo(mappedX0, zeroY) + close() + } + val areaPaint = Paint().apply { + style = Paint.Style.FILL + color = color0 + isAntiAlias = true + alpha = line.alpha + } + canvas.drawPath(areaPath, areaPaint) + } + } else { + val areaPath = Path().apply { + moveTo(mapX(prev.x), mapY(prev.y)) + lineTo(mapX(curr.x), mapY(curr.y)) + lineTo(mapX(curr.x), zeroY) + lineTo(mapX(prev.x), zeroY) + close() + } + val areaPaint = Paint().apply { + style = Paint.Style.FILL + color = line.color + isAntiAlias = true + alpha = line.alpha + } + canvas.drawPath(areaPath, areaPaint) } - linePaint.style = Paint.Style.STROKE } - canvas.drawPath(path, linePaint) + // Draw the line, colorized per segment (improved: split for colorFunc) + for (i in 1 until line.dataPoints.size) { + val prev = line.dataPoints[i - 1] + val curr = line.dataPoints[i] + if (line.colorFunc != null) { + val N = 4 // Number of sub-segments (tweak for smoothness/performance) + for (j in 0 until N) { + val t0 = j / N.toFloat() + val t1 = (j + 1) / N.toFloat() + val x0 = prev.x + (curr.x - prev.x) * t0 + val y0 = prev.y + (curr.y - prev.y) * t0 + val x1 = prev.x + (curr.x - prev.x) * t1 + val y1 = prev.y + (curr.y - prev.y) * t1 + val color0 = line.colorFunc.invoke(y0) + // Optionally, blend color0 and color1 for the segment, or just use color0 + val segPaint = Paint(linePaint).apply { + color = color0 + } + canvas.drawLine( + mapX(x0), mapY(y0), + mapX(x1), mapY(y1), + segPaint + ) + } + } else { + val segPaint = Paint(linePaint).apply { + color = line.color + } + canvas.drawLine( + mapX(prev.x), mapY(prev.y), + mapX(curr.x), mapY(curr.y), + segPaint + ) + } + } + + // Draw circles if enabled + if (line.drawCircles) { + for (point in line.dataPoints) { + val circlePaint = Paint(linePaint).apply { + style = Paint.Style.FILL + color = line.colorFunc?.invoke(point.y) ?: line.color + } + canvas.drawCircle(mapX(point.x), mapY(point.y), 8f, circlePaint) + } + } } // Draw Left Y-axis ticks and labels