Compare commits
4 Commits
2378c944e6
...
1f27a33b64
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f27a33b64 | |||
|
|
109c002533 | ||
|
|
5169048143 | ||
|
|
be7ca192b2 |
54
.github/workflows/release_comment.yml
vendored
Normal file
54
.github/workflows/release_comment.yml
vendored
Normal file
@ -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
|
||||||
@ -23,8 +23,9 @@ After installing this app on your Karoo and opening it once from the main menu,
|
|||||||
|
|
||||||
- Headwind (graphical, 1x1 field): Shows the headwind direction and speed as a circle with a triangular direction indicator. The speed is shown at the center in your set unit of measurement (default is kilometers per hour if you have set up metric units in your Karoo, otherwise miles per hour). Both direction and speed are relative to the current riding direction by default, i. e., riding directly into a wind of 20 km/h will show a headwind speed of 20 km/h, while riding in the same direction will show -20 km/h.
|
- Headwind (graphical, 1x1 field): Shows the headwind direction and speed as a circle with a triangular direction indicator. The speed is shown at the center in your set unit of measurement (default is kilometers per hour if you have set up metric units in your Karoo, otherwise miles per hour). Both direction and speed are relative to the current riding direction by default, i. e., riding directly into a wind of 20 km/h will show a headwind speed of 20 km/h, while riding in the same direction will show -20 km/h.
|
||||||
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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 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.
|
- 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.
|
||||||
|
|||||||
@ -72,7 +72,7 @@ tasks.register("generateManifest") {
|
|||||||
"latestVersionCode" to android.defaultConfig.versionCode,
|
"latestVersionCode" to android.defaultConfig.versionCode,
|
||||||
"developer" to "github.com/timklge",
|
"developer" to "github.com/timklge",
|
||||||
"description" to "Open-source extension that provides headwind direction, wind speed, forecast and other weather data fields.",
|
"description" to "Open-source extension that provides headwind direction, wind speed, forecast and other weather data fields.",
|
||||||
"releaseNotes" to "* Add route forecast support for OpenWeatherMap (thx @lockevod!)\n* Crop timespan in forecast widgets to 6 hours, use am / pm time format, enlarge font\n* Remove tailwind, icon forecast, weather data fields",
|
"releaseNotes" to "* Add UV-index datafield (thx @saversux!)\n* Readd a datafield that shows headwind direction and absolute wind speed datafield\n* Split wind forecast field into wind and headwind forecast fields",
|
||||||
"screenshotUrls" to listOf(
|
"screenshotUrls" to listOf(
|
||||||
"$baseUrl/preview1.png",
|
"$baseUrl/preview1.png",
|
||||||
"$baseUrl/preview3.png",
|
"$baseUrl/preview3.png",
|
||||||
|
|||||||
@ -294,9 +294,9 @@ fun lerpWeather(
|
|||||||
weatherCode = closestWeatherData.weatherCode,
|
weatherCode = closestWeatherData.weatherCode,
|
||||||
isForecast = closestWeatherData.isForecast,
|
isForecast = closestWeatherData.isForecast,
|
||||||
isNight = closestWeatherData.isNight,
|
isNight = closestWeatherData.isNight,
|
||||||
|
uvi = start.uvi + (end.uvi - start.uvi) * factor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun lerpWeatherTime(
|
fun lerpWeatherTime(
|
||||||
weatherData: List<WeatherData>?,
|
weatherData: List<WeatherData>?,
|
||||||
currentWeatherData: WeatherData
|
currentWeatherData: WeatherData
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import com.mapbox.turf.TurfMeasurement
|
|||||||
import de.timklge.karooheadwind.datatypes.CloudCoverDataType
|
import de.timklge.karooheadwind.datatypes.CloudCoverDataType
|
||||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||||
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
|
import de.timklge.karooheadwind.datatypes.HeadwindDirectionDataType
|
||||||
|
import de.timklge.karooheadwind.datatypes.HeadwindForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType
|
import de.timklge.karooheadwind.datatypes.HeadwindSpeedDataType
|
||||||
import de.timklge.karooheadwind.datatypes.PrecipitationDataType
|
import de.timklge.karooheadwind.datatypes.PrecipitationDataType
|
||||||
import de.timklge.karooheadwind.datatypes.PrecipitationForecastDataType
|
import de.timklge.karooheadwind.datatypes.PrecipitationForecastDataType
|
||||||
@ -18,8 +19,10 @@ import de.timklge.karooheadwind.datatypes.SurfacePressureDataType
|
|||||||
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
|
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
|
||||||
import de.timklge.karooheadwind.datatypes.TemperatureDataType
|
import de.timklge.karooheadwind.datatypes.TemperatureDataType
|
||||||
import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType
|
import de.timklge.karooheadwind.datatypes.TemperatureForecastDataType
|
||||||
|
import de.timklge.karooheadwind.datatypes.UviDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType
|
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType
|
||||||
|
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataTypeCircle
|
||||||
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindForecastDataType
|
import de.timklge.karooheadwind.datatypes.WindForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindGustsDataType
|
import de.timklge.karooheadwind.datatypes.WindGustsDataType
|
||||||
@ -43,8 +46,6 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.retry
|
import kotlinx.coroutines.flow.retry
|
||||||
import kotlinx.coroutines.flow.transformLatest
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.time.debounce
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
@ -66,6 +67,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
|||||||
listOf(
|
listOf(
|
||||||
HeadwindDirectionDataType(karooSystem, applicationContext),
|
HeadwindDirectionDataType(karooSystem, applicationContext),
|
||||||
TailwindAndRideSpeedDataType(karooSystem, applicationContext),
|
TailwindAndRideSpeedDataType(karooSystem, applicationContext),
|
||||||
|
WindDirectionAndSpeedDataTypeCircle(karooSystem, applicationContext),
|
||||||
WeatherForecastDataType(karooSystem),
|
WeatherForecastDataType(karooSystem),
|
||||||
HeadwindSpeedDataType(karooSystem, applicationContext),
|
HeadwindSpeedDataType(karooSystem, applicationContext),
|
||||||
RelativeHumidityDataType(karooSystem, applicationContext),
|
RelativeHumidityDataType(karooSystem, applicationContext),
|
||||||
@ -80,10 +82,12 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
|||||||
TemperatureForecastDataType(karooSystem),
|
TemperatureForecastDataType(karooSystem),
|
||||||
PrecipitationForecastDataType(karooSystem),
|
PrecipitationForecastDataType(karooSystem),
|
||||||
WindForecastDataType(karooSystem),
|
WindForecastDataType(karooSystem),
|
||||||
|
HeadwindForecastDataType(karooSystem),
|
||||||
WindDirectionAndSpeedDataType(karooSystem, applicationContext),
|
WindDirectionAndSpeedDataType(karooSystem, applicationContext),
|
||||||
RelativeGradeDataType(karooSystem, applicationContext),
|
RelativeGradeDataType(karooSystem, applicationContext),
|
||||||
RelativeElevationGainDataType(karooSystem, applicationContext),
|
RelativeElevationGainDataType(karooSystem, applicationContext),
|
||||||
TemperatureDataType(karooSystem, applicationContext)
|
TemperatureDataType(karooSystem, applicationContext),
|
||||||
|
UviDataType(karooSystem, applicationContext)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -65,14 +65,14 @@ import kotlinx.coroutines.flow.flow
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
abstract class ForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) {
|
abstract class ForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) {
|
||||||
@Composable
|
@Composable
|
||||||
abstract fun RenderWidget(arrowBitmap: Bitmap,
|
abstract fun RenderWidget(
|
||||||
|
arrowBitmap: Bitmap,
|
||||||
current: WeatherInterpretation,
|
current: WeatherInterpretation,
|
||||||
windBearing: Int,
|
windBearing: Int,
|
||||||
windSpeed: Int,
|
windSpeed: Int,
|
||||||
@ -85,7 +85,9 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
dateLabel: String?,
|
dateLabel: String?,
|
||||||
distance: Double?,
|
distance: Double?,
|
||||||
isImperial: Boolean,
|
isImperial: Boolean,
|
||||||
isNight: Boolean)
|
isNight: Boolean,
|
||||||
|
uvi: Double
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
private val glance = GlanceRemoteViews()
|
private val glance = GlanceRemoteViews()
|
||||||
@ -107,6 +109,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
val weatherData = (0..<12).map {
|
val weatherData = (0..<12).map {
|
||||||
val forecastTime = timeAtFullHour + it * 60 * 60
|
val forecastTime = timeAtFullHour + it * 60 * 60
|
||||||
val forecastTemperature = 20.0 + (-20..20).random()
|
val forecastTemperature = 20.0 + (-20..20).random()
|
||||||
|
val forecastUvi = 0.0 + (0..12).random().toDouble()
|
||||||
val forecastPrecipitation = 0.0 + (0..10).random()
|
val forecastPrecipitation = 0.0 + (0..10).random()
|
||||||
val forecastPrecipitationProbability = (0..100).random()
|
val forecastPrecipitationProbability = (0..100).random()
|
||||||
val forecastWeatherCode = WeatherInterpretation.getKnownWeatherCodes().random()
|
val forecastWeatherCode = WeatherInterpretation.getKnownWeatherCodes().random()
|
||||||
@ -127,7 +130,8 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
windGusts = forecastWindGusts,
|
windGusts = forecastWindGusts,
|
||||||
weatherCode = forecastWeatherCode,
|
weatherCode = forecastWeatherCode,
|
||||||
isForecast = true,
|
isForecast = true,
|
||||||
isNight = it < 2
|
isNight = it < 2,
|
||||||
|
uvi = forecastUvi
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +153,8 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
windGusts = 10.0,
|
windGusts = 10.0,
|
||||||
weatherCode = WeatherInterpretation.getKnownWeatherCodes().random(),
|
weatherCode = WeatherInterpretation.getKnownWeatherCodes().random(),
|
||||||
isForecast = false,
|
isForecast = false,
|
||||||
isNight = false
|
isNight = false,
|
||||||
|
uvi = 2.0
|
||||||
),
|
),
|
||||||
coords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour),
|
coords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour),
|
||||||
timezone = "UTC",
|
timezone = "UTC",
|
||||||
@ -333,7 +338,8 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
dateLabel = if (hasNewDate) formattedDate else null,
|
dateLabel = if (hasNewDate) formattedDate else null,
|
||||||
distance = null,
|
distance = null,
|
||||||
isImperial = settingsAndProfile.isImperial,
|
isImperial = settingsAndProfile.isImperial,
|
||||||
isNight = data.current.isNight
|
isNight = data.current.isNight,
|
||||||
|
uvi = data.current.uvi
|
||||||
)
|
)
|
||||||
|
|
||||||
previousDate = formattedDate
|
previousDate = formattedDate
|
||||||
@ -359,7 +365,8 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
dateLabel = if (hasNewDate) formattedDate else null,
|
dateLabel = if (hasNewDate) formattedDate else null,
|
||||||
distance = if (settingsAndProfile.settings.showDistanceInForecast) distanceFromCurrent else null,
|
distance = if (settingsAndProfile.settings.showDistanceInForecast) distanceFromCurrent else null,
|
||||||
isImperial = settingsAndProfile.isImperial,
|
isImperial = settingsAndProfile.isImperial,
|
||||||
isNight = weatherData?.isNight == true
|
isNight = weatherData?.isNight == true,
|
||||||
|
uvi = weatherData?.uvi ?: 0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
previousDate = formattedDate
|
previousDate = formattedDate
|
||||||
|
|||||||
@ -67,24 +67,7 @@ class HeadwindDirectionDataType(
|
|||||||
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
||||||
|
|
||||||
var returnValue = 0.0
|
var returnValue = 0.0
|
||||||
if (value == null || streamData.absoluteWindDirection == null || streamData.windSpeed == null){
|
if (value != null && streamData.absoluteWindDirection != 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 = value
|
var windDirection = value
|
||||||
|
|
||||||
if (windDirection < 0) windDirection += 360
|
if (windDirection < 0) windDirection += 360
|
||||||
@ -223,14 +206,3 @@ class HeadwindDirectionDataType(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun KarooSystemService.getRefreshRateInMilliseconds(context: Context): Long {
|
|
||||||
val refreshRate = context.streamSettings(this).first().refreshRate
|
|
||||||
val isK2 = hardwareType == HardwareType.K2
|
|
||||||
|
|
||||||
return if (isK2){
|
|
||||||
refreshRate.k2Ms
|
|
||||||
} else {
|
|
||||||
refreshRate.k3Ms
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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<LineData>,
|
||||||
|
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..<HEADWIND_SAMPLE_COUNT).mapNotNull { i ->
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
@ -16,6 +17,7 @@ import de.timklge.karooheadwind.HeadingResponse
|
|||||||
import de.timklge.karooheadwind.HeadwindSettings
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.HeadwindWidgetSettings
|
import de.timklge.karooheadwind.HeadwindWidgetSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
|
import de.timklge.karooheadwind.R
|
||||||
import de.timklge.karooheadwind.UpcomingRoute
|
import de.timklge.karooheadwind.UpcomingRoute
|
||||||
import de.timklge.karooheadwind.WeatherDataProvider
|
import de.timklge.karooheadwind.WeatherDataProvider
|
||||||
import de.timklge.karooheadwind.getHeadingFlow
|
import de.timklge.karooheadwind.getHeadingFlow
|
||||||
@ -58,6 +60,11 @@ import kotlin.math.ceil
|
|||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) {
|
abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) {
|
||||||
|
sealed class LineGraphForecastData {
|
||||||
|
data class LineData(val data: Set<LineGraphBuilder.Line>) : LineGraphForecastData()
|
||||||
|
data class Error(val message: String) : LineGraphForecastData()
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
private val glance = GlanceRemoteViews()
|
private val glance = GlanceRemoteViews()
|
||||||
|
|
||||||
@ -75,7 +82,7 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
|||||||
upcomingRoute: UpcomingRoute?,
|
upcomingRoute: UpcomingRoute?,
|
||||||
isPreview: Boolean,
|
isPreview: Boolean,
|
||||||
context: Context
|
context: Context
|
||||||
): Set<LineGraphBuilder.Line>
|
): LineGraphForecastData
|
||||||
|
|
||||||
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> =
|
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> =
|
||||||
flow {
|
flow {
|
||||||
@ -94,6 +101,7 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
|||||||
val forecastWindSpeed = 0.0 + (0..10).random()
|
val forecastWindSpeed = 0.0 + (0..10).random()
|
||||||
val forecastWindDirection = 0.0 + (0..360).random()
|
val forecastWindDirection = 0.0 + (0..360).random()
|
||||||
val forecastWindGusts = 0.0 + (0..10).random()
|
val forecastWindGusts = 0.0 + (0..10).random()
|
||||||
|
val forcastUvi = 0.0 + (0..12).random()
|
||||||
WeatherData(
|
WeatherData(
|
||||||
time = forecastTime,
|
time = forecastTime,
|
||||||
temperature = forecastTemperature,
|
temperature = forecastTemperature,
|
||||||
@ -108,7 +116,8 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
|||||||
windGusts = forecastWindGusts,
|
windGusts = forecastWindGusts,
|
||||||
weatherCode = forecastWeatherCode,
|
weatherCode = forecastWeatherCode,
|
||||||
isForecast = true,
|
isForecast = true,
|
||||||
isNight = it < 2
|
isNight = it < 2,
|
||||||
|
uvi = forcastUvi
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +139,8 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
|||||||
windGusts = 10.0,
|
windGusts = 10.0,
|
||||||
weatherCode = WeatherInterpretation.getKnownWeatherCodes().random(),
|
weatherCode = WeatherInterpretation.getKnownWeatherCodes().random(),
|
||||||
isForecast = false,
|
isForecast = false,
|
||||||
isNight = false
|
isNight = false,
|
||||||
|
uvi = 2.0
|
||||||
),
|
),
|
||||||
coords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour),
|
coords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour),
|
||||||
timezone = "UTC",
|
timezone = "UTC",
|
||||||
@ -272,7 +282,9 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
|||||||
context
|
context
|
||||||
)
|
)
|
||||||
|
|
||||||
val bitmap = LineGraphBuilder(context).drawLineGraph(config.viewSize.first, config.viewSize.second, config.gridSize.first, config.gridSize.second, pointData) { x ->
|
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 startTime = data.firstOrNull()?.time
|
||||||
val time = startTime?.plus(floor(x).toLong(), ChronoUnit.HOURS)
|
val time = startTime?.plus(floor(x).toLong(), ChronoUnit.HOURS)
|
||||||
val timeLabel = getTimeFormatter(context).format(time?.atZone(ZoneId.systemDefault())?.toLocalTime())
|
val timeLabel = getTimeFormatter(context).format(time?.atZone(ZoneId.systemDefault())?.toLocalTime())
|
||||||
@ -299,6 +311,15 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is LineGraphForecastData.Error -> {
|
||||||
|
emitter.onNext(ShowCustomStreamState(pointData.message, null))
|
||||||
|
Box(modifier = GlanceModifier.fillMaxSize()){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emitter.updateView(result.remoteViews)
|
emitter.updateView(result.remoteViews)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph
|
|||||||
upcomingRoute: UpcomingRoute?,
|
upcomingRoute: UpcomingRoute?,
|
||||||
isPreview: Boolean,
|
isPreview: Boolean,
|
||||||
context: Context
|
context: Context
|
||||||
): Set<LineGraphBuilder.Line> {
|
): LineGraphForecastData {
|
||||||
val precipitationPoints = lineData.map { data ->
|
val precipitationPoints = lineData.map { data ->
|
||||||
if (isImperial) { // Convert mm to inches
|
if (isImperial) { // Convert mm to inches
|
||||||
data.weatherData.precipitation * 0.0393701 // 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
|
(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(
|
LineGraphBuilder.Line(
|
||||||
dataPoints = precipitationPoints.mapIndexed { index, value ->
|
dataPoints = precipitationPoints.mapIndexed { index, value ->
|
||||||
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
||||||
@ -42,7 +42,7 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph
|
|||||||
label = "%",
|
label = "%",
|
||||||
yAxis = LineGraphBuilder.YAxis.RIGHT
|
yAxis = LineGraphBuilder.YAxis.RIGHT
|
||||||
)
|
)
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -12,7 +12,7 @@ class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphFo
|
|||||||
upcomingRoute: UpcomingRoute?,
|
upcomingRoute: UpcomingRoute?,
|
||||||
isPreview: Boolean,
|
isPreview: Boolean,
|
||||||
context: Context
|
context: Context
|
||||||
): Set<LineGraphBuilder.Line> {
|
): LineGraphForecastData {
|
||||||
val linePoints = lineData.map { data ->
|
val linePoints = lineData.map { data ->
|
||||||
if (isImperial) {
|
if (isImperial) {
|
||||||
data.weatherData.temperature * 9 / 5 + 32 // Convert Celsius to Fahrenheit
|
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(
|
LineGraphBuilder.Line(
|
||||||
dataPoints = linePoints.mapIndexed { index, value ->
|
dataPoints = linePoints.mapIndexed { index, value ->
|
||||||
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
||||||
@ -29,7 +29,7 @@ class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphFo
|
|||||||
color = android.graphics.Color.RED,
|
color = android.graphics.Color.RED,
|
||||||
label = if (!isImperial) "°C" else "°F",
|
label = if (!isImperial) "°C" else "°F",
|
||||||
)
|
)
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
|
||||||
|
class UviDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "uvi"){
|
||||||
|
override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
|
||||||
|
return data.uvi
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,10 @@ import androidx.glance.text.TextStyle
|
|||||||
import de.timklge.karooheadwind.HeadingResponse
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
import de.timklge.karooheadwind.HeadwindSettings
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
|
import de.timklge.karooheadwind.streamSettings
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.HardwareType
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings: HeadwindSettings?, headingResponse: HeadingResponse?): RemoteViewsCompositionResult {
|
suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, settings: HeadwindSettings?, headingResponse: HeadingResponse?): RemoteViewsCompositionResult {
|
||||||
@ -76,3 +80,14 @@ suspend fun getErrorWidget(glance: GlanceRemoteViews, context: Context, errorCod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun KarooSystemService.getRefreshRateInMilliseconds(context: Context): Long {
|
||||||
|
val refreshRate = context.streamSettings(this).first().refreshRate
|
||||||
|
val isK2 = hardwareType == HardwareType.K2
|
||||||
|
|
||||||
|
return if (isK2){
|
||||||
|
refreshRate.k2Ms
|
||||||
|
} else {
|
||||||
|
refreshRate.k3Ms
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ class WeatherForecastDataType(karooSystem: KarooSystemService) : ForecastDataTyp
|
|||||||
distance: Double?,
|
distance: Double?,
|
||||||
isImperial: Boolean,
|
isImperial: Boolean,
|
||||||
isNight: Boolean,
|
isNight: Boolean,
|
||||||
|
uvi: Double,
|
||||||
) {
|
) {
|
||||||
Weather(
|
Weather(
|
||||||
arrowBitmap = arrowBitmap,
|
arrowBitmap = arrowBitmap,
|
||||||
|
|||||||
@ -0,0 +1,145 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||||
|
import androidx.glance.appwidget.GlanceRemoteViews
|
||||||
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
|
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType.StreamData
|
||||||
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
|
import de.timklge.karooheadwind.streamDatatypeIsVisible
|
||||||
|
import de.timklge.karooheadwind.streamSettings
|
||||||
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
|
import de.timklge.karooheadwind.throttle
|
||||||
|
import de.timklge.karooheadwind.util.msInUserUnit
|
||||||
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
|
import io.hammerhead.karooext.internal.Emitter
|
||||||
|
import io.hammerhead.karooext.internal.ViewEmitter
|
||||||
|
import io.hammerhead.karooext.models.DataPoint
|
||||||
|
import io.hammerhead.karooext.models.DataType
|
||||||
|
import io.hammerhead.karooext.models.HardwareType
|
||||||
|
import io.hammerhead.karooext.models.StreamState
|
||||||
|
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
import io.hammerhead.karooext.models.ViewConfig
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
|
class WindDirectionAndSpeedDataTypeCircle(
|
||||||
|
private val karooSystem: KarooSystemService,
|
||||||
|
private val applicationContext: Context
|
||||||
|
) : DataTypeImpl("karoo-headwind", "windDirectionAndSpeedCircle") {
|
||||||
|
private val glance = GlanceRemoteViews()
|
||||||
|
|
||||||
|
data class StreamData(val headingResponse: HeadingResponse, val absoluteWindDirection: Double?, val windSpeed: Double?, val settings: HeadwindSettings)
|
||||||
|
|
||||||
|
private fun previewFlow(profileFlow: Flow<UserProfile>): Flow<de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType.StreamData> {
|
||||||
|
return flow {
|
||||||
|
val profile = profileFlow.first()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val bearing = (0..360).random().toDouble()
|
||||||
|
val windSpeed = (0..10).random()
|
||||||
|
val gustSpeed = windSpeed * ((10..20).random().toDouble() / 10)
|
||||||
|
val isImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||||
|
|
||||||
|
emit(StreamData(HeadingResponse.Value(bearing), bearing, windSpeed.toDouble(), HeadwindSettings(), gustSpeed = gustSpeed, isImperial = isImperial, isVisible = true))
|
||||||
|
|
||||||
|
delay(2_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Starting headwind direction view with $emitter")
|
||||||
|
|
||||||
|
val baseBitmap = BitmapFactory.decodeResource(
|
||||||
|
context.resources,
|
||||||
|
de.timklge.karooheadwind.R.drawable.circle
|
||||||
|
)
|
||||||
|
|
||||||
|
val configJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
emitter.onNext(UpdateGraphicConfig(showHeader = false))
|
||||||
|
awaitCancellation()
|
||||||
|
}
|
||||||
|
|
||||||
|
val flow = if (config.preview) {
|
||||||
|
previewFlow(karooSystem.streamUserProfile())
|
||||||
|
} else {
|
||||||
|
combine(karooSystem.getRelativeHeadingFlow(context),
|
||||||
|
context.streamCurrentWeatherData(karooSystem),
|
||||||
|
context.streamSettings(karooSystem),
|
||||||
|
karooSystem.streamUserProfile(),
|
||||||
|
karooSystem.streamDatatypeIsVisible(dataTypeId)
|
||||||
|
) { headingResponse, weatherData, settings, userProfile, isVisible ->
|
||||||
|
val isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||||
|
val absoluteWindDirection = weatherData?.windDirection
|
||||||
|
val windSpeed = weatherData?.windSpeed
|
||||||
|
val gustSpeed = weatherData?.windGusts
|
||||||
|
|
||||||
|
StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val refreshRate = karooSystem.getRefreshRateInMilliseconds(context)
|
||||||
|
|
||||||
|
flow.filter { it.isVisible }.throttle(refreshRate).collect { streamData ->
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Updating headwind direction view")
|
||||||
|
|
||||||
|
val value = (streamData.headingResponse as? HeadingResponse.Value)?.diff
|
||||||
|
if (value == null || streamData.absoluteWindDirection == null || streamData.windSpeed == null){
|
||||||
|
var headingResponse = streamData.headingResponse
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
val windSpeed = streamData.windSpeed
|
||||||
|
val windSpeedUserUnit = msInUserUnit(windSpeed, streamData.isImperial)
|
||||||
|
|
||||||
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
|
HeadwindDirection(
|
||||||
|
baseBitmap,
|
||||||
|
value.roundToInt(),
|
||||||
|
config.textSize,
|
||||||
|
windSpeedUserUnit.roundToInt().toString(),
|
||||||
|
preview = config.preview,
|
||||||
|
wideMode = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.updateView(result.remoteViews)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitter.setCancellable {
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Stopping headwind view with $emitter")
|
||||||
|
configJob.cancel()
|
||||||
|
viewJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,7 +29,7 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
|
|||||||
upcomingRoute: UpcomingRoute?,
|
upcomingRoute: UpcomingRoute?,
|
||||||
isPreview: Boolean,
|
isPreview: Boolean,
|
||||||
context: Context
|
context: Context
|
||||||
): Set<LineGraphBuilder.Line> {
|
): LineGraphForecastData {
|
||||||
val windPoints = lineData.map { data ->
|
val windPoints = lineData.map { data ->
|
||||||
if (isImperial) { // Convert m/s to mph
|
if (isImperial) { // Convert m/s to mph
|
||||||
data.weatherData.windSpeed * 2.23694 // 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 {
|
return LineGraphForecastData.LineData(buildSet {
|
||||||
if (upcomingRoute != null || isPreview){
|
|
||||||
(0..<HEADWIND_SAMPLE_COUNT).mapNotNull { i ->
|
|
||||||
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 {
|
|
||||||
add(LineGraphBuilder.Line(
|
add(LineGraphBuilder.Line(
|
||||||
dataPoints = gustPoints.mapIndexed { index, value ->
|
dataPoints = gustPoints.mapIndexed { index, value ->
|
||||||
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
||||||
@ -134,25 +62,6 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
|
|||||||
color = Color.GRAY,
|
color = Color.GRAY,
|
||||||
label = "Wind" // if (!isImperial) "Wind km/h" else "Wind mph",
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -17,6 +17,7 @@ data class WeatherData(
|
|||||||
val windGusts: Double,
|
val windGusts: Double,
|
||||||
val weatherCode: Int,
|
val weatherCode: Int,
|
||||||
val isForecast: Boolean,
|
val isForecast: Boolean,
|
||||||
val isNight: Boolean
|
val isNight: Boolean,
|
||||||
|
val uvi: Double,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ data class OpenMeteoWeatherData(
|
|||||||
@SerialName("wind_gusts_10m") val windGusts: Double,
|
@SerialName("wind_gusts_10m") val windGusts: Double,
|
||||||
@SerialName("weather_code") val weatherCode: Int,
|
@SerialName("weather_code") val weatherCode: Int,
|
||||||
@SerialName("is_day") val isDay: Int,
|
@SerialName("is_day") val isDay: Int,
|
||||||
|
@SerialName("uv_index") val uvi: Double,
|
||||||
) {
|
) {
|
||||||
fun toWeatherData(): WeatherData = WeatherData(
|
fun toWeatherData(): WeatherData = WeatherData(
|
||||||
temperature = temperature,
|
temperature = temperature,
|
||||||
@ -33,6 +34,7 @@ data class OpenMeteoWeatherData(
|
|||||||
time = time,
|
time = time,
|
||||||
isForecast = false,
|
isForecast = false,
|
||||||
isNight = isDay == 0,
|
isNight = isDay == 0,
|
||||||
|
uvi = uvi
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ data class OpenMeteoWeatherForecastData(
|
|||||||
@SerialName("pressure_msl") val sealevelPressure: List<Double>,
|
@SerialName("pressure_msl") val sealevelPressure: List<Double>,
|
||||||
@SerialName("is_day") val isDay: List<Int>,
|
@SerialName("is_day") val isDay: List<Int>,
|
||||||
@SerialName("relative_humidity_2m") val relativeHumidity: List<Int>,
|
@SerialName("relative_humidity_2m") val relativeHumidity: List<Int>,
|
||||||
|
@SerialName("uv_index") val uvi: List<Double>,
|
||||||
) {
|
) {
|
||||||
fun toWeatherData(): List<WeatherData> {
|
fun toWeatherData(): List<WeatherData> {
|
||||||
return time.mapIndexed { index, t ->
|
return time.mapIndexed { index, t ->
|
||||||
@ -37,6 +38,7 @@ data class OpenMeteoWeatherForecastData(
|
|||||||
surfacePressure = surfacePressure[index],
|
surfacePressure = surfacePressure[index],
|
||||||
sealevelPressure = sealevelPressure[index],
|
sealevelPressure = sealevelPressure[index],
|
||||||
relativeHumidity = relativeHumidity[index],
|
relativeHumidity = relativeHumidity[index],
|
||||||
|
uvi = uvi[index]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ class OpenMeteoWeatherProvider : WeatherProvider {
|
|||||||
// https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t=is_day,surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m&timeformat=unixtime&past_hours=1&forecast_days=1&forecast_hours=12
|
// https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41¤t=is_day,surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m&timeformat=unixtime&past_hours=1&forecast_days=1&forecast_hours=12
|
||||||
val lats = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lat) }
|
val lats = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lat) }
|
||||||
val lons = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lon) }
|
val lons = gpsCoordinates.joinToString(",") { String.format(Locale.US, "%.6f", it.lon) }
|
||||||
val url = "https://api.open-meteo.com/v1/forecast?latitude=${lats}&longitude=${lons}¤t=is_day,surface_pressure,pressure_msl,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m,is_day,surface_pressure,pressure_msl,relative_humidity_2m,cloud_cover&timeformat=unixtime&past_hours=0&forecast_days=1&forecast_hours=12&wind_speed_unit=ms"
|
val url = "https://api.open-meteo.com/v1/forecast?latitude=${lats}&longitude=${lons}¤t=is_day,surface_pressure,pressure_msl,uv_index,temperature_2m,relative_humidity_2m,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=uv_index,temperature_2m,precipitation_probability,precipitation,weather_code,wind_speed_10m,wind_direction_10m,wind_gusts_10m,is_day,surface_pressure,pressure_msl,relative_humidity_2m,cloud_cover&timeformat=unixtime&past_hours=0&forecast_days=1&forecast_hours=12&wind_speed_unit=ms"
|
||||||
|
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Http request to ${url}...")
|
Log.d(KarooHeadwindExtension.TAG, "Http request to ${url}...")
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,8 @@ data class OpenWeatherMapForecastData(
|
|||||||
val pop: Double,
|
val pop: Double,
|
||||||
val rain: Rain? = null,
|
val rain: Rain? = null,
|
||||||
val snow: Snow? = null,
|
val snow: Snow? = null,
|
||||||
val weather: List<Weather>
|
val weather: List<Weather>,
|
||||||
|
val uvi: Double,
|
||||||
) {
|
) {
|
||||||
fun toWeatherData(currentWeatherData: OpenWeatherMapWeatherData): WeatherData {
|
fun toWeatherData(currentWeatherData: OpenWeatherMapWeatherData): WeatherData {
|
||||||
val dtInstant = Instant.ofEpochSecond(dt)
|
val dtInstant = Instant.ofEpochSecond(dt)
|
||||||
@ -32,6 +33,7 @@ data class OpenWeatherMapForecastData(
|
|||||||
val sunsetTime = sunsetInstant.atZone(ZoneOffset.UTC).toLocalTime()
|
val sunsetTime = sunsetInstant.atZone(ZoneOffset.UTC).toLocalTime()
|
||||||
|
|
||||||
return WeatherData(
|
return WeatherData(
|
||||||
|
uvi = uvi,
|
||||||
temperature = temp,
|
temperature = temp,
|
||||||
relativeHumidity = humidity,
|
relativeHumidity = humidity,
|
||||||
precipitation = rain?.h1 ?: 0.0,
|
precipitation = rain?.h1 ?: 0.0,
|
||||||
|
|||||||
@ -19,9 +19,11 @@ data class OpenWeatherMapWeatherData(
|
|||||||
val wind_gust: Double? = null,
|
val wind_gust: Double? = null,
|
||||||
val rain: Rain? = null,
|
val rain: Rain? = null,
|
||||||
val snow: Snow? = null,
|
val snow: Snow? = null,
|
||||||
|
val uvi: Double,
|
||||||
val weather: List<Weather>){
|
val weather: List<Weather>){
|
||||||
|
|
||||||
fun toWeatherData(): WeatherData = WeatherData(
|
fun toWeatherData(): WeatherData = WeatherData(
|
||||||
|
uvi = uvi,
|
||||||
temperature = temp,
|
temperature = temp,
|
||||||
relativeHumidity = humidity,
|
relativeHumidity = humidity,
|
||||||
precipitation = rain?.h1 ?: 0.0,
|
precipitation = rain?.h1 ?: 0.0,
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
<color name="white">#ffffff</color>
|
<color name="white">#ffffff</color>
|
||||||
<color name="black">#000000</color>
|
<color name="black">#000000</color>
|
||||||
|
<color name="gray">#808080</color>
|
||||||
|
|
||||||
<color name="green">#00ff00</color>
|
<color name="green">#00ff00</color>
|
||||||
<color name="orange">#ff9930</color>
|
<color name="orange">#ff9930</color>
|
||||||
|
|||||||
@ -10,11 +10,11 @@
|
|||||||
<string name="cloudCover">Cloud cover</string>
|
<string name="cloudCover">Cloud cover</string>
|
||||||
<string name="cloudCover_description">Current cloud cover in percent</string>
|
<string name="cloudCover_description">Current cloud cover in percent</string>
|
||||||
<string name="windSpeed">Wind speed</string>
|
<string name="windSpeed">Wind speed</string>
|
||||||
<string name="windSpeed_description">Current wind speed in configured unit</string>
|
<string name="windSpeed_description">Current absolute wind speed</string>
|
||||||
<string name="windGusts">Wind gusts</string>
|
<string name="windGusts">Wind gusts</string>
|
||||||
<string name="windGusts_description">Current wind gust speed in configured unit</string>
|
<string name="windGusts_description">Current wind gust speed in configured unit</string>
|
||||||
<string name="windDirection">Absolute wind direction</string>
|
<string name="windDirection">Absolute wind direction</string>
|
||||||
<string name="windDirection_description">Current wind direction</string>
|
<string name="windDirection_description">Current absolute wind direction</string>
|
||||||
<string name="precipitation">Rainfall</string>
|
<string name="precipitation">Rainfall</string>
|
||||||
<string name="precipitation_description">Current precipitation (rainfall / snowfall)</string>
|
<string name="precipitation_description">Current precipitation (rainfall / snowfall)</string>
|
||||||
<string name="surfacePressure">Surface pressure</string>
|
<string name="surfacePressure">Surface pressure</string>
|
||||||
@ -22,11 +22,15 @@
|
|||||||
<string name="sealevelPressure">Sealevel pressure</string>
|
<string name="sealevelPressure">Sealevel pressure</string>
|
||||||
<string name="sealevelPressure_description">Atmospheric pressure at sea level in configured unit</string>
|
<string name="sealevelPressure_description">Atmospheric pressure at sea level in configured unit</string>
|
||||||
<string name="weather_forecast">Weather Forecast</string>
|
<string name="weather_forecast">Weather Forecast</string>
|
||||||
|
<string name="uvi">UV Index</string>
|
||||||
|
<string name="uvi_description">Current UV Index at current location</string>
|
||||||
<string name="weather_forecast_description">Current hourly weather forecast</string>
|
<string name="weather_forecast_description">Current hourly weather forecast</string>
|
||||||
<string name="temperature_forecast">Temperature Forecast</string>
|
<string name="temperature_forecast">Temperature Forecast</string>
|
||||||
<string name="temperature_forecast_description">Current hourly temperature forecast</string>
|
<string name="temperature_forecast_description">Current hourly temperature forecast</string>
|
||||||
<string name="wind_forecast">Wind Forecast</string>
|
<string name="wind_forecast">Wind Forecast</string>
|
||||||
<string name="wind_forecast_description">Current hourly wind forecast</string>
|
<string name="wind_forecast_description">Current hourly wind forecast</string>
|
||||||
|
<string name="headwind_forecast">Headwind Forecast</string>
|
||||||
|
<string name="headwind_forecast_description">Current hourly headwind forecast for loaded route</string>
|
||||||
<string name="precipitation_forecast">Precipitation Forecast</string>
|
<string name="precipitation_forecast">Precipitation Forecast</string>
|
||||||
<string name="precipitation_forecast_description">Current hourly precipitation forecast</string>
|
<string name="precipitation_forecast_description">Current hourly precipitation forecast</string>
|
||||||
<string name="temperature">Temperature</string>
|
<string name="temperature">Temperature</string>
|
||||||
@ -39,6 +43,8 @@
|
|||||||
<string name="relativeGrade_description">Perceived grade in percent</string>
|
<string name="relativeGrade_description">Perceived grade in percent</string>
|
||||||
<string name="relativeElevationGain">Relative Elevation Gain</string>
|
<string name="relativeElevationGain">Relative Elevation Gain</string>
|
||||||
<string name="relativeElevationGain_description">Perceived elevation gain in meters</string>
|
<string name="relativeElevationGain_description">Perceived elevation gain in meters</string>
|
||||||
<string name="windDirectionAndSpeed">Wind direction and speed</string>
|
<string name="windDirectionAndSpeed">Wind direction, speed</string>
|
||||||
<string name="windDirectionAndSpeed_description">Current wind direction and wind speed</string>
|
<string name="windDirectionAndSpeed_description">Current wind direction and wind speed</string>
|
||||||
|
<string name="windDirectionAndSpeedCircle">Wind direction, speed (Circle)</string>
|
||||||
|
<string name="windDirectionAndSpeedCircle_description">Current wind direction and wind speed (Circle graphics)</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -22,10 +22,17 @@
|
|||||||
<DataType
|
<DataType
|
||||||
description="@string/windDirectionAndSpeed_description"
|
description="@string/windDirectionAndSpeed_description"
|
||||||
displayName="@string/windDirectionAndSpeed"
|
displayName="@string/windDirectionAndSpeed"
|
||||||
graphical="false"
|
graphical="true"
|
||||||
icon="@drawable/wind"
|
icon="@drawable/wind"
|
||||||
typeId="windDirectionAndSpeed" />
|
typeId="windDirectionAndSpeed" />
|
||||||
|
|
||||||
|
<DataType
|
||||||
|
description="@string/windDirectionAndSpeedCircle_description"
|
||||||
|
displayName="@string/windDirectionAndSpeedCircle"
|
||||||
|
graphical="true"
|
||||||
|
icon="@drawable/wind"
|
||||||
|
typeId="windDirectionAndSpeedCircle" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/weather_forecast_description"
|
description="@string/weather_forecast_description"
|
||||||
displayName="@string/weather_forecast"
|
displayName="@string/weather_forecast"
|
||||||
@ -54,6 +61,13 @@
|
|||||||
icon="@drawable/wind"
|
icon="@drawable/wind"
|
||||||
typeId="windForecast" />
|
typeId="windForecast" />
|
||||||
|
|
||||||
|
<DataType
|
||||||
|
description="@string/headwind_forecast_description"
|
||||||
|
displayName="@string/headwind_forecast"
|
||||||
|
graphical="true"
|
||||||
|
icon="@drawable/wind"
|
||||||
|
typeId="headwindForecast" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/headwind_speed_description"
|
description="@string/headwind_speed_description"
|
||||||
displayName="@string/headwind_speed"
|
displayName="@string/headwind_speed"
|
||||||
@ -124,6 +138,13 @@
|
|||||||
icon="@drawable/thermometer"
|
icon="@drawable/thermometer"
|
||||||
typeId="temperature" />
|
typeId="temperature" />
|
||||||
|
|
||||||
|
<DataType
|
||||||
|
description="@string/uvi_description"
|
||||||
|
displayName="@string/uvi"
|
||||||
|
graphical="false"
|
||||||
|
icon="@drawable/thermometer"
|
||||||
|
typeId="uvi" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/relativeGrade_description"
|
description="@string/relativeGrade_description"
|
||||||
displayName="@string/relativeGrade"
|
displayName="@string/relativeGrade"
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import de.timklge.karooheadwind.datatypes.RelativeGradeDataType
|
import de.timklge.karooheadwind.datatypes.RelativeGradeDataType
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
Loading…
x
Reference in New Issue
Block a user