Compare commits

..

12 Commits
0.0.7 ... 0.1.1

Author SHA1 Message Date
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
21 changed files with 451 additions and 79 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 29
versionCode 7
versionName "0.0.7"
versionCode 8
versionName "0.1.1"
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

@@ -86,7 +86,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
@@ -131,7 +132,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

@@ -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,

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,6 +194,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
}
binding.fabLayers.setOnClickListener {
openLayersMenu()
}
binding.detailView.goingelectricButton.setOnClickListener {
val charger = vm.charger.value?.data
if (charger != null) {
@@ -190,7 +207,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
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 +240,30 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
private fun openLayersMenu() {
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() {
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 +283,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onStateChanged(bottomSheet: View, newState: Int) {
vm.bottomSheetState.value = newState
backPressedCallback.isEnabled = newState != STATE_HIDDEN
updateBackPressedCallback()
}
})
vm.chargerSparse.observe(viewLifecycleOwner, Observer {
@@ -266,6 +307,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 +432,18 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
binding.detailView.details.apply {
adapter = DetailAdapter()
adapter = DetailAdapter().apply {
onClickListener = {
when (it.icon) {
R.drawable.ic_location -> {
val charger = vm.chargerSparse.value
if (charger != null) {
(activity as? MapsActivity)?.showLocation(charger)
}
}
}
}
}
itemAnimator = null
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
@@ -372,7 +458,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 +482,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 +651,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 +688,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

@@ -6,6 +6,7 @@ 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.databinding.BindingAdapter
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
@@ -97,6 +98,17 @@ 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
}
}
private fun availabilityColor(
status: List<ChargepointStatus>?,
context: Context

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,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

@@ -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,35 @@
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"
app:backgroundTint="?android:colorBackground"
app:borderWidth="0dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_layers" />
<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: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

@@ -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,16 @@
<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>
</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,16 @@
<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>
</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
}
}
}