mirror of
https://github.com/PranshulGG/WeatherMaster.git
synced 2026-10-22 02:52:37 -05:00
add imet integration
This commit is contained in:
1 parent
b229b37e93
commit
72c4ffbbf0
13 files changed
+949
-1
No files matched your search
@@ -22,6 +22,9 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.eccc.EcccA
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.fmi.FmiApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.gismeteo.GismeteoApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.imd.ImdApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.IbgeApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.InmetForecastApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.InmetObservationApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.ipma.IpmaApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteoam.MeteoamApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteofrance.MeteoFranceApi
|
||||
@@ -164,4 +167,16 @@ object ApiModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCwaApi(): CwaApi = CwaApi.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideInmetForecastApi(): InmetForecastApi = InmetForecastApi.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideInmetObservationApi(): InmetObservationApi = InmetObservationApi.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIbgeApi(): IbgeApi = IbgeApi.create()
|
||||
}
|
||||
+15
@@ -32,6 +32,10 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.gismeteo.G
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.gismeteo.GismeteoRepository
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.imd.ImdApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.imd.ImdRepository
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.IbgeApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.InmetForecastApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.InmetObservationApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.InmetRepository
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.ipma.IpmaApi
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.ipma.IpmaRepository
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteoam.MeteoamApi
|
||||
@@ -278,4 +282,15 @@ object WeatherRepositoryModule {
|
||||
apiKeysDao: ApiKeysDao,
|
||||
locationKeysDao: LocationKeysDao
|
||||
): CwaRepository = CwaRepository(dao, weatherDao, api, apiKeysDao, locationKeysDao)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideInmetRepository(
|
||||
dao: LocationsDao,
|
||||
weatherDao: WeatherDao,
|
||||
forecastApi: InmetForecastApi,
|
||||
observationApi: InmetObservationApi,
|
||||
ibgeApi: IbgeApi,
|
||||
locationKeysDao: LocationKeysDao
|
||||
): InmetRepository = InmetRepository(dao, weatherDao, forecastApi, observationApi, ibgeApi, locationKeysDao)
|
||||
}
|
||||
+8
@@ -128,6 +128,12 @@ enum class WeatherSource(
|
||||
displayLink = "https://opendata.cwa.gov.tw/",
|
||||
countryNameRes = R.string.country_taiwan,
|
||||
requiresUserApiKey = true
|
||||
),
|
||||
INMET(
|
||||
displayName = "INMET",
|
||||
fullName = "Instituto Nacional de Meteorologia",
|
||||
displayLink = "https://portal.inmet.gov.br/",
|
||||
countryNameRes = R.string.country_brazil
|
||||
);
|
||||
|
||||
// Sources that provide snow/rain as precipitation
|
||||
@@ -139,6 +145,7 @@ enum class WeatherSource(
|
||||
BMKG -> false
|
||||
IMD -> false
|
||||
CWA -> false
|
||||
INMET -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
@@ -164,6 +171,7 @@ private val weatherSourcesByCountry = buildMap {
|
||||
put("ES", listOf(WeatherSource.AEMET))
|
||||
put("IN", listOf(WeatherSource.IMD))
|
||||
put("TW", listOf(WeatherSource.CWA))
|
||||
put("BR", listOf(WeatherSource.INMET))
|
||||
|
||||
}
|
||||
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package com.pranshulgg.weather_master_app.core.network.sources.weather.inmet
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json.IbgeMunicipioJson
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json.InmetDayJson
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json.InmetHourlyEntryJson
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json.InmetStationJson
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
interface InmetForecastApi {
|
||||
|
||||
@GET("previsao/{ibgeCode}")
|
||||
suspend fun fetchForecast(
|
||||
@Path("ibgeCode") ibgeCode: String
|
||||
): Response<Map<String, Map<String, InmetDayJson>>>
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://apiprevmet3.inmet.gov.br/"
|
||||
|
||||
fun create(): InmetForecastApi {
|
||||
val client = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(InmetForecastApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface InmetObservationApi {
|
||||
|
||||
@GET("estacoes/T")
|
||||
suspend fun fetchStations(): Response<List<InmetStationJson>>
|
||||
|
||||
@GET("estacao/{dataInicio}/{dataFim}/{stationCode}")
|
||||
suspend fun fetchHourlyData(
|
||||
@Path("dataInicio") dataInicio: String,
|
||||
@Path("dataFim") dataFim: String,
|
||||
@Path("stationCode") stationCode: String
|
||||
): Response<List<InmetHourlyEntryJson>>
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://apitempo.inmet.gov.br/"
|
||||
|
||||
fun create(): InmetObservationApi {
|
||||
val client = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(InmetObservationApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface IbgeApi {
|
||||
|
||||
@GET("localidades/estados/{uf}/municipios")
|
||||
suspend fun fetchMunicipiosByState(
|
||||
@Path("uf") uf: String
|
||||
): Response<List<IbgeMunicipioJson>>
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://servicodados.ibge.gov.br/api/v1/"
|
||||
|
||||
fun create(): IbgeApi {
|
||||
val client = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(IbgeApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
package com.pranshulgg.weather_master_app.core.network.sources.weather.inmet
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.toAppException
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.WeatherResultType
|
||||
import com.pranshulgg.weather_master_app.core.network.calls.safeApiCall
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json.IbgeMunicipioJson
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json.InmetStationJson
|
||||
import com.pranshulgg.weather_master_app.core.utils.weather.cache.isWeatherCacheSafe
|
||||
import com.pranshulgg.weather_master_app.core.utils.weather.cache.shouldReturnWeatherCache
|
||||
import com.pranshulgg.weather_master_app.core.utils.weather.forecast.mergeHourlyWeather
|
||||
import com.pranshulgg.weather_master_app.data.local.dao.location.LocationKeysDao
|
||||
import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao
|
||||
import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherDao
|
||||
import com.pranshulgg.weather_master_app.data.local.entity.location.LocationKeyEntity
|
||||
import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.inmet.InmetWeatherBundle
|
||||
import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.inmet.toDomain
|
||||
import com.pranshulgg.weather_master_app.data.local.mapper.weather.toCurrentWeatherEntity
|
||||
import com.pranshulgg.weather_master_app.data.local.mapper.weather.toDailyWeatherEntity
|
||||
import com.pranshulgg.weather_master_app.data.local.mapper.weather.toDomain
|
||||
import com.pranshulgg.weather_master_app.data.local.mapper.weather.toHourlyWeatherEntity
|
||||
import com.pranshulgg.weather_master_app.data.repository.WeatherRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
|
||||
class InmetRepository @Inject constructor(
|
||||
val dao: LocationsDao,
|
||||
val weatherDao: WeatherDao,
|
||||
val forecastApi: InmetForecastApi,
|
||||
val observationApi: InmetObservationApi,
|
||||
val ibgeApi: IbgeApi,
|
||||
val locationKeysDao: LocationKeysDao
|
||||
) : WeatherRepository {
|
||||
|
||||
@Volatile
|
||||
private var cachedStations: List<InmetStationJson>? = null
|
||||
|
||||
override suspend fun getWeather(
|
||||
location: Location,
|
||||
isManualRefresh: Boolean,
|
||||
isForceRefresh: Boolean
|
||||
): WeatherResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
val cache = dao.getWeatherDataForLocation(location.id)
|
||||
|
||||
val shouldReturnCache = shouldReturnWeatherCache(cache, isManualRefresh, isForceRefresh)
|
||||
|
||||
val existingHourly = weatherDao.getHourlyDataForLocation(location.id, location.source)
|
||||
|
||||
when (shouldReturnCache) {
|
||||
WeatherResultType.REFRESH_TOO_EARLY -> return@withContext WeatherResult.RefreshNotAvailable
|
||||
WeatherResultType.SUCCESS -> return@withContext WeatherResult.Success(cache!!.toDomain())
|
||||
else -> {}
|
||||
}
|
||||
|
||||
return@withContext try {
|
||||
val ibgeCode = resolveIbgeCode(location)
|
||||
|
||||
val forecastResponse = safeApiCall {
|
||||
forecastApi.fetchForecast(ibgeCode)
|
||||
}.getOrElse { return@withContext WeatherResult.Error(exception = it.toAppException()) }
|
||||
|
||||
val hourlyObservations = try {
|
||||
val stationCode = resolveNearestStationCode(location)
|
||||
if (stationCode != null) {
|
||||
val today = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
val result = safeApiCall {
|
||||
observationApi.fetchHourlyData(today, today, stationCode)
|
||||
}
|
||||
result.getOrNull()?.takeIf { it.isNotEmpty() }
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val bundle = InmetWeatherBundle(
|
||||
forecast = forecastResponse,
|
||||
hourlyObservations = hourlyObservations
|
||||
)
|
||||
|
||||
val domain = bundle.toDomain(location)
|
||||
|
||||
val mergedHourly = mergeHourlyWeather(
|
||||
existing = existingHourly,
|
||||
incoming = domain.hourly.toHourlyWeatherEntity(location)
|
||||
)
|
||||
weatherDao.insertWeather(
|
||||
domain.current.toCurrentWeatherEntity(location.id),
|
||||
mergedHourly,
|
||||
domain.daily.toDailyWeatherEntity(location.id),
|
||||
location.id
|
||||
)
|
||||
WeatherResult.Success(domain)
|
||||
|
||||
} catch (e: Exception) {
|
||||
val isCacheSafe = isWeatherCacheSafe(cache)
|
||||
WeatherResult.Error(
|
||||
exception = e,
|
||||
if (isCacheSafe) cache?.toDomain() else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resolveIbgeCode(location: Location): String {
|
||||
locationKeysDao.getCityKeyForLocation(location.id)?.cityKey?.let { return it }
|
||||
|
||||
val stations = getStations()
|
||||
val nearestStation = stations?.findNearestStation(location.latitude, location.longitude)
|
||||
|
||||
val uf = nearestStation?.sgEstado
|
||||
?: inferUfFromState(location)
|
||||
|
||||
val ibgeCode = if (uf != null) {
|
||||
val municipios = safeApiCall {
|
||||
ibgeApi.fetchMunicipiosByState(uf)
|
||||
}.getOrNull()
|
||||
|
||||
municipios?.let { list ->
|
||||
val stationName = nearestStation?.dcNome
|
||||
val matchTarget = stationName ?: location.name
|
||||
|
||||
list.bestMatch(matchTarget)?.id?.toString()
|
||||
}
|
||||
} else null
|
||||
|
||||
val resolvedCode = ibgeCode ?: "3550308"
|
||||
|
||||
locationKeysDao.insertCityKey(
|
||||
LocationKeyEntity(
|
||||
locationId = location.id,
|
||||
cityKey = resolvedCode
|
||||
)
|
||||
)
|
||||
|
||||
return resolvedCode
|
||||
}
|
||||
|
||||
private suspend fun resolveNearestStationCode(location: Location): String? {
|
||||
val stations = getStations() ?: return null
|
||||
return stations.findNearestStation(location.latitude, location.longitude)?.cdEstacao
|
||||
}
|
||||
|
||||
private suspend fun getStations(): List<InmetStationJson>? {
|
||||
cachedStations?.let { return it }
|
||||
return try {
|
||||
val result = safeApiCall { observationApi.fetchStations() }
|
||||
val stations = result.getOrNull()
|
||||
?.filter { it.cdSituacao == "Operante" && it.tpEstacao == "Automatica" }
|
||||
?.filter { it.vlLatitude.toDoubleOrNull() != null && it.vlLongitude.toDoubleOrNull() != null }
|
||||
stations?.let { cachedStations = it }
|
||||
stations
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun inferUfFromState(location: Location): String? {
|
||||
val stateLower = location.state.lowercase().trim()
|
||||
val stateMap = mapOf(
|
||||
"acre" to "AC", "alagoas" to "AL", "amapá" to "AP", "amazonas" to "AM",
|
||||
"bahia" to "BA", "ceará" to "CE", "distrito federal" to "DF",
|
||||
"espírito santo" to "ES", "goiás" to "GO", "maranhão" to "MA",
|
||||
"mato grosso" to "MT", "mato grosso do sul" to "MS",
|
||||
"minas gerais" to "MG", "pará" to "PA", "paraíba" to "PB",
|
||||
"paraná" to "PR", "pernambuco" to "PE", "piauí" to "PI",
|
||||
"rio de janeiro" to "RJ", "rio grande do norte" to "RN",
|
||||
"rio grande do sul" to "RS", "rondônia" to "RO", "roraima" to "RR",
|
||||
"santa catarina" to "SC", "são paulo" to "SP", "sergipe" to "SE",
|
||||
"tocantins" to "TO"
|
||||
)
|
||||
return stateMap[stateLower] ?: stateMap.entries.firstOrNull {
|
||||
stateLower.contains(it.key) || it.key.contains(stateLower)
|
||||
}?.value
|
||||
}
|
||||
|
||||
private fun List<InmetStationJson>.findNearestStation(
|
||||
lat: Double,
|
||||
lon: Double
|
||||
): InmetStationJson? {
|
||||
return minByOrNull { station ->
|
||||
val stationLat = station.vlLatitude.toDoubleOrNull() ?: return@minByOrNull Double.MAX_VALUE
|
||||
val stationLon = station.vlLongitude.toDoubleOrNull() ?: return@minByOrNull Double.MAX_VALUE
|
||||
haversineDistance(lat, lon, stationLat, stationLon)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<IbgeMunicipioJson>.bestMatch(
|
||||
target: String
|
||||
): IbgeMunicipioJson? {
|
||||
val normalizedTarget = stripAccents(target.lowercase().trim())
|
||||
return firstOrNull { stripAccents(it.nome.lowercase().trim()) == normalizedTarget }
|
||||
?: firstOrNull { stripAccents(it.nome.lowercase().trim()).contains(normalizedTarget) }
|
||||
?: firstOrNull { normalizedTarget.contains(stripAccents(it.nome.lowercase().trim())) }
|
||||
?: minByOrNull {
|
||||
levenshteinDistance(normalizedTarget, stripAccents(it.nome.lowercase().trim()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun haversineDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
|
||||
val r = 6371.0
|
||||
val dLat = Math.toRadians(lat2 - lat1)
|
||||
val dLon = Math.toRadians(lon2 - lon1)
|
||||
val a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
|
||||
sin(dLon / 2) * sin(dLon / 2)
|
||||
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||
return r * c
|
||||
}
|
||||
|
||||
private fun stripAccents(input: String): String {
|
||||
val normalized = java.text.Normalizer.normalize(input, java.text.Normalizer.Form.NFD)
|
||||
return normalized.replace(Regex("\\p{InCombiningDiacriticalMarks}+"), "")
|
||||
}
|
||||
|
||||
private fun levenshteinDistance(a: String, b: String): Int {
|
||||
val dp = Array(a.length + 1) { IntArray(b.length + 1) }
|
||||
for (i in 0..a.length) dp[i][0] = i
|
||||
for (j in 0..b.length) dp[0][j] = j
|
||||
for (i in 1..a.length) {
|
||||
for (j in 1..b.length) {
|
||||
dp[i][j] = minOf(
|
||||
dp[i - 1][j] + 1,
|
||||
dp[i][j - 1] + 1,
|
||||
dp[i - 1][j - 1] + if (a[i - 1] == b[j - 1]) 0 else 1
|
||||
)
|
||||
}
|
||||
}
|
||||
return dp[a.length][b.length]
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.pranshulgg.weather_master_app.core.network.sources.weather.inmet
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.WeatherCondition
|
||||
|
||||
object InmetWeatherConditionMap {
|
||||
|
||||
fun getCondition(codIcone: String?, resumo: String?): WeatherCondition {
|
||||
val byCode = codIcone?.toIntOrNull()?.let { mapByCode(it) }
|
||||
if (byCode != null && byCode != WeatherCondition.NO_CONDITION_FOUND) return byCode
|
||||
|
||||
val byText = resumo?.let { mapByText(it) }
|
||||
if (byText != null) return byText
|
||||
|
||||
return WeatherCondition.NO_CONDITION_FOUND
|
||||
}
|
||||
|
||||
private fun mapByCode(code: Int): WeatherCondition {
|
||||
return when (code) {
|
||||
1, 25 -> WeatherCondition.CLEAR_SKY
|
||||
2, 29, 30 -> WeatherCondition.PARTLY_CLOUDY
|
||||
3 -> WeatherCondition.OVERCAST
|
||||
4 -> WeatherCondition.OVERCAST
|
||||
5, 6 -> WeatherCondition.RAIN
|
||||
7 -> WeatherCondition.THUNDERSTORM
|
||||
8 -> WeatherCondition.THUNDERSTORM
|
||||
9, 11 -> WeatherCondition.FOG_HAZE
|
||||
10 -> WeatherCondition.SNOW
|
||||
12 -> WeatherCondition.HAIL
|
||||
13 -> WeatherCondition.LIGHT_SNOW
|
||||
else -> WeatherCondition.NO_CONDITION_FOUND
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapByText(text: String): WeatherCondition? {
|
||||
val lower = text.lowercase().trim()
|
||||
|
||||
if (lower.contains("tempestade") || lower.contains("trovoada") ||
|
||||
lower.contains("raio") || lower.contains("relâmpago")
|
||||
) return WeatherCondition.THUNDERSTORM
|
||||
|
||||
if (lower.contains("granizo")) return WeatherCondition.HAIL
|
||||
if (lower.contains("neve")) return WeatherCondition.SNOW
|
||||
if (lower.contains("geada")) return WeatherCondition.LIGHT_SNOW
|
||||
|
||||
if (lower.contains("chuva forte") || lower.contains("pancada forte") ||
|
||||
lower.contains("temporada")
|
||||
) return WeatherCondition.HEAVY_RAIN
|
||||
|
||||
if (lower.contains("chuva fraca") || lower.contains("leve chuva") ||
|
||||
lower.contains("garoa")
|
||||
) return WeatherCondition.LIGHT_RAIN
|
||||
|
||||
if (lower.contains("chuva") || lower.contains("pancada")) return WeatherCondition.RAIN
|
||||
|
||||
if (lower.contains("nevoeiro") || lower.contains("névoa") ||
|
||||
lower.contains("neblina") || lower.contains("fumaça")
|
||||
) return WeatherCondition.FOG_HAZE
|
||||
|
||||
if (lower.contains("encoberto")) return WeatherCondition.OVERCAST
|
||||
if (lower.contains("nublado")) return WeatherCondition.OVERCAST
|
||||
|
||||
if (lower.contains("poucas nuvens") || lower.contains("parcialmente nublado") ||
|
||||
lower.contains("sol entre nuvens")
|
||||
) return WeatherCondition.PARTLY_CLOUDY
|
||||
|
||||
if (lower.contains("céu claro") || lower.contains("ceu claro") ||
|
||||
lower.contains("sol") || lower.contains("limpo")
|
||||
) return WeatherCondition.CLEAR_SKY
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class InmetDayJson(
|
||||
val uf: String? = null,
|
||||
val entidade: String? = null,
|
||||
val resumo: String? = null,
|
||||
val tempo: String? = null,
|
||||
@SerializedName("temp_max") val tempMax: Int? = null,
|
||||
@SerializedName("temp_min") val tempMin: Int? = null,
|
||||
@SerializedName("dir_vento") val dirVento: String? = null,
|
||||
@SerializedName("int_vento") val intVento: String? = null,
|
||||
@SerializedName("cod_icone") val codIcone: String? = null,
|
||||
val manha: InmetPeriodJson? = null,
|
||||
val tarde: InmetPeriodJson? = null,
|
||||
val noite: InmetPeriodJson? = null
|
||||
)
|
||||
|
||||
data class InmetPeriodJson(
|
||||
val uf: String? = null,
|
||||
val entidade: String? = null,
|
||||
val resumo: String? = null,
|
||||
val tempo: String? = null,
|
||||
@SerializedName("temp_max") val tempMax: Int? = null,
|
||||
@SerializedName("temp_min") val tempMin: Int? = null,
|
||||
@SerializedName("dir_vento") val dirVento: String? = null,
|
||||
@SerializedName("int_vento") val intVento: String? = null,
|
||||
@SerializedName("cod_icone") val codIcone: String? = null
|
||||
)
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class IbgeMunicipioJson(
|
||||
val id: Int,
|
||||
val nome: String,
|
||||
val microrregiao: IbgeMicrorregiaoJson? = null
|
||||
)
|
||||
|
||||
data class IbgeMicrorregiaoJson(
|
||||
val nome: String? = null,
|
||||
val uf: IbgeUfJson? = null
|
||||
)
|
||||
|
||||
data class IbgeUfJson(
|
||||
val id: Int? = null,
|
||||
val sigla: String? = null,
|
||||
val nome: String? = null
|
||||
)
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class InmetStationJson(
|
||||
@SerializedName("CD_ESTACAO") val cdEstacao: String,
|
||||
@SerializedName("DC_NOME") val dcNome: String,
|
||||
@SerializedName("VL_LATITUDE") val vlLatitude: String,
|
||||
@SerializedName("VL_LONGITUDE") val vlLongitude: String,
|
||||
@SerializedName("SG_ESTADO") val sgEstado: String,
|
||||
@SerializedName("VL_ALTITUDE") val vlAltitude: String?,
|
||||
@SerializedName("CD_SITUACAO") val cdSituacao: String?,
|
||||
@SerializedName("TP_ESTACAO") val tpEstacao: String?,
|
||||
@SerializedName("FL_CAPITAL") val flCapital: String?
|
||||
)
|
||||
|
||||
data class InmetHourlyEntryJson(
|
||||
@SerializedName("CD_ESTACAO") val cdEstacao: String? = null,
|
||||
@SerializedName("DT_MEDICAO") val dtMedicao: String? = null,
|
||||
@SerializedName("HR_MEDICAO") val hrMedicao: String? = null,
|
||||
@SerializedName("TEMP_AR") val tempAr: String? = null,
|
||||
@SerializedName("TEMP_MIN_HORA") val tempMinHora: String? = null,
|
||||
@SerializedName("TEMP_MAX_HORA") val tempMaxHora: String? = null,
|
||||
@SerializedName("UMID_REL_AR") val umidRelAr: String? = null,
|
||||
@SerializedName("UMID_MIN_HORA") val umidMinHora: String? = null,
|
||||
@SerializedName("VENT_VEL") val ventVel: String? = null,
|
||||
@SerializedName("VENT_DIR") val ventDir: String? = null,
|
||||
@SerializedName("VENT_RAJ_MAX") val ventRajMax: String? = null,
|
||||
@SerializedName("PRESS_ATM_EST") val pressAtmEst: String? = null,
|
||||
@SerializedName("PRECIPITACAO_TOTAL") val precipitacaoTotal: String? = null,
|
||||
@SerializedName("PREV_CHUVA") val prevChuva: String? = null
|
||||
)
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
package com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.inmet
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.astro.MoonPhase
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherCurrent
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherDaily
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherHourly
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.WindSpeedUnit
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.WeatherCondition
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.wind.WindDirection
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.InmetWeatherConditionMap
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json.InmetDayJson
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json.InmetHourlyEntryJson
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.json.InmetPeriodJson
|
||||
import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.normalizeToDay
|
||||
import com.pranshulgg.weather_master_app.core.utils.formatters.safeZoneId
|
||||
import com.pranshulgg.weather_master_app.core.utils.weather.astronomy.getMoonTimings
|
||||
import com.pranshulgg.weather_master_app.core.utils.weather.astronomy.getSunTimings
|
||||
import com.pranshulgg.weather_master_app.core.utils.weather.calculations.computeApparentTemperature
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
|
||||
data class InmetWeatherBundle(
|
||||
val forecast: Map<String, Map<String, InmetDayJson>>,
|
||||
val hourlyObservations: List<InmetHourlyEntryJson>?
|
||||
)
|
||||
|
||||
|
||||
fun InmetWeatherBundle.toDomain(location: Location): Weather {
|
||||
|
||||
val ibgeCode = forecast.keys.firstOrNull()
|
||||
val days = ibgeCode?.let { forecast[it] } ?: emptyMap()
|
||||
|
||||
val zone = safeZoneId(location.timezone)
|
||||
|
||||
val dailyEntries = mutableListOf<WeatherDaily>()
|
||||
val hourlyEntries = mutableListOf<WeatherHourly>()
|
||||
|
||||
val dayTimeMillis = mutableListOf<Long>()
|
||||
|
||||
for ((dateStr, dayJson) in days) {
|
||||
val dayMillis = parseInmetDate(dateStr, zone)
|
||||
dayTimeMillis.add(dayMillis)
|
||||
|
||||
val periods = listOfNotNull(dayJson.manha, dayJson.tarde, dayJson.noite)
|
||||
|
||||
if (periods.isNotEmpty()) {
|
||||
for ((periodJson, hourOffset) in listOf(
|
||||
dayJson.manha to 9,
|
||||
dayJson.tarde to 15,
|
||||
dayJson.noite to 21
|
||||
)) {
|
||||
periodJson?.let {
|
||||
hourlyEntries.add(
|
||||
createHourlyFromPeriod(it, dayMillis, hourOffset, zone)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val tempMin = periods.minOfOrNull { it.tempMin ?: Int.MAX_VALUE }
|
||||
?.takeIf { it != Int.MAX_VALUE }?.toDouble()
|
||||
val tempMax = periods.maxOfOrNull { it.tempMax ?: Int.MIN_VALUE }
|
||||
?.takeIf { it != Int.MIN_VALUE }?.toDouble()
|
||||
|
||||
val windSpeed = periods.mapNotNull { parseWindIntensityToKmh(it.intVento) }.averageOrNull()
|
||||
val windDir = periods.firstNotNullOfOrNull { parseWindDirection(it.dirVento) }
|
||||
|
||||
val condition = periods.firstNotNullOfOrNull {
|
||||
InmetWeatherConditionMap.getCondition(it.codIcone, it.resumo)
|
||||
} ?: WeatherCondition.NO_CONDITION_FOUND
|
||||
|
||||
dailyEntries.add(
|
||||
WeatherDaily(
|
||||
temperatureMin = tempMin,
|
||||
temperatureMax = tempMax,
|
||||
windSpeed = windSpeed,
|
||||
windDirection = windDir,
|
||||
rainSum = 0.0,
|
||||
snowfallSum = null,
|
||||
uvIndexMax = null,
|
||||
weatherCondition = condition,
|
||||
time = dayMillis,
|
||||
precipitationProbabilityMax = null,
|
||||
pressureMsl = null,
|
||||
visibility = null,
|
||||
humidity = null,
|
||||
dewPoint = null,
|
||||
sunrise = -1L,
|
||||
sunset = -1L,
|
||||
moonrise = -1L,
|
||||
moonset = -1L,
|
||||
moonPhase = MoonPhase.NEW_MOON,
|
||||
dawn = -1L,
|
||||
dusk = -1L
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val tempMin = dayJson.tempMin?.toDouble()
|
||||
val tempMax = dayJson.tempMax?.toDouble()
|
||||
val windSpeed = parseWindIntensityToKmh(dayJson.intVento)
|
||||
val windDir = parseWindDirection(dayJson.dirVento)
|
||||
val condition = InmetWeatherConditionMap.getCondition(dayJson.codIcone, dayJson.resumo)
|
||||
|
||||
dailyEntries.add(
|
||||
WeatherDaily(
|
||||
temperatureMin = tempMin,
|
||||
temperatureMax = tempMax,
|
||||
windSpeed = windSpeed,
|
||||
windDirection = windDir,
|
||||
rainSum = 0.0,
|
||||
snowfallSum = null,
|
||||
uvIndexMax = null,
|
||||
weatherCondition = condition,
|
||||
time = dayMillis,
|
||||
precipitationProbabilityMax = null,
|
||||
pressureMsl = null,
|
||||
visibility = null,
|
||||
humidity = null,
|
||||
dewPoint = null,
|
||||
sunrise = -1L,
|
||||
sunset = -1L,
|
||||
moonrise = -1L,
|
||||
moonset = -1L,
|
||||
moonPhase = MoonPhase.NEW_MOON,
|
||||
dawn = -1L,
|
||||
dusk = -1L
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (hourlyObservations != null && hourlyObservations.isNotEmpty()) {
|
||||
hourlyEntries.clear()
|
||||
hourlyEntries.addAll(hourlyObservations.map { createHourlyFromObservation(it, zone) })
|
||||
}
|
||||
|
||||
val current = if (hourlyObservations != null && hourlyObservations.isNotEmpty()) {
|
||||
createCurrentFromObservation(hourlyObservations.last(), location)
|
||||
} else {
|
||||
createCurrentFromForecast(days, zone, location)
|
||||
}
|
||||
|
||||
val sunTimings = getSunTimings(
|
||||
dayTimeMillis,
|
||||
location.timezone,
|
||||
location.latitude,
|
||||
location.longitude
|
||||
)
|
||||
val moonTimings = getMoonTimings(
|
||||
dayTimeMillis,
|
||||
location.timezone,
|
||||
location.latitude,
|
||||
location.longitude
|
||||
)
|
||||
|
||||
dailyEntries.forEachIndexed { index, day ->
|
||||
if (index < sunTimings.size && index < moonTimings.size) {
|
||||
val sun = sunTimings[index]
|
||||
val moon = moonTimings[index]
|
||||
dailyEntries[index] = day.copy(
|
||||
sunrise = sun.sunrise ?: -1L,
|
||||
sunset = sun.sunset ?: -1L,
|
||||
moonrise = moon.moonrise ?: -1L,
|
||||
moonset = moon.moonset ?: -1L,
|
||||
moonPhase = moon.phase,
|
||||
dawn = sun.dawn ?: -1L,
|
||||
dusk = sun.dusk ?: -1L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return Weather(
|
||||
location = location,
|
||||
current = current,
|
||||
hourly = hourlyEntries,
|
||||
daily = dailyEntries
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun parseInmetDate(dateStr: String, zone: ZoneId): Long {
|
||||
return try {
|
||||
val formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
|
||||
LocalDate.parse(dateStr.trim(), formatter)
|
||||
.atStartOfDay(zone)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
} catch (e: Exception) {
|
||||
System.currentTimeMillis().normalizeToDay(zone.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun parseObservationTimestamp(dateStr: String?, timeStr: String?, zone: ZoneId): Long {
|
||||
return try {
|
||||
val datePart = dateStr?.trim() ?: return System.currentTimeMillis()
|
||||
val timePart = timeStr?.trim() ?: "0000"
|
||||
|
||||
val hours = timePart.padStart(4, '0').substring(0, 2).toInt()
|
||||
val minutes = timePart.padStart(4, '0').substring(2, 4).toInt()
|
||||
|
||||
LocalDate.parse(datePart, DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
.atTime(hours, minutes)
|
||||
.atZone(zone)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
} catch (e: Exception) {
|
||||
System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun parseWindDirection(dirVento: String?): WindDirection? {
|
||||
if (dirVento.isNullOrBlank()) return null
|
||||
val firstDir = dirVento.split("-", "/").firstOrNull()?.trim()
|
||||
return WindDirection.toWindDirectionFromString(firstDir)
|
||||
}
|
||||
|
||||
|
||||
private fun parseWindIntensityToKmh(intVento: String?): Double? {
|
||||
if (intVento.isNullOrBlank()) return null
|
||||
val lower = intVento.lowercase().trim()
|
||||
return when {
|
||||
lower.contains("calma") -> 0.0
|
||||
lower.contains("muito fraco") || lower.contains("muito fracas") -> 5.0
|
||||
lower.contains("fraco") || lower.contains("fracas") -> 10.0
|
||||
lower.contains("moderado") || lower.contains("moderadas") -> 20.0
|
||||
lower.contains("muito forte") || lower.contains("muito fortes") -> 45.0
|
||||
lower.contains("forte") || lower.contains("fortes") -> 30.0
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun parseDoubleSafe(value: String?): Double? {
|
||||
return value?.trim()?.replace(",", ".")?.toDoubleOrNull()
|
||||
}
|
||||
|
||||
|
||||
private fun createHourlyFromPeriod(
|
||||
period: InmetPeriodJson,
|
||||
dayMillis: Long,
|
||||
hourOffset: Int,
|
||||
zone: ZoneId
|
||||
): WeatherHourly {
|
||||
val timeMilli = try {
|
||||
Instant.ofEpochMilli(dayMillis)
|
||||
.atZone(zone)
|
||||
.toLocalDate()
|
||||
.atTime(hourOffset, 0)
|
||||
.atZone(zone)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
} catch (e: Exception) {
|
||||
dayMillis
|
||||
}
|
||||
|
||||
val temp = listOfNotNull(period.tempMax, period.tempMin).averageOrNull()
|
||||
|
||||
return WeatherHourly(
|
||||
temperature = temp,
|
||||
windSpeed = parseWindIntensityToKmh(period.intVento),
|
||||
windDirection = parseWindDirection(period.dirVento),
|
||||
rain = 0.0,
|
||||
snowfall = null,
|
||||
uvIndex = null,
|
||||
pressureMsl = null,
|
||||
visibility = null,
|
||||
humidity = null,
|
||||
dewPoint = null,
|
||||
weatherCondition = InmetWeatherConditionMap.getCondition(period.codIcone, period.resumo),
|
||||
time = timeMilli,
|
||||
precipitationProbability = null
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun createHourlyFromObservation(
|
||||
entry: InmetHourlyEntryJson,
|
||||
zone: ZoneId
|
||||
): WeatherHourly {
|
||||
val timeMilli = parseObservationTimestamp(entry.dtMedicao, entry.hrMedicao, zone)
|
||||
val windMs = parseDoubleSafe(entry.ventVel)
|
||||
val windKmh = windMs?.let { WindSpeedUnit.MPS.convert(it, WindSpeedUnit.KPH) }
|
||||
val windDir = parseDoubleSafe(entry.ventDir)?.toInt()?.let {
|
||||
WindDirection.toWindDirectionFromDegrees(it)
|
||||
}
|
||||
|
||||
return WeatherHourly(
|
||||
temperature = parseDoubleSafe(entry.tempAr),
|
||||
windSpeed = windKmh,
|
||||
windDirection = windDir,
|
||||
rain = parseDoubleSafe(entry.precipitacaoTotal) ?: 0.0,
|
||||
snowfall = null,
|
||||
uvIndex = null,
|
||||
pressureMsl = parseDoubleSafe(entry.pressAtmEst),
|
||||
visibility = null,
|
||||
humidity = parseDoubleSafe(entry.umidRelAr),
|
||||
dewPoint = null,
|
||||
weatherCondition = WeatherCondition.NO_CONDITION_FOUND,
|
||||
time = timeMilli,
|
||||
precipitationProbability = null
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun createCurrentFromObservation(
|
||||
entry: InmetHourlyEntryJson,
|
||||
location: Location
|
||||
): WeatherCurrent {
|
||||
val zone = safeZoneId(location.timezone)
|
||||
val timeMilli = parseObservationTimestamp(entry.dtMedicao, entry.hrMedicao, zone)
|
||||
val temp = parseDoubleSafe(entry.tempAr)
|
||||
val humidity = parseDoubleSafe(entry.umidRelAr) ?: 0.0
|
||||
val windMs = parseDoubleSafe(entry.ventVel)
|
||||
val windKmh = windMs?.let { WindSpeedUnit.MPS.convert(it, WindSpeedUnit.KPH) }
|
||||
val windDir = parseDoubleSafe(entry.ventDir)?.toInt()?.let {
|
||||
WindDirection.toWindDirectionFromDegrees(it)
|
||||
}
|
||||
val pressure = parseDoubleSafe(entry.pressAtmEst)
|
||||
|
||||
return WeatherCurrent(
|
||||
temperature = temp,
|
||||
humidity = humidity,
|
||||
windSpeed = windKmh,
|
||||
windDirection = windDir,
|
||||
pressureMsl = pressure,
|
||||
visibility = null,
|
||||
cloudCover = null,
|
||||
uvIndex = null,
|
||||
weatherCondition = WeatherCondition.NO_CONDITION_FOUND,
|
||||
feelsLike = computeApparentTemperature(temp, humidity, windMs),
|
||||
time = timeMilli,
|
||||
dewPoint = null,
|
||||
utcOffsetSeconds = null,
|
||||
lastUpdatedInMilli = System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun createCurrentFromForecast(
|
||||
days: Map<String, InmetDayJson>,
|
||||
zone: ZoneId,
|
||||
location: Location
|
||||
): WeatherCurrent {
|
||||
val todayEntry = days.entries.firstOrNull()
|
||||
val dayJson = todayEntry?.value
|
||||
val dayMillis = todayEntry?.key?.let { parseInmetDate(it, zone) } ?: System.currentTimeMillis()
|
||||
|
||||
val periods = listOfNotNull(dayJson?.manha, dayJson?.tarde, dayJson?.noite)
|
||||
val nowHour = Instant.ofEpochMilli(System.currentTimeMillis())
|
||||
.atZone(zone)
|
||||
.hour
|
||||
|
||||
val currentPeriod = when {
|
||||
periods.isEmpty() -> null
|
||||
nowHour < 12 -> periods.firstOrNull()
|
||||
nowHour < 18 -> periods.getOrNull(1) ?: periods.firstOrNull()
|
||||
else -> periods.getOrNull(2) ?: periods.firstOrNull()
|
||||
}
|
||||
|
||||
val temp = currentPeriod?.let {
|
||||
listOfNotNull(it.tempMax, it.tempMin).averageOrNull()
|
||||
} ?: dayJson?.let {
|
||||
listOfNotNull(it.tempMax, it.tempMin).averageOrNull()
|
||||
}
|
||||
|
||||
val humidity = 0.0
|
||||
val windKmh = currentPeriod?.intVento?.let { parseWindIntensityToKmh(it) }
|
||||
?: dayJson?.intVento?.let { parseWindIntensityToKmh(it) }
|
||||
val windMs = windKmh?.let { WindSpeedUnit.KPH.convert(it, WindSpeedUnit.MPS) }
|
||||
val windDir = currentPeriod?.dirVento?.let { parseWindDirection(it) }
|
||||
?: dayJson?.dirVento?.let { parseWindDirection(it) }
|
||||
|
||||
val condition = currentPeriod?.let {
|
||||
InmetWeatherConditionMap.getCondition(it.codIcone, it.resumo)
|
||||
} ?: dayJson?.let {
|
||||
InmetWeatherConditionMap.getCondition(it.codIcone, it.resumo)
|
||||
} ?: WeatherCondition.NO_CONDITION_FOUND
|
||||
|
||||
return WeatherCurrent(
|
||||
temperature = temp,
|
||||
humidity = humidity,
|
||||
windSpeed = windKmh,
|
||||
windDirection = windDir,
|
||||
pressureMsl = null,
|
||||
visibility = null,
|
||||
cloudCover = null,
|
||||
uvIndex = null,
|
||||
weatherCondition = condition,
|
||||
feelsLike = computeApparentTemperature(temp, humidity, windMs),
|
||||
time = System.currentTimeMillis(),
|
||||
dewPoint = null,
|
||||
utcOffsetSeconds = null,
|
||||
lastUpdatedInMilli = System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@JvmName("averageDoubleOrNull")
|
||||
private fun List<Double>.averageOrNull(): Double? {
|
||||
return if (isEmpty()) null else average()
|
||||
}
|
||||
|
||||
@JvmName("averageIntOrNull")
|
||||
private fun List<Int>.averageOrNull(): Double? {
|
||||
return if (isEmpty()) null else average()
|
||||
}
|
||||
+4
-1
@@ -11,6 +11,7 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.eccc.EcccR
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.fmi.FmiRepository
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.gismeteo.GismeteoRepository
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.imd.ImdRepository
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.inmet.InmetRepository
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.ipma.IpmaRepository
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteoam.MeteoamRepository
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.meteofrance.MeteoFranceRepository
|
||||
@@ -42,7 +43,8 @@ class WeatherRepositoryProvider @Inject constructor(
|
||||
private val aemetRepository: AemetRepository,
|
||||
private val imdRepository: ImdRepository,
|
||||
private val pirateWeatherRepository: PirateWeatherRepository,
|
||||
private val cwaRepository: CwaRepository
|
||||
private val cwaRepository: CwaRepository,
|
||||
private val inmetRepository: InmetRepository
|
||||
) {
|
||||
|
||||
fun getRepository(source: WeatherSource): WeatherRepository {
|
||||
@@ -66,6 +68,7 @@ class WeatherRepositoryProvider @Inject constructor(
|
||||
WeatherSource.IMD -> imdRepository
|
||||
WeatherSource.PIRATE_WEATHER -> pirateWeatherRepository
|
||||
WeatherSource.CWA -> cwaRepository
|
||||
WeatherSource.INMET -> inmetRepository
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
<string name="country_russia">Rússia</string>
|
||||
<string name="country_spain">Espanha</string>
|
||||
<string name="country_taiwan">Taiwan</string>
|
||||
<string name="country_brazil">Brasil</string>
|
||||
<string name="settings_date_format">Formato de data</string>
|
||||
<string name="widget_settings_display_hourly_secondary">Exibe a previsão por hora em vez da previsão diária</string>
|
||||
<string name="settings_widget_background">Plano de fundo do widget</string>
|
||||
|
||||
@@ -426,6 +426,7 @@
|
||||
<string name="country_russia">Russia</string>
|
||||
<string name="country_spain">Spain</string>
|
||||
<string name="country_taiwan">Taiwan</string>
|
||||
<string name="country_brazil">Brazil</string>
|
||||
|
||||
<string name="settings_date_format">Date format</string>
|
||||
<string name="widget_settings_display_hourly_secondary">Displays hourly instead of daily forecast</string>
|
||||
|
||||
Reference in new issue
Block a user