Add route forecast support for OpenWeatherMap (#145)
* Added openweathermap support * Added openweathermap support * Added openweathermap support * Added openweathermap support * Added openweathermap support * Added openweathermap support * timklge updates 20250308 Forecast in route (openweathermap) * Forecast in route (openweathermap) * Forecast in route (openweathermap) * Update app/src/main/kotlin/de/timklge/karooheadwind/weatherprovider/openweathermap/OpenWeatherMapWeatherProvider.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
958c576a5e
commit
5b163f6f7a
@ -6,7 +6,6 @@ import de.timklge.karooheadwind.KarooHeadwindExtension
|
|||||||
import de.timklge.karooheadwind.WeatherDataProvider
|
import de.timklge.karooheadwind.WeatherDataProvider
|
||||||
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.WeatherDataForLocation
|
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
import de.timklge.karooheadwind.weatherprovider.WeatherDataResponse
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherProvider
|
import de.timklge.karooheadwind.weatherprovider.WeatherProvider
|
||||||
import de.timklge.karooheadwind.weatherprovider.WeatherProviderException
|
import de.timklge.karooheadwind.weatherprovider.WeatherProviderException
|
||||||
@ -16,13 +15,17 @@ import io.hammerhead.karooext.models.OnHttpResponse
|
|||||||
import io.hammerhead.karooext.models.UserProfile
|
import io.hammerhead.karooext.models.UserProfile
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.single
|
import kotlinx.coroutines.flow.single
|
||||||
import kotlinx.coroutines.flow.timeout
|
import kotlinx.coroutines.flow.timeout
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
||||||
@ -48,6 +51,8 @@ data class Snow(
|
|||||||
|
|
||||||
class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvider {
|
class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvider {
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val MAX_API_CALLS = 4
|
||||||
|
|
||||||
fun convertWeatherCodeToOpenMeteo(owmCode: Int): Int {
|
fun convertWeatherCodeToOpenMeteo(owmCode: Int): Int {
|
||||||
// Mapping OpenWeatherMap to WMO OpenMeteo
|
// Mapping OpenWeatherMap to WMO OpenMeteo
|
||||||
return when (owmCode) {
|
return when (owmCode) {
|
||||||
@ -67,33 +72,80 @@ class OpenWeatherMapWeatherProvider(private val apiKey: String) : WeatherProvide
|
|||||||
coordinates: List<GpsCoordinates>,
|
coordinates: List<GpsCoordinates>,
|
||||||
settings: HeadwindSettings,
|
settings: HeadwindSettings,
|
||||||
profile: UserProfile?
|
profile: UserProfile?
|
||||||
): WeatherDataResponse {
|
): WeatherDataResponse = coroutineScope {
|
||||||
|
|
||||||
val response = makeOpenWeatherMapRequest(karooSystem, coordinates, apiKey)
|
|
||||||
val responseBody = response.body?.let { String(it) } ?: throw Exception("Null response from OpenWeatherMap")
|
|
||||||
|
|
||||||
val responses = mutableListOf<WeatherDataForLocation>()
|
val selectedCoordinates = when {
|
||||||
|
coordinates.size <= MAX_API_CALLS -> coordinates
|
||||||
|
else -> {
|
||||||
|
|
||||||
val openWeatherMapWeatherDataForLocation = jsonWithUnknownKeys.decodeFromString<OpenWeatherMapWeatherDataForLocation>(responseBody)
|
val mandatoryCoordinates = coordinates.take(3).toMutableList()
|
||||||
responses.add(openWeatherMapWeatherDataForLocation.toWeatherDataForLocation(null))
|
|
||||||
|
|
||||||
// FIXME Route forecast
|
|
||||||
|
|
||||||
return WeatherDataResponse(
|
val fourthIndex = if (coordinates.size > 6) {
|
||||||
|
coordinates.size - 3
|
||||||
|
} else {
|
||||||
|
(coordinates.size / 2) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mandatoryCoordinates.add(coordinates[fourthIndex.coerceIn(3, coordinates.lastIndex)])
|
||||||
|
mandatoryCoordinates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 weatherDataForSelectedLocations = selectedCoordinates.map { coordinate ->
|
||||||
|
async {
|
||||||
|
val response = makeOpenWeatherMapRequest(karooSystem, coordinate, apiKey)
|
||||||
|
val responseBody = response.body?.let { String(it) }
|
||||||
|
?: throw WeatherProviderException(response.statusCode, "Null Response from OpenWeatherMap")
|
||||||
|
|
||||||
|
val weatherData = jsonWithUnknownKeys.decodeFromString<OpenWeatherMapWeatherDataForLocation>(responseBody)
|
||||||
|
coordinate to weatherData
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
||||||
provider = WeatherDataProvider.OPEN_WEATHER_MAP,
|
provider = WeatherDataProvider.OPEN_WEATHER_MAP,
|
||||||
data = responses
|
data = allLocationData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
private suspend fun makeOpenWeatherMapRequest(
|
private suspend fun makeOpenWeatherMapRequest(
|
||||||
service: KarooSystemService,
|
service: KarooSystemService,
|
||||||
coordinates: List<GpsCoordinates>,
|
coordinate: GpsCoordinates,
|
||||||
apiKey: String
|
apiKey: String
|
||||||
): 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
|
|
||||||
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}" +
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user