WMO Severe Weather added as alert source

This commit is contained in:
PranshulGG committed 2026-08-09 18:07:08 +05:30
1 parent 45ad8a2011
commit abfca933cd
9 files changed
+295 -1

No files matched your search

@@ -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()
}
@@ -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)
}
@@ -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/"
)
}
@@ -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<WmoSevereWeatherJson>
@GET("v2/cap-alerts/{url}")
suspend fun fetchAlertsXml(
@Path(value = "url", encoded = true) url: String
): Response<ResponseBody>
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)
}
}
}
@@ -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
)
}
@@ -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<WmoSevereWeatherFeatureJson>
)
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?
)
@@ -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?
)
@@ -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<Pair<WmoSevereWeatherFeatureJson, WmoCapAlert?>>,
locationId: String
): List<Alert> {
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
}
}
@@ -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
}
}