diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/DwdApi.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/DwdApi.kt new file mode 100644 index 000000000..b14a0d322 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/DwdApi.kt @@ -0,0 +1,56 @@ +package com.pranshulgg.weather_master_app.core.network.sources.weather.dwd + +import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.DwdCurrentWeatherJson +import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.DwdWeatherForecastJson +import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.MetNorwayApi +import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.json.MetNorwayForecastJson +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.Query +import java.util.concurrent.TimeUnit + +interface DwdApi { + + @GET("current_weather") + suspend fun fetchCurrentWeather( + @Query("lat") latitude: Double, + @Query("lon") longitude: Double, + ): Response + + @GET("weather") + suspend fun fetchWeatherForecast( + @Query("lat") latitude: Double, + @Query("lon") longitude: Double, + @Query("date") date: String, + @Query("last_date") lastDate: String + ): Response + + companion object { + const val BASE_URL = "https://api.brightsky.dev/" + + fun create(): DwdApi { + val logging = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + + val client = OkHttpClient.Builder() + .addInterceptor(logging) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(DwdApi::class.java) + } + } +} + diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/DwdRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/DwdRepository.kt new file mode 100644 index 000000000..2fe489f1b --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/DwdRepository.kt @@ -0,0 +1,103 @@ +package com.pranshulgg.weather_master_app.core.network.sources.weather.dwd + +import com.pranshulgg.weather_master_app.core.model.domain.location.Location +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.sources.weather.dwd.json.bundle.DwdWeatherJsonBundle +import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.MetNorwayApi +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.data.local.dao.location.LocationsDao +import com.pranshulgg.weather_master_app.data.local.dao.weather.WeatherDao +import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.dwd.toDomain +import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.metnorway.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.net.UnknownHostException +import java.time.LocalDate +import java.time.ZoneId +import javax.inject.Inject + +class DwdRepository @Inject constructor( + val dao: LocationsDao, + val weatherDao: WeatherDao, + val api: DwdApi +) : WeatherRepository { + + 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) + + when (shouldReturnCache) { + WeatherResultType.REFRESH_TOO_EARLY -> return@withContext WeatherResult.RefreshNotAvailable + WeatherResultType.SUCCESS -> return@withContext WeatherResult.Success(cache!!.toDomain()) + else -> {} + } + + return@withContext try { + + + val response = api.fetchCurrentWeather(location.latitude, location.longitude) + val dates = getStartEndDate(location) + val forecastResponse = api.fetchWeatherForecast( + location.latitude, location.longitude, dates.first, dates.second + ) + + val body = response.body() + ?: return@withContext WeatherResult.Error(exception = UnknownHostException()) + val forecastBody = forecastResponse.body() + ?: return@withContext WeatherResult.Error(exception = UnknownHostException()) + + val final = DwdWeatherJsonBundle( + current = body, + forecastJson = forecastBody + ) + + val domain = final.toDomain(location) + + weatherDao.insertWeather( + domain.current.toCurrentWeatherEntity(location.id), + domain.hourly.toHourlyWeatherEntity(location.id), + 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 fun getStartEndDate(location: Location): Pair { + val zoneId = ZoneId.of(location.timezone) + + val start = LocalDate.now(zoneId) + val end = start.plusDays(5) + + return Pair(start.toString(), end.toString()) +} + diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/DwdWeatherConditionMap.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/DwdWeatherConditionMap.kt new file mode 100644 index 000000000..183fb55be --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/DwdWeatherConditionMap.kt @@ -0,0 +1,19 @@ +package com.pranshulgg.weather_master_app.core.network.sources.weather.dwd + +import com.pranshulgg.weather_master_app.core.model.weather.WeatherCondition + + +object DwdWeatherConditionMap { + fun getCondition(id: String?): WeatherCondition { + return when (id) { + "clear-day", "clear-night", "wind" -> WeatherCondition.CLEAR_SKY + "partly-cloudy-day", "partly-cloudy-night" -> WeatherCondition.PARTLY_CLOUDY + "cloudy" -> WeatherCondition.OVERCAST + "fog" -> WeatherCondition.FOG_HAZE + "rain" -> WeatherCondition.RAIN + "sleet", "snow", "hail" -> WeatherCondition.SNOW + "thunderstorm" -> WeatherCondition.THUNDERSTORM + else -> WeatherCondition.NO_CONDITION_FOUND + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/json/DwdForecastJson.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/json/DwdForecastJson.kt new file mode 100644 index 000000000..749bd891e --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/json/DwdForecastJson.kt @@ -0,0 +1,41 @@ +package com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json + +import com.google.gson.annotations.SerializedName + + +data class DwdCurrentWeatherJson( + val weather: DwdCurrentWeatherDataJson +) + + +data class DwdWeatherForecastJson( + val weather: List +) + +data class DwdWeatherForecastDataJson( + val timestamp: String, + + @SerializedName("dew_point") val dewPoint: Double?, + @SerializedName("pressure_msl") val pressureMsl: Double?, + @SerializedName("relative_humidity") val humidity: Int?, + val visibility: Int?, + @SerializedName("wind_direction") val windDirection: Int?, + @SerializedName("wind_speed") val windSpeed: Double?, + val temperature: Double, + val icon: String, + val precipitation: Double?, + @SerializedName("precipitation_probability") val precipitationProbability: Int?, +) + +data class DwdCurrentWeatherDataJson( + val timestamp: String, + + @SerializedName("dew_point") val dewPoint: Double?, + @SerializedName("pressure_msl") val pressureMsl: Double?, + @SerializedName("relative_humidity") val humidity: Int?, + val visibility: Int?, + @SerializedName("wind_direction_10") val windDirection: Int?, + @SerializedName("wind_speed_10") val windSpeed: Double?, + val temperature: Double, + val icon: String +) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/json/bundle/DwdWeatherJsonBundle.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/json/bundle/DwdWeatherJsonBundle.kt new file mode 100644 index 000000000..b57f1caec --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/weather/dwd/json/bundle/DwdWeatherJsonBundle.kt @@ -0,0 +1,9 @@ +package com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.bundle + +import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.DwdCurrentWeatherJson +import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.DwdWeatherForecastJson + +data class DwdWeatherJsonBundle( + val current: DwdCurrentWeatherJson, + val forecastJson: DwdWeatherForecastJson +) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/dwd/DwdJsonMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/dwd/DwdJsonMapper.kt new file mode 100644 index 000000000..34607b6fa --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/weather/sources/dwd/DwdJsonMapper.kt @@ -0,0 +1,180 @@ +package com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.dwd + +import android.util.Log +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.sources.WeatherSource +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.dwd.DwdWeatherConditionMap +import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.DwdWeatherForecastDataJson +import com.pranshulgg.weather_master_app.core.network.sources.weather.dwd.json.bundle.DwdWeatherJsonBundle +import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.MetNorwayWeatherConditionMap +import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.iso8601TimestampToMilliseconds +import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.normalizeToDay +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 com.pranshulgg.weather_master_app.core.utils.weather.computing.computeDailyWeatherCondition + +// ---------------------------- JSON TO DOMAIN ---------------------------- + + +fun DwdWeatherJsonBundle.toDomain(location: Location): Weather { + + val current = this.current.weather + val forecast = this.forecastJson.weather + val daily = computeDaily(forecast, location) + + return Weather( + location = location, + current = WeatherCurrent( + temperature = current.temperature, + humidity = current.humidity?.toDouble() ?: 0.0, + windSpeed = current.windSpeed, + windDirection = WindDirection.toWindDirectionFromDegrees(current.windDirection), + pressureMsl = current.pressureMsl, + visibility = current.visibility, + cloudCover = null, // NOT USED IN THE APP + uvIndex = null, + weatherCondition = DwdWeatherConditionMap.getCondition(current.icon), + feelsLike = computeApparentTemperature( + current.temperature, + current.humidity?.toDouble(), + current.windSpeed?.kmhToMs() + ), + time = current.timestamp.iso8601TimestampToMilliseconds(), + dewPoint = current.dewPoint, + utcOffsetSeconds = null, + lastUpdatedInMilli = System.currentTimeMillis() + ), + hourly = forecast.map { + WeatherHourly( + temperature = it.temperature, + windSpeed = it.windSpeed, + windDirection = WindDirection.toWindDirectionFromDegrees(it.windDirection), + rain = it.precipitation ?: 0.0, + snowfall = null, + uvIndex = null, + pressureMsl = it.pressureMsl, + visibility = it.visibility, + humidity = it.humidity?.toDouble(), + dewPoint = it.dewPoint, + weatherCondition = DwdWeatherConditionMap.getCondition(it.icon), + time = it.timestamp.iso8601TimestampToMilliseconds(), + precipitationProbability = it.precipitationProbability + ) + }, + daily = daily + ) +} + +private fun computeDaily( + data: List, + location: Location +): List { + + + val zoneId = location.timezone + + val groupedByDay = data.groupBy { + it.timestamp.iso8601TimestampToMilliseconds().normalizeToDay(zoneId) + + } + + val sunTimings = getSunTimings( + groupedByDay.map { + it.key + }, + location.timezone, + location.latitude, + location.longitude + ) + + val moonTimings = getMoonTimings( + groupedByDay.map { + it.key + }, + location.timezone, + location.latitude, + location.longitude + ) + + return groupedByDay.map { dailyIt -> + + val minTemperature = dailyIt.value.minOf { it.temperature } + val maxTemperature = dailyIt.value.maxOf { it.temperature } + val windSpeed = dailyIt.value + .mapNotNull { it.windSpeed } + .maxOrNull() + + val windDirection = + dailyIt.value.mapNotNull { it.windDirection }.maxOrNull() + + val rainSum = + dailyIt.value.sumOf { it.precipitation ?: 0.0 } + + val time = dailyIt.key + val icon = dailyIt.value.map { it.icon }.groupingBy { it } + .eachCount().entries.maxByOrNull { it.value } + + Log.d("DWD ICON", "${icon}") + + val condition = computeDailyWeatherCondition( + getHourlyConditionsForDay(dailyIt.value, time), + MetNorwayWeatherConditionMap.getCondition(icon?.key) + ) + + val precipitationProbabilityMax = dailyIt.value.mapNotNull { it.precipitationProbability } + .maxOrNull() + + + val index = groupedByDay.keys.indexOf(dailyIt.key) + + WeatherDaily( + temperatureMin = minTemperature, + temperatureMax = maxTemperature, + windSpeed = windSpeed, + windDirection = WindDirection.toWindDirectionFromDegrees(windDirection), + rainSum = rainSum, + snowfallSum = null, + uvIndexMax = null, + weatherCondition = condition, + time = time, + precipitationProbabilityMax = precipitationProbabilityMax, + sunrise = sunTimings[index].sunrise ?: -0L, + sunset = sunTimings[index].sunset ?: -0L, + moonrise = moonTimings[index].moonrise ?: -0L, + moonset = moonTimings[index].moonset ?: -0L, + moonPhase = moonTimings[index].phase, + dawn = sunTimings[index].dawn ?: 0L, + dusk = sunTimings[index].dusk ?: 0L + ) + } +} + +private fun getHourlyConditionsForDay( + data: List, + time: Long +): List { + val startIndex = + data.indexOfFirst { it.timestamp.iso8601TimestampToMilliseconds() >= time } + .takeIf { it != -1 } ?: 0 + + + val conditions = data.drop(maxOf(0, startIndex - 1)) + .take(WeatherSource.DWD.hourlyAggregationLimitHours) + .map { + DwdWeatherConditionMap.getCondition(it.icon) + } + + return conditions +} + + +private fun Double.kmhToMs(): Double { + return (this / 3.6) +} \ No newline at end of file