From abfca933cdfd3829da934b9a6809d7e384c3e26e Mon Sep 17 00:00:00 2001 From: PranshulGG Date: Sun, 9 Aug 2026 18:07:08 +0530 Subject: [PATCH] WMO Severe Weather added as alert source --- .../core/di/network/ApiModule.kt | 5 + .../di/weather/WeatherRepositoryModule.kt | 9 ++ .../core/model/sources/AlertSource.kt | 5 + .../wmosevereweather/WmoSevereWeatherApi.kt | 51 +++++++ .../WmoSevereWeatherRepository.kt | 137 ++++++++++++++++++ .../json/WmoSevereWeatherJson.kt | 23 +++ .../wmosevereweather/model/WmoCapAlert.kt | 13 ++ .../WmoSevereWeatherAlertsMapper.kt | 48 ++++++ .../data/provider/AlertsRepositoryProvider.kt | 5 +- 9 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/WmoSevereWeatherApi.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/WmoSevereWeatherRepository.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/json/WmoSevereWeatherJson.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/model/WmoCapAlert.kt create mode 100644 app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/wmosevereweather/WmoSevereWeatherAlertsMapper.kt diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt index cf401719e..11680ac90 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/network/ApiModule.kt @@ -6,6 +6,7 @@ import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.Ac import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiApi import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuApi import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApi +import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.WmoSevereWeatherApi import com.pranshulgg.weather_master_app.core.network.sources.search.accu.AccuSearchApi import com.pranshulgg.weather_master_app.core.network.sources.search.geonames.GeoNamesSearchApi import com.pranshulgg.weather_master_app.core.network.sources.search.geonames.timezone.GeoNamesTimezoneApi @@ -124,4 +125,8 @@ object ApiModule { @Provides @Singleton fun provideAccuSearchApi(): AccuSearchApi = AccuSearchApi.create() + + @Provides + @Singleton + fun provideWmoSevereWeatherAlertsApi(): WmoSevereWeatherApi = WmoSevereWeatherApi.create() } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt index 33a82fbb0..989d383c5 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/di/weather/WeatherRepositoryModule.kt @@ -8,6 +8,8 @@ import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.Alerts import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuRepository import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApi import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApiRepository +import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.WmoSevereWeatherApi +import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.WmoSevereWeatherRepository import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuApi import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuRepository import com.pranshulgg.weather_master_app.core.network.sources.weather.bmkg.BmkgApi @@ -189,4 +191,11 @@ object WeatherRepositoryModule { locationKeysDao: LocationKeysDao ): IpmaRepository = IpmaRepository(dao, weatherDao, api, locationKeysDao) + @Provides + @Singleton + fun provideWmoSevereWeatherAlertsRepository( + api: WmoSevereWeatherApi, + dao: AlertsDao + ): WmoSevereWeatherRepository = WmoSevereWeatherRepository(api, dao) + } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/sources/AlertSource.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/sources/AlertSource.kt index 53f7c6c0f..d4f8907f5 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/core/model/sources/AlertSource.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/model/sources/AlertSource.kt @@ -18,5 +18,10 @@ enum class AlertSource( fullName = "WeatherApi.com", displayLink = "https://www.weatherapi.com/" ), + WMO_SEVERE_WEATHER( + displayName = "WMO Severe Weather", + fullName = "WMO Severe Weather Information Centre", + displayLink = "https://www.wmo.int/" + ) } \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/WmoSevereWeatherApi.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/WmoSevereWeatherApi.kt new file mode 100644 index 000000000..051238db3 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/WmoSevereWeatherApi.kt @@ -0,0 +1,51 @@ +package com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather + +import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.json.WmoSevereWeatherJson +import okhttp3.OkHttpClient +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query +import java.util.concurrent.TimeUnit + + +interface WmoSevereWeatherApi { + + @GET("f/wfs") + suspend fun fetchAlerts( + @Query("typeName") typeName: String = "local_postgis:postgis_geojsons", + @Query("cql_filter") cqlFilter: String, + @Query("version") version: String = "1.1.0", + @Query("outputFormat") outputFormat: String = "json", + @Query("request") request: String = "GetFeature", + ): Response + + @GET("v2/cap-alerts/{url}") + suspend fun fetchAlertsXml( + @Path(value = "url", encoded = true) url: String + ): Response + + + companion object { + const val BASE_URL = "https://severeweather.wmo.int/" + + fun create(): WmoSevereWeatherApi { + + + 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(WmoSevereWeatherApi::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/WmoSevereWeatherRepository.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/WmoSevereWeatherRepository.kt new file mode 100644 index 000000000..20839ba04 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/WmoSevereWeatherRepository.kt @@ -0,0 +1,137 @@ +package com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather + +import android.util.Xml +import com.pranshulgg.weather_master_app.core.model.domain.AppException +import com.pranshulgg.weather_master_app.core.model.domain.location.Location +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResult +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResultType +import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.model.WmoCapAlert +import com.pranshulgg.weather_master_app.core.utils.weather.cache.shouldReturnAlertsCache +import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.sources.wmosevereweather.wmoSevereWeatherAlertsMapper +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toDomain +import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toEntity +import com.pranshulgg.weather_master_app.data.repository.AlertRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.xmlpull.v1.XmlPullParser +import java.io.InputStream +import javax.inject.Inject + +class WmoSevereWeatherRepository @Inject constructor( + private val api: WmoSevereWeatherApi, + private val dao: AlertsDao +) : AlertRepository { + override suspend fun getAlerts( + location: Location, + isManualRefresh: Boolean, + isForceRefresh: Boolean + ): AlertResult = withContext(Dispatchers.IO) { + + val cache = dao.getAlertsForLocation(location.id) + val shouldReturnCache = shouldReturnAlertsCache(cache, isManualRefresh, isForceRefresh) + + when (shouldReturnCache) { + AlertResultType.RETURN_CACHE -> return@withContext AlertResult.Success(cache.map { it!!.toDomain() }) + else -> {} + } + + val cqlFilter = + "INTERSECTS(wkb_geometry, POINT (${location.latitude} ${location.longitude})) AND row_type NEQ 'BOUNDARY'" + + return@withContext try { + + val response = api.fetchAlerts(cqlFilter = cqlFilter) + + + val body = response.body() + ?: return@withContext AlertResult.Error(exception = AppException.Unknown()) + + + val alertsWithCap = body.features.filter { + it.properties != null + }.map { feature -> + val url = feature.properties?.capUrl ?: feature.properties?.rLink + + val capAlert = if (!url.isNullOrBlank()) { + val responseAlert = api.fetchAlertsXml(url) + + if (responseAlert.isSuccessful) { + responseAlert.body()?.byteStream()?.use { stream -> + parseAlertXmlBody(stream) + } + } else { + null + } + } else { + null + } + + feature to capAlert + } + + + val domain = wmoSevereWeatherAlertsMapper(alertsWithCap, location.id) + + dao.insertAlerts( + domain.map { it.toEntity(location.id) }, + location.id + ) + + AlertResult.Success(domain) + + } catch (e: Exception) { + AlertResult.Error(exception = e, cacheAlerts = cache.map { it!!.toDomain() }) + } + + + } +} + +private fun parseAlertXmlBody(stream: InputStream): WmoCapAlert { + val parser = Xml.newPullParser() + parser.setInput(stream, null) + + + var type = parser.eventType + + var language: String? = null + var event: String? = null + var severity: String? = null + var effective: String? = null + var expires: String? = null + var senderName: String? = null + var description: String? = null + var headline: String? = null + + while (type != XmlPullParser.END_DOCUMENT) { + + when (type) { + XmlPullParser.START_TAG -> { + when (parser.name) { + "language" -> language = parser.nextText() + "event" -> event = parser.nextText() + "severity" -> severity = parser.nextText() + "effective" -> effective = parser.nextText() + "expires" -> expires = parser.nextText() + "senderName" -> senderName = parser.nextText() + "description" -> description = parser.nextText() + "headline" -> headline = parser.nextText() + } + } + } + type = parser.next() + } + return WmoCapAlert( + language = language, + event = event, + severity = severity, + effective = effective, + expires = expires, + senderName = senderName, + headline = headline, + description = description + ) + + +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/json/WmoSevereWeatherJson.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/json/WmoSevereWeatherJson.kt new file mode 100644 index 000000000..b0f0b4483 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/json/WmoSevereWeatherJson.kt @@ -0,0 +1,23 @@ +package com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.json + +import com.google.gson.annotations.SerializedName + +data class WmoSevereWeatherJson( + val features: List +) + +data class WmoSevereWeatherFeatureJson( + val properties: WmoSevereWeatherFeaturePropertiesJson? +) + + +data class WmoSevereWeatherFeaturePropertiesJson( + @SerializedName("capurl") val capUrl: String?, + @SerializedName("rlink") val rLink: String?, + val description: String?, + val event: String?, + val effective: String?, + val expires: String?, + @SerializedName("s") val severity: Int?, + val headline: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/model/WmoCapAlert.kt b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/model/WmoCapAlert.kt new file mode 100644 index 000000000..ab876dc3d --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/core/network/sources/alerts/wmosevereweather/model/WmoCapAlert.kt @@ -0,0 +1,13 @@ +package com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.model + +data class WmoCapAlert( + val language: String?, + val event: String?, + val severity: String?, + val effective: String?, + val expires: String?, + val senderName: String?, + val headline: String?, + val description: String? +) + diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/wmosevereweather/WmoSevereWeatherAlertsMapper.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/wmosevereweather/WmoSevereWeatherAlertsMapper.kt new file mode 100644 index 000000000..8b42bc580 --- /dev/null +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/local/mapper/alerts/sources/wmosevereweather/WmoSevereWeatherAlertsMapper.kt @@ -0,0 +1,48 @@ +package com.pranshulgg.weather_master_app.data.local.mapper.alerts.sources.wmosevereweather + +import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert +import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity +import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.json.WmoSevereWeatherFeatureJson +import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.model.WmoCapAlert +import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.iso8601TimestampToMilliseconds + + +fun wmoSevereWeatherAlertsMapper( + alerts: List>, + locationId: String +): List { + + + return alerts.filter { + !it.first.properties?.event.isNullOrBlank() + || !it.first.properties?.headline.isNullOrBlank() + || !it.second?.event.isNullOrBlank() + || !it.second?.headline.isNullOrBlank() + }.map { (feature, capAlert) -> + + + Alert( + locationId = locationId, + event = capAlert?.event ?: capAlert?.headline ?: feature.properties?.event + ?: feature.properties?.headline ?: "", + severity = getSeverity(feature.properties?.severity), + effective = capAlert?.effective?.iso8601TimestampToMilliseconds() + ?: feature.properties?.effective?.iso8601TimestampToMilliseconds(), + expires = capAlert?.expires?.iso8601TimestampToMilliseconds() + ?: feature.properties?.expires?.iso8601TimestampToMilliseconds(), + description = capAlert?.description ?: feature.properties?.description ?: "", + lastUpdatedInMilli = System.currentTimeMillis(), + source = capAlert?.senderName ?: "WMO Severe Weather" + ) + } +} + +private fun getSeverity(code: Int?): AlertSeverity { + return when (code) { + 4 -> AlertSeverity.CRITICAL + 3 -> AlertSeverity.HIGH + 2 -> AlertSeverity.MODERATE + 1 -> AlertSeverity.LOW + else -> AlertSeverity.UNKNOWN + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt b/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt index 59a657fda..a1ba515fa 100644 --- a/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt +++ b/app/src/main/java/com/pranshulgg/weather_master_app/data/provider/AlertsRepositoryProvider.kt @@ -3,19 +3,22 @@ package com.pranshulgg.weather_master_app.data.provider import com.pranshulgg.weather_master_app.core.model.sources.AlertSource import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuRepository import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApiRepository +import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.WmoSevereWeatherRepository import com.pranshulgg.weather_master_app.data.repository.AlertRepository import javax.inject.Inject class AlertsRepositoryProvider @Inject constructor( private val alertsAccuRepository: AlertsAccuRepository, - private val alertsWeatherApiRepository: AlertsWeatherApiRepository + private val alertsWeatherApiRepository: AlertsWeatherApiRepository, + private val wmoSevereWeatherRepository: WmoSevereWeatherRepository ) { fun getRepository(source: AlertSource): AlertRepository? { return when (source) { AlertSource.ACCU_WEATHER -> alertsAccuRepository AlertSource.WEATHER_API -> alertsWeatherApiRepository + AlertSource.WMO_SEVERE_WEATHER -> wmoSevereWeatherRepository AlertSource.NONE -> null } }