mirror of
https://github.com/ev-map/EVMap.git
synced 2026-02-24 18:36:10 -05:00
working implementation for first filter (free charging) #9
This commit is contained in:
@@ -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++
|
||||
|
||||
@@ -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/")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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`))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
10
app/src/main/res/drawable/ic_check.xml
Normal file
10
app/src/main/res/drawable/ic_check.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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<BooleanFilterValue>" />
|
||||
</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" />
|
||||
|
||||
9
app/src/main/res/menu/filter.xml
Normal file
9
app/src/main/res/menu/filter.xml
Normal 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>
|
||||
Reference in New Issue
Block a user