ref #35: Set headwind data type field to direction according to user setting, add userwind data type (#36)
* ref #35: Set headwind data type fields to values according to user settings * Use single datafield per type, add userwindSpeed data type to expose (head-)wind speed depending on user setting * Enable minification for release builds * Fix signed angle difference in headwind view * Remove bitmap cache * Format
This commit is contained in:
parent
0869121176
commit
01959ce3b7
11
README.md
11
README.md
@ -45,3 +45,14 @@ The app will automatically attempt to download weather data for your current app
|
|||||||
- Icons are from [boxicons.com](https://boxicons.com) ([MIT-licensed](icon_credits.txt))
|
- Icons are from [boxicons.com](https://boxicons.com) ([MIT-licensed](icon_credits.txt))
|
||||||
- Made possible by the generous usage terms of [open-meteo.com](https://open-meteo.com)
|
- Made possible by the generous usage terms of [open-meteo.com](https://open-meteo.com)
|
||||||
- Uses [karoo-ext](https://github.com/hammerheadnav/karoo-ext) (Apache2-licensed)
|
- Uses [karoo-ext](https://github.com/hammerheadnav/karoo-ext) (Apache2-licensed)
|
||||||
|
|
||||||
|
## Extension Developers: Headwind Data Type
|
||||||
|
|
||||||
|
If the user has installed the headwind extension on his karoo, you can stream the headwind data type from other extensions via `karoo-ext`.
|
||||||
|
Use extension id `karoo-headwind` with datatype ids `headwind` and `userwindSpeed`.
|
||||||
|
|
||||||
|
- The `headwind` datatype contains a single field that either represents an error code or the wind direction. A `-1.0` indicates missing gps receiption, `-2.0` no weather data, `-3.0` that the headwind extension
|
||||||
|
has not been set up. Otherwise, the value is the wind direction in degrees; if the user has set the headwind indicator to depict the absolute wind direction, the field will contain the absolute wind direction; otherwise
|
||||||
|
it will contain the headwind direction.
|
||||||
|
- The `userwindSpeed` datatype contains a single field with the wind speed in the user's defined unit. If the user has set the headwind indicator to show the absolute wind speed,
|
||||||
|
this field will contain the absolute wind speed; otherwise it will contain the headwind speed.
|
||||||
@ -15,8 +15,8 @@ android {
|
|||||||
applicationId = "de.timklge.karooheadwind"
|
applicationId = "de.timklge.karooheadwind"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 10
|
versionCode = 11
|
||||||
versionName = "1.2.2"
|
versionName = "1.2.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@ -38,7 +38,7 @@ android {
|
|||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,9 @@
|
|||||||
"packageName": "de.timklge.karooheadwind",
|
"packageName": "de.timklge.karooheadwind",
|
||||||
"iconUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/karoo-headwind.png",
|
"iconUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/karoo-headwind.png",
|
||||||
"latestApkUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/app-release.apk",
|
"latestApkUrl": "https://github.com/timklge/karoo-headwind/releases/latest/download/app-release.apk",
|
||||||
"latestVersion": "1.2.2",
|
"latestVersion": "1.2.3",
|
||||||
"latestVersionCode": 10,
|
"latestVersionCode": 11,
|
||||||
"developer": "timklge",
|
"developer": "timklge",
|
||||||
"description": "Provides headwind direction, wind speed and other weather data fields",
|
"description": "Provides headwind direction, wind speed and other weather data fields",
|
||||||
"releaseNotes": "Reduce font size in tailwind indicators, update gps bearing retrieval"
|
"releaseNotes": "Update gps bearing retrieval, data type exposure for other extensions, optimize build"
|
||||||
}
|
}
|
||||||
@ -4,8 +4,6 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.models.DataType
|
|
||||||
import io.hammerhead.karooext.models.StreamState
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@ -15,7 +13,6 @@ import kotlinx.coroutines.flow.filter
|
|||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.scan
|
|
||||||
|
|
||||||
|
|
||||||
sealed class HeadingResponse {
|
sealed class HeadingResponse {
|
||||||
@ -35,7 +32,7 @@ fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<HeadingRes
|
|||||||
val windBearing = data.current.windDirection + 180
|
val windBearing = data.current.windDirection + 180
|
||||||
val diff = signedAngleDifference(bearing.diff, windBearing)
|
val diff = signedAngleDifference(bearing.diff, windBearing)
|
||||||
|
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Wind bearing: $bearing vs $windBearing => $diff")
|
Log.d(KarooHeadwindExtension.TAG, "Wind bearing: Heading $bearing vs wind $windBearing => $diff")
|
||||||
|
|
||||||
HeadingResponse.Value(diff)
|
HeadingResponse.Value(diff)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType
|
|||||||
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
|
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
|
||||||
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
|
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
|
||||||
import de.timklge.karooheadwind.datatypes.TemperatureDataType
|
import de.timklge.karooheadwind.datatypes.TemperatureDataType
|
||||||
|
import de.timklge.karooheadwind.datatypes.UserWindSpeedDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherDataType
|
import de.timklge.karooheadwind.datatypes.WeatherDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindSpeedDataType
|
import de.timklge.karooheadwind.datatypes.WindSpeedDataType
|
||||||
@ -41,12 +42,11 @@ import kotlin.math.absoluteValue
|
|||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.2.2") {
|
class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.2.3") {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "karoo-headwind"
|
const val TAG = "karoo-headwind"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
lateinit var karooSystem: KarooSystemService
|
lateinit var karooSystem: KarooSystemService
|
||||||
|
|
||||||
private var updateLastKnownGpsJob: Job? = null
|
private var updateLastKnownGpsJob: Job? = null
|
||||||
@ -67,7 +67,8 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.2.2") {
|
|||||||
TemperatureDataType(applicationContext),
|
TemperatureDataType(applicationContext),
|
||||||
WindDirectionDataType(karooSystem, applicationContext),
|
WindDirectionDataType(karooSystem, applicationContext),
|
||||||
PrecipitationDataType(applicationContext),
|
PrecipitationDataType(applicationContext),
|
||||||
SurfacePressureDataType(applicationContext)
|
SurfacePressureDataType(applicationContext),
|
||||||
|
UserWindSpeedDataType(karooSystem, applicationContext)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import de.timklge.karooheadwind.KarooHeadwindExtension
|
|||||||
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.screens.WindDirectionIndicatorSetting
|
import de.timklge.karooheadwind.screens.WindDirectionIndicatorSetting
|
||||||
import de.timklge.karooheadwind.screens.WindDirectionIndicatorTextSetting
|
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
|
import de.timklge.karooheadwind.streamDataFlow
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
@ -30,8 +30,9 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.cos
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
@ -44,9 +45,41 @@ class HeadwindDirectionDataType(
|
|||||||
override fun startStream(emitter: Emitter<StreamState>) {
|
override fun startStream(emitter: Emitter<StreamState>) {
|
||||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
val job = CoroutineScope(Dispatchers.IO).launch {
|
||||||
karooSystem.getRelativeHeadingFlow(applicationContext)
|
karooSystem.getRelativeHeadingFlow(applicationContext)
|
||||||
.collect { diff ->
|
.combine(applicationContext.streamCurrentWeatherData()) { headingResponse, data -> StreamData(headingResponse, data?.current?.windDirection, data?.current?.windSpeed) }
|
||||||
val value = (diff as? HeadingResponse.Value)?.diff ?: 0.0
|
.combine(applicationContext.streamSettings(karooSystem)) { data, settings -> data.copy(settings = settings) }
|
||||||
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to value))))
|
.collect { streamData ->
|
||||||
|
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
||||||
|
|
||||||
|
var returnValue = 0.0
|
||||||
|
if (value == null || streamData.absoluteWindDirection == null || streamData.settings == null || streamData.windSpeed == null){
|
||||||
|
var errorCode = 1.0
|
||||||
|
var headingResponse = streamData.headingResponse
|
||||||
|
|
||||||
|
if (headingResponse is HeadingResponse.Value && (streamData.absoluteWindDirection == null || streamData.windSpeed == null)){
|
||||||
|
headingResponse = HeadingResponse.NoWeatherData
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamData.settings?.welcomeDialogAccepted == false){
|
||||||
|
errorCode = ERROR_APP_NOT_SET_UP.toDouble()
|
||||||
|
} else if (headingResponse is HeadingResponse.NoGps){
|
||||||
|
errorCode = ERROR_NO_GPS.toDouble()
|
||||||
|
} else {
|
||||||
|
errorCode = ERROR_NO_WEATHER_DATA.toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
returnValue = errorCode
|
||||||
|
} else {
|
||||||
|
var windDirection = when (streamData.settings.windDirectionIndicatorSetting){
|
||||||
|
WindDirectionIndicatorSetting.HEADWIND_DIRECTION -> value
|
||||||
|
WindDirectionIndicatorSetting.WIND_DIRECTION -> streamData.absoluteWindDirection + 180
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windDirection < 0) windDirection += 360
|
||||||
|
|
||||||
|
returnValue = windDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to returnValue))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emitter.setCancellable {
|
emitter.setCancellable {
|
||||||
@ -56,13 +89,16 @@ class HeadwindDirectionDataType(
|
|||||||
|
|
||||||
data class StreamData(val headingResponse: HeadingResponse?, val absoluteWindDirection: Double?, val windSpeed: Double?, val settings: HeadwindSettings? = null)
|
data class StreamData(val headingResponse: HeadingResponse?, val absoluteWindDirection: Double?, val windSpeed: Double?, val settings: HeadwindSettings? = null)
|
||||||
|
|
||||||
private fun previewFlow(): Flow<StreamData> {
|
data class DirectionAndSpeed(val bearing: Double, val speed: Double?)
|
||||||
|
|
||||||
|
private fun previewFlow(): Flow<DirectionAndSpeed> {
|
||||||
return flow {
|
return flow {
|
||||||
while (true) {
|
while (true) {
|
||||||
val bearing = (0..360).random().toDouble()
|
val bearing = (0..360).random().toDouble()
|
||||||
val windSpeed = (0..20).random()
|
val windSpeed = (0..20).random()
|
||||||
|
|
||||||
emit(StreamData(HeadingResponse.Value(bearing), bearing, windSpeed.toDouble(), HeadwindSettings()))
|
emit(DirectionAndSpeed(bearing, windSpeed.toDouble()))
|
||||||
|
|
||||||
delay(2_000)
|
delay(2_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,44 +121,34 @@ class HeadwindDirectionDataType(
|
|||||||
val flow = if (config.preview) {
|
val flow = if (config.preview) {
|
||||||
previewFlow()
|
previewFlow()
|
||||||
} else {
|
} else {
|
||||||
karooSystem.getRelativeHeadingFlow(context)
|
val directionFlow = karooSystem.streamDataFlow(dataTypeId).mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
|
||||||
.combine(context.streamCurrentWeatherData()) { headingResponse, data -> StreamData(headingResponse, data?.current?.windDirection, data?.current?.windSpeed) }
|
val speedFlow = karooSystem.streamDataFlow(DataType.dataTypeId("karoo-headwind", "userwindSpeed")).map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
|
||||||
.combine(context.streamSettings(karooSystem)) { data, settings -> data.copy(settings = settings) }
|
|
||||||
|
combine(directionFlow, speedFlow) { direction, speed ->
|
||||||
|
DirectionAndSpeed(direction, speed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
flow.collect { streamData ->
|
flow.collect { streamData ->
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view")
|
Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view")
|
||||||
|
|
||||||
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
val errorCode = streamData.bearing.let { if(it < 0) it.toInt() else null }
|
||||||
if (value == null || streamData.absoluteWindDirection == null || streamData.settings == null || streamData.windSpeed == null){
|
if (errorCode != null) {
|
||||||
var headingResponse = streamData.headingResponse
|
emitter.updateView(getErrorWidget(glance, context, errorCode).remoteViews)
|
||||||
|
|
||||||
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
|
return@collect
|
||||||
}
|
}
|
||||||
|
|
||||||
val windSpeed = streamData.windSpeed
|
val windDirection = streamData.bearing
|
||||||
val windDirection = when (streamData.settings.windDirectionIndicatorSetting){
|
val windSpeed = streamData.speed
|
||||||
WindDirectionIndicatorSetting.HEADWIND_DIRECTION -> value
|
|
||||||
WindDirectionIndicatorSetting.WIND_DIRECTION -> streamData.absoluteWindDirection + 180
|
|
||||||
}
|
|
||||||
val text = when (streamData.settings.windDirectionIndicatorTextSetting) {
|
|
||||||
WindDirectionIndicatorTextSetting.HEADWIND_SPEED -> {
|
|
||||||
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
|
||||||
headwindSpeed.roundToInt().toString()
|
|
||||||
}
|
|
||||||
WindDirectionIndicatorTextSetting.WIND_SPEED -> windSpeed.roundToInt().toString()
|
|
||||||
WindDirectionIndicatorTextSetting.NONE -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = glance.compose(context, DpSize.Unspecified) {
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
HeadwindDirection(baseBitmap, windDirection.roundToInt(), config.textSize, text, viewSize = config.viewSize)
|
HeadwindDirection(
|
||||||
|
baseBitmap,
|
||||||
|
windDirection.roundToInt(),
|
||||||
|
config.textSize,
|
||||||
|
windSpeed?.toInt()?.toString() ?: ""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.updateView(result.remoteViews)
|
emitter.updateView(result.remoteViews)
|
||||||
@ -134,4 +160,10 @@ class HeadwindDirectionDataType(
|
|||||||
viewJob.cancel()
|
viewJob.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ERROR_NO_GPS = -1
|
||||||
|
const val ERROR_NO_WEATHER_DATA = -2
|
||||||
|
const val ERROR_APP_NOT_SET_UP = -3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -22,7 +22,6 @@ import androidx.glance.layout.Row
|
|||||||
import androidx.glance.layout.fillMaxSize
|
import androidx.glance.layout.fillMaxSize
|
||||||
import androidx.glance.layout.padding
|
import androidx.glance.layout.padding
|
||||||
import androidx.glance.layout.size
|
import androidx.glance.layout.size
|
||||||
import androidx.glance.preview.ExperimentalGlancePreviewApi
|
|
||||||
import androidx.glance.text.FontFamily
|
import androidx.glance.text.FontFamily
|
||||||
import androidx.glance.text.FontWeight
|
import androidx.glance.text.FontWeight
|
||||||
import androidx.glance.text.Text
|
import androidx.glance.text.Text
|
||||||
@ -30,47 +29,34 @@ import androidx.glance.text.TextStyle
|
|||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
data class BitmapWithBearing(val bitmap: Bitmap, val bearing: Int)
|
|
||||||
|
|
||||||
val bitmapsByBearing = mutableMapOf<BitmapWithBearing, Bitmap>()
|
|
||||||
|
|
||||||
fun getArrowBitmapByBearing(baseBitmap: Bitmap, bearing: Int): Bitmap {
|
fun getArrowBitmapByBearing(baseBitmap: Bitmap, bearing: Int): Bitmap {
|
||||||
synchronized(bitmapsByBearing) {
|
|
||||||
val bearingRounded = (((bearing + 360) / 10.0).roundToInt() * 10) % 360
|
val bearingRounded = (((bearing + 360) / 10.0).roundToInt() * 10) % 360
|
||||||
|
|
||||||
val bitmapWithBearing = BitmapWithBearing(baseBitmap, bearingRounded)
|
|
||||||
val storedBitmap = bitmapsByBearing[bitmapWithBearing]
|
|
||||||
if (storedBitmap != null) return storedBitmap
|
|
||||||
|
|
||||||
val bitmap = Bitmap.createBitmap(128, 128, Bitmap.Config.ARGB_8888)
|
val bitmap = Bitmap.createBitmap(128, 128, Bitmap.Config.ARGB_8888)
|
||||||
val canvas = Canvas(bitmap)
|
val canvas = Canvas(bitmap)
|
||||||
|
|
||||||
val paint = Paint().apply {
|
val paint = Paint().apply {
|
||||||
color = android.graphics.Color.BLACK
|
color = android.graphics.Color.BLACK
|
||||||
style = Paint.Style.STROKE
|
style = Paint.Style.STROKE
|
||||||
// strokeWidth = 15f
|
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.save()
|
canvas.save()
|
||||||
canvas.scale((bitmap.width / baseBitmap.width.toFloat()), (bitmap.height / baseBitmap.height.toFloat()), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat())
|
canvas.scale((bitmap.width / baseBitmap.width.toFloat()), (bitmap.height / baseBitmap.height.toFloat()), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat())
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Drawing arrow at $bearingRounded")
|
Log.d(KarooHeadwindExtension.TAG, "Drawing arrow at $bearingRounded")
|
||||||
canvas.rotate(bearing.toFloat(), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat())
|
canvas.rotate(bearingRounded.toFloat(), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat())
|
||||||
canvas.drawBitmap(baseBitmap, ((bitmap.width - baseBitmap.width) / 2).toFloat(), ((bitmap.height - baseBitmap.height) / 2).toFloat(), paint)
|
canvas.drawBitmap(baseBitmap, ((bitmap.width - baseBitmap.width) / 2).toFloat(), ((bitmap.height - baseBitmap.height) / 2).toFloat(), paint)
|
||||||
canvas.restore()
|
canvas.restore()
|
||||||
|
|
||||||
bitmapsByBearing[bitmapWithBearing] = bitmap
|
|
||||||
|
|
||||||
return bitmap
|
return bitmap
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalGlancePreviewApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HeadwindDirection(baseBitmap: Bitmap, bearing: Int, fontSize: Int,
|
fun HeadwindDirection(
|
||||||
|
baseBitmap: Bitmap, bearing: Int, fontSize: Int,
|
||||||
overlayText: String, overlaySubText: String? = null,
|
overlayText: String, overlaySubText: String? = null,
|
||||||
dayColor: Color = Color.Black, nightColor: Color = Color.White,
|
dayColor: Color = Color.Black, nightColor: Color = Color.White
|
||||||
viewSize: Pair<Int, Int>) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = GlanceModifier.fillMaxSize().padding(5.dp),
|
modifier = GlanceModifier.fillMaxSize().padding(5.dp),
|
||||||
contentAlignment = Alignment(
|
contentAlignment = Alignment(
|
||||||
|
|||||||
@ -46,3 +46,4 @@ class HeadwindSpeedDataType(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -199,8 +199,15 @@ class TailwindAndRideSpeedDataType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val result = glance.compose(context, DpSize.Unspecified) {
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
HeadwindDirection(baseBitmap, windDirection.roundToInt(), config.textSize, text, subtextWithSign,
|
HeadwindDirection(
|
||||||
dayColor, nightColor, viewSize = config.viewSize)
|
baseBitmap,
|
||||||
|
windDirection.roundToInt(),
|
||||||
|
config.textSize,
|
||||||
|
text,
|
||||||
|
subtextWithSign,
|
||||||
|
dayColor,
|
||||||
|
nightColor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
emitter.updateView(result.remoteViews)
|
emitter.updateView(result.remoteViews)
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse
|
||||||
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
|
import de.timklge.karooheadwind.screens.HeadwindSettings
|
||||||
|
import de.timklge.karooheadwind.screens.WindDirectionIndicatorTextSetting
|
||||||
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
|
import de.timklge.karooheadwind.streamSettings
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
|
import io.hammerhead.karooext.internal.Emitter
|
||||||
|
import io.hammerhead.karooext.models.DataPoint
|
||||||
|
import io.hammerhead.karooext.models.DataType
|
||||||
|
import io.hammerhead.karooext.models.StreamState
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.cos
|
||||||
|
|
||||||
|
class UserWindSpeedDataType(
|
||||||
|
private val karooSystem: KarooSystemService,
|
||||||
|
private val context: Context
|
||||||
|
) : DataTypeImpl("karoo-headwind", "userwindSpeed"){
|
||||||
|
|
||||||
|
data class StreamData(val headingResponse: HeadingResponse, val weatherResponse: OpenMeteoCurrentWeatherResponse?, val settings: HeadwindSettings)
|
||||||
|
|
||||||
|
override fun startStream(emitter: Emitter<StreamState>) {
|
||||||
|
val job = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
karooSystem.getRelativeHeadingFlow(context)
|
||||||
|
.combine(context.streamCurrentWeatherData()) { value, data -> value to data }
|
||||||
|
.combine(context.streamSettings(karooSystem)) { (value, data), settings -> StreamData(value, data, settings) }
|
||||||
|
.filter { it.weatherResponse != null }
|
||||||
|
.collect { streamData ->
|
||||||
|
val windSpeed = streamData.weatherResponse?.current?.windSpeed ?: 0.0
|
||||||
|
val windDirection = (streamData.headingResponse as? HeadingResponse.Value)?.diff ?: 0.0
|
||||||
|
|
||||||
|
if (streamData.settings.windDirectionIndicatorTextSetting == WindDirectionIndicatorTextSetting.HEADWIND_SPEED){
|
||||||
|
val headwindSpeed = cos((windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||||
|
|
||||||
|
emitter.onNext(
|
||||||
|
StreamState.Streaming(
|
||||||
|
DataPoint(
|
||||||
|
dataTypeId,
|
||||||
|
mapOf(DataType.Field.SINGLE to headwindSpeed)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
emitter.onNext(
|
||||||
|
StreamState.Streaming(
|
||||||
|
DataPoint(
|
||||||
|
dataTypeId,
|
||||||
|
mapOf(DataType.Field.SINGLE to windSpeed)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.setCancellable {
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -37,7 +37,38 @@ suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings
|
|||||||
|
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Error widget: $errorMessage")
|
Log.d(KarooHeadwindExtension.TAG, "Error widget: $errorMessage")
|
||||||
|
|
||||||
Text(text = errorMessage, style = TextStyle(fontSize = TextUnit(16f, TextUnitType.Sp),
|
Text(text = errorMessage,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = TextUnit(16f, TextUnitType.Sp),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = ColorProvider(Color.Black, Color.White)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
|
suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, errorCode: Int): RemoteViewsCompositionResult {
|
||||||
|
return glance.compose(context, DpSize.Unspecified) {
|
||||||
|
Box(modifier = GlanceModifier.fillMaxSize().padding(5.dp), contentAlignment = Alignment.Center) {
|
||||||
|
val errorMessage = when (errorCode) {
|
||||||
|
HeadwindDirectionDataType.ERROR_APP_NOT_SET_UP -> {
|
||||||
|
"Headwind app not set up"
|
||||||
|
}
|
||||||
|
HeadwindDirectionDataType.ERROR_NO_GPS -> {
|
||||||
|
"No GPS signal"
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
"Weather data download failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Error widget: $errorMessage")
|
||||||
|
|
||||||
|
Text(text = errorMessage,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = TextUnit(16f, TextUnitType.Sp),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = ColorProvider(Color.Black, Color.White)
|
color = ColorProvider(Color.Black, Color.White)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -27,4 +27,6 @@
|
|||||||
<string name="headwind_speed_description">Current headwind speed</string>
|
<string name="headwind_speed_description">Current headwind speed</string>
|
||||||
<string name="temperature">Temperature</string>
|
<string name="temperature">Temperature</string>
|
||||||
<string name="temperature_description">Current temperature in configured unit</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>
|
||||||
</resources>
|
</resources>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
description="@string/tailwind_and_speed_description"
|
description="@string/tailwind_and_speed_description"
|
||||||
displayName="@string/tailwind_and_speed"
|
displayName="@string/tailwind_and_speed"
|
||||||
graphical="true"
|
graphical="true"
|
||||||
icon="@drawable/ic_launcher"
|
icon="@drawable/wind"
|
||||||
typeId="tailwind-and-ride-speed" />
|
typeId="tailwind-and-ride-speed" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
@ -39,6 +39,13 @@
|
|||||||
icon="@drawable/wind"
|
icon="@drawable/wind"
|
||||||
typeId="headwindSpeed" />
|
typeId="headwindSpeed" />
|
||||||
|
|
||||||
|
<DataType
|
||||||
|
description="@string/userwind_speed_description"
|
||||||
|
displayName="@string/userwind_speed"
|
||||||
|
graphical="false"
|
||||||
|
icon="@drawable/wind"
|
||||||
|
typeId="userwindSpeed" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/relativeHumidity_description"
|
description="@string/relativeHumidity_description"
|
||||||
displayName="@string/relativeHumidity"
|
displayName="@string/relativeHumidity"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user