ref #8: Replace fixed arrow images, use gps bearing, moving average
@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.filterNotNull
|
|||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import kotlinx.coroutines.flow.scan
|
||||||
import kotlinx.coroutines.flow.single
|
import kotlinx.coroutines.flow.single
|
||||||
import kotlinx.coroutines.flow.timeout
|
import kotlinx.coroutines.flow.timeout
|
||||||
import kotlinx.coroutines.time.debounce
|
import kotlinx.coroutines.time.debounce
|
||||||
@ -167,24 +168,36 @@ fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<Double> {
|
|||||||
|
|
||||||
return getHeadingFlow()
|
return getHeadingFlow()
|
||||||
.filter { it >= 0 }
|
.filter { it >= 0 }
|
||||||
.combine(currentWeatherData) { cardinalDirection, data -> cardinalDirection to data }
|
.combine(currentWeatherData) { bearing, data -> bearing to data }
|
||||||
.map { (cardinalDirection, data) ->
|
.map { (bearing, data) ->
|
||||||
val bearing = cardinalDirection * 45.0
|
|
||||||
val windBearing = data.current.windDirection + 180
|
val windBearing = data.current.windDirection + 180
|
||||||
val diff = (signedAngleDifference(bearing, windBearing) + 360) % 360
|
val diff = signedAngleDifference(bearing, windBearing)
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Wind bearing: $bearing vs $windBearing => $diff")
|
Log.d(KarooHeadwindExtension.TAG, "Wind bearing: $bearing vs $windBearing => $diff")
|
||||||
|
|
||||||
diff
|
diff
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun KarooSystemService.getHeadingFlow(): Flow<Int> {
|
fun Double.lerp(target: Double, alpha: Double): Double {
|
||||||
// return flowOf(2)
|
return this + (target - this) * alpha
|
||||||
|
}
|
||||||
|
|
||||||
return streamDataFlow(DataType.Type.HEADING)
|
fun KarooSystemService.getHeadingFlow(): Flow<Double> {
|
||||||
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
|
//return flowOf(20.0)
|
||||||
.map { it.roundToInt() }
|
|
||||||
|
return streamDataFlow("TYPE_LOCATION_ID")
|
||||||
|
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.values }
|
||||||
|
.map { values ->
|
||||||
|
val heading = values[DataType.Field.LOC_BEARING]
|
||||||
|
|
||||||
|
heading ?: 0.0
|
||||||
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
.scan(emptyList<Double>()) { acc, value ->
|
||||||
|
val newAcc = acc + value
|
||||||
|
if (newAcc.size > 3) newAcc.drop(1) else newAcc
|
||||||
|
}
|
||||||
|
.map { it.average() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
|||||||
import androidx.glance.appwidget.GlanceRemoteViews
|
import androidx.glance.appwidget.GlanceRemoteViews
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||||
import de.timklge.karooheadwind.getHeadingFlow
|
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
import de.timklge.karooheadwind.screens.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.R
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.TextUnitType
|
import androidx.compose.ui.unit.TextUnitType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -9,6 +17,7 @@ import androidx.glance.ColorFilter
|
|||||||
import androidx.glance.GlanceModifier
|
import androidx.glance.GlanceModifier
|
||||||
import androidx.glance.Image
|
import androidx.glance.Image
|
||||||
import androidx.glance.ImageProvider
|
import androidx.glance.ImageProvider
|
||||||
|
import androidx.glance.LocalContext
|
||||||
import androidx.glance.background
|
import androidx.glance.background
|
||||||
import androidx.glance.color.ColorProvider
|
import androidx.glance.color.ColorProvider
|
||||||
import androidx.glance.layout.Alignment
|
import androidx.glance.layout.Alignment
|
||||||
@ -20,27 +29,45 @@ import androidx.glance.preview.ExperimentalGlancePreviewApi
|
|||||||
import androidx.glance.preview.Preview
|
import androidx.glance.preview.Preview
|
||||||
import androidx.glance.text.Text
|
import androidx.glance.text.Text
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.timklge.karooheadwind.R
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
fun getArrowResourceByBearing(bearing: Int): Int {
|
|
||||||
val oclock = ((bearing % 360) / 30.0).roundToInt()
|
|
||||||
|
|
||||||
return when (oclock){
|
val bitmapsByBearing = mutableMapOf<Int, Bitmap>()
|
||||||
0 -> R.drawable.arrow_0
|
|
||||||
1 -> R.drawable.arrow_1
|
fun getArrowBitmapByBearing(bearing: Int): Bitmap {
|
||||||
2 -> R.drawable.arrow_2
|
synchronized(bitmapsByBearing) {
|
||||||
3 -> R.drawable.arrow_3
|
val bearingRounded = (((bearing + 360) / 5.0).roundToInt() * 5) % 360
|
||||||
4 -> R.drawable.arrow_4
|
|
||||||
5 -> R.drawable.arrow_5
|
val storedBitmap = bitmapsByBearing[bearingRounded]
|
||||||
6 -> R.drawable.arrow_6
|
if (storedBitmap != null) return storedBitmap
|
||||||
7 -> R.drawable.arrow_7
|
|
||||||
8 -> R.drawable.arrow_8
|
val bitmap = Bitmap.createBitmap(128, 128, Bitmap.Config.ARGB_8888)
|
||||||
9 -> R.drawable.arrow_9
|
val canvas = Canvas(bitmap)
|
||||||
10 -> R.drawable.arrow_10
|
|
||||||
11 -> R.drawable.arrow_11
|
val paint = Paint().apply {
|
||||||
12 -> R.drawable.arrow_0
|
color = android.graphics.Color.WHITE
|
||||||
else -> error("Bearing $bearing out of range")
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = 15f
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(64f, 0f) // Top point of the arrow
|
||||||
|
lineTo(128f, 128f) // Bottom right point of the arrow
|
||||||
|
lineTo(64f, 96f) // Middle bottom point of the arrow
|
||||||
|
lineTo(0f, 128f) // Bottom left point of the arrow
|
||||||
|
close() // Close the path to form the arrow shape
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.save()
|
||||||
|
canvas.rotate(bearing.toFloat(), 64f, 64f) // Rotate the canvas based on the bearing
|
||||||
|
canvas.scale(0.75f, 0.75f, 64f, 64f) // Scale the arrow down to fit the canvas
|
||||||
|
canvas.drawPath(path, paint)
|
||||||
|
canvas.restore()
|
||||||
|
|
||||||
|
bitmapsByBearing[bearingRounded] = bitmap
|
||||||
|
|
||||||
|
return bitmap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +84,7 @@ fun HeadwindDirection(bearing: Int, fontSize: Int, overlayText: String? = null)
|
|||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
modifier = GlanceModifier.fillMaxSize(),
|
modifier = GlanceModifier.fillMaxSize(),
|
||||||
provider = ImageProvider(getArrowResourceByBearing(bearing)),
|
provider = ImageProvider(getArrowBitmapByBearing(bearing)),
|
||||||
contentDescription = "Relative wind direction indicator",
|
contentDescription = "Relative wind direction indicator",
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import androidx.glance.appwidget.GlanceRemoteViews
|
|||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||||
import de.timklge.karooheadwind.WeatherInterpretation
|
import de.timklge.karooheadwind.WeatherInterpretation
|
||||||
import de.timklge.karooheadwind.getHeadingFlow
|
|
||||||
import de.timklge.karooheadwind.screens.HeadwindSettings
|
import de.timklge.karooheadwind.screens.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
@ -25,7 +24,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.awaitCancellation
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import androidx.glance.ColorFilter
|
|||||||
import androidx.glance.GlanceModifier
|
import androidx.glance.GlanceModifier
|
||||||
import androidx.glance.Image
|
import androidx.glance.Image
|
||||||
import androidx.glance.ImageProvider
|
import androidx.glance.ImageProvider
|
||||||
|
import androidx.glance.LocalContext
|
||||||
import androidx.glance.color.ColorProvider
|
import androidx.glance.color.ColorProvider
|
||||||
import androidx.glance.layout.Alignment
|
import androidx.glance.layout.Alignment
|
||||||
import androidx.glance.layout.Column
|
import androidx.glance.layout.Column
|
||||||
@ -57,7 +58,7 @@ fun Weather(current: WeatherInterpretation, windBearing: Int, windSpeed: Int, wi
|
|||||||
Row(horizontalAlignment = Alignment.CenterHorizontally, verticalAlignment = Alignment.CenterVertically) {
|
Row(horizontalAlignment = Alignment.CenterHorizontally, verticalAlignment = Alignment.CenterVertically) {
|
||||||
Image(
|
Image(
|
||||||
modifier = GlanceModifier.height(20.dp).width(12.dp),
|
modifier = GlanceModifier.height(20.dp).width(12.dp),
|
||||||
provider = ImageProvider(getArrowResourceByBearing(windBearing)),
|
provider = ImageProvider(getArrowBitmapByBearing(windBearing)),
|
||||||
contentDescription = "Current wind direction",
|
contentDescription = "Current wind direction",
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 980 B |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 749 B |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 943 B |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 758 B |