From d01371f6e9a34cb1ced802695cc19d8a9b2d2426 Mon Sep 17 00:00:00 2001 From: Johan von Forstner Date: Thu, 4 Jun 2020 17:32:20 +0200 Subject: [PATCH] add filters by network and charge card --- .../api/goingelectric/GoingElectricApi.kt | 6 +- .../api/goingelectric/GoingElectricModel.kt | 16 +++- .../vonforst/evmap/storage/ChargeCardDao.kt | 47 ++++++++++ .../net/vonforst/evmap/storage/Database.kt | 21 ++++- .../net/vonforst/evmap/storage/NetworkDao.kt | 49 ++++++++++ .../evmap/storage/PreferenceDataSource.kt | 12 +++ .../evmap/viewmodel/FilterViewModel.kt | 91 ++++++++++++------- .../vonforst/evmap/viewmodel/MapViewModel.kt | 70 +++++++++----- app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 10 files changed, 257 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/net/vonforst/evmap/storage/ChargeCardDao.kt create mode 100644 app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt diff --git a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt index 5d241a19..8d5a71fe 100644 --- a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt @@ -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 @GET("chargepoints/") @@ -36,7 +38,7 @@ interface GoingElectricApi { suspend fun getNetworks(): Response @GET("chargepoints/chargecardlist/") - suspend fun getChargeCards(): Response + suspend fun getChargeCards(): Response companion object { private val cacheSize = 10L * 1024 * 1024; // 10MB diff --git a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt index a68225c0..8da48d8a 100644 --- a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt @@ -33,6 +33,12 @@ data class StringList( val result: List ) +@JsonClass(generateAdapter = true) +data class ChargeCardList( + val status: String, + val result: List +) + 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?) \ No newline at end of file +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 +) \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/ChargeCardDao.kt b/app/src/main/java/net/vonforst/evmap/storage/ChargeCardDao.kt new file mode 100644 index 00000000..269416a9 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeCardDao.kt @@ -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> +} + +class ChargeCardRepository( + private val api: GoingElectricApi, private val scope: CoroutineScope, + private val dao: ChargeCardDao, private val prefs: PreferenceDataSource +) { + fun getChargeCards(): LiveData> { + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/Database.kt b/app/src/main/java/net/vonforst/evmap/storage/Database.kt index b7a10391..cf23579b 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/Database.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/Database.kt @@ -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`))") + } + } } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt b/app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt new file mode 100644 index 00000000..981c3c3d --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt @@ -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> +} + +class NetworkRepository( + private val api: GoingElectricApi, private val scope: CoroutineScope, + private val dao: NetworkDao, private val prefs: PreferenceDataSource +) { + fun getNetworks(): LiveData> { + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt b/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt index 2e2493dd..b1d399a2 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt index ba96e9ef..6838dcd2 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt @@ -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> + plugs: LiveData>, + networks: LiveData>, + chargeCards: LiveData> ): LiveData>> { return MediatorLiveData>>().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>>.buildFilters( + plugs: LiveData>, + plugNames: Map, + networks: LiveData>, + chargeCards: LiveData>, + 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>>, @@ -107,9 +129,14 @@ class FilterViewModel(application: Application, geApiKey: String) : private val plugs: LiveData> by lazy { PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs() } - + private val networks: LiveData> by lazy { + NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks() + } + private val chargeCards: LiveData> by lazy { + ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards() + } private val filters: LiveData>> by lazy { - getFilters(application, plugs) + getFilters(application, plugs, networks, chargeCards) } private val filterValues: LiveData> by lazy { diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt index 3d443132..48ac3fdf 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt @@ -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> by lazy { PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs() } - private val filters = getFilters(application, plugs) + private val networks: LiveData> by lazy { + NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks() + } + private val chargeCards: LiveData> by lazy { + ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards() + } + private val filters = getFilters(application, plugs, networks, chargeCards) private val filtersWithValue: LiveData>> by lazy { filtersWithValue(filters, filterValues, filtersActive) @@ -191,22 +191,31 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode zoom: Float, filters: List> ): Resource> { - 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>, + key: String + ) = (filters.find { it.value.key == key }!!.value as BooleanFilterValue).value + + private fun getSliderValue( + filters: List>, + key: String + ) = (filters.find { it.value.key == key }!!.value as SliderFilterValue).value + + private fun getMultipleChoiceValue( + filters: List>, + 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) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3acae060..928874a5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -81,4 +81,6 @@ Preisvergleich Störungsmeldung Störungsmeldung (Letztes Update: %s) + Verbünde + Ladetarife \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 56295ae9..db4a90ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,4 +80,6 @@ Compare prices Fault report Fault report (last update: %s) + Networks + Payment methods