Compare commits

..

No commits in common. "87a244ef27cf01599d84d197f50a77071fa433df" and "302bdef42964225546b804ab500f9dbb71c037f4" have entirely different histories.

9 changed files with 31 additions and 123 deletions

View File

@ -72,7 +72,7 @@ tasks.register("generateManifest") {
"latestVersionCode" to android.defaultConfig.versionCode,
"developer" to "github.com/timklge",
"description" to "Open-source extension that provides headwind direction, wind speed, forecast and other weather data fields.",
"releaseNotes" to "* Add route forecast support for OpenWeatherMap (thx @lockevod!)\n* Crop timespan in forecast widgets to 6 hours, use am / pm time format, enlarge font\n* Remove tailwind, icon forecast, weather data fields",
"releaseNotes" to "* Crop timespan in forecast widgets to 6 hours, use am / pm time format, enlarge font\n* Remove tailwind, icon forecast, weather data fields",
"screenshotUrls" to listOf(
"$baseUrl/preview1.png",
"$baseUrl/preview3.png",

View File

@ -5,8 +5,7 @@ import android.util.Log
import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.util.signedAngleDifference
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.DataType
import io.hammerhead.karooext.models.StreamState
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@ -14,7 +13,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
@ -101,33 +99,7 @@ fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinat
val initialFlow = flow {
val lastKnownPosition = context.getLastKnownPosition()
if (lastKnownPosition == null) {
val initialState = streamDataFlow(DataType.Type.LOCATION).firstOrNull()?.let { it as? StreamState.Streaming }
initialState?.dataPoint?.let { dataPoint ->
val lat = dataPoint.values[DataType.Field.LOC_LATITUDE]
val lng = dataPoint.values[DataType.Field.LOC_LONGITUDE]
val orientation = dataPoint.values[DataType.Field.LOC_BEARING]
if (lat != null && lng != null) {
emit(GpsCoordinates(lat, lng, orientation))
Log.i(KarooHeadwindExtension.TAG, "No last known position found, fetched initial GPS position")
} else {
emit(null)
Log.w(KarooHeadwindExtension.TAG, "No last known position found, initial GPS position is unavailable")
}
} ?: run {
emit(null)
Log.w(KarooHeadwindExtension.TAG, "No last known position found, initial GPS position is unavailable")
}
} else {
emit(lastKnownPosition)
Log.i(KarooHeadwindExtension.TAG, "Using last known position: $lastKnownPosition")
}
}
val gpsFlow = streamLocation()

View File

@ -115,7 +115,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
old == new
}
}
.throttle(5_000L)
.debounce(Duration.ofSeconds(5))
var requestedGpsCoordinates: List<GpsCoordinates> = emptyList()
@ -234,7 +234,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", BuildConfig.VERS
response
}.retry(Long.MAX_VALUE) { e ->
Log.w(TAG, "Failed to get weather data", e)
delay(2.minutes); true
delay(1.minutes); true
}.collect { response ->
try {
saveCurrentData(applicationContext, response)

View File

@ -70,8 +70,7 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
abstract fun getLineData(
lineData: List<LineData>,
isImperial: Boolean,
upcomingRoute: UpcomingRoute?,
isPreview: Boolean
upcomingRoute: UpcomingRoute?
): Set<LineGraphBuilder.Line>
private fun previewFlow(settingsAndProfileStream: Flow<SettingsAndProfile>): Flow<StreamData> =
@ -206,7 +205,8 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
val viewJob = CoroutineScope(Dispatchers.IO).launch {
emitter.onNext(ShowCustomStreamState("", null))
dataFlow.filter { it.isVisible }.collect { (allData, settingsAndProfile, _, headingResponse, upcomingRoute) ->
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()){
@ -261,12 +261,7 @@ abstract class LineGraphForecastDataType(private val karooSystem: KarooSystemSer
}
}
val pointData = getLineData(
data,
settingsAndProfile.isImperialTemperature,
upcomingRoute,
config.preview
)
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)

View File

@ -8,8 +8,7 @@ class PrecipitationForecastDataType(karooSystem: KarooSystemService) : LineGraph
override fun getLineData(
lineData: List<LineData>,
isImperial: Boolean,
upcomingRoute: UpcomingRoute?,
isPreview: Boolean
upcomingRoute: UpcomingRoute?
): Set<LineGraphBuilder.Line> {
val precipitationPoints = lineData.map { data ->
if (isImperial) { // Convert mm to inches

View File

@ -8,8 +8,7 @@ class TemperatureForecastDataType(karooSystem: KarooSystemService) : LineGraphFo
override fun getLineData(
lineData: List<LineData>,
isImperial: Boolean,
upcomingRoute: UpcomingRoute?,
isPreview: Boolean
upcomingRoute: UpcomingRoute?
): Set<LineGraphBuilder.Line> {
val linePoints = lineData.map { data ->
if (isImperial) {

View File

@ -22,8 +22,7 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
override fun getLineData(
lineData: List<LineData>,
isImperial: Boolean,
upcomingRoute: UpcomingRoute?,
isPreview: Boolean
upcomingRoute: UpcomingRoute?
): Set<LineGraphBuilder.Line> {
val windPoints = lineData.map { data ->
if (isImperial) { // Convert m/s to mph
@ -42,16 +41,9 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
}
val headwindPoints = try {
if (upcomingRoute != null || isPreview){
if (upcomingRoute != null){
(0..<HEADWIND_SAMPLE_COUNT).mapNotNull { i ->
val t = i / HEADWIND_SAMPLE_COUNT.toDouble()
if (isPreview) {
return@mapNotNull LineGraphBuilder.DataPoint(i.toFloat() * (windPoints.size / HEADWIND_SAMPLE_COUNT.toFloat()), (-10f) + (20f * kotlin.random.Random.nextFloat()))
}
if (upcomingRoute == null) return@mapNotNull null
val beforeLineData = lineData.getOrNull(floor(lineData.size * t).toInt()) ?: lineData.firstOrNull()
val afterLineData = lineData.getOrNull(ceil(lineData.size * t).toInt()) ?: lineData.lastOrNull()
@ -67,24 +59,9 @@ class WindForecastDataType(karooSystem: KarooSystemService) : LineGraphForecastD
val beforeDistanceAlongRoute = beforeLineData.distance
val afterDistanceAlongRoute = afterLineData.distance
val distanceAlongRoute = (beforeDistanceAlongRoute + (afterDistanceAlongRoute - beforeDistanceAlongRoute) * dt).coerceIn(0.0, upcomingRoute.routeLength)
val coordsAlongRoute = try {
TurfMeasurement.along(upcomingRoute.routePolyline, distanceAlongRoute, TurfConstants.UNIT_METERS)
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error getting coordinates along route", e)
return@mapNotNull null
}
val nextCoordsAlongRoute = try {
TurfMeasurement.along(upcomingRoute.routePolyline, distanceAlongRoute + 5, TurfConstants.UNIT_METERS)
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error getting next coordinates along route", e)
return@mapNotNull null
}
val bearingAlongRoute = try {
TurfMeasurement.bearing(coordsAlongRoute, nextCoordsAlongRoute)
} catch(e: Exception) {
Log.e(KarooHeadwindExtension.TAG, "Error calculating bearing along route", e)
return@mapNotNull null
}
val 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

View File

@ -381,7 +381,7 @@ class LineGraphBuilder(val context: Context) {
// Draw Left Y-axis ticks and labels
if (hasLeftYAxisData) {
textPaint.textAlign = Align.RIGHT
val numYTicks = if (gridHeight > 15) (gridHeight / 10) else 1
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

View File

@ -6,6 +6,7 @@ import de.timklge.karooheadwind.KarooHeadwindExtension
import de.timklge.karooheadwind.WeatherDataProvider
import de.timklge.karooheadwind.datatypes.GpsCoordinates
import de.timklge.karooheadwind.jsonWithUnknownKeys
import de.timklge.karooheadwind.weatherprovider.WeatherDataForLocation
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
import de.timklge.karooheadwind.weatherprovider.WeatherProvider
import de.timklge.karooheadwind.weatherprovider.WeatherProviderException
@ -16,14 +17,12 @@ import io.hammerhead.karooext.models.UserProfile
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.timeout
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.seconds
@ -49,8 +48,6 @@ data class Snow(
class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvider {
companion object {
private const val MAX_API_CALLS = 3
fun convertWeatherCodeToOpenMeteo(owmCode: Int): Int {
// Mapping OpenWeatherMap to WMO OpenMeteo
return when (owmCode) {
@ -70,64 +67,33 @@ class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvide
coordinates: List<GpsCoordinates>,
settings: HeadwindSettings,
profile: UserProfile?
): WeatherDataResponse = coroutineScope {
val selectedCoordinates = coordinates.take((MAX_API_CALLS - 1).coerceAtLeast(1)).toMutableList()
): WeatherDataResponse {
if (coordinates.isNotEmpty() && !selectedCoordinates.contains(coordinates.last())){
selectedCoordinates.add(coordinates.last())
}
val response = makeOpenWeatherMapRequest(karooSystem, coordinates, apiKey)
val responseBody = response.body?.let { String(it) } ?: throw Exception("Null response from OpenWeatherMap")
Log.d(KarooHeadwindExtension.TAG, "OpenWeatherMap: searching for ${selectedCoordinates.size} locations from ${coordinates.size} total")
selectedCoordinates.forEachIndexed { index, coord ->
Log.d(KarooHeadwindExtension.TAG, "Point #$index: ${coord.lat}, ${coord.lon}, distance: ${coord.distanceAlongRoute}")
}
val responses = mutableListOf<WeatherDataForLocation>()
val weatherDataForSelectedLocations = buildList {
for (coordinate in selectedCoordinates){
val response = makeOpenWeatherMapRequest(karooSystem, coordinate, apiKey)
val responseBody = response.body?.let { String(it) }
?: throw WeatherProviderException(response.statusCode, "Null Response from OpenWeatherMap")
val openWeatherMapWeatherDataForLocation = jsonWithUnknownKeys.decodeFromString<OpenWeatherMapWeatherDataForLocation>(responseBody)
responses.add(openWeatherMapWeatherDataForLocation.toWeatherDataForLocation(null))
val weatherData = jsonWithUnknownKeys.decodeFromString<OpenWeatherMapWeatherDataForLocation>(responseBody)
// FIXME Route forecast
add(coordinate to weatherData)
}
}
val allLocationData = coordinates.map { originalCoord ->
val directMatch = weatherDataForSelectedLocations.find { it.first == originalCoord }
if (directMatch != null) {
directMatch.second.toWeatherDataForLocation(originalCoord.distanceAlongRoute)
} else {
val closestCoord = weatherDataForSelectedLocations.minByOrNull { (coord, _) ->
if (originalCoord.distanceAlongRoute != null && coord.distanceAlongRoute != null) {
(originalCoord.distanceAlongRoute - coord.distanceAlongRoute).absoluteValue
} else {
originalCoord.distanceTo(coord)
}
} ?: throw WeatherProviderException(500, "Error finding nearest coordinate")
closestCoord.second.toWeatherDataForLocation(originalCoord.distanceAlongRoute)
}
}
WeatherDataResponse(
return WeatherDataResponse(
provider = WeatherDataProvider.OPEN_WEATHER_MAP,
data = allLocationData
data = responses
)
}
@OptIn(FlowPreview::class)
private suspend fun makeOpenWeatherMapRequest(
service: KarooSystemService,
coordinate: GpsCoordinates,
coordinates: List<GpsCoordinates>,
apiKey: String
): HttpResponseState.Complete {
val response = callbackFlow {
// OpenWeatherMap only supports setting imperial or metric units for all measurements, not individually for distance / temperature
val coordinate = coordinates.first()
// URL API 3.0 with onecall endpoint
val url = "https://api.openweathermap.org/data/3.0/onecall?lat=${coordinate.lat}&lon=${coordinate.lon}" +