working implementation for first filter (free charging) #9

This commit is contained in:
Johan von Forstner
2020-04-28 19:38:10 +02:00
parent 810338ba38
commit 5c72ee718b
12 changed files with 232 additions and 32 deletions

View File

@@ -13,10 +13,7 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.viewmodel.BooleanFilter
import net.vonforst.evmap.viewmodel.FavoritesViewModel
import net.vonforst.evmap.viewmodel.Filter
import net.vonforst.evmap.viewmodel.MultipleChoiceFilter
import net.vonforst.evmap.viewmodel.*
interface Equatable {
override fun equals(other: Any?): Boolean;
@@ -137,7 +134,7 @@ class FavoritesAdapter(val vm: FavoritesViewModel) :
override fun getItemId(position: Int): Long = getItem(position).charger.id
}
class FiltersAdapter : DataBindingAdapter<Filter>() {
class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
init {
setHasStableIds(true)
}
@@ -145,13 +142,13 @@ class FiltersAdapter : DataBindingAdapter<Filter>() {
val itemids = mutableMapOf<String, Long>()
var maxId = 0L
override fun getItemViewType(position: Int): Int = when (getItem(position)) {
override fun getItemViewType(position: Int): Int = when (getItem(position).filter) {
is BooleanFilter -> R.layout.item_filter_boolean
is MultipleChoiceFilter -> R.layout.item_filter_boolean
}
override fun getItemId(position: Int): Long {
val key = getItem(position).key
val key = getItem(position).filter.key
var value = itemids[key]
if (value == null) {
maxId++

View File

@@ -16,7 +16,8 @@ interface GoingElectricApi {
@Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double,
@Query("clustering") clustering: Boolean,
@Query("zoom") zoom: Float,
@Query("cluster_distance") clusterDistance: Int
@Query("cluster_distance") clusterDistance: Int,
@Query("freecharging") freecharging: Boolean
): Call<ChargepointList>
@GET("chargepoints/")

View File

@@ -1,19 +1,19 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.launch
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.FiltersAdapter
@@ -43,6 +43,7 @@ class FilterFragment : Fragment() {
binding.lifecycleOwner = this
binding.vm = vm
setHasOptionsMenu(true)
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object :
OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
@@ -55,6 +56,7 @@ class FilterFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
val navController = findNavController()
toolbar.setupWithNavController(
@@ -74,11 +76,23 @@ class FilterFragment : Fragment() {
}
view.startCircularReveal()
toolbar.setNavigationOnClickListener {
exitAfterTransition()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.filter, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
R.id.menu_apply -> {
lifecycleScope.launch {
vm.saveFilterValues()
}
exitAfterTransition()
true
}

View File

@@ -5,22 +5,42 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.viewmodel.BooleanFilterValue
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
@Database(entities = [ChargeLocation::class], version = 1)
@Database(
entities = [
ChargeLocation::class,
BooleanFilterValue::class,
MultipleChoiceFilterValue::class
], version = 2
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun chargeLocationsDao(): ChargeLocationsDao
abstract fun filterValueDao(): FilterValueDao
companion object {
private lateinit var context: Context
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db").build()
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db")
.addMigrations(MIGRATION_2)
.build()
}
fun getInstance(context: Context): AppDatabase {
this.context = context.applicationContext
return database
}
private val MIGRATION_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `BooleanFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))")
db.execSQL("CREATE TABLE IF NOT EXISTS `MultipleChoiceFilterValue` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, PRIMARY KEY(`key`))")
}
}
}
}

View File

@@ -0,0 +1,43 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.room.*
import net.vonforst.evmap.viewmodel.BooleanFilterValue
import net.vonforst.evmap.viewmodel.FilterValue
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
@Dao
abstract class FilterValueDao {
@Query("SELECT * FROM booleanfiltervalue")
protected abstract fun getBooleanFilterValues(): LiveData<List<BooleanFilterValue>>
@Query("SELECT * FROM multiplechoicefiltervalue")
protected abstract fun getMultipleChoiceFilterValues(): LiveData<List<MultipleChoiceFilterValue>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun insert(vararg values: MultipleChoiceFilterValue)
open fun getFilterValues(): LiveData<List<FilterValue>> =
MediatorLiveData<List<FilterValue>>().apply {
val sources = listOf(getBooleanFilterValues(), getMultipleChoiceFilterValues())
for (source in sources) {
addSource(source) {
value = sources.mapNotNull { it.value }.flatten()
}
}
}
@Transaction
open suspend fun insert(vararg values: FilterValue) {
values.forEach {
when (it) {
is BooleanFilterValue -> insert(it)
is MultipleChoiceFilterValue -> insert(it)
}
}
}
}

View File

@@ -17,6 +17,10 @@ class Converters {
val type = Types.newParameterizedType(List::class.java, ChargerPhoto::class.java)
moshi.adapter<List<ChargerPhoto>>(type)
}
private val stringSetAdapter by lazy {
val type = Types.newParameterizedType(Set::class.java, String::class.java)
moshi.adapter<Set<String>>(type)
}
@TypeConverter
fun fromChargepointList(value: List<Chargepoint>?): String {
@@ -49,4 +53,14 @@ class Converters {
LocalTime.parse(it)
}
}
@TypeConverter
fun fromStringSet(value: Set<String>?): String {
return stringSetAdapter.toJson(value)
}
@TypeConverter
fun toStringSet(value: String): Set<String>? {
return stringSetAdapter.fromJson(value)
}
}

View File

@@ -2,35 +2,99 @@ package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
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.GoingElectricApi
import net.vonforst.evmap.storage.AppDatabase
import kotlin.reflect.KClass
import kotlin.reflect.full.cast
class FilterViewModel(application: Application, geApiKey: String) :
AndroidViewModel(application) {
private var api = GoingElectricApi.create(geApiKey)
private var db = AppDatabase.getInstance(application)
val filters: MutableLiveData<List<Filter>> by lazy {
MutableLiveData<List<Filter>>().apply {
private val filters: MutableLiveData<List<Filter<out FilterValue>>> by lazy {
MutableLiveData<List<Filter<out FilterValue>>>().apply {
value = listOf(
BooleanFilter(application.getString(R.string.filter_free), "freecharging")
)
}
}
private val filterValues: LiveData<List<FilterValue>> by lazy {
db.filterValueDao().getFilterValues()
}
val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
for (source in listOf(filters, filterValues)) {
addSource(source) {
val filters = filters.value
val values = filterValues.value
if (filters != null && values != null) {
value = filters.map { filter ->
val value =
values.find { it.key == filter.key } ?: filter.defaultValue()
FilterWithValue(filter, filter.valueClass.cast(value))
}
} else {
value = null
}
}
}
}
}
suspend fun saveFilterValues() {
filtersWithValue.value?.forEach {
db.filterValueDao().insert(it.value)
}
}
}
sealed class Filter : Equatable {
sealed class Filter<out T : FilterValue> : Equatable {
abstract val name: String
abstract val key: String
abstract val valueClass: KClass<out T>
abstract fun defaultValue(): T
}
data class BooleanFilter(override val name: String, override val key: String) : Filter()
data class BooleanFilter(override val name: String, override val key: String) :
Filter<BooleanFilterValue>() {
override val valueClass: KClass<BooleanFilterValue> = BooleanFilterValue::class
override fun defaultValue() = BooleanFilterValue(key, false)
}
data class MultipleChoiceFilter(
override val name: String,
override val key: String,
val choices: Map<String, String>
) : Filter()
) : Filter<MultipleChoiceFilterValue>() {
override val valueClass: KClass<MultipleChoiceFilterValue> = MultipleChoiceFilterValue::class
override fun defaultValue() = MultipleChoiceFilterValue(key, emptySet(), true)
}
sealed class FilterValue : Equatable {
abstract val key: String
}
@Entity
data class BooleanFilterValue(
@PrimaryKey override val key: String,
var value: Boolean
) : FilterValue()
@Entity
data class MultipleChoiceFilterValue(
@PrimaryKey override val key: String,
val values: Set<String>,
val all: Boolean
) : FilterValue()
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable

View File

@@ -28,12 +28,19 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
val mapPosition: MutableLiveData<MapPosition> by lazy {
MutableLiveData<MapPosition>()
}
private val filterValues: LiveData<List<FilterValue>> by lazy {
db.filterValueDao().getFilterValues()
}
val chargepoints: MediatorLiveData<Resource<List<ChargepointListItem>>> by lazy {
MediatorLiveData<Resource<List<ChargepointListItem>>>()
.apply {
value = Resource.loading(emptyList())
addSource(mapPosition) {
mapPosition.value?.let { pos -> loadChargepoints(pos) }
listOf(mapPosition, filterValues).forEach {
addSource(it) {
val pos = mapPosition.value ?: return@addSource
val filterValues = filterValues.value ?: return@addSource
loadChargepoints(pos, filterValues)
}
}
}
}
@@ -97,16 +104,12 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
private fun loadChargepoints(mapPosition: MapPosition) {
private fun loadChargepoints(mapPosition: MapPosition, filterValues: List<FilterValue>) {
chargepoints.value = Resource.loading(chargepoints.value?.data)
val bounds = mapPosition.bounds
val zoom = mapPosition.zoom
api.getChargepoints(
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude,
clustering = zoom < 13, zoom = zoom,
clusterDistance = 70
).enqueue(object : Callback<ChargepointList> {
getChargepointsWithFilters(bounds, zoom, filterValues).enqueue(object :
Callback<ChargepointList> {
override fun onFailure(call: Call<ChargepointList>, t: Throwable) {
chargepoints.value = Resource.error(t.message, chargepoints.value?.data)
t.printStackTrace()
@@ -126,6 +129,26 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
})
}
private fun getChargepointsWithFilters(
bounds: LatLngBounds,
zoom: Float,
filterValues: List<FilterValue>
): Call<ChargepointList> {
var freecharging = false
filterValues.forEach {
when (it.key) {
"freecharging" -> freecharging = (it as BooleanFilterValue).value
}
}
return api.getChargepoints(
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude,
clustering = zoom < 13, zoom = zoom,
clusterDistance = 70, freecharging = freecharging
)
}
private suspend fun loadAvailability(charger: ChargeLocation) {
availability.value = Resource.loading(null)
availability.value = getAvailability(charger)

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>

View File

@@ -33,6 +33,6 @@
android:id="@+id/filters_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:data="@{vm.filters}" />
app:data="@{vm.filtersWithValue}" />
</LinearLayout>
</layout>

View File

@@ -7,9 +7,13 @@
<import type="net.vonforst.evmap.viewmodel.BooleanFilter" />
<import type="net.vonforst.evmap.viewmodel.BooleanFilterValue" />
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
<variable
name="item"
type="BooleanFilter" />
type="FilterWithValue&lt;BooleanFilterValue&gt;" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@@ -23,7 +27,7 @@
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@{item.name}"
android:text="@{item.filter.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch1"
@@ -38,6 +42,7 @@
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:checked="@={item.value.value}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_apply"
android:title="@string/menu_filter"
android:icon="@drawable/ic_check"
app:showAsAction="ifRoom" />
</menu>