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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,12 @@
package net.vonforst.evmap.storage package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData import androidx.room.Dao
import androidx.lifecycle.map import androidx.room.Entity
import androidx.room.* 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.Geometry
import co.anbora.labs.spatia.geometry.LineString import co.anbora.labs.spatia.geometry.LineString
import co.anbora.labs.spatia.geometry.Polygon import co.anbora.labs.spatia.geometry.Polygon
@@ -35,31 +39,31 @@ abstract class SavedRegionDao {
@SkipQueryVerification @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)") @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, lat1: Double,
lat2: Double, lat2: Double,
lng1: Double, lng1: Double,
lng2: Double, lng2: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): LiveData<Int> ): Int
@SkipQueryVerification @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)") @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, lat: Double,
lng: Double, lng: Double,
radiusLat: Double, radiusLat: Double,
radiusLng: Double, radiusLng: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): LiveData<Int> ): Int
fun savedRegionCovers( suspend fun savedRegionCovers(
lat1: Double, lat1: Double,
lat2: Double, lat2: Double,
lng1: Double, lng1: Double,
lng2: Double, lng2: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): LiveData<Boolean> { ): Boolean {
return savedRegionCoversInt( return savedRegionCoversInt(
lat1, lat1,
lat2, lat2,
@@ -69,15 +73,15 @@ abstract class SavedRegionDao {
after, after,
filters, filters,
isDetailed isDetailed
).map { it == 1 } ) == 1
} }
fun savedRegionCoversRadius( suspend fun savedRegionCoversRadius(
lat: Double, lat: Double,
lng: Double, lng: Double,
radius: Double, radius: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): LiveData<Boolean> { ): Boolean {
val (radiusLat, radiusLng) = circleAsEllipse(lat, lng, radius) val (radiusLat, radiusLng) = circleAsEllipse(lat, lng, radius)
return savedRegionCoversRadiusInt( return savedRegionCoversRadiusInt(
lat, lat,
@@ -88,7 +92,7 @@ abstract class SavedRegionDao {
after, after,
filters, filters,
isDetailed isDetailed
).map { it == 1 } ) == 1
} }
@Insert @Insert

View File

@@ -148,20 +148,4 @@ suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
removeObserver(observer) 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
} }