mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-24 07:37:46 -05:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
795c96d901 | ||
|
|
cc76310b2b | ||
|
|
2a6ac0ac1b | ||
|
|
8673efd1cd | ||
|
|
ae40b8c634 | ||
|
|
0cdb12711d | ||
|
|
69ccc55ad4 | ||
|
|
304f46e189 | ||
|
|
01f06621f4 | ||
|
|
f986a68db8 | ||
|
|
441e78d807 | ||
|
|
6481d651a0 | ||
|
|
9a7db8997a | ||
|
|
d94053261c | ||
|
|
39dc50724e | ||
|
|
34fe126fd0 | ||
|
|
1f81a11ad1 | ||
|
|
74b74dcd07 | ||
|
|
ec623c9396 | ||
|
|
c10c59e3b1 | ||
|
|
2bd5f746ed | ||
|
|
fbc15f2925 |
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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?)
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
4
app/src/main/res/drawable/ic_chargeprice.xml
Normal file
4
app/src/main/res/drawable/ic_chargeprice.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/ic_fault_report.xml
Normal file
10
app/src/main/res/drawable/ic_fault_report.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/ic_layers.xml
Normal file
10
app/src/main/res/drawable/ic_layers.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
102
app/src/main/res/layout/map_layers.xml
Normal file
102
app/src/main/res/layout/map_layers.xml
Normal 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>
|
||||
11
app/src/main/res/menu/popup_filter.xml
Normal file
11
app/src/main/res/menu/popup_filter.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user