mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-27 00:57:45 -05:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f7bf7913f | ||
|
|
d11925eb33 | ||
|
|
6ac49fd84d | ||
|
|
097b7941a2 | ||
|
|
23b87e69c0 | ||
|
|
3bb5521c18 | ||
|
|
76f7b97c1f | ||
|
|
50de0009c7 | ||
|
|
f906846fcc | ||
|
|
b50225af32 | ||
|
|
8abd5219aa | ||
|
|
71f9a25c5a | ||
|
|
b5f4314795 | ||
|
|
034196b9fa | ||
|
|
72d7f7dc57 |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 18
|
||||
versionName "0.2.0"
|
||||
versionCode 19
|
||||
versionName "0.2.1"
|
||||
|
||||
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"
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,7 @@ 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.*
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.goingelectric.*
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
|
||||
@@ -26,6 +25,7 @@ import net.vonforst.evmap.viewmodel.*
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import kotlin.math.max
|
||||
|
||||
interface Equatable {
|
||||
override fun equals(other: Any?): Boolean;
|
||||
@@ -116,6 +116,7 @@ class DetailAdapter : DataBindingAdapter<DetailAdapter.Detail>() {
|
||||
fun buildDetails(
|
||||
loc: ChargeLocation?,
|
||||
chargeCards: Map<Long, ChargeCard>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
ctx: Context
|
||||
): List<DetailAdapter.Detail> {
|
||||
if (loc == null) return emptyList()
|
||||
@@ -174,7 +175,7 @@ fun buildDetails(
|
||||
R.plurals.charge_cards_compatible_num,
|
||||
loc.chargecards.size, loc.chargecards.size
|
||||
),
|
||||
formatChargeCards(loc.chargecards, chargeCards, ctx),
|
||||
formatChargeCards(loc.chargecards, chargeCards, filteredChargeCards, ctx),
|
||||
clickable = true
|
||||
) else null,
|
||||
DetailAdapter.Detail(
|
||||
@@ -191,15 +192,23 @@ fun buildDetails(
|
||||
fun formatChargeCards(
|
||||
chargecards: List<ChargeCardId>,
|
||||
chargecardData: Map<Long, ChargeCard>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
ctx: Context
|
||||
): String {
|
||||
): CharSequence {
|
||||
if (chargecardData == null) return ""
|
||||
|
||||
val maxItems = 5
|
||||
var result = chargecards
|
||||
.sortedByDescending { filteredChargeCards?.contains(it.id) }
|
||||
.take(maxItems)
|
||||
.mapNotNull { chargecardData[it.id]?.name }
|
||||
.joinToString()
|
||||
.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)
|
||||
}
|
||||
@@ -378,15 +387,15 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
filter: SliderFilter,
|
||||
value: SliderFilterValue
|
||||
) {
|
||||
binding.progress = filter.inverseMapping(value.value)
|
||||
binding.mappedValue = value.value
|
||||
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)
|
||||
val mapped = filter.mapping(binding.progress + filter.min)
|
||||
value.value = mapped
|
||||
binding.mappedValue = mapped
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -64,7 +64,7 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
list.adapter = adapter
|
||||
list.layoutManager = LinearLayoutManager(view.context)
|
||||
|
||||
items = data.entries.toList().sortedBy { it.key }.map {
|
||||
items = data.entries.toList().sortedBy { it.value }.map {
|
||||
MultiSelectItem(it.key, it.value, it.key in selected)
|
||||
}
|
||||
adapter.submitList(items)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,4 +39,7 @@ class PreferenceDataSource(context: Context) {
|
||||
|
||||
val language: String
|
||||
get() = sp.getString("language", "default")!!
|
||||
|
||||
val darkmode: String
|
||||
get() = sp.getString("darkmode", "default")!!
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
14
app/src/main/java/net/vonforst/evmap/ui/NightModeUtils.kt
Normal file
14
app/src/main/java/net/vonforst/evmap/ui/NightModeUtils.kt
Normal 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
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,8 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10
|
||||
10,
|
||||
min = 1
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_networks), "networks",
|
||||
@@ -185,6 +186,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? = ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
name="chargeCards"
|
||||
type="java.util.Map<Long, ChargeCard>" />
|
||||
|
||||
<variable
|
||||
name="filteredChargeCards"
|
||||
type="java.util.Set<Long>" />
|
||||
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
@@ -188,7 +192,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{DataBindingAdaptersKt.buildDetails(charger.data, chargeCards, context)}"
|
||||
app:data="@{DataBindingAdaptersKt.buildDetails(charger.data, chargeCards, filteredChargeCards, context)}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user