mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-24 15:47:44 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0685f14d06 |
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user