Compare commits

...

22 Commits
0.0.7 ... 0.1.3

Author SHA1 Message Date
johan12345
795c96d901 Release 0.1.3 2020-05-28 09:03:02 +02:00
johan12345
cc76310b2b fix string 2020-05-28 09:02:13 +02:00
johan12345
2a6ac0ac1b Release 0.1.2 2020-05-27 21:08:10 +02:00
johan12345
8673efd1cd favorites view: limit length of text fields 2020-05-27 21:05:37 +02:00
johan12345
ae40b8c634 show fault reports (fixes #2) 2020-05-27 21:03:46 +02:00
johan12345
0cdb12711d do not show opening hours if they are not available 2020-05-27 20:14:49 +02:00
johan12345
69ccc55ad4 move Chargeprice.app button below connectors (#12) 2020-05-27 20:10:37 +02:00
johan12345
304f46e189 fix hiding and showing of layers FAB and menu when detail view is openend 2020-05-26 23:33:01 +02:00
johan12345
01f06621f4 add link to chargeprice.app to compare prices (#12) 2020-05-26 23:09:48 +02:00
Johan von Forstner
f986a68db8 update version code 2020-05-24 16:54:12 +02:00
Johan von Forstner
441e78d807 Release 0.1.1 2020-05-24 16:52:16 +02:00
Johan von Forstner
6481d651a0 add way to quickly enable and disable filters (first step towards #16) 2020-05-24 16:51:18 +02:00
Johan von Forstner
9a7db8997a Add link from coordinates to maps app (fixes #17) 2020-05-24 16:10:33 +02:00
Johan von Forstner
d94053261c remove debugging println call 2020-05-24 15:38:21 +02:00
Johan von Forstner
39dc50724e add FAQ page with legend for marker colors (fixes #21) 2020-05-24 11:54:50 +02:00
Johan von Forstner
34fe126fd0 add option to show Google Maps traffic layer (fixes #19) 2020-05-24 11:26:13 +02:00
Johan von Forstner
1f81a11ad1 add map type chooser 2020-05-24 09:53:56 +02:00
Johan von Forstner
74b74dcd07 add marker for selected search result (fixes #18) 2020-05-24 08:16:04 +02:00
Johan von Forstner
ec623c9396 make clustering more dynamic (fixes #14) 2020-05-23 19:51:44 +02:00
Johan von Forstner
c10c59e3b1 fix lint error 2020-05-22 09:04:23 +02:00
Johan von Forstner
2bd5f746ed Release 0.1.0 2020-05-21 16:46:36 +02:00
Johan von Forstner
fbc15f2925 sort donations by price 2020-05-21 16:45:54 +02:00
30 changed files with 714 additions and 98 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 29
versionCode 7
versionName "0.0.7"
versionCode 11
versionName "0.1.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -83,9 +83,10 @@ dependencies {
implementation 'androidx.core:core:1.3.0-rc01'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.android.material:material:1.2.0-alpha06'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.browser:browser:1.2.0'
implementation 'com.google.maps.android:android-maps-utils:0.5'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
implementation 'com.google.android.gms:play-services-maps:17.0.0'

View File

@@ -5,6 +5,8 @@ import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
import androidx.navigation.findNavController
@@ -48,33 +50,39 @@ class MapsActivity : AppCompatActivity() {
}
fun navigateTo(charger: ChargeLocation) {
val intent = Intent(Intent.ACTION_VIEW)
val coord = charger.coordinates
// google maps navigation
val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("google.navigation:q=${coord.lat},${coord.lng}")
val pm = packageManager
if (intent.resolveActivity(pm) != null && prefs.navigateUseMaps) {
if (prefs.navigateUseMaps && intent.resolveActivity(packageManager) != null) {
startActivity(intent);
} else {
// fallback: generic geo intent
intent.data = Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
if (intent.resolveActivity(pm) != null) {
startActivity(intent);
} else {
val cb = fragmentCallback ?: return
Snackbar.make(
cb.getRootView(),
R.string.no_maps_app_found,
Snackbar.LENGTH_SHORT
)
}
showLocation(charger)
}
}
fun showLocation(charger: ChargeLocation) {
val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent);
} else {
val cb = fragmentCallback ?: return
Snackbar.make(
cb.getRootView(),
R.string.no_maps_app_found,
Snackbar.LENGTH_SHORT
)
}
}
fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
val intent = CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary))
.build()
intent.launchUrl(this, Uri.parse(url))
}
fun shareUrl(url: String) {

View File

@@ -20,6 +20,9 @@ import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
import net.vonforst.evmap.viewmodel.*
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
interface Equatable {
override fun equals(other: Any?): Boolean;
@@ -86,7 +89,8 @@ class DetailAdapter : DataBindingAdapter<DetailAdapter.Detail>() {
val contentDescription: Int,
val text: CharSequence,
val detailText: CharSequence? = null,
val links: Boolean = true
val links: Boolean = true,
val clickable: Boolean = false
) : Equatable
override fun getItemViewType(position: Int): Int = R.layout.item_detail
@@ -112,8 +116,20 @@ fun buildDetails(loc: ChargeLocation?, ctx: Context): List<DetailAdapter.Detail>
R.string.network,
loc.network
) else null,
if (loc.faultReport != null) DetailAdapter.Detail(
R.drawable.ic_fault_report,
R.string.fault_report,
loc.faultReport.created?.let {
ctx.getString(R.string.fault_report_date,
loc.faultReport.created
.atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)))
} ?: "",
loc.faultReport.description ?: "",
clickable = true
) else null,
// TODO: separate layout for opening hours with expandable details
if (loc.openinghours != null) DetailAdapter.Detail(
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailAdapter.Detail(
R.drawable.ic_hours,
R.string.hours,
loc.openinghours.getStatusText(ctx),
@@ -131,7 +147,8 @@ fun buildDetails(loc: ChargeLocation?, ctx: Context): List<DetailAdapter.Detail>
R.string.coordinates,
loc.coordinates.formatDMS(),
loc.coordinates.formatDecimal(),
false
links = false,
clickable = true
)
)
}

View File

@@ -2,6 +2,7 @@ package net.vonforst.evmap.api.goingelectric
import com.squareup.moshi.*
import java.lang.reflect.Type
import java.time.Instant
import java.time.LocalTime
@@ -65,7 +66,8 @@ internal class ChargepointListItemJsonAdapter(val moshi: Moshi) :
}
internal class JsonObjectOrFalseAdapter<T> private constructor(
private val objectDelegate: JsonAdapter<T>?
private val objectDelegate: JsonAdapter<T>,
private val clazz: Class<*>
) : JsonAdapter<T>() {
class Factory() : JsonAdapter.Factory {
@@ -80,27 +82,32 @@ internal class JsonObjectOrFalseAdapter<T> private constructor(
)) {
false -> null
true -> JsonObjectOrFalseAdapter(
moshi.adapter(clazz)
moshi.adapter(clazz), clazz
)
}
}
}
override fun fromJson(reader: JsonReader) = when (reader.peek()) {
@Suppress("UNCHECKED_CAST")
override fun fromJson(reader: JsonReader): T? = when (reader.peek()) {
JsonReader.Token.BOOLEAN -> when (reader.nextBoolean()) {
false -> null // Response was false
else ->
throw IllegalStateException("Non-false boolean for @JsonObjectOrFalse field")
else -> {
if (this.clazz == FaultReport::class.java) {
FaultReport(null, null) as T
} else {
throw IllegalStateException("Non-false boolean for @JsonObjectOrFalse field")
}
}
}
JsonReader.Token.BEGIN_OBJECT -> objectDelegate?.fromJson(reader)
JsonReader.Token.STRING -> objectDelegate?.fromJson(reader)
JsonReader.Token.NUMBER -> objectDelegate?.fromJson(reader)
JsonReader.Token.BEGIN_OBJECT -> objectDelegate.fromJson(reader)
JsonReader.Token.STRING -> objectDelegate.fromJson(reader)
JsonReader.Token.NUMBER -> objectDelegate.fromJson(reader)
else ->
throw IllegalStateException("Non-object-non-boolean value for @JsonObjectOrFalse field")
}
override fun toJson(writer: JsonWriter, value: T?) =
objectDelegate?.toJson(writer, value) ?: Unit
override fun toJson(writer: JsonWriter, value: T?) = objectDelegate.toJson(writer, value)
}
private fun hasJsonObjectOrFalseAnnotation(annotations: Set<Annotation>?) =
@@ -139,4 +146,14 @@ internal class HoursAdapter {
}
}
}
internal class InstantAdapter {
@FromJson
fun fromJson(value: Long?): Instant? = value?.let {
Instant.ofEpochSecond(it)
}
@ToJson
fun toJson(value: Instant?): Long? = value?.epochSecond
}

View File

@@ -19,7 +19,7 @@ interface GoingElectricApi {
@Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double,
@Query("clustering") clustering: Boolean,
@Query("zoom") zoom: Float,
@Query("cluster_distance") clusterDistance: Int,
@Query("cluster_distance") clusterDistance: Int?,
@Query("freecharging") freecharging: Boolean,
@Query("freeparking") freeparking: Boolean,
@Query("min_power") minPower: Int,
@@ -64,6 +64,7 @@ interface GoingElectricApi {
.add(ChargepointListItemJsonAdapterFactory())
.add(JsonObjectOrFalseAdapter.Factory())
.add(HoursAdapter())
.add(InstantAdapter())
.build()
val retrofit = Retrofit.Builder()

View File

@@ -12,6 +12,7 @@ import kotlinx.android.parcel.Parcelize
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import java.time.DayOfWeek
import java.time.Instant
import java.time.LocalDate
import java.time.LocalTime
import java.util.*
@@ -42,7 +43,7 @@ data class ChargeLocation(
val chargepoints: List<Chargepoint>,
@JsonObjectOrFalse val network: String?,
val url: String,
// @Json(name = "fault_report") val faultReport: Boolean, <- Object or false in detail, true or false in overview
@Embedded(prefix="fault_report_") @JsonObjectOrFalse @Json(name = "fault_report") val faultReport: FaultReport?,
val verified: Boolean,
// only shown in details:
@JsonObjectOrFalse val operator: String?,
@@ -107,6 +108,10 @@ data class OpeningHours(
@JsonObjectOrFalse val description: String?,
@Embedded val days: OpeningHoursDays?
) {
val isEmpty: Boolean
get() = description == "Leider noch keine Informationen zu Öffnungszeiten vorhanden."
&& days == null && !twentyfourSeven
fun getStatusText(ctx: Context): CharSequence {
if (twentyfourSeven) {
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
@@ -134,8 +139,6 @@ data class OpeningHours(
), 0
)
}
} else if (description != null) {
return description
} else {
return ""
}
@@ -242,4 +245,7 @@ data class Chargepoint(val type: String, val power: Double, val count: Int) : Eq
const val CEE_BLAU = "CEE Blau"
const val CEE_ROT = "CEE Rot"
}
}
}
@JsonClass(generateAdapter = true)
data class FaultReport(val created: Instant?, val description: String?)

View File

@@ -41,6 +41,10 @@ class AboutFragment : PreferenceFragmentCompat() {
(activity as? MapsActivity)?.openUrl(getString(R.string.privacy_link))
true
}
"faq" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.faq_link))
true
}
"oss_licenses" -> {
LibsBuilder()
.withLicenseShown(true)

View File

@@ -2,7 +2,6 @@ package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.*
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.databinding.DataBindingUtil
@@ -18,8 +17,6 @@ import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.FiltersAdapter
import net.vonforst.evmap.databinding.FragmentFilterBinding
import net.vonforst.evmap.ui.exitCircularReveal
import net.vonforst.evmap.ui.startCircularReveal
import net.vonforst.evmap.viewmodel.FilterViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
@@ -44,12 +41,6 @@ class FilterFragment : Fragment() {
binding.vm = vm
setHasOptionsMenu(true)
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object :
OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
exitAfterTransition()
}
})
return binding.root
}
@@ -75,10 +66,8 @@ class FilterFragment : Fragment() {
)
}
view.startCircularReveal()
toolbar.setNavigationOnClickListener {
exitAfterTransition()
findNavController().popBackStack()
}
}
@@ -92,17 +81,11 @@ class FilterFragment : Fragment() {
R.id.menu_apply -> {
lifecycleScope.launch {
vm.saveFilterValues()
exitAfterTransition()
findNavController().popBackStack()
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun exitAfterTransition() {
view?.exitCircularReveal {
findNavController().popBackStack()
}
}
}

View File

@@ -7,12 +7,15 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.transition.TransitionManager
import android.view.*
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.PopupMenu
import androidx.core.app.SharedElementCallback
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
@@ -39,6 +42,8 @@ import com.google.android.gms.maps.model.*
import com.google.android.libraries.places.api.model.Place
import com.google.android.libraries.places.widget.Autocomplete
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
import com.google.android.material.transition.MaterialArcMotion
import com.google.android.material.transition.MaterialContainerTransform
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
@@ -82,6 +87,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
private var clusterMarkers: List<Marker> = emptyList()
private var searchResultMarker: Marker? = null
private lateinit var clusterIconGenerator: ClusterIconGenerator
private lateinit var chargerIconGenerator: ChargerIconGenerator
@@ -89,11 +95,19 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private lateinit var favToggle: MenuItem
private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
val value = vm.layersMenuOpen.value
if (value != null && value) {
closeLayersMenu()
return
}
val state = bottomSheetBehavior.state
if (state != STATE_COLLAPSED && state != STATE_HIDDEN) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else if (state == STATE_COLLAPSED) {
vm.chargerSparse.value = null
} else if (state == STATE_HIDDEN) {
vm.searchResult.value = null
}
}
}
@@ -180,17 +194,25 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
}
binding.fabLayers.setOnClickListener {
openLayersMenu()
}
binding.detailView.goingelectricButton.setOnClickListener {
val charger = vm.charger.value?.data
if (charger != null) {
(activity as? MapsActivity)?.openUrl("https:${charger.url}")
}
}
binding.detailView.btnChargeprice.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener
(activity as? MapsActivity)?.openUrl(
"https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric")
}
binding.detailView.topPart.setOnClickListener {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
}
binding.search.setOnClickListener {
val fields = listOf(Place.Field.LAT_LNG)
val fields = listOf(Place.Field.LAT_LNG, Place.Field.VIEWPORT)
val intent: Intent = Autocomplete.IntentBuilder(
AutocompleteActivityMode.OVERLAY, fields
)
@@ -223,6 +245,32 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
private fun openLayersMenu() {
binding.fabLayers.tag = false
val materialTransform = MaterialContainerTransform().apply {
startView = binding.fabLayers
endView = binding.layersSheet
pathMotion = MaterialArcMotion()
duration = 250
scrimColor = Color.TRANSPARENT
}
TransitionManager.beginDelayedTransition(binding.root, materialTransform)
vm.layersMenuOpen.value = true
}
private fun closeLayersMenu() {
binding.fabLayers.tag = true
val materialTransform = MaterialContainerTransform().apply {
startView = binding.layersSheet
endView = binding.fabLayers
pathMotion = MaterialArcMotion()
duration = 200
scrimColor = Color.TRANSPARENT
}
TransitionManager.beginDelayedTransition(binding.root, materialTransform)
vm.layersMenuOpen.value = false
}
private fun toggleFavorite() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
@@ -242,7 +290,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onStateChanged(bottomSheet: View, newState: Int) {
vm.bottomSheetState.value = newState
backPressedCallback.isEnabled = newState != STATE_HIDDEN
updateBackPressedCallback()
if (vm.layersMenuOpen.value!! && newState !in listOf(BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING, BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN, BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED)) {
closeLayersMenu()
}
}
})
vm.chargerSparse.observe(viewLifecycleOwner, Observer {
@@ -266,6 +318,40 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.favorites.observe(viewLifecycleOwner, Observer {
updateFavoriteToggle()
})
vm.searchResult.observe(viewLifecycleOwner, Observer { place ->
val map = this.map ?: return@Observer
searchResultMarker?.remove()
searchResultMarker = null
if (place != null) {
if (place.viewport != null) {
map.animateCamera(CameraUpdateFactory.newLatLngBounds(place.viewport, 0))
} else {
map.animateCamera(CameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
}
searchResultMarker = map.addMarker(MarkerOptions().position(place.latLng!!))
}
updateBackPressedCallback()
})
vm.layersMenuOpen.observe(viewLifecycleOwner, Observer { open ->
binding.fabLayers.visibility = if (open) View.GONE else View.VISIBLE
binding.layersSheet.visibility = if (open) View.VISIBLE else View.GONE
updateBackPressedCallback()
})
vm.mapType.observe(viewLifecycleOwner, Observer {
map?.mapType = it
})
vm.mapTrafficEnabled.observe(viewLifecycleOwner, Observer {
map?.isTrafficEnabled = it
})
}
private fun updateBackPressedCallback() {
backPressedCallback.isEnabled =
vm.bottomSheetState.value != STATE_HIDDEN || vm.searchResult.value != null
|| (vm.layersMenuOpen.value ?: false)
}
private fun unhighlightAllMarkers() {
@@ -357,7 +443,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
binding.detailView.details.apply {
adapter = DetailAdapter()
adapter = DetailAdapter().apply {
onClickListener = {
val charger = vm.chargerSparse.value
if (charger != null) {
when (it.icon) {
R.drawable.ic_location -> {
(activity as? MapsActivity)?.showLocation(charger)
}
R.drawable.ic_fault_report -> {
(activity as? MapsActivity)?.openUrl("https:${charger.url}")
}
}
}
}
}
itemAnimator = null
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
@@ -372,7 +472,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onMapReady(map: GoogleMap) {
this.map = map
map.uiSettings.isTiltGesturesEnabled = false;
map.uiSettings.isTiltGesturesEnabled = false
map.isIndoorEnabled = false
map.uiSettings.isIndoorLevelPickerEnabled = false
map.setOnCameraIdleListener {
vm.mapPosition.value = MapPosition(
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
@@ -394,7 +496,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
map.setOnMapClickListener {
vm.chargerSparse.value = null
if (backPressedCallback.isEnabled) {
backPressedCallback.handleOnBackPressed()
}
}
// set padding so that compass is not obstructed by toolbar
@@ -561,19 +665,36 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
})
}
filterView?.setOnClickListener {
onOptionsItemSelected(filterItem)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_filter -> {
requireView().findNavController().navigate(
R.id.action_map_to_filterFragment
)
true
val popup = PopupMenu(requireContext(), it, Gravity.END)
popup.menuInflater.inflate(R.menu.popup_filter, popup.menu)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_edit_filters -> {
requireView().findNavController().navigate(
R.id.action_map_to_filterFragment
)
true
}
R.id.menu_filters_active -> {
vm.filtersActive.value = !vm.filtersActive.value!!
true
}
else -> false
}
}
else -> super.onOptionsItemSelected(item)
val checkItem = popup.menu.findItem(R.id.menu_filters_active)
vm.filtersActive.observe(viewLifecycleOwner, Observer {
checkItem.isChecked = it
})
popup.show()
}
filterView?.setOnLongClickListener {
requireView().findNavController().navigate(
R.id.action_map_to_filterFragment
)
true
}
}
@@ -581,9 +702,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
when (requestCode) {
REQUEST_AUTOCOMPLETE -> {
if (resultCode == Activity.RESULT_OK) {
val place = Autocomplete.getPlaceFromIntent(data!!)
val zoom = 12f
map?.animateCamera(CameraUpdateFactory.newLatLngZoom(place.latLng, zoom))
vm.searchResult.value = Autocomplete.getPlaceFromIntent(data!!)
}
}
else -> super.onActivityResult(requestCode, resultCode, data)

View File

@@ -19,7 +19,7 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
MultipleChoiceFilterValue::class,
SliderFilterValue::class,
Plug::class
], version = 5
], version = 6
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
@@ -31,7 +31,7 @@ abstract class AppDatabase : RoomDatabase() {
private lateinit var context: Context
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db")
.addMigrations(MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5)
.addMigrations(MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6)
.build()
}
@@ -86,5 +86,18 @@ abstract class AppDatabase : RoomDatabase() {
}
}
}
private val MIGRATION_6 = object : Migration(5, 6) {
override fun migrate(db: SupportSQLiteDatabase) {
db.beginTransaction()
try {
db.execSQL("ALTER TABLE `ChargeLocation` ADD `fault_report_created` INTEGER")
db.execSQL("ALTER TABLE `ChargeLocation` ADD `fault_report_description` TEXT")
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}
}
}

View File

@@ -5,6 +5,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
import java.time.Instant
import java.time.LocalTime
class Converters {
@@ -49,11 +50,23 @@ class Converters {
@TypeConverter
fun toLocalTime(value: String?): LocalTime? {
return value.let {
return value?.let {
LocalTime.parse(it)
}
}
@TypeConverter
fun fromInstant(value: Instant?): Long? {
return value?.toEpochMilli()
}
@TypeConverter
fun toInstant(value: Long?): Instant? {
return value?.let {
Instant.ofEpochMilli(it)
}
}
@TypeConverter
fun fromStringSet(value: Set<String>?): String {
return stringSetAdapter.toJson(value)

View File

@@ -2,10 +2,15 @@ package net.vonforst.evmap.ui
import android.content.Context
import android.content.res.ColorStateList
import android.os.Build
import android.text.Html
import android.text.Spanned
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.res.use
import androidx.core.text.HtmlCompat
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
@@ -97,6 +102,26 @@ fun setBackgroundTintAvailability(view: View, available: List<ChargepointStatus>
view.backgroundTintList = ColorStateList.valueOf(availabilityColor(available, view.context))
}
@BindingAdapter("selectableItemBackground")
fun applySelectableItemBackground(view: View, apply: Boolean) {
if (apply) {
view.context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
view.background = it.getDrawable(0)
}
} else {
view.background = null
}
}
@BindingAdapter("htmlText")
fun setHtmlTextValue(textView: TextView, htmlText: String?) {
if (htmlText == null) {
textView.text = null
} else {
textView.text = HtmlCompat.fromHtml(htmlText, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}
private fun availabilityColor(
status: List<ChargepointStatus>?,
context: Context

View File

@@ -0,0 +1,112 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.widget.NestedScrollView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
class HideOnExpandFabBehavior(context: Context, attrs: AttributeSet) :
FloatingActionButton.Behavior(context, attrs) {
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL || super.onStartNestedScroll(
coordinatorLayout,
child,
directTargetChild,
target,
axes,
type
)
}
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: FloatingActionButton,
dependency: View
): Boolean {
if (dependency is NestedScrollView) {
try {
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
behavior.addBottomSheetCallback(object :
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
onDependentViewChanged(parent, child, dependency)
}
})
return true
} catch (e: IllegalArgumentException) {
}
}
return false
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: FloatingActionButton,
dependency: View
): Boolean {
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
when (behavior.state) {
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING -> {
}
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
if (child.tag as? Boolean != false) child.show()
}
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED -> {
if (child.tag as? Boolean != false) child.show()
}
else -> {
child.hide()
}
}
return false
}
override fun onNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
super.onNestedScroll(
coordinatorLayout,
child,
target,
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
type,
consumed
)
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
// User scrolled down and the FAB is currently visible -> hide the FAB
child.hide();
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
// User scrolled up and the FAB is currently not visible -> show the FAB
child.show();
}
}
}

View File

@@ -54,7 +54,10 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
.build()
billingClient.querySkuDetailsAsync(params) { result, details ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
products.value = Resource.success(details.map { DonationItem(it) })
products.value = Resource.success(details
.sortedBy { it.priceAmountMicros }
.map { DonationItem(it) }
)
} else {
products.value = Resource.error(result.debugMessage, null)
}

View File

@@ -74,17 +74,25 @@ internal fun getFilters(
internal fun filtersWithValue(
filters: LiveData<List<Filter<FilterValue>>>,
filterValues: LiveData<List<FilterValue>>
filterValues: LiveData<List<FilterValue>>,
active: LiveData<Boolean>? = null
): MediatorLiveData<List<FilterWithValue<out FilterValue>>> =
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
listOf(filters, filterValues).forEach {
listOf(filters, filterValues, active).forEach {
if (it == null) return@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))
value = if (active != null && !active.value!!) {
filters.map { filter ->
FilterWithValue(filter, filter.defaultValue())
}
} else {
val values = filterValues.value ?: return@addSource
filters.map { filter ->
val value =
values.find { it.key == filter.key } ?: filter.defaultValue()
FilterWithValue(filter, filter.valueClass.cast(value))
}
}
}
}

View File

@@ -2,7 +2,9 @@ package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.*
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.libraries.places.api.model.Place
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.availability.ChargeLocationStatus
@@ -21,6 +23,16 @@ import retrofit2.Response
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
internal fun getClusterDistance(zoom: Float): Int? {
return when (zoom) {
in 0.0..7.0 -> 100
in 7.0..11.5 -> 75
in 11.5..12.5 -> 60
in 12.5..13.0 -> 45
else -> null
}
}
class MapViewModel(application: Application, geApiKey: String) : AndroidViewModel(application) {
private var api = GoingElectricApi.create(geApiKey, context = application)
private var db = AppDatabase.getInstance(application)
@@ -43,7 +55,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
private val filters = getFilters(application, plugs)
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
filtersWithValue(filters, filterValues)
filtersWithValue(filters, filterValues, filtersActive)
}
val filtersCount: LiveData<Int> by lazy {
@@ -112,11 +124,42 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
val layersMenuOpen: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = false
}
}
val favorites: LiveData<List<ChargeLocation>> by lazy {
db.chargeLocationsDao().getAllChargeLocations()
}
val searchResult: MutableLiveData<Place> by lazy {
MutableLiveData<Place>()
}
val mapType: MutableLiveData<Int> by lazy {
MutableLiveData<Int>().apply {
value = GoogleMap.MAP_TYPE_NORMAL
}
}
val mapTrafficEnabled: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = false
}
}
val filtersActive: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = true
}
}
fun setMapType(type: Int) {
mapType.value = type
}
fun insertFavorite(charger: ChargeLocation) {
viewModelScope.launch {
db.chargeLocationsDao().insert(charger)
@@ -166,13 +209,14 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
// do not use clustering if filters need to be applied locally.
val useClustering = minConnectors <= 1
val useClustering = minConnectors <= 1 && zoom < 13
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
val response = api.getChargepoints(
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude,
clustering = zoom < 13 && useClustering, zoom = zoom,
clusterDistance = 40, freecharging = freecharging, minPower = minPower,
clustering = useClustering, zoom = zoom,
clusterDistance = clusterDistance, freecharging = freecharging, minPower = minPower,
freeparking = freeparking, plugs = connectors
)

View File

@@ -0,0 +1,4 @@
<vector android:height="15.811624dp" android:viewportHeight="131.5"
android:viewportWidth="199.6" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M197.544,65.685l-9.2,-4.8l8,-4.2c2.7,-1.4 2.7,-3.8 0,-5.2l-8.6,-4.5l8.6,-4.5c2.7,-1.4 2.7,-3.8 0,-5.2l-68.9,-36.1c-2.7,-1.4 -7.2,-1.4 -9.9,0l-115.5,59.7c-2.7,1.4 -2.7,3.7 0,5.1l8.8,4.5l-8.8,4.6c-2.7,1.4 -2.7,3.7 0,5.1l9.4,4.8l-8.2,4.3c-2.7,1.4 -2.7,3.7 0,5.1l70.4,36.2c2.7,1.4 7.2,1.4 9.9,0l114,-59.6C200.344,69.385 200.344,67.085 197.544,65.685L197.544,65.685zM123.144,18.785L105.844,38.685c-0.9,1 -0.6,2.3 0.6,2.9l13.7,7.1c1.2,0.6 1.2,1.6 0,2.2l-43.1,22.3c-1.2,0.6 -1.4,0.3 -0.6,-0.7l17.3,-19.9c0.9,-1 0.6,-2.3 -0.6,-2.9l-13.7,-7.1c-1.2,-0.6 -1.2,-1.6 0,-2.2l43.1,-22.3C123.744,17.485 123.944,17.785 123.144,18.785L123.144,18.785z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16zM12,4.53L17.74,9 12,13.47 6.26,9 12,4.53z" />
</vector>

View File

@@ -184,7 +184,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView13"
app:layout_constraintTop_toBottomOf="@+id/btnChargeprice"
tools:itemCount="3"
tools:listitem="@layout/item_detail" />
@@ -230,6 +230,18 @@
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/textView" />
<Button
android:id="@+id/btnChargeprice"
style="@style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/go_to_chargeprice"
app:icon="@drawable/ic_chargeprice"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView13" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@@ -120,7 +120,7 @@
app:behavior_peekHeight="@dimen/peek_height"
app:bottomsheetbehavior_defaultState="stateHidden"
app:layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike"
tools:bottomsheetbehavior_defaultState="stateAnchorPoint">
tools:bottomsheetbehavior_defaultState="stateHidden">
<include
android:id="@+id/detail_view"
@@ -148,5 +148,39 @@
android:layout_height="wrap_content"
app:layout_behavior="@string/MergedAppBarLayoutBehavior"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_layers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:layout_gravity="top|end"
android:layout_marginEnd="12dp"
android:layout_marginTop="96dp"
android:tint="?colorControlNormal"
android:elevation="-1dp"
app:backgroundTint="?android:colorBackground"
app:borderWidth="0dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_layers"
app:layout_behavior="net.vonforst.evmap.ui.HideOnExpandFabBehavior"/>
<androidx.cardview.widget.CardView
android:id="@+id/layers_sheet"
android:layout_height="wrap_content"
android:layout_width="200dp"
android:layout_gravity="top|end"
android:layout_marginEnd="8dp"
android:layout_marginTop="96dp"
android:elevation="-1dp"
android:visibility="gone"
tools:visibility="visible">
<include
android:id="@+id/layers"
layout="@layout/map_layers"
app:vm="@{vm}" />
</androidx.cardview.widget.CardView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@@ -13,7 +13,9 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:clickable="@{item.clickable}"
app:selectableItemBackground="@{item.clickable}">
<TextView
android:id="@+id/textView9"

View File

@@ -34,10 +34,12 @@
android:id="@+id/textView2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.charger.address.toString()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/textView7"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView15"
tools:text="Beispielstraße 10, 12345 Berlin" />
@@ -46,10 +48,12 @@
android:id="@+id/textView3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.charger.formatChargepoints()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/textView7"
app:layout_constraintStart_toStartOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="2x Typ 2 22 kW" />

View File

@@ -0,0 +1,102 @@
<?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.MapViewModel" />
<import type="com.google.android.gms.maps.GoogleMap" />
<variable
name="vm"
type="MapViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/textView22"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/map_type"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RadioGroup
android:id="@+id/radioGroup"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView22">
<RadioButton
android:id="@+id/rbStandard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(GoogleMap.MAP_TYPE_NORMAL)}"
android:onClick="@{() -> vm.setMapType(GoogleMap.MAP_TYPE_NORMAL)}"
android:text="@string/map_type_normal" />
<RadioButton
android:id="@+id/rbSatellite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(GoogleMap.MAP_TYPE_HYBRID)}"
android:onClick="@{() -> vm.setMapType(GoogleMap.MAP_TYPE_HYBRID)}"
android:text="@string/map_type_satellite" />
<RadioButton
android:id="@+id/rbTerrain"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@{vm.mapType.equals(GoogleMap.MAP_TYPE_TERRAIN)}"
android:onClick="@{() -> vm.setMapType(GoogleMap.MAP_TYPE_TERRAIN)}"
android:text="@string/map_type_terrain" />
</RadioGroup>
<TextView
android:id="@+id/textView23"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/map_details"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/radioGroup" />
<CheckBox
android:id="@+id/cbTraffic"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/map_traffic"
android:checked="@={vm.mapTrafficEnabled}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView23" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_filters_active"
android:title="@string/menu_filters_active"
android:checkable="true"
android:checked="true" />
<item
android:id="@+id/menu_edit_filters"
android:title="@string/menu_edit_filters" />
</menu>

View File

@@ -64,7 +64,19 @@
<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>
<string name="donations_info">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.</string>
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.</string>
<string name="donation_successful">Vielen Dank! ❤️</string>
<string name="donation_failed">Etwas ist schiefgelaufen. 😕</string>
<string name="map_type_normal">Standard</string>
<string name="map_type_satellite">Satellit</string>
<string name="map_type_terrain">Gelände</string>
<string name="map_type">Kartentyp</string>
<string name="map_details">Kartendetails</string>
<string name="map_traffic">Verkehr</string>
<string name="faq">FAQ</string>
<string name="menu_filters_active">Filter aktiv</string>
<string name="menu_edit_filters">Filter bearbeiten…</string>
<string name="go_to_chargeprice">Preisvergleich</string>
<string name="fault_report">Störungsmeldung</string>
<string name="fault_report_date">Störungsmeldung (Letztes Update: %s)</string>
</resources>

View File

@@ -3,4 +3,5 @@
<string name="shared_element_picture">picture</string>
<string name="github_link">https://github.com/johan12345/EVMap</string>
<string name="privacy_link">https://evmap.vonforst.net/privacy.html</string>
<string name="faq_link">https://evmap.vonforst.net/faq.html</string>
</resources>

View File

@@ -63,7 +63,19 @@
<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>
<string name="donations_info">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation.</string>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation.</string>
<string name="donation_successful">Thank you! ❤️</string>
<string name="donation_failed">Something went wrong. 😕</string>
<string name="map_type_normal">Default</string>
<string name="map_type_satellite">Satellite</string>
<string name="map_type_terrain">Terrain</string>
<string name="map_type">Map type</string>
<string name="map_details">Map details</string>
<string name="map_traffic">Traffic</string>
<string name="faq">FAQ</string>
<string name="menu_filters_active">Filters active</string>
<string name="menu_edit_filters">Edit filters…</string>
<string name="go_to_chargeprice">Compare prices</string>
<string name="fault_report">Fault report</string>
<string name="fault_report_date">Fault report (last update: %s)</string>
</resources>

View File

@@ -14,6 +14,10 @@
android:title="@string/copyright"
android:summary="@string/copyright_summary" />
<Preference
android:key="faq"
android:title="@string/faq" />
<Preference
android:key="donate"
android:title="@string/donate" />

View File

@@ -0,0 +1,24 @@
package net.vonforst.evmap.viewmodel
import org.junit.Test
class MapViewModelTest {
@Test
fun testGetClusterDistance() {
var zoom = 0.0f
var previousDistance: Int? = 999
while (zoom < 20.0f) {
val distance = getClusterDistance(zoom)
if (previousDistance != null) {
if (distance != null) {
assert(distance <= previousDistance)
}
} else {
assert(distance == null)
}
previousDistance = distance
zoom += 0.1f
}
}
}