add filters by network and charge card

This commit is contained in:
Johan von Forstner
2020-06-04 17:32:20 +02:00
parent 6130b190e1
commit d01371f6e9
10 changed files with 257 additions and 59 deletions

View File

@@ -23,7 +23,9 @@ interface GoingElectricApi {
@Query("freecharging") freecharging: Boolean,
@Query("freeparking") freeparking: Boolean,
@Query("min_power") minPower: Int,
@Query("plugs") plugs: String?
@Query("plugs") plugs: String?,
@Query("chargecards") chargecards: String?,
@Query("networks") networks: String?
): Response<ChargepointList>
@GET("chargepoints/")
@@ -36,7 +38,7 @@ interface GoingElectricApi {
suspend fun getNetworks(): Response<StringList>
@GET("chargepoints/chargecardlist/")
suspend fun getChargeCards(): Response<StringList>
suspend fun getChargeCards(): Response<ChargeCardList>
companion object {
private val cacheSize = 10L * 1024 * 1024; // 10MB

View File

@@ -33,6 +33,12 @@ data class StringList(
val result: List<String>
)
@JsonClass(generateAdapter = true)
data class ChargeCardList(
val status: String,
val result: List<ChargeCard>
)
sealed class ChargepointListItem
@JsonClass(generateAdapter = true)
@@ -263,4 +269,12 @@ data class Chargepoint(val type: String, val power: Double, val count: Int) : Eq
}
@JsonClass(generateAdapter = true)
data class FaultReport(val created: Instant?, val description: String?)
data class FaultReport(val created: Instant?, val description: String?)
@Entity
@JsonClass(generateAdapter = true)
data class ChargeCard(
@Json(name = "card_id") @PrimaryKey val id: Long,
val name: String,
val url: String
)

View File

@@ -0,0 +1,47 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import java.time.Duration
import java.time.Instant
@Dao
interface ChargeCardDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg chargeCards: ChargeCard)
@Delete
suspend fun delete(vararg chargeCards: ChargeCard)
@Query("SELECT * FROM chargeCard")
fun getAllChargeCards(): LiveData<List<ChargeCard>>
}
class ChargeCardRepository(
private val api: GoingElectricApi, private val scope: CoroutineScope,
private val dao: ChargeCardDao, private val prefs: PreferenceDataSource
) {
fun getChargeCards(): LiveData<List<ChargeCard>> {
scope.launch {
updateChargeCards()
}
return dao.getAllChargeCards()
}
private suspend fun updateChargeCards() {
if (Duration.between(prefs.lastChargeCardUpdate, Instant.now()) < Duration.ofDays(1)) return
val response = api.getChargeCards()
if (!response.isSuccessful) return
for (card in response.body()!!.result) {
dao.insert(card)
}
prefs.lastChargeCardUpdate = Instant.now()
}
}

View File

@@ -7,6 +7,7 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.viewmodel.BooleanFilterValue
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
@@ -18,20 +19,27 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
BooleanFilterValue::class,
MultipleChoiceFilterValue::class,
SliderFilterValue::class,
Plug::class
], version = 6
Plug::class,
Network::class,
ChargeCard::class
], version = 7
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun chargeLocationsDao(): ChargeLocationsDao
abstract fun filterValueDao(): FilterValueDao
abstract fun plugDao(): PlugDao
abstract fun networkDao(): NetworkDao
abstract fun chargeCardDao(): ChargeCardDao
companion object {
private lateinit var context: Context
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db")
.addMigrations(MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6)
.addMigrations(
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
MIGRATION_7
)
.build()
}
@@ -99,5 +107,12 @@ abstract class AppDatabase : RoomDatabase() {
}
}
}
private val MIGRATION_7 = object : Migration(6, 7) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `Network` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))")
db.execSQL("CREATE TABLE IF NOT EXISTS `ChargeCard` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
}
}
}
}

View File

@@ -0,0 +1,49 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import java.time.Duration
import java.time.Instant
@Entity
data class Network(@PrimaryKey val name: String)
@Dao
interface NetworkDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg networks: Network)
@Delete
suspend fun delete(vararg networks: Network)
@Query("SELECT * FROM network")
fun getAllNetworks(): LiveData<List<Network>>
}
class NetworkRepository(
private val api: GoingElectricApi, private val scope: CoroutineScope,
private val dao: NetworkDao, private val prefs: PreferenceDataSource
) {
fun getNetworks(): LiveData<List<Network>> {
scope.launch {
updateNetworks()
}
return dao.getAllNetworks()
}
private suspend fun updateNetworks() {
if (Duration.between(prefs.lastNetworkUpdate, Instant.now()) < Duration.ofDays(1)) return
val response = api.getNetworks()
if (!response.isSuccessful) return
for (name in response.body()!!.result) {
dao.insert(Network(name))
}
prefs.lastNetworkUpdate = Instant.now()
}
}

View File

@@ -18,4 +18,16 @@ class PreferenceDataSource(context: Context) {
set(value) {
sp.edit().putLong("last_plug_update", value.toEpochMilli()).apply()
}
var lastNetworkUpdate: Instant
get() = Instant.ofEpochMilli(sp.getLong("last_network_update", 0L))
set(value) {
sp.edit().putLong("last_network_update", value.toEpochMilli()).apply()
}
var lastChargeCardUpdate: Instant
get() = Instant.ofEpochMilli(sp.getLong("last_chargecard_update", 0L))
set(value) {
sp.edit().putLong("last_chargecard_update", value.toEpochMilli()).apply()
}
}

View File

@@ -10,12 +10,10 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.Plug
import net.vonforst.evmap.storage.PlugRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.storage.*
import kotlin.math.abs
import kotlin.reflect.KClass
import kotlin.reflect.full.cast
@@ -28,7 +26,9 @@ internal fun mapPowerInverse(power: Int) = powerSteps
internal fun getFilters(
application: Application,
plugs: LiveData<List<Plug>>
plugs: LiveData<List<Plug>>,
networks: LiveData<List<Network>>,
chargeCards: LiveData<List<ChargeCard>>
): LiveData<List<Filter<FilterValue>>> {
return MediatorLiveData<List<Filter<FilterValue>>>().apply {
val plugNames = mapOf(
@@ -42,35 +42,57 @@ internal fun getFilters(
Chargepoint.CEE_BLAU to application.getString(R.string.plug_cee_blau),
Chargepoint.CEE_ROT to application.getString(R.string.plug_cee_rot)
)
addSource(plugs) { plugs ->
val plugMap = plugs.map { plug ->
plug.name to (plugNames[plug.name] ?: plug.name)
}.toMap()
value = listOf(
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
SliderFilter(
application.getString(R.string.filter_min_power), "min_power",
powerSteps.size - 1,
mapping = ::mapPower,
inverseMapping = ::mapPowerInverse,
unit = "kW"
),
MultipleChoiceFilter(
application.getString(R.string.filter_connectors), "connectors",
plugMap,
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO)
),
SliderFilter(
application.getString(R.string.filter_min_connectors),
"min_connectors",
10
)
)
listOf(plugs, networks, chargeCards).forEach { source ->
addSource(source) { _ ->
buildFilters(plugs, plugNames, networks, chargeCards, application)
}
}
}
}
private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
plugs: LiveData<List<Plug>>,
plugNames: Map<String, String>,
networks: LiveData<List<Network>>,
chargeCards: LiveData<List<ChargeCard>>,
application: Application
) {
val plugMap = plugs.value?.map { plug ->
plug.name to (plugNames[plug.name] ?: plug.name)
}?.toMap() ?: return
val networkMap = networks.value?.map { it.name to it.name }?.toMap() ?: return
val chargecardMap = chargeCards.value?.map { it.name to it.name }?.toMap() ?: return
value = listOf(
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
SliderFilter(
application.getString(R.string.filter_min_power), "min_power",
powerSteps.size - 1,
mapping = ::mapPower,
inverseMapping = ::mapPowerInverse,
unit = "kW"
),
MultipleChoiceFilter(
application.getString(R.string.filter_connectors), "connectors",
plugMap,
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO)
),
SliderFilter(
application.getString(R.string.filter_min_connectors),
"min_connectors",
10
),
MultipleChoiceFilter(
application.getString(R.string.filter_networks), "networks",
networkMap
),
MultipleChoiceFilter(
application.getString(R.string.filter_chargecards), "chargecards",
chargecardMap
)
)
}
internal fun filtersWithValue(
filters: LiveData<List<Filter<FilterValue>>>,
@@ -107,9 +129,14 @@ class FilterViewModel(application: Application, geApiKey: String) :
private val plugs: LiveData<List<Plug>> by lazy {
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
}
private val networks: LiveData<List<Network>> by lazy {
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
}
private val chargeCards: LiveData<List<ChargeCard>> by lazy {
ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards()
}
private val filters: LiveData<List<Filter<FilterValue>>> by lazy {
getFilters(application, plugs)
getFilters(application, plugs, networks, chargeCards)
}
private val filterValues: LiveData<List<FilterValue>> by lazy {

View File

@@ -9,14 +9,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.ChargepointList
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.Plug
import net.vonforst.evmap.storage.PlugRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.api.goingelectric.*
import net.vonforst.evmap.storage.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@@ -52,7 +46,13 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
private val plugs: LiveData<List<Plug>> by lazy {
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
}
private val filters = getFilters(application, plugs)
private val networks: LiveData<List<Network>> by lazy {
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
}
private val chargeCards: LiveData<List<ChargeCard>> by lazy {
ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards()
}
private val filters = getFilters(application, plugs, networks, chargeCards)
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
filtersWithValue(filters, filterValues, filtersActive)
@@ -191,22 +191,31 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
zoom: Float,
filters: List<FilterWithValue<out FilterValue>>
): Resource<List<ChargepointListItem>> {
val freecharging =
(filters.find { it.value.key == "freecharging" }!!.value as BooleanFilterValue).value
val freeparking =
(filters.find { it.value.key == "freeparking" }!!.value as BooleanFilterValue).value
val minPower =
(filters.find { it.value.key == "min_power" }!!.value as SliderFilterValue).value
val minConnectors =
(filters.find { it.value.key == "min_connectors" }!!.value as SliderFilterValue).value
val freecharging = getBooleanValue(filters, "freecharging")
val freeparking = getBooleanValue(filters, "freeparking")
val minPower = getSliderValue(filters, "min_power")
val minConnectors = getSliderValue(filters, "min_connectors")
val connectorsVal =
filters.find { it.value.key == "connectors" }!!.value as MultipleChoiceFilterValue
val connectors = if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
val connectorsVal = getMultipleChoiceValue(filters, "connectors")
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
}
val connectors = formatMultipleChoice(connectorsVal)
val chargeCardsVal = getMultipleChoiceValue(filters, "chargecards")
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
// no chargeCards chosen
return Resource.success(emptyList())
}
val chargeCards = formatMultipleChoice(chargeCardsVal)
val networksVal = getMultipleChoiceValue(filters, "networks")
if (networksVal.values.isEmpty() && !networksVal.all) {
// no networks chosen
return Resource.success(emptyList())
}
val networks = formatMultipleChoice(networksVal)
// do not use clustering if filters need to be applied locally.
val useClustering = minConnectors <= 1 && zoom < 13
@@ -217,7 +226,8 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
bounds.northeast.latitude, bounds.northeast.longitude,
clustering = useClustering, zoom = zoom,
clusterDistance = clusterDistance, freecharging = freecharging, minPower = minPower,
freeparking = freeparking, plugs = connectors
freeparking = freeparking, plugs = connectors, chargecards = chargeCards,
networks = networks
)
if (!response.isSuccessful || response.body()!!.status != "ok") {
@@ -239,6 +249,24 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =
if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
private fun getBooleanValue(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = (filters.find { it.value.key == key }!!.value as BooleanFilterValue).value
private fun getSliderValue(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = (filters.find { it.value.key == key }!!.value as SliderFilterValue).value
private fun getMultipleChoiceValue(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = filters.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
private suspend fun loadAvailability(charger: ChargeLocation) {
availability.value = Resource.loading(null)
availability.value = getAvailability(charger)