mirror of
https://github.com/ev-map/EVMap.git
synced 2026-05-19 12:26:20 -04:00
add filters by network and charge card
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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`))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt
Normal file
49
app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -81,4 +81,6 @@
|
||||
<string name="go_to_chargeprice">Preisvergleich</string>
|
||||
<string name="fault_report">Störungsmeldung</string>
|
||||
<string name="fault_report_date">Störungsmeldung (Letztes Update: %s)</string>
|
||||
<string name="filter_networks">Verbünde</string>
|
||||
<string name="filter_chargecards">Ladetarife</string>
|
||||
</resources>
|
||||
@@ -80,4 +80,6 @@
|
||||
<string name="go_to_chargeprice">Compare prices</string>
|
||||
<string name="fault_report">Fault report</string>
|
||||
<string name="fault_report_date">Fault report (last update: %s)</string>
|
||||
<string name="filter_networks">Networks</string>
|
||||
<string name="filter_chargecards">Payment methods</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user