Compare commits

...

1 Commits

Author SHA1 Message Date
Johan von Forstner
0685f14d06 WIP: refactor LiveData to Flows 2025-08-24 16:26:35 +02:00
7 changed files with 205 additions and 248 deletions

View File

@@ -3,6 +3,11 @@ package net.vonforst.evmap.storage
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.Status
@@ -141,4 +146,44 @@ class PreferCacheLiveData(
}
}
}
}
/**
* Flow-based implementation that allows loading data both from a cache and an API.
*
* It first tries loading from cache, and if the result is newer than `cacheSoftLimit` it does not
* reload from the API.
*/
fun preferCacheFlow(
cache: Flow<ChargeLocation?>,
api: Flow<Resource<ChargeLocation>>,
cacheSoftLimit: Duration
): Flow<Resource<ChargeLocation>> = flow {
emit(Resource.loading(null)) // initial state
val cacheRes = cache.firstOrNull() // read cache once
if (cacheRes != null) {
if (cacheRes.isDetailed && cacheRes.timeRetrieved > Instant.now() - cacheSoftLimit) {
emit(Resource.success(cacheRes))
return@flow
} else {
emit(Resource.loading(cacheRes))
emitAll(api.map { apiRes ->
when (apiRes.status) {
Status.SUCCESS -> apiRes
Status.ERROR -> Resource.error(apiRes.message, cacheRes)
Status.LOADING -> Resource.loading(cacheRes)
}
})
}
} else {
// No cache → straight to API
emitAll(api.map { apiRes ->
when (apiRes.status) {
Status.SUCCESS -> apiRes
Status.ERROR -> Resource.error(apiRes.message, null)
Status.LOADING -> Resource.loading(null)
}
})
}
}

View File

@@ -14,7 +14,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.api.ChargepointApi
@@ -34,9 +41,9 @@ import net.vonforst.evmap.utils.splitAtAntimeridian
import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.await
import net.vonforst.evmap.viewmodel.singleSwitchMap
import java.time.Duration
import java.time.Instant
import kotlin.time.TimeSource
const val CLUSTER_MAX_ZOOM_LEVEL = 11f
@@ -170,10 +177,9 @@ private const val TAG = "ChargeLocationsDao"
* and clustering functionality.
*/
class ChargeLocationsRepository(
api: ChargepointApi<ReferenceData>, private val scope: CoroutineScope,
private val api: ChargepointApi<ReferenceData>, private val scope: CoroutineScope,
private val db: AppDatabase, private val prefs: PreferenceDataSource
) {
val api = MutableLiveData<ChargepointApi<ReferenceData>>().apply { value = api }
// if zoom level is below this value, server-side clustering will be used (if the API provides it)
private val serverSideClusteringThreshold = 9f
@@ -182,35 +188,33 @@ class ChargeLocationsRepository(
// if cached data is available and more recent than this duration, API will not be queried
private val cacheSoftLimit = Duration.ofDays(1)
val referenceData = this.api.switchMap { api ->
when (api) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
api,
scope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
api,
scope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenStreetMapApiWrapper -> {
OSMReferenceDataRepository(db.osmReferenceDataDao()).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
}
val referenceData = when (api) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
api,
scope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
api,
scope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenStreetMapApiWrapper -> {
OSMReferenceDataRepository(db.osmReferenceDataDao()).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}.shareIn(scope, SharingStarted.Lazily, 1)
private val chargeLocationsDao = db.chargeLocationsDao()
private val savedRegionDao = db.savedRegionDao()
@@ -221,41 +225,36 @@ class ChargeLocationsRepository(
bounds: LatLngBounds,
zoom: Float,
filters: FilterValues?,
overrideCache: Boolean = false
): LiveData<Resource<List<ChargepointListItem>>> {
overrideCache: Boolean = false,
): Flow<List<ChargepointListItem>> {
if (bounds.crossesAntimeridian()) {
val (a, b) = bounds.splitAtAntimeridian()
val liveDataA = getChargepoints(a, zoom, filters, overrideCache)
val liveDataB = getChargepoints(b, zoom, filters, overrideCache)
return combineLiveData(liveDataA, liveDataB)
val flowA = getChargepoints(a, zoom, filters, overrideCache)
val flowB = getChargepoints(b, zoom, filters, overrideCache)
return flowA.combine(flowB) { a, b -> a + b }
}
val api = api.value!!
val t1 = System.currentTimeMillis()
val dbResult = if (filters.isNullOrEmpty()) {
liveData {
emit(
Resource.success(
chargeLocationsDao.getChargeLocationsClustered(
bounds.southwest.latitude,
bounds.northeast.latitude,
bounds.southwest.longitude,
bounds.northeast.longitude,
api.id,
cacheLimitDate(api),
zoom
)
)
val dbResult = flow {
val t1 = TimeSource.Monotonic.markNow()
val result = if (filters.isNullOrEmpty()) {
chargeLocationsDao.getChargeLocationsClustered(
bounds.southwest.latitude,
bounds.northeast.latitude,
bounds.southwest.longitude,
bounds.northeast.longitude,
api.id,
cacheLimitDate(api),
zoom
)
} else {
queryWithFiltersClustered(api, filters, bounds, zoom)
}
} else {
queryWithFiltersClustered(api, filters, bounds, zoom)
}.map {
val t2 = System.currentTimeMillis()
val t2 = TimeSource.Monotonic.markNow()
Log.d(TAG, "DB loading time: ${t2 - t1}")
Log.d(TAG, "number of chargers: ${it.data?.size}")
it
Log.d(TAG, "number of chargers: ${result.size}")
emit(result)
}
val filtersSerialized =
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
?.serialize()
@@ -272,8 +271,8 @@ class ChargeLocationsRepository(
)
val useClustering = shouldUseServerSideClustering(zoom)
if (api.supportsOnlineQueries) {
val apiResult = liveData {
val refData = referenceData.await()
val apiResult = flow {
val refData = referenceData.first()
val time = Instant.now()
val result = api.getChargepoints(refData, bounds, zoom, useClustering, filters)
emit(applyLocalClustering(result, zoom))
@@ -328,39 +327,11 @@ class ChargeLocationsRepository(
}
}
private fun combineLiveData(
liveDataA: LiveData<Resource<List<ChargepointListItem>>>,
liveDataB: LiveData<Resource<List<ChargepointListItem>>>
) = MediatorLiveData<Resource<List<ChargepointListItem>>>().apply {
listOf(liveDataA, liveDataB).forEach {
addSource(it) {
val valA = liveDataA.value
val valB = liveDataB.value
val combinedList = if (valA?.data != null && valB?.data != null) {
valA.data + valB.data
} else if (valA?.data != null) {
valA.data
} else if (valB?.data != null) {
valB.data
} else null
if (valA?.status == Status.SUCCESS && valB?.status == Status.SUCCESS) {
Resource.success(combinedList)
} else if (valA?.status == Status.ERROR || valB?.status == Status.ERROR) {
Resource.error(valA?.message ?: valB?.message, combinedList)
} else {
Resource.loading(combinedList)
}
}
}
}
fun getChargepointsRadius(
location: LatLng,
radius: Int,
filters: FilterValues?
): LiveData<Resource<List<ChargeLocation>>> {
val api = api.value!!
val radiusMeters = radius.toDouble() * 1000
val dbResult = if (filters.isNullOrEmpty()) {
liveData {
@@ -394,7 +365,7 @@ class ChargeLocationsRepository(
)
if (api.supportsOnlineQueries) {
val apiResult = liveData {
val refData = referenceData.await()
val refData = referenceData.first()
val time = Instant.now()
val result =
api.getChargepointsRadius(
@@ -455,7 +426,7 @@ class ChargeLocationsRepository(
}
}
private fun applyLocalClustering(
private suspend fun applyLocalClustering(
result: Resource<ChargepointList>,
zoom: Float
): Resource<List<ChargepointListItem>> {
@@ -472,7 +443,7 @@ class ChargeLocationsRepository(
return Resource(result.status, clustered, result.message)
}
private fun applyLocalClustering(
private suspend fun applyLocalClustering(
chargers: List<ChargeLocation>,
zoom: Float
): List<ChargepointListItem> {
@@ -482,7 +453,7 @@ class ChargeLocationsRepository(
val useClustering = chargers.size > 500 || zoom <= CLUSTER_MAX_ZOOM_LEVEL
val chargersClustered = if (useClustering) {
Dispatchers.Default.run {
withContext(Dispatchers.Default) {
cluster(chargers, zoom)
}
} else chargers
@@ -492,9 +463,8 @@ class ChargeLocationsRepository(
fun getChargepointDetail(
id: Long,
overrideCache: Boolean = false
): LiveData<Resource<ChargeLocation>> {
val api = api.value!!
val dbResult = liveData {
): Flow<Resource<ChargeLocation>> {
val dbResult = flow {
emit(
chargeLocationsDao.getChargeLocationById(
id,
@@ -504,9 +474,8 @@ class ChargeLocationsRepository(
)
}
if (api.supportsOnlineQueries) {
val apiResult = liveData {
emit(Resource.loading(null))
val refData = referenceData.await()
val apiResult = flow {
val refData = referenceData.first()
val result = api.getChargepointDetail(refData, id)
emit(result)
if (result.status == Status.SUCCESS) {
@@ -516,22 +485,15 @@ class ChargeLocationsRepository(
return if (overrideCache) {
apiResult
} else {
PreferCacheLiveData(dbResult, apiResult, cacheSoftLimit)
preferCacheFlow(dbResult, apiResult, cacheSoftLimit)
}
} else {
return dbResult.map { Resource.success(it) }
}
}
fun getFilters(sp: StringProvider) = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { refData: ReferenceData? ->
refData?.let { value = api.value!!.getFilters(refData, sp) }
}
}
suspend fun getFiltersAsync(sp: StringProvider): List<Filter<FilterValue>> {
val refData = referenceData.await()
return api.value!!.getFilters(refData, sp)
fun getFilters(sp: StringProvider) = referenceData.map {
api.getFilters(it, sp)
}
val chargeCardMap by lazy {
@@ -546,29 +508,29 @@ class ChargeLocationsRepository(
}
}
private fun queryWithFilters(
private suspend fun queryWithFilters(
api: ChargepointApi<ReferenceData>,
filters: FilterValues,
bounds: LatLngBounds
): LiveData<Resource<List<ChargeLocation>>> {
): List<ChargeLocation> {
return queryWithFilters(api, filters, boundsSpatialIndexQuery(bounds))
}
private fun queryWithFiltersClustered(
private suspend fun queryWithFiltersClustered(
api: ChargepointApi<ReferenceData>,
filters: FilterValues,
bounds: LatLngBounds,
zoom: Float
): LiveData<Resource<List<ChargepointListItem>>> {
): List<ChargepointListItem> {
return queryWithFiltersClustered(api, filters, boundsSpatialIndexQuery(bounds), zoom)
}
private fun queryWithFilters(
private suspend fun queryWithFilters(
api: ChargepointApi<ReferenceData>,
filters: FilterValues,
location: LatLng,
radius: Double
): LiveData<Resource<List<ChargeLocation>>> {
): List<ChargeLocation> {
val region =
radiusSpatialIndexQuery(location, radius)
val order =
@@ -582,76 +544,50 @@ class ChargeLocationsRepository(
private fun radiusSpatialIndexQuery(location: LatLng, radius: Double) =
"PtDistWithin(coordinates, MakePoint(${location.longitude}, ${location.latitude}, 4326), ${radius}) AND ChargeLocation.ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND search_frame = BuildCircleMbr(${location.longitude}, ${location.latitude}, $radius))"
private fun queryWithFilters(
private suspend fun queryWithFilters(
api: ChargepointApi<ReferenceData>,
filters: FilterValues,
regionSql: String,
orderSql: String? = null
): LiveData<Resource<List<ChargeLocation>>> = referenceData.singleSwitchMap { refData ->
try {
val query = api.convertFiltersToSQL(filters, refData)
val after = cacheLimitDate(api)
val sql = buildFilteredQuery(query, regionSql, after, orderSql)
): List<ChargeLocation> {
val query = api.convertFiltersToSQL(filters, referenceData.first())
val after = cacheLimitDate(api)
val sql = buildFilteredQuery(query, regionSql, after, orderSql)
liveData {
emit(
Resource.success(
chargeLocationsDao.getChargeLocationsCustom(
SimpleSQLiteQuery(
sql,
null
)
)
)
)
}
} catch (e: NotImplementedError) {
MutableLiveData() // in this case we cannot get a DB result
}
return chargeLocationsDao.getChargeLocationsCustom(
SimpleSQLiteQuery(
sql,
null
)
)
}
private fun queryWithFiltersClustered(
private suspend fun queryWithFiltersClustered(
api: ChargepointApi<ReferenceData>,
filters: FilterValues,
regionSql: String,
zoom: Float,
orderSql: String? = null
): LiveData<Resource<List<ChargepointListItem>>> = referenceData.singleSwitchMap { refData ->
try {
if (zoom > CLUSTER_MAX_ZOOM_LEVEL) {
queryWithFilters(api, filters, regionSql, orderSql).map { it }
} else {
val query = api.convertFiltersToSQL(filters, refData)
val after = cacheLimitDate(api)
val clusterPrecision = getClusterPrecision(zoom)
val sql = buildFilteredQuery(query, regionSql, after, orderSql, clusterPrecision)
): List<ChargepointListItem> = if (zoom > CLUSTER_MAX_ZOOM_LEVEL) {
queryWithFilters(api, filters, regionSql, orderSql).map { it }
} else {
val query = api.convertFiltersToSQL(filters, referenceData.first())
val after = cacheLimitDate(api)
val clusterPrecision = getClusterPrecision(zoom)
val sql = buildFilteredQuery(query, regionSql, after, orderSql, clusterPrecision)
liveData {
val clusters = chargeLocationsDao.getChargeLocationClustersCustom(
SimpleSQLiteQuery(
sql,
null
)
)
val singleChargers =
chargeLocationsDao.getChargeLocationsById(clusters.filter { it.clusterCount == 1 }
.map { it.ids }
.flatten(), prefs.dataSource, after)
emit(
Resource.success(
clusters.filter { it.clusterCount > 1 }
.map { it.convert() } + singleChargers
))
}
}
} catch (e: NotImplementedError) {
MutableLiveData(
Resource.error(
e.message,
null
)
) // in this case we cannot get a DB result
}
val clusters = chargeLocationsDao.getChargeLocationClustersCustom(
SimpleSQLiteQuery(
sql,
null
)
)
val singleChargers =
chargeLocationsDao.getChargeLocationsById(clusters.filter { it.clusterCount == 1 }
.map { it.ids }
.flatten(), prefs.dataSource, after)
clusters.filter { it.clusterCount > 1 }
.map { it.convert() } + singleChargers
}
private fun buildFilteredQuery(
@@ -691,7 +627,6 @@ class ChargeLocationsRepository(
}.toString()
private suspend fun fullDownload() {
val api = api.value!!
if (!api.supportsFullDownload) return
val time = Instant.now()

View File

@@ -1,9 +1,15 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.room.*
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.goingelectric.GEChargeCard
import net.vonforst.evmap.api.goingelectric.GEReferenceData
@@ -36,7 +42,7 @@ abstract class GEReferenceDataDao {
}
@Query("SELECT * FROM genetwork")
abstract fun getAllNetworks(): LiveData<List<GENetwork>>
abstract fun getAllNetworks(): Flow<List<GENetwork>>
// PLUGS
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -54,7 +60,7 @@ abstract class GEReferenceDataDao {
}
@Query("SELECT * FROM geplug")
abstract fun getAllPlugs(): LiveData<List<GEPlug>>
abstract fun getAllPlugs(): Flow<List<GEPlug>>
// CHARGE CARDS
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -72,31 +78,21 @@ abstract class GEReferenceDataDao {
}
@Query("SELECT * FROM gechargecard")
abstract fun getAllChargeCards(): LiveData<List<GEChargeCard>>
abstract fun getAllChargeCards(): Flow<List<GEChargeCard>>
}
class GEReferenceDataRepository(
private val api: GoingElectricApiWrapper, private val scope: CoroutineScope,
private val dao: GEReferenceDataDao, private val prefs: PreferenceDataSource
) {
fun getReferenceData(): LiveData<GEReferenceData> {
fun getReferenceData(): Flow<GEReferenceData> {
scope.launch {
updateData()
}
val plugs = dao.getAllPlugs()
val networks = dao.getAllNetworks()
val chargeCards = dao.getAllChargeCards()
return MediatorLiveData<GEReferenceData>().apply {
value = null
listOf(chargeCards, networks, plugs).map { source ->
addSource(source) { _ ->
val p = plugs.value ?: return@addSource
val n = networks.value ?: return@addSource
val cc = chargeCards.value ?: return@addSource
value = GEReferenceData(p.map { it.name }, n.map { it.name }, cc)
}
}
}
return combine(plugs, networks, chargeCards) { p, n, c -> GEReferenceData(p.map { it.name }, n.map { it.name }, c) }
}
private suspend fun updateData() {

View File

@@ -1,11 +1,19 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.openchargemap.*
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
import net.vonforst.evmap.api.openchargemap.OCMCountry
import net.vonforst.evmap.api.openchargemap.OCMOperator
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.viewmodel.Status
import java.time.Duration
import java.time.Instant
@@ -28,7 +36,7 @@ abstract class OCMReferenceDataDao {
}
@Query("SELECT * FROM ocmconnectiontype")
abstract fun getAllConnectionTypes(): LiveData<List<OCMConnectionType>>
abstract fun getAllConnectionTypes(): Flow<List<OCMConnectionType>>
// COUNTRIES
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -46,7 +54,7 @@ abstract class OCMReferenceDataDao {
}
@Query("SELECT * FROM ocmcountry")
abstract fun getAllCountries(): LiveData<List<OCMCountry>>
abstract fun getAllCountries(): Flow<List<OCMCountry>>
// OPERATORS
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -64,32 +72,21 @@ abstract class OCMReferenceDataDao {
}
@Query("SELECT * FROM ocmoperator")
abstract fun getAllOperators(): LiveData<List<OCMOperator>>
abstract fun getAllOperators(): Flow<List<OCMOperator>>
}
class OCMReferenceDataRepository(
private val api: OpenChargeMapApiWrapper, private val scope: CoroutineScope,
private val dao: OCMReferenceDataDao, private val prefs: PreferenceDataSource
) {
fun getReferenceData(): LiveData<OCMReferenceData> {
fun getReferenceData(): Flow<OCMReferenceData> {
scope.launch {
updateData()
}
val connectionTypes = dao.getAllConnectionTypes()
val countries = dao.getAllCountries()
val operators = dao.getAllOperators()
return MediatorLiveData<OCMReferenceData>().apply {
value = null
listOf(countries, connectionTypes, operators).map { source ->
addSource(source) { _ ->
val ct = connectionTypes.value
val c = countries.value
val o = operators.value
if (ct.isNullOrEmpty() || c.isNullOrEmpty() || o.isNullOrEmpty()) return@addSource
value = OCMReferenceData(ct, c, o)
}
}
}
return combine(connectionTypes, countries, operators) { ct, c, o -> OCMReferenceData(ct, c, o) }
}
private suspend fun updateData() {

View File

@@ -4,6 +4,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.openchargemap.*
import net.vonforst.evmap.api.openstreetmap.OSMReferenceData
@@ -32,19 +34,13 @@ abstract class OSMReferenceDataDao {
}
@Query("SELECT * FROM osmnetwork")
abstract fun getAllNetworks(): LiveData<List<OSMNetwork>>
abstract fun getAllNetworks(): Flow<List<OSMNetwork>>
}
class OSMReferenceDataRepository(private val dao: OSMReferenceDataDao) {
fun getReferenceData(): LiveData<OSMReferenceData> {
fun getReferenceData(): Flow<OSMReferenceData> {
val networks = dao.getAllNetworks()
return MediatorLiveData<OSMReferenceData>().apply {
value = null
addSource(networks) { _ ->
val n = networks.value ?: return@addSource
value = OSMReferenceData(n.map { it.name })
}
}
return networks.map { OSMReferenceData(it.map { it.name }) }
}
suspend fun updateReferenceData(refData: OSMReferenceData) {

View File

@@ -1,8 +1,12 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import androidx.room.*
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Index
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.SkipQueryVerification
import co.anbora.labs.spatia.geometry.Geometry
import co.anbora.labs.spatia.geometry.LineString
import co.anbora.labs.spatia.geometry.Polygon
@@ -35,31 +39,31 @@ abstract class SavedRegionDao {
@SkipQueryVerification
@Query("SELECT Covers(GUnion(region), BuildMbr(:lng1, :lat1, :lng2, :lat2, 4326)) FROM savedregion WHERE dataSource == :dataSource AND timeRetrieved > :after AND Intersects(region, BuildMbr(:lng1, :lat1, :lng2, :lat2, 4326)) AND (filters == :filters OR filters IS NULL) AND (isDetailed OR NOT :isDetailed)")
protected abstract fun savedRegionCoversInt(
protected abstract suspend fun savedRegionCoversInt(
lat1: Double,
lat2: Double,
lng1: Double,
lng2: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): LiveData<Int>
): Int
@SkipQueryVerification
@Query("SELECT Covers(GUnion(region), MakeEllipse(:lng, :lat, :radiusLng, :radiusLat, 4326)) FROM savedregion WHERE dataSource == :dataSource AND timeRetrieved > :after AND Intersects(region, MakeEllipse(:lng, :lat, :radiusLng, :radiusLat, 4326)) AND (filters == :filters OR filters IS NULL) AND (isDetailed OR NOT :isDetailed)")
protected abstract fun savedRegionCoversRadiusInt(
protected abstract suspend fun savedRegionCoversRadiusInt(
lat: Double,
lng: Double,
radiusLat: Double,
radiusLng: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): LiveData<Int>
): Int
fun savedRegionCovers(
suspend fun savedRegionCovers(
lat1: Double,
lat2: Double,
lng1: Double,
lng2: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): LiveData<Boolean> {
): Boolean {
return savedRegionCoversInt(
lat1,
lat2,
@@ -69,15 +73,15 @@ abstract class SavedRegionDao {
after,
filters,
isDetailed
).map { it == 1 }
) == 1
}
fun savedRegionCoversRadius(
suspend fun savedRegionCoversRadius(
lat: Double,
lng: Double,
radius: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): LiveData<Boolean> {
): Boolean {
val (radiusLat, radiusLng) = circleAsEllipse(lat, lng, radius)
return savedRegionCoversRadiusInt(
lat,
@@ -88,7 +92,7 @@ abstract class SavedRegionDao {
after,
filters,
isDetailed
).map { it == 1 }
) == 1
}
@Insert

View File

@@ -148,20 +148,4 @@ suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
removeObserver(observer)
}
}
}
inline fun <X, Y> LiveData<X>.singleSwitchMap(crossinline transform: (X) -> LiveData<Y>?): MediatorLiveData<Y> {
val result = MediatorLiveData<Y>()
result.addSource(this@singleSwitchMap, object : Observer<X> {
override fun onChanged(t: X) {
if (t == null) return
result.removeSource(this@singleSwitchMap)
transform(t)?.let { transformed ->
result.addSource(transformed) {
result.value = it
}
}
}
})
return result
}