mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 16:17:45 -05:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb88c983e | ||
|
|
d460c34219 | ||
|
|
e91b7d26f8 | ||
|
|
12e41bc38f | ||
|
|
ea94f67187 | ||
|
|
9ad2f86b39 | ||
|
|
d71e781c26 | ||
|
|
03410a4c49 | ||
|
|
3488e89dbc | ||
|
|
ddbc63ae2a | ||
|
|
ee78ca31fe | ||
|
|
f79bd78a5d | ||
|
|
374402c43a | ||
|
|
c82e12bb47 | ||
|
|
02d24a3b3f | ||
|
|
4031c8f142 | ||
|
|
d0851be528 | ||
|
|
2bd57f85d8 | ||
|
|
eccd29b368 | ||
|
|
4b1a3c424f | ||
|
|
391bb094e0 | ||
|
|
d6d5ab05a9 | ||
|
|
be29316329 | ||
|
|
513d3ce4fb | ||
|
|
7da49b256e | ||
|
|
e4932f2e4c | ||
|
|
5c25ba1eca | ||
|
|
09880a58f4 | ||
|
|
cb6abe53fc | ||
|
|
22744da54b | ||
|
|
8fabfd6aa6 | ||
|
|
e44903ff3b |
@@ -28,4 +28,5 @@ deploy:
|
||||
file: app/build/outputs/apk/release/app-release.apk
|
||||
on:
|
||||
repo: johan12345/EVMap
|
||||
tags: true
|
||||
skip_cleanup: 'true'
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 4
|
||||
versionName "0.0.4"
|
||||
versionCode 5
|
||||
versionName "0.0.5"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -83,7 +83,7 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.maps.android:android-maps-utils:0.5'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:73dd449f6f'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
|
||||
implementation 'com.google.android.gms:play-services-maps:17.0.0'
|
||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
implementation 'com.google.android.libraries.places:places:2.2.0'
|
||||
@@ -94,9 +94,10 @@ dependencies {
|
||||
implementation 'com.github.MikeOrtiz:TouchImageView:2.3.3'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
|
||||
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
|
||||
implementation 'com.airbnb.android:lottie:3.4.0'
|
||||
|
||||
// navigation library
|
||||
def nav_version = "2.3.0-alpha05"
|
||||
def nav_version = "2.3.0-alpha06"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
@@ -111,6 +112,11 @@ dependencies {
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
|
||||
// billing library
|
||||
def billing_version = "2.2.0"
|
||||
implementation "com.android.billingclient:billing:$billing_version"
|
||||
implementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
|
||||
// debug tools
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
|
||||
|
||||
17
app/src/main/java/net/vonforst/evmap/Utils.kt
Normal file
17
app/src/main/java/net/vonforst/evmap/Utils.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
fun Bundle.optDouble(name: String): Double? {
|
||||
if (!this.containsKey(name)) return null
|
||||
|
||||
val dbl = this.getDouble(name, Double.NaN)
|
||||
return if (dbl.isNaN()) null else dbl
|
||||
}
|
||||
|
||||
fun Bundle.optLong(name: String): Long? {
|
||||
if (!this.containsKey(name)) return null
|
||||
|
||||
val lng = this.getLong(name, Long.MIN_VALUE)
|
||||
return if (lng == Long.MIN_VALUE) null else lng
|
||||
}
|
||||
@@ -2,18 +2,22 @@ package net.vonforst.evmap.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SeekBar
|
||||
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.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
|
||||
@@ -24,6 +28,8 @@ interface Equatable {
|
||||
abstract class DataBindingAdapter<T : Equatable>() :
|
||||
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback()) {
|
||||
|
||||
var onClickListener: ((T) -> Unit)? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<T> {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
val binding =
|
||||
@@ -41,6 +47,10 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
class DiffCallback<T : Equatable> : DiffUtil.ItemCallback<T>() {
|
||||
@@ -55,14 +65,16 @@ fun chargepointWithAvailability(
|
||||
availability: Map<Chargepoint, List<ChargepointStatus>>?
|
||||
): List<ConnectorAdapter.ChargepointWithAvailability>? {
|
||||
return chargepoints?.map {
|
||||
ConnectorAdapter.ChargepointWithAvailability(
|
||||
it, availability?.get(it)?.count { it == ChargepointStatus.AVAILABLE }
|
||||
)
|
||||
val status = availability?.get(it)
|
||||
ConnectorAdapter.ChargepointWithAvailability(it, status)
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvailability>() {
|
||||
data class ChargepointWithAvailability(val chargepoint: Chargepoint, val available: Int?) :
|
||||
data class ChargepointWithAvailability(
|
||||
val chargepoint: Chargepoint,
|
||||
val status: List<ChargepointStatus>?
|
||||
) :
|
||||
Equatable
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_connector
|
||||
@@ -146,7 +158,7 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (getItem(position).filter) {
|
||||
is BooleanFilter -> R.layout.item_filter_boolean
|
||||
is MultipleChoiceFilter -> R.layout.item_filter_boolean
|
||||
is MultipleChoiceFilter -> R.layout.item_filter_multiple_choice
|
||||
is SliderFilter -> R.layout.item_filter_slider
|
||||
}
|
||||
|
||||
@@ -157,29 +169,125 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
super.bind(holder, item)
|
||||
when (item.value) {
|
||||
is SliderFilterValue -> {
|
||||
val binding = holder.binding as ItemFilterSliderBinding
|
||||
binding.progress = item.value.value
|
||||
binding.seekBar.setOnSeekBarChangeListener(object :
|
||||
SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(
|
||||
seekBar: SeekBar,
|
||||
progress: Int,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
item.value.value = progress
|
||||
binding.progress = progress
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
}
|
||||
})
|
||||
setupSlider(
|
||||
holder.binding as ItemFilterSliderBinding,
|
||||
item.filter as SliderFilter, item.value
|
||||
)
|
||||
}
|
||||
is MultipleChoiceFilterValue -> {
|
||||
setupMultipleChoice(
|
||||
holder.binding as ItemFilterMultipleChoiceBinding,
|
||||
item.filter as MultipleChoiceFilter, 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 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]
|
||||
|
||||
@@ -40,7 +40,7 @@ interface NewMotionApi {
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NMEvse(val evseId: String, val status: String, val connectors: List<NMConnector>)
|
||||
data class NMEvse(val evseId: String?, val status: String, val connectors: List<NMConnector>)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NMConnector(
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package net.vonforst.evmap.api.goingelectric
|
||||
|
||||
import android.content.Context
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
@@ -11,7 +14,7 @@ import retrofit2.http.Query
|
||||
|
||||
interface GoingElectricApi {
|
||||
@GET("chargepoints/")
|
||||
fun getChargepoints(
|
||||
suspend fun getChargepoints(
|
||||
@Query("sw_lat") swlat: Double, @Query("sw_lng") sw_lng: Double,
|
||||
@Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double,
|
||||
@Query("clustering") clustering: Boolean,
|
||||
@@ -19,27 +22,43 @@ interface GoingElectricApi {
|
||||
@Query("cluster_distance") clusterDistance: Int,
|
||||
@Query("freecharging") freecharging: Boolean,
|
||||
@Query("freeparking") freeparking: Boolean,
|
||||
@Query("min_power") minPower: Int
|
||||
): Call<ChargepointList>
|
||||
@Query("min_power") minPower: Int,
|
||||
@Query("plugs") plugs: String?
|
||||
): Response<ChargepointList>
|
||||
|
||||
@GET("chargepoints/")
|
||||
fun getChargepointDetail(@Query("ge_id") id: Long): Call<ChargepointList>
|
||||
|
||||
@GET("chargepoints/pluglist/")
|
||||
suspend fun getPlugs(): Response<StringList>
|
||||
|
||||
@GET("chargepoints/networklist/")
|
||||
suspend fun getNetworks(): Response<StringList>
|
||||
|
||||
@GET("chargepoints/chargecardlist/")
|
||||
suspend fun getChargeCards(): Response<StringList>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 10L * 1024 * 1024; // 10MB
|
||||
|
||||
fun create(
|
||||
apikey: String,
|
||||
baseurl: String = "https://api.goingelectric.de"
|
||||
baseurl: String = "https://api.goingelectric.de",
|
||||
context: Context? = null
|
||||
): GoingElectricApi {
|
||||
val client = OkHttpClient.Builder()
|
||||
.addInterceptor { chain ->
|
||||
val client = OkHttpClient.Builder().apply {
|
||||
addInterceptor { chain ->
|
||||
// add API key to every request
|
||||
var original = chain.request()
|
||||
val url = original.url().newBuilder().addQueryParameter("key", apikey).build()
|
||||
original = original.newBuilder().url(url).build()
|
||||
chain.proceed(original)
|
||||
}
|
||||
.addNetworkInterceptor(StethoInterceptor())
|
||||
.build()
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
if (context != null) {
|
||||
cache(Cache(context.getCacheDir(), cacheSize))
|
||||
}
|
||||
}.build()
|
||||
|
||||
val moshi = Moshi.Builder()
|
||||
.add(ChargepointListItemJsonAdapterFactory())
|
||||
|
||||
@@ -24,6 +24,12 @@ data class ChargepointList(
|
||||
val chargelocations: List<ChargepointListItem>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StringList(
|
||||
val status: String,
|
||||
val result: List<String>
|
||||
)
|
||||
|
||||
sealed class ChargepointListItem
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -204,13 +210,13 @@ data class Coordinate(val lat: Double, val lng: Double) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Address(
|
||||
val city: String,
|
||||
val country: String,
|
||||
val postcode: String,
|
||||
val street: String
|
||||
@JsonObjectOrFalse val city: String?,
|
||||
@JsonObjectOrFalse val country: String?,
|
||||
@JsonObjectOrFalse val postcode: String?,
|
||||
@JsonObjectOrFalse val street: String?
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "$street, $postcode $city"
|
||||
return "${street ?: ""}, ${postcode ?: ""} ${city ?: ""}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
setPreferencesFromResource(R.xml.about, rootKey)
|
||||
|
||||
findPreference<Preference>("version")?.summary = BuildConfig.VERSION_NAME
|
||||
|
||||
//TODO: disable donations until fully implemented
|
||||
findPreference<Preference>("donate")?.isVisible = false
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
||||
@@ -51,6 +54,10 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
.start(requireActivity())
|
||||
true
|
||||
}
|
||||
"donate" -> {
|
||||
findNavController().navigate(R.id.action_about_to_donateFragment)
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.FragmentDonateBinding
|
||||
import net.vonforst.evmap.viewmodel.DonateViewModel
|
||||
|
||||
class DonateFragment : Fragment() {
|
||||
private lateinit var binding: FragmentDonateBinding
|
||||
private val vm: DonateViewModel by viewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_donate, container, false)
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
return binding.root
|
||||
}
|
||||
|
||||
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(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -66,8 +66,13 @@ class FavoritesFragment : Fragment() {
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
val favAdapter = FavoritesAdapter(vm).apply {
|
||||
onClickListener = {
|
||||
navController.navigate(R.id.action_favs_to_map, MapFragment.showCharger(it.charger))
|
||||
}
|
||||
}
|
||||
binding.favsList.apply {
|
||||
adapter = FavoritesAdapter(vm)
|
||||
adapter = favAdapter
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
addItemDecoration(
|
||||
|
||||
@@ -92,8 +92,8 @@ class FilterFragment : Fragment() {
|
||||
R.id.menu_apply -> {
|
||||
lifecycleScope.launch {
|
||||
vm.saveFilterValues()
|
||||
exitAfterTransition()
|
||||
}
|
||||
exitAfterTransition()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
|
||||
@@ -8,8 +8,10 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.view.*
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -42,9 +44,7 @@ import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STAT
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
|
||||
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
|
||||
import kotlinx.android.synthetic.main.fragment_map.*
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.REQUEST_LOCATION_PERMISSION
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.DetailAdapter
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
@@ -56,12 +56,12 @@ import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.ClusterIconGenerator
|
||||
import net.vonforst.evmap.ui.MarkerAnimator
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.viewmodel.GalleryViewModel
|
||||
import net.vonforst.evmap.viewmodel.MapPosition
|
||||
import net.vonforst.evmap.viewmodel.MapViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
|
||||
const val REQUEST_AUTOCOMPLETE = 2
|
||||
const val ARG_CHARGER_ID = "chargerId"
|
||||
const val ARG_LAT = "lat"
|
||||
const val ARG_LON = "lon"
|
||||
|
||||
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback {
|
||||
private lateinit var binding: FragmentMapBinding
|
||||
@@ -307,6 +307,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
startPostponedEnterTransition()
|
||||
} else {
|
||||
binding.gallery.scrollToPosition(galleryPosition)
|
||||
// make sure that the app does not freeze waiting for a picture to load
|
||||
Handler().postDelayed({
|
||||
startPostponedEnterTransition()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
binding.detailView.connectors.apply {
|
||||
@@ -357,6 +361,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.chargerSparse.value = null
|
||||
}
|
||||
|
||||
// set padding so that compass is not obstructed by toolbar
|
||||
map.setPadding(0, binding.toolbarContainer.height, 0, 0)
|
||||
|
||||
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
map.setMapStyle(
|
||||
if (mode == Configuration.UI_MODE_NIGHT_YES) {
|
||||
@@ -366,23 +373,51 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
|
||||
val position = vm.mapPosition.value
|
||||
if (hasLocationPermission()) {
|
||||
enableLocation(position == null, false)
|
||||
} else if (position == null) {
|
||||
// center the camera on Europe
|
||||
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f)
|
||||
map.moveCamera(cameraUpdate)
|
||||
}
|
||||
val lat = arguments?.optDouble(ARG_LAT)
|
||||
val lon = arguments?.optDouble(ARG_LON)
|
||||
var positionSet = false
|
||||
|
||||
if (position != null) {
|
||||
val cameraUpdate =
|
||||
CameraUpdateFactory.newLatLngZoom(position.bounds.center, position.zoom)
|
||||
map.moveCamera(cameraUpdate)
|
||||
} else {
|
||||
vm.mapPosition.value = MapPosition(
|
||||
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
|
||||
)
|
||||
positionSet = true
|
||||
} else if (lat != null && lon != null) {
|
||||
// show given position
|
||||
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(lat, lon), 16f)
|
||||
map.moveCamera(cameraUpdate)
|
||||
|
||||
// show charger detail after chargers were loaded
|
||||
val chargerId = arguments?.optLong(ARG_CHARGER_ID)
|
||||
vm.chargepoints.observe(
|
||||
viewLifecycleOwner,
|
||||
object : Observer<Resource<List<ChargepointListItem>>> {
|
||||
override fun onChanged(res: Resource<List<ChargepointListItem>>) {
|
||||
if (res.data == null) return
|
||||
for (item in res.data) {
|
||||
if (item is ChargeLocation && item.id == chargerId) {
|
||||
vm.chargerSparse.value = item
|
||||
vm.chargepoints.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
positionSet = true
|
||||
}
|
||||
if (hasLocationPermission()) {
|
||||
enableLocation(!positionSet, false)
|
||||
positionSet = true
|
||||
}
|
||||
if (!positionSet) {
|
||||
// center the camera on Europe
|
||||
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f)
|
||||
map.moveCamera(cameraUpdate)
|
||||
}
|
||||
|
||||
vm.mapPosition.value = MapPosition(
|
||||
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@@ -476,7 +511,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.map, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
|
||||
val filterItem = menu.findItem(R.id.menu_filter)
|
||||
val filterView = filterItem.actionView
|
||||
|
||||
val filterBadge = filterView?.findViewById<TextView>(R.id.filter_badge)
|
||||
if (filterBadge != null) {
|
||||
// set up badge showing number of active filters
|
||||
vm.filtersCount.observe(viewLifecycleOwner, Observer {
|
||||
filterBadge.visibility = if (it > 0) View.VISIBLE else View.GONE
|
||||
filterBadge.text = it.toString()
|
||||
})
|
||||
}
|
||||
filterView?.setOnClickListener {
|
||||
onOptionsItemSelected(filterItem)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -523,4 +572,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
sharedElements[names[0]] = vh.itemView
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun showCharger(charger: ChargeLocation): Bundle {
|
||||
return Bundle().apply {
|
||||
putLong(ARG_CHARGER_ID, charger.id)
|
||||
putDouble(ARG_LAT, charger.coordinates.lat)
|
||||
putDouble(ARG_LON, charger.coordinates.lng)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,19 +17,21 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
|
||||
ChargeLocation::class,
|
||||
BooleanFilterValue::class,
|
||||
MultipleChoiceFilterValue::class,
|
||||
SliderFilterValue::class
|
||||
], version = 2
|
||||
SliderFilterValue::class,
|
||||
Plug::class
|
||||
], version = 5
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun chargeLocationsDao(): ChargeLocationsDao
|
||||
abstract fun filterValueDao(): FilterValueDao
|
||||
abstract fun plugDao(): PlugDao
|
||||
|
||||
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)
|
||||
.addMigrations(MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -46,5 +48,35 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))");
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_3 = object : Migration(2, 3) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// recreate ChargeLocation table to make postcode nullable
|
||||
db.beginTransaction()
|
||||
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT NOT NULL, `country` TEXT NOT NULL, `postcode` TEXT, `street` TEXT NOT NULL, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))");
|
||||
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`");
|
||||
db.execSQL("DROP TABLE `ChargeLocation`")
|
||||
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_4 = object : Migration(3, 4) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `Plug` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_5 = object : Migration(4, 5) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// recreate ChargeLocation table to make other address fields nullable
|
||||
db.beginTransaction()
|
||||
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))");
|
||||
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`");
|
||||
db.execSQL("DROP TABLE `ChargeLocation`")
|
||||
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
app/src/main/java/net/vonforst/evmap/storage/PlugDao.kt
Normal file
49
app/src/main/java/net/vonforst/evmap/storage/PlugDao.kt
Normal 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 Plug(@PrimaryKey val name: String)
|
||||
|
||||
@Dao
|
||||
interface PlugDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg plugs: Plug)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg plugs: Plug)
|
||||
|
||||
@Query("SELECT * FROM plug")
|
||||
fun getAllPlugs(): LiveData<List<Plug>>
|
||||
}
|
||||
|
||||
class PlugRepository(
|
||||
private val api: GoingElectricApi, private val scope: CoroutineScope,
|
||||
private val dao: PlugDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getPlugs(): LiveData<List<Plug>> {
|
||||
scope.launch {
|
||||
updatePlugs()
|
||||
}
|
||||
return dao.getAllPlugs()
|
||||
}
|
||||
|
||||
private suspend fun updatePlugs() {
|
||||
if (Duration.between(prefs.lastPlugUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
val response = api.getPlugs()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Plug(name))
|
||||
}
|
||||
|
||||
prefs.lastPlugUpdate = Instant.now()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package net.vonforst.evmap.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.time.Instant
|
||||
|
||||
class PreferenceDataSource(context: Context) {
|
||||
val sp = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@@ -11,4 +12,10 @@ class PreferenceDataSource(context: Context) {
|
||||
set(value) {
|
||||
sp.edit().putBoolean("navigate_use_maps", value).apply()
|
||||
}
|
||||
|
||||
var lastPlugUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_plug_update", 0L))
|
||||
set(value) {
|
||||
sp.edit().putLong("last_plug_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
|
||||
|
||||
@@ -82,25 +83,30 @@ fun setContentDescriptionResource(imageView: ImageView, resource: Int) {
|
||||
}
|
||||
|
||||
@BindingAdapter("tintAvailability")
|
||||
fun setImageTintAvailability(view: ImageView, available: Int?) {
|
||||
fun setImageTintAvailability(view: ImageView, available: List<ChargepointStatus>?) {
|
||||
view.imageTintList = ColorStateList.valueOf(availabilityColor(available, view.context))
|
||||
}
|
||||
|
||||
@BindingAdapter("textColorAvailability")
|
||||
fun setTextColorAvailability(view: TextView, available: Int?) {
|
||||
fun setTextColorAvailability(view: TextView, available: List<ChargepointStatus>?) {
|
||||
view.setTextColor(availabilityColor(available, view.context))
|
||||
}
|
||||
|
||||
@BindingAdapter("backgroundTintAvailability")
|
||||
fun setBackgroundTintAvailability(view: View, available: Int?) {
|
||||
fun setBackgroundTintAvailability(view: View, available: List<ChargepointStatus>?) {
|
||||
view.backgroundTintList = ColorStateList.valueOf(availabilityColor(available, view.context))
|
||||
}
|
||||
|
||||
private fun availabilityColor(
|
||||
available: Int?,
|
||||
status: List<ChargepointStatus>?,
|
||||
context: Context
|
||||
): Int = if (available != null) {
|
||||
if (available > 0) {
|
||||
): Int = if (status != null) {
|
||||
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
|
||||
val available = status.count { it == ChargepointStatus.AVAILABLE }
|
||||
|
||||
if (unknown) {
|
||||
ContextCompat.getColor(context, R.color.unknown)
|
||||
} else if (available > 0) {
|
||||
ContextCompat.getColor(context, R.color.available)
|
||||
} else {
|
||||
ContextCompat.getColor(context, R.color.unavailable)
|
||||
@@ -108,4 +114,16 @@ private fun availabilityColor(
|
||||
} else {
|
||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorControlNormal))
|
||||
ta.getColor(0, 0)
|
||||
}
|
||||
|
||||
fun availabilityText(status: List<ChargepointStatus>?): String? {
|
||||
if (status == null) return null
|
||||
|
||||
val total = status.size
|
||||
val unknown = status.count { it == ChargepointStatus.UNKNOWN }
|
||||
val available = status.count { it == ChargepointStatus.AVAILABLE }
|
||||
|
||||
return if (unknown > 0) {
|
||||
if (unknown == total) "?" else "$available?"
|
||||
} else available.toString()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.android.billingclient.api.*
|
||||
|
||||
class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
PurchasesUpdatedListener {
|
||||
private var billingClient = BillingClient.newBuilder(application)
|
||||
.setListener(this)
|
||||
.enablePendingPurchases()
|
||||
.build()
|
||||
|
||||
val products: MutableLiveData<List<SkuDetails>> by lazy {
|
||||
MutableLiveData<List<SkuDetails>>().apply {
|
||||
value = null
|
||||
|
||||
val params = SkuDetailsParams.newBuilder().setType(BillingClient.SkuType.INAPP).build()
|
||||
billingClient.querySkuDetailsAsync(params) { result, details ->
|
||||
value = details
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import net.vonforst.evmap.storage.AppDatabase
|
||||
|
||||
class FavoritesViewModel(application: Application, geApiKey: String) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey)
|
||||
private var api = GoingElectricApi.create(geApiKey, context = application)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
|
||||
val favorites: LiveData<List<ChargeLocation>> by lazy {
|
||||
@@ -81,18 +81,18 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
|
||||
|
||||
data class FavoritesListItem(
|
||||
val charger: ChargeLocation,
|
||||
val available: Resource<Int>,
|
||||
val available: Resource<List<ChargepointStatus>>,
|
||||
val total: Int,
|
||||
val distance: Double?
|
||||
) : Equatable
|
||||
|
||||
private fun totalAvailable(id: Long): Resource<Int> {
|
||||
private fun totalAvailable(id: Long): Resource<List<ChargepointStatus>> {
|
||||
val availability = availability.value?.get(id) ?: return Resource.error(null, null)
|
||||
if (availability.status != Status.SUCCESS) {
|
||||
return Resource(availability.status, null, availability.message)
|
||||
} else {
|
||||
val values = availability.data?.status?.values ?: return Resource.error(null, null)
|
||||
return Resource.success(values.sumBy { it.filter { it == ChargepointStatus.AVAILABLE }.size })
|
||||
return Resource.success(values.flatten())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,48 +5,111 @@ import androidx.databinding.BaseObservable
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.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 kotlin.math.abs
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.cast
|
||||
|
||||
fun getFilters(application: Application): List<Filter<FilterValue>> {
|
||||
return 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", 350)
|
||||
)
|
||||
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 100, 150, 200, 250, 300, 350)
|
||||
internal fun mapPower(i: Int) = powerSteps[i]
|
||||
internal fun mapPowerInverse(power: Int) = powerSteps
|
||||
.mapIndexed { index, v -> abs(v - power) to index }
|
||||
.minBy { it.first }?.second ?: 0
|
||||
|
||||
internal fun getFilters(
|
||||
application: Application,
|
||||
plugs: LiveData<List<Plug>>
|
||||
): LiveData<List<Filter<FilterValue>>> {
|
||||
return MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
val plugNames = mapOf(
|
||||
Chargepoint.TYPE_1 to application.getString(R.string.plug_type_1),
|
||||
Chargepoint.TYPE_2 to application.getString(R.string.plug_type_2),
|
||||
Chargepoint.TYPE_3 to application.getString(R.string.plug_type_3),
|
||||
Chargepoint.CCS to application.getString(R.string.plug_ccs),
|
||||
Chargepoint.SCHUKO to application.getString(R.string.plug_schuko),
|
||||
Chargepoint.CHADEMO to application.getString(R.string.plug_chademo),
|
||||
Chargepoint.SUPERCHARGER to application.getString(R.string.plug_supercharger),
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal fun filtersWithValue(
|
||||
filters: LiveData<List<Filter<FilterValue>>>,
|
||||
filterValues: LiveData<List<FilterValue>>
|
||||
): MediatorLiveData<List<FilterWithValue<out FilterValue>>> =
|
||||
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
|
||||
listOf(filters, filterValues).forEach {
|
||||
addSource(it) {
|
||||
val filters = filters.value ?: return@addSource
|
||||
val values = filterValues.value ?: return@addSource
|
||||
value = filters.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FilterViewModel(application: Application, geApiKey: String) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey)
|
||||
private var api = GoingElectricApi.create(geApiKey, context = application)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
|
||||
private val filters = getFilters(application)
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
}
|
||||
|
||||
private val filters: LiveData<List<Filter<FilterValue>>> by lazy {
|
||||
getFilters(application, plugs)
|
||||
}
|
||||
|
||||
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 {
|
||||
addSource(filterValues) { values ->
|
||||
value = if (values != null) {
|
||||
filters.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
filtersWithValue(filters, filterValues)
|
||||
}
|
||||
|
||||
suspend fun saveFilterValues() {
|
||||
@@ -72,16 +135,20 @@ data class BooleanFilter(override val name: String, override val key: String) :
|
||||
data class MultipleChoiceFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val choices: Map<String, String>
|
||||
val choices: Map<String, String>,
|
||||
val commonChoices: Set<String>? = null
|
||||
) : Filter<MultipleChoiceFilterValue>() {
|
||||
override val valueClass: KClass<MultipleChoiceFilterValue> = MultipleChoiceFilterValue::class
|
||||
override fun defaultValue() = MultipleChoiceFilterValue(key, emptySet(), true)
|
||||
override fun defaultValue() = MultipleChoiceFilterValue(key, mutableSetOf(), true)
|
||||
}
|
||||
|
||||
data class SliderFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val max: Int
|
||||
val max: Int,
|
||||
val mapping: ((Int) -> Int) = { it },
|
||||
val inverseMapping: ((Int) -> Int) = { it },
|
||||
val unit: String? = ""
|
||||
) : Filter<SliderFilterValue>() {
|
||||
override val valueClass: KClass<SliderFilterValue> = SliderFilterValue::class
|
||||
override fun defaultValue() = SliderFilterValue(key, 0)
|
||||
@@ -100,9 +167,20 @@ data class BooleanFilterValue(
|
||||
@Entity
|
||||
data class MultipleChoiceFilterValue(
|
||||
@PrimaryKey override val key: String,
|
||||
var values: Set<String>,
|
||||
var values: MutableSet<String>,
|
||||
var all: Boolean
|
||||
) : FilterValue()
|
||||
) : FilterValue() {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other !is MultipleChoiceFilterValue) return false
|
||||
if (key != other.key) return false
|
||||
|
||||
return if (all) {
|
||||
other.all
|
||||
} else {
|
||||
!other.all && values == other.values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
data class SliderFilterValue(
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.viewmodel
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.*
|
||||
import com.google.android.gms.maps.model.LatLngBounds
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
@@ -11,16 +12,20 @@ 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 retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import kotlin.reflect.full.cast
|
||||
|
||||
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
|
||||
|
||||
class MapViewModel(application: Application, geApiKey: String) : AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey)
|
||||
private var api = GoingElectricApi.create(geApiKey, context = application)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
private var chargepointLoader: Job? = null
|
||||
|
||||
val bottomSheetState: MutableLiveData<Int> by lazy {
|
||||
MutableLiveData<Int>()
|
||||
@@ -32,20 +37,21 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
db.filterValueDao().getFilterValues()
|
||||
}
|
||||
private val filters = getFilters(application)
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
}
|
||||
private val filters = getFilters(application, plugs)
|
||||
|
||||
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
|
||||
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
|
||||
addSource(filterValues) {
|
||||
val values = filterValues.value
|
||||
if (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
|
||||
filtersWithValue(filters, filterValues)
|
||||
}
|
||||
|
||||
val filtersCount: LiveData<Int> by lazy {
|
||||
MediatorLiveData<Int>().apply {
|
||||
value = 0
|
||||
addSource(filtersWithValue) { filtersWithValue ->
|
||||
value = filtersWithValue.count {
|
||||
it.filter.defaultValue() != it.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,49 +133,66 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
mapPosition: MapPosition,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
) {
|
||||
chargepointLoader?.cancel()
|
||||
|
||||
chargepoints.value = Resource.loading(chargepoints.value?.data)
|
||||
val bounds = mapPosition.bounds
|
||||
val zoom = mapPosition.zoom
|
||||
getChargepointsWithFilters(bounds, zoom, filters).enqueue(object :
|
||||
Callback<ChargepointList> {
|
||||
override fun onFailure(call: Call<ChargepointList>, t: Throwable) {
|
||||
chargepoints.value = Resource.error(t.message, chargepoints.value?.data)
|
||||
t.printStackTrace()
|
||||
}
|
||||
|
||||
override fun onResponse(
|
||||
call: Call<ChargepointList>,
|
||||
response: Response<ChargepointList>
|
||||
) {
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
chargepoints.value =
|
||||
Resource.error(response.message(), chargepoints.value?.data)
|
||||
} else {
|
||||
chargepoints.value = Resource.success(response.body()!!.chargelocations)
|
||||
}
|
||||
}
|
||||
})
|
||||
chargepointLoader = viewModelScope.launch {
|
||||
chargepoints.value = getChargepointsWithFilters(bounds, zoom, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChargepointsWithFilters(
|
||||
private suspend fun getChargepointsWithFilters(
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
): Call<ChargepointList> {
|
||||
): 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
|
||||
|
||||
return api.getChargepoints(
|
||||
val connectorsVal =
|
||||
filters.find { it.value.key == "connectors" }!!.value as MultipleChoiceFilterValue
|
||||
val connectors = if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
|
||||
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
|
||||
// do not use clustering if filters need to be applied locally.
|
||||
val useClustering = minConnectors <= 1
|
||||
|
||||
val response = api.getChargepoints(
|
||||
bounds.southwest.latitude, bounds.southwest.longitude,
|
||||
bounds.northeast.latitude, bounds.northeast.longitude,
|
||||
clustering = zoom < 13, zoom = zoom,
|
||||
clustering = zoom < 13 && useClustering, zoom = zoom,
|
||||
clusterDistance = 70, freecharging = freecharging, minPower = minPower,
|
||||
freeparking = freeparking
|
||||
freeparking = freeparking, plugs = connectors
|
||||
)
|
||||
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Resource.error(response.message(), chargepoints.value?.data)
|
||||
} else {
|
||||
val data = response.body()!!.chargelocations.filter { it ->
|
||||
// apply filters which GoingElectric does not support natively
|
||||
if (it is ChargeLocation) {
|
||||
it.chargepoints
|
||||
.filter { it.power >= minPower }
|
||||
.filter { if (!connectorsVal.all) it.type in connectorsVal.values else true }
|
||||
.sumBy { it.count } >= minConnectors
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
return Resource.success(data)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadAvailability(charger: ChargeLocation) {
|
||||
|
||||
34
app/src/main/res/layout/action_filter.xml
Normal file
34
app/src/main/res/layout/action_filter.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout 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"
|
||||
style="?attr/actionButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:focusable="true">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
app:srcCompat="@drawable/ic_filter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filter_badge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="16dp"
|
||||
android:minWidth="15dp"
|
||||
android:layout_gravity="right|end|bottom"
|
||||
android:layout_marginEnd="-5dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:background="@drawable/rounded_rect_4dp"
|
||||
android:backgroundTint="?colorPrimary"
|
||||
android:gravity="center"
|
||||
android:padding="0dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:visibility="gone"
|
||||
tools:text="0"
|
||||
android:textSize="10sp" />
|
||||
|
||||
</FrameLayout>
|
||||
38
app/src/main/res/layout/fragment_donate.xml
Normal file
38
app/src/main/res/layout/fragment_donate.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.DonateViewModel" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
type="DonateViewModel" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:actionBarSize" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/products_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:data="@{vm.products}" />
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -11,28 +11,73 @@
|
||||
type="FavoritesViewModel" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:actionBarSize" />
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/favs_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:data="@{vm.listData}" />
|
||||
</LinearLayout>
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:data="@{vm.listData}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar_container" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/animation_view"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:goneUnless="@{vm.listData.size() == 0}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/textView19"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.497"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_rawRes="@raw/ev_anim" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView19"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:breakStrategy="balanced"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/favorites_empty_state"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:goneUnless="@{vm.listData.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/animation_view" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -6,6 +6,7 @@
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.adapter.ConnectorAdapter.ChargepointWithAvailability" />
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
@@ -29,7 +30,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tintAvailability="@{item.available}"
|
||||
app:tintAvailability="@{item.status}"
|
||||
tools:tint="@color/available"
|
||||
tools:srcCompat="@drawable/ic_connector_typ2" />
|
||||
|
||||
@@ -43,7 +44,7 @@
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintStart_toStartOf="@+id/imageView"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView"
|
||||
app:goneUnless="@{item.available == null}"
|
||||
app:goneUnless="@{item.status == null}"
|
||||
tools:visibility="gone"
|
||||
tools:text="×99" />
|
||||
|
||||
@@ -53,15 +54,15 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginTop="30dp"
|
||||
android:text="@{String.format("%d/%d", item.available, item.chargepoint.count)}"
|
||||
android:text="@{String.format("%s/%d", BindingAdaptersKt.availabilityText(item.status), item.chargepoint.count)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:background="@drawable/rounded_rect"
|
||||
android:padding="2dp"
|
||||
android:textColor="@android:color/white"
|
||||
app:layout_constraintStart_toStartOf="@+id/imageView"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView"
|
||||
app:goneUnless="@{item.available != null}"
|
||||
app:backgroundTintAvailability="@{item.available}"
|
||||
app:goneUnless="@{item.status != null}"
|
||||
app:backgroundTintAvailability="@{item.status}"
|
||||
tools:backgroundTint="@color/available"
|
||||
tools:text="80/99" />
|
||||
|
||||
@@ -77,7 +78,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||
app:textColorAvailability="@{item.available}"
|
||||
app:textColorAvailability="@{item.status}"
|
||||
tools:textColor="@color/available"
|
||||
tools:text="350 kW" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
android:text="@{item.detailText}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:autoLink="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
|
||||
android:linksClickable="@{item.links}"
|
||||
app:goneUnless="@{item.detailText != null}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
<import type="net.vonforst.evmap.api.UtilsKt" />
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
@@ -16,7 +17,8 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView15"
|
||||
@@ -68,7 +70,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/rounded_rect"
|
||||
android:padding="2dp"
|
||||
android:text="@{String.format("%d/%d", item.available.data, item.total)}"
|
||||
android:text="@{String.format("%s/%d", BindingAdaptersKt.availabilityText(item.available.data), item.total)}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTintAvailability="@{item.available.data}"
|
||||
|
||||
84
app/src/main/res/layout/item_filter_multiple_choice.xml
Normal file
84
app/src/main/res/layout/item_filter_multiple_choice.xml
Normal file
@@ -0,0 +1,84 @@
|
||||
<?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<MultipleChoiceFilterValue>" />
|
||||
|
||||
<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:text="@{item.filter.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Connectors" />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chip_group"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:animateLayoutChanges="true"
|
||||
app:chipSpacingVertical="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btnAll">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipMore"
|
||||
style="@style/Widget.MaterialComponents.Chip.Action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{showingAll ? @string/show_less : @string/show_more}"
|
||||
app:chipMinTouchTargetSize="40dp" />
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
<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/textView17"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnNone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnNone"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog.Flush"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/none"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/textView17"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.chip.Chip 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"
|
||||
style="@style/Widget.MaterialComponents.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:chipMinTouchTargetSize="40dp"
|
||||
tools:text="Typ 2" />
|
||||
@@ -18,6 +18,10 @@
|
||||
<variable
|
||||
name="progress"
|
||||
type="int" />
|
||||
|
||||
<variable
|
||||
name="mappedValue"
|
||||
type="int" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
@@ -46,7 +50,7 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:max="@{((SliderFilter) item.filter).max}"
|
||||
android:progress="@={item.value.value}"
|
||||
android:progress="@={progress}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView18"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -57,7 +61,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@{progress + " kW"}"
|
||||
android:text="@{String.valueOf(mappedValue) + ' ' + ((SliderFilter) item.filter).unit}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/seekBar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/seekBar"
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
android:id="@+id/menu_filter"
|
||||
android:title="@string/menu_filter"
|
||||
android:icon="@drawable/ic_filter"
|
||||
app:showAsAction="ifRoom" />
|
||||
app:showAsAction="ifRoom"
|
||||
app:actionLayout="@layout/action_filter" />
|
||||
</menu>
|
||||
@@ -29,7 +29,11 @@
|
||||
android:id="@+id/about"
|
||||
android:name="net.vonforst.evmap.fragment.AboutFragment"
|
||||
android:label="@string/about"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
tools:layout="@layout/fragment_preference">
|
||||
<action
|
||||
android:id="@+id/action_about_to_donateFragment"
|
||||
app:destination="@id/donate" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/settings"
|
||||
android:name="net.vonforst.evmap.fragment.SettingsFragment"
|
||||
@@ -44,10 +48,19 @@
|
||||
android:id="@+id/favs"
|
||||
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
|
||||
android:label="@string/menu_favs"
|
||||
tools:layout="@layout/fragment_favorites" />
|
||||
tools:layout="@layout/fragment_favorites">
|
||||
<action
|
||||
android:id="@+id/action_favs_to_map"
|
||||
app:destination="@id/map" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/filter"
|
||||
android:name="net.vonforst.evmap.fragment.FilterFragment"
|
||||
android:label="@string/menu_filter"
|
||||
tools:layout="@layout/fragment_filter" />
|
||||
<fragment
|
||||
android:id="@+id/donate"
|
||||
android:name="net.vonforst.evmap.fragment.DonateFragment"
|
||||
android:label="@string/donate"
|
||||
tools:layout="@layout/fragment_donate" />
|
||||
</navigation>
|
||||
4021
app/src/main/res/raw/ev_anim.json
Normal file
4021
app/src/main/res/raw/ev_anim.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -47,4 +47,21 @@
|
||||
<string name="filter_free">Nur kostenlose Ladesäulen</string>
|
||||
<string name="filter_min_power">Minimale Leistung</string>
|
||||
<string name="filter_free_parking">Nur Ladesäulen mit kostenlosem Parkplatz</string>
|
||||
<string name="filter_min_connectors">Mindestzahl Anschlüsse</string>
|
||||
<string name="filter_connectors">Anschlüsse</string>
|
||||
<string name="plug_type_1">Typ 1</string>
|
||||
<string name="plug_type_2">Typ 2</string>
|
||||
<string name="plug_type_3">Typ 3a</string>
|
||||
<string name="plug_ccs">CCS</string>
|
||||
<string name="plug_schuko">Schuko</string>
|
||||
<string name="plug_chademo">CHAdeMO</string>
|
||||
<string name="plug_supercharger">Tesla Supercharger</string>
|
||||
<string name="plug_cee_blau">CEE Blau</string>
|
||||
<string name="plug_cee_rot">CEE Rot</string>
|
||||
<string name="all">alle</string>
|
||||
<string name="none">keine</string>
|
||||
<string name="show_more">mehr…</string>
|
||||
<string name="show_less">weniger…</string>
|
||||
<string name="favorites_empty_state">Wenn du Ladestationen als Favorit markierst, tauchen sie hier auf.</string>
|
||||
<string name="donate">Spenden</string>
|
||||
</resources>
|
||||
@@ -11,5 +11,6 @@
|
||||
<color name="charger_low">#9e9e9e</color>
|
||||
<color name="available">#4caf50</color>
|
||||
<color name="unavailable">#f44336</color>
|
||||
<color name="unknown">#9e9e9e</color>
|
||||
<color name="status_bar_scrim">#C3000000</color>
|
||||
</resources>
|
||||
|
||||
@@ -46,4 +46,21 @@
|
||||
<string name="filter_free">Only free chargers</string>
|
||||
<string name="filter_min_power">Minimum power</string>
|
||||
<string name="filter_free_parking">Only chargers with free parking</string>
|
||||
<string name="filter_min_connectors">Minimum number of connectors</string>
|
||||
<string name="filter_connectors">Connectors</string>
|
||||
<string name="plug_type_1">Type 1</string>
|
||||
<string name="plug_type_2">Type 2</string>
|
||||
<string name="plug_type_3">Type 3a</string>
|
||||
<string name="plug_ccs">CCS</string>
|
||||
<string name="plug_schuko">Schuko</string>
|
||||
<string name="plug_chademo">CHAdeMO</string>
|
||||
<string name="plug_supercharger">Tesla Supercharger</string>
|
||||
<string name="plug_cee_blau">CEE Blue</string>
|
||||
<string name="plug_cee_rot">CEE Red</string>
|
||||
<string name="all">all</string>
|
||||
<string name="none">none</string>
|
||||
<string name="show_more">more…</string>
|
||||
<string name="show_less">less…</string>
|
||||
<string name="favorites_empty_state">If you add chargers as favorites, they will show up here.</string>
|
||||
<string name="donate">Donate</string>
|
||||
</resources>
|
||||
|
||||
@@ -14,8 +14,12 @@
|
||||
android:title="@string/copyright"
|
||||
android:summary="@string/copyright_summary" />
|
||||
|
||||
<Preference
|
||||
android:key="donate"
|
||||
android:title="@string/donate" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/other">
|
||||
<Preference
|
||||
android:key="github_link"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class FilterViewModelTest {
|
||||
@Test
|
||||
fun testPowerMapping() {
|
||||
val sliderValues = powerSteps.indices.toList()
|
||||
|
||||
val mappedValues = (sliderValues).map(::mapPower)
|
||||
assertTrue(mappedValues.distinct() == mappedValues)
|
||||
assertEquals(350, mappedValues.last())
|
||||
assertEquals(0, mappedValues.first())
|
||||
|
||||
val reverseMappedValues = mappedValues.map(::mapPowerInverse)
|
||||
assertEquals(sliderValues, reverseMappedValues)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPowerMappingInbetween() {
|
||||
val sliderValue = 54
|
||||
assertEquals(50, mapPower(mapPowerInverse(sliderValue)))
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.0-beta03'
|
||||
classpath 'com.android.tools.build:gradle:4.0.0-rc01'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user