Compare commits

..

6 Commits
0.1.4 ... 0.1.5

Author SHA1 Message Date
Johan von Forstner
2e9112f5c2 Release 0.1.5 2020-06-13 16:44:06 +02:00
Johan von Forstner
3c709fa3c5 add visual and haptic feedback when enabling/disabling filters 2020-06-13 16:19:50 +02:00
Johan von Forstner
11c868af66 remove TODO 2020-06-13 16:08:57 +02:00
Johan von Forstner
e3ea72bac6 implement new selection interface for network and chargecard filters 2020-06-13 16:03:52 +02:00
Johan von Forstner
d01371f6e9 add filters by network and charge card 2020-06-13 15:48:02 +02:00
Johan von Forstner
6130b190e1 disable/enable filters with long click on filter view 2020-06-13 08:04:06 +02:00
20 changed files with 675 additions and 85 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 29
versionCode 12
versionName "0.1.4"
versionCode 13
versionName "0.1.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import androidx.databinding.DataBindingUtil
import androidx.databinding.Observable
@@ -19,7 +20,9 @@ import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.api.goingelectric.OpeningHoursDays
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceLargeBinding
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
import net.vonforst.evmap.fragment.MultiSelectDialog
import net.vonforst.evmap.viewmodel.*
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@@ -29,8 +32,8 @@ interface Equatable {
override fun equals(other: Any?): Boolean;
}
abstract class DataBindingAdapter<T : Equatable>() :
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback()) {
abstract class DataBindingAdapter<T : Equatable>(getKey: ((T) -> Any)? = null) :
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback(getKey)) {
var onClickListener: ((T) -> Unit)? = null
@@ -51,14 +54,20 @@ abstract class DataBindingAdapter<T : Equatable>() :
open fun bind(holder: ViewHolder<T>, item: T) {
holder.binding.setVariable(BR.item, item)
holder.binding.executePendingBindings()
holder.binding.root.setOnClickListener {
val listener = onClickListener ?: return@setOnClickListener
listener(item)
if (onClickListener != null) {
holder.binding.root.setOnClickListener {
val listener = onClickListener ?: return@setOnClickListener
listener(item)
}
}
}
class DiffCallback<T : Equatable> : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = oldItem === newItem
class DiffCallback<T : Equatable>(val getKey: ((T) -> Any)?) : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = if (getKey != null) {
(getKey)(oldItem) == (getKey)(newItem)
} else {
oldItem === newItem
}
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = oldItem == newItem
}
@@ -137,7 +146,6 @@ fun buildDetails(loc: ChargeLocation?, ctx: Context): List<DetailAdapter.Detail>
loc.faultReport.description ?: "",
clickable = true
) else null,
// TODO: separate layout for opening hours with expandable details
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailAdapter.Detail(
R.drawable.ic_hours,
R.string.hours,
@@ -183,11 +191,18 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
val itemids = mutableMapOf<String, Long>()
var maxId = 0L
override fun getItemViewType(position: Int): Int = when (getItem(position).filter) {
is BooleanFilter -> R.layout.item_filter_boolean
is MultipleChoiceFilter -> R.layout.item_filter_multiple_choice
is SliderFilter -> R.layout.item_filter_slider
}
override fun getItemViewType(position: Int): Int =
when (val filter = getItem(position).filter) {
is BooleanFilter -> R.layout.item_filter_boolean
is MultipleChoiceFilter -> {
if (filter.manyChoices) {
R.layout.item_filter_multiple_choice_large
} else {
R.layout.item_filter_multiple_choice
}
}
is SliderFilter -> R.layout.item_filter_slider
}
override fun bind(
holder: ViewHolder<FilterWithValue<FilterValue>>,
@@ -202,10 +217,18 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
)
}
is MultipleChoiceFilterValue -> {
setupMultipleChoice(
holder.binding as ItemFilterMultipleChoiceBinding,
item.filter as MultipleChoiceFilter, item.value
)
val filter = item.filter as MultipleChoiceFilter
if (filter.manyChoices) {
setupMultipleChoiceMany(
holder.binding as ItemFilterMultipleChoiceLargeBinding,
filter, item.value
)
} else {
setupMultipleChoice(
holder.binding as ItemFilterMultipleChoiceBinding,
filter, item.value
)
}
}
}
}
@@ -293,6 +316,27 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
updateButtons()
}
private fun setupMultipleChoiceMany(
binding: ItemFilterMultipleChoiceLargeBinding,
filter: MultipleChoiceFilter,
value: MultipleChoiceFilterValue
) {
if (value.all) {
value.values = filter.choices.keys.toMutableSet()
binding.notifyPropertyChanged(BR.item)
}
binding.btnEdit.setOnClickListener {
val dialog = MultiSelectDialog.getInstance(filter.name, filter.choices, value.values)
dialog.okListener = { selected ->
value.values = selected.toMutableSet()
value.all = value.values == filter.choices.keys
binding.item = binding.item
}
dialog.show((binding.root.context as AppCompatActivity).supportFragmentManager, null)
}
}
private fun setupSlider(
binding: ItemFilterSliderBinding,
filter: SliderFilter,

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

@@ -10,7 +10,6 @@ import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import androidx.transition.TransitionManager
import android.view.*
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
@@ -32,6 +31,7 @@ import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.CameraUpdateFactory
@@ -42,6 +42,7 @@ import com.google.android.gms.maps.model.*
import com.google.android.libraries.places.api.model.Place
import com.google.android.libraries.places.widget.Autocomplete
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialArcMotion
import com.google.android.material.transition.MaterialContainerTransform
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
@@ -691,9 +692,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
filterView?.setOnLongClickListener {
requireView().findNavController().navigate(
R.id.action_map_to_filterFragment
// enable/disable filters
vm.filtersActive.value = !vm.filtersActive.value!!
// haptic feedback
filterView.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
)
// show snackbar
Snackbar.make(
requireView(), if (vm.filtersActive.value!!) {
R.string.filters_activated
} else {
R.string.filters_deactivated
}, Snackbar.LENGTH_SHORT
).show()
true
}
}

View File

@@ -0,0 +1,114 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.dialog_multi_select.*
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.DataBindingAdapter
import net.vonforst.evmap.adapter.Equatable
import java.util.*
import kotlin.collections.HashMap
import kotlin.collections.HashSet
class MultiSelectDialog : AppCompatDialogFragment() {
companion object {
fun getInstance(
title: String,
data: Map<String, String>,
selected: Set<String>
): MultiSelectDialog {
val dialog = MultiSelectDialog()
dialog.arguments = Bundle().apply {
putString("title", title)
putSerializable("data", HashMap(data))
putSerializable("selected", HashSet(selected))
}
return dialog
}
}
var okListener: ((Set<String>) -> Unit)? = null
var cancelListener: (() -> Unit)? = null
private lateinit var items: List<MultiSelectItem>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_multi_select, container)
}
override fun onStart() {
super.onStart()
// dialog with 95% screen height
dialog?.window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
(resources.displayMetrics.heightPixels * 0.95).toInt()
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val data = requireArguments().getSerializable("data") as HashMap<String, String>
val selected = requireArguments().getSerializable("selected") as HashSet<String>
val title = requireArguments().getString("title")
dialogTitle.text = title
val adapter = Adapter()
list.adapter = adapter
list.layoutManager = LinearLayoutManager(view.context)
items = data.entries.toList().sortedBy { it.key }.map {
MultiSelectItem(it.key, it.value, it.key in selected)
}
adapter.submitList(items)
etSearch.doAfterTextChanged { text ->
adapter.submitList(search(items, text.toString()))
}
btnCancel.setOnClickListener {
cancelListener?.let { listener ->
listener()
}
dismiss()
}
btnOK.setOnClickListener {
okListener?.let { listener ->
val result = items.filter { it.selected }.map { it.key }.toSet()
listener(result)
}
dismiss()
}
btnAll.setOnClickListener {
items = items.map { MultiSelectItem(it.key, it.name, true) }
adapter.submitList(search(items, etSearch.text.toString()))
}
btnNone.setOnClickListener {
items = items.map { MultiSelectItem(it.key, it.name, false) }
adapter.submitList(search(items, etSearch.text.toString()))
}
}
private fun search(
items: List<MultiSelectItem>,
text: String
): List<MultiSelectItem> {
return items.filter { item ->
// search for string within name
text.toLowerCase(Locale.getDefault()) in item.name.toLowerCase(Locale.getDefault())
}
}
class Adapter() : DataBindingAdapter<MultiSelectItem>({ it.key }) {
override fun getItemViewType(position: Int) = R.layout.dialog_multi_select_item
}
}
data class MultiSelectItem(val key: String, val name: String, var selected: Boolean) : Equatable

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

@@ -2,9 +2,6 @@ package net.vonforst.evmap.ui
import android.content.Context
import android.content.res.ColorStateList
import android.os.Build
import android.text.Html
import android.text.Spanned
import android.view.View
import android.widget.ImageView
import android.widget.TextView

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, manyChoices = true
),
MultipleChoiceFilter(
application.getString(R.string.filter_chargecards), "chargecards",
chargecardMap, manyChoices = true
)
)
}
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 {
@@ -144,7 +171,8 @@ data class MultipleChoiceFilter(
override val name: String,
override val key: String,
val choices: Map<String, String>,
val commonChoices: Set<String>? = null
val commonChoices: Set<String>? = null,
val manyChoices: Boolean = false
) : Filter<MultipleChoiceFilterValue>() {
override val valueClass: KClass<MultipleChoiceFilterValue> = MultipleChoiceFilterValue::class
override fun defaultValue() = MultipleChoiceFilterValue(key, mutableSetOf(), true)

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)

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="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
</vector>

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="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/dialogTitle"
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="18dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/btnAll"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Select Something" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toTopOf="@+id/btnOK"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tilSearch" />
<Button
android:id="@+id/btnAll"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all"
app:layout_constraintBaseline_toBaselineOf="@+id/dialogTitle"
app:layout_constraintEnd_toStartOf="@+id/btnNone" />
<Button
android:id="@+id/btnOK"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="@string/ok"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btnCancel"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnOK" />
<Button
android:id="@+id/btnNone"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/none"
app:layout_constraintBaseline_toBaselineOf="@+id/btnAll"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilSearch"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:hint="@string/search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnAll"
app:startIconDrawable="@drawable/ic_search">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.fragment.MultiSelectItem" />
<variable
name="item"
type="MultiSelectItem" />
</data>
<CheckBox
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:layout_marginStart="?attr/dialogPreferredPadding"
android:layout_marginEnd="?attr/dialogPreferredPadding"
android:paddingStart="20dp"
android:ellipsize="marquee"
android:text="@{item.name}"
android:checked="@={item.selected}"
tools:text="Item"
tools:ignore="RtlSymmetry" />
</layout>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilter" />
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue" />
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
<variable
name="item"
type="FilterWithValue&lt;MultipleChoiceFilterValue&gt;" />
<variable
name="showingAll"
type="boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView17"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:text="@{item.filter.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintEnd_toStartOf="@+id/btnEdit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Networks" />
<ImageButton
android:id="@+id/btnEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="?selectableItemBackgroundBorderless"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_edit"
android:contentDescription="@string/edit" />
<TextView
android:id="@+id/textView26"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:text="@{item.value.all ? @string/all_selected : @string/number_selected(item.value.values.size())}"
app:layout_constraintEnd_toStartOf="@+id/btnEdit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView17"
tools:text="4 selected" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -77,8 +77,17 @@
<string name="map_traffic">Verkehr</string>
<string name="faq">FAQ</string>
<string name="menu_filters_active">Filter aktiv</string>
<string name="filters_activated">Filter aktiviert</string>
<string name="filters_deactivated">Filter deaktiviert</string>
<string name="menu_edit_filters">Filter bearbeiten…</string>
<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>
<string name="all_selected">Alle ausgewählt</string>
<string name="number_selected">%d ausgewählt</string>
<string name="edit">bearbeiten</string>
<string name="cancel">Abbrechen</string>
<string name="ok">OK</string>
</resources>

View File

@@ -76,8 +76,17 @@
<string name="map_traffic">Traffic</string>
<string name="faq">FAQ</string>
<string name="menu_filters_active">Filters active</string>
<string name="filters_activated">Filters activated</string>
<string name="filters_deactivated">Filters deactivated</string>
<string name="menu_edit_filters">Edit filters…</string>
<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>
<string name="all_selected">All selected</string>
<string name="number_selected">%d selected</string>
<string name="edit">edit</string>
<string name="cancel">Cancel</string>
<string name="ok">OK</string>
</resources>