diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f000fec..a023463 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -102,4 +102,5 @@ dependencies { implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.cardview) implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.mapbox.sdk.turf) } \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt index c610dfc..6fa4cc1 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt @@ -26,6 +26,9 @@ class CustomProgressBar @JvmOverloads constructor( var progress: Double? = 0.5 var location: PowerbarLocation = PowerbarLocation.BOTTOM var label: String = "" + var minTarget: Double? = null + var maxTarget: Double? = null + var target: Double? = null var showLabel: Boolean = true @ColorInt var progressColor: Int = 0xFF2b86e6.toInt() @@ -35,6 +38,15 @@ class CustomProgressBar @JvmOverloads constructor( textPaint.textSize = value.fontSize } + private val targetColor = 0xFF9933FF.toInt() + + private val targetZonePaint = Paint().apply { + isAntiAlias = true + strokeWidth = 10f + style = Paint.Style.STROKE + color = targetColor + } + private val linePaint = Paint().apply { isAntiAlias = true strokeWidth = 1f @@ -81,28 +93,39 @@ class CustomProgressBar @JvmOverloads constructor( color = Color.WHITE strokeWidth = 3f textSize = size.fontSize - typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD); + typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD) textAlign = Paint.Align.CENTER } override fun onDrawForeground(canvas: Canvas) { super.onDrawForeground(canvas) - linePaint.color = progressColor - lineStrokePaint.color = progressColor - blurPaint.color = progressColor - blurPaintHighlight.color = ColorUtils.blendARGB(progressColor, 0xFFFFFF, 0.5f) + // Determine if the current progress is within the target range + val isTargetMet = progress != null && minTarget != null && maxTarget != null && progress!! >= minTarget!! && progress!! <= maxTarget!! + + val color = if (isTargetMet) { + targetColor // Use Zone 8 color if target is met + } else { + progressColor // Use default progress color otherwise + } + + linePaint.color = color + lineStrokePaint.color = color + blurPaint.color = color + blurPaintHighlight.color = ColorUtils.blendARGB(color, 0xFFFFFF, 0.5f) when(location){ PowerbarLocation.TOP -> { + val barTop = 15f + val barBottom = barTop + size.barHeight val rect = RectF( 1f, - 15f, + barTop, ((canvas.width.toDouble() - 1f) * (progress ?: 0.0).coerceIn(0.0, 1.0)).toFloat(), - 15f + size.barHeight + barBottom ) - canvas.drawRect(0f, 15f, canvas.width.toFloat(), 15f + size.barHeight, backgroundPaint) + canvas.drawRect(0f, barTop, canvas.width.toFloat(), barBottom, backgroundPaint) if (progress != null) { canvas.drawRoundRect(rect, 2f, 2f, blurPaint) @@ -110,6 +133,19 @@ class CustomProgressBar @JvmOverloads constructor( canvas.drawRoundRect(rect.right-4, rect.top, rect.right+4, rect.bottom, 2f, 2f, blurPaintHighlight) + // Draw target indicator lines first to be "behind" the label + if (minTarget != null && maxTarget != null) { + val minTargetX = (canvas.width * minTarget!!).toFloat() + val maxTargetX = (canvas.width * maxTarget!!).toFloat() + val edgeOffset = targetZonePaint.strokeWidth / 2f + + // Horizontal line at the top edge of the canvas + canvas.drawLine(minTargetX, edgeOffset, maxTargetX, edgeOffset, targetZonePaint) + // Vertical lines spanning most of the canvas height + canvas.drawLine(minTargetX, edgeOffset, minTargetX, edgeOffset + 40, targetZonePaint) + canvas.drawLine(maxTargetX, edgeOffset, maxTargetX, edgeOffset + 40, targetZonePaint) + } + if (showLabel){ val textBounds = textPaint.measureText(label) val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f @@ -122,23 +158,28 @@ class CustomProgressBar @JvmOverloads constructor( val r = x + xOffset * 2 val b = rect.bottom + yOffset + // Use targetZonePaint for outline if target is met + val currentOutlinePaint = if (isTargetMet) targetZonePaint else lineStrokePaint + canvas.drawRoundRect(x, y, r, b, 2f, 2f, textBackgroundPaint) canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint) - canvas.drawRoundRect(x, y, r, b, 2f, 2f, lineStrokePaint) + canvas.drawRoundRect(x, y, r, b, 2f, 2f, currentOutlinePaint) canvas.drawText(label, x + xOffset, rect.top + size.barHeight + 6, textPaint) } } } PowerbarLocation.BOTTOM -> { + val barTop = canvas.height.toFloat() - 1f - size.barHeight + val barBottom = canvas.height.toFloat() val rect = RectF( 1f, - canvas.height.toFloat() - 1f - size.barHeight, + barTop, ((canvas.width.toDouble() - 1f) * (progress ?: 0.0).coerceIn(0.0, 1.0)).toFloat(), - canvas.height.toFloat() + barBottom ) - canvas.drawRect(0f, canvas.height.toFloat() - size.barHeight, canvas.width.toFloat(), canvas.height.toFloat(), backgroundPaint) + canvas.drawRect(0f, barTop, canvas.width.toFloat(), barBottom, backgroundPaint) if (progress != null) { canvas.drawRoundRect(rect, 2f, 2f, blurPaint) @@ -146,6 +187,19 @@ class CustomProgressBar @JvmOverloads constructor( canvas.drawRoundRect(rect.right-4, rect.top, rect.right+4, rect.bottom, 2f, 2f, blurPaintHighlight) + // Draw target indicator lines first to be "behind" the label + if (minTarget != null && maxTarget != null) { + val minTargetX = (canvas.width * minTarget!!).toFloat() + val maxTargetX = (canvas.width * maxTarget!!).toFloat() + val edgeOffset = targetZonePaint.strokeWidth / 2f + + // Horizontal line at the bottom edge of the canvas + canvas.drawLine(minTargetX, canvas.height - edgeOffset, maxTargetX, canvas.height - edgeOffset, targetZonePaint) + // Vertical lines spanning most of the canvas height + canvas.drawLine(minTargetX, canvas.height - edgeOffset - 40, minTargetX, canvas.height - edgeOffset, targetZonePaint) + canvas.drawLine(maxTargetX, canvas.height - edgeOffset - 40, maxTargetX, canvas.height - edgeOffset, targetZonePaint) + } + if (showLabel){ val textBounds = textPaint.measureText(label) val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f @@ -159,9 +213,12 @@ class CustomProgressBar @JvmOverloads constructor( val r = x + xOffset * 2 val b = rect.bottom + 5 + // Use targetZonePaint for outline if target is met + val currentOutlinePaint = if (isTargetMet) targetZonePaint else lineStrokePaint + canvas.drawRoundRect(x, y, r, b, 2f, 2f, textBackgroundPaint) canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint) - canvas.drawRoundRect(x, y, r, b, 2f, 2f, lineStrokePaint) + canvas.drawRoundRect(x, y, r, b, 2f, 2f, currentOutlinePaint) canvas.drawText(label, x + xOffset, rect.top + size.barHeight - 1, textPaint) } @@ -169,4 +226,4 @@ class CustomProgressBar @JvmOverloads constructor( } } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt index b5eb8c0..2bfbe1d 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt @@ -1,14 +1,18 @@ package de.timklge.karoopowerbar import io.hammerhead.karooext.KarooSystemService +import io.hammerhead.karooext.models.OnNavigationState import io.hammerhead.karooext.models.OnStreamState import io.hammerhead.karooext.models.RideState import io.hammerhead.karooext.models.StreamState import io.hammerhead.karooext.models.UserProfile import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.transform import kotlinx.serialization.json.Json val jsonWithUnknownKeys = Json { ignoreUnknownKeys = true } @@ -35,6 +39,17 @@ fun KarooSystemService.streamRideState(): Flow { } } +fun KarooSystemService.streamNavigationState(): Flow { + return callbackFlow { + val listenerId = addConsumer { navigationState: OnNavigationState -> + trySendBlocking(navigationState) + } + awaitClose { + removeConsumer(listenerId) + } + } +} + fun KarooSystemService.streamUserProfile(): Flow { return callbackFlow { val listenerId = addConsumer { userProfile: UserProfile -> @@ -44,4 +59,11 @@ fun KarooSystemService.streamUserProfile(): Flow { removeConsumer(listenerId) } } -} \ No newline at end of file +} + +fun Flow.throttle(timeout: Long): Flow = this + .conflate() + .transform { + emit(it) + delay(timeout) + } \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt index 110c12b..9bd5ee0 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt @@ -17,10 +17,15 @@ import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowManager import androidx.annotation.ColorRes +import com.mapbox.geojson.LineString +import com.mapbox.turf.TurfConstants.UNIT_METERS +import com.mapbox.turf.TurfMeasurement import de.timklge.karoopowerbar.KarooPowerbarExtension.Companion.TAG import de.timklge.karoopowerbar.screens.SelectedSource import io.hammerhead.karooext.KarooSystemService +import io.hammerhead.karooext.models.DataPoint import io.hammerhead.karooext.models.DataType +import io.hammerhead.karooext.models.OnNavigationState import io.hammerhead.karooext.models.StreamState import io.hammerhead.karooext.models.UserProfile import kotlinx.coroutines.CoroutineScope @@ -29,12 +34,15 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.math.roundToInt -fun remap(value: Double, fromMin: Double, fromMax: Double, toMin: Double, toMax: Double): Double { +fun remap(value: Double?, fromMin: Double, fromMax: Double, toMin: Double, toMax: Double): Double? { + if (value == null) return null + return (value - fromMin) * (toMax - toMin) / (fromMax - fromMin) + toMin } @@ -48,6 +56,12 @@ class Window( val showLabel: Boolean, val powerbarSize: CustomProgressBarSize ) { + companion object { + val FIELD_TARGET_VALUE_ID = "FIELD_WORKOUT_TARGET_VALUE_ID"; + val FIELD_TARGET_MIN_ID = "FIELD_WORKOUT_TARGET_MIN_VALUE_ID"; + val FIELD_TARGET_MAX_ID = "FIELD_WORKOUT_TARGET_MAX_VALUE_ID"; + } + private val rootView: View private var layoutParams: WindowManager.LayoutParams? = null private val windowManager: WindowManager @@ -100,8 +114,6 @@ class Window( private val karooSystem: KarooSystemService = KarooSystemService(context) - data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null) - private var serviceJob: Job? = null @SuppressLint("UnspecifiedRegisterReceiverFlag") @@ -136,6 +148,7 @@ class Window( SelectedSource.SPEED_3S -> streamSpeed(true) SelectedSource.CADENCE -> streamCadence(false) SelectedSource.CADENCE_3S -> streamCadence(true) + SelectedSource.ROUTE_PROGRESS -> streamRouteProgress() else -> {} } } @@ -151,6 +164,54 @@ class Window( } } + private suspend fun streamRouteProgress() { + data class StreamData( + val userProfile: UserProfile, + val distanceToDestination: Double?, + val navigationState: OnNavigationState + ) + + var lastKnownRoutePolyline: String? = null + var lastKnownRouteLength: Double? = null + + combine(karooSystem.streamUserProfile(), karooSystem.streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION), karooSystem.streamNavigationState()) { userProfile, distanceToDestination, navigationState -> + StreamData(userProfile, (distanceToDestination as? StreamState.Streaming)?.dataPoint?.singleValue, navigationState) + }.distinctUntilChanged().throttle(5_000).collect { (userProfile, distanceToDestination, navigationState) -> + val state = navigationState.state + val routePolyline = when (state) { + is OnNavigationState.NavigationState.NavigatingRoute -> state.routePolyline + is OnNavigationState.NavigationState.NavigatingToDestination -> state.polyline + else -> null + } + + if (routePolyline != lastKnownRoutePolyline) { + lastKnownRoutePolyline = routePolyline + lastKnownRouteLength = when (state){ + is OnNavigationState.NavigationState.NavigatingRoute -> state.routeDistance + is OnNavigationState.NavigationState.NavigatingToDestination -> try { + TurfMeasurement.length(LineString.fromPolyline(state.polyline, 5), UNIT_METERS) + } catch (e: Exception) { + Log.e(TAG, "Failed to calculate route length", e) + null + } + else -> null + } + } + + val routeLength = lastKnownRouteLength + val routeProgressMeters = routeLength?.let { routeLength - (distanceToDestination ?: 0.0) }?.coerceAtLeast(0.0) + val routeProgress = if (routeLength != null && routeProgressMeters != null) remap(routeProgressMeters, 0.0, routeLength, 0.0, 1.0) else null + val routeProgressInUserUnit = when (userProfile.preferredUnit.distance) { + UserProfile.PreferredUnit.UnitType.IMPERIAL -> routeProgressMeters?.times(0.000621371)?.roundToInt() // Miles + else -> routeProgressMeters?.times(0.001)?.roundToInt() // Kilometers + } + + powerbar.progressColor = context.getColor(R.color.zone0) + powerbar.progress = routeProgress + powerbar.label = "$routeProgressInUserUnit" + } + } + private suspend fun streamSpeed(smoothed: Boolean) { val speedFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_SPEED else DataType.Type.SPEED) .map { (it as? StreamState.Streaming)?.dataPoint?.singleValue } @@ -158,12 +219,11 @@ class Window( val settingsFlow = context.streamSettings() - karooSystem.streamUserProfile() - .distinctUntilChanged() - .combine(speedFlow) { userProfile, speed -> StreamData(userProfile, speed) } - .combine(settingsFlow) { streamData, settings -> streamData.copy(settings = settings) } - .distinctUntilChanged() - .collect { streamData -> + data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null) + + combine(karooSystem.streamUserProfile(), speedFlow, settingsFlow) { userProfile, speed, settings -> + StreamData(userProfile, speed, settings) + }.distinctUntilChanged().throttle(1_000).collect { streamData -> val valueMetersPerSecond = streamData.value val value = when (streamData.userProfile.preferredUnit.distance){ UserProfile.PreferredUnit.UnitType.IMPERIAL -> valueMetersPerSecond?.times(2.23694) @@ -173,8 +233,7 @@ class Window( if (value != null && valueMetersPerSecond != null) { val minSpeed = streamData.settings?.minSpeed ?: PowerbarSettings.defaultMinSpeedMs val maxSpeed = streamData.settings?.maxSpeed ?: PowerbarSettings.defaultMaxSpeedMs - val progress = - remap(valueMetersPerSecond, minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0) + val progress = remap(valueMetersPerSecond, minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0) ?: 0.0 @ColorRes val zoneColorRes = Zone.entries[(progress * Zone.entries.size).roundToInt().coerceIn(0.. StreamData(userProfile, speed) } - .combine(settingsFlow) { streamData, settings -> streamData.copy(settings = settings) } - .distinctUntilChanged() - .collect { streamData -> - val value = streamData.value?.roundToInt() - if (value != null) { - val minCadence = streamData.settings?.minCadence ?: PowerbarSettings.defaultMinCadence - val maxCadence = streamData.settings?.maxCadence ?: PowerbarSettings.defaultMaxCadence - val progress = - remap(value.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) + combine(karooSystem.streamUserProfile(), cadenceFlow, settingsFlow, cadenceTargetFlow) { userProfile, speed, settings, cadenceTarget -> + StreamData(userProfile, speed, settings, cadenceTarget) + }.distinctUntilChanged().throttle(1_000).collect { streamData -> + val value = streamData.value?.roundToInt() - @ColorRes val zoneColorRes = Zone.entries[(progress * Zone.entries.size).roundToInt().coerceIn(0.. 0) progress else null - powerbar.label = "$value" + powerbar.minTarget = remap(streamData.cadenceTarget?.values[FIELD_TARGET_MIN_ID]?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) + powerbar.maxTarget = remap(streamData.cadenceTarget?.values[FIELD_TARGET_MAX_ID]?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) + powerbar.target = remap(streamData.cadenceTarget?.values[FIELD_TARGET_VALUE_ID]?.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0) - Log.d(TAG, "Cadence: $value min: $minCadence max: $maxCadence") + @ColorRes val zoneColorRes = Zone.entries[(progress * Zone.entries.size).roundToInt().coerceIn(0.. 0) progress else null + powerbar.label = "$value" + + Log.d(TAG, "Cadence: $value min: $minCadence max: $maxCadence") + } else { + powerbar.progressColor = context.getColor(R.color.zone0) + powerbar.progress = null + powerbar.label = "?" + + Log.d(TAG, "Cadence: Unavailable") } + powerbar.invalidate() + } } private suspend fun streamHeartrate() { @@ -247,40 +311,46 @@ class Window( .distinctUntilChanged() val settingsFlow = context.streamSettings() - - karooSystem.streamUserProfile() + val hrTargetFlow = karooSystem.streamDataFlow("TYPE_WORKOUT_HEART_RATE_TARGET_ID") + .map { (it as? StreamState.Streaming)?.dataPoint } .distinctUntilChanged() - .combine(hrFlow) { userProfile, hr -> StreamData(userProfile, hr) } - .combine(settingsFlow) { streamData, settings -> streamData.copy(settings = settings) } - .distinctUntilChanged() - .collect { streamData -> - val value = streamData.value?.roundToInt() - if (value != null) { - val customMinHr = if (streamData.settings?.useCustomHrRange == true) streamData.settings.minHr else null - val customMaxHr = if (streamData.settings?.useCustomHrRange == true) streamData.settings.maxHr else null - val minHr = customMinHr ?: streamData.userProfile.restingHr - val maxHr = customMaxHr ?: streamData.userProfile.maxHr - val progress = remap(value.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0) + data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null, val heartrateTarget: DataPoint? = null) - powerbar.progressColor = if (streamData.settings?.useZoneColors == true) { - context.getColor(getZone(streamData.userProfile.heartRateZones, value)?.colorResource ?: R.color.zone7) - } else { - context.getColor(R.color.zone0) - } - powerbar.progress = if (value > 0) progress else null - powerbar.label = "$value" + combine(karooSystem.streamUserProfile(), hrFlow, settingsFlow, hrTargetFlow) { userProfile, hr, settings, hrTarget -> + StreamData(userProfile, hr, settings, hrTarget) + }.distinctUntilChanged().throttle(1_000).collect { streamData -> + val value = streamData.value?.roundToInt() - Log.d(TAG, "Hr: $value min: $minHr max: $maxHr") + if (value != null) { + val customMinHr = if (streamData.settings?.useCustomHrRange == true) streamData.settings.minHr else null + val customMaxHr = if (streamData.settings?.useCustomHrRange == true) streamData.settings.maxHr else null + val minHr = customMinHr ?: streamData.userProfile.restingHr + val maxHr = customMaxHr ?: streamData.userProfile.maxHr + val progress = remap(value.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0) + + powerbar.minTarget = remap(streamData.heartrateTarget?.values[FIELD_TARGET_MIN_ID]?.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0) + powerbar.maxTarget = remap(streamData.heartrateTarget?.values[FIELD_TARGET_MAX_ID]?.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0) + powerbar.target = remap(streamData.heartrateTarget?.values[FIELD_TARGET_VALUE_ID]?.toDouble(), minHr.toDouble(), maxHr.toDouble(), 0.0, 1.0) + + powerbar.progressColor = if (streamData.settings?.useZoneColors == true) { + context.getColor(getZone(streamData.userProfile.heartRateZones, value)?.colorResource ?: R.color.zone7) } else { - powerbar.progressColor = context.getColor(R.color.zone0) - powerbar.progress = null - powerbar.label = "?" - - Log.d(TAG, "Hr: Unavailable") + context.getColor(R.color.zone0) } - powerbar.invalidate() + powerbar.progress = if (value > 0) progress else null + powerbar.label = "$value" + + Log.d(TAG, "Hr: $value min: $minHr max: $maxHr") + } else { + powerbar.progressColor = context.getColor(R.color.zone0) + powerbar.progress = null + powerbar.label = "?" + + Log.d(TAG, "Hr: Unavailable") } + powerbar.invalidate() + } } enum class PowerStreamSmoothing(val dataTypeId: String){ @@ -296,39 +366,46 @@ class Window( val settingsFlow = context.streamSettings() - karooSystem.streamUserProfile() + val powerTargetFlow = karooSystem.streamDataFlow("TYPE_WORKOUT_POWER_TARGET_ID") // TYPE_WORKOUT_HEART_RATE_TARGET_ID, TYPE_WORKOUT_CADENCE_TARGET_ID, + .map { (it as? StreamState.Streaming)?.dataPoint } .distinctUntilChanged() - .combine(powerFlow) { userProfile, hr -> StreamData(userProfile, hr) } - .combine(settingsFlow) { streamData, settings -> streamData.copy(settings = settings) } - .distinctUntilChanged() - .collect { streamData -> - val value = streamData.value?.roundToInt() - if (value != null) { - val customMinPower = if (streamData.settings?.useCustomPowerRange == true) streamData.settings.minPower else null - val customMaxPower = if (streamData.settings?.useCustomPowerRange == true) streamData.settings.maxPower else null - val minPower = customMinPower ?: streamData.userProfile.powerZones.first().min - val maxPower = customMaxPower ?: (streamData.userProfile.powerZones.last().min + 50) - val progress = remap(value.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0) + data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null, val powerTarget: DataPoint? = null) - powerbar.progressColor = if (streamData.settings?.useZoneColors == true) { - context.getColor(getZone(streamData.userProfile.powerZones, value)?.colorResource ?: R.color.zone7) - } else { - context.getColor(R.color.zone0) - } - powerbar.progress = if (value > 0) progress else null - powerbar.label = "${value}W" + combine(karooSystem.streamUserProfile(), powerFlow, settingsFlow, powerTargetFlow) { userProfile, hr, settings, powerTarget -> + StreamData(userProfile, hr, settings, powerTarget) + }.distinctUntilChanged().throttle(1_000).collect { streamData -> + val value = streamData.value?.roundToInt() - Log.d(TAG, "Power: $value min: $minPower max: $maxPower") + if (value != null) { + val customMinPower = if (streamData.settings?.useCustomPowerRange == true) streamData.settings.minPower else null + val customMaxPower = if (streamData.settings?.useCustomPowerRange == true) streamData.settings.maxPower else null + val minPower = customMinPower ?: streamData.userProfile.powerZones.first().min + val maxPower = customMaxPower ?: (streamData.userProfile.powerZones.last().min + 30) + val progress = remap(value.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0) + + powerbar.minTarget = remap(streamData.powerTarget?.values[FIELD_TARGET_MIN_ID]?.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0) + powerbar.maxTarget = remap(streamData.powerTarget?.values[FIELD_TARGET_MAX_ID]?.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0) + powerbar.target = remap(streamData.powerTarget?.values[FIELD_TARGET_VALUE_ID]?.toDouble(), minPower.toDouble(), maxPower.toDouble(), 0.0, 1.0) + + powerbar.progressColor = if (streamData.settings?.useZoneColors == true) { + context.getColor(getZone(streamData.userProfile.powerZones, value)?.colorResource ?: R.color.zone7) } else { - powerbar.progressColor = context.getColor(R.color.zone0) - powerbar.progress = null - powerbar.label = "?" - - Log.d(TAG, "Power: Unavailable") + context.getColor(R.color.zone0) } - powerbar.invalidate() + powerbar.progress = if (value > 0) progress else null + powerbar.label = "${value}W" + + Log.d(TAG, "Power: $value min: $minPower max: $maxPower") + } else { + powerbar.progressColor = context.getColor(R.color.zone0) + powerbar.progress = null + powerbar.label = "?" + + Log.d(TAG, "Power: Unavailable") } + powerbar.invalidate() + } } private var currentHideJob: Job? = null diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt index 84f4e6a..faeb792 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt @@ -87,7 +87,8 @@ enum class SelectedSource(val id: String, val label: String) { SPEED("speed", "Speed"), SPEED_3S("speed_3s", "Speed (3 sec avg"), CADENCE("cadence", "Cadence"), - CADENCE_3S("cadence_3s", "Cadence (3 sec avg)"); + CADENCE_3S("cadence_3s", "Cadence (3 sec avg)"), + ROUTE_PROGRESS("route_progress", "Route Progress"); fun isPower() = this == POWER || this == POWER_3S || this == POWER_10S } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 169a7a7..0596253 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ lifecycleRuntimeKtx = "2.8.7" navigationRuntimeKtx = "2.8.4" navigationCompose = "2.8.4" cardview = "1.0.0" +mapboxSdkTurf = "7.3.1" [plugins] android-application = { id = "com.android.application", version.ref = "agp" } @@ -24,7 +25,7 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = " androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } color = { module = "com.maxkeppeler.sheets-compose-dialogs:color", version.ref = "color" } -hammerhead-karoo-ext = { group = "io.hammerhead", name = "karoo-ext", version = "1.1.2" } +hammerhead-karoo-ext = { group = "io.hammerhead", name = "karoo-ext", version = "1.1.5" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" } @@ -43,6 +44,7 @@ androidx-navigation-runtime-ktx = { group = "androidx.navigation", name = "navig androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } androidx-cardview = { group = "androidx.cardview", name = "cardview", version.ref = "cardview" } +mapbox-sdk-turf = { module = "com.mapbox.mapboxsdk:mapbox-sdk-turf", version.ref = "mapboxSdkTurf" } [bundles] androidx-lifeycle = ["androidx-lifecycle-runtime-compose", "androidx-lifecycle-viewmodel-compose"] diff --git a/settings.gradle.kts b/settings.gradle.kts index d07d436..a5ec10a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,6 +34,11 @@ dependencyResolutionManagement { password = gprKey } } + + // mapbox + maven { + url = uri("https://api.mapbox.com/downloads/v2/releases/maven") + } } }