Compare commits
3 Commits
617a41c7c8
...
8028226fac
| Author | SHA1 | Date | |
|---|---|---|---|
| 8028226fac | |||
|
|
dae1369cd8 | ||
|
|
188863727f |
34
.github/workflows/android.yml
vendored
@ -3,10 +3,10 @@ name: Build
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ "master" ]
|
branches: [ "**" ]
|
||||||
tags: [ "*" ]
|
tags: [ "*" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "master" ]
|
branches: [ "**" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -19,13 +19,14 @@ jobs:
|
|||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
- name: Set up environment variables
|
- name: Set up environment variables
|
||||||
run: |
|
run: |
|
||||||
echo "GPR_USER=${{ github.actor }}" >> $GITHUB_ENV
|
echo "GPR_USER=${{ secrets.GHUB_USER || github.actor }}" >> $GITHUB_ENV
|
||||||
echo "GPR_KEY=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
|
echo "GPR_KEY=${{ secrets.GHUB_TOKEN || secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
|
||||||
echo "KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> $GITHUB_ENV
|
echo "KEY_ALIAS=${{ secrets.KEY_ALIAS }}" >> $GITHUB_ENV
|
||||||
echo "KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> $GITHUB_ENV
|
echo "KEY_PASSWORD=${{ secrets.KEY_PASSWORD }}" >> $GITHUB_ENV
|
||||||
echo "KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> $GITHUB_ENV
|
echo "KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> $GITHUB_ENV
|
||||||
echo "KEYSTORE_BASE64=${{ secrets.KEYSTORE_BASE64 }}" >> $GITHUB_ENV
|
echo "KEYSTORE_BASE64=${{ secrets.KEYSTORE_BASE64 }}" >> $GITHUB_ENV
|
||||||
echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV
|
echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV
|
||||||
|
echo "BASE_URL=${{ secrets.BASE_URL || 'https://github.com/timklge/karoo-headwind/releases/latest/download' }}" >> $GITHUB_ENV
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
@ -34,25 +35,30 @@ jobs:
|
|||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build
|
run: ./gradlew build
|
||||||
|
|
||||||
- name: Archive APK
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: app-release.apk
|
|
||||||
path: app/build/outputs/apk/release/app-release.apk
|
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: ncipollo/release-action@v1
|
uses: softprops/action-gh-release@v2
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
name: ${{ github.ref_name }}
|
name: ${{ github.ref_name }}
|
||||||
prerelease: false
|
draft: false
|
||||||
generateReleaseNotes: true
|
generate_release_notes: true
|
||||||
artifacts: app/build/outputs/apk/release/app-release.apk, app/manifest.json, app/karoo-headwind.png, preview0.png, preview1.png, preview2.png, preview3.png
|
make_latest: true
|
||||||
|
files: |
|
||||||
|
app/build/outputs/apk/release/app-release.apk
|
||||||
|
app/manifest.json
|
||||||
|
app/karoo-headwind.png
|
||||||
|
preview0.png
|
||||||
|
preview1.png
|
||||||
|
preview2.png
|
||||||
|
preview3.png
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
1
.gitignore
vendored
@ -9,3 +9,4 @@
|
|||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
/app/release
|
/app/release
|
||||||
|
app/manifest.json
|
||||||
17
README.md
@ -24,6 +24,8 @@ 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. You can change this behavior in the app settings to show the absolute wind direction and speed instead.
|
- 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. You can change this behavior in the app settings to show the absolute wind direction and speed instead.
|
||||||
- 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.
|
||||||
- Tailwind (graphical, 1x1 field): Similar to the tailwind and riding speed field, but shows tailwind speed, wind speed and wind gust speed instead of riding speed.
|
- Tailwind (graphical, 1x1 field): Similar to the tailwind and riding speed field, but shows tailwind speed, wind speed and wind gust speed instead of riding speed.
|
||||||
|
- Wind direction and speed (graphical, 1x1 field): Similar to the tailwind data field, but shows the absolute wind speed and gust speed 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.
|
||||||
- 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.
|
- 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.
|
||||||
- Current weather (graphical, 1x1 field): Shows current weather conditions (same as forecast widget, but only for the current time).
|
- Current weather (graphical, 1x1 field): Shows current weather conditions (same as forecast widget, but only for the current time).
|
||||||
- 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).
|
||||||
@ -41,7 +43,7 @@ If the app cannot connect to the weather service, it will retry the download eve
|
|||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- Icons are from [boxicons.com](https://boxicons.com) ([MIT-licensed](icon_credits.txt))
|
- Icons are from [boxicons.com](https://boxicons.com) ([MIT-licensed](icon_credits.txt)) and the [Google Noto Color Emoji font](https://fonts.google.com/noto/specimen/Noto+Color+Emoji) (SIL Open Font License 1.1)
|
||||||
- Made possible by the generous usage terms of [open-meteo.com](https://open-meteo.com)
|
- Made possible by the generous usage terms of [open-meteo.com](https://open-meteo.com)
|
||||||
- Interfaces with [openweathermap.org](https://openweathermap.org)
|
- Interfaces with [openweathermap.org](https://openweathermap.org)
|
||||||
- Uses [karoo-ext](https://github.com/hammerheadnav/karoo-ext) (Apache2-licensed)
|
- Uses [karoo-ext](https://github.com/hammerheadnav/karoo-ext) (Apache2-licensed)
|
||||||
@ -53,10 +55,11 @@ This app uses Google Crashlytics for crash reporting to help improve stability a
|
|||||||
## Extension Developers: Headwind Data Type
|
## Extension Developers: Headwind Data Type
|
||||||
|
|
||||||
If the user has installed the headwind extension on his karoo, you can stream the headwind data type from other extensions via `karoo-ext`.
|
If the user has installed the headwind extension on his karoo, you can stream the headwind data type from other extensions via `karoo-ext`.
|
||||||
Use extension id `karoo-headwind` with datatype ids `headwind` and `userwindSpeed`.
|
Use data type id `TYPE_EXT::karoo-headwind::TYPE_ID` with `TYPE_ID` being one of `headwind`, `windDirection`, `headwindSpeed`, `windSpeed` etc.
|
||||||
|
|
||||||
- The `headwind` datatype contains a single field that either represents an error code or the wind direction. A `-1.0` indicates missing gps receiption, `-2.0` no weather data, `-3.0` that the headwind extension
|
- The `headwind` datatype contains a single field that either represents an error code or the *relative* wind direction. A `-1.0` indicates missing gps receiption, `-2.0` no weather data, `-3.0` that the headwind extension
|
||||||
has not been set up. Otherwise, the value is the wind direction in degrees; if the user has set the headwind indicator to depict the absolute wind direction, the field will contain the absolute wind direction; otherwise
|
has not been set up. Otherwise, the value is the headwind direction in degrees.
|
||||||
it will contain the headwind direction.
|
- The `windDirection` datatype contains a single field with the *absolute* wind direction in degrees (so 0 = North, 90 = East etc.)
|
||||||
- The `userwindSpeed` datatype contains a single field with the wind speed in the user's defined unit. If the user has set the headwind indicator to show the absolute wind speed,
|
- The `headwindSpeed` datatype contains a single field that contains the *relative* headwind speed in meters per second.
|
||||||
this field will contain the absolute wind speed; otherwise it will contain the headwind speed.
|
- The `windSpeed` datatype contains a single field that contains the *absolute* wind speed in meters per second.
|
||||||
|
- Other datatypes like `windGusts` etc. are also available, see [extension_info.xml](https://github.com/timklge/karoo-headwind/blob/master/app/src/main/res/xml/extension_info.xml)
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import com.android.build.gradle.tasks.ProcessApplicationManifest
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@ -60,29 +61,38 @@ tasks.register("generateManifest") {
|
|||||||
group = "build"
|
group = "build"
|
||||||
|
|
||||||
doLast {
|
doLast {
|
||||||
|
val baseUrl = System.getenv("BASE_URL") ?: "https://github.com/timklge/karoo-headwind/releases/latest/download"
|
||||||
val manifestFile = file("$projectDir/manifest.json")
|
val manifestFile = file("$projectDir/manifest.json")
|
||||||
val manifest = mapOf(
|
val manifest = mapOf(
|
||||||
"label" to "Headwind",
|
"label" to "Headwind",
|
||||||
"packageName" to "de.timklge.karooheadwind",
|
"packageName" to "de.timklge.karooheadwind",
|
||||||
"iconUrl" to "https://github.com/timklge/karoo-headwind/releases/latest/download/karoo-headwind.png",
|
"iconUrl" to "$baseUrl/karoo-headwind.png",
|
||||||
"latestApkUrl" to "https://github.com/timklge/karoo-headwind/releases/latest/download/app-release.apk",
|
"latestApkUrl" to "$baseUrl/app-release.apk",
|
||||||
"latestVersion" to android.defaultConfig.versionName,
|
"latestVersion" to android.defaultConfig.versionName,
|
||||||
"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 "* Remove crashlytics\n" +
|
"releaseNotes" to "* Add forecast line graphs\n* Add wind direction and speed field\n* Refactor unit conversions\n* Remove crashlytics\n" +
|
||||||
"* Reduce refresh rate on K2, add refresh rate setting\n" +
|
"* Reduce refresh rate on K2, add refresh rate setting\n" +
|
||||||
"screenshotUrls" to listOf(
|
"screenshotUrls" to listOf(
|
||||||
"https://github.com/timklge/karoo-headwind/releases/latest/download/preview1.png",
|
"$baseUrl/preview1.png",
|
||||||
"https://github.com/timklge/karoo-headwind/releases/latest/download/preview3.png",
|
"$baseUrl/preview3.png",
|
||||||
"https://github.com/timklge/karoo-headwind/releases/latest/download/preview2.png",
|
"$baseUrl/preview2.png",
|
||||||
"https://github.com/timklge/karoo-headwind/releases/latest/download/preview0.png",
|
"$baseUrl/preview0.png",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val gson = groovy.json.JsonBuilder(manifest).toPrettyString()
|
val gson = groovy.json.JsonBuilder(manifest).toPrettyString()
|
||||||
manifestFile.writeText(gson)
|
manifestFile.writeText(gson)
|
||||||
println("Generated manifest.json with version ${android.defaultConfig.versionName} (${android.defaultConfig.versionCode})")
|
println("Generated manifest.json with version ${android.defaultConfig.versionName} (${android.defaultConfig.versionCode})")
|
||||||
|
|
||||||
|
if (System.getenv()["BASE_URL"] != null){
|
||||||
|
val androidManifestFile = file("$projectDir/src/main/AndroidManifest.xml")
|
||||||
|
var androidManifestContent = androidManifestFile.readText()
|
||||||
|
androidManifestContent = androidManifestContent.replace("\$BASE_URL\$", baseUrl)
|
||||||
|
androidManifestFile.writeText(androidManifestContent)
|
||||||
|
println("Replaced \$BASE_URL$ in AndroidManifest.xml")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +100,12 @@ tasks.named("assemble") {
|
|||||||
dependsOn("generateManifest")
|
dependsOn("generateManifest")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType<ProcessApplicationManifest>().configureEach {
|
||||||
|
if (name == "processDebugMainManifest" || name == "processReleaseMainManifest") {
|
||||||
|
dependsOn(tasks.named("generateManifest"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.mapbox.sdk.turf)
|
implementation(libs.mapbox.sdk.turf)
|
||||||
implementation(libs.hammerhead.karoo.ext)
|
implementation(libs.hammerhead.karoo.ext)
|
||||||
|
|||||||
@ -32,6 +32,6 @@
|
|||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.hammerhead.karooext.MANIFEST_URL"
|
android:name="io.hammerhead.karooext.MANIFEST_URL"
|
||||||
android:value="https://github.com/timklge/karoo-headwind/releases/latest/download/manifest.json" />
|
android:value="$BASE_URL$/manifest.json" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@ -210,6 +210,22 @@ fun Context.streamCurrentForecastWeatherData(): Flow<WeatherDataResponse?> {
|
|||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun lerp(
|
||||||
|
start: Double,
|
||||||
|
end: Double,
|
||||||
|
factor: Double
|
||||||
|
): Double {
|
||||||
|
return start + (end - start) * factor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lerp(
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
factor: Double
|
||||||
|
): Int {
|
||||||
|
return (start + (end - start) * factor).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
fun lerpNullable(
|
fun lerpNullable(
|
||||||
start: Double?,
|
start: Double?,
|
||||||
end: Double?,
|
end: Double?,
|
||||||
@ -266,12 +282,12 @@ fun lerpWeather(
|
|||||||
return WeatherData(
|
return WeatherData(
|
||||||
time = (start.time + (end.time - start.time) * factor).toLong(),
|
time = (start.time + (end.time - start.time) * factor).toLong(),
|
||||||
temperature = start.temperature + (end.temperature - start.temperature) * factor,
|
temperature = start.temperature + (end.temperature - start.temperature) * factor,
|
||||||
relativeHumidity = lerpNullable(start.relativeHumidity, end.relativeHumidity, factor),
|
relativeHumidity = lerp(start.relativeHumidity, end.relativeHumidity, factor),
|
||||||
precipitation = start.precipitation + (end.precipitation - start.precipitation) * factor,
|
precipitation = start.precipitation + (end.precipitation - start.precipitation) * factor,
|
||||||
precipitationProbability = lerpNullable(start.precipitationProbability, end.precipitationProbability, factor),
|
precipitationProbability = lerpNullable(start.precipitationProbability, end.precipitationProbability, factor),
|
||||||
cloudCover = lerpNullable(start.cloudCover, end.cloudCover, factor),
|
cloudCover = lerp(start.cloudCover, end.cloudCover, factor),
|
||||||
surfacePressure = lerpNullable(start.surfacePressure, end.surfacePressure, factor),
|
surfacePressure = lerp(start.surfacePressure, end.surfacePressure, factor),
|
||||||
sealevelPressure = lerpNullable(start.sealevelPressure, end.sealevelPressure, factor),
|
sealevelPressure = lerp(start.sealevelPressure, end.sealevelPressure, factor),
|
||||||
windSpeed = start.windSpeed + (end.windSpeed - start.windSpeed) * factor,
|
windSpeed = start.windSpeed + (end.windSpeed - start.windSpeed) * factor,
|
||||||
windDirection = lerpAngle(start.windDirection, end.windDirection, factor),
|
windDirection = lerpAngle(start.windDirection, end.windDirection, factor),
|
||||||
windGusts = start.windGusts + (end.windGusts - start.windGusts) * factor,
|
windGusts = start.windGusts + (end.windGusts - start.windGusts) * factor,
|
||||||
|
|||||||
@ -18,17 +18,6 @@ enum class PrecipitationUnit(val id: String, val label: String, val unitDisplay:
|
|||||||
INCH("inch", "Inch", "in")
|
INCH("inch", "Inch", "in")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class WindDirectionIndicatorTextSetting(val id: String, val label: String){
|
|
||||||
HEADWIND_SPEED("headwind-speed", "Headwind speed"),
|
|
||||||
WIND_SPEED("absolute-wind-speed", "Absolute wind speed"),
|
|
||||||
NONE("none", "None")
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class WindDirectionIndicatorSetting(val id: String, val label: String){
|
|
||||||
HEADWIND_DIRECTION("headwind-direction", "Headwind"),
|
|
||||||
WIND_DIRECTION("wind-direction", "Absolute wind direction"),
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class TemperatureUnit(val id: String, val label: String, val unitDisplay: String){
|
enum class TemperatureUnit(val id: String, val label: String, val unitDisplay: String){
|
||||||
CELSIUS("celsius", "Celsius (°C)", "°C"),
|
CELSIUS("celsius", "Celsius (°C)", "°C"),
|
||||||
FAHRENHEIT("fahrenheit", "Fahrenheit (°F)", "°F")
|
FAHRENHEIT("fahrenheit", "Fahrenheit (°F)", "°F")
|
||||||
@ -91,8 +80,6 @@ enum class RefreshRate(val id: String, val k2Ms: Long, val k3Ms: Long) {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class HeadwindSettings(
|
data class HeadwindSettings(
|
||||||
val welcomeDialogAccepted: Boolean = false,
|
val welcomeDialogAccepted: Boolean = false,
|
||||||
val windDirectionIndicatorTextSetting: WindDirectionIndicatorTextSetting = WindDirectionIndicatorTextSetting.HEADWIND_SPEED,
|
|
||||||
val windDirectionIndicatorSetting: WindDirectionIndicatorSetting = WindDirectionIndicatorSetting.HEADWIND_DIRECTION,
|
|
||||||
val roundLocationTo: RoundLocationSetting = RoundLocationSetting.KM_3,
|
val roundLocationTo: RoundLocationSetting = RoundLocationSetting.KM_3,
|
||||||
val forecastedKmPerHour: Int = 20,
|
val forecastedKmPerHour: Int = 20,
|
||||||
val forecastedMilesPerHour: Int = 12,
|
val forecastedMilesPerHour: Int = 12,
|
||||||
|
|||||||
@ -17,10 +17,9 @@ import de.timklge.karooheadwind.datatypes.RelativeHumidityDataType
|
|||||||
import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
|
import de.timklge.karooheadwind.datatypes.SealevelPressureDataType
|
||||||
import de.timklge.karooheadwind.datatypes.SurfacePressureDataType
|
import de.timklge.karooheadwind.datatypes.SurfacePressureDataType
|
||||||
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
|
import de.timklge.karooheadwind.datatypes.TailwindAndRideSpeedDataType
|
||||||
import de.timklge.karooheadwind.datatypes.TailwindDataType
|
import de.timklge.karooheadwind.datatypes.WindDirectionAndSpeedDataType
|
||||||
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.UserWindSpeedDataType
|
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherDataType
|
import de.timklge.karooheadwind.datatypes.WeatherDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
import de.timklge.karooheadwind.datatypes.WeatherForecastDataType
|
||||||
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
import de.timklge.karooheadwind.datatypes.WindDirectionDataType
|
||||||
@ -78,15 +77,15 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
|||||||
WindSpeedDataType(karooSystem, applicationContext),
|
WindSpeedDataType(karooSystem, applicationContext),
|
||||||
TemperatureDataType(karooSystem, applicationContext),
|
TemperatureDataType(karooSystem, applicationContext),
|
||||||
WindDirectionDataType(karooSystem, applicationContext),
|
WindDirectionDataType(karooSystem, applicationContext),
|
||||||
|
WindDirectionAndSpeedDataType(karooSystem, applicationContext),
|
||||||
PrecipitationDataType(karooSystem, applicationContext),
|
PrecipitationDataType(karooSystem, applicationContext),
|
||||||
SurfacePressureDataType(karooSystem, applicationContext),
|
SurfacePressureDataType(karooSystem, applicationContext),
|
||||||
SealevelPressureDataType(karooSystem, applicationContext),
|
SealevelPressureDataType(karooSystem, applicationContext),
|
||||||
UserWindSpeedDataType(karooSystem, applicationContext),
|
|
||||||
TemperatureForecastDataType(karooSystem),
|
TemperatureForecastDataType(karooSystem),
|
||||||
PrecipitationForecastDataType(karooSystem),
|
PrecipitationForecastDataType(karooSystem),
|
||||||
WindForecastDataType(karooSystem),
|
WindForecastDataType(karooSystem),
|
||||||
GraphicalForecastDataType(karooSystem),
|
GraphicalForecastDataType(karooSystem),
|
||||||
TailwindDataType(karooSystem, applicationContext),
|
WindDirectionAndSpeedDataType(karooSystem, applicationContext),
|
||||||
RelativeGradeDataType(karooSystem, applicationContext),
|
RelativeGradeDataType(karooSystem, applicationContext),
|
||||||
RelativeElevationGainDataType(karooSystem, applicationContext),
|
RelativeElevationGainDataType(karooSystem, applicationContext),
|
||||||
)
|
)
|
||||||
@ -169,7 +168,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
|
|||||||
Log.d(TAG, "Minutes to next full hour: ${msToNextFullHour / 1000 / 60}, Distance to next full hour: ${(calculatedDistanceToNextFullHour / 1000).roundToInt()}km")
|
Log.d(TAG, "Minutes to next full hour: ${msToNextFullHour / 1000 / 60}, Distance to next full hour: ${(calculatedDistanceToNextFullHour / 1000).roundToInt()}km")
|
||||||
|
|
||||||
requestedGpsCoordinates = buildList {
|
requestedGpsCoordinates = buildList {
|
||||||
add(gps)
|
add(GpsCoordinates(gps.lat, gps.lon, gps.bearing, distanceAlongRoute = positionOnRoute))
|
||||||
|
|
||||||
var currentPosition = positionOnRoute + calculatedDistanceToNextFullHour
|
var currentPosition = positionOnRoute + calculatedDistanceToNextFullHour
|
||||||
var lastRequestedPosition = positionOnRoute
|
var lastRequestedPosition = positionOnRoute
|
||||||
|
|||||||
@ -4,16 +4,23 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
import de.timklge.karooheadwind.throttle
|
import de.timklge.karooheadwind.throttle
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
import io.hammerhead.karooext.internal.Emitter
|
import io.hammerhead.karooext.internal.Emitter
|
||||||
|
import io.hammerhead.karooext.internal.ViewEmitter
|
||||||
import io.hammerhead.karooext.models.DataPoint
|
import io.hammerhead.karooext.models.DataPoint
|
||||||
import io.hammerhead.karooext.models.DataType
|
import io.hammerhead.karooext.models.DataType
|
||||||
import io.hammerhead.karooext.models.StreamState
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ -22,20 +29,25 @@ abstract class BaseDataType(
|
|||||||
private val applicationContext: Context,
|
private val applicationContext: Context,
|
||||||
dataTypeId: String
|
dataTypeId: String
|
||||||
) : DataTypeImpl("karoo-headwind", dataTypeId) {
|
) : DataTypeImpl("karoo-headwind", dataTypeId) {
|
||||||
abstract fun getValue(data: WeatherData): Double?
|
abstract fun getValue(data: WeatherData, userProfile: UserProfile): Double?
|
||||||
|
|
||||||
|
open fun getFormatDataType(): String? = null
|
||||||
|
|
||||||
override fun startStream(emitter: Emitter<StreamState>) {
|
override fun startStream(emitter: Emitter<StreamState>) {
|
||||||
Log.d(KarooHeadwindExtension.TAG, "start $dataTypeId stream")
|
Log.d(KarooHeadwindExtension.TAG, "start $dataTypeId stream")
|
||||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
val job = CoroutineScope(Dispatchers.IO).launch {
|
||||||
val currentWeatherData = applicationContext.streamCurrentWeatherData(karooSystemService)
|
data class StreamData(val weatherData: WeatherData, val userProfile: UserProfile)
|
||||||
|
|
||||||
|
val currentWeatherData = combine(applicationContext.streamCurrentWeatherData(karooSystemService).filterNotNull(), karooSystemService.streamUserProfile()) { weatherData, userProfile ->
|
||||||
|
StreamData(weatherData, userProfile)
|
||||||
|
}
|
||||||
|
|
||||||
val refreshRate = karooSystemService.getRefreshRateInMilliseconds(applicationContext)
|
val refreshRate = karooSystemService.getRefreshRateInMilliseconds(applicationContext)
|
||||||
|
|
||||||
currentWeatherData
|
currentWeatherData.filterNotNull()
|
||||||
.filterNotNull()
|
|
||||||
.throttle(refreshRate)
|
.throttle(refreshRate)
|
||||||
.collect { data ->
|
.collect { (data, userProfile) ->
|
||||||
val value = getValue(data)
|
val value = getValue(data, userProfile)
|
||||||
Log.d(KarooHeadwindExtension.TAG, "$dataTypeId: $value")
|
Log.d(KarooHeadwindExtension.TAG, "$dataTypeId: $value")
|
||||||
|
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
@ -50,4 +62,12 @@ abstract class BaseDataType(
|
|||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Starting $dataTypeId view with $emitter")
|
||||||
|
|
||||||
|
if (getFormatDataType() != null){
|
||||||
|
emitter.onNext(UpdateGraphicConfig(formatDataTypeId = getFormatDataType()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
|
||||||
class CloudCoverDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "cloudCover"){
|
class CloudCoverDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "cloudCover"){
|
||||||
override fun getValue(data: WeatherData): Double? {
|
override fun getValue(data: WeatherData, userProfile: UserProfile): Double? {
|
||||||
return data.cloudCover
|
return data.cloudCover
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,6 +37,9 @@ import de.timklge.karooheadwind.streamUpcomingRoute
|
|||||||
import de.timklge.karooheadwind.streamUserProfile
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
import de.timklge.karooheadwind.streamWidgetSettings
|
import de.timklge.karooheadwind.streamWidgetSettings
|
||||||
import de.timklge.karooheadwind.throttle
|
import de.timklge.karooheadwind.throttle
|
||||||
|
import de.timklge.karooheadwind.util.celciusInUserUnit
|
||||||
|
import de.timklge.karooheadwind.util.millimetersInUserUnit
|
||||||
|
import de.timklge.karooheadwind.util.msInUserUnit
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
|
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
||||||
@ -116,7 +119,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
WeatherData(
|
WeatherData(
|
||||||
time = forecastTime,
|
time = forecastTime,
|
||||||
temperature = forecastTemperature,
|
temperature = forecastTemperature,
|
||||||
relativeHumidity = 20.0,
|
relativeHumidity = 20,
|
||||||
precipitation = forecastPrecipitation,
|
precipitation = forecastPrecipitation,
|
||||||
cloudCover = 3.0,
|
cloudCover = 3.0,
|
||||||
sealevelPressure = 1013.25,
|
sealevelPressure = 1013.25,
|
||||||
@ -139,7 +142,7 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
current = WeatherData(
|
current = WeatherData(
|
||||||
time = timeAtFullHour,
|
time = timeAtFullHour,
|
||||||
temperature = 20.0,
|
temperature = 20.0,
|
||||||
relativeHumidity = 20.0,
|
relativeHumidity = 20,
|
||||||
precipitation = 0.0,
|
precipitation = 0.0,
|
||||||
cloudCover = 3.0,
|
cloudCover = 3.0,
|
||||||
sealevelPressure = 1013.25,
|
sealevelPressure = 1013.25,
|
||||||
@ -311,11 +314,11 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
arrowBitmap = baseBitmap,
|
arrowBitmap = baseBitmap,
|
||||||
current = interpretation,
|
current = interpretation,
|
||||||
windBearing = data.current.windDirection.roundToInt(),
|
windBearing = data.current.windDirection.roundToInt(),
|
||||||
windSpeed = data.current.windSpeed.roundToInt(),
|
windSpeed = msInUserUnit(data.current.windSpeed, settingsAndProfile.isImperial).roundToInt(),
|
||||||
windGusts = data.current.windGusts.roundToInt(),
|
windGusts = msInUserUnit(data.current.windGusts, settingsAndProfile.isImperial).roundToInt(),
|
||||||
precipitation = data.current.precipitation,
|
precipitation = millimetersInUserUnit(data.current.precipitation, settingsAndProfile.isImperial),
|
||||||
precipitationProbability = null,
|
precipitationProbability = null,
|
||||||
temperature = data.current.temperature.roundToInt(),
|
temperature = celciusInUserUnit(data.current.temperature, settingsAndProfile.isImperialTemperature).roundToInt(),
|
||||||
temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS,
|
temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS,
|
||||||
timeLabel = formattedTime,
|
timeLabel = formattedTime,
|
||||||
dateLabel = if (hasNewDate) formattedDate else null,
|
dateLabel = if (hasNewDate) formattedDate else null,
|
||||||
@ -337,11 +340,11 @@ abstract class ForecastDataType(private val karooSystem: KarooSystemService, typ
|
|||||||
arrowBitmap = baseBitmap,
|
arrowBitmap = baseBitmap,
|
||||||
current = interpretation,
|
current = interpretation,
|
||||||
windBearing = weatherData?.windDirection?.roundToInt() ?: 0,
|
windBearing = weatherData?.windDirection?.roundToInt() ?: 0,
|
||||||
windSpeed = weatherData?.windSpeed?.roundToInt() ?: 0,
|
windSpeed = msInUserUnit(weatherData?.windSpeed ?: 0.0, settingsAndProfile.isImperial).roundToInt(),
|
||||||
windGusts = weatherData?.windGusts?.roundToInt() ?: 0,
|
windGusts = msInUserUnit(weatherData?.windGusts ?: 0.0, settingsAndProfile.isImperial).roundToInt(),
|
||||||
precipitation = weatherData?.precipitation ?: 0.0,
|
precipitation = millimetersInUserUnit(weatherData?.precipitation ?: 0.0, settingsAndProfile.isImperial),
|
||||||
precipitationProbability = weatherData?.precipitationProbability?.toInt(),
|
precipitationProbability = weatherData?.precipitationProbability?.toInt(),
|
||||||
temperature = weatherData?.temperature?.roundToInt() ?: 0,
|
temperature = celciusInUserUnit(weatherData?.temperature ?: 0.0, settingsAndProfile.isImperialTemperature).roundToInt(),
|
||||||
temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS,
|
temperatureUnit = if (settingsAndProfile.isImperialTemperature) TemperatureUnit.FAHRENHEIT else TemperatureUnit.CELSIUS,
|
||||||
timeLabel = formattedTime,
|
timeLabel = formattedTime,
|
||||||
dateLabel = if (hasNewDate) formattedDate else null,
|
dateLabel = if (hasNewDate) formattedDate else null,
|
||||||
|
|||||||
@ -44,7 +44,6 @@ fun GraphicalForecast(
|
|||||||
provider = ImageProvider(getWeatherIcon(current, isNight)),
|
provider = ImageProvider(getWeatherIcon(current, isNight)),
|
||||||
contentDescription = "Current weather information",
|
contentDescription = "Current weather information",
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,14 @@ import androidx.glance.appwidget.GlanceRemoteViews
|
|||||||
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.WindDirectionIndicatorSetting
|
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamDatatypeIsVisible
|
import de.timklge.karooheadwind.streamDatatypeIsVisible
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
import de.timklge.karooheadwind.throttle
|
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.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
import io.hammerhead.karooext.internal.Emitter
|
import io.hammerhead.karooext.internal.Emitter
|
||||||
@ -24,6 +26,7 @@ import io.hammerhead.karooext.models.DataType
|
|||||||
import io.hammerhead.karooext.models.HardwareType
|
import io.hammerhead.karooext.models.HardwareType
|
||||||
import io.hammerhead.karooext.models.StreamState
|
import io.hammerhead.karooext.models.StreamState
|
||||||
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
import io.hammerhead.karooext.models.ViewConfig
|
import io.hammerhead.karooext.models.ViewConfig
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -36,6 +39,7 @@ import kotlinx.coroutines.flow.filter
|
|||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.cos
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
@ -63,7 +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.settings == null || streamData.windSpeed == null){
|
if (value == null || streamData.absoluteWindDirection == null || streamData.windSpeed == null){
|
||||||
var errorCode = 1.0
|
var errorCode = 1.0
|
||||||
var headingResponse = streamData.headingResponse
|
var headingResponse = streamData.headingResponse
|
||||||
|
|
||||||
@ -71,7 +75,7 @@ class HeadwindDirectionDataType(
|
|||||||
headingResponse = HeadingResponse.NoWeatherData
|
headingResponse = HeadingResponse.NoWeatherData
|
||||||
}
|
}
|
||||||
|
|
||||||
if (streamData.settings?.welcomeDialogAccepted == false){
|
if (streamData.settings.welcomeDialogAccepted == false){
|
||||||
errorCode = ERROR_APP_NOT_SET_UP.toDouble()
|
errorCode = ERROR_APP_NOT_SET_UP.toDouble()
|
||||||
} else if (headingResponse is HeadingResponse.NoGps){
|
} else if (headingResponse is HeadingResponse.NoGps){
|
||||||
errorCode = ERROR_NO_GPS.toDouble()
|
errorCode = ERROR_NO_GPS.toDouble()
|
||||||
@ -81,10 +85,7 @@ class HeadwindDirectionDataType(
|
|||||||
|
|
||||||
returnValue = errorCode
|
returnValue = errorCode
|
||||||
} else {
|
} else {
|
||||||
var windDirection = when (streamData.settings.windDirectionIndicatorSetting){
|
var windDirection = value
|
||||||
WindDirectionIndicatorSetting.HEADWIND_DIRECTION -> value
|
|
||||||
WindDirectionIndicatorSetting.WIND_DIRECTION -> streamData.absoluteWindDirection + 180
|
|
||||||
}
|
|
||||||
|
|
||||||
if (windDirection < 0) windDirection += 360
|
if (windDirection < 0) windDirection += 360
|
||||||
|
|
||||||
@ -106,15 +107,25 @@ class HeadwindDirectionDataType(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DirectionAndSpeed(val bearing: Double, val speed: Double?, val isVisible: Boolean)
|
data class DirectionAndSpeed(
|
||||||
|
val bearing: Double,
|
||||||
|
val speed: Double?,
|
||||||
|
val isVisible: Boolean,
|
||||||
|
val isImperial: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
private fun previewFlow(): Flow<DirectionAndSpeed> {
|
private fun previewFlow(): Flow<DirectionAndSpeed> {
|
||||||
return flow {
|
return flow {
|
||||||
while (true) {
|
while (true) {
|
||||||
val bearing = (0..360).random().toDouble()
|
val bearing = (0..360).random().toDouble()
|
||||||
val windSpeed = (0..20).random()
|
val windSpeed = (0..10).random()
|
||||||
|
|
||||||
emit(DirectionAndSpeed(bearing, windSpeed.toDouble(), true))
|
emit(DirectionAndSpeed(
|
||||||
|
bearing,
|
||||||
|
windSpeed.toDouble(),
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
))
|
||||||
|
|
||||||
delay(2_000)
|
delay(2_000)
|
||||||
}
|
}
|
||||||
@ -141,11 +152,11 @@ class HeadwindDirectionDataType(
|
|||||||
val directionFlow = streamValues()
|
val directionFlow = streamValues()
|
||||||
val speedFlow = flow {
|
val speedFlow = flow {
|
||||||
emit(0.0)
|
emit(0.0)
|
||||||
emitAll(UserWindSpeedDataType.streamValues(context, karooSystem))
|
emitAll(streamValues(context, karooSystem))
|
||||||
}
|
}
|
||||||
|
|
||||||
combine(directionFlow, speedFlow, karooSystem.streamDatatypeIsVisible(dataTypeId)) { direction, speed, isVisible ->
|
combine(directionFlow, speedFlow, karooSystem.streamDatatypeIsVisible(dataTypeId), karooSystem.streamUserProfile()) { direction, speed, isVisible, profile ->
|
||||||
DirectionAndSpeed(direction, speed, isVisible)
|
DirectionAndSpeed(direction, speed, isVisible, profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,14 +173,15 @@ class HeadwindDirectionDataType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val windDirection = streamData.bearing
|
val windDirection = streamData.bearing
|
||||||
val windSpeed = streamData.speed
|
val windSpeed = streamData.speed ?: 0.0
|
||||||
|
val windSpeedUserUnit = msInUserUnit(windSpeed, streamData.isImperial)
|
||||||
|
|
||||||
val result = glance.compose(context, DpSize.Unspecified) {
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
HeadwindDirection(
|
HeadwindDirection(
|
||||||
baseBitmap,
|
baseBitmap,
|
||||||
windDirection.roundToInt(),
|
windDirection.roundToInt(),
|
||||||
config.textSize,
|
config.textSize,
|
||||||
windSpeed?.toInt()?.toString() ?: "",
|
windSpeedUserUnit.roundToInt().toString(),
|
||||||
preview = config.preview,
|
preview = config.preview,
|
||||||
wideMode = false
|
wideMode = false
|
||||||
)
|
)
|
||||||
@ -189,6 +201,26 @@ class HeadwindDirectionDataType(
|
|||||||
const val ERROR_NO_GPS = -1
|
const val ERROR_NO_GPS = -1
|
||||||
const val ERROR_NO_WEATHER_DATA = -2
|
const val ERROR_NO_WEATHER_DATA = -2
|
||||||
const val ERROR_APP_NOT_SET_UP = -3
|
const val ERROR_APP_NOT_SET_UP = -3
|
||||||
|
|
||||||
|
fun streamValues(context: Context, karooSystem: KarooSystemService): Flow<Double> = flow {
|
||||||
|
data class StreamData(
|
||||||
|
val headingResponse: HeadingResponse,
|
||||||
|
val weatherResponse: WeatherData?,
|
||||||
|
val settings: HeadwindSettings
|
||||||
|
)
|
||||||
|
|
||||||
|
combine(karooSystem.getRelativeHeadingFlow(context), context.streamCurrentWeatherData(karooSystem), context.streamSettings(karooSystem)) { headingResponse, weatherResponse, settings ->
|
||||||
|
StreamData(headingResponse, weatherResponse, settings)
|
||||||
|
}.filter { it.weatherResponse != null }
|
||||||
|
.collect { streamData ->
|
||||||
|
val windSpeed = streamData.weatherResponse?.windSpeed ?: 0.0
|
||||||
|
val windDirection = (streamData.headingResponse as? HeadingResponse.Value)?.diff ?: 0.0
|
||||||
|
|
||||||
|
val headwindSpeed = cos((windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||||
|
|
||||||
|
emit(headwindSpeed)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,9 +11,12 @@ import de.timklge.karooheadwind.throttle
|
|||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
import io.hammerhead.karooext.internal.Emitter
|
import io.hammerhead.karooext.internal.Emitter
|
||||||
|
import io.hammerhead.karooext.internal.ViewEmitter
|
||||||
import io.hammerhead.karooext.models.DataPoint
|
import io.hammerhead.karooext.models.DataPoint
|
||||||
import io.hammerhead.karooext.models.DataType
|
import io.hammerhead.karooext.models.DataType
|
||||||
import io.hammerhead.karooext.models.StreamState
|
import io.hammerhead.karooext.models.StreamState
|
||||||
|
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
||||||
|
import io.hammerhead.karooext.models.ViewConfig
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@ -49,5 +52,9 @@ class HeadwindSpeedDataType(
|
|||||||
job.cancel()
|
job.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||||
|
emitter.onNext(UpdateGraphicConfig(formatDataTypeId = DataType.Type.SPEED))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,297 @@
|
|||||||
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.glance.GlanceModifier
|
||||||
|
import androidx.glance.Image
|
||||||
|
import androidx.glance.ImageProvider
|
||||||
|
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||||
|
import androidx.glance.appwidget.GlanceRemoteViews
|
||||||
|
import androidx.glance.layout.Box
|
||||||
|
import androidx.glance.layout.fillMaxSize
|
||||||
|
import de.timklge.karooheadwind.HeadingResponse
|
||||||
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
|
import de.timklge.karooheadwind.HeadwindWidgetSettings
|
||||||
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
|
import de.timklge.karooheadwind.UpcomingRoute
|
||||||
|
import de.timklge.karooheadwind.WeatherDataProvider
|
||||||
|
import de.timklge.karooheadwind.getHeadingFlow
|
||||||
|
import de.timklge.karooheadwind.screens.LineGraphBuilder
|
||||||
|
import de.timklge.karooheadwind.streamCurrentForecastWeatherData
|
||||||
|
import de.timklge.karooheadwind.streamDatatypeIsVisible
|
||||||
|
import de.timklge.karooheadwind.streamSettings
|
||||||
|
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||||
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
|
import de.timklge.karooheadwind.streamWidgetSettings
|
||||||
|
import de.timklge.karooheadwind.throttle
|
||||||
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
|
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
|
||||||
|
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
||||||
|
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||||
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
|
import io.hammerhead.karooext.internal.ViewEmitter
|
||||||
|
import io.hammerhead.karooext.models.ShowCustomStreamState
|
||||||
|
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.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.floor
|
||||||
|
|
||||||
|
abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemService, typeId: String) : DataTypeImpl("karoo-headwind", typeId) {
|
||||||
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
|
private val glance = GlanceRemoteViews()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.of("UTC"))
|
||||||
|
}
|
||||||
|
|
||||||
|
data class StreamData(val data: WeatherDataResponse?, val settings: SettingsAndProfile,
|
||||||
|
val widgetSettings: HeadwindWidgetSettings? = null,
|
||||||
|
val headingResponse: HeadingResponse? = null, val upcomingRoute: UpcomingRoute? = null, val isVisible: Boolean)
|
||||||
|
|
||||||
|
data class SettingsAndProfile(val settings: HeadwindSettings, val isImperial: Boolean, val isImperialTemperature: Boolean)
|
||||||
|
|
||||||
|
data class LineData(val time: Instant? = null, val distance: Float? = null, val weatherData: WeatherData)
|
||||||
|
|
||||||
|
abstract fun getLineData(
|
||||||
|
lineData: List<LineData>,
|
||||||
|
isImperial: Boolean,
|
||||||
|
upcomingRoute: UpcomingRoute?
|
||||||
|
): Set<LineGraphBuilder.Line>
|
||||||
|
|
||||||
|
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> =
|
||||||
|
flow {
|
||||||
|
val settingsAndProfile = settingsAndProfileStream.firstOrNull()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val data = (0..<10).map { index ->
|
||||||
|
val timeAtFullHour = Instant.now().truncatedTo(ChronoUnit.HOURS).epochSecond
|
||||||
|
|
||||||
|
val weatherData = (0..<12).map {
|
||||||
|
val forecastTime = timeAtFullHour + it * 60 * 60
|
||||||
|
val forecastTemperature = 20.0 + (-20..20).random()
|
||||||
|
val forecastPrecipitation = 0.0 + (0..10).random()
|
||||||
|
val forecastPrecipitationProbability = (0..100).random()
|
||||||
|
val forecastWeatherCode = WeatherInterpretation.getKnownWeatherCodes().random()
|
||||||
|
val forecastWindSpeed = 0.0 + (0..10).random()
|
||||||
|
val forecastWindDirection = 0.0 + (0..360).random()
|
||||||
|
val forecastWindGusts = 0.0 + (0..10).random()
|
||||||
|
WeatherData(
|
||||||
|
time = forecastTime,
|
||||||
|
temperature = forecastTemperature,
|
||||||
|
relativeHumidity = 20,
|
||||||
|
precipitation = forecastPrecipitation,
|
||||||
|
cloudCover = 3.0,
|
||||||
|
sealevelPressure = 1013.25,
|
||||||
|
surfacePressure = 1013.25,
|
||||||
|
precipitationProbability = forecastPrecipitationProbability.toDouble(),
|
||||||
|
windSpeed = forecastWindSpeed,
|
||||||
|
windDirection = forecastWindDirection,
|
||||||
|
windGusts = forecastWindGusts,
|
||||||
|
weatherCode = forecastWeatherCode,
|
||||||
|
isForecast = true,
|
||||||
|
isNight = it < 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val distancePerHour =
|
||||||
|
settingsAndProfile?.settings?.getForecastMetersPerHour(settingsAndProfile.isImperial)
|
||||||
|
?.toDouble() ?: 0.0
|
||||||
|
|
||||||
|
WeatherDataForLocation(
|
||||||
|
current = WeatherData(
|
||||||
|
time = timeAtFullHour,
|
||||||
|
temperature = 20.0,
|
||||||
|
relativeHumidity = 20,
|
||||||
|
precipitation = 0.0,
|
||||||
|
cloudCover = 3.0,
|
||||||
|
sealevelPressure = 1013.25,
|
||||||
|
surfacePressure = 1013.25,
|
||||||
|
windSpeed = 5.0,
|
||||||
|
windDirection = 180.0,
|
||||||
|
windGusts = 10.0,
|
||||||
|
weatherCode = WeatherInterpretation.getKnownWeatherCodes().random(),
|
||||||
|
isForecast = false,
|
||||||
|
isNight = false
|
||||||
|
),
|
||||||
|
coords = GpsCoordinates(0.0, 0.0, distanceAlongRoute = index * distancePerHour),
|
||||||
|
timezone = "UTC",
|
||||||
|
elevation = null,
|
||||||
|
forecasts = weatherData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
emit(
|
||||||
|
StreamData(
|
||||||
|
WeatherDataResponse(provider = WeatherDataProvider.OPEN_METEO, data = data),
|
||||||
|
SettingsAndProfile(
|
||||||
|
HeadwindSettings(),
|
||||||
|
settingsAndProfile?.isImperial == true,
|
||||||
|
settingsAndProfile?.isImperialTemperature == true
|
||||||
|
),
|
||||||
|
isVisible = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
delay(5_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
|
override fun startView(context: Context, config: ViewConfig, emitter: ViewEmitter) {
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Starting weather forecast view with $emitter")
|
||||||
|
val configJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
emitter.onNext(UpdateGraphicConfig(showHeader = false))
|
||||||
|
awaitCancellation()
|
||||||
|
}
|
||||||
|
|
||||||
|
val settingsAndProfileStream = context.streamSettings(karooSystem).combine(karooSystem.streamUserProfile()) { settings, userProfile ->
|
||||||
|
SettingsAndProfile(settings = settings, isImperial = userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL,
|
||||||
|
isImperialTemperature = userProfile.preferredUnit.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataFlow = if (config.preview){
|
||||||
|
previewFlow(settingsAndProfileStream)
|
||||||
|
} else {
|
||||||
|
combine(
|
||||||
|
context.streamCurrentForecastWeatherData(),
|
||||||
|
settingsAndProfileStream,
|
||||||
|
context.streamWidgetSettings(),
|
||||||
|
karooSystem.getHeadingFlow(context).throttle(3 * 60_000L),
|
||||||
|
karooSystem.streamUpcomingRoute().distinctUntilChanged { old, new ->
|
||||||
|
val oldDistance = old?.distanceAlongRoute
|
||||||
|
val newDistance = new?.distanceAlongRoute
|
||||||
|
|
||||||
|
if (oldDistance == null && newDistance == null) return@distinctUntilChanged true
|
||||||
|
if (oldDistance == null || newDistance == null) return@distinctUntilChanged false
|
||||||
|
|
||||||
|
abs(oldDistance - newDistance) < 1_000
|
||||||
|
},
|
||||||
|
karooSystem.streamDatatypeIsVisible(dataTypeId)
|
||||||
|
) { data ->
|
||||||
|
val weatherData = data[0] as WeatherDataResponse?
|
||||||
|
val settings = data[1] as SettingsAndProfile
|
||||||
|
val widgetSettings = data[2] as HeadwindWidgetSettings?
|
||||||
|
val heading = data[3] as HeadingResponse?
|
||||||
|
val upcomingRoute = data[4] as UpcomingRoute?
|
||||||
|
val isVisible = data[5] as Boolean
|
||||||
|
|
||||||
|
StreamData(
|
||||||
|
data = weatherData,
|
||||||
|
settings = settings,
|
||||||
|
widgetSettings = widgetSettings,
|
||||||
|
headingResponse = heading,
|
||||||
|
upcomingRoute = upcomingRoute,
|
||||||
|
isVisible = isVisible
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
emitter.onNext(ShowCustomStreamState("", null))
|
||||||
|
|
||||||
|
dataFlow.filter { it.isVisible }.collect { (allData, settingsAndProfile, widgetSettings, headingResponse, actualUpcomingRoute) ->
|
||||||
|
val upcomingRoute = if (allData?.provider == WeatherDataProvider.OPEN_METEO) actualUpcomingRoute else null
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")
|
||||||
|
|
||||||
|
if (allData?.data.isNullOrEmpty()){
|
||||||
|
emitter.updateView(
|
||||||
|
getErrorWidget(
|
||||||
|
glance,
|
||||||
|
context,
|
||||||
|
settingsAndProfile.settings,
|
||||||
|
headingResponse
|
||||||
|
).remoteViews)
|
||||||
|
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
|
val data = buildList {
|
||||||
|
for(i in 0..<12){
|
||||||
|
val locationData = if (upcomingRoute != null){
|
||||||
|
allData?.data?.getOrNull(i)
|
||||||
|
} else {
|
||||||
|
allData?.data?.firstOrNull()
|
||||||
|
}
|
||||||
|
val data = if (i == 0){
|
||||||
|
locationData?.current
|
||||||
|
} else {
|
||||||
|
locationData?.forecasts?.getOrNull(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
Log.w(KarooHeadwindExtension.TAG, "No weather data available for forecast index $i")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val time = Instant.ofEpochSecond(data.time)
|
||||||
|
|
||||||
|
add(LineData(
|
||||||
|
time = time,
|
||||||
|
distance = locationData?.coords?.distanceAlongRoute?.toFloat(),
|
||||||
|
weatherData = data,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pointData = getLineData(data, settingsAndProfile.isImperialTemperature, upcomingRoute)
|
||||||
|
val bitmap = LineGraphBuilder(context).drawLineGraph(config.viewSize.first, config.viewSize.second, config.gridSize.first, config.gridSize.second, pointData) { x ->
|
||||||
|
val startTime = data.firstOrNull()?.time
|
||||||
|
val time = startTime?.plus(floor(x).toLong(), ChronoUnit.HOURS)
|
||||||
|
val timeLabel = timeFormatter.format(time)
|
||||||
|
val beforeData = data.getOrNull(floor(x).toInt().coerceAtLeast(0))
|
||||||
|
val afterData = data.getOrNull(ceil(x).toInt().coerceAtMost(data.size - 1))
|
||||||
|
|
||||||
|
if (beforeData?.distance != null || afterData?.distance != null) {
|
||||||
|
val start = beforeData?.distance ?: 0.0f
|
||||||
|
val end = (afterData?.distance ?: upcomingRoute?.routeLength?.toFloat()) ?: 0.0f
|
||||||
|
val distance = start + (end - start) * (x - floor(x))
|
||||||
|
val distanceLabel = if (settingsAndProfile.isImperial) {
|
||||||
|
"${(distance * 0.000621371).toInt()}mi"
|
||||||
|
} else {
|
||||||
|
"${(distance / 1000).toInt()}km"
|
||||||
|
}
|
||||||
|
return@drawLineGraph distanceLabel
|
||||||
|
} else {
|
||||||
|
timeLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = GlanceModifier.fillMaxSize()){
|
||||||
|
Image(ImageProvider(bitmap), "Forecast", modifier = GlanceModifier.fillMaxSize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.updateView(result.remoteViews)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitter.setCancellable {
|
||||||
|
Log.d(
|
||||||
|
KarooHeadwindExtension.TAG,
|
||||||
|
"Stopping headwind weather forecast view with $emitter"
|
||||||
|
)
|
||||||
|
configJob.cancel()
|
||||||
|
viewJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,13 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import de.timklge.karooheadwind.util.millimetersInUserUnit
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
|
||||||
class PrecipitationDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "precipitation"){
|
class PrecipitationDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "precipitation"){
|
||||||
override fun getValue(data: WeatherData): Double {
|
override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
|
||||||
return data.precipitation
|
return millimetersInUserUnit(data.precipitation, userProfile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,108 +1,45 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import de.timklge.karooheadwind.UpcomingRoute
|
||||||
import androidx.compose.runtime.Composable
|
import de.timklge.karooheadwind.screens.LineGraphBuilder
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.TextUnitType
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.glance.GlanceModifier
|
|
||||||
import androidx.glance.color.ColorProvider
|
|
||||||
import androidx.glance.layout.Alignment
|
|
||||||
import androidx.glance.layout.Column
|
|
||||||
import androidx.glance.layout.Row
|
|
||||||
import androidx.glance.layout.fillMaxHeight
|
|
||||||
import androidx.glance.layout.fillMaxWidth
|
|
||||||
import androidx.glance.layout.padding
|
|
||||||
import androidx.glance.layout.width
|
|
||||||
import androidx.glance.text.FontFamily
|
|
||||||
import androidx.glance.text.FontWeight
|
|
||||||
import androidx.glance.text.Text
|
|
||||||
import androidx.glance.text.TextAlign
|
|
||||||
import androidx.glance.text.TextStyle
|
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
@Composable
|
class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastDataType(karooSystem, "precipitationForecast") {
|
||||||
fun PrecipitationForecast(
|
override fun getLineData(
|
||||||
precipitation: Int,
|
lineData: List<LineData>,
|
||||||
precipitationProbability: Int?,
|
|
||||||
distance: Double? = null,
|
|
||||||
timeLabel: String? = null,
|
|
||||||
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
|
||||||
isImperial: Boolean?,
|
|
||||||
) {
|
|
||||||
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
|
||||||
Row(modifier = GlanceModifier.defaultWeight().fillMaxWidth(), horizontalAlignment = rowAlignment, verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
val precipitationProbabilityText = if (precipitationProbability != null) "${precipitationProbability}% " else ""
|
|
||||||
val precipitationText = precipitation.toString()
|
|
||||||
Text(
|
|
||||||
text = "${precipitationProbabilityText}${precipitationText}",
|
|
||||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(24f, TextUnitType.Sp), textAlign = TextAlign.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (distance != null && isImperial != null){
|
|
||||||
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
|
||||||
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
|
||||||
val text = if(distanceInUserUnit > 0){
|
|
||||||
"In $label"
|
|
||||||
} else {
|
|
||||||
"$label ago"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (distanceInUserUnit != 0){
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = TextStyle(
|
|
||||||
color = ColorProvider(Color.Black, Color.White),
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = TextUnit(18f, TextUnitType.Sp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeLabel != null){
|
|
||||||
Text(
|
|
||||||
text = timeLabel,
|
|
||||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
|
||||||
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PrecipitationForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "precipitationForecast") {
|
|
||||||
@Composable
|
|
||||||
override fun RenderWidget(
|
|
||||||
arrowBitmap: Bitmap,
|
|
||||||
current: WeatherInterpretation,
|
|
||||||
windBearing: Int,
|
|
||||||
windSpeed: Int,
|
|
||||||
windGusts: Int,
|
|
||||||
precipitation: Double,
|
|
||||||
precipitationProbability: Int?,
|
|
||||||
temperature: Int,
|
|
||||||
temperatureUnit: TemperatureUnit,
|
|
||||||
timeLabel: String,
|
|
||||||
dateLabel: String?,
|
|
||||||
distance: Double?,
|
|
||||||
isImperial: Boolean,
|
isImperial: Boolean,
|
||||||
isNight: Boolean,
|
upcomingRoute: UpcomingRoute?
|
||||||
) {
|
): Set<LineGraphBuilder.Line> {
|
||||||
PrecipitationForecast(
|
val precipitationPoints = lineData.map { data ->
|
||||||
precipitation = ceil(precipitation).toInt(),
|
if (isImperial) { // Convert mm to inches
|
||||||
precipitationProbability = precipitationProbability,
|
data.weatherData.precipitation * 0.0393701 // Convert mm to inches
|
||||||
distance = distance,
|
} else {
|
||||||
timeLabel = timeLabel,
|
data.weatherData.precipitation
|
||||||
isImperial = isImperial,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val precipitationPropagation = lineData.map { data ->
|
||||||
|
data.weatherData.precipitationProbability ?: 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return setOf(
|
||||||
|
LineGraphBuilder.Line(
|
||||||
|
dataPoints = precipitationPoints.mapIndexed { index, value ->
|
||||||
|
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
||||||
|
},
|
||||||
|
color = android.graphics.Color.BLUE,
|
||||||
|
label = if (!isImperial) "mm" else "in",
|
||||||
|
),
|
||||||
|
|
||||||
|
LineGraphBuilder.Line(
|
||||||
|
dataPoints = precipitationPropagation.mapIndexed { index, value ->
|
||||||
|
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
||||||
|
},
|
||||||
|
color = android.graphics.Color.CYAN,
|
||||||
|
label = "%",
|
||||||
|
yAxis = LineGraphBuilder.YAxis.RIGHT
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -129,30 +129,9 @@ class RelativeGradeDataType(private val karooSystemService: KarooSystemService,
|
|||||||
|
|
||||||
val refreshRate = karooSystemService.getRefreshRateInMilliseconds(context)
|
val refreshRate = karooSystemService.getRefreshRateInMilliseconds(context)
|
||||||
|
|
||||||
val windSpeedFlow = combine(context.streamSettings(karooSystemService), karooSystemService.streamUserProfile(), context.streamCurrentWeatherData(karooSystemService).filterNotNull()) { settings, profile, weatherData ->
|
val windSpeedFlow = context.streamCurrentWeatherData(karooSystemService).filterNotNull().map { weatherData ->
|
||||||
val isOpenMeteo = settings.weatherProvider == WeatherDataProvider.OPEN_METEO
|
|
||||||
val profileIsImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
|
||||||
|
|
||||||
if (isOpenMeteo) {
|
|
||||||
if (profileIsImperial) { // OpenMeteo returns wind speed in mph
|
|
||||||
val windSpeedInMilesPerHour = weatherData.windSpeed
|
|
||||||
|
|
||||||
windSpeedInMilesPerHour * 0.44704
|
|
||||||
} else { // Wind speed reported by openmeteo is in km/h
|
|
||||||
val windSpeedInKmh = weatherData.windSpeed
|
|
||||||
|
|
||||||
windSpeedInKmh * 0.277778
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (profileIsImperial) { // OpenWeatherMap returns wind speed in mph
|
|
||||||
val windSpeedInMilesPerHour = weatherData.windSpeed
|
|
||||||
|
|
||||||
windSpeedInMilesPerHour * 0.44704
|
|
||||||
} else { // Wind speed reported by openweathermap is in m/s
|
|
||||||
weatherData.windSpeed
|
weatherData.windSpeed
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class StreamValues(
|
data class StreamValues(
|
||||||
val relativeWindDirection: Double,
|
val relativeWindDirection: Double,
|
||||||
|
|||||||
@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
|
||||||
class RelativeHumidityDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "relativeHumidity"){
|
class RelativeHumidityDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "relativeHumidity"){
|
||||||
override fun getValue(data: WeatherData): Double? {
|
override fun getValue(data: WeatherData, userProfile: UserProfile): Double? {
|
||||||
return data.relativeHumidity
|
return data.relativeHumidity.toDouble()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
|
||||||
class SealevelPressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "sealevelPressure"){
|
class SealevelPressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "sealevelPressure"){
|
||||||
override fun getValue(data: WeatherData): Double? {
|
override fun getValue(data: WeatherData, userProfile: UserProfile): Double? {
|
||||||
return data.sealevelPressure
|
return data.sealevelPressure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
|
||||||
class SurfacePressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "surfacePressure"){
|
class SurfacePressureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "surfacePressure"){
|
||||||
override fun getValue(data: WeatherData): Double? {
|
override fun getValue(data: WeatherData, userProfile: UserProfile): Double? {
|
||||||
return data.surfacePressure
|
return data.surfacePressure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -15,8 +15,6 @@ 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.R
|
import de.timklge.karooheadwind.R
|
||||||
import de.timklge.karooheadwind.WindDirectionIndicatorSetting
|
|
||||||
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
|
|
||||||
import de.timklge.karooheadwind.datatypes.TailwindDataType.StreamData
|
import de.timklge.karooheadwind.datatypes.TailwindDataType.StreamData
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
@ -25,6 +23,7 @@ import de.timklge.karooheadwind.streamDatatypeIsVisible
|
|||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import de.timklge.karooheadwind.streamUserProfile
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
import de.timklge.karooheadwind.throttle
|
import de.timklge.karooheadwind.throttle
|
||||||
|
import de.timklge.karooheadwind.util.msInUserUnit
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
@ -82,8 +81,8 @@ class TailwindAndRideSpeedDataType(
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val bearing = (0..360).random().toDouble()
|
val bearing = (0..360).random().toDouble()
|
||||||
val windSpeed = (0..20).random()
|
val windSpeed = (0..10).random()
|
||||||
val rideSpeed = (10..40).random().toDouble()
|
val rideSpeed = (5..10).random().toDouble()
|
||||||
val gustSpeed = windSpeed * ((10..40).random().toDouble() / 10)
|
val gustSpeed = windSpeed * ((10..40).random().toDouble() / 10)
|
||||||
val isImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
val isImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||||
|
|
||||||
@ -133,11 +132,7 @@ class TailwindAndRideSpeedDataType(
|
|||||||
val absoluteWindDirection = weatherData?.windDirection
|
val absoluteWindDirection = weatherData?.windDirection
|
||||||
val windSpeed = weatherData?.windSpeed
|
val windSpeed = weatherData?.windSpeed
|
||||||
val gustSpeed = weatherData?.windGusts
|
val gustSpeed = weatherData?.windGusts
|
||||||
val rideSpeed = if (isImperial){
|
val rideSpeed = rideSpeedInMs
|
||||||
rideSpeedInMs * 2.23694
|
|
||||||
} else {
|
|
||||||
rideSpeedInMs * 3.6
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, rideSpeed = rideSpeed, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible)
|
StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, rideSpeed = rideSpeed, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible)
|
||||||
}
|
}
|
||||||
@ -164,47 +159,44 @@ class TailwindAndRideSpeedDataType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val windSpeed = streamData.windSpeed
|
val windSpeed = streamData.windSpeed
|
||||||
val windDirection = when (streamData.settings.windDirectionIndicatorSetting){
|
val windDirection = streamData.headingResponse.diff
|
||||||
WindDirectionIndicatorSetting.HEADWIND_DIRECTION -> streamData.headingResponse.diff
|
|
||||||
WindDirectionIndicatorSetting.WIND_DIRECTION -> streamData.absoluteWindDirection + 180
|
|
||||||
}
|
|
||||||
|
|
||||||
val text = streamData.rideSpeed?.let { String.format(Locale.current.platformLocale, "%.1f", it) } ?: ""
|
val rideSpeedInUserUnit = msInUserUnit(streamData.rideSpeed ?: 0.0, streamData.isImperial)
|
||||||
|
val text = String.format(Locale.current.platformLocale, "%.1f", rideSpeedInUserUnit)
|
||||||
|
|
||||||
val wideMode = config.gridSize.first == 60
|
val wideMode = config.gridSize.first == 60
|
||||||
|
|
||||||
|
val gustSpeedInUserUnit = msInUserUnit(streamData.gustSpeed ?: 0.0, streamData.isImperial)
|
||||||
|
|
||||||
val gustSpeedAddon = if (wideMode) {
|
val gustSpeedAddon = if (wideMode) {
|
||||||
"-${streamData.gustSpeed?.roundToInt() ?: 0}"
|
"-${gustSpeedInUserUnit.roundToInt()}"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
val subtextWithSign = when (streamData.settings.windDirectionIndicatorTextSetting) {
|
val windSpeedUserUnit = msInUserUnit(windSpeed, streamData.isImperial)
|
||||||
WindDirectionIndicatorTextSetting.HEADWIND_SPEED -> {
|
|
||||||
|
val subtextWithSign = let {
|
||||||
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||||
headwindSpeed.roundToInt().toString()
|
headwindSpeed.roundToInt().toString()
|
||||||
|
|
||||||
val sign = if (headwindSpeed < 0) "+" else {
|
val sign = if (headwindSpeed < 0) "+" else {
|
||||||
if (headwindSpeed > 0) "-" else ""
|
if (headwindSpeed > 0) "-" else ""
|
||||||
}
|
}
|
||||||
"$sign${headwindSpeed.roundToInt().absoluteValue} ${windSpeed.roundToInt()}${gustSpeedAddon}"
|
|
||||||
}
|
val headwindSpeedUserUnit = msInUserUnit(headwindSpeed, streamData.isImperial)
|
||||||
WindDirectionIndicatorTextSetting.WIND_SPEED -> "${windSpeed.roundToInt()}${gustSpeedAddon}"
|
|
||||||
WindDirectionIndicatorTextSetting.NONE -> ""
|
"$sign${headwindSpeedUserUnit.roundToInt().absoluteValue} ${windSpeedUserUnit.roundToInt()}${gustSpeedAddon}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var dayColor = Color(ContextCompat.getColor(context, R.color.black))
|
var dayColor = Color(ContextCompat.getColor(context, R.color.black))
|
||||||
var nightColor = Color(ContextCompat.getColor(context, R.color.white))
|
var nightColor = Color(ContextCompat.getColor(context, R.color.white))
|
||||||
|
|
||||||
if (streamData.settings.windDirectionIndicatorSetting == WindDirectionIndicatorSetting.HEADWIND_DIRECTION) {
|
|
||||||
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||||
val windSpeedInKmh = if (streamData.isImperial == true){
|
val windSpeedInKmh = headwindSpeed * 3.6
|
||||||
headwindSpeed / 2.23694 * 3.6
|
|
||||||
} else {
|
|
||||||
headwindSpeed
|
|
||||||
}
|
|
||||||
dayColor = interpolateWindColor(windSpeedInKmh, false, context)
|
dayColor = interpolateWindColor(windSpeedInKmh, false, context)
|
||||||
nightColor = interpolateWindColor(windSpeedInKmh, true, context)
|
nightColor = interpolateWindColor(windSpeedInKmh, true, context)
|
||||||
}
|
|
||||||
|
|
||||||
val result = glance.compose(context, DpSize.Unspecified) {
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
HeadwindDirection(
|
HeadwindDirection(
|
||||||
|
|||||||
@ -3,17 +3,13 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
|
||||||
import androidx.glance.appwidget.GlanceRemoteViews
|
import androidx.glance.appwidget.GlanceRemoteViews
|
||||||
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.R
|
import de.timklge.karooheadwind.R
|
||||||
import de.timklge.karooheadwind.WindDirectionIndicatorSetting
|
|
||||||
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
|
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
import de.timklge.karooheadwind.streamCurrentWeatherData
|
||||||
import de.timklge.karooheadwind.streamDataFlow
|
import de.timklge.karooheadwind.streamDataFlow
|
||||||
@ -21,6 +17,7 @@ import de.timklge.karooheadwind.streamDatatypeIsVisible
|
|||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import de.timklge.karooheadwind.streamUserProfile
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
import de.timklge.karooheadwind.throttle
|
import de.timklge.karooheadwind.throttle
|
||||||
|
import de.timklge.karooheadwind.util.msInUserUnit
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
@ -68,8 +65,8 @@ class TailwindDataType(
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val bearing = (0..360).random().toDouble()
|
val bearing = (0..360).random().toDouble()
|
||||||
val windSpeed = (0..20).random()
|
val windSpeed = (0..10).random()
|
||||||
val rideSpeed = (10..40).random().toDouble()
|
val rideSpeed = (5..10).random().toDouble()
|
||||||
val gustSpeed = windSpeed * ((10..40).random().toDouble() / 10)
|
val gustSpeed = windSpeed * ((10..40).random().toDouble() / 10)
|
||||||
val isImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
val isImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL
|
||||||
|
|
||||||
@ -120,13 +117,8 @@ class TailwindDataType(
|
|||||||
val absoluteWindDirection = weatherData?.windDirection
|
val absoluteWindDirection = weatherData?.windDirection
|
||||||
val windSpeed = weatherData?.windSpeed
|
val windSpeed = weatherData?.windSpeed
|
||||||
val gustSpeed = weatherData?.windGusts
|
val gustSpeed = weatherData?.windGusts
|
||||||
val rideSpeed = if (isImperial){
|
|
||||||
rideSpeedInMs * 2.23694
|
|
||||||
} else {
|
|
||||||
rideSpeedInMs * 3.6
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, rideSpeed = rideSpeed, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible)
|
StreamData(headingResponse, absoluteWindDirection, windSpeed, settings, rideSpeed = rideSpeedInMs, isImperial = isImperial, gustSpeed = gustSpeed, isVisible = isVisible)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,40 +143,28 @@ class TailwindDataType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val windSpeed = streamData.windSpeed
|
val windSpeed = streamData.windSpeed
|
||||||
val windDirection = when (streamData.settings.windDirectionIndicatorSetting){
|
val windDirection = streamData.headingResponse.diff
|
||||||
WindDirectionIndicatorSetting.HEADWIND_DIRECTION -> streamData.headingResponse.diff
|
|
||||||
WindDirectionIndicatorSetting.WIND_DIRECTION -> streamData.absoluteWindDirection + 180
|
|
||||||
}
|
|
||||||
|
|
||||||
val mainText = when (streamData.settings.windDirectionIndicatorTextSetting) {
|
val mainText = let {
|
||||||
WindDirectionIndicatorTextSetting.HEADWIND_SPEED -> {
|
|
||||||
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||||
headwindSpeed.roundToInt().toString()
|
headwindSpeed.roundToInt().toString()
|
||||||
|
|
||||||
val sign = if (headwindSpeed < 0) "+" else {
|
val sign = if (headwindSpeed < 0) "+" else {
|
||||||
if (headwindSpeed > 0) "-" else ""
|
if (headwindSpeed > 0) "-" else ""
|
||||||
}
|
}
|
||||||
"$sign${headwindSpeed.roundToInt().absoluteValue}"
|
|
||||||
}
|
val headwindSpeedUserUnit = msInUserUnit(headwindSpeed, streamData.isImperial)
|
||||||
WindDirectionIndicatorTextSetting.WIND_SPEED -> windSpeed.roundToInt().toString()
|
|
||||||
WindDirectionIndicatorTextSetting.NONE -> ""
|
"$sign${headwindSpeedUserUnit.roundToInt().absoluteValue}"
|
||||||
}
|
}
|
||||||
|
|
||||||
val subtext = "${windSpeed.roundToInt()}-${streamData.gustSpeed?.roundToInt()}"
|
val windSpeedUserUnit = msInUserUnit(windSpeed, streamData.isImperial)
|
||||||
|
val gustSpeedUserUnit = msInUserUnit(streamData.gustSpeed ?: 0.0, streamData.isImperial)
|
||||||
|
|
||||||
var dayColor = Color(ContextCompat.getColor(context, R.color.black))
|
val subtext = "${windSpeedUserUnit.roundToInt()}-${gustSpeedUserUnit.roundToInt()}"
|
||||||
var nightColor = Color(ContextCompat.getColor(context, R.color.white))
|
|
||||||
|
|
||||||
if (streamData.settings.windDirectionIndicatorSetting == WindDirectionIndicatorSetting.HEADWIND_DIRECTION) {
|
|
||||||
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||||
val windSpeedInKmh = if (streamData.isImperial){
|
val windSpeedInKmh = headwindSpeed * 3.6
|
||||||
headwindSpeed / 2.23694 * 3.6
|
|
||||||
} else {
|
|
||||||
headwindSpeed
|
|
||||||
}
|
|
||||||
dayColor = interpolateWindColor(windSpeedInKmh, false, context)
|
|
||||||
nightColor = interpolateWindColor(windSpeedInKmh, true, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = glance.compose(context, DpSize.Unspecified) {
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
HeadwindDirection(
|
HeadwindDirection(
|
||||||
@ -193,8 +173,8 @@ class TailwindDataType(
|
|||||||
config.textSize,
|
config.textSize,
|
||||||
mainText,
|
mainText,
|
||||||
subtext,
|
subtext,
|
||||||
dayColor,
|
interpolateWindColor(windSpeedInKmh, false, context),
|
||||||
nightColor,
|
interpolateWindColor(windSpeedInKmh, true, context),
|
||||||
wideMode = config.gridSize.first == 60,
|
wideMode = config.gridSize.first == 60,
|
||||||
preview = config.preview,
|
preview = config.preview,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,9 +3,15 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.DataType
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
|
||||||
class TemperatureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "temperature"){
|
class TemperatureDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "temperature"){
|
||||||
override fun getValue(data: WeatherData): Double {
|
override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
|
||||||
return data.temperature
|
return data.temperature
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getFormatDataType(): String? {
|
||||||
|
return DataType.Type.TEMPERATURE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,106 +1,33 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import de.timklge.karooheadwind.UpcomingRoute
|
||||||
import androidx.compose.runtime.Composable
|
import de.timklge.karooheadwind.screens.LineGraphBuilder
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.TextUnitType
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.glance.GlanceModifier
|
|
||||||
import androidx.glance.color.ColorProvider
|
|
||||||
import androidx.glance.layout.Alignment
|
|
||||||
import androidx.glance.layout.Column
|
|
||||||
import androidx.glance.layout.Row
|
|
||||||
import androidx.glance.layout.fillMaxHeight
|
|
||||||
import androidx.glance.layout.fillMaxWidth
|
|
||||||
import androidx.glance.layout.padding
|
|
||||||
import androidx.glance.layout.width
|
|
||||||
import androidx.glance.text.FontFamily
|
|
||||||
import androidx.glance.text.FontWeight
|
|
||||||
import androidx.glance.text.Text
|
|
||||||
import androidx.glance.text.TextAlign
|
|
||||||
import androidx.glance.text.TextStyle
|
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
|
|
||||||
@Composable
|
class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastDataType(karooSystem, "temperatureForecast") {
|
||||||
fun TemperatureForecast(
|
override fun getLineData(
|
||||||
temperature: Int,
|
lineData: List<LineData>,
|
||||||
temperatureUnit: TemperatureUnit,
|
|
||||||
distance: Double? = null,
|
|
||||||
timeLabel: String? = null,
|
|
||||||
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
|
||||||
isImperial: Boolean?
|
|
||||||
) {
|
|
||||||
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
|
||||||
Row(modifier = GlanceModifier.defaultWeight().fillMaxWidth(), horizontalAlignment = rowAlignment, verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(
|
|
||||||
text = "${temperature}${temperatureUnit.unitDisplay}",
|
|
||||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(28f, TextUnitType.Sp), textAlign = TextAlign.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (distance != null && isImperial != null){
|
|
||||||
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
|
||||||
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
|
||||||
val text = if(distanceInUserUnit > 0){
|
|
||||||
"In $label"
|
|
||||||
} else {
|
|
||||||
"$label ago"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (distanceInUserUnit != 0){
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = TextStyle(
|
|
||||||
color = ColorProvider(Color.Black, Color.White),
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = TextUnit(18f, TextUnitType.Sp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeLabel != null){
|
|
||||||
Text(
|
|
||||||
text = timeLabel,
|
|
||||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
|
||||||
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TemperatureForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "temperatureForecast") {
|
|
||||||
@Composable
|
|
||||||
override fun RenderWidget(
|
|
||||||
arrowBitmap: Bitmap,
|
|
||||||
current: WeatherInterpretation,
|
|
||||||
windBearing: Int,
|
|
||||||
windSpeed: Int,
|
|
||||||
windGusts: Int,
|
|
||||||
precipitation: Double,
|
|
||||||
precipitationProbability: Int?,
|
|
||||||
temperature: Int,
|
|
||||||
temperatureUnit: TemperatureUnit,
|
|
||||||
timeLabel: String,
|
|
||||||
dateLabel: String?,
|
|
||||||
distance: Double?,
|
|
||||||
isImperial: Boolean,
|
isImperial: Boolean,
|
||||||
isNight: Boolean
|
upcomingRoute: UpcomingRoute?
|
||||||
) {
|
): Set<LineGraphBuilder.Line> {
|
||||||
TemperatureForecast(
|
val linePoints = lineData.map { data ->
|
||||||
temperature = temperature,
|
if (isImperial) {
|
||||||
temperatureUnit = temperatureUnit,
|
data.weatherData.temperature * 9 / 5 + 32 // Convert Celsius to Fahrenheit
|
||||||
distance = distance,
|
} else {
|
||||||
timeLabel = timeLabel,
|
data.weatherData.temperature // Keep Celsius
|
||||||
isImperial = isImperial,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return setOf(
|
||||||
|
LineGraphBuilder.Line(
|
||||||
|
dataPoints = linePoints.mapIndexed { index, value ->
|
||||||
|
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
||||||
|
},
|
||||||
|
color = android.graphics.Color.RED,
|
||||||
|
label = if (!isImperial) "°C" else "°F",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import de.timklge.karooheadwind.HeadingResponse
|
|
||||||
import de.timklge.karooheadwind.HeadwindSettings
|
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
|
||||||
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
|
|
||||||
import de.timklge.karooheadwind.getRelativeHeadingFlow
|
|
||||||
import de.timklge.karooheadwind.streamCurrentWeatherData
|
|
||||||
import de.timklge.karooheadwind.streamSettings
|
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
|
||||||
import io.hammerhead.karooext.extension.DataTypeImpl
|
|
||||||
import io.hammerhead.karooext.internal.Emitter
|
|
||||||
import io.hammerhead.karooext.models.DataPoint
|
|
||||||
import io.hammerhead.karooext.models.DataType
|
|
||||||
import io.hammerhead.karooext.models.StreamState
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.cos
|
|
||||||
|
|
||||||
class UserWindSpeedDataType(
|
|
||||||
private val karooSystem: KarooSystemService,
|
|
||||||
private val context: Context
|
|
||||||
) : DataTypeImpl("karoo-headwind", "userwindSpeed"){
|
|
||||||
|
|
||||||
data class StreamData(val headingResponse: HeadingResponse, val weatherResponse: WeatherData?, val settings: HeadwindSettings)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun streamValues(context: Context, karooSystem: KarooSystemService): Flow<Double> = flow {
|
|
||||||
karooSystem.getRelativeHeadingFlow(context)
|
|
||||||
.combine(context.streamCurrentWeatherData(karooSystem)) { value, data -> value to data }
|
|
||||||
.combine(context.streamSettings(karooSystem)) { (value, data), settings ->
|
|
||||||
StreamData(value, data, settings)
|
|
||||||
}
|
|
||||||
.filter { it.weatherResponse != null }
|
|
||||||
.collect { streamData ->
|
|
||||||
val windSpeed = streamData.weatherResponse?.windSpeed ?: 0.0
|
|
||||||
val windDirection = (streamData.headingResponse as? HeadingResponse.Value)?.diff ?: 0.0
|
|
||||||
|
|
||||||
if (streamData.settings.windDirectionIndicatorTextSetting == WindDirectionIndicatorTextSetting.HEADWIND_SPEED){
|
|
||||||
val headwindSpeed = cos((windDirection + 180) * Math.PI / 180.0) * windSpeed
|
|
||||||
|
|
||||||
emit(headwindSpeed)
|
|
||||||
} else {
|
|
||||||
emit(windSpeed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun startStream(emitter: Emitter<StreamState>) {
|
|
||||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
streamValues(context, karooSystem)
|
|
||||||
.collect { value ->
|
|
||||||
emitter.onNext(
|
|
||||||
StreamState.Streaming(
|
|
||||||
DataPoint(
|
|
||||||
dataTypeId,
|
|
||||||
mapOf(DataType.Field.SINGLE to value)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emitter.setCancellable {
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -24,6 +24,9 @@ import de.timklge.karooheadwind.streamDatatypeIsVisible
|
|||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
import de.timklge.karooheadwind.streamUserProfile
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
import de.timklge.karooheadwind.throttle
|
import de.timklge.karooheadwind.throttle
|
||||||
|
import de.timklge.karooheadwind.util.celciusInUserUnit
|
||||||
|
import de.timklge.karooheadwind.util.millimetersInUserUnit
|
||||||
|
import de.timklge.karooheadwind.util.msInUserUnit
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
@ -85,7 +88,7 @@ class WeatherDataType(
|
|||||||
emit(StreamData(
|
emit(StreamData(
|
||||||
WeatherData(
|
WeatherData(
|
||||||
Instant.now().epochSecond, 0.0,
|
Instant.now().epochSecond, 0.0,
|
||||||
20.0, 50.0, 3.0, 0.0, 1013.25, 980.0, 15.0, 30.0, 30.0,
|
20, 50.0, 3.0, 0.0, 1013.25, 980.0, 5.0, 30.0, 10.0,
|
||||||
WeatherInterpretation.getKnownWeatherCodes().random(), isForecast = false,
|
WeatherInterpretation.getKnownWeatherCodes().random(), isForecast = false,
|
||||||
isNight = listOf(true, false).random()
|
isNight = listOf(true, false).random()
|
||||||
), HeadwindSettings(), isVisible = true))
|
), HeadwindSettings(), isVisible = true))
|
||||||
@ -147,11 +150,11 @@ class WeatherDataType(
|
|||||||
baseBitmap,
|
baseBitmap,
|
||||||
current = interpretation,
|
current = interpretation,
|
||||||
windBearing = data.windDirection.roundToInt(),
|
windBearing = data.windDirection.roundToInt(),
|
||||||
windSpeed = data.windSpeed.roundToInt(),
|
windSpeed = msInUserUnit(data.windSpeed, userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||||
windGusts = data.windGusts.roundToInt(),
|
windGusts = msInUserUnit(data.windGusts, userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||||
precipitation = data.precipitation,
|
precipitation = millimetersInUserUnit(data.precipitation, userProfile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL),
|
||||||
precipitationProbability = null,
|
precipitationProbability = null,
|
||||||
temperature = data.temperature.roundToInt(),
|
temperature = celciusInUserUnit(data.temperature, userProfile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||||
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
temperatureUnit = if (userProfile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||||
timeLabel = formattedTime,
|
timeLabel = formattedTime,
|
||||||
rowAlignment = when (config.alignment){
|
rowAlignment = when (config.alignment){
|
||||||
|
|||||||
@ -47,12 +47,12 @@ fun getShortDateFormatter(): DateTimeFormatter = DateTimeFormatter.ofPattern(
|
|||||||
|
|
||||||
fun getWeatherIcon(interpretation: WeatherInterpretation, isNight: Boolean): Int {
|
fun getWeatherIcon(interpretation: WeatherInterpretation, isNight: Boolean): Int {
|
||||||
return when (interpretation){
|
return when (interpretation){
|
||||||
WeatherInterpretation.CLEAR -> if (isNight) R.drawable.moon else R.drawable.bx_clear
|
WeatherInterpretation.CLEAR -> if (isNight) R.drawable.crescent_moon else R.drawable.sun
|
||||||
WeatherInterpretation.CLOUDY -> R.drawable.bx_cloud
|
WeatherInterpretation.CLOUDY -> R.drawable.cloud
|
||||||
WeatherInterpretation.RAINY -> R.drawable.bx_cloud_rain
|
WeatherInterpretation.RAINY -> R.drawable.cloud_with_rain
|
||||||
WeatherInterpretation.SNOWY -> R.drawable.bx_cloud_snow
|
WeatherInterpretation.SNOWY -> R.drawable.cloud_with_snow
|
||||||
WeatherInterpretation.DRIZZLE -> R.drawable.bx_cloud_drizzle
|
WeatherInterpretation.DRIZZLE -> R.drawable.cloud_with_light_rain
|
||||||
WeatherInterpretation.THUNDERSTORM -> R.drawable.bx_cloud_lightning
|
WeatherInterpretation.THUNDERSTORM -> R.drawable.cloud_with_lightning_and_rain
|
||||||
WeatherInterpretation.UNKNOWN -> R.drawable.question_mark_regular_240
|
WeatherInterpretation.UNKNOWN -> R.drawable.question_mark_regular_240
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,7 +87,6 @@ fun Weather(
|
|||||||
provider = ImageProvider(getWeatherIcon(current, isNight)),
|
provider = ImageProvider(getWeatherIcon(current, isNight)),
|
||||||
contentDescription = "Current weather information",
|
contentDescription = "Current weather information",
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +142,6 @@ fun Weather(
|
|||||||
provider = ImageProvider(R.drawable.thermometer),
|
provider = ImageProvider(R.drawable.thermometer),
|
||||||
contentDescription = "Temperature",
|
contentDescription = "Temperature",
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@ -0,0 +1,164 @@
|
|||||||
|
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.R
|
||||||
|
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 io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.extension.DataTypeImpl
|
||||||
|
import io.hammerhead.karooext.internal.ViewEmitter
|
||||||
|
import io.hammerhead.karooext.models.ShowCustomStreamState
|
||||||
|
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.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class WindDirectionAndSpeedDataType(
|
||||||
|
private val karooSystem: KarooSystemService,
|
||||||
|
private val applicationContext: Context
|
||||||
|
) : DataTypeImpl("karoo-headwind", "windDirectionAndSpeed") {
|
||||||
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
|
private val glance = GlanceRemoteViews()
|
||||||
|
|
||||||
|
data class StreamData(val headingResponse: HeadingResponse,
|
||||||
|
val absoluteWindDirection: Double?,
|
||||||
|
val windSpeed: Double?,
|
||||||
|
val settings: HeadwindSettings,
|
||||||
|
val gustSpeed: Double?,
|
||||||
|
val isImperial: Boolean,
|
||||||
|
val isVisible: Boolean)
|
||||||
|
|
||||||
|
private fun previewFlow(profileFlow: Flow<UserProfile>): Flow<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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalGlanceRemoteViewsApi::class)
|
||||||
|
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,
|
||||||
|
R.drawable.arrow_0
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
emitter.onNext(ShowCustomStreamState("", null))
|
||||||
|
|
||||||
|
val refreshRate = karooSystem.getRefreshRateInMilliseconds(context)
|
||||||
|
flow.filter { it.isVisible }.throttle(refreshRate).collect { streamData ->
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Updating wind speed and 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 windDirection = streamData.headingResponse.diff
|
||||||
|
|
||||||
|
val windSpeedUserUnit = msInUserUnit(windSpeed, streamData.isImperial)
|
||||||
|
val gustSpeedUserUnit = msInUserUnit(streamData.gustSpeed ?: 0.0, streamData.isImperial)
|
||||||
|
|
||||||
|
val mainText = let {
|
||||||
|
"${windSpeedUserUnit.roundToInt().absoluteValue}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val subtext = "Max ${gustSpeedUserUnit.roundToInt()}"
|
||||||
|
|
||||||
|
val headwindSpeed = cos( (windDirection + 180) * Math.PI / 180.0) * windSpeed
|
||||||
|
val windSpeedInKmh = headwindSpeed * 3.6
|
||||||
|
|
||||||
|
val result = glance.compose(context, DpSize.Unspecified) {
|
||||||
|
HeadwindDirection(
|
||||||
|
baseBitmap,
|
||||||
|
windDirection.roundToInt(),
|
||||||
|
config.textSize,
|
||||||
|
mainText,
|
||||||
|
subtext,
|
||||||
|
interpolateWindColor(windSpeedInKmh, false, context),
|
||||||
|
interpolateWindColor(windSpeedInKmh, true, context),
|
||||||
|
wideMode = config.gridSize.first == 60,
|
||||||
|
preview = config.preview,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.updateView(result.remoteViews)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitter.setCancellable {
|
||||||
|
Log.d(KarooHeadwindExtension.TAG, "Stopping wind speed and direction view with $emitter")
|
||||||
|
configJob.cancel()
|
||||||
|
viewJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,6 +26,7 @@ import io.hammerhead.karooext.internal.ViewEmitter
|
|||||||
import io.hammerhead.karooext.models.ShowCustomStreamState
|
import io.hammerhead.karooext.models.ShowCustomStreamState
|
||||||
import io.hammerhead.karooext.models.StreamState
|
import io.hammerhead.karooext.models.StreamState
|
||||||
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
import io.hammerhead.karooext.models.UpdateGraphicConfig
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
import io.hammerhead.karooext.models.ViewConfig
|
import io.hammerhead.karooext.models.ViewConfig
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -51,7 +52,7 @@ class WindDirectionDataType(val karooSystem: KarooSystemService, context: Contex
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getValue(data: WeatherData): Double {
|
override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
|
||||||
return data.windDirection
|
return data.windDirection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,119 +1,116 @@
|
|||||||
package de.timklge.karooheadwind.datatypes
|
package de.timklge.karooheadwind.datatypes
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Composable
|
import com.mapbox.turf.TurfConstants
|
||||||
import androidx.compose.ui.graphics.Color
|
import com.mapbox.turf.TurfMeasurement
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import androidx.compose.ui.unit.TextUnitType
|
import de.timklge.karooheadwind.UpcomingRoute
|
||||||
import androidx.compose.ui.unit.dp
|
import de.timklge.karooheadwind.lerpWeather
|
||||||
import androidx.glance.ColorFilter
|
import de.timklge.karooheadwind.screens.LineGraphBuilder
|
||||||
import androidx.glance.GlanceModifier
|
import de.timklge.karooheadwind.util.signedAngleDifference
|
||||||
import androidx.glance.Image
|
|
||||||
import androidx.glance.ImageProvider
|
|
||||||
import androidx.glance.color.ColorProvider
|
|
||||||
import androidx.glance.layout.Alignment
|
|
||||||
import androidx.glance.layout.Column
|
|
||||||
import androidx.glance.layout.ContentScale
|
|
||||||
import androidx.glance.layout.Row
|
|
||||||
import androidx.glance.layout.fillMaxHeight
|
|
||||||
import androidx.glance.layout.fillMaxWidth
|
|
||||||
import androidx.glance.layout.padding
|
|
||||||
import androidx.glance.layout.width
|
|
||||||
import androidx.glance.text.FontFamily
|
|
||||||
import androidx.glance.text.FontWeight
|
|
||||||
import androidx.glance.text.Text
|
|
||||||
import androidx.glance.text.TextAlign
|
|
||||||
import androidx.glance.text.TextStyle
|
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherInterpretation
|
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.floor
|
||||||
|
|
||||||
@Composable
|
fun remap(value: Float, fromLow: Float, fromHigh: Float, toLow: Float, toHigh: Float): Float {
|
||||||
fun WindForecast(
|
if (fromHigh == fromLow) return toLow
|
||||||
arrowBitmap: Bitmap,
|
return toLow + (value - fromLow) / (fromHigh - fromLow) * (toHigh - toLow)
|
||||||
windBearing: Int,
|
|
||||||
windSpeed: Int,
|
|
||||||
gustSpeed: Int,
|
|
||||||
distance: Double? = null,
|
|
||||||
timeLabel: String? = null,
|
|
||||||
rowAlignment: Alignment.Horizontal = Alignment.Horizontal.CenterHorizontally,
|
|
||||||
isImperial: Boolean?
|
|
||||||
) {
|
|
||||||
Column(modifier = GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) {
|
|
||||||
Image(
|
|
||||||
modifier = GlanceModifier.defaultWeight().fillMaxWidth(),
|
|
||||||
provider = ImageProvider(getArrowBitmapByBearing(arrowBitmap, windBearing + 180)),
|
|
||||||
contentDescription = "Current wind direction",
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White))
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "${windSpeed}-${gustSpeed}",
|
|
||||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp), textAlign = TextAlign.Center)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (distance != null && isImperial != null){
|
|
||||||
val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt()
|
|
||||||
val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}"
|
|
||||||
val text = if(distanceInUserUnit > 0){
|
|
||||||
"In $label"
|
|
||||||
} else {
|
|
||||||
"$label ago"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distanceInUserUnit != 0){
|
class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastDataType(karooSystem, "windForecast") {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
override fun getLineData(
|
||||||
Text(
|
lineData: List<LineData>,
|
||||||
text = text,
|
|
||||||
style = TextStyle(
|
|
||||||
color = ColorProvider(Color.Black, Color.White),
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = TextUnit(18f, TextUnitType.Sp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeLabel != null){
|
|
||||||
Text(
|
|
||||||
text = timeLabel,
|
|
||||||
style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontWeight = FontWeight.Bold,
|
|
||||||
fontFamily = FontFamily.Monospace, fontSize = TextUnit(18f, TextUnitType.Sp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WindForecastDataType(karooSystem: KarooSystemService) : ForecastDataType(karooSystem, "windForecast") {
|
|
||||||
@Composable
|
|
||||||
override fun RenderWidget(
|
|
||||||
arrowBitmap: Bitmap,
|
|
||||||
current: WeatherInterpretation,
|
|
||||||
windBearing: Int,
|
|
||||||
windSpeed: Int,
|
|
||||||
windGusts: Int,
|
|
||||||
precipitation: Double,
|
|
||||||
precipitationProbability: Int?,
|
|
||||||
temperature: Int,
|
|
||||||
temperatureUnit: TemperatureUnit,
|
|
||||||
timeLabel: String,
|
|
||||||
dateLabel: String?,
|
|
||||||
distance: Double?,
|
|
||||||
isImperial: Boolean,
|
isImperial: Boolean,
|
||||||
isNight: Boolean
|
upcomingRoute: UpcomingRoute?
|
||||||
) {
|
): Set<LineGraphBuilder.Line> {
|
||||||
WindForecast(
|
val windPoints = lineData.map { data ->
|
||||||
arrowBitmap = arrowBitmap,
|
if (isImperial) { // Convert m/s to mph
|
||||||
windBearing = windBearing,
|
data.weatherData.windSpeed * 2.23694 // Convert m/s to mph
|
||||||
windSpeed = windSpeed,
|
} else { // Convert m/s to km/h
|
||||||
gustSpeed = windGusts,
|
data.weatherData.windSpeed * 3.6 // Convert m/s to km/h
|
||||||
distance = distance,
|
|
||||||
timeLabel = timeLabel,
|
|
||||||
isImperial = isImperial,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val gustPoints = lineData.map { data ->
|
||||||
|
if (isImperial) { // Convert m/s to mph
|
||||||
|
data.weatherData.windGusts * 2.23694 // Convert m/s to mph
|
||||||
|
} else { // Convert m/s to km/h
|
||||||
|
data.weatherData.windGusts * 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()
|
||||||
|
val beforeLineData = lineData.getOrNull(floor(lineData.size * t).toInt()) ?: lineData.firstOrNull()
|
||||||
|
val afterLineData = lineData.getOrNull(ceil(lineData.size * t).toInt()) ?: lineData.lastOrNull()
|
||||||
|
|
||||||
|
if (beforeLineData?.weatherData == null || afterLineData?.weatherData == null || beforeLineData.distance == null
|
||||||
|
|| afterLineData.distance == null) 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 = TurfMeasurement.along(upcomingRoute.routePolyline, distanceAlongRoute, TurfConstants.UNIT_METERS)
|
||||||
|
val nextCoordsAlongRoute = TurfMeasurement.along(upcomingRoute.routePolyline, distanceAlongRoute + 5, TurfConstants.UNIT_METERS)
|
||||||
|
val bearingAlongRoute = TurfMeasurement.bearing(coordsAlongRoute, nextCoordsAlongRoute)
|
||||||
|
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(i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()), headwindSpeedInUserUnit.toFloat())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
} catch(e: Exception) {
|
||||||
|
Log.e(KarooHeadwindExtension.TAG, "Error calculating headwind points", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildSet {
|
||||||
|
add(LineGraphBuilder.Line(
|
||||||
|
dataPoints = windPoints.mapIndexed { index, value ->
|
||||||
|
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
||||||
|
},
|
||||||
|
color = android.graphics.Color.GRAY,
|
||||||
|
label = "Wind" // if (!isImperial) "Wind km/h" else "Wind mph",
|
||||||
|
))
|
||||||
|
add(LineGraphBuilder.Line(
|
||||||
|
dataPoints = gustPoints.mapIndexed { index, value ->
|
||||||
|
LineGraphBuilder.DataPoint(index.toFloat(), value.toFloat())
|
||||||
|
},
|
||||||
|
color = android.graphics.Color.DKGRAY,
|
||||||
|
label = "Gust" // if (!isImperial) "Gust km/h" else "Gust mph",
|
||||||
|
))
|
||||||
|
|
||||||
|
if (headwindPoints.isNotEmpty()) {
|
||||||
|
add(LineGraphBuilder.Line(
|
||||||
|
dataPoints = headwindPoints,
|
||||||
|
color = android.graphics.Color.MAGENTA,
|
||||||
|
label = "Headwind", // if (!isImperial) "Headwind km/h" else "Headwind mph",
|
||||||
|
drawCircles = false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val HEADWIND_SAMPLE_COUNT = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
|
||||||
class WindGustsDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windGusts"){
|
class WindGustsDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windGusts"){
|
||||||
override fun getValue(data: WeatherData): Double {
|
override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
|
||||||
return data.windGusts
|
return data.windGusts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,9 +3,10 @@ package de.timklge.karooheadwind.datatypes
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
import de.timklge.karooheadwind.weatherprovider.WeatherData
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
|
|
||||||
class WindSpeedDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windSpeed"){
|
class WindSpeedDataType(karooSystemService: KarooSystemService, context: Context) : BaseDataType(karooSystemService, context, "windSpeed"){
|
||||||
override fun getValue(data: WeatherData): Double {
|
override fun getValue(data: WeatherData, userProfile: UserProfile): Double {
|
||||||
return data.windSpeed
|
return data.windSpeed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,597 @@
|
|||||||
|
package de.timklge.karooheadwind.screens
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Paint.Align
|
||||||
|
import android.graphics.Path
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import kotlin.math.abs
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
|
class LineGraphBuilder(val context: Context) {
|
||||||
|
enum class YAxis {
|
||||||
|
LEFT, RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DataPoint(val x: Float, val y: Float)
|
||||||
|
|
||||||
|
data class Line(
|
||||||
|
val dataPoints: List<DataPoint>,
|
||||||
|
@ColorInt val color: Int,
|
||||||
|
val label: String? = null,
|
||||||
|
val yAxis: YAxis = YAxis.LEFT, // Default to left Y-axis
|
||||||
|
val drawCircles: Boolean = true // Default to true
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun isNightMode(): Boolean {
|
||||||
|
val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||||
|
return nightModeFlags == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drawLineGraph(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
gridWidth: Int,
|
||||||
|
gridHeight: Int,
|
||||||
|
lines: Set<Line>,
|
||||||
|
labelProvider: ((Float) -> String)
|
||||||
|
): Bitmap {
|
||||||
|
val isNightMode = isNightMode()
|
||||||
|
|
||||||
|
val bitmap = createBitmap(width, height)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
|
||||||
|
val backgroundColor = if (isNightMode) Color.BLACK else Color.WHITE
|
||||||
|
val primaryTextColor = if (isNightMode) Color.WHITE else Color.BLACK
|
||||||
|
val secondaryTextColor = if (isNightMode) Color.LTGRAY else Color.DKGRAY // For axes
|
||||||
|
|
||||||
|
canvas.drawColor(backgroundColor)
|
||||||
|
|
||||||
|
if (lines.isEmpty() || lines.all { it.dataPoints.isEmpty() }) {
|
||||||
|
val emptyPaint = Paint().apply {
|
||||||
|
color = primaryTextColor
|
||||||
|
textSize = 24f // Increased from 20f
|
||||||
|
textAlign = Align.CENTER
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
canvas.drawText("No data to display", width / 2f, height / 2f, emptyPaint)
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
val marginTop = 10f
|
||||||
|
val marginBottom = 40f // Increased from 30f
|
||||||
|
var marginRight = 20f // Increased from 5f // Made var
|
||||||
|
|
||||||
|
var dataMinX = Float.MAX_VALUE
|
||||||
|
var dataMaxX = Float.MIN_VALUE
|
||||||
|
var dataMinYLeft = Float.MAX_VALUE
|
||||||
|
var dataMaxYLeft = Float.MIN_VALUE
|
||||||
|
var dataMinYRight = Float.MAX_VALUE
|
||||||
|
var dataMaxYRight = Float.MIN_VALUE
|
||||||
|
var hasLeftYAxisData = false
|
||||||
|
var hasRightYAxisData = false
|
||||||
|
|
||||||
|
var hasData = false
|
||||||
|
lines.forEach { line ->
|
||||||
|
if (line.dataPoints.isNotEmpty()) {
|
||||||
|
hasData = true
|
||||||
|
if (line.yAxis == YAxis.LEFT) {
|
||||||
|
hasLeftYAxisData = true
|
||||||
|
line.dataPoints.forEach { point ->
|
||||||
|
dataMinX = minOf(dataMinX, point.x)
|
||||||
|
dataMaxX = maxOf(dataMaxX, point.x)
|
||||||
|
dataMinYLeft = minOf(dataMinYLeft, point.y)
|
||||||
|
dataMaxYLeft = maxOf(dataMaxYLeft, point.y)
|
||||||
|
}
|
||||||
|
} else { // YAxis.RIGHT
|
||||||
|
hasRightYAxisData = true
|
||||||
|
line.dataPoints.forEach { point ->
|
||||||
|
dataMinX = minOf(dataMinX, point.x)
|
||||||
|
dataMaxX = maxOf(dataMaxX, point.x)
|
||||||
|
dataMinYRight = minOf(dataMinYRight, point.y)
|
||||||
|
dataMaxYRight = maxOf(dataMaxYRight, point.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasData) {
|
||||||
|
val emptyPaint = Paint().apply {
|
||||||
|
color = primaryTextColor
|
||||||
|
textSize = 48f // Increased from 40f
|
||||||
|
textAlign = Align.CENTER
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
canvas.drawText("No data points", width / 2f, height / 2f, emptyPaint)
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically calculate marginLeft based on Y-axis label widths
|
||||||
|
val yAxisLabelPaint = Paint().apply {
|
||||||
|
textSize = 32f
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxLabelWidthLeft = 0f
|
||||||
|
if (hasLeftYAxisData) {
|
||||||
|
val yLabelStringsLeft = mutableListOf<String>()
|
||||||
|
val numYTicksForCalc = 2 // As used later for drawing Y-axis ticks
|
||||||
|
|
||||||
|
// Determine Y-axis label strings (mirrors logic from where labels are drawn)
|
||||||
|
if (abs(dataMaxYLeft - dataMinYLeft) < 0.0001f) {
|
||||||
|
yLabelStringsLeft.add(
|
||||||
|
String.format(
|
||||||
|
java.util.Locale.getDefault(),
|
||||||
|
"%.0f",
|
||||||
|
dataMinYLeft
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
for (i in 0..numYTicksForCalc) {
|
||||||
|
val value =
|
||||||
|
dataMinYLeft + ((dataMaxYLeft - dataMinYLeft) / numYTicksForCalc) * i
|
||||||
|
yLabelStringsLeft.add(
|
||||||
|
String.format(
|
||||||
|
java.util.Locale.getDefault(),
|
||||||
|
"%.0f",
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (labelStr in yLabelStringsLeft) {
|
||||||
|
maxLabelWidthLeft =
|
||||||
|
kotlin.math.max(maxLabelWidthLeft, yAxisLabelPaint.measureText(labelStr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val yAxisTextRightToAxisGap = 15f // Current gap used: graphLeft - 15f
|
||||||
|
val canvasEdgePadding = 5f // Desired padding from the canvas edge
|
||||||
|
|
||||||
|
val dynamicMarginLeft =
|
||||||
|
if (hasLeftYAxisData) maxLabelWidthLeft + yAxisTextRightToAxisGap + canvasEdgePadding else canvasEdgePadding
|
||||||
|
|
||||||
|
// Dynamically calculate marginRight based on Right Y-axis label widths
|
||||||
|
var maxLabelWidthRight = 0f
|
||||||
|
if (hasRightYAxisData) {
|
||||||
|
val yLabelStringsRight = mutableListOf<String>()
|
||||||
|
val numYTicksForCalc = 2 // As used later for drawing Y-axis ticks
|
||||||
|
|
||||||
|
if (abs(dataMaxYRight - dataMinYRight) < 0.0001f) {
|
||||||
|
yLabelStringsRight.add(
|
||||||
|
String.format(
|
||||||
|
java.util.Locale.getDefault(),
|
||||||
|
"%.0f",
|
||||||
|
dataMinYRight
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
for (i in 0..numYTicksForCalc) {
|
||||||
|
val value =
|
||||||
|
dataMinYRight + ((dataMaxYRight - dataMinYRight) / numYTicksForCalc) * i
|
||||||
|
yLabelStringsRight.add(
|
||||||
|
String.format(
|
||||||
|
java.util.Locale.getDefault(),
|
||||||
|
"%.0f",
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (labelStr in yLabelStringsRight) {
|
||||||
|
maxLabelWidthRight =
|
||||||
|
kotlin.math.max(maxLabelWidthRight, yAxisLabelPaint.measureText(labelStr))
|
||||||
|
}
|
||||||
|
val dynamicMarginRight =
|
||||||
|
maxLabelWidthRight + yAxisTextRightToAxisGap + canvasEdgePadding
|
||||||
|
marginRight = dynamicMarginRight // Update marginRight
|
||||||
|
}
|
||||||
|
|
||||||
|
val graphWidth = width - dynamicMarginLeft - marginRight
|
||||||
|
val graphHeight = height - marginTop - marginBottom
|
||||||
|
val graphLeft = dynamicMarginLeft
|
||||||
|
val graphTop = marginTop
|
||||||
|
val graphBottom = height - marginBottom
|
||||||
|
val graphRight = width - marginRight // Define graphRight for clarity
|
||||||
|
|
||||||
|
// Legend properties
|
||||||
|
val legendTextSize = 26f // Increased from 22f
|
||||||
|
val legendTextColor = primaryTextColor
|
||||||
|
val legendPadding = 5f
|
||||||
|
val legendEntryHeight = 30f // Increased from 25f
|
||||||
|
val legendColorBoxSize = 24f // Increased from 20f
|
||||||
|
val legendTextMargin = 5f
|
||||||
|
|
||||||
|
var effectiveMinX = dataMinX
|
||||||
|
var effectiveMaxX = dataMaxX
|
||||||
|
var effectiveMinYLeft = dataMinYLeft
|
||||||
|
var effectiveMaxYLeft = dataMaxYLeft
|
||||||
|
var effectiveMinYRight = dataMinYRight
|
||||||
|
var effectiveMaxYRight = dataMaxYRight
|
||||||
|
|
||||||
|
if (dataMinX == dataMaxX) {
|
||||||
|
effectiveMinX -= 1f
|
||||||
|
effectiveMaxX += 1f
|
||||||
|
} else {
|
||||||
|
val paddingX = (dataMaxX - dataMinX) * 0.05f
|
||||||
|
if (paddingX > 0.0001f) {
|
||||||
|
effectiveMinX -= paddingX
|
||||||
|
effectiveMaxX += paddingX
|
||||||
|
} else {
|
||||||
|
effectiveMinX -= 1f
|
||||||
|
effectiveMaxX += 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y-axis Left: Adjust effective range based on new rules
|
||||||
|
if (hasLeftYAxisData) {
|
||||||
|
// effectiveMinYLeft is dataMinYLeft, effectiveMaxYLeft is dataMaxYLeft at this point
|
||||||
|
if (abs(dataMaxYLeft - dataMinYLeft) < 0.0001f) { // All Y_Left values are equal
|
||||||
|
val commonValue = dataMinYLeft
|
||||||
|
if (commonValue >= 0f) {
|
||||||
|
effectiveMinYLeft = 0f
|
||||||
|
effectiveMaxYLeft =
|
||||||
|
if (commonValue == 0f) 1f else commonValue + kotlin.math.max(
|
||||||
|
abs(commonValue * 0.1f),
|
||||||
|
1f
|
||||||
|
)
|
||||||
|
} else { // commonValue < 0f
|
||||||
|
effectiveMaxYLeft = 0f
|
||||||
|
effectiveMinYLeft = commonValue - kotlin.math.max(abs(commonValue * 0.1f), 1f)
|
||||||
|
}
|
||||||
|
} else { // Y_Left values are not all equal, apply standard 5% padding
|
||||||
|
val paddingYLeft = (dataMaxYLeft - dataMinYLeft) * 0.05f
|
||||||
|
if (paddingYLeft > 0.0001f) {
|
||||||
|
effectiveMinYLeft -= paddingYLeft // equivalent to dataMinYLeft - padding
|
||||||
|
effectiveMaxYLeft += paddingYLeft // equivalent to dataMaxYLeft + padding
|
||||||
|
} else {
|
||||||
|
effectiveMinYLeft -= 1f
|
||||||
|
effectiveMaxYLeft += 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Safety check: ensure min < max for left Y-axis
|
||||||
|
if (effectiveMinYLeft >= effectiveMaxYLeft) {
|
||||||
|
effectiveMaxYLeft = effectiveMinYLeft + 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y-axis Right: Adjust effective range based on new rules
|
||||||
|
if (hasRightYAxisData) {
|
||||||
|
// effectiveMinYRight is dataMinYRight, effectiveMaxYRight is dataMaxYRight at this point
|
||||||
|
if (abs(dataMaxYRight - dataMinYRight) < 0.0001f) { // All Y_Right values are equal
|
||||||
|
val commonValue = dataMinYRight
|
||||||
|
if (commonValue >= 0f) {
|
||||||
|
effectiveMinYRight = 0f
|
||||||
|
effectiveMaxYRight =
|
||||||
|
if (commonValue == 0f) 1f else commonValue + kotlin.math.max(
|
||||||
|
abs(commonValue * 0.1f),
|
||||||
|
1f
|
||||||
|
)
|
||||||
|
} else { // commonValue < 0f
|
||||||
|
effectiveMaxYRight = 0f
|
||||||
|
effectiveMinYRight = commonValue - kotlin.math.max(abs(commonValue * 0.1f), 1f)
|
||||||
|
}
|
||||||
|
} else { // Y_Right values are not all equal, apply standard 5% padding
|
||||||
|
val paddingYRight = (dataMaxYRight - dataMinYRight) * 0.05f
|
||||||
|
if (paddingYRight > 0.0001f) {
|
||||||
|
effectiveMinYRight -= paddingYRight
|
||||||
|
effectiveMaxYRight += paddingYRight
|
||||||
|
} else {
|
||||||
|
effectiveMinYRight -= 1f
|
||||||
|
effectiveMaxYRight += 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Safety check: ensure min < max for right Y-axis
|
||||||
|
if (effectiveMinYRight >= effectiveMaxYRight) {
|
||||||
|
effectiveMaxYRight = effectiveMinYRight + 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val rangeX =
|
||||||
|
if (abs(effectiveMaxX - effectiveMinX) < 0.0001f) 1f else (effectiveMaxX - effectiveMinX)
|
||||||
|
val rangeYLeft =
|
||||||
|
if (!hasLeftYAxisData || abs(effectiveMaxYLeft - effectiveMinYLeft) < 0.0001f) 1f else (effectiveMaxYLeft - effectiveMinYLeft)
|
||||||
|
val rangeYRight =
|
||||||
|
if (!hasRightYAxisData || abs(effectiveMaxYRight - effectiveMinYRight) < 0.0001f) 1f else (effectiveMaxYRight - effectiveMinYRight)
|
||||||
|
|
||||||
|
fun mapX(originalX: Float): Float {
|
||||||
|
return graphLeft + ((originalX - effectiveMinX) / rangeX) * graphWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapYLeft(originalY: Float): Float {
|
||||||
|
return graphBottom - ((originalY - effectiveMinYLeft) / rangeYLeft) * graphHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapYRight(originalY: Float): Float {
|
||||||
|
return graphBottom - ((originalY - effectiveMinYRight) / rangeYRight) * graphHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
val axisPaint = Paint().apply {
|
||||||
|
color = secondaryTextColor
|
||||||
|
strokeWidth = 3f
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
canvas.drawLine(
|
||||||
|
graphLeft,
|
||||||
|
graphBottom,
|
||||||
|
graphLeft + graphWidth,
|
||||||
|
graphBottom,
|
||||||
|
axisPaint
|
||||||
|
) // X-axis
|
||||||
|
if (hasLeftYAxisData) {
|
||||||
|
canvas.drawLine(graphLeft, graphTop, graphLeft, graphBottom, axisPaint) // Left Y-axis
|
||||||
|
}
|
||||||
|
if (hasRightYAxisData) {
|
||||||
|
canvas.drawLine(
|
||||||
|
graphRight, // Use graphRight for clarity and consistency
|
||||||
|
graphTop,
|
||||||
|
graphRight,
|
||||||
|
graphBottom,
|
||||||
|
axisPaint
|
||||||
|
) // Right Y-axis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid line paint
|
||||||
|
val gridLinePaint = Paint().apply {
|
||||||
|
color = if (isNightMode) Color.DKGRAY else Color.LTGRAY // Faint color
|
||||||
|
strokeWidth = 1f
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val linePaint = Paint().apply {
|
||||||
|
strokeWidth = 6f
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
isAntiAlias = true
|
||||||
|
strokeCap = Paint.Cap.ROUND
|
||||||
|
strokeJoin = Paint.Join.ROUND
|
||||||
|
}
|
||||||
|
|
||||||
|
val textPaint = Paint().apply {
|
||||||
|
color = primaryTextColor
|
||||||
|
textSize = 32f // Increased from 28f
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (line in lines) {
|
||||||
|
if (line.dataPoints.isEmpty()) continue
|
||||||
|
|
||||||
|
linePaint.color = line.color
|
||||||
|
val path = Path()
|
||||||
|
val firstPoint = line.dataPoints.first()
|
||||||
|
val mapY = if (line.yAxis == YAxis.LEFT) ::mapYLeft else ::mapYRight
|
||||||
|
|
||||||
|
path.moveTo(mapX(firstPoint.x), mapY(firstPoint.y))
|
||||||
|
if (line.drawCircles) {
|
||||||
|
canvas.drawCircle(
|
||||||
|
mapX(firstPoint.x),
|
||||||
|
mapY(firstPoint.y),
|
||||||
|
8f,
|
||||||
|
linePaint.apply { style = Paint.Style.FILL })
|
||||||
|
}
|
||||||
|
linePaint.style = Paint.Style.STROKE
|
||||||
|
|
||||||
|
for (i in 1 until line.dataPoints.size) {
|
||||||
|
val point = line.dataPoints[i]
|
||||||
|
path.lineTo(mapX(point.x), mapY(point.y))
|
||||||
|
if (line.drawCircles) {
|
||||||
|
canvas.drawCircle(
|
||||||
|
mapX(point.x),
|
||||||
|
mapY(point.y),
|
||||||
|
8f,
|
||||||
|
linePaint.apply { style = Paint.Style.FILL })
|
||||||
|
}
|
||||||
|
linePaint.style = Paint.Style.STROKE
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawPath(path, linePaint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Left Y-axis ticks and labels
|
||||||
|
if (hasLeftYAxisData) {
|
||||||
|
textPaint.textAlign = Align.RIGHT
|
||||||
|
val numYTicks = if (gridWidth > 15) 2 else 1
|
||||||
|
if (abs(dataMaxYLeft - dataMinYLeft) > 0.0001f) {
|
||||||
|
for (i in 0..numYTicks) {
|
||||||
|
val value = dataMinYLeft + ((dataMaxYLeft - dataMinYLeft) / numYTicks) * i
|
||||||
|
val yPos = mapYLeft(value)
|
||||||
|
if (yPos >= graphTop - 5f && yPos <= graphBottom + 5f) {
|
||||||
|
canvas.drawLine(graphLeft - 5f, yPos, graphLeft + 5f, yPos, axisPaint)
|
||||||
|
// Draw faint horizontal grid line
|
||||||
|
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
|
||||||
|
canvas.drawText(
|
||||||
|
String.format(java.util.Locale.getDefault(), "%.0f", value),
|
||||||
|
graphLeft - 15f,
|
||||||
|
yPos + (textPaint.textSize / 3),
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val yPos = mapYLeft(dataMinYLeft)
|
||||||
|
canvas.drawLine(graphLeft - 5f, yPos, graphLeft + 5f, yPos, axisPaint)
|
||||||
|
// Draw faint horizontal grid line
|
||||||
|
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
|
||||||
|
canvas.drawText(
|
||||||
|
String.format(java.util.Locale.getDefault(), "%.0f", dataMinYLeft),
|
||||||
|
graphLeft - 15f,
|
||||||
|
yPos + (textPaint.textSize / 3),
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Right Y-axis ticks and labels
|
||||||
|
if (hasRightYAxisData) {
|
||||||
|
textPaint.textAlign = Align.LEFT
|
||||||
|
val numYTicks = if (gridWidth > 15) 2 else 1
|
||||||
|
if (abs(dataMaxYRight - dataMinYRight) > 0.0001f) {
|
||||||
|
for (i in 0..numYTicks) {
|
||||||
|
val value = dataMinYRight + ((dataMaxYRight - dataMinYRight) / numYTicks) * i
|
||||||
|
val yPos = mapYRight(value)
|
||||||
|
if (yPos >= graphTop - 5f && yPos <= graphBottom + 5f) {
|
||||||
|
canvas.drawLine(
|
||||||
|
graphRight - 5f,
|
||||||
|
yPos,
|
||||||
|
graphRight + 5f,
|
||||||
|
yPos,
|
||||||
|
axisPaint
|
||||||
|
)
|
||||||
|
// Draw faint horizontal grid line
|
||||||
|
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
|
||||||
|
canvas.drawText(
|
||||||
|
String.format(java.util.Locale.getDefault(), "%.0f", value),
|
||||||
|
graphRight + 15f,
|
||||||
|
yPos + (textPaint.textSize / 3),
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val yPos = mapYRight(dataMinYRight)
|
||||||
|
canvas.drawLine(
|
||||||
|
graphRight - 5f,
|
||||||
|
yPos,
|
||||||
|
graphRight + 5f,
|
||||||
|
yPos,
|
||||||
|
axisPaint
|
||||||
|
)
|
||||||
|
// Draw faint horizontal grid line
|
||||||
|
canvas.drawLine(graphLeft, yPos, graphRight, yPos, gridLinePaint)
|
||||||
|
canvas.drawText(
|
||||||
|
String.format(java.util.Locale.getDefault(), "%.0f", dataMinYRight),
|
||||||
|
graphRight + 15f,
|
||||||
|
yPos + (textPaint.textSize / 3),
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Y zero line (solid, using axisPaint)
|
||||||
|
// This is drawn after faint grid lines from Y-ticks, so it will be on top.
|
||||||
|
// It will not be drawn if it coincides with the X-axis (graphBottom), as X-axis is already solid.
|
||||||
|
val yZeroLinePaint = axisPaint // Use the same paint as other axes for consistency
|
||||||
|
|
||||||
|
if (hasLeftYAxisData) {
|
||||||
|
// If left Y-axis has data and its range includes 0
|
||||||
|
if (effectiveMinYLeft <= 0f && effectiveMaxYLeft >= 0f) {
|
||||||
|
val yZeroPos = mapYLeft(0f)
|
||||||
|
// Draw if the zero position is within graph bounds (inclusive top)
|
||||||
|
// and not effectively the same as the X-axis (graphBottom).
|
||||||
|
if (yZeroPos in graphTop..graphBottom && abs(yZeroPos - graphBottom) > 0.1f) {
|
||||||
|
canvas.drawLine(graphLeft, yZeroPos, graphRight, yZeroPos, yZeroLinePaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (hasRightYAxisData) {
|
||||||
|
// Else, if no left Y-axis data, but right Y-axis has data and its range includes 0
|
||||||
|
if (effectiveMinYRight <= 0f && effectiveMaxYRight >= 0f) {
|
||||||
|
val yZeroPos = mapYRight(0f)
|
||||||
|
// Draw if the zero position is within graph bounds (inclusive top)
|
||||||
|
// and not effectively the same as the X-axis (graphBottom).
|
||||||
|
if (yZeroPos in graphTop..graphBottom && abs(yZeroPos - graphBottom) > 0.1f) {
|
||||||
|
canvas.drawLine(graphLeft, yZeroPos, graphRight, yZeroPos, yZeroLinePaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textPaint.textAlign = Align.CENTER
|
||||||
|
val numXTicks = if (gridHeight > 15) 3 else 1
|
||||||
|
if (abs(dataMaxX - dataMinX) > 0.0001f) {
|
||||||
|
for (i in 0..numXTicks) {
|
||||||
|
val value = dataMinX + ((dataMaxX - dataMinX) / numXTicks) * i
|
||||||
|
val xPos = mapX(value)
|
||||||
|
if (xPos >= graphLeft - 5f && xPos <= graphLeft + graphWidth + 5f) {
|
||||||
|
canvas.drawLine(xPos, graphBottom - 5f, xPos, graphBottom + 5f, axisPaint)
|
||||||
|
canvas.drawText(labelProvider(value), xPos, graphBottom + 30f, textPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val xPos = mapX(dataMinX)
|
||||||
|
canvas.drawLine(xPos, graphBottom - 5f, xPos, graphBottom + 5f, axisPaint)
|
||||||
|
canvas.drawText(labelProvider(dataMinX), xPos, graphBottom + 30f, textPaint)
|
||||||
|
}
|
||||||
|
|
||||||
|
textPaint.textAlign = Align.CENTER
|
||||||
|
textPaint.color = primaryTextColor // Ensure textPaint color is reset before drawing legend
|
||||||
|
|
||||||
|
// Draw Legend
|
||||||
|
val legendPaint = Paint().apply {
|
||||||
|
textSize = legendTextSize
|
||||||
|
color = legendTextColor
|
||||||
|
isAntiAlias = true
|
||||||
|
textAlign = Align.LEFT // Important for measuring text width correctly
|
||||||
|
}
|
||||||
|
val legendColorPaint = Paint().apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val legendItems = lines.filter { it.label != null }
|
||||||
|
if (legendItems.isNotEmpty()) {
|
||||||
|
var maxLegendLabelWidth = 0f
|
||||||
|
for (item in legendItems) {
|
||||||
|
maxLegendLabelWidth =
|
||||||
|
kotlin.math.max(maxLegendLabelWidth, legendPaint.measureText(item.label!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
val legendContentActualLeft =
|
||||||
|
(width - marginRight - legendPadding - legendColorBoxSize - legendTextMargin - maxLegendLabelWidth)
|
||||||
|
val legendContentActualRight =
|
||||||
|
(width - marginRight - legendPadding) // Right edge of the color box
|
||||||
|
|
||||||
|
val legendContentActualTop = graphTop + legendPadding // Top edge of the first color box
|
||||||
|
val legendContentActualBottom =
|
||||||
|
legendContentActualTop + (legendItems.size - 1) * legendEntryHeight + legendColorBoxSize // Bottom edge of the last color box
|
||||||
|
|
||||||
|
val legendBgPaint = Paint().apply {
|
||||||
|
color = if (isNightMode) {
|
||||||
|
Color.argb(210, 0, 0, 0)
|
||||||
|
} else {
|
||||||
|
Color.argb(210, 255, 255, 255)
|
||||||
|
}
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
canvas.drawRoundRect(
|
||||||
|
legendContentActualLeft,
|
||||||
|
legendContentActualTop,
|
||||||
|
legendContentActualRight,
|
||||||
|
legendContentActualBottom,
|
||||||
|
5f,
|
||||||
|
5f,
|
||||||
|
legendBgPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentLegendY = graphTop + legendPadding
|
||||||
|
|
||||||
|
for (line in legendItems) {
|
||||||
|
// Draw color box
|
||||||
|
legendColorPaint.color = line.color
|
||||||
|
canvas.drawRect(
|
||||||
|
width - marginRight - legendPadding - legendColorBoxSize, // left
|
||||||
|
currentLegendY, // top
|
||||||
|
width - marginRight - legendPadding, // right
|
||||||
|
currentLegendY + legendColorBoxSize, // bottom
|
||||||
|
legendColorPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw label text
|
||||||
|
canvas.drawText(
|
||||||
|
line.label!!,
|
||||||
|
width - marginRight - legendPadding - legendColorBoxSize - legendTextMargin - legendPaint.measureText(
|
||||||
|
line.label
|
||||||
|
), // x: Align text to the left of the color box
|
||||||
|
currentLegendY + legendColorBoxSize / 2 + legendTextSize / 3, // y: Vertically center text with color box
|
||||||
|
legendPaint
|
||||||
|
)
|
||||||
|
currentLegendY += legendEntryHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -42,8 +42,6 @@ import de.timklge.karooheadwind.KarooHeadwindExtension
|
|||||||
import de.timklge.karooheadwind.RefreshRate
|
import de.timklge.karooheadwind.RefreshRate
|
||||||
import de.timklge.karooheadwind.RoundLocationSetting
|
import de.timklge.karooheadwind.RoundLocationSetting
|
||||||
import de.timklge.karooheadwind.WeatherDataProvider
|
import de.timklge.karooheadwind.WeatherDataProvider
|
||||||
import de.timklge.karooheadwind.WindDirectionIndicatorSetting
|
|
||||||
import de.timklge.karooheadwind.WindDirectionIndicatorTextSetting
|
|
||||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||||
import de.timklge.karooheadwind.saveSettings
|
import de.timklge.karooheadwind.saveSettings
|
||||||
import de.timklge.karooheadwind.streamSettings
|
import de.timklge.karooheadwind.streamSettings
|
||||||
@ -64,16 +62,6 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
|||||||
val karooSystem = remember { KarooSystemService(ctx) }
|
val karooSystem = remember { KarooSystemService(ctx) }
|
||||||
|
|
||||||
var refreshRateSetting by remember { mutableStateOf(RefreshRate.STANDARD) }
|
var refreshRateSetting by remember { mutableStateOf(RefreshRate.STANDARD) }
|
||||||
var selectedWindDirectionIndicatorTextSetting by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
WindDirectionIndicatorTextSetting.HEADWIND_SPEED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var selectedWindDirectionIndicatorSetting by remember {
|
|
||||||
mutableStateOf(
|
|
||||||
WindDirectionIndicatorSetting.HEADWIND_DIRECTION
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedRoundLocationSetting by remember { mutableStateOf(RoundLocationSetting.KM_3) }
|
var selectedRoundLocationSetting by remember { mutableStateOf(RoundLocationSetting.KM_3) }
|
||||||
var forecastKmPerHour by remember { mutableStateOf("20") }
|
var forecastKmPerHour by remember { mutableStateOf("20") }
|
||||||
@ -88,8 +76,6 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
|||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
ctx.streamSettings(karooSystem).collect { settings ->
|
ctx.streamSettings(karooSystem).collect { settings ->
|
||||||
selectedWindDirectionIndicatorTextSetting = settings.windDirectionIndicatorTextSetting
|
|
||||||
selectedWindDirectionIndicatorSetting = settings.windDirectionIndicatorSetting
|
|
||||||
selectedRoundLocationSetting = settings.roundLocationTo
|
selectedRoundLocationSetting = settings.roundLocationTo
|
||||||
forecastKmPerHour = settings.forecastedKmPerHour.toString()
|
forecastKmPerHour = settings.forecastedKmPerHour.toString()
|
||||||
forecastMilesPerHour = settings.forecastedMilesPerHour.toString()
|
forecastMilesPerHour = settings.forecastedMilesPerHour.toString()
|
||||||
@ -118,8 +104,6 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
|||||||
|
|
||||||
val newSettings = HeadwindSettings(
|
val newSettings = HeadwindSettings(
|
||||||
welcomeDialogAccepted = true,
|
welcomeDialogAccepted = true,
|
||||||
windDirectionIndicatorSetting = selectedWindDirectionIndicatorSetting,
|
|
||||||
windDirectionIndicatorTextSetting = selectedWindDirectionIndicatorTextSetting,
|
|
||||||
roundLocationTo = selectedRoundLocationSetting,
|
roundLocationTo = selectedRoundLocationSetting,
|
||||||
forecastedMilesPerHour = forecastMilesPerHour.toIntOrNull()?.coerceIn(3, 30) ?: 12,
|
forecastedMilesPerHour = forecastMilesPerHour.toIntOrNull()?.coerceIn(3, 30) ?: 12,
|
||||||
forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20,
|
forecastedKmPerHour = forecastKmPerHour.toIntOrNull()?.coerceIn(5, 50) ?: 20,
|
||||||
@ -165,37 +149,6 @@ fun SettingsScreen(onFinish: () -> Unit) {
|
|||||||
refreshRateSetting = RefreshRate.entries.find { unit -> unit.id == selectedOption.id }!!
|
refreshRateSetting = RefreshRate.entries.find { unit -> unit.id == selectedOption.id }!!
|
||||||
}
|
}
|
||||||
|
|
||||||
val windDirectionIndicatorSettingDropdownOptions =
|
|
||||||
WindDirectionIndicatorSetting.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
|
|
||||||
val windDirectionIndicatorSettingSelection by remember(selectedWindDirectionIndicatorSetting) {
|
|
||||||
mutableStateOf(windDirectionIndicatorSettingDropdownOptions.find { option -> option.id == selectedWindDirectionIndicatorSetting.id }!!)
|
|
||||||
}
|
|
||||||
Dropdown(
|
|
||||||
label = "Wind Direction Indicator",
|
|
||||||
options = windDirectionIndicatorSettingDropdownOptions,
|
|
||||||
selected = windDirectionIndicatorSettingSelection
|
|
||||||
) { selectedOption ->
|
|
||||||
selectedWindDirectionIndicatorSetting =
|
|
||||||
WindDirectionIndicatorSetting.entries.find { unit -> unit.id == selectedOption.id }!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val windDirectionIndicatorTextSettingDropdownOptions =
|
|
||||||
WindDirectionIndicatorTextSetting.entries.toList()
|
|
||||||
.map { unit -> DropdownOption(unit.id, unit.label) }
|
|
||||||
val windDirectionIndicatorTextSettingSelection by remember(
|
|
||||||
selectedWindDirectionIndicatorTextSetting
|
|
||||||
) {
|
|
||||||
mutableStateOf(windDirectionIndicatorTextSettingDropdownOptions.find { option -> option.id == selectedWindDirectionIndicatorTextSetting.id }!!)
|
|
||||||
}
|
|
||||||
Dropdown(
|
|
||||||
label = "Text on Headwind Indicator",
|
|
||||||
options = windDirectionIndicatorTextSettingDropdownOptions,
|
|
||||||
selected = windDirectionIndicatorTextSettingSelection
|
|
||||||
) { selectedOption ->
|
|
||||||
selectedWindDirectionIndicatorTextSetting =
|
|
||||||
WindDirectionIndicatorTextSetting.entries.find { unit -> unit.id == selectedOption.id }!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val roundLocationDropdownOptions = RoundLocationSetting.entries.toList()
|
val roundLocationDropdownOptions = RoundLocationSetting.entries.toList()
|
||||||
.map { unit -> DropdownOption(unit.id, unit.label) }
|
.map { unit -> DropdownOption(unit.id, unit.label) }
|
||||||
val roundLocationInitialSelection by remember(selectedRoundLocationSetting) {
|
val roundLocationInitialSelection by remember(selectedRoundLocationSetting) {
|
||||||
|
|||||||
@ -37,6 +37,9 @@ import de.timklge.karooheadwind.streamCurrentWeatherData
|
|||||||
import de.timklge.karooheadwind.streamStats
|
import de.timklge.karooheadwind.streamStats
|
||||||
import de.timklge.karooheadwind.streamUpcomingRoute
|
import de.timklge.karooheadwind.streamUpcomingRoute
|
||||||
import de.timklge.karooheadwind.streamUserProfile
|
import de.timklge.karooheadwind.streamUserProfile
|
||||||
|
import de.timklge.karooheadwind.util.celciusInUserUnit
|
||||||
|
import de.timklge.karooheadwind.util.millimetersInUserUnit
|
||||||
|
import de.timklge.karooheadwind.util.msInUserUnit
|
||||||
import io.hammerhead.karooext.KarooSystemService
|
import io.hammerhead.karooext.KarooSystemService
|
||||||
import io.hammerhead.karooext.models.UserProfile
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -110,10 +113,10 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
|||||||
baseBitmap = baseBitmap,
|
baseBitmap = baseBitmap,
|
||||||
current = WeatherInterpretation.fromWeatherCode(currentWeatherData?.weatherCode),
|
current = WeatherInterpretation.fromWeatherCode(currentWeatherData?.weatherCode),
|
||||||
windBearing = currentWeatherData?.windDirection?.roundToInt() ?: 0,
|
windBearing = currentWeatherData?.windDirection?.roundToInt() ?: 0,
|
||||||
windSpeed = currentWeatherData?.windSpeed?.roundToInt() ?: 0,
|
windSpeed = msInUserUnit(currentWeatherData?.windSpeed ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||||
windGusts = currentWeatherData?.windGusts?.roundToInt() ?: 0,
|
windGusts = msInUserUnit(currentWeatherData?.windGusts ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||||
precipitation = currentWeatherData?.precipitation ?: 0.0,
|
precipitation = millimetersInUserUnit(currentWeatherData?.precipitation ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL),
|
||||||
temperature = currentWeatherData?.temperature?.toInt() ?: 0,
|
temperature = celciusInUserUnit(currentWeatherData?.temperature ?: 0.0, profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||||
temperatureUnit = if(profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
temperatureUnit = if(profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||||
timeLabel = formattedTime,
|
timeLabel = formattedTime,
|
||||||
dateLabel = formattedDate,
|
dateLabel = formattedDate,
|
||||||
@ -230,10 +233,10 @@ fun WeatherScreen(onFinish: () -> Unit) {
|
|||||||
baseBitmap,
|
baseBitmap,
|
||||||
current = interpretation,
|
current = interpretation,
|
||||||
windBearing = weatherData?.windDirection?.roundToInt() ?: 0,
|
windBearing = weatherData?.windDirection?.roundToInt() ?: 0,
|
||||||
windSpeed = weatherData?.windSpeed?.roundToInt() ?: 0,
|
windSpeed = msInUserUnit(weatherData?.windSpeed ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||||
windGusts = weatherData?.windGusts?.roundToInt() ?: 0,
|
windGusts = msInUserUnit(weatherData?.windGusts ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||||
precipitation = weatherData?.precipitation ?: 0.0,
|
precipitation = millimetersInUserUnit(weatherData?.precipitation ?: 0.0, profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL),
|
||||||
temperature = weatherData?.temperature?.toInt() ?: 0,
|
temperature = celciusInUserUnit(weatherData?.temperature ?: 0.0, profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL).roundToInt(),
|
||||||
temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT,
|
||||||
timeLabel = formattedForecastTime,
|
timeLabel = formattedForecastTime,
|
||||||
dateLabel = formattedForecastDate,
|
dateLabel = formattedForecastDate,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@ -49,7 +50,7 @@ fun WeatherWidget(
|
|||||||
isImperial: Boolean,
|
isImperial: Boolean,
|
||||||
isNight: Boolean
|
isNight: Boolean
|
||||||
) {
|
) {
|
||||||
val fontSize = 20.sp
|
val fontSize = 18.sp
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
modifier = Modifier.fillMaxWidth().padding(5.dp),
|
||||||
@ -98,22 +99,20 @@ fun WeatherWidget(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Weather icon (larger)
|
Image(
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = getWeatherIcon(current, isNight)),
|
painter = painterResource(id = getWeatherIcon(current, isNight)),
|
||||||
contentDescription = "Current weather",
|
contentDescription = "Current weather",
|
||||||
modifier = Modifier.size(72.dp)
|
modifier = Modifier.size(72.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(horizontalAlignment = Alignment.End) {
|
Column(horizontalAlignment = Alignment.End) {
|
||||||
// Temperature (larger)
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.thermometer),
|
painter = painterResource(id = R.drawable.thermometer),
|
||||||
contentDescription = "Temperature",
|
contentDescription = "Temperature",
|
||||||
modifier = Modifier.size(18.dp)
|
modifier = Modifier.size(18.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
@ -135,8 +134,8 @@ fun WeatherWidget(
|
|||||||
val precipitationProbabilityLabel =
|
val precipitationProbabilityLabel =
|
||||||
if (precipitationProbability != null) "${precipitationProbability}% " else ""
|
if (precipitationProbability != null) "${precipitationProbability}% " else ""
|
||||||
|
|
||||||
Icon(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.droplet_regular),
|
painter = painterResource(id = R.drawable.droplet),
|
||||||
contentDescription = "Precipitation",
|
contentDescription = "Precipitation",
|
||||||
modifier = Modifier.size(18.dp)
|
modifier = Modifier.size(18.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
package de.timklge.karooheadwind.util
|
||||||
|
|
||||||
|
fun celciusInUserUnit(celcius: Double, isImperial: Boolean): Double {
|
||||||
|
return if (isImperial) {
|
||||||
|
celcius * 9.0 / 5 + 32.0
|
||||||
|
} else {
|
||||||
|
celcius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun millimetersInUserUnit(millimeters: Double, isImperial: Boolean): Double {
|
||||||
|
return if (isImperial) {
|
||||||
|
millimeters / 25.4
|
||||||
|
} else {
|
||||||
|
millimeters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the given speed value (m / s) in user unit (km/h or mph)
|
||||||
|
fun msInUserUnit(ms: Double, isImperial: Boolean): Double {
|
||||||
|
return if (isImperial) {
|
||||||
|
ms * 2.2369362920544
|
||||||
|
} else {
|
||||||
|
ms * 3.6
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,12 +6,12 @@ import kotlinx.serialization.Serializable
|
|||||||
data class WeatherData(
|
data class WeatherData(
|
||||||
val time: Long,
|
val time: Long,
|
||||||
val temperature: Double,
|
val temperature: Double,
|
||||||
val relativeHumidity: Double? = null,
|
val relativeHumidity: Int,
|
||||||
val precipitation: Double,
|
val precipitation: Double,
|
||||||
val precipitationProbability: Double? = null,
|
val precipitationProbability: Double? = null,
|
||||||
val cloudCover: Double? = null,
|
val cloudCover: Double,
|
||||||
val sealevelPressure: Double? = null,
|
val sealevelPressure: Double,
|
||||||
val surfacePressure: Double? = null,
|
val surfacePressure: Double,
|
||||||
val windSpeed: Double,
|
val windSpeed: Double,
|
||||||
val windDirection: Double,
|
val windDirection: Double,
|
||||||
val windGusts: Double,
|
val windGusts: Double,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ data class OpenMeteoWeatherData(
|
|||||||
@SerialName("precipitation") val precipitation: Double,
|
@SerialName("precipitation") val precipitation: Double,
|
||||||
@SerialName("cloud_cover") val cloudCover: Int,
|
@SerialName("cloud_cover") val cloudCover: Int,
|
||||||
@SerialName("surface_pressure") val surfacePressure: Double,
|
@SerialName("surface_pressure") val surfacePressure: Double,
|
||||||
@SerialName("pressure_msl") val sealevelPressure: Double? = null,
|
@SerialName("pressure_msl") val sealevelPressure: Double,
|
||||||
@SerialName("wind_speed_10m") val windSpeed: Double,
|
@SerialName("wind_speed_10m") val windSpeed: Double,
|
||||||
@SerialName("wind_direction_10m") val windDirection: Double,
|
@SerialName("wind_direction_10m") val windDirection: Double,
|
||||||
@SerialName("wind_gusts_10m") val windGusts: Double,
|
@SerialName("wind_gusts_10m") val windGusts: Double,
|
||||||
@ -21,7 +21,7 @@ data class OpenMeteoWeatherData(
|
|||||||
) {
|
) {
|
||||||
fun toWeatherData(): WeatherData = WeatherData(
|
fun toWeatherData(): WeatherData = WeatherData(
|
||||||
temperature = temperature,
|
temperature = temperature,
|
||||||
relativeHumidity = relativeHumidity.toDouble(),
|
relativeHumidity = relativeHumidity,
|
||||||
precipitation = precipitation,
|
precipitation = precipitation,
|
||||||
cloudCover = cloudCover.toDouble(),
|
cloudCover = cloudCover.toDouble(),
|
||||||
surfacePressure = surfacePressure,
|
surfacePressure = surfacePressure,
|
||||||
@ -32,7 +32,7 @@ data class OpenMeteoWeatherData(
|
|||||||
weatherCode = weatherCode,
|
weatherCode = weatherCode,
|
||||||
time = time,
|
time = time,
|
||||||
isForecast = false,
|
isForecast = false,
|
||||||
isNight = isDay == 0
|
isNight = isDay == 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,11 @@ data class OpenMeteoWeatherForecastData(
|
|||||||
@SerialName("wind_speed_10m") val windSpeed: List<Double>,
|
@SerialName("wind_speed_10m") val windSpeed: List<Double>,
|
||||||
@SerialName("wind_direction_10m") val windDirection: List<Double>,
|
@SerialName("wind_direction_10m") val windDirection: List<Double>,
|
||||||
@SerialName("wind_gusts_10m") val windGusts: List<Double>,
|
@SerialName("wind_gusts_10m") val windGusts: List<Double>,
|
||||||
|
@SerialName("cloud_cover") val cloudCover: List<Double>,
|
||||||
|
@SerialName("surface_pressure") val surfacePressure: 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>,
|
||||||
) {
|
) {
|
||||||
fun toWeatherData(): List<WeatherData> {
|
fun toWeatherData(): List<WeatherData> {
|
||||||
return time.mapIndexed { index, t ->
|
return time.mapIndexed { index, t ->
|
||||||
@ -29,6 +33,10 @@ data class OpenMeteoWeatherForecastData(
|
|||||||
isNight = isDay[index] == 0,
|
isNight = isDay[index] == 0,
|
||||||
time = t,
|
time = t,
|
||||||
isForecast = true,
|
isForecast = true,
|
||||||
|
cloudCover = cloudCover[index],
|
||||||
|
surfacePressure = surfacePressure[index],
|
||||||
|
sealevelPressure = sealevelPressure[index],
|
||||||
|
relativeHumidity = relativeHumidity[index],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,7 @@ package de.timklge.karooheadwind.weatherprovider.openmeteo
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import de.timklge.karooheadwind.HeadwindSettings
|
import de.timklge.karooheadwind.HeadwindSettings
|
||||||
import de.timklge.karooheadwind.KarooHeadwindExtension
|
import de.timklge.karooheadwind.KarooHeadwindExtension
|
||||||
import de.timklge.karooheadwind.PrecipitationUnit
|
|
||||||
import de.timklge.karooheadwind.TemperatureUnit
|
|
||||||
import de.timklge.karooheadwind.WeatherDataProvider
|
import de.timklge.karooheadwind.WeatherDataProvider
|
||||||
import de.timklge.karooheadwind.WindUnit
|
|
||||||
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
import de.timklge.karooheadwind.datatypes.GpsCoordinates
|
||||||
import de.timklge.karooheadwind.jsonWithUnknownKeys
|
import de.timklge.karooheadwind.jsonWithUnknownKeys
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
||||||
@ -28,16 +25,12 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
class OpenMeteoWeatherProvider : WeatherProvider {
|
class OpenMeteoWeatherProvider : WeatherProvider {
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
private suspend fun makeOpenMeteoWeatherRequest(karooSystemService: KarooSystemService, gpsCoordinates: List<GpsCoordinates>, settings: HeadwindSettings, profile: UserProfile?): HttpResponseState.Complete {
|
private suspend fun makeOpenMeteoWeatherRequest(karooSystemService: KarooSystemService, gpsCoordinates: List<GpsCoordinates>): HttpResponseState.Complete {
|
||||||
val precipitationUnit = if (profile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH
|
|
||||||
val temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT
|
|
||||||
val windUnit = if (profile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) WindUnit.KILOMETERS_PER_HOUR else WindUnit.MILES_PER_HOUR
|
|
||||||
|
|
||||||
val response = callbackFlow {
|
val response = callbackFlow {
|
||||||
// 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&timeformat=unixtime&past_hours=0&forecast_days=1&forecast_hours=12&wind_speed_unit=${windUnit.id}&precipitation_unit=${precipitationUnit.id}&temperature_unit=${temperatureUnit.id}"
|
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"
|
||||||
|
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Http request to ${url}...")
|
Log.d(KarooHeadwindExtension.TAG, "Http request to ${url}...")
|
||||||
|
|
||||||
@ -84,7 +77,7 @@ class OpenMeteoWeatherProvider : WeatherProvider {
|
|||||||
settings: HeadwindSettings,
|
settings: HeadwindSettings,
|
||||||
profile: UserProfile?
|
profile: UserProfile?
|
||||||
): WeatherDataResponse {
|
): WeatherDataResponse {
|
||||||
val openMeteoResponse = makeOpenMeteoWeatherRequest(karooSystem, coordinates, settings, profile)
|
val openMeteoResponse = makeOpenMeteoWeatherRequest(karooSystem, coordinates)
|
||||||
val responseBody = openMeteoResponse.body?.let { String(it) } ?: throw WeatherProviderException(500, "Null response from OpenMeteo")
|
val responseBody = openMeteoResponse.body?.let { String(it) } ?: throw WeatherProviderException(500, "Null response from OpenMeteo")
|
||||||
|
|
||||||
val weatherData = if (coordinates.size == 1) {
|
val weatherData = if (coordinates.size == 1) {
|
||||||
|
|||||||
@ -33,7 +33,7 @@ data class OpenWeatherMapForecastData(
|
|||||||
|
|
||||||
return WeatherData(
|
return WeatherData(
|
||||||
temperature = temp,
|
temperature = temp,
|
||||||
relativeHumidity = humidity.toDouble(),
|
relativeHumidity = humidity,
|
||||||
precipitation = rain?.h1 ?: 0.0,
|
precipitation = rain?.h1 ?: 0.0,
|
||||||
cloudCover = clouds.toDouble(),
|
cloudCover = clouds.toDouble(),
|
||||||
surfacePressure = pressure.toDouble(),
|
surfacePressure = pressure.toDouble(),
|
||||||
|
|||||||
@ -23,7 +23,7 @@ data class OpenWeatherMapWeatherData(
|
|||||||
|
|
||||||
fun toWeatherData(): WeatherData = WeatherData(
|
fun toWeatherData(): WeatherData = WeatherData(
|
||||||
temperature = temp,
|
temperature = temp,
|
||||||
relativeHumidity = humidity.toDouble(),
|
relativeHumidity = humidity,
|
||||||
precipitation = rain?.h1 ?: 0.0,
|
precipitation = rain?.h1 ?: 0.0,
|
||||||
cloudCover = clouds.toDouble(),
|
cloudCover = clouds.toDouble(),
|
||||||
surfacePressure = pressure.toDouble(),
|
surfacePressure = pressure.toDouble(),
|
||||||
|
|||||||
@ -69,7 +69,7 @@ class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvide
|
|||||||
profile: UserProfile?
|
profile: UserProfile?
|
||||||
): WeatherDataResponse {
|
): WeatherDataResponse {
|
||||||
|
|
||||||
val response = makeOpenWeatherMapRequest(karooSystem, coordinates, apiKey, profile)
|
val response = makeOpenWeatherMapRequest(karooSystem, coordinates, apiKey)
|
||||||
val responseBody = response.body?.let { String(it) } ?: throw Exception("Null response from OpenWeatherMap")
|
val responseBody = response.body?.let { String(it) } ?: throw Exception("Null response from OpenWeatherMap")
|
||||||
|
|
||||||
val responses = mutableListOf<WeatherDataForLocation>()
|
val responses = mutableListOf<WeatherDataForLocation>()
|
||||||
@ -89,21 +89,15 @@ class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvide
|
|||||||
private suspend fun makeOpenWeatherMapRequest(
|
private suspend fun makeOpenWeatherMapRequest(
|
||||||
service: KarooSystemService,
|
service: KarooSystemService,
|
||||||
coordinates: List<GpsCoordinates>,
|
coordinates: List<GpsCoordinates>,
|
||||||
apiKey: String,
|
apiKey: String
|
||||||
profile: UserProfile?
|
|
||||||
): HttpResponseState.Complete {
|
): HttpResponseState.Complete {
|
||||||
val response = callbackFlow {
|
val response = callbackFlow {
|
||||||
// OpenWeatherMap only supports setting imperial or metric units for all measurements, not individually for distance / temperature
|
// OpenWeatherMap only supports setting imperial or metric units for all measurements, not individually for distance / temperature
|
||||||
val unitsString = if (profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL || profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL) {
|
|
||||||
"imperial"
|
|
||||||
} else {
|
|
||||||
"metric"
|
|
||||||
}
|
|
||||||
val coordinate = coordinates.first()
|
val coordinate = coordinates.first()
|
||||||
|
|
||||||
// URL API 3.0 with onecall endpoint
|
// URL API 3.0 with onecall endpoint
|
||||||
val url = "https://api.openweathermap.org/data/3.0/onecall?lat=${coordinate.lat}&lon=${coordinate.lon}" +
|
val url = "https://api.openweathermap.org/data/3.0/onecall?lat=${coordinate.lat}&lon=${coordinate.lon}" +
|
||||||
"&appid=$apiKey&exclude=minutely,daily,alerts&units=${unitsString}"
|
"&appid=$apiKey&exclude=minutely,daily,alerts&units=metric"
|
||||||
|
|
||||||
Log.d(KarooHeadwindExtension.TAG, "Http request to OpenWeatherMap API 3.0: $url")
|
Log.d(KarooHeadwindExtension.TAG, "Http request to OpenWeatherMap API 3.0: $url")
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
40
app/src/main/res/drawable/cloud.xml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:pathData="m106.54,56.4c-0.68,0 -1.35,0.05 -2.01,0.13 0.52,-1.75 0.81,-3.6 0.81,-5.52 0,-10.7 -8.67,-19.37 -19.37,-19.37 -4.55,0 -8.73,1.58 -12.04,4.21 -4.45,-8.44 -13.3,-14.2 -23.51,-14.2 -14.67,0 -26.56,11.89 -26.56,26.56 0,0.53 0.02,1.05 0.05,1.57 -11.06,1.36 -19.63,10.78 -19.63,22.2 0,12.22 9.81,22.15 21.98,22.36 4.08,7.08 12.2,11.94 21.59,12.01 9.08,0.07 17.03,-4.36 21.33,-11 3.47,3.79 8.44,6.19 13.99,6.19 7.94,0 14.74,-4.89 17.56,-11.81 1.82,0.65 3.76,1.03 5.8,1.03 9.49,0 17.18,-7.69 17.18,-17.18 0.01,-9.49 -7.68,-17.18 -17.17,-17.18z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="36.79"
|
||||||
|
android:centerY="18.87"
|
||||||
|
android:gradientRadius="114.63"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.14" android:color="#FFE3F2FD"/>
|
||||||
|
<item android:offset="1" android:color="#FF90CAF9"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m47.86,105c-8.55,0 -16.33,-4.46 -20.3,-11.34 -0.26,-0.46 -0.75,-0.78 -1.27,-0.79 -11.31,-0.2 -20.51,-9.57 -20.51,-20.87 0,-10.52 7.87,-19.42 18.31,-20.7 0.79,-0.1 1.36,-0.79 1.31,-1.58 -0.03,-0.5 -0.05,-1 -0.05,-1.5 0,-13.82 11.24,-25.06 25.06,-25.06 9.33,0 17.83,5.13 22.18,13.4 0.21,0.4 0.58,0.68 1.02,0.77 0.1,0.02 0.21,0.03 0.31,0.03 0.34,0 0.67,-0.11 0.93,-0.33 3.2,-2.54 7.04,-3.89 11.11,-3.89 9.86,0 17.87,8.02 17.87,17.87 0,1.71 -0.25,3.42 -0.75,5.09 -0.14,0.48 -0.03,1.01 0.29,1.39 0.29,0.34 0.71,0.53 1.15,0.53 0.06,0 0.12,0 0.17,-0.01 0.68,-0.08 1.28,-0.12 1.83,-0.12 8.64,0 15.68,7.03 15.68,15.68s-7.03,15.68 -15.68,15.68c-1.78,0 -3.56,-0.32 -5.3,-0.94 -0.17,-0.06 -0.34,-0.09 -0.51,-0.09 -0.59,0 -1.15,0.35 -1.39,0.93 -2.7,6.61 -9.05,10.88 -16.18,10.88 -4.88,0 -9.58,-2.08 -12.89,-5.71 -0.29,-0.31 -0.69,-0.49 -1.11,-0.49h-0.12c-0.46,0.04 -0.88,0.36 -1.13,0.76 -4.13,6.37 -11.73,10.4 -19.85,10.4h-0.18z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillAlpha="0.2">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="37.48"
|
||||||
|
android:centerY="20.46"
|
||||||
|
android:gradientRadius="111.26"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.14" android:color="#FFE3F2FD"/>
|
||||||
|
<item android:offset="1" android:color="#FF90CAF9"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m50.42,24.65c8.77,0 16.76,4.83 20.85,12.6 0.42,0.79 1.16,1.35 2.04,1.54 0.2,0.04 0.41,0.06 0.62,0.06 0.67,0 1.33,-0.23 1.87,-0.65 2.93,-2.33 6.45,-3.56 10.17,-3.56 9.03,0 16.37,7.34 16.37,16.37 0,1.57 -0.23,3.14 -0.68,4.67 -0.29,0.97 -0.07,2.01 0.58,2.78 0.57,0.68 1.42,1.07 2.3,1.07 0.12,0 0.23,-0.01 0.35,-0.02 0.62,-0.07 1.16,-0.11 1.66,-0.11 7.82,0 14.18,6.36 14.18,14.18s-6.36,14.18 -14.18,14.18c-1.61,0 -3.22,-0.29 -4.79,-0.85 -0.33,-0.12 -0.68,-0.18 -1.01,-0.18 -1.19,0 -2.3,0.71 -2.78,1.87 -2.47,6.04 -8.27,9.95 -14.79,9.95 -4.46,0 -8.75,-1.9 -11.78,-5.22 -0.57,-0.63 -1.38,-0.98 -2.22,-0.98 -0.08,0 -0.16,0 -0.25,0.01 -0.93,0.08 -1.77,0.4 -2.27,1.18 -3.86,5.94 -10.98,9.46 -18.6,9.46h-0.18c-8.02,0 -15.31,-3.92 -19.01,-10.34 -0.53,-0.91 -1.49,-1.4 -2.55,-1.42 -10.5,-0.18 -19.04,-8.81 -19.04,-19.3 0,-9.76 7.3,-18 16.99,-19.18 1.57,-0.19 2.72,-1.56 2.63,-3.14 -0.03,-0.53 -0.05,-0.98 -0.05,-1.4 0.01,-13 10.58,-23.57 23.57,-23.57m0,-3c-14.67,0 -26.56,11.89 -26.56,26.56 0,0.53 0.02,1.06 0.05,1.58 -11.06,1.36 -19.63,10.77 -19.63,22.19 0,12.22 9.81,21.97 21.98,22.17 4.08,7.08 12.2,11.84 21.59,11.84h0.21c8.99,0 16.85,-4.25 21.12,-10.84 3.47,3.8 8.45,6.29 14,6.29 7.94,0 14.74,-4.84 17.56,-11.77 1.82,0.65 3.76,1.05 5.8,1.05 9.49,0 17.18,-7.68 17.18,-17.16s-7.69,-17.17 -17.18,-17.17c-0.68,0 -1.35,0.05 -2.01,0.13 0.52,-1.75 0.81,-3.6 0.81,-5.52 0,-10.7 -8.67,-19.37 -19.37,-19.37 -4.55,0 -8.73,1.58 -12.04,4.21 -4.45,-8.43 -13.31,-14.19 -23.51,-14.19z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
</vector>
|
||||||
49
app/src/main/res/drawable/cloud_with_light_rain.xml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:pathData="m106.97,39.56c-0.68,0 -1.35,0.05 -2,0.13 0.52,-1.75 0.81,-3.59 0.81,-5.51 0,-10.68 -8.66,-19.34 -19.34,-19.34 -4.55,0 -8.72,1.58 -12.02,4.21 -4.45,-8.43 -13.29,-14.18 -23.48,-14.18 -14.64,0 -26.52,11.87 -26.52,26.52 0,0.53 0.02,1.05 0.05,1.57 -11.03,1.35 -19.58,10.75 -19.58,22.15 0,12.2 9.79,22.11 21.94,22.32 4.07,7.07 12.18,11.92 21.55,11.99 9.06,0.07 17,-4.35 21.3,-10.98 3.46,3.79 8.43,6.18 13.96,6.18 7.93,0 14.71,-4.88 17.53,-11.79 1.81,0.65 3.76,1.02 5.79,1.02 9.47,0 17.15,-7.68 17.15,-17.15 0,-9.46 -7.68,-17.14 -17.14,-17.14z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="59.47"
|
||||||
|
android:centerY="-5.18"
|
||||||
|
android:gradientRadius="120.22"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.26" android:color="#FFE3F2FD"/>
|
||||||
|
<item android:offset="0.92" android:color="#FF90CAF9"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m50.94,7.87c8.75,0 16.73,4.82 20.81,12.57 0.42,0.79 1.16,1.35 2.04,1.54 0.2,0.04 0.41,0.06 0.62,0.06 0.67,0 1.33,-0.23 1.87,-0.65 2.92,-2.32 6.43,-3.55 10.15,-3.55 9.01,0 16.34,7.33 16.34,16.34 0,1.56 -0.23,3.13 -0.68,4.66 -0.29,0.97 -0.07,2.01 0.58,2.78 0.57,0.68 1.42,1.07 2.3,1.07 0.12,0 0.23,-0.01 0.35,-0.02 0.62,-0.07 1.16,-0.11 1.66,-0.11 7.8,0 14.15,6.35 14.15,14.15s-6.35,14.15 -14.15,14.15c-1.61,0 -3.21,-0.29 -4.78,-0.85 -0.33,-0.12 -0.68,-0.18 -1.01,-0.18 -1.19,0 -2.3,0.71 -2.78,1.87 -2.46,6.03 -8.25,9.92 -14.76,9.92 -4.45,0 -8.74,-1.9 -11.76,-5.21 -0.57,-0.63 -1.38,-0.98 -2.22,-0.98 -0.08,0 -0.16,0 -0.25,0.01 -0.93,0.08 -1.77,0.37 -2.27,1.15 -3.85,5.93 -10.96,9.41 -18.56,9.41h-0.19c-8.01,0 -15.28,-3.87 -18.97,-10.28 -0.53,-0.91 -1.49,-1.38 -2.55,-1.4 -10.47,-0.18 -18.99,-8.79 -18.99,-19.26 0,-9.74 7.29,-17.95 16.95,-19.14 1.57,-0.19 2.72,-1.56 2.63,-3.14 -0.03,-0.52 -0.05,-0.98 -0.05,-1.4 0.01,-12.96 10.56,-23.51 23.52,-23.51m0,-3c-14.64,0 -26.52,11.87 -26.52,26.52 0,0.53 0.02,1.06 0.05,1.58 -11.03,1.35 -19.58,10.74 -19.58,22.14 0,12.2 9.79,21.89 21.94,22.1 4.07,7.07 12.17,11.79 21.55,11.79h0.21c8.98,0 16.82,-4.2 21.08,-10.78 3.46,3.79 8.43,6.29 13.98,6.29 7.93,0 14.71,-4.82 17.53,-11.74 1.81,0.65 3.76,1.05 5.79,1.05 9.47,0 17.15,-7.66 17.15,-17.13s-7.68,-17.14 -17.15,-17.14c-0.68,0 -1.35,0.05 -2,0.13 0.52,-1.75 0.81,-3.59 0.81,-5.51 0,-10.68 -8.66,-19.34 -19.34,-19.34 -4.55,0 -8.72,1.58 -12.02,4.21 -4.45,-8.42 -13.29,-14.17 -23.48,-14.17z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m91.26,111.28c-1.78,3.88 -6.36,5.58 -10.24,3.8 -3.88,-1.78 -5.58,-6.36 -3.8,-10.24 1.78,-3.88 13.36,-11.57 13.36,-11.57 0,0 2.46,14.13 0.68,18.01z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m91.26,111.28c-1.78,3.88 -6.36,5.58 -10.24,3.8 -3.88,-1.78 -5.58,-6.36 -3.8,-10.24 1.78,-3.88 13.36,-11.57 13.36,-11.57 0,0 2.46,14.13 0.68,18.01z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m50.84,113.15c-1.78,3.88 -6.36,5.58 -10.24,3.8 -3.88,-1.78 -5.58,-6.36 -3.8,-10.24 1.78,-3.88 13.36,-11.57 13.36,-11.57 0,0 2.46,14.13 0.68,18.01z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m48.68,98.59c0.74,5.36 1.22,11.83 0.35,13.72 -0.93,2.03 -2.97,3.34 -5.21,3.34 -0.82,0 -1.62,-0.18 -2.38,-0.52 -2.87,-1.32 -4.13,-4.72 -2.82,-7.59 0.88,-1.91 5.78,-5.89 10.06,-8.95m1.48,-3.45c0,0 -11.58,7.7 -13.36,11.57 -1.78,3.88 -0.08,8.46 3.8,10.24 1.04,0.48 2.14,0.71 3.21,0.71 2.93,0 5.72,-1.67 7.02,-4.5 1.79,-3.89 -0.67,-18.02 -0.67,-18.02z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m89.1,96.72c0.74,5.36 1.22,11.83 0.35,13.72 -0.93,2.03 -2.97,3.34 -5.21,3.34 -0.82,0 -1.62,-0.18 -2.38,-0.52 -1.39,-0.64 -2.45,-1.78 -2.98,-3.21 -0.53,-1.43 -0.47,-2.99 0.16,-4.38 0.88,-1.91 5.78,-5.89 10.06,-8.95m1.48,-3.45c0,0 -11.58,7.7 -13.36,11.57 -1.78,3.88 -0.08,8.46 3.8,10.24 1.04,0.48 2.14,0.71 3.21,0.71 2.93,0 5.72,-1.67 7.02,-4.5 1.79,-3.89 -0.67,-18.02 -0.67,-18.02z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m89.1,96.72c0.74,5.36 1.22,11.83 0.35,13.72 -0.93,2.03 -2.97,3.34 -5.21,3.34 -0.82,0 -1.62,-0.18 -2.38,-0.52 -1.39,-0.64 -2.45,-1.78 -2.98,-3.21 -0.53,-1.43 -0.47,-2.99 0.16,-4.38 0.88,-1.91 5.78,-5.89 10.06,-8.95m1.48,-3.45c0,0 -11.58,7.7 -13.36,11.57 -1.78,3.88 -0.08,8.46 3.8,10.24 1.04,0.48 2.14,0.71 3.21,0.71 2.93,0 5.72,-1.67 7.02,-4.5 1.79,-3.89 -0.67,-18.02 -0.67,-18.02z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
</vector>
|
||||||
44
app/src/main/res/drawable/cloud_with_lightning.xml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:pathData="m51.04,121.76 l13.8,-24.27c0.39,-0.68 -0.1,-1.53 -0.89,-1.54l-26.19,-0.1c-0.95,0 -1.39,-1.17 -0.68,-1.8l34.24,-30.28c0.85,-0.75 2.12,0.23 1.6,1.24l-10.37,20.07c-0.35,0.68 0.14,1.5 0.91,1.5l26.78,0.17c0.94,0.01 1.38,1.16 0.68,1.79l-38.3,34.49c-0.87,0.79 -2.16,-0.25 -1.58,-1.27z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="59.01"
|
||||||
|
android:startY="73.87"
|
||||||
|
android:endX="71.67"
|
||||||
|
android:endY="118.12"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFFBC02D"/>
|
||||||
|
<item android:offset="1" android:color="#FFFFEB3B"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m65.41,73.01 l-5.52,10.69c-0.65,1.25 -0.6,2.72 0.13,3.93s2.01,1.94 3.42,1.94l21.69,0.14 -25.47,22.93 7.78,-13.68c0.71,-1.24 0.7,-2.78 -0.02,-4.01 -0.71,-1.23 -2.05,-2 -3.47,-2.01l-21.01,-0.08zM71.99,63.51c-0.23,0 -0.46,0.08 -0.67,0.27l-34.24,30.28c-0.71,0.63 -0.27,1.8 0.68,1.8l26.19,0.1c0.79,0 1.28,0.85 0.89,1.54l-13.8,24.27c-0.44,0.77 0.19,1.55 0.9,1.55 0.23,0 0.47,-0.08 0.68,-0.27l38.3,-34.49c0.7,-0.63 0.26,-1.79 -0.68,-1.79l-26.78,-0.17c-0.77,-0.01 -1.26,-0.82 -0.91,-1.5l10.36,-20.06c0.4,-0.79 -0.22,-1.53 -0.92,-1.53z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m103.18,36.62c-0.63,0 -1.24,0.05 -1.85,0.12 0.48,-1.61 0.74,-3.32 0.74,-5.08 0,-9.85 -7.99,-17.84 -17.84,-17.84 -4.19,0 -8.04,1.46 -11.09,3.88 -4.09,-7.78 -12.25,-13.08 -21.65,-13.08 -13.51,0 -24.46,10.95 -24.46,24.46 0,0.49 0.02,0.97 0.05,1.45 -10.19,1.24 -18.07,9.91 -18.07,20.43 0,11.26 9.03,20.4 20.24,20.59 3.76,6.52 11.23,10.99 19.88,11.06 8.36,0.06 15.69,-4.01 19.65,-10.13 3.19,3.49 7.78,5.7 12.88,5.7 7.31,0 13.57,-4.5 16.18,-10.88 1.67,0.6 3.47,0.95 5.34,0.95 8.74,0 15.82,-7.08 15.82,-15.82 -0.01,-8.73 -7.09,-15.81 -15.82,-15.81z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="59.36"
|
||||||
|
android:centerY="-4.66"
|
||||||
|
android:gradientRadius="110.91"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.26" android:color="#FFE3F2FD"/>
|
||||||
|
<item android:offset="0.92" android:color="#FF90CAF9"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m51.49,7.62c7.99,0 15.27,4.4 19,11.47 0.42,0.79 1.16,1.35 2.04,1.54 0.2,0.04 0.41,0.06 0.62,0.06 0.67,0 1.33,-0.23 1.87,-0.65 2.65,-2.11 5.84,-3.23 9.22,-3.23 8.18,0 14.84,6.66 14.84,14.84 0,1.42 -0.21,2.84 -0.62,4.23 -0.29,0.97 -0.07,2.01 0.58,2.78 0.57,0.68 1.42,1.07 2.3,1.07 0.12,0 0.23,-0.01 0.35,-0.02 0.56,-0.07 1.05,-0.1 1.5,-0.1 7.07,0 12.82,5.75 12.82,12.82s-5.75,12.82 -12.82,12.82c-1.45,0 -2.91,-0.26 -4.33,-0.77 -0.33,-0.12 -0.68,-0.18 -1.01,-0.18 -1.19,0 -2.3,0.71 -2.78,1.87 -2.23,5.47 -7.49,9.01 -13.4,9.01 -4.04,0 -7.93,-1.72 -10.68,-4.73 -0.57,-0.63 -1.38,-0.98 -2.22,-0.98 -0.08,0 -0.16,0 -0.25,0.01 -0.93,0.08 -1.77,0.77 -2.27,1.55 -3.51,5.42 -9.99,8.97 -16.93,8.97h-0.17c-7.31,0 -13.94,-3.91 -17.3,-9.76 -0.53,-0.91 -1.49,-1.58 -2.55,-1.6 -9.54,-0.16 -17.29,-8.1 -17.29,-17.63 0,-8.87 6.64,-16.4 15.44,-17.48 1.57,-0.19 2.72,-1.59 2.63,-3.17 -0.03,-0.48 -0.04,-0.9 -0.04,-1.29 -0.01,-11.82 9.62,-21.45 21.45,-21.45m0,-3c-13.51,0 -24.46,10.95 -24.46,24.46 0,0.49 0.02,0.98 0.05,1.46 -10.18,1.24 -18.07,9.91 -18.07,20.42 0,11.26 9.03,20.58 20.24,20.77 3.75,6.53 11.23,11.27 19.88,11.27h0.19c8.28,0 15.51,-4.26 19.45,-10.33 3.2,3.5 7.78,5.61 12.89,5.61 7.31,0 13.57,-4.55 16.18,-10.93 1.67,0.6 3.47,0.92 5.34,0.92 8.74,0 15.82,-7.09 15.82,-15.83s-7.08,-15.82 -15.82,-15.82c-0.63,0 -1.24,0.04 -1.85,0.11 0.48,-1.61 0.74,-3.32 0.74,-5.08 0,-9.85 -7.99,-17.84 -17.84,-17.84 -4.19,0 -8.04,1.46 -11.09,3.88 -4.09,-7.77 -12.25,-13.07 -21.65,-13.07z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
</vector>
|
||||||
92
app/src/main/res/drawable/cloud_with_lightning_and_rain.xml
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:pathData="m51.04,121.76 l13.8,-24.27c0.39,-0.68 -0.1,-1.53 -0.89,-1.54l-26.19,-0.1c-0.95,0 -1.39,-1.17 -0.68,-1.8l34.24,-30.28c0.85,-0.75 2.12,0.23 1.6,1.24l-10.37,20.07c-0.35,0.68 0.14,1.5 0.91,1.5l26.78,0.17c0.94,0.01 1.38,1.16 0.68,1.79l-38.3,34.49c-0.87,0.79 -2.16,-0.25 -1.58,-1.27z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="59.01"
|
||||||
|
android:startY="73.87"
|
||||||
|
android:endX="71.67"
|
||||||
|
android:endY="118.12"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFFBC02D"/>
|
||||||
|
<item android:offset="1" android:color="#FFFFEB3B"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m65.41,73.01 l-5.52,10.69c-0.65,1.25 -0.6,2.72 0.13,3.93s2.01,1.94 3.42,1.94l21.69,0.14 -25.47,22.93 7.78,-13.68c0.71,-1.24 0.7,-2.78 -0.02,-4.01 -0.71,-1.23 -2.05,-2 -3.47,-2.01l-21.01,-0.08zM71.99,63.51c-0.23,0 -0.46,0.08 -0.67,0.27l-34.24,30.28c-0.71,0.63 -0.27,1.8 0.68,1.8l26.19,0.1c0.79,0 1.28,0.85 0.89,1.54l-13.8,24.27c-0.44,0.77 0.19,1.55 0.9,1.55 0.23,0 0.47,-0.08 0.68,-0.27l38.3,-34.49c0.7,-0.63 0.26,-1.79 -0.68,-1.79l-26.78,-0.17c-0.77,-0.01 -1.26,-0.82 -0.91,-1.5l10.36,-20.06c0.4,-0.79 -0.22,-1.53 -0.92,-1.53z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m20.02,85.12c-1.44,3.13 -5.14,4.51 -8.28,3.07 -3.13,-1.44 -4.51,-5.14 -3.07,-8.28s10.8,-9.36 10.8,-9.36 1.99,11.43 0.55,14.57z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m23.71,107.43c-1.44,3.13 -5.14,4.51 -8.28,3.07 -3.13,-1.44 -4.51,-5.14 -3.07,-8.28 1.44,-3.13 10.8,-9.36 10.8,-9.36s1.99,11.43 0.55,14.57z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m44.6,119.67c-1.44,3.13 -5.14,4.51 -8.28,3.07 -3.13,-1.44 -4.51,-5.14 -3.07,-8.28s10.8,-9.36 10.8,-9.36 1.99,11.43 0.55,14.57z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m112.91,85.12c-1.44,3.13 -5.14,4.51 -8.28,3.07 -3.13,-1.44 -4.51,-5.14 -3.07,-8.28 1.44,-3.13 10.8,-9.36 10.8,-9.36s1.98,11.43 0.55,14.57z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m88.84,119.67c-1.44,3.13 -5.14,4.51 -8.28,3.07 -3.13,-1.44 -4.51,-5.14 -3.07,-8.28 1.44,-3.13 10.8,-9.36 10.8,-9.36s1.99,11.43 0.55,14.57z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m116.01,107.46c-1.44,3.13 -5.14,4.51 -8.28,3.07 -3.13,-1.44 -4.51,-5.14 -3.07,-8.28 1.44,-3.13 10.8,-9.36 10.8,-9.36s1.99,11.44 0.55,14.57z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m17.97,74.03c0.58,4.44 0.81,8.97 0.23,10.25 -0.69,1.5 -2.21,2.48 -3.86,2.48 -0.61,0 -1.2,-0.13 -1.77,-0.39 -1.03,-0.47 -1.81,-1.32 -2.21,-2.38 -0.39,-1.06 -0.35,-2.22 0.12,-3.25 0.65,-1.41 4.21,-4.33 7.49,-6.71m1.5,-3.47s-9.36,6.22 -10.8,9.36c-1.44,3.13 -0.06,6.84 3.07,8.28 0.84,0.39 1.73,0.57 2.6,0.57 2.37,0 4.63,-1.35 5.68,-3.64 1.44,-3.15 -0.55,-14.57 -0.55,-14.57z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m21.66,96.34c0.58,4.44 0.81,8.97 0.23,10.25 -0.69,1.5 -2.21,2.48 -3.86,2.48 -0.61,0 -1.2,-0.13 -1.77,-0.39 -2.13,-0.98 -3.06,-3.5 -2.09,-5.63 0.65,-1.41 4.21,-4.33 7.49,-6.71m1.5,-3.48s-9.36,6.22 -10.8,9.36c-1.44,3.13 -0.06,6.84 3.07,8.28 0.84,0.39 1.73,0.57 2.6,0.57 2.37,0 4.63,-1.35 5.68,-3.64 1.44,-3.14 -0.55,-14.57 -0.55,-14.57z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m42.56,108.58c0.58,4.44 0.81,8.98 0.23,10.25 -0.69,1.5 -2.21,2.48 -3.86,2.48 -0.61,0 -1.2,-0.13 -1.77,-0.39 -1.03,-0.47 -1.81,-1.32 -2.21,-2.38 -0.39,-1.06 -0.35,-2.22 0.12,-3.25 0.65,-1.41 4.2,-4.33 7.49,-6.71m1.49,-3.48s-9.36,6.22 -10.8,9.36 -0.06,6.84 3.07,8.28c0.84,0.39 1.73,0.57 2.6,0.57 2.37,0 4.63,-1.35 5.68,-3.64 1.44,-3.14 -0.55,-14.57 -0.55,-14.57z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m110.86,74.03c0.58,4.44 0.81,8.97 0.23,10.25 -0.69,1.5 -2.21,2.48 -3.86,2.48 -0.61,0 -1.2,-0.13 -1.77,-0.39 -1.03,-0.47 -1.81,-1.32 -2.21,-2.38 -0.39,-1.06 -0.35,-2.22 0.12,-3.25 0.65,-1.41 4.21,-4.33 7.49,-6.71m1.5,-3.47s-9.36,6.22 -10.8,9.36c-1.44,3.13 -0.06,6.84 3.07,8.28 0.84,0.39 1.73,0.57 2.6,0.57 2.37,0 4.63,-1.35 5.68,-3.64 1.43,-3.15 -0.55,-14.57 -0.55,-14.57z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m86.79,108.58c0.58,4.44 0.81,8.97 0.23,10.25 -0.69,1.5 -2.21,2.48 -3.86,2.48 -0.61,0 -1.2,-0.13 -1.77,-0.39 -1.03,-0.47 -1.81,-1.32 -2.21,-2.38 -0.39,-1.06 -0.35,-2.22 0.12,-3.25 0.65,-1.41 4.21,-4.33 7.49,-6.71m1.5,-3.48s-9.36,6.22 -10.8,9.36c-1.44,3.13 -0.06,6.84 3.07,8.28 0.84,0.39 1.73,0.57 2.6,0.57 2.37,0 4.63,-1.35 5.68,-3.64 1.44,-3.14 -0.55,-14.57 -0.55,-14.57z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m113.96,96.37c0.58,4.44 0.81,8.98 0.23,10.25 -0.69,1.5 -2.21,2.48 -3.86,2.48 -0.61,0 -1.2,-0.13 -1.77,-0.39 -2.13,-0.98 -3.06,-3.5 -2.09,-5.63 0.65,-1.4 4.21,-4.33 7.49,-6.71m1.5,-3.47s-9.36,6.22 -10.8,9.36c-1.44,3.13 -0.06,6.84 3.07,8.28 0.84,0.39 1.73,0.57 2.6,0.57 2.37,0 4.63,-1.35 5.68,-3.64 1.44,-3.14 -0.55,-14.57 -0.55,-14.57z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m103.18,36.62c-0.63,0 -1.24,0.05 -1.85,0.12 0.48,-1.61 0.74,-3.32 0.74,-5.08 0,-9.85 -7.99,-17.84 -17.84,-17.84 -4.19,0 -8.04,1.46 -11.09,3.88 -4.09,-7.78 -12.25,-13.08 -21.65,-13.08 -13.51,0 -24.46,10.95 -24.46,24.46 0,0.49 0.02,0.97 0.05,1.45 -10.19,1.24 -18.07,9.91 -18.07,20.43 0,11.26 9.03,20.4 20.24,20.59 3.76,6.52 11.23,10.99 19.88,11.06 8.36,0.06 15.69,-4.01 19.65,-10.13 3.19,3.49 7.78,5.7 12.88,5.7 7.31,0 13.57,-4.5 16.18,-10.88 1.67,0.6 3.47,0.95 5.34,0.95 8.74,0 15.82,-7.08 15.82,-15.82 -0.01,-8.73 -7.09,-15.81 -15.82,-15.81z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="59.36"
|
||||||
|
android:centerY="-4.66"
|
||||||
|
android:gradientRadius="110.91"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.26" android:color="#FFE3F2FD"/>
|
||||||
|
<item android:offset="0.92" android:color="#FF90CAF9"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m51.49,7.62c7.99,0 15.27,4.4 19,11.47 0.42,0.79 1.16,1.35 2.04,1.54 0.2,0.04 0.41,0.06 0.62,0.06 0.67,0 1.33,-0.23 1.87,-0.65 2.65,-2.11 5.84,-3.23 9.22,-3.23 8.18,0 14.84,6.66 14.84,14.84 0,1.42 -0.21,2.84 -0.62,4.23 -0.29,0.97 -0.07,2.01 0.58,2.78 0.57,0.68 1.42,1.07 2.3,1.07 0.12,0 0.23,-0.01 0.35,-0.02 0.56,-0.07 1.05,-0.1 1.5,-0.1 7.07,0 12.82,5.75 12.82,12.82s-5.75,12.82 -12.82,12.82c-1.45,0 -2.91,-0.26 -4.33,-0.77 -0.33,-0.12 -0.68,-0.18 -1.01,-0.18 -1.19,0 -2.3,0.71 -2.78,1.87 -2.23,5.47 -7.49,9.01 -13.4,9.01 -4.04,0 -7.93,-1.72 -10.68,-4.73 -0.57,-0.63 -1.38,-0.98 -2.22,-0.98 -0.08,0 -0.16,0 -0.25,0.01 -0.93,0.08 -1.77,0.77 -2.27,1.55 -3.51,5.42 -9.99,8.97 -16.93,8.97h-0.17c-7.31,0 -13.94,-3.91 -17.3,-9.76 -0.53,-0.91 -1.49,-1.58 -2.55,-1.6 -9.54,-0.16 -17.29,-8.1 -17.29,-17.63 0,-8.87 6.64,-16.4 15.44,-17.48 1.57,-0.19 2.72,-1.59 2.63,-3.17 -0.03,-0.48 -0.04,-0.9 -0.04,-1.29 -0.01,-11.82 9.62,-21.45 21.45,-21.45m0,-3c-13.51,0 -24.46,10.95 -24.46,24.46 0,0.49 0.02,0.98 0.05,1.46 -10.18,1.24 -18.07,9.91 -18.07,20.42 0,11.26 9.03,20.58 20.24,20.77 3.75,6.53 11.23,11.27 19.88,11.27h0.19c8.28,0 15.51,-4.26 19.45,-10.33 3.2,3.5 7.78,5.61 12.89,5.61 7.31,0 13.57,-4.55 16.18,-10.93 1.67,0.6 3.47,0.92 5.34,0.92 8.74,0 15.82,-7.09 15.82,-15.83s-7.08,-15.82 -15.82,-15.82c-0.63,0 -1.24,0.04 -1.85,0.11 0.48,-1.61 0.74,-3.32 0.74,-5.08 0,-9.85 -7.99,-17.84 -17.84,-17.84 -4.19,0 -8.04,1.46 -11.09,3.88 -4.09,-7.77 -12.25,-13.07 -21.65,-13.07z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
</vector>
|
||||||
73
app/src/main/res/drawable/cloud_with_rain.xml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:pathData="m106.97,39.56c-0.68,0 -1.35,0.05 -2,0.13 0.52,-1.75 0.81,-3.59 0.81,-5.51 0,-10.68 -8.66,-19.34 -19.34,-19.34 -4.55,0 -8.72,1.58 -12.02,4.21 -4.45,-8.43 -13.29,-14.18 -23.48,-14.18 -14.64,0 -26.52,11.87 -26.52,26.52 0,0.53 0.02,1.05 0.05,1.57 -11.03,1.35 -19.58,10.75 -19.58,22.15 0,12.2 9.79,22.11 21.94,22.32 4.07,7.07 12.18,11.92 21.55,11.99 9.06,0.07 17,-4.35 21.3,-10.98 3.46,3.79 8.43,6.18 13.96,6.18 7.93,0 14.71,-4.88 17.53,-11.79 1.81,0.65 3.76,1.02 5.79,1.02 9.47,0 17.15,-7.68 17.15,-17.15 0,-9.46 -7.68,-17.14 -17.14,-17.14z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="59.47"
|
||||||
|
android:centerY="-5.18"
|
||||||
|
android:gradientRadius="120.22"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.26" android:color="#FFE3F2FD"/>
|
||||||
|
<item android:offset="0.92" android:color="#FF90CAF9"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m50.94,7.87c8.75,0 16.73,4.82 20.81,12.57 0.42,0.79 1.16,1.35 2.04,1.54 0.2,0.04 0.41,0.06 0.62,0.06 0.67,0 1.33,-0.23 1.87,-0.65 2.92,-2.32 6.43,-3.55 10.15,-3.55 9.01,0 16.34,7.33 16.34,16.34 0,1.56 -0.23,3.13 -0.68,4.66 -0.29,0.97 -0.07,2.01 0.58,2.78 0.57,0.68 1.42,1.07 2.3,1.07 0.12,0 0.23,-0.01 0.35,-0.02 0.62,-0.07 1.16,-0.11 1.66,-0.11 7.8,0 14.15,6.35 14.15,14.15s-6.35,14.15 -14.15,14.15c-1.61,0 -3.21,-0.29 -4.78,-0.85 -0.33,-0.12 -0.68,-0.18 -1.01,-0.18 -1.19,0 -2.3,0.71 -2.78,1.87 -2.46,6.03 -8.25,9.92 -14.76,9.92 -4.45,0 -8.74,-1.9 -11.76,-5.21 -0.57,-0.63 -1.38,-0.98 -2.22,-0.98 -0.08,0 -0.16,0 -0.25,0.01 -0.93,0.08 -1.77,0.37 -2.27,1.15 -3.85,5.93 -10.96,9.41 -18.56,9.41h-0.19c-8.01,0 -15.28,-3.87 -18.97,-10.28 -0.53,-0.91 -1.49,-1.38 -2.55,-1.4 -10.47,-0.18 -18.99,-8.79 -18.99,-19.26 0,-9.74 7.29,-17.95 16.95,-19.14 1.57,-0.19 2.72,-1.56 2.63,-3.14 -0.03,-0.52 -0.05,-0.98 -0.05,-1.4 0.01,-12.96 10.56,-23.51 23.52,-23.51m0,-3c-14.64,0 -26.52,11.87 -26.52,26.52 0,0.53 0.02,1.06 0.05,1.58 -11.03,1.35 -19.58,10.74 -19.58,22.14 0,12.2 9.79,21.89 21.94,22.1 4.07,7.07 12.17,11.79 21.55,11.79h0.21c8.98,0 16.82,-4.2 21.08,-10.78 3.46,3.79 8.43,6.29 13.98,6.29 7.93,0 14.71,-4.82 17.53,-11.74 1.81,0.65 3.76,1.05 5.79,1.05 9.47,0 17.15,-7.66 17.15,-17.13s-7.68,-17.14 -17.15,-17.14c-0.68,0 -1.35,0.05 -2,0.13 0.52,-1.75 0.81,-3.59 0.81,-5.51 0,-10.68 -8.66,-19.34 -19.34,-19.34 -4.55,0 -8.72,1.58 -12.02,4.21 -4.45,-8.42 -13.29,-14.17 -23.48,-14.17z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m97.36,119.15c-1.78,3.88 -6.36,5.58 -10.24,3.8s-5.58,-6.36 -3.8,-10.24 13.36,-11.57 13.36,-11.57 2.46,14.13 0.68,18.01z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m97.36,119.15c-1.78,3.88 -6.36,5.58 -10.24,3.8s-5.58,-6.36 -3.8,-10.24 13.36,-11.57 13.36,-11.57 2.46,14.13 0.68,18.01z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m25.59,96.63c-1.78,3.88 -6.36,5.58 -10.24,3.8s-5.58,-6.36 -3.8,-10.24 13.36,-11.57 13.36,-11.57 2.46,14.13 0.68,18.01z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m23.42,82.08c0.74,5.36 1.22,11.83 0.35,13.72 -0.93,2.03 -2.97,3.34 -5.21,3.34 -0.82,0 -1.62,-0.18 -2.38,-0.52 -1.39,-0.64 -2.45,-1.78 -2.98,-3.21s-0.47,-2.99 0.16,-4.38c0.89,-1.91 5.79,-5.9 10.06,-8.95m1.49,-3.46s-11.58,7.7 -13.36,11.57c-1.78,3.88 -0.08,8.46 3.8,10.24 1.04,0.48 2.14,0.71 3.21,0.71 2.93,0 5.72,-1.67 7.02,-4.5 1.79,-3.89 -0.67,-18.02 -0.67,-18.02z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m39.66,119.15c-1.78,3.88 -6.36,5.58 -10.24,3.8s-5.58,-6.36 -3.8,-10.24 13.36,-11.57 13.36,-11.57 2.46,14.13 0.68,18.01z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m37.5,104.59c0.74,5.36 1.22,11.83 0.35,13.72 -0.93,2.03 -2.97,3.34 -5.21,3.34 -0.82,0 -1.62,-0.18 -2.38,-0.52 -2.87,-1.32 -4.13,-4.72 -2.82,-7.59 0.88,-1.91 5.78,-5.89 10.06,-8.95m1.48,-3.45s-11.58,7.7 -13.36,11.57c-1.78,3.88 -0.08,8.46 3.8,10.24 1.04,0.48 2.14,0.71 3.21,0.71 2.93,0 5.72,-1.67 7.02,-4.5 1.79,-3.89 -0.67,-18.02 -0.67,-18.02z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m69.22,107.89c-1.78,3.88 -6.36,5.58 -10.24,3.8s-5.58,-6.36 -3.8,-10.24 13.36,-11.57 13.36,-11.57 2.46,14.13 0.68,18.01z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m67.05,93.33c0.74,5.36 1.22,11.83 0.35,13.72 -0.93,2.03 -2.97,3.34 -5.21,3.34 -0.82,0 -1.62,-0.18 -2.38,-0.52 -2.87,-1.32 -4.13,-4.72 -2.82,-7.59 0.88,-1.91 5.78,-5.89 10.06,-8.95m1.49,-3.45s-11.58,7.7 -13.36,11.57c-1.78,3.88 -0.08,8.46 3.8,10.24 1.04,0.48 2.14,0.71 3.22,0.71 2.93,0 5.72,-1.67 7.02,-4.5 1.78,-3.89 -0.68,-18.02 -0.68,-18.02z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m115.66,96.63c-1.78,3.88 -6.36,5.58 -10.24,3.8s-5.58,-6.36 -3.8,-10.24 13.36,-11.57 13.36,-11.57 2.46,14.13 0.68,18.01z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m113.49,82.08c0.74,5.36 1.22,11.83 0.35,13.72 -0.93,2.03 -2.97,3.34 -5.21,3.34 -0.82,0 -1.62,-0.18 -2.38,-0.52 -2.87,-1.32 -4.13,-4.72 -2.82,-7.59 0.88,-1.91 5.78,-5.9 10.06,-8.95m1.49,-3.46s-11.58,7.7 -13.36,11.57c-1.78,3.88 -0.08,8.46 3.8,10.24 1.04,0.48 2.14,0.71 3.21,0.71 2.93,0 5.72,-1.67 7.02,-4.5 1.79,-3.89 -0.67,-18.02 -0.67,-18.02z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m95.2,104.59c0.74,5.36 1.22,11.83 0.35,13.72 -0.93,2.03 -2.97,3.34 -5.21,3.34 -0.82,0 -1.62,-0.18 -2.38,-0.52 -1.39,-0.64 -2.45,-1.78 -2.98,-3.21s-0.47,-2.99 0.16,-4.38c0.88,-1.91 5.78,-5.89 10.06,-8.95m1.48,-3.45s-11.58,7.7 -13.36,11.57c-1.78,3.88 -0.08,8.46 3.8,10.24 1.04,0.48 2.14,0.71 3.21,0.71 2.93,0 5.72,-1.67 7.02,-4.5 1.79,-3.89 -0.67,-18.02 -0.67,-18.02z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m95.2,104.59c0.74,5.36 1.22,11.83 0.35,13.72 -0.93,2.03 -2.97,3.34 -5.21,3.34 -0.82,0 -1.62,-0.18 -2.38,-0.52 -1.39,-0.64 -2.45,-1.78 -2.98,-3.21s-0.47,-2.99 0.16,-4.38c0.88,-1.91 5.78,-5.89 10.06,-8.95m1.48,-3.45s-11.58,7.7 -13.36,11.57c-1.78,3.88 -0.08,8.46 3.8,10.24 1.04,0.48 2.14,0.71 3.21,0.71 2.93,0 5.72,-1.67 7.02,-4.5 1.79,-3.89 -0.67,-18.02 -0.67,-18.02z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
</vector>
|
||||||
55
app/src/main/res/drawable/cloud_with_snow.xml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:pathData="m106.97,37.87c-0.68,0 -1.35,0.05 -2,0.12 0.52,-1.71 0.81,-3.51 0.81,-5.38 0,-10.43 -8.66,-18.88 -19.34,-18.88 -4.55,0 -8.72,1.54 -12.02,4.11 -4.45,-8.23 -13.29,-13.84 -23.48,-13.84 -14.64,0 -26.51,11.59 -26.51,25.89 0,0.51 0.02,1.02 0.05,1.53 -11.04,1.32 -19.59,10.5 -19.59,21.63 0,11.91 9.79,21.59 21.94,21.79 4.07,6.9 12.18,11.63 21.55,11.7 9.06,0.07 17,-4.25 21.3,-10.72 3.46,3.7 8.43,6.03 13.96,6.03 7.93,0 14.71,-4.76 17.53,-11.51 1.81,0.64 3.76,1 5.79,1 9.47,0 17.15,-7.49 17.15,-16.74 0,-9.24 -7.68,-16.73 -17.14,-16.73z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="59.47"
|
||||||
|
android:centerY="-5.81"
|
||||||
|
android:gradientRadius="120.22"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.26" android:color="#FFE3F2FD"/>
|
||||||
|
<item android:offset="0.92" android:color="#FF90CAF9"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m50.94,7c8.76,0 16.74,4.7 20.83,12.26 0.42,0.78 1.16,1.33 2.02,1.51 0.21,0.04 0.41,0.06 0.62,0.06 0.66,0 1.31,-0.22 1.84,-0.63 2.93,-2.27 6.45,-3.48 10.18,-3.48 9.01,0 16.34,7.12 16.34,15.88 0,1.51 -0.23,3.03 -0.68,4.51 -0.29,0.97 -0.08,2.02 0.56,2.79 0.57,0.69 1.42,1.08 2.3,1.08 0.11,0 0.23,-0.01 0.34,-0.02 0.62,-0.07 1.16,-0.1 1.66,-0.1 7.8,0 14.15,6.16 14.15,13.74s-6.35,13.74 -14.15,13.74c-1.61,0 -3.23,-0.28 -4.8,-0.83 -0.33,-0.12 -0.66,-0.17 -0.99,-0.17 -1.18,0 -2.29,0.7 -2.77,1.84 -2.46,5.87 -8.25,9.67 -14.77,9.67 -4.46,0 -8.76,-1.86 -11.78,-5.09 -0.57,-0.61 -1.36,-0.95 -2.19,-0.95 -0.08,0 -0.17,0 -0.25,0.01 -0.91,0.08 -1.74,0.79 -2.25,1.56 -3.83,5.8 -10.95,9.62 -18.56,9.62h-0.19c-8.02,-1 -15.3,-4.21 -18.99,-10.46 -0.53,-0.9 -1.49,-1.57 -2.53,-1.59 -10.47,-0.18 -18.99,-8.66 -18.99,-18.84 0,-9.47 7.29,-17.51 16.95,-18.67 1.58,-0.19 2.73,-1.59 2.64,-3.17 -0.03,-0.51 -0.05,-0.96 -0.05,-1.37 0,-12.63 10.55,-22.9 23.51,-22.9m0,-3c-14.64,0 -26.51,11.59 -26.51,25.89 0,0.52 0.02,1.03 0.05,1.54 -11.04,1.32 -19.59,10.49 -19.59,21.62 0,11.91 9.79,21.8 21.94,22.01 4.07,6.9 12.17,10.94 21.55,11.94h0.21c8.98,0 16.82,-4.53 21.08,-10.96 3.46,3.7 8.43,5.93 13.98,5.93 7.93,0 14.71,-4.82 17.53,-11.57 1.81,0.64 3.76,0.97 5.79,0.97 9.47,0 17.15,-7.51 17.15,-16.75s-7.68,-16.75 -17.15,-16.75c-0.68,0 -1.35,0.05 -2,0.12 0.52,-1.71 0.81,-3.51 0.81,-5.38 0,-10.43 -8.66,-18.88 -19.34,-18.88 -4.55,0 -8.72,1.54 -12.02,4.11 -4.45,-8.23 -13.29,-13.84 -23.48,-13.84z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m21.61,104.5 l-0.63,0.02c-0.83,0.03 -1.52,-0.62 -1.55,-1.45l-0.67,-21.17c-0.03,-0.83 0.62,-1.52 1.45,-1.55l0.63,-0.02c0.83,-0.03 1.52,0.62 1.55,1.45l0.68,21.17c0.02,0.83 -0.63,1.52 -1.46,1.55z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m30.95,95.56 l-6.15,-3.26 5.93,-3.64c0.87,-0.53 1.17,-1.73 0.67,-2.67s-1.61,-1.28 -2.48,-0.74l-8.09,4.97 -8.39,-4.44c-0.9,-0.48 -1.99,-0.07 -2.42,0.9 -0.44,0.97 -0.06,2.15 0.84,2.63l6.16,3.25 -5.93,3.64c-0.87,0.53 -1.17,1.73 -0.67,2.67s1.61,1.28 2.48,0.74l8.09,-4.97 8.39,4.44c0.9,0.48 1.99,0.07 2.42,-0.9s0.05,-2.15 -0.85,-2.62z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m97.88,82.66 l0.36,-0.51c0.48,-0.67 1.42,-0.83 2.09,-0.35l17.25,12.29c0.67,0.48 0.83,1.42 0.35,2.09l-0.36,0.51c-0.48,0.67 -1.42,0.83 -2.09,0.35l-17.25,-12.28c-0.67,-0.49 -0.83,-1.42 -0.35,-2.1z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m99.33,95.51 l6.32,-2.92 -0.7,6.93c-0.1,1.01 0.67,1.98 1.72,2.16s1.99,-0.5 2.09,-1.52l0.95,-9.44 8.62,-3.98c0.93,-0.43 1.26,-1.53 0.75,-2.47s-1.67,-1.35 -2.6,-0.92l-6.32,2.92 0.7,-6.93c0.1,-1.02 -0.67,-1.98 -1.72,-2.16s-1.99,0.5 -2.09,1.52l-0.95,9.44 -8.62,3.98c-0.93,0.43 -1.26,1.53 -0.75,2.47s1.68,1.35 2.6,0.92z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m62.17,112.31 l-0.61,-0.13c-0.81,-0.17 -1.33,-0.97 -1.15,-1.78l4.46,-20.71c0.17,-0.81 0.97,-1.33 1.78,-1.15l0.61,0.13c0.81,0.17 1.33,0.97 1.15,1.78l-4.46,20.71c-0.17,0.81 -0.97,1.33 -1.78,1.15z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m73.4,105.89 l-5.19,-4.65 6.64,-2.1c0.97,-0.31 1.55,-1.4 1.3,-2.43 -0.26,-1.04 -1.25,-1.63 -2.22,-1.32l-9.05,2.86 -7.07,-6.33c-0.76,-0.68 -1.91,-0.55 -2.57,0.29s-0.58,2.07 0.18,2.75l5.19,4.65 -6.64,2.1c-0.97,0.31 -1.55,1.4 -1.3,2.43 0.25,1.04 1.25,1.63 2.22,1.32l9.05,-2.86 7.07,6.33c0.76,0.68 1.91,0.55 2.57,-0.29s0.58,-2.07 -0.18,-2.75z"
|
||||||
|
android:fillColor="#64b5f6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m32.23,117.52c-0.45,-0.45 -0.45,-1.18 0,-1.63l9.23,-9.23c0.45,-0.45 1.18,-0.45 1.63,0s0.45,1.18 0,1.63l-9.23,9.23c-0.45,0.45 -1.18,0.45 -1.63,0z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m40.57,118.1 l-1.17,-4.27 4.27,1.17c0.62,0.17 1.31,-0.21 1.53,-0.85s-0.11,-1.3 -0.73,-1.47l-5.82,-1.59 -1.59,-5.82c-0.17,-0.62 -0.83,-0.95 -1.47,-0.73s-1.02,0.91 -0.85,1.53l1.17,4.27 -4.27,-1.17c-0.62,-0.17 -1.31,0.21 -1.53,0.85s0.11,1.3 0.73,1.47l5.82,1.59 1.59,5.82c0.17,0.62 0.83,0.95 1.47,0.73s1.02,-0.9 0.85,-1.53z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m88.94,117.06c-0.58,-0.27 -0.83,-0.95 -0.56,-1.53l5.48,-11.85c0.27,-0.58 0.95,-0.83 1.53,-0.56s0.83,0.95 0.56,1.53l-5.48,11.85c-0.27,0.58 -0.95,0.83 -1.53,0.56z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m96.97,114.73 l-2.57,-3.6 4.41,-0.38c0.65,-0.06 1.16,-0.65 1.14,-1.33 -0.01,-0.68 -0.55,-1.18 -1.19,-1.13l-6.01,0.52 -3.5,-4.91c-0.38,-0.53 -1.11,-0.61 -1.63,-0.18s-0.65,1.2 -0.27,1.73l2.57,3.6 -4.41,0.38c-0.65,0.06 -1.16,0.65 -1.14,1.33 0.01,0.68 0.55,1.18 1.19,1.13l6.01,-0.52 3.5,4.91c0.38,0.53 1.11,0.61 1.63,0.18s0.64,-1.21 0.27,-1.73z"
|
||||||
|
android:fillColor="#90caf9"/>
|
||||||
|
</vector>
|
||||||
84
app/src/main/res/drawable/crescent_moon.xml
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:pathData="m89.47,28.93c-2.46,-9.66 -7.29,-18.09 -13.69,-24.84 20.75,5.42 38.01,21.66 43.67,43.91 8.29,32.59 -11.41,65.72 -43.99,74.01 -27.79,7.07 -55.96,-6.22 -68.8,-30.49 12.29,7.12 27.26,9.64 42.12,5.86 30.14,-7.65 48.36,-38.31 40.69,-68.45z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="67.56"
|
||||||
|
android:centerY="24.47"
|
||||||
|
android:gradientRadius="96.59"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.28" android:color="#FFFFF157"/>
|
||||||
|
<item android:offset="0.52" android:color="#FFFEEE54"/>
|
||||||
|
<item android:offset="0.72" android:color="#FFFAE44A"/>
|
||||||
|
<item android:offset="0.9" android:color="#FFF4D538"/>
|
||||||
|
<item android:offset="1" android:color="#FFF0C92C"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m91.78,80.3c0.61,6.13 6.41,10.57 12.96,9.92 6.54,-0.65 11.35,-6.15 10.74,-12.28s-6.41,-10.57 -12.96,-9.92c-6.54,0.65 -11.35,6.15 -10.74,12.28z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="103.31"
|
||||||
|
android:centerY="75.44"
|
||||||
|
android:gradientRadius="14.31"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.01" android:color="#FFE0A800"/>
|
||||||
|
<item android:offset="0.61" android:color="#1BE0A800"/>
|
||||||
|
<item android:offset="0.68" android:color="#00E0A800"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m89.42,103.89c1.77,3.82 6.29,5.48 10.09,3.72 3.8,-1.77 5.44,-6.29 3.67,-10.11s-6.29,-5.48 -10.09,-3.72c-3.8,1.77 -5.44,6.3 -3.67,10.11z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="95.12"
|
||||||
|
android:centerY="92.32"
|
||||||
|
android:gradientRadius="13.45"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.01" android:color="#FFE0A800"/>
|
||||||
|
<item android:offset="0.61" android:color="#1BE0A800"/>
|
||||||
|
<item android:offset="0.68" android:color="#00E0A800"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m107.44,62.08c1.21,2.61 4.31,3.75 6.91,2.54s3.73,-4.31 2.51,-6.92c-1.21,-2.61 -4.31,-3.75 -6.91,-2.54 -2.6,1.2 -3.73,4.3 -2.51,6.92z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="111.34"
|
||||||
|
android:centerY="54.15"
|
||||||
|
android:gradientRadius="9.2"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.01" android:color="#FFE0A800"/>
|
||||||
|
<item android:offset="0.61" android:color="#1BE0A800"/>
|
||||||
|
<item android:offset="0.68" android:color="#00E0A800"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M73.24,109.06m-8.97,0a8.97,8.97 0,1 1,17.94 0a8.97,8.97 0,1 1,-17.94 0">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="73.01"
|
||||||
|
android:centerY="105.52"
|
||||||
|
android:gradientRadius="15.24"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.01" android:color="#FFE0A800"/>
|
||||||
|
<item android:offset="0.61" android:color="#1BE0A800"/>
|
||||||
|
<item android:offset="0.68" android:color="#00E0A800"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m84.57,10.4c15.75,7.23 27.62,21.23 31.98,38.34 7.87,30.93 -10.89,62.5 -41.83,70.37 -4.7,1.19 -9.51,1.8 -14.31,1.8 -18.06,0 -34.78,-8.35 -45.62,-22.3 6.43,2.32 13.23,3.53 20.08,3.53 4.91,0 9.84,-0.62 14.65,-1.84 31.7,-8.06 50.93,-40.41 42.86,-72.11 -1.61,-6.33 -4.27,-12.35 -7.81,-17.79m-8.79,-6.31c6.41,6.75 11.23,15.18 13.69,24.84 7.67,30.14 -10.55,60.79 -40.69,68.46 -4.65,1.18 -9.32,1.75 -13.91,1.75 -10.06,0 -19.77,-2.72 -28.21,-7.61 10.51,19.87 31.31,32.38 53.75,32.38 4.97,0 10.01,-0.61 15.05,-1.89 32.59,-8.29 52.28,-41.43 43.99,-74.01 -5.66,-22.26 -22.92,-38.5 -43.67,-43.92z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
</vector>
|
||||||
23
app/src/main/res/drawable/droplet.xml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:pathData="m64.6,124c-20.1,0 -35.56,-19.48 -35.56,-36.18 0,-11.75 5.26,-25.36 12.68,-44.22 0.93,-2.78 2.16,-5.57 3.4,-8.66 3.54,-8.84 6.66,-18.42 11.41,-26.71 3.24,-5.66 11.46,-5.65 14.58,0.08 4.43,8.14 7.45,16.96 12.05,27.25 12.99,29.07 16.7,40.82 16.7,52.57 0.3,16.39 -15.47,35.87 -35.26,35.87z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="67.14"
|
||||||
|
android:centerY="19.26"
|
||||||
|
android:gradientRadius="79.34"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.46" android:color="#FF29B6F6"/>
|
||||||
|
<item android:offset="1" android:color="#FF1E88E5"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m86.34,101.71c-4.55,7.02 -14.84,5.69 -14.84,-5.97 0,-7.45 1.52,-45.74 7.92,-40.39 10.41,8.72 13.38,36.43 6.92,46.36z"
|
||||||
|
android:fillColor="#81d4fa"/>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
43
app/src/main/res/drawable/sun.xml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:pathData="m81.56,35.12 l23.3,-13.38c1.48,-0.85 3.12,0.83 2.24,2.29l-13.86,23.01c-2.07,3.44 -0.28,7.91 3.6,8.95l25.94,7.01c1.65,0.45 1.62,2.79 -0.03,3.2l-26.08,6.47c-3.9,0.97 -5.79,5.4 -3.79,8.88l13.38,23.3c0.85,1.48 -0.83,3.12 -2.29,2.24l-23.01,-13.85c-3.44,-2.07 -7.91,-0.28 -8.95,3.6l-7.01,25.94c-0.45,1.65 -2.79,1.62 -3.2,-0.03l-6.47,-26.08c-0.97,-3.9 -5.4,-5.79 -8.88,-3.79l-23.3,13.38c-1.48,0.85 -3.12,-0.83 -2.24,-2.29l13.86,-23.02c2.07,-3.44 0.28,-7.91 -3.6,-8.95l-25.95,-7.01c-1.65,-0.45 -1.62,-2.79 0.03,-3.2l26.08,-6.47c3.9,-0.97 5.79,-5.4 3.79,-8.88l-13.38,-23.3c-0.85,-1.48 0.83,-3.12 2.29,-2.24l23.02,13.86c3.44,2.07 7.91,0.28 8.95,-3.6l7.01,-25.94c0.45,-1.65 2.79,-1.62 3.2,0.03l6.47,26.08c0.97,3.9 5.4,5.79 8.88,3.79z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="64.01"
|
||||||
|
android:centerY="64"
|
||||||
|
android:gradientRadius="55.57"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.39" android:color="#FFFF8F00"/>
|
||||||
|
<item android:offset="0.82" android:color="#FFFFB300"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M64.07,63.97m-30.03,0a30.03,30.03 0,1 1,60.06 0a30.03,30.03 0,1 1,-60.06 0">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:centerX="64.07"
|
||||||
|
android:centerY="63.97"
|
||||||
|
android:gradientRadius="37.05"
|
||||||
|
android:type="radial">
|
||||||
|
<item android:offset="0.58" android:color="#FFFFEB3B"/>
|
||||||
|
<item android:offset="0.84" android:color="#FFFBC02D"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m64.11,35.99c3.74,0 7.4,0.74 10.87,2.21 14.21,6.01 20.88,22.47 14.87,36.68 -4.39,10.38 -14.52,17.08 -25.81,17.08 -3.74,0 -7.4,-0.74 -10.87,-2.21 -14.21,-6.02 -20.88,-22.47 -14.87,-36.69 4.39,-10.37 14.52,-17.07 25.81,-17.07m0,-2c-11.69,0 -22.82,6.88 -27.66,18.3 -6.44,15.23 0.7,32.86 15.93,39.3 3.8,1.61 7.75,2.37 11.65,2.37 11.69,0 22.82,-6.88 27.66,-18.3 6.44,-15.23 -0.7,-32.86 -15.93,-39.3 -3.8,-1.61 -7.76,-2.37 -11.65,-2.37z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#eee"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m64.55,11.02 l5.22,21.04c1,4.04 4.62,6.87 8.79,6.87 1.57,0 3.12,-0.42 4.5,-1.21l18.8,-10.8 -11.19,18.57c-1.48,2.45 -1.71,5.38 -0.64,8.04s3.27,4.61 6.03,5.36l20.92,5.66 -21.04,5.22c-2.78,0.69 -5.02,2.6 -6.14,5.23s-0.95,5.57 0.48,8.05l10.8,18.8 -18.57,-11.18c-1.41,-0.85 -3.02,-1.3 -4.66,-1.3 -4.08,0 -7.68,2.75 -8.74,6.69l-5.66,20.92 -5.22,-21.04c-1,-4.04 -4.62,-6.87 -8.79,-6.87 -1.57,0 -3.12,0.42 -4.5,1.21l-18.8,10.8 11.18,-18.57c1.48,-2.45 1.71,-5.38 0.64,-8.04s-3.27,-4.61 -6.03,-5.36l-20.92,-5.66 21.04,-5.22c2.78,-0.69 5.02,-2.6 6.14,-5.23s0.95,-5.57 -0.48,-8.05l-10.8,-18.8 18.57,11.18c1.41,0.85 3.02,1.3 4.66,1.3 4.08,0 7.68,-2.75 8.74,-6.69zM64.61,4c-0.69,0 -1.37,0.41 -1.59,1.22l-7.02,25.94c-0.75,2.76 -3.23,4.47 -5.85,4.47 -1.05,0 -2.12,-0.28 -3.11,-0.87l-23.02,-13.86c-0.28,-0.17 -0.58,-0.25 -0.86,-0.25 -1.16,0 -2.11,1.3 -1.43,2.49l13.38,23.3c2,3.48 0.11,7.91 -3.79,8.88l-26.07,6.47c-1.66,0.41 -1.68,2.75 -0.03,3.2l25.94,7.01c3.88,1.05 5.67,5.51 3.6,8.95l-13.86,23.03c-0.72,1.19 0.23,2.52 1.41,2.52 0.27,0 0.55,-0.07 0.83,-0.23l23.3,-13.38c0.96,-0.55 1.99,-0.81 3,-0.81 2.66,0 5.18,1.77 5.88,4.59l6.47,26.08c0.21,0.83 0.91,1.25 1.6,1.25s1.37,-0.41 1.59,-1.22l7.02,-25.94c0.75,-2.76 3.23,-4.47 5.85,-4.47 1.05,0 2.12,0.28 3.11,0.87l23.02,13.86c0.28,0.17 0.58,0.25 0.86,0.25 1.16,0 2.11,-1.3 1.43,-2.49l-13.38,-23.3c-2,-3.48 -0.11,-7.91 3.79,-8.88l26.08,-6.47c1.66,-0.41 1.68,-2.75 0.03,-3.2l-25.95,-7.01c-3.88,-1.05 -5.67,-5.51 -3.6,-8.95l13.86,-23.02c0.72,-1.19 -0.23,-2.52 -1.41,-2.52 -0.27,0 -0.55,0.07 -0.83,0.23l-23.3,13.38c-0.96,0.55 -1.99,0.81 -3,0.81 -2.66,0 -5.18,-1.77 -5.88,-4.59l-6.47,-26.09c-0.21,-0.83 -0.91,-1.25 -1.6,-1.25z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#eee"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 5.6 KiB |
38
app/src/main/res/drawable/thermometer.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="128dp"
|
||||||
|
android:height="128dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<path
|
||||||
|
android:pathData="M76,80.34V16.29C76,9.77 70.82,4 64.3,4h-1.55C56.24,4 51,9.77 51,16.29v64.04c-7,4.2 -11.24,11.68 -11.24,20.2c0,13.14 10.64,23.8 23.79,23.8s23.74,-10.65 23.74,-23.8C87.28,92.01 83,84.54 76,80.34z"
|
||||||
|
android:fillColor="#E1F5FE"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m69.92,85.89v-62.07c0,-3.53 -2.86,-6.39 -6.39,-6.39s-6.39,2.86 -6.39,6.39v62.08c-5.82,2.54 -9.83,8.44 -9.58,15.25 0.3,8.19 6.99,14.96 15.18,15.36 9.19,0.44 16.78,-6.87 16.78,-15.96 -0.01,-6.57 -3.95,-12.19 -9.6,-14.66z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="67.01"
|
||||||
|
android:startY="38.59"
|
||||||
|
android:endX="63.28"
|
||||||
|
android:endY="104.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.6" android:color="#FFEF5350"/>
|
||||||
|
<item android:offset="1" android:color="#FFE53935"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m62.87,39.76h-11.92v-6.39h11.92c1.66,0 3,1.34 3,3v0.39c0,1.66 -1.34,3 -3,3z"
|
||||||
|
android:fillColor="#616161"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m62.87,58.32h-11.92v-6.39h11.92c1.66,0 3,1.34 3,3v0.39c0,1.65 -1.34,3 -3,3z"
|
||||||
|
android:fillColor="#616161"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m62.87,76.88h-11.92v-6.39h11.92c1.66,0 3,1.34 3,3v0.39c0,1.65 -1.34,3 -3,3z"
|
||||||
|
android:fillColor="#616161"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M64.15,7C68.87,7 73,11.26 73,16.29v64.04c0,1.05 0.55,2.03 1.46,2.57c6.15,3.69 9.82,10.28 9.82,17.63c0,11.47 -9.3,20.8 -20.74,20.8c-11.46,0 -20.79,-9.33 -20.79,-20.8c0,-7.36 3.66,-13.95 9.79,-17.63c0.9,-0.54 1.46,-1.52 1.46,-2.57V16.29C54,11.26 58.01,7 62.76,7H64M64.3,4h-1.55C56.24,4 51,9.77 51,16.29v64.04c-7,4.2 -11.24,11.68 -11.24,20.2c0,13.14 10.64,23.8 23.79,23.8s23.74,-10.65 23.74,-23.8c0,-8.52 -4.28,-16 -11.28,-20.2V16.29C76,9.77 70.82,4 64.3,4L64.3,4z"
|
||||||
|
android:strokeAlpha="0.2"
|
||||||
|
android:fillColor="#424242"
|
||||||
|
android:fillAlpha="0.2"/>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 4.9 KiB |
@ -45,4 +45,6 @@
|
|||||||
<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_description">Current wind direction and wind speed</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -76,11 +76,11 @@
|
|||||||
typeId="headwindSpeed" />
|
typeId="headwindSpeed" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/userwind_speed_description"
|
description="@string/windDirectionAndSpeed_description"
|
||||||
displayName="@string/userwind_speed"
|
displayName="@string/windDirectionAndSpeed"
|
||||||
graphical="false"
|
graphical="false"
|
||||||
icon="@drawable/wind"
|
icon="@drawable/wind"
|
||||||
typeId="userwindSpeed" />
|
typeId="windDirectionAndSpeed" />
|
||||||
|
|
||||||
<DataType
|
<DataType
|
||||||
description="@string/relativeHumidity_description"
|
description="@string/relativeHumidity_description"
|
||||||
|
|||||||
BIN
preview0.png
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 39 KiB |
BIN
preview1.png
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 56 KiB |
BIN
preview2.png
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 24 KiB |
BIN
preview3.png
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 44 KiB |