Migrate to OWM provider to OWM geocoding API

This commit is contained in:
MM20 2023-08-08 20:11:24 +02:00
parent 8bca768197
commit a726f487d2
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
2 changed files with 151 additions and 48 deletions

View File

@ -5,7 +5,7 @@ import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
data class CurrentWeatherResult( data class CurrentWeatherResult(
val coords: WeatherResultCoords?, val coord: WeatherResultCoords?,
val weather: Array<WeatherResultWeather>?, val weather: Array<WeatherResultWeather>?,
val main: WeatherResultMain?, val main: WeatherResultMain?,
val wind: WeatherResultWind?, val wind: WeatherResultWind?,
@ -106,9 +106,17 @@ data class ForecastResultCity(
val timezone: Long?, val timezone: Long?,
) )
data class GeocodeResult(
val name: String?,
val local_names: Map<String, String>?,
val lat: Double?,
val lon: Double?,
val country: String?,
val state: String?,
)
interface OpenWeatherMapApi { interface OpenWeatherMapApi {
@GET("weather") @GET("data/2.5/weather")
suspend fun currentWeather( suspend fun currentWeather(
@Query("q") q: String? = null, @Query("q") q: String? = null,
@Query("id") id: Int? = null, @Query("id") id: Int? = null,
@ -118,7 +126,7 @@ interface OpenWeatherMapApi {
@Query("lang") lang: String, @Query("lang") lang: String,
): CurrentWeatherResult ): CurrentWeatherResult
@GET("forecast") @GET("data/2.5/forecast")
suspend fun forecast5Day3Hour( suspend fun forecast5Day3Hour(
@Query("q") q: String? = null, @Query("q") q: String? = null,
@Query("id") id: Int? = null, @Query("id") id: Int? = null,
@ -127,4 +135,11 @@ interface OpenWeatherMapApi {
@Query("appid") appid: String, @Query("appid") appid: String,
@Query("lang") lang: String, @Query("lang") lang: String,
): ForecastResult ): ForecastResult
@GET("geo/1.0/direct")
suspend fun geocode(
@Query("q") q: String,
@Query("appid") appid: String,
@Query("limit") limit: Int = 5,
): Array<GeocodeResult>
} }

View File

@ -5,10 +5,16 @@ import android.content.SharedPreferences
import android.util.Log import android.util.Log
import androidx.core.content.edit import androidx.core.content.edit
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.weather.* import de.mm20.launcher2.ktx.getDouble
import de.mm20.launcher2.ktx.putDouble
import de.mm20.launcher2.weather.Forecast
import de.mm20.launcher2.weather.R
import de.mm20.launcher2.weather.WeatherLocation
import de.mm20.launcher2.weather.WeatherProvider
import de.mm20.launcher2.weather.WeatherUpdateResult
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.util.* import java.util.Locale
class OpenWeatherMapProvider(override val context: Context) : class OpenWeatherMapProvider(override val context: Context) :
@ -16,7 +22,7 @@ class OpenWeatherMapProvider(override val context: Context) :
private val retrofit by lazy { private val retrofit by lazy {
Retrofit.Builder() Retrofit.Builder()
.baseUrl("https://api.openweathermap.org/data/2.5/") .baseUrl("https://api.openweathermap.org/")
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build() .build()
} }
@ -30,28 +36,28 @@ class OpenWeatherMapProvider(override val context: Context) :
} }
override suspend fun lookupLocation(query: String): List<OpenWeatherMapLocation> { override suspend fun lookupLocation(query: String): List<OpenWeatherMapLocation> {
val lang = getLanguageCode()
val response = try { val response = try {
openWeatherMapService.currentWeather( openWeatherMapService.geocode(
appid = getApiKey() ?: return emptyList(), appid = getApiKey() ?: return emptyList(),
q = query, q = query,
lang = lang
) )
} catch (e: Exception) { } catch (e: Exception) {
CrashReporter.logException(e) CrashReporter.logException(e)
return emptyList() return emptyList()
} }
val city = response.name ?: return emptyList() // Here, OWM uses the correct language codes, so we don't need to map anything
val country = response.sys?.country ?: "" val lang = Locale.getDefault().language
val cityId = response.id ?: return emptyList()
val loc = "$city, $country" return response.mapNotNull {
return listOf( val name = it.local_names?.get(lang) ?: it.name ?: return@mapNotNull null
OpenWeatherMapLocation( OpenWeatherMapLatLonLocation(
name = loc, id = cityId name = "$name, ${it.country}",
lat = it.lat ?: return@mapNotNull null,
lon = it.lon ?: return@mapNotNull null,
) )
) }
} }
override suspend fun loadWeatherData(location: OpenWeatherMapLocation): WeatherUpdateResult<OpenWeatherMapLocation>? { override suspend fun loadWeatherData(location: OpenWeatherMapLocation): WeatherUpdateResult<OpenWeatherMapLocation>? {
@ -73,13 +79,29 @@ class OpenWeatherMapProvider(override val context: Context) :
val lang = getLanguageCode() val lang = getLanguageCode()
val currentWeather = try { val currentWeather = try {
openWeatherMapService.currentWeather( when {
appid = getApiKey() ?: return null, location is OpenWeatherMapLatLonLocation -> openWeatherMapService.currentWeather(
id = location?.id?.takeIf { lat == null || lon == null }, appid = getApiKey() ?: return null,
lat = lat, lat = location.lat,
lon = lon, lon = location.lon,
lang = lang, lang = lang,
) )
location is OpenWeatherMapLegacyLocation -> openWeatherMapService.currentWeather(
appid = getApiKey() ?: return null,
id = location.id,
lang = lang,
)
lat != null && lon != null -> openWeatherMapService.currentWeather(
appid = getApiKey() ?: return null,
lat = lat,
lon = lon,
lang = lang,
)
else -> {
Log.w("MM20", "OpenWeatherMapProvider returned no data because no location was provided")
return null
}
}
} catch (e: Exception) { } catch (e: Exception) {
CrashReporter.logException(e) CrashReporter.logException(e)
return null return null
@ -90,11 +112,14 @@ class OpenWeatherMapProvider(override val context: Context) :
val city = currentWeather.name val city = currentWeather.name
val country = currentWeather.sys?.country ?: return null val country = currentWeather.sys?.country ?: return null
val cityId = currentWeather.id ?: return null val cityId = currentWeather.id ?: return null
val loc = "$city, $country" val coords = currentWeather.coord ?: return null
if (coords.lat == null || coords.lon == null) return null
val loc = location?.name ?: "$city, $country"
val forecasts = try { val forecasts = try {
openWeatherMapService.forecast5Day3Hour( openWeatherMapService.forecast5Day3Hour(
id = cityId, lat = coords.lat,
lon = coords.lon,
appid = getApiKey() ?: return null, appid = getApiKey() ?: return null,
lang = lang lang = lang
) )
@ -158,19 +183,25 @@ class OpenWeatherMapProvider(override val context: Context) :
) )
return WeatherUpdateResult( return WeatherUpdateResult(
forecasts = forecastList, forecasts = forecastList,
location = OpenWeatherMapLocation( location = OpenWeatherMapLatLonLocation(
name = loc, name = loc,
id = cityId lat = coords.lat,
lon = coords.lon,
) )
) )
} }
private fun getLanguageCode(): String { private fun getLanguageCode(): String {
val lang = Locale.getDefault().language val lang = Locale.getDefault().language
// OWM incorrectly expects Czech to be "cz" instead of "cs" // OWM incorrectly expects country codes instead of language codes for some languages
// see https://openweathermap.org/current#multi // see https://openweathermap.org/current#multi
if (lang == "cs") return "cz" when (lang) {
return lang "cs" -> return "cz"
"al" -> return "sq"
"kr" -> return "ko"
"lv" -> return "la"
else -> return lang
}
} }
private fun getApiKey(): String? { private fun getApiKey(): String? {
@ -225,50 +256,107 @@ class OpenWeatherMapProvider(override val context: Context) :
preferences.edit { preferences.edit {
if (location == null) { if (location == null) {
remove(CITY_ID) remove(CITY_ID)
remove(LAT)
remove(LON)
remove(LOCATION) remove(LOCATION)
} else { } else {
putInt(CITY_ID, location.id) if (location is OpenWeatherMapLatLonLocation) {
putString(LOCATION, location.name) putDouble(LAT, location.lat)
putDouble(LON, location.lon)
putString(LOCATION, location.name)
} else if (location is OpenWeatherMapLegacyLocation) {
putInt(CITY_ID, location.id)
putString(LOCATION, location.name)
}
} }
} }
} }
override fun getLocation(): OpenWeatherMapLocation? { override fun getLocation(): OpenWeatherMapLocation? {
val id = preferences.getInt(CITY_ID, -1).takeIf { it != -1 } ?: return null val lat = preferences.getDouble(LAT, Double.NaN).takeIf { !it.isNaN() }
val lon = preferences.getDouble(LON, Double.NaN).takeIf { !it.isNaN() }
val name = preferences.getString(LOCATION, null) ?: return null val name = preferences.getString(LOCATION, null) ?: return null
return OpenWeatherMapLocation( if (lat != null && lon != null) {
name = name, return OpenWeatherMapLatLonLocation(
id = id, name = name,
) lat = lat,
lon = lon,
)
}
val id = preferences.getInt(CITY_ID, -1).takeIf { it != -1 }
if (id != null) {
return OpenWeatherMapLegacyLocation(
name = name,
id = id,
)
}
return null
} }
override fun getLastLocation(): OpenWeatherMapLocation? { override fun getLastLocation(): OpenWeatherMapLocation? {
val id = preferences.getInt(LAST_CITY_ID, -1).takeIf { it != -1 } ?: return null val lat = preferences.getDouble(LAST_LAT, Double.NaN).takeIf { !it.isNaN() }
val lon = preferences.getDouble(LAST_LON, Double.NaN).takeIf { !it.isNaN() }
val name = preferences.getString(LAST_LOCATION, null) ?: return null val name = preferences.getString(LAST_LOCATION, null) ?: return null
return OpenWeatherMapLocation( if (lat != null && lon != null) {
name = name, return OpenWeatherMapLatLonLocation(
id = id, name = name,
) lat = lat,
lon = lon,
)
}
val id = preferences.getInt(LAST_CITY_ID, -1).takeIf { it != -1 }
if (id != null) {
return OpenWeatherMapLegacyLocation(
name = name,
id = id,
)
}
return null
} }
override fun saveLastLocation(location: OpenWeatherMapLocation) { override fun saveLastLocation(location: OpenWeatherMapLocation) {
preferences.edit { preferences.edit {
putString(LAST_LOCATION, location.name) if (location is OpenWeatherMapLatLonLocation) {
putInt(LAST_CITY_ID, location.id) putDouble(LAST_LAT, location.lat)
putDouble(LAST_LON, location.lon)
remove(LAST_CITY_ID)
putString(LAST_LOCATION, location.name)
} else if (location is OpenWeatherMapLegacyLocation) {
putInt(LAST_CITY_ID, location.id)
remove(LAST_LAT)
remove(LAST_LON)
putString(LAST_LOCATION, location.name)
}
} }
} }
companion object { companion object {
private const val PREFERENCES = "openweathermap" private const val PREFERENCES = "openweathermap"
@Deprecated("Use LAT and LON instead")
private const val CITY_ID = "city_id" private const val CITY_ID = "city_id"
@Deprecated("Use LAST_LAT and LAST_LON instead")
private const val LAST_CITY_ID = "last_city_id" private const val LAST_CITY_ID = "last_city_id"
private const val LAST_UPDATE = "last_update" private const val LAST_UPDATE = "last_update"
private const val LOCATION = "location" private const val LOCATION = "location"
private const val LAT = "lat"
private const val LON = "lon"
private const val LAST_LOCATION = "last_location" private const val LAST_LOCATION = "last_location"
private const val LAST_LAT = "last_lat"
private const val LAST_LON = "last_lon"
} }
} }
data class OpenWeatherMapLocation( sealed interface OpenWeatherMapLocation : WeatherLocation
data class OpenWeatherMapLatLonLocation(
override val name: String, override val name: String,
val id: Int val lat: Double,
) : WeatherLocation val lon: Double,
) : OpenWeatherMapLocation
data class OpenWeatherMapLegacyLocation(
override val name: String,
val id: Int,
) : OpenWeatherMapLocation