Compare commits

..

23 Commits
0.2.0 ... 0.2.2

Author SHA1 Message Date
johan12345
d041513516 Release 0.2.2 2020-07-13 19:42:25 +02:00
johan12345
1effba77d1 update JUnit 2020-07-13 19:37:41 +02:00
Johan von Forstner
df79f02e1d fix crashes with missing internet connection 2020-07-11 18:25:29 +02:00
Johan von Forstner
c4d44f9ddf switch connectors filter to MultiSelectDialog
because Chip interface is buggy
2020-07-05 12:38:58 +02:00
Johan von Forstner
6bec397133 try to improve MultipleChoiceFilter 2020-07-05 11:20:32 +02:00
Johan von Forstner
474b621af0 fix imports 2020-07-05 11:13:23 +02:00
Johan von Forstner
36aeb201ca move some adapters out of DataBindingAdapters.kt 2020-07-05 11:07:36 +02:00
Johan von Forstner
76a241d691 add missing German translations for "map" and "favorites" 2020-07-02 19:55:41 +02:00
Johan von Forstner
0f7bf7913f Release 0.2.1 2020-07-02 19:42:29 +02:00
Johan von Forstner
d11925eb33 update libraries 2020-07-02 19:25:50 +02:00
Johan von Forstner
6ac49fd84d highlight selected charging cards also in detail dialog
(refs #32)
2020-07-02 19:15:44 +02:00
Johan von Forstner
097b7941a2 close keyboard when pressing enter in MultiSelectDialog search 2020-07-02 19:02:45 +02:00
Johan von Forstner
23b87e69c0 highlight selected charging cards in preview of compatible charging cards
(fixes #32)
2020-07-02 18:54:22 +02:00
johan12345
3bb5521c18 minimum connectors filter: start at 1 (fixes #34) 2020-06-30 17:28:16 +02:00
johan12345
76f7b97c1f Set marker color depending on selected connectors (fixes #33) 2020-06-30 16:56:05 +02:00
johan12345
50de0009c7 MultiSelectDialog: sort by name instead of by ID (fixes #31) 2020-06-29 07:54:01 +02:00
johan12345
f906846fcc improve performance of IconGenerator by caching BitmapDescriptors instead of Bitmaps 2020-06-28 20:18:37 +02:00
johan12345
b50225af32 further improvements to MarkerAnimator 2020-06-28 19:39:15 +02:00
Johan von Forstner
8abd5219aa improvements to marker animations 2020-06-27 19:00:13 +02:00
Johan von Forstner
71f9a25c5a IconGenerator: increase cache size 2020-06-27 18:44:22 +02:00
Johan von Forstner
b5f4314795 preserve night mode across app restarts 2020-06-26 08:26:49 +02:00
Johan von Forstner
034196b9fa Add setting to manually enable/disable night mode (fixes #35) 2020-06-25 18:52:30 +02:00
Johan von Forstner
72d7f7dc57 LocaleContextWrapper.kt: remove unused code 2020-06-25 18:52:29 +02:00
31 changed files with 722 additions and 471 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 29
versionCode 18
versionName "0.2.0"
versionCode 20
versionName "0.2.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -114,7 +114,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// navigation library
def nav_version = "2.3.0-rc01"
def nav_version = "2.3.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
@@ -137,7 +137,7 @@ dependencies {
// debug tools
implementation 'com.facebook.stetho:stetho:1.5.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13'
testImplementation "com.squareup.okhttp3:mockwebserver:3.14.7"
//noinspection GradleDependency
testImplementation 'org.json:json:20080701'
@@ -146,5 +146,5 @@ dependencies {
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
}

View File

@@ -3,11 +3,14 @@ package net.vonforst.evmap
import android.app.Application
import com.facebook.stetho.Stetho
import com.google.android.libraries.places.api.Places
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.updateNightMode
class EvMapApplication : Application() {
override fun onCreate() {
super.onCreate()
updateNightMode(PreferenceDataSource(this))
Stetho.initializeWithDefaults(this);
Places.initialize(getApplicationContext(), getString(R.string.google_maps_key));
Places.initialize(applicationContext, getString(R.string.google_maps_key));
}
}

View File

@@ -1,6 +1,9 @@
package net.vonforst.evmap
import android.graphics.Typeface
import android.os.Bundle
import android.text.*
import android.text.style.StyleSpan
fun Bundle.optDouble(name: String): Double? {
if (!this.containsKey(name)) return null
@@ -14,4 +17,38 @@ fun Bundle.optLong(name: String): Long? {
val lng = this.getLong(name, Long.MIN_VALUE)
return if (lng == Long.MIN_VALUE) null else lng
}
fun <T> Iterable<T>.joinToSpannedString(
separator: CharSequence = ", ",
prefix: CharSequence = "",
postfix: CharSequence = "",
limit: Int = -1,
truncated: CharSequence = "...",
transform: ((T) -> CharSequence)? = null
): CharSequence {
return SpannedString(
joinTo(
SpannableStringBuilder(),
separator,
prefix,
postfix,
limit,
truncated,
transform
)
)
}
operator fun CharSequence.plus(other: CharSequence): CharSequence {
return TextUtils.concat(this, other)
}
fun String.bold(): CharSequence {
return SpannableString(this).apply {
setSpan(
StyleSpan(Typeface.BOLD), 0, this.length,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
}
}

View File

@@ -1,31 +1,18 @@
package net.vonforst.evmap.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.text.HtmlCompat
import androidx.core.view.children
import androidx.databinding.DataBindingUtil
import androidx.databinding.Observable
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import net.vonforst.evmap.BR
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.goingelectric.*
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
import java.time.format.FormatStyle
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.viewmodel.DonationItem
import net.vonforst.evmap.viewmodel.FavoritesViewModel
interface Equatable {
override fun equals(other: Any?): Boolean;
@@ -92,121 +79,6 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
override fun getItemViewType(position: Int): Int = R.layout.item_connector
}
class DetailAdapter : DataBindingAdapter<DetailAdapter.Detail>() {
data class Detail(
val icon: Int,
val contentDescription: Int,
val text: CharSequence,
val detailText: CharSequence? = null,
val links: Boolean = true,
val clickable: Boolean = false,
val hoursDays: OpeningHoursDays? = null
) : Equatable
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
if (item.hoursDays != null) {
return R.layout.item_detail_openinghours
} else {
return R.layout.item_detail
}
}
}
fun buildDetails(
loc: ChargeLocation?,
chargeCards: Map<Long, ChargeCard>?,
ctx: Context
): List<DetailAdapter.Detail> {
if (loc == null) return emptyList()
return listOfNotNull(
DetailAdapter.Detail(
R.drawable.ic_address,
R.string.address,
loc.address.toString(),
loc.locationDescription
),
if (loc.operator != null) DetailAdapter.Detail(
R.drawable.ic_operator,
R.string.operator,
loc.operator
) else null,
if (loc.network != null) DetailAdapter.Detail(
R.drawable.ic_network,
R.string.network,
loc.network
) else null,
if (loc.faultReport != null) DetailAdapter.Detail(
R.drawable.ic_fault_report,
R.string.fault_report,
loc.faultReport.created?.let {
ctx.getString(
R.string.fault_report_date,
loc.faultReport.created
.atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
} ?: "",
loc.faultReport.description?.let {
HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY)
} ?: "",
clickable = true
) else null,
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailAdapter.Detail(
R.drawable.ic_hours,
R.string.hours,
loc.openinghours.getStatusText(ctx),
loc.openinghours.description,
hoursDays = loc.openinghours.days
) else null,
if (loc.cost != null) DetailAdapter.Detail(
R.drawable.ic_cost,
R.string.cost,
loc.cost.getStatusText(ctx),
loc.cost.descriptionLong ?: loc.cost.descriptionShort
)
else null,
if (loc.chargecards != null && loc.chargecards.isNotEmpty()) DetailAdapter.Detail(
R.drawable.ic_payment,
R.string.charge_cards,
ctx.resources.getQuantityString(
R.plurals.charge_cards_compatible_num,
loc.chargecards.size, loc.chargecards.size
),
formatChargeCards(loc.chargecards, chargeCards, ctx),
clickable = true
) else null,
DetailAdapter.Detail(
R.drawable.ic_location,
R.string.coordinates,
loc.coordinates.formatDMS(),
loc.coordinates.formatDecimal(),
links = false,
clickable = true
)
)
}
fun formatChargeCards(
chargecards: List<ChargeCardId>,
chargecardData: Map<Long, ChargeCard>?,
ctx: Context
): String {
if (chargecardData == null) return ""
val maxItems = 5
var result = chargecards
.take(maxItems)
.mapNotNull { chargecardData[it.id]?.name }
.joinToString()
if (chargecards.size > maxItems) {
result += " " + ctx.getString(R.string.and_n_others, chargecards.size - maxItems)
}
return result
}
class FavoritesAdapter(val vm: FavoritesViewModel) :
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
@@ -219,194 +91,6 @@ class FavoritesAdapter(val vm: FavoritesViewModel) :
override fun getItemId(position: Int): Long = getItem(position).charger.id
}
class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
init {
setHasStableIds(true)
}
val itemids = mutableMapOf<String, Long>()
var maxId = 0L
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>>,
item: FilterWithValue<FilterValue>
) {
super.bind(holder, item)
when (item.value) {
is SliderFilterValue -> {
setupSlider(
holder.binding as ItemFilterSliderBinding,
item.filter as SliderFilter, item.value
)
}
is MultipleChoiceFilterValue -> {
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
)
}
}
}
}
private fun setupMultipleChoice(
binding: ItemFilterMultipleChoiceBinding,
filter: MultipleChoiceFilter,
value: MultipleChoiceFilterValue
) {
val inflater = LayoutInflater.from(binding.root.context)
value.values.toList().forEach {
// delete values that cannot be selected anymore
if (it !in filter.choices.keys) value.values.remove(it)
}
fun updateButtons() {
value.all = value.values == filter.choices.keys
binding.btnAll.isEnabled = !value.all
binding.btnNone.isEnabled = value.values.isNotEmpty()
}
val chips = mutableMapOf<String, Chip>()
binding.chipGroup.children.forEach {
if (it.id != R.id.chipMore) binding.chipGroup.removeView(it)
}
filter.choices.entries.sortedByDescending {
it.key in value.values
}.sortedByDescending {
if (filter.commonChoices != null) it.key in filter.commonChoices else false
}.forEach { choice ->
val chip = inflater.inflate(
R.layout.item_filter_multiple_choice_chip,
binding.chipGroup,
false
) as Chip
chip.text = choice.value
chip.isChecked = choice.key in value.values || value.all
if (value.all && choice.key !in value.values) value.values.add(choice.key)
chip.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
value.values.add(choice.key)
} else {
value.values.remove(choice.key)
}
updateButtons()
}
if (filter.commonChoices != null && choice.key !in filter.commonChoices
&& !(chip.isChecked && !value.all) && !binding.showingAll
) {
chip.visibility = View.GONE
} else {
chip.visibility = View.VISIBLE
}
binding.chipGroup.addView(chip, binding.chipGroup.childCount - 1)
chips[choice.key] = chip
}
binding.btnAll.setOnClickListener {
value.all = true
value.values.addAll(filter.choices.keys)
chips.values.forEach { it.isChecked = true }
updateButtons()
}
binding.btnNone.setOnClickListener {
value.all = true
value.values.addAll(filter.choices.keys)
chips.values.forEach { it.isChecked = false }
updateButtons()
}
binding.chipMore.setOnClickListener {
binding.showingAll = !binding.showingAll
chips.forEach { (key, chip) ->
if (filter.commonChoices != null && key !in filter.commonChoices
&& !(chip.isChecked && !value.all) && !binding.showingAll
) {
chip.visibility = View.GONE
} else {
chip.visibility = View.VISIBLE
}
}
}
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,
value: SliderFilterValue
) {
binding.progress = filter.inverseMapping(value.value)
binding.mappedValue = value.value
binding.addOnPropertyChangedCallback(object :
Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
when (propertyId) {
BR.progress -> {
val mapped = filter.mapping(binding.progress)
value.value = mapped
binding.mappedValue = mapped
}
}
}
})
}
override fun getItemId(position: Int): Long {
val key = getItem(position).filter.key
var value = itemids[key]
if (value == null) {
maxId++
value = maxId
itemids[key] = maxId
}
return value
}
}
class DonationAdapter() : DataBindingAdapter<DonationItem>() {
override fun getItemViewType(position: Int): Int = R.layout.item_donation
}

View File

@@ -0,0 +1,139 @@
package net.vonforst.evmap.adapter
import android.content.Context
import androidx.core.text.HtmlCompat
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.ChargeCardId
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.OpeningHoursDays
import net.vonforst.evmap.bold
import net.vonforst.evmap.joinToSpannedString
import net.vonforst.evmap.plus
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
class DetailsAdapter : DataBindingAdapter<DetailsAdapter.Detail>() {
data class Detail(
val icon: Int,
val contentDescription: Int,
val text: CharSequence,
val detailText: CharSequence? = null,
val links: Boolean = true,
val clickable: Boolean = false,
val hoursDays: OpeningHoursDays? = null
) : Equatable
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
if (item.hoursDays != null) {
return R.layout.item_detail_openinghours
} else {
return R.layout.item_detail
}
}
}
fun buildDetails(
loc: ChargeLocation?,
chargeCards: Map<Long, ChargeCard>?,
filteredChargeCards: Set<Long>?,
ctx: Context
): List<DetailsAdapter.Detail> {
if (loc == null) return emptyList()
return listOfNotNull(
DetailsAdapter.Detail(
R.drawable.ic_address,
R.string.address,
loc.address.toString(),
loc.locationDescription
),
if (loc.operator != null) DetailsAdapter.Detail(
R.drawable.ic_operator,
R.string.operator,
loc.operator
) else null,
if (loc.network != null) DetailsAdapter.Detail(
R.drawable.ic_network,
R.string.network,
loc.network
) else null,
if (loc.faultReport != null) DetailsAdapter.Detail(
R.drawable.ic_fault_report,
R.string.fault_report,
loc.faultReport.created?.let {
ctx.getString(
R.string.fault_report_date,
loc.faultReport.created
.atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
} ?: "",
loc.faultReport.description?.let {
HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY)
} ?: "",
clickable = true
) else null,
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailsAdapter.Detail(
R.drawable.ic_hours,
R.string.hours,
loc.openinghours.getStatusText(ctx),
loc.openinghours.description,
hoursDays = loc.openinghours.days
) else null,
if (loc.cost != null) DetailsAdapter.Detail(
R.drawable.ic_cost,
R.string.cost,
loc.cost.getStatusText(ctx),
loc.cost.descriptionLong ?: loc.cost.descriptionShort
)
else null,
if (loc.chargecards != null && loc.chargecards.isNotEmpty()) DetailsAdapter.Detail(
R.drawable.ic_payment,
R.string.charge_cards,
ctx.resources.getQuantityString(
R.plurals.charge_cards_compatible_num,
loc.chargecards.size, loc.chargecards.size
),
formatChargeCards(loc.chargecards, chargeCards, filteredChargeCards, ctx),
clickable = true
) else null,
DetailsAdapter.Detail(
R.drawable.ic_location,
R.string.coordinates,
loc.coordinates.formatDMS(),
loc.coordinates.formatDecimal(),
links = false,
clickable = true
)
)
}
fun formatChargeCards(
chargecards: List<ChargeCardId>,
chargecardData: Map<Long, ChargeCard>?,
filteredChargeCards: Set<Long>?,
ctx: Context
): CharSequence {
if (chargecardData == null) return ""
val maxItems = 5
var result = chargecards
.sortedByDescending { filteredChargeCards?.contains(it.id) }
.take(maxItems)
.mapNotNull {
val name = chargecardData[it.id]?.name ?: return@mapNotNull null
if (filteredChargeCards?.contains(it.id) == true) {
name.bold()
} else {
name
}
}.joinToSpannedString()
if (chargecards.size > maxItems) {
result += " " + ctx.getString(R.string.and_n_others, chargecards.size - maxItems)
}
return result
}

View File

@@ -0,0 +1,227 @@
package net.vonforst.evmap.adapter
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import androidx.databinding.Observable
import com.google.android.material.chip.Chip
import net.vonforst.evmap.BR
import net.vonforst.evmap.R
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 kotlin.math.max
class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
init {
setHasStableIds(true)
}
val itemids = mutableMapOf<String, Long>()
var maxId = 0L
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>>,
item: FilterWithValue<FilterValue>
) {
super.bind(holder, item)
when (item.value) {
is SliderFilterValue -> {
setupSlider(
holder.binding as ItemFilterSliderBinding,
item.filter as SliderFilter, item.value
)
}
is MultipleChoiceFilterValue -> {
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
)
}
}
}
}
private fun setupMultipleChoice(
binding: ItemFilterMultipleChoiceBinding,
filter: MultipleChoiceFilter,
value: MultipleChoiceFilterValue
) {
// TODO: this implementation seems to be buggy
val inflater = LayoutInflater.from(binding.root.context)
value.values.toList().forEach {
// delete values that cannot be selected anymore
if (it !in filter.choices.keys) value.values.remove(it)
}
fun updateButtons() {
value.all = value.values == filter.choices.keys
binding.btnAll.isEnabled = !value.all
binding.btnNone.isEnabled = value.values.isNotEmpty()
}
val chips = mutableMapOf<String, Chip>()
// reuse existing chips in layout
val reuseChips = binding.chipGroup.children.filter {
it.id != R.id.chipMore
}.toMutableList()
binding.chipGroup.children.forEach {
if (it.id != R.id.chipMore) binding.chipGroup.removeView(it)
}
filter.choices.entries.sortedByDescending {
it.key in value.values
}.sortedByDescending {
if (filter.commonChoices != null) it.key in filter.commonChoices else false
}.forEach { choice ->
var reused = false
val chip = if (reuseChips.size > 0) {
reused = true
reuseChips.removeAt(0) as Chip
} else {
inflater.inflate(
R.layout.item_filter_multiple_choice_chip,
binding.chipGroup,
false
) as Chip
}
chip.text = choice.value
chip.isChecked = choice.key in value.values || value.all
if (value.all && choice.key !in value.values) value.values.add(choice.key)
chip.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
value.values.add(choice.key)
} else {
value.values.remove(choice.key)
}
updateButtons()
}
if (filter.commonChoices != null && choice.key !in filter.commonChoices
&& !(chip.isChecked && !value.all) && !binding.showingAll
) {
chip.visibility = View.GONE
} else {
chip.visibility = View.VISIBLE
}
if (!reused) binding.chipGroup.addView(chip, binding.chipGroup.childCount - 1)
chips[choice.key] = chip
}
// delete surplus reusable chips
reuseChips.forEach {
binding.chipGroup.removeView(it)
}
binding.btnAll.setOnClickListener {
value.all = true
value.values.addAll(filter.choices.keys)
chips.values.forEach { it.isChecked = true }
updateButtons()
}
binding.btnNone.setOnClickListener {
value.all = true
value.values.addAll(filter.choices.keys)
chips.values.forEach { it.isChecked = false }
updateButtons()
}
binding.chipMore.setOnClickListener {
binding.showingAll = !binding.showingAll
chips.forEach { (key, chip) ->
if (filter.commonChoices != null && key !in filter.commonChoices
&& !(chip.isChecked && !value.all) && !binding.showingAll
) {
chip.visibility = View.GONE
} else {
chip.visibility = View.VISIBLE
}
}
}
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,
commonChoices = filter.commonChoices
)
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,
value: SliderFilterValue
) {
binding.progress =
max(filter.inverseMapping(value.value) - filter.min, 0)
binding.mappedValue = filter.mapping(binding.progress + filter.min)
binding.addOnPropertyChangedCallback(object :
Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
when (propertyId) {
BR.progress -> {
val mapped = filter.mapping(binding.progress + filter.min)
value.value = mapped
binding.mappedValue = mapped
}
}
}
})
}
override fun getItemId(position: Int): Long {
val key = getItem(position).filter.key
var value = itemids[key]
if (value == null) {
maxId++
value = maxId
itemids[key] = maxId
}
return value
}
}

View File

@@ -64,11 +64,22 @@ data class ChargeLocation(
@Embedded val openinghours: OpeningHours?,
@Embedded val cost: Cost?
) : ChargepointListItem(), Equatable {
/**
* maximum power available from this charger.
*/
val maxPower: Double
get() {
return chargepoints.map { it.power }.max() ?: 0.0
return maxPower()
}
/**
* Gets the maximum power available from certain connectors of this charger.
*/
fun maxPower(connectors: Set<String>? = null): Double {
return chargepoints.filter { connectors?.contains(it.type) ?: true }
.map { it.power }.max() ?: 0.0
}
/**
* Merges chargepoints if they have the same plug and power
*

View File

@@ -55,7 +55,7 @@ import io.michaelrocks.bimap.MutableBiMap
import kotlinx.android.synthetic.main.fragment_map.*
import net.vonforst.evmap.*
import net.vonforst.evmap.adapter.ConnectorAdapter
import net.vonforst.evmap.adapter.DetailAdapter
import net.vonforst.evmap.adapter.DetailsAdapter
import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.ChargeLocationCluster
@@ -91,6 +91,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private var clusterMarkers: List<Marker> = emptyList()
private var searchResultMarker: Marker? = null
private var connectionErrorSnackbar: Snackbar? = null
private var previousChargepointIds: Set<Long>? = null
private lateinit var clusterIconGenerator: ClusterIconGenerator
private lateinit var chargerIconGenerator: ChargerIconGenerator
@@ -386,7 +387,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
markers.forEach { (m, c) ->
m.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(c), fault = c.faultReport != null
getMarkerTint(c, vm.filteredConnectors.value), fault = c.faultReport != null
)
)
}
@@ -397,7 +398,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
// highlight this marker
marker.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(charger), highlight = true, fault = charger.faultReport != null
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = true,
fault = charger.faultReport != null
)
)
animator.animateMarkerBounce(marker)
@@ -407,7 +410,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (m != marker) {
m.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(c), fault = c.faultReport != null
getMarkerTint(c, vm.filteredConnectors.value), fault = c.faultReport != null
)
)
}
@@ -471,7 +474,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
binding.detailView.details.apply {
adapter = DetailAdapter().apply {
adapter = DetailsAdapter().apply {
onClickListener = {
val charger = vm.chargerDetails.value?.data
if (charger != null) {
@@ -505,13 +508,22 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val activity = activity ?: return
val chargecardData = vm.chargeCardMap.value ?: return
val chargecards = charger.chargecards ?: return
val filteredChargeCards = vm.filteredChargeCards.value
val data = chargecards.map { chargecardData[it.id] }.sortedBy { it?.name }
val names = data.map { it?.name ?: "" }
val data = chargecards.mapNotNull { chargecardData[it.id] }
.sortedBy { it.name }
.sortedByDescending { filteredChargeCards?.contains(it.id) }
val names = data.map {
if (filteredChargeCards?.contains(it.id) == true) {
it.name.bold()
} else {
it.name
}
}
AlertDialog.Builder(activity)
.setTitle(R.string.charge_cards)
.setItems(names.toTypedArray()) { _, i ->
val card = data[i] ?: return@setItems
val card = data[i]
(activity as? MapsActivity)?.openUrl("https:${card.url}")
}.show()
}
@@ -635,6 +647,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
) == PackageManager.PERMISSION_GRANTED
}
@Synchronized
private fun updateMap(chargepoints: List<ChargepointListItem>) {
val map = this.map ?: return
clusterMarkers.forEach { it.remove() }
@@ -643,39 +656,54 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val chargers = chargepoints.filterIsInstance<ChargeLocation>()
val chargepointIds = chargers.map { it.id }.toSet()
// remove markers that disappeared
markers.entries.toList().forEach {
if (!chargepointIds.contains(it.value.id)) {
if (it.key.isVisible) {
val tint = getMarkerTint(it.value)
val highlight = it.value == vm.chargerSparse.value
val fault = it.value.faultReport != null
animator.animateMarkerDisappear(it.key, tint, highlight, fault)
} else {
it.key.remove()
}
markers.remove(it.key)
}
}
// add new markers
chargers.filter {
!markers.containsValue(it)
}.forEach { charger ->
val tint = getMarkerTint(charger)
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
.icon(
chargerIconGenerator.getBitmapDescriptor(
tint, highlight = highlight,
fault = charger.faultReport != null
)
)
// update icons of existing markers (connector filter may have changed)
for ((marker, charger) in markers) {
marker.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = charger == vm.chargerSparse.value,
fault = charger.faultReport != null
)
)
animator.animateMarkerAppear(marker, tint, highlight, fault)
markers[marker] = charger
}
if (chargers.toSet() != markers.values) {
// remove markers that disappeared
val bounds = map.projection.visibleRegion.latLngBounds
markers.entries.toList().forEach {
val marker = it.key
val charger = it.value
if (!chargepointIds.contains(charger.id)) {
// animate marker if it is visible, otherwise remove immediately
if (bounds.contains(marker.position)) {
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
animator.animateMarkerDisappear(marker, tint, highlight, fault)
} else {
animator.deleteMarker(marker)
}
markers.remove(marker)
}
}
// add new markers
val map1 = markers.values.map { it.id }
for (charger in chargers) {
if (!map1.contains(charger.id)) {
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
.visible(false)
)
animator.animateMarkerAppear(marker, tint, highlight, fault)
markers[marker] = charger
}
}
previousChargepointIds = chargepointIds
}
clusterMarkers = clusters.map { cluster ->
map.addMarker(

View File

@@ -20,13 +20,15 @@ class MultiSelectDialog : AppCompatDialogFragment() {
fun getInstance(
title: String,
data: Map<String, String>,
selected: Set<String>
selected: Set<String>,
commonChoices: Set<String>?
): MultiSelectDialog {
val dialog = MultiSelectDialog()
dialog.arguments = Bundle().apply {
putString("title", title)
putSerializable("data", HashMap(data))
putSerializable("selected", HashSet(selected))
if (commonChoices != null) putSerializable("commonChoices", HashSet(commonChoices))
}
return dialog
}
@@ -55,18 +57,23 @@ class MultiSelectDialog : AppCompatDialogFragment() {
}
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")
val args = requireArguments()
val data = args.getSerializable("data") as HashMap<String, String>
val selected = args.getSerializable("selected") as HashSet<String>
val title = args.getString("title")
val commonChoices = if (args.containsKey("commonChoices")) {
args.getSerializable("commonChoices") as HashSet<String>
} else null
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)
}
items = data.entries.toList()
.sortedBy { it.value }
.sortedByDescending { commonChoices?.contains(it.key) == true }
.map { MultiSelectItem(it.key, it.value, it.key in selected) }
adapter.submitList(items)
etSearch.doAfterTextChanged { text ->
@@ -95,20 +102,20 @@ class MultiSelectDialog : AppCompatDialogFragment() {
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
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

@@ -10,13 +10,18 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.updateNightMode
class SettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
private lateinit var prefs: PreferenceDataSource
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
prefs = PreferenceDataSource(requireContext())
val navController = findNavController()
toolbar.setupWithNavController(
@@ -43,6 +48,9 @@ class SettingsFragment : PreferenceFragmentCompat(),
it.startActivity(it.intent);
}
}
"darkmode" -> {
updateNightMode(prefs)
}
}
}

View File

@@ -6,6 +6,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import java.io.IOException
import java.time.Duration
import java.time.Instant
@@ -35,13 +36,19 @@ class ChargeCardRepository(
private suspend fun updateChargeCards() {
if (Duration.between(prefs.lastChargeCardUpdate, Instant.now()) < Duration.ofDays(1)) return
val response = api.getChargeCards()
if (!response.isSuccessful) return
try {
val response = api.getChargeCards()
if (!response.isSuccessful) return
for (card in response.body()!!.result) {
dao.insert(card)
for (card in response.body()!!.result) {
dao.insert(card)
}
prefs.lastChargeCardUpdate = Instant.now()
} catch (e: IOException) {
// ignore, and retry next time
e.printStackTrace()
return
}
prefs.lastChargeCardUpdate = Instant.now()
}
}

View File

@@ -5,6 +5,7 @@ import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import java.io.IOException
import java.time.Duration
import java.time.Instant
@@ -37,13 +38,19 @@ class NetworkRepository(
private suspend fun updateNetworks() {
if (Duration.between(prefs.lastNetworkUpdate, Instant.now()) < Duration.ofDays(1)) return
val response = api.getNetworks()
if (!response.isSuccessful) return
try {
val response = api.getNetworks()
if (!response.isSuccessful) return
for (name in response.body()!!.result) {
dao.insert(Network(name))
for (name in response.body()!!.result) {
dao.insert(Network(name))
}
prefs.lastNetworkUpdate = Instant.now()
} catch (e: IOException) {
// ignore, and retry next time
e.printStackTrace()
return
}
prefs.lastNetworkUpdate = Instant.now()
}
}

View File

@@ -5,6 +5,7 @@ import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import java.io.IOException
import java.time.Duration
import java.time.Instant
@@ -37,13 +38,19 @@ class PlugRepository(
private suspend fun updatePlugs() {
if (Duration.between(prefs.lastPlugUpdate, Instant.now()) < Duration.ofDays(1)) return
val response = api.getPlugs()
if (!response.isSuccessful) return
try {
val response = api.getPlugs()
if (!response.isSuccessful) return
for (name in response.body()!!.result) {
dao.insert(Plug(name))
for (name in response.body()!!.result) {
dao.insert(Plug(name))
}
prefs.lastPlugUpdate = Instant.now()
} catch (e: IOException) {
// ignore, and retry next time
e.printStackTrace()
return
}
prefs.lastPlugUpdate = Instant.now()
}
}

View File

@@ -39,4 +39,7 @@ class PreferenceDataSource(context: Context) {
val language: String
get() = sp.getString("language", "default")!!
val darkmode: String
get() = sp.getString("darkmode", "default")!!
}

View File

@@ -50,12 +50,8 @@ class ChargerIconGenerator(val context: Context) {
val fault: Boolean
)
val cacheSize = 8 * 1024 * 1024; // 8MiB
val cache = object : LruCache<BitmapData, Bitmap>(cacheSize) {
override fun sizeOf(key: BitmapData, value: Bitmap): Int {
return value.byteCount
}
}
val cacheSize = 420; // 420 items: 21 sizes, 5 colors, highlight on/off, fault on/off
val cache = LruCache<BitmapData, BitmapDescriptor>(cacheSize)
val oversize = 1.4f // increase to add padding for fault icon or scale > 1
val icon = R.drawable.ic_map_marker_charging
val highlightIcon = R.drawable.ic_map_marker_highlight
@@ -65,7 +61,7 @@ class ChargerIconGenerator(val context: Context) {
preloadCache()
}
fun preloadCache() {
private fun preloadCache() {
// pre-generates images for scale from 0 to 255 for all possible tint colors
val tints = listOf(
R.color.charger_100kw,
@@ -78,8 +74,7 @@ class ChargerIconGenerator(val context: Context) {
for (highlight in listOf(false, true)) {
for (tint in tints) {
for (scale in 0..20) {
val data = BitmapData(tint, scale, 255, highlight, fault)
cache.put(data, generateBitmap(data))
getBitmapDescriptor(tint, scale, 255, highlight, fault)
}
}
}
@@ -96,11 +91,12 @@ class ChargerIconGenerator(val context: Context) {
val data = BitmapData(tint, scale, alpha, highlight, fault)
val cachedImg = cache[data]
return if (cachedImg != null) {
BitmapDescriptorFactory.fromBitmap(cachedImg)
cachedImg
} else {
val bitmap = generateBitmap(data)
cache.put(data, bitmap)
BitmapDescriptorFactory.fromBitmap(bitmap)
val bmd = BitmapDescriptorFactory.fromBitmap(bitmap)
cache.put(data, bmd)
bmd
}
}

View File

@@ -10,16 +10,19 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import kotlin.math.max
fun getMarkerTint(charger: ChargeLocation): Int = when {
charger.maxPower >= 100 -> R.color.charger_100kw
charger.maxPower >= 43 -> R.color.charger_43kw
charger.maxPower >= 20 -> R.color.charger_20kw
charger.maxPower >= 11 -> R.color.charger_11kw
fun getMarkerTint(
charger: ChargeLocation,
connectors: Set<String>?
): Int = when {
charger.maxPower(connectors) >= 100 -> R.color.charger_100kw
charger.maxPower(connectors) >= 43 -> R.color.charger_43kw
charger.maxPower(connectors) >= 20 -> R.color.charger_20kw
charger.maxPower(connectors) >= 11 -> R.color.charger_11kw
else -> R.color.charger_low
}
class MarkerAnimator(val gen: ChargerIconGenerator) {
val animatingMarkers = hashMapOf<Marker, ValueAnimator>()
private val animatingMarkers = hashMapOf<String, ValueAnimator>()
fun animateMarkerAppear(
marker: Marker,
@@ -27,18 +30,15 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
highlight: Boolean,
fault: Boolean
) {
animatingMarkers[marker]?.cancel()
animatingMarkers.remove(marker)
animatingMarkers[marker.id]?.let {
it.cancel()
animatingMarkers.remove(marker.id)
}
val anim = ValueAnimator.ofInt(0, 20).apply {
duration = 250
interpolator = LinearOutSlowInInterpolator()
addUpdateListener { animationState ->
if (!marker.isVisible) {
cancel()
animatingMarkers.remove(marker)
return@addUpdateListener
}
val scale = animationState.animatedValue as Int
marker.setIcon(
gen.getBitmapDescriptor(
@@ -48,12 +48,15 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
fault = fault
)
)
marker.isVisible = true
}
addListener(onEnd = {
animatingMarkers.remove(marker)
animatingMarkers.remove(marker.id)
}, onCancel = {
animatingMarkers.remove(marker.id)
})
}
animatingMarkers[marker] = anim
animatingMarkers[marker.id] = anim
anim.start()
}
@@ -63,18 +66,15 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
highlight: Boolean,
fault: Boolean
) {
animatingMarkers[marker]?.cancel()
animatingMarkers.remove(marker)
animatingMarkers[marker.id]?.let {
it.cancel()
animatingMarkers.remove(marker.id)
}
val anim = ValueAnimator.ofInt(20, 0).apply {
duration = 200
interpolator = FastOutLinearInInterpolator()
addUpdateListener { animationState ->
if (!marker.isVisible) {
cancel()
animatingMarkers.remove(marker)
return@addUpdateListener
}
val scale = animationState.animatedValue as Int
marker.setIcon(
gen.getBitmapDescriptor(
@@ -86,32 +86,45 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
)
}
addListener(onEnd = {
animatingMarkers.remove(marker)
marker.remove()
animatingMarkers.remove(marker.id)
}, onCancel = {
marker.remove()
animatingMarkers.remove(marker.id)
})
}
animatingMarkers[marker] = anim
animatingMarkers[marker.id] = anim
anim.start()
}
fun deleteMarker(marker: Marker) {
animatingMarkers[marker.id]?.let {
it.cancel()
animatingMarkers.remove(marker.id)
}
marker.remove()
}
fun animateMarkerBounce(marker: Marker) {
animatingMarkers[marker]?.cancel()
animatingMarkers.remove(marker)
animatingMarkers[marker.id]?.let {
it.cancel()
animatingMarkers.remove(marker.id)
}
val anim = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 700
interpolator = BounceInterpolator()
addUpdateListener { state ->
if (!marker.isVisible) {
cancel()
animatingMarkers.remove(marker)
return@addUpdateListener
}
val t = max(1f - state.animatedValue as Float, 0f) / 2
marker.setAnchor(0.5f, 1.0f + t)
}
addListener(onEnd = {
animatingMarkers.remove(marker.id)
}, onCancel = {
animatingMarkers.remove(marker.id)
})
}
animatingMarkers[marker] = anim
animatingMarkers[marker.id] = anim
anim.start()
}
}

View File

@@ -0,0 +1,14 @@
package net.vonforst.evmap.ui
import androidx.appcompat.app.AppCompatDelegate
import net.vonforst.evmap.storage.PreferenceDataSource
fun updateNightMode(prefs: PreferenceDataSource) {
AppCompatDelegate.setDefaultNightMode(
when (prefs.darkmode) {
"on" -> AppCompatDelegate.MODE_NIGHT_YES
"off" -> AppCompatDelegate.MODE_NIGHT_NO
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
)
}

View File

@@ -1,6 +1,5 @@
package net.vonforst.evmap.utils
import android.annotation.TargetApi
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
@@ -33,10 +32,5 @@ class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
}
return LocaleContextWrapper(ctx)
}
@TargetApi(Build.VERSION_CODES.N)
fun setSystemLocale(config: Configuration, locale: Locale?) {
config.setLocale(locale)
}
}
}

View File

@@ -76,12 +76,14 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
MultipleChoiceFilter(
application.getString(R.string.filter_connectors), "connectors",
plugMap,
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO)
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO),
manyChoices = true
),
SliderFilter(
application.getString(R.string.filter_min_connectors),
"min_connectors",
10
10,
min = 1
),
MultipleChoiceFilter(
application.getString(R.string.filter_networks), "networks",
@@ -185,6 +187,7 @@ data class SliderFilter(
override val name: String,
override val key: String,
val max: Int,
val min: Int = 0,
val mapping: ((Int) -> Int) = { it },
val inverseMapping: ((Int) -> Int) = { it },
val unit: String? = ""

View File

@@ -93,6 +93,12 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
}
val filteredConnectors: MutableLiveData<Set<String>> by lazy {
MutableLiveData<Set<String>>()
}
val filteredChargeCards: MutableLiveData<Set<Long>> by lazy {
MutableLiveData<Set<Long>>()
}
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
MutableLiveData<ChargeLocation>()
@@ -200,10 +206,15 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
chargepointLoader?.cancel()
chargepoints.value = Resource.loading(chargepoints.value?.data)
filteredConnectors.value = null
filteredChargeCards.value = null
val bounds = mapPosition.bounds
val zoom = mapPosition.zoom
chargepointLoader = viewModelScope.launch {
chargepoints.value = getChargepointsWithFilters(bounds, zoom, filters)
val result = getChargepointsWithFilters(bounds, zoom, filters)
filteredConnectors.value = result.second
filteredChargeCards.value = result.third
chargepoints.value = result.first
}
}
@@ -211,7 +222,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
bounds: LatLngBounds,
zoom: Float,
filters: List<FilterWithValue<out FilterValue>>
): Resource<List<ChargepointListItem>> {
): Triple<Resource<List<ChargepointListItem>>, Set<String>?, Set<Long>?> {
val freecharging = getBooleanValue(filters, "freecharging")
val freeparking = getBooleanValue(filters, "freeparking")
val open247 = getBooleanValue(filters, "open_247")
@@ -223,21 +234,24 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
val connectorsVal = getMultipleChoiceValue(filters, "connectors")
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
return Triple(Resource.success(emptyList()), null, null)
}
val connectors = formatMultipleChoice(connectorsVal)
val filteredConnectors = if (connectorsVal.all) null else connectorsVal.values
val chargeCardsVal = getMultipleChoiceValue(filters, "chargecards")
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
// no chargeCards chosen
return Resource.success(emptyList())
return Triple(Resource.success(emptyList()), filteredConnectors, null)
}
val chargeCards = formatMultipleChoice(chargeCardsVal)
val filteredChargeCards =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }.toSet()
val networksVal = getMultipleChoiceValue(filters, "networks")
if (networksVal.values.isEmpty() && !networksVal.all) {
// no networks chosen
return Resource.success(emptyList())
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
}
val networks = formatMultipleChoice(networksVal)
@@ -272,14 +286,22 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
startkey = startkey
)
if (!response.isSuccessful || response.body()!!.status != "ok") {
return Resource.error(response.message(), chargepoints.value?.data)
return Triple(
Resource.error(response.message(), chargepoints.value?.data),
null,
null
)
} else {
val body = response.body()!!
data.addAll(body.chargelocations)
startkey = body.startkey
}
} catch (e: IOException) {
return Resource.error(e.message, chargepoints.value?.data)
return Triple(
Resource.error(e.message, chargepoints.value?.data),
filteredConnectors,
filteredChargeCards
)
}
} while (startkey != null && startkey < 10000)
@@ -301,7 +323,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
return Resource.success(result)
return Triple(Resource.success(result), filteredConnectors, filteredChargeCards)
}
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =
@@ -317,6 +339,11 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
key: String
) = (filters.find { it.value.key == key }!!.value as SliderFilterValue).value
private fun getMultipleChoiceFilter(
filters: List<FilterWithValue<out FilterValue>>,
key: String
) = filters.find { it.value.key == key }!!.filter as MultipleChoiceFilter
private fun getMultipleChoiceValue(
filters: List<FilterWithValue<out FilterValue>>,
key: String

View File

@@ -15,6 +15,8 @@
<import type="net.vonforst.evmap.adapter.DataBindingAdaptersKt" />
<import type="net.vonforst.evmap.adapter.DetailsAdapterKt" />
<import type="net.vonforst.evmap.viewmodel.Resource" />
<import type="net.vonforst.evmap.viewmodel.Status" />
@@ -31,6 +33,10 @@
name="chargeCards"
type="java.util.Map&lt;Long, ChargeCard&gt;" />
<variable
name="filteredChargeCards"
type="java.util.Set&lt;Long&gt;" />
</data>
<androidx.cardview.widget.CardView
@@ -188,7 +194,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:nestedScrollingEnabled="false"
app:data="@{DataBindingAdaptersKt.buildDetails(charger.data, chargeCards, context)}"
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, context)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -86,7 +86,9 @@
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -127,7 +127,8 @@
layout="@layout/detail_view"
app:charger="@{vm.charger}"
app:availability="@{vm.availability}"
app:chargeCards="@{vm.chargeCardMap}" />
app:chargeCards="@{vm.chargeCardMap}"
app:filteredChargeCards="@{vm.filteredChargeCards}" />
</androidx.core.widget.NestedScrollView>

View File

@@ -8,7 +8,7 @@
<variable
name="item"
type="net.vonforst.evmap.adapter.DetailAdapter.Detail" />
type="net.vonforst.evmap.adapter.DetailsAdapter.Detail" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout

View File

@@ -13,7 +13,7 @@
<variable
name="item"
type="net.vonforst.evmap.adapter.DetailAdapter.Detail" />
type="net.vonforst.evmap.adapter.DetailsAdapter.Detail" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout

View File

@@ -49,7 +49,7 @@
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:max="@{((SliderFilter) item.filter).max}"
android:max="@{((SliderFilter) item.filter).max - ((SliderFilter) item.filter).min}"
android:progress="@={progress}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/textView18"

View File

@@ -5,4 +5,9 @@
<item>Englisch</item>
<item>Deutsch</item>
</string-array>
<string-array name="pref_darkmode_names">
<item>Geräteeinstellung verwenden</item>
<item>immer an</item>
<item>immer aus</item>
</string-array>
</resources>

View File

@@ -25,8 +25,8 @@
<string name="realtime_data_source">Quelle Echtzeitdaten (beta): %s</string>
<string name="go_to_goingelectric">Quelle: goingelectric.de</string>
<string name="search">Suche</string>
<string name="menu_map">Map</string>
<string name="menu_favs">Favorites</string>
<string name="menu_map">Karte</string>
<string name="menu_favs">Favoriten</string>
<string name="menu_filter">Filtern</string>
<string name="not_implemented">noch nicht implementiert</string>
<string name="about">Über EVMap</string>
@@ -92,6 +92,8 @@
<string name="ok">OK</string>
<string name="pref_language">Sprache</string>
<string name="pref_language_summary">App-Sprache ändern</string>
<string name="pref_darkmode">Dunkles Design</string>
<string name="pref_darkmode_summary">Einstellen, wann der Nachtmodus genutzt wird</string>
<string name="connection_error">Ladesäulen konnten nicht geladen werden</string>
<string name="retry">Wiederholen</string>
<string name="filter_open_247">24 Stunden geöffnet</string>

View File

@@ -10,4 +10,14 @@
<item>en</item>
<item>de</item>
</string-array>
<string-array name="pref_darkmode_names">
<item>Device default</item>
<item>always on</item>
<item>always off</item>
</string-array>
<string-array name="pref_darkmode_values" tranlatable="false">
<item>default</item>
<item>on</item>
<item>off</item>
</string-array>
</resources>

View File

@@ -91,6 +91,8 @@
<string name="ok">OK</string>
<string name="pref_language">Language</string>
<string name="pref_language_summary">Change the app language</string>
<string name="pref_darkmode">Dark mode</string>
<string name="pref_darkmode_summary">Set when dark mode is activated</string>
<string name="connection_error">Could not load charging stations</string>
<string name="retry">Retry</string>
<string name="filter_open_247">Available 24/7</string>

View File

@@ -18,5 +18,13 @@
android:defaultValue="default"
android:summary="@string/pref_language_summary" />
<ListPreference
android:key="darkmode"
android:title="@string/pref_darkmode"
android:entries="@array/pref_darkmode_names"
android:entryValues="@array/pref_darkmode_values"
android:defaultValue="default"
android:summary="@string/pref_darkmode_summary" />
</PreferenceCategory>
</PreferenceScreen>