mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 08:07:46 -05:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7759c230db | ||
|
|
cdc575ff33 | ||
|
|
cb250de79e | ||
|
|
c7885ae729 | ||
|
|
024b56952d | ||
|
|
75b2240247 | ||
|
|
d8f011b64b | ||
|
|
a1760a35ff | ||
|
|
e5e5f8ef3c | ||
|
|
b5a4fe2dc8 | ||
|
|
676e703a52 | ||
|
|
b9997cbb5a | ||
|
|
2558052f4f | ||
|
|
980c8cc0af | ||
|
|
ffb6740da8 | ||
|
|
2e9112f5c2 | ||
|
|
3c709fa3c5 | ||
|
|
11c868af66 | ||
|
|
e3ea72bac6 | ||
|
|
d01371f6e9 | ||
|
|
6130b190e1 | ||
|
|
128d156306 | ||
|
|
f855874d56 | ||
|
|
92ebf6c1e5 | ||
|
|
1e98be0f8f | ||
|
|
c0bec92d4c | ||
|
|
71ecd492e9 | ||
|
|
fcac8f91ad | ||
|
|
795c96d901 | ||
|
|
cc76310b2b |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 10
|
||||
versionName "0.1.2"
|
||||
versionCode 16
|
||||
versionName "0.1.8"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -80,18 +80,14 @@ dependencies {
|
||||
implementation "androidx.activity:activity-ktx:1.1.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.2.4"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.core:core:1.3.0-rc01'
|
||||
implementation 'androidx.core:core:1.3.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'com.google.android.material:material:1.2.0-alpha06'
|
||||
implementation 'com.google.android.material:material:1.2.0-beta01'
|
||||
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'
|
||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
implementation 'com.google.android.libraries.places:places:2.2.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.7.2'
|
||||
implementation 'com.squareup.moshi:moshi-kotlin:1.9.2'
|
||||
@@ -102,8 +98,21 @@ dependencies {
|
||||
implementation 'com.airbnb.android:lottie:3.4.0'
|
||||
implementation 'io.michaelrocks:bimap:1.0.2'
|
||||
|
||||
// Google Maps v3 Beta
|
||||
implementation name:'maps-sdk-3.0.0-beta', ext:'aar'
|
||||
implementation name:'places-maps-sdk-3.0.0-beta', ext:'aar'
|
||||
implementation 'com.google.maps.android:android-maps-utils-v3:1.3.3'
|
||||
implementation 'com.google.android.gms:play-services-basement:17.3.0'
|
||||
implementation 'com.google.android.gms:play-services-base:17.3.0'
|
||||
implementation 'com.google.android.gms:play-services-gcm:17.0.0'
|
||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
implementation 'com.google.android.gms:play-services-clearcut:17.0.0'
|
||||
implementation 'com.android.volley:volley:1.1.1'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
|
||||
// navigation library
|
||||
def nav_version = "2.3.0-alpha06"
|
||||
def nav_version = "2.3.0-beta01"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
@@ -119,7 +128,7 @@ dependencies {
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
|
||||
// billing library
|
||||
def billing_version = "2.2.0"
|
||||
def billing_version = "2.2.1"
|
||||
implementation "com.android.billingclient:billing:$billing_version"
|
||||
implementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
|
||||
|
||||
BIN
app/libs/maps-sdk-3.0.0-beta.aar
Normal file
BIN
app/libs/maps-sdk-3.0.0-beta.aar
Normal file
Binary file not shown.
BIN
app/libs/places-maps-sdk-3.0.0-beta.aar
Normal file
BIN
app/libs/places-maps-sdk-3.0.0-beta.aar
Normal file
Binary file not shown.
@@ -1,8 +1,10 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
@@ -12,10 +14,14 @@ import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.LocaleContextWrapper
|
||||
|
||||
|
||||
const val REQUEST_LOCATION_PERMISSION = 1
|
||||
|
||||
@@ -30,6 +36,14 @@ class MapsActivity : AppCompatActivity() {
|
||||
var fragmentCallback: FragmentCallback? = null
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
return super.attachBaseContext(
|
||||
LocaleContextWrapper.wrap(
|
||||
newBase, PreferenceDataSource(newBase).language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -47,6 +61,8 @@ class MapsActivity : AppCompatActivity() {
|
||||
findViewById<NavigationView>(R.id.nav_view).setupWithNavController(navController)
|
||||
|
||||
prefs = PreferenceDataSource(this)
|
||||
|
||||
checkPlayServices()
|
||||
}
|
||||
|
||||
fun navigateTo(charger: ChargeLocation) {
|
||||
@@ -92,4 +108,19 @@ class MapsActivity : AppCompatActivity() {
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun checkPlayServices(): Boolean {
|
||||
val request = 9000
|
||||
val apiAvailability = GoogleApiAvailability.getInstance()
|
||||
val resultCode = apiAvailability.isGooglePlayServicesAvailable(this)
|
||||
if (resultCode != ConnectionResult.SUCCESS) {
|
||||
if (apiAvailability.isUserResolvableError(resultCode)) {
|
||||
apiAvailability.getErrorDialog(this, resultCode, request).show()
|
||||
} else {
|
||||
Log.d("EVMap", "This device is not supported.")
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.children
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.Observable
|
||||
@@ -17,8 +18,11 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.OpeningHoursDays
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceLargeBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
|
||||
import net.vonforst.evmap.fragment.MultiSelectDialog
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -28,8 +32,8 @@ interface Equatable {
|
||||
override fun equals(other: Any?): Boolean;
|
||||
}
|
||||
|
||||
abstract class DataBindingAdapter<T : Equatable>() :
|
||||
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback()) {
|
||||
abstract class DataBindingAdapter<T : Equatable>(getKey: ((T) -> Any)? = null) :
|
||||
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback(getKey)) {
|
||||
|
||||
var onClickListener: ((T) -> Unit)? = null
|
||||
|
||||
@@ -50,14 +54,20 @@ abstract class DataBindingAdapter<T : Equatable>() :
|
||||
open fun bind(holder: ViewHolder<T>, item: T) {
|
||||
holder.binding.setVariable(BR.item, item)
|
||||
holder.binding.executePendingBindings()
|
||||
holder.binding.root.setOnClickListener {
|
||||
val listener = onClickListener ?: return@setOnClickListener
|
||||
listener(item)
|
||||
if (onClickListener != null) {
|
||||
holder.binding.root.setOnClickListener {
|
||||
val listener = onClickListener ?: return@setOnClickListener
|
||||
listener(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DiffCallback<T : Equatable> : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = oldItem === newItem
|
||||
class DiffCallback<T : Equatable>(val getKey: ((T) -> Any)?) : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = if (getKey != null) {
|
||||
(getKey)(oldItem) == (getKey)(newItem)
|
||||
} else {
|
||||
oldItem === newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = oldItem == newItem
|
||||
}
|
||||
@@ -90,10 +100,18 @@ class DetailAdapter : DataBindingAdapter<DetailAdapter.Detail>() {
|
||||
val text: CharSequence,
|
||||
val detailText: CharSequence? = null,
|
||||
val links: Boolean = true,
|
||||
val clickable: Boolean = false
|
||||
val clickable: Boolean = false,
|
||||
val hoursDays: OpeningHoursDays? = null
|
||||
) : Equatable
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_detail
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val item = getItem(position)
|
||||
if (item.hoursDays != null) {
|
||||
return R.layout.item_detail_openinghours
|
||||
} else {
|
||||
return R.layout.item_detail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDetails(loc: ChargeLocation?, ctx: Context): List<DetailAdapter.Detail> {
|
||||
@@ -128,12 +146,12 @@ fun buildDetails(loc: ChargeLocation?, ctx: Context): List<DetailAdapter.Detail>
|
||||
loc.faultReport.description ?: "",
|
||||
clickable = true
|
||||
) else null,
|
||||
// TODO: separate layout for opening hours with expandable details
|
||||
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailAdapter.Detail(
|
||||
R.drawable.ic_hours,
|
||||
R.string.hours,
|
||||
loc.openinghours.getStatusText(ctx),
|
||||
loc.openinghours.description
|
||||
loc.openinghours.description,
|
||||
hoursDays = loc.openinghours.days
|
||||
) else null,
|
||||
if (loc.cost != null) DetailAdapter.Detail(
|
||||
R.drawable.ic_cost,
|
||||
@@ -173,11 +191,18 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
val itemids = mutableMapOf<String, Long>()
|
||||
var maxId = 0L
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (getItem(position).filter) {
|
||||
is BooleanFilter -> R.layout.item_filter_boolean
|
||||
is MultipleChoiceFilter -> R.layout.item_filter_multiple_choice
|
||||
is SliderFilter -> R.layout.item_filter_slider
|
||||
}
|
||||
override fun getItemViewType(position: Int): Int =
|
||||
when (val filter = getItem(position).filter) {
|
||||
is BooleanFilter -> R.layout.item_filter_boolean
|
||||
is MultipleChoiceFilter -> {
|
||||
if (filter.manyChoices) {
|
||||
R.layout.item_filter_multiple_choice_large
|
||||
} else {
|
||||
R.layout.item_filter_multiple_choice
|
||||
}
|
||||
}
|
||||
is SliderFilter -> R.layout.item_filter_slider
|
||||
}
|
||||
|
||||
override fun bind(
|
||||
holder: ViewHolder<FilterWithValue<FilterValue>>,
|
||||
@@ -192,10 +217,18 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
)
|
||||
}
|
||||
is MultipleChoiceFilterValue -> {
|
||||
setupMultipleChoice(
|
||||
holder.binding as ItemFilterMultipleChoiceBinding,
|
||||
item.filter as MultipleChoiceFilter, item.value
|
||||
)
|
||||
val filter = item.filter as MultipleChoiceFilter
|
||||
if (filter.manyChoices) {
|
||||
setupMultipleChoiceMany(
|
||||
holder.binding as ItemFilterMultipleChoiceLargeBinding,
|
||||
filter, item.value
|
||||
)
|
||||
} else {
|
||||
setupMultipleChoice(
|
||||
holder.binding as ItemFilterMultipleChoiceBinding,
|
||||
filter, item.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,6 +316,27 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
private fun setupMultipleChoiceMany(
|
||||
binding: ItemFilterMultipleChoiceLargeBinding,
|
||||
filter: MultipleChoiceFilter,
|
||||
value: MultipleChoiceFilterValue
|
||||
) {
|
||||
if (value.all) {
|
||||
value.values = filter.choices.keys.toMutableSet()
|
||||
binding.notifyPropertyChanged(BR.item)
|
||||
}
|
||||
|
||||
binding.btnEdit.setOnClickListener {
|
||||
val dialog = MultiSelectDialog.getInstance(filter.name, filter.choices, value.values)
|
||||
dialog.okListener = { selected ->
|
||||
value.values = selected.toMutableSet()
|
||||
value.all = value.values == filter.choices.keys
|
||||
binding.item = binding.item
|
||||
}
|
||||
dialog.show((binding.root.context as AppCompatActivity).supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSlider(
|
||||
binding: ItemFilterSliderBinding,
|
||||
filter: SliderFilter,
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.api.goingelectric
|
||||
import android.content.Context
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Call
|
||||
@@ -17,13 +18,16 @@ interface GoingElectricApi {
|
||||
suspend fun getChargepoints(
|
||||
@Query("sw_lat") swlat: Double, @Query("sw_lng") sw_lng: Double,
|
||||
@Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double,
|
||||
@Query("clustering") clustering: Boolean,
|
||||
@Query("zoom") zoom: Float,
|
||||
@Query("cluster_distance") clusterDistance: Int?,
|
||||
@Query("freecharging") freecharging: Boolean,
|
||||
@Query("freeparking") freeparking: Boolean,
|
||||
@Query("min_power") minPower: Int,
|
||||
@Query("plugs") plugs: String?
|
||||
@Query("clustering") clustering: Boolean = false,
|
||||
@Query("cluster_distance") clusterDistance: Int? = null,
|
||||
@Query("freecharging") freecharging: Boolean = false,
|
||||
@Query("freeparking") freeparking: Boolean = false,
|
||||
@Query("min_power") minPower: Int = 0,
|
||||
@Query("plugs") plugs: String? = null,
|
||||
@Query("chargecards") chargecards: String? = null,
|
||||
@Query("networks") networks: String? = null,
|
||||
@Query("startkey") startkey: Int? = null
|
||||
): Response<ChargepointList>
|
||||
|
||||
@GET("chargepoints/")
|
||||
@@ -36,7 +40,7 @@ interface GoingElectricApi {
|
||||
suspend fun getNetworks(): Response<StringList>
|
||||
|
||||
@GET("chargepoints/chargecardlist/")
|
||||
suspend fun getChargeCards(): Response<StringList>
|
||||
suspend fun getChargeCards(): Response<ChargeCardList>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 10L * 1024 * 1024; // 10MB
|
||||
@@ -54,7 +58,9 @@ interface GoingElectricApi {
|
||||
original = original.newBuilder().url(url).build()
|
||||
chain.proceed(original)
|
||||
}
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
if (BuildConfig.DEBUG) {
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.getCacheDir(), cacheSize))
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import java.time.DayOfWeek
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
@@ -22,7 +24,8 @@ import kotlin.math.floor
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepointList(
|
||||
val status: String,
|
||||
val chargelocations: List<ChargepointListItem>
|
||||
val chargelocations: List<ChargepointListItem>,
|
||||
@JsonObjectOrFalse val startkey: Int?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -31,6 +34,12 @@ data class StringList(
|
||||
val result: List<String>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCardList(
|
||||
val status: String,
|
||||
val result: List<ChargeCard>
|
||||
)
|
||||
|
||||
sealed class ChargepointListItem
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -158,9 +167,12 @@ data class OpeningHoursDays(
|
||||
) {
|
||||
fun getHoursForDate(date: LocalDate): Hours {
|
||||
// TODO: check for holidays
|
||||
return getHoursForDayOfWeek(date.dayOfWeek)
|
||||
}
|
||||
|
||||
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours {
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
return when (date.dayOfWeek) {
|
||||
return when (dayOfWeek) {
|
||||
DayOfWeek.MONDAY -> monday
|
||||
DayOfWeek.TUESDAY -> tuesday
|
||||
DayOfWeek.WEDNESDAY -> wednesday
|
||||
@@ -168,6 +180,7 @@ data class OpeningHoursDays(
|
||||
DayOfWeek.FRIDAY -> friday
|
||||
DayOfWeek.SATURDAY -> saturday
|
||||
DayOfWeek.SUNDAY -> sunday
|
||||
null -> holiday
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +188,16 @@ data class OpeningHoursDays(
|
||||
data class Hours(
|
||||
val start: LocalTime?,
|
||||
val end: LocalTime?
|
||||
)
|
||||
) {
|
||||
override fun toString(): String {
|
||||
if (start != null && end != null) {
|
||||
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
return "${start.format(fmt)} - ${end.format(fmt)}"
|
||||
} else {
|
||||
return "closed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
@@ -248,4 +270,12 @@ data class Chargepoint(val type: String, val power: Double, val count: Int) : Eq
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FaultReport(val created: Instant?, val description: String?)
|
||||
data class FaultReport(val created: Instant?, val description: String?)
|
||||
|
||||
@Entity
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCard(
|
||||
@Json(name = "card_id") @PrimaryKey val id: Long,
|
||||
val name: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.gms.location.FusedLocationProviderClient
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.android.libraries.maps.model.LatLng
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.FavoritesAdapter
|
||||
|
||||
@@ -10,7 +10,6 @@ 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
|
||||
@@ -32,16 +31,18 @@ import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.transition.TransitionInflater
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.gms.location.FusedLocationProviderClient
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.maps.CameraUpdateFactory
|
||||
import com.google.android.gms.maps.GoogleMap
|
||||
import com.google.android.gms.maps.OnMapReadyCallback
|
||||
import com.google.android.gms.maps.SupportMapFragment
|
||||
import com.google.android.gms.maps.model.*
|
||||
import com.google.android.libraries.maps.CameraUpdateFactory
|
||||
import com.google.android.libraries.maps.GoogleMap
|
||||
import com.google.android.libraries.maps.OnMapReadyCallback
|
||||
import com.google.android.libraries.maps.SupportMapFragment
|
||||
import com.google.android.libraries.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.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialArcMotion
|
||||
import com.google.android.material.transition.MaterialContainerTransform
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
@@ -88,6 +89,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
|
||||
private var clusterMarkers: List<Marker> = emptyList()
|
||||
private var searchResultMarker: Marker? = null
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
|
||||
private lateinit var clusterIconGenerator: ClusterIconGenerator
|
||||
private lateinit var chargerIconGenerator: ChargerIconGenerator
|
||||
@@ -250,7 +252,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val materialTransform = MaterialContainerTransform().apply {
|
||||
startView = binding.fabLayers
|
||||
endView = binding.layersSheet
|
||||
pathMotion = MaterialArcMotion()
|
||||
setPathMotion(MaterialArcMotion())
|
||||
duration = 250
|
||||
scrimColor = Color.TRANSPARENT
|
||||
}
|
||||
@@ -263,7 +265,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val materialTransform = MaterialContainerTransform().apply {
|
||||
startView = binding.layersSheet
|
||||
endView = binding.fabLayers
|
||||
pathMotion = MaterialArcMotion()
|
||||
setPathMotion(MaterialArcMotion())
|
||||
duration = 200
|
||||
scrimColor = Color.TRANSPARENT
|
||||
}
|
||||
@@ -311,9 +313,31 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
unhighlightAllMarkers()
|
||||
}
|
||||
})
|
||||
vm.chargepoints.observe(viewLifecycleOwner, Observer {
|
||||
val chargepoints = it.data
|
||||
if (chargepoints != null) updateMap(chargepoints)
|
||||
vm.chargepoints.observe(viewLifecycleOwner, Observer { res ->
|
||||
when (res.status) {
|
||||
Status.ERROR -> {
|
||||
val view = view ?: return@Observer
|
||||
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
connectionErrorSnackbar = Snackbar
|
||||
.make(view, R.string.connection_error, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.retry) {
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
vm.reloadChargepoints()
|
||||
}
|
||||
connectionErrorSnackbar!!.show()
|
||||
}
|
||||
Status.SUCCESS -> {
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
}
|
||||
Status.LOADING -> {
|
||||
}
|
||||
}
|
||||
|
||||
val chargepoints = res.data
|
||||
if (chargepoints != null) {
|
||||
updateMap(chargepoints)
|
||||
}
|
||||
})
|
||||
vm.favorites.observe(viewLifecycleOwner, Observer {
|
||||
updateFavoriteToggle()
|
||||
@@ -346,11 +370,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.mapTrafficEnabled.observe(viewLifecycleOwner, Observer {
|
||||
map?.isTrafficEnabled = it
|
||||
})
|
||||
|
||||
updateBackPressedCallback()
|
||||
}
|
||||
|
||||
private fun updateBackPressedCallback() {
|
||||
backPressedCallback.isEnabled =
|
||||
vm.bottomSheetState.value != STATE_HIDDEN || vm.searchResult.value != null
|
||||
vm.bottomSheetState.value != null && vm.bottomSheetState.value != STATE_HIDDEN
|
||||
|| vm.searchResult.value != null
|
||||
|| (vm.layersMenuOpen.value ?: false)
|
||||
}
|
||||
|
||||
@@ -691,9 +718,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
filterView?.setOnLongClickListener {
|
||||
requireView().findNavController().navigate(
|
||||
R.id.action_map_to_filterFragment
|
||||
// enable/disable filters
|
||||
vm.filtersActive.value = !vm.filtersActive.value!!
|
||||
// haptic feedback
|
||||
filterView.performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS,
|
||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
||||
)
|
||||
// show snackbar
|
||||
Snackbar.make(
|
||||
requireView(), if (vm.filtersActive.value!!) {
|
||||
R.string.filters_activated
|
||||
} else {
|
||||
R.string.filters_deactivated
|
||||
}, Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.dialog_multi_select.*
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.DataBindingAdapter
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
companion object {
|
||||
fun getInstance(
|
||||
title: String,
|
||||
data: Map<String, String>,
|
||||
selected: Set<String>
|
||||
): MultiSelectDialog {
|
||||
val dialog = MultiSelectDialog()
|
||||
dialog.arguments = Bundle().apply {
|
||||
putString("title", title)
|
||||
putSerializable("data", HashMap(data))
|
||||
putSerializable("selected", HashSet(selected))
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
|
||||
var okListener: ((Set<String>) -> Unit)? = null
|
||||
var cancelListener: (() -> Unit)? = null
|
||||
private lateinit var items: List<MultiSelectItem>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.dialog_multi_select, container)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
// dialog with 95% screen height
|
||||
dialog?.window?.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
(resources.displayMetrics.heightPixels * 0.95).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val data = requireArguments().getSerializable("data") as HashMap<String, String>
|
||||
val selected = requireArguments().getSerializable("selected") as HashSet<String>
|
||||
val title = requireArguments().getString("title")
|
||||
|
||||
dialogTitle.text = title
|
||||
val adapter = Adapter()
|
||||
list.adapter = adapter
|
||||
list.layoutManager = LinearLayoutManager(view.context)
|
||||
|
||||
items = data.entries.toList().sortedBy { it.key }.map {
|
||||
MultiSelectItem(it.key, it.value, it.key in selected)
|
||||
}
|
||||
adapter.submitList(items)
|
||||
|
||||
etSearch.doAfterTextChanged { text ->
|
||||
adapter.submitList(search(items, text.toString()))
|
||||
}
|
||||
|
||||
btnCancel.setOnClickListener {
|
||||
cancelListener?.let { listener ->
|
||||
listener()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
btnOK.setOnClickListener {
|
||||
okListener?.let { listener ->
|
||||
val result = items.filter { it.selected }.map { it.key }.toSet()
|
||||
listener(result)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
btnAll.setOnClickListener {
|
||||
items = items.map { MultiSelectItem(it.key, it.name, true) }
|
||||
adapter.submitList(search(items, etSearch.text.toString()))
|
||||
}
|
||||
btnNone.setOnClickListener {
|
||||
items = items.map { MultiSelectItem(it.key, it.name, false) }
|
||||
adapter.submitList(search(items, etSearch.text.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun search(
|
||||
items: List<MultiSelectItem>,
|
||||
text: String
|
||||
): List<MultiSelectItem> {
|
||||
return items.filter { item ->
|
||||
// search for string within name
|
||||
text.toLowerCase(Locale.getDefault()) in item.name.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
}
|
||||
|
||||
class Adapter() : DataBindingAdapter<MultiSelectItem>({ it.key }) {
|
||||
override fun getItemViewType(position: Int) = R.layout.dialog_multi_select_item
|
||||
}
|
||||
}
|
||||
|
||||
data class MultiSelectItem(val key: String, val name: String, var selected: Boolean) : Equatable
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@@ -11,7 +12,8 @@ import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
class SettingsFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
@@ -33,4 +35,26 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
"language" -> {
|
||||
activity?.let {
|
||||
it.finish();
|
||||
it.startActivity(it.intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
preferenceManager.sharedPreferences
|
||||
.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Dao
|
||||
interface ChargeCardDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg chargeCards: ChargeCard)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg chargeCards: ChargeCard)
|
||||
|
||||
@Query("SELECT * FROM chargeCard")
|
||||
fun getAllChargeCards(): LiveData<List<ChargeCard>>
|
||||
}
|
||||
|
||||
class ChargeCardRepository(
|
||||
private val api: GoingElectricApi, private val scope: CoroutineScope,
|
||||
private val dao: ChargeCardDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getChargeCards(): LiveData<List<ChargeCard>> {
|
||||
scope.launch {
|
||||
updateChargeCards()
|
||||
}
|
||||
return dao.getAllChargeCards()
|
||||
}
|
||||
|
||||
private suspend fun updateChargeCards() {
|
||||
if (Duration.between(prefs.lastChargeCardUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
val response = api.getChargeCards()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (card in response.body()!!.result) {
|
||||
dao.insert(card)
|
||||
}
|
||||
|
||||
prefs.lastChargeCardUpdate = Instant.now()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.viewmodel.BooleanFilterValue
|
||||
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
|
||||
@@ -18,20 +19,27 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
|
||||
BooleanFilterValue::class,
|
||||
MultipleChoiceFilterValue::class,
|
||||
SliderFilterValue::class,
|
||||
Plug::class
|
||||
], version = 6
|
||||
Plug::class,
|
||||
Network::class,
|
||||
ChargeCard::class
|
||||
], version = 7
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun chargeLocationsDao(): ChargeLocationsDao
|
||||
abstract fun filterValueDao(): FilterValueDao
|
||||
abstract fun plugDao(): PlugDao
|
||||
abstract fun networkDao(): NetworkDao
|
||||
abstract fun chargeCardDao(): ChargeCardDao
|
||||
|
||||
companion object {
|
||||
private lateinit var context: Context
|
||||
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db")
|
||||
.addMigrations(MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6)
|
||||
.addMigrations(
|
||||
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
|
||||
MIGRATION_7
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -99,5 +107,12 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_7 = object : Migration(6, 7) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `Network` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `ChargeCard` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt
Normal file
49
app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
data class Network(@PrimaryKey val name: String)
|
||||
|
||||
@Dao
|
||||
interface NetworkDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg networks: Network)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg networks: Network)
|
||||
|
||||
@Query("SELECT * FROM network")
|
||||
fun getAllNetworks(): LiveData<List<Network>>
|
||||
}
|
||||
|
||||
class NetworkRepository(
|
||||
private val api: GoingElectricApi, private val scope: CoroutineScope,
|
||||
private val dao: NetworkDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getNetworks(): LiveData<List<Network>> {
|
||||
scope.launch {
|
||||
updateNetworks()
|
||||
}
|
||||
return dao.getAllNetworks()
|
||||
}
|
||||
|
||||
private suspend fun updateNetworks() {
|
||||
if (Duration.between(prefs.lastNetworkUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
val response = api.getNetworks()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Network(name))
|
||||
}
|
||||
|
||||
prefs.lastNetworkUpdate = Instant.now()
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,19 @@ class PreferenceDataSource(context: Context) {
|
||||
set(value) {
|
||||
sp.edit().putLong("last_plug_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
var lastNetworkUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_network_update", 0L))
|
||||
set(value) {
|
||||
sp.edit().putLong("last_network_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
var lastChargeCardUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_chargecard_update", 0L))
|
||||
set(value) {
|
||||
sp.edit().putLong("last_chargecard_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
val language: String
|
||||
get() = sp.getString("language", "default")!!
|
||||
}
|
||||
@@ -2,10 +2,8 @@ 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.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -19,6 +17,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@BindingAdapter("goneUnless")
|
||||
@@ -122,6 +121,16 @@ fun setHtmlTextValue(textView: TextView, htmlText: String?) {
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("android:layout_marginTop")
|
||||
fun setTopMargin(view: View, topMargin: Float) {
|
||||
val layoutParams = view.layoutParams as MarginLayoutParams
|
||||
layoutParams.setMargins(
|
||||
layoutParams.leftMargin, topMargin.roundToInt(),
|
||||
layoutParams.rightMargin, layoutParams.bottomMargin
|
||||
)
|
||||
view.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
private fun availabilityColor(
|
||||
status: List<ChargepointStatus>?,
|
||||
context: Context
|
||||
|
||||
41
app/src/main/java/net/vonforst/evmap/ui/Clustering.kt
Normal file
41
app/src/main/java/net/vonforst/evmap/ui/Clustering.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package net.vonforst.evmap.ui;
|
||||
|
||||
import com.google.android.libraries.maps.model.LatLng
|
||||
import com.google.maps.android.clustering.ClusterItem
|
||||
import com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocationCluster
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.api.goingelectric.Coordinate
|
||||
|
||||
|
||||
fun cluster(
|
||||
result: List<ChargepointListItem>,
|
||||
zoom: Float,
|
||||
clusterDistance: Int
|
||||
): List<ChargepointListItem> {
|
||||
val clusters = result.filterIsInstance<ChargeLocationCluster>()
|
||||
val locations = result.filterIsInstance<ChargeLocation>()
|
||||
|
||||
val clusterItems = locations.map { ChargepointClusterItem(it) }
|
||||
|
||||
val algo = NonHierarchicalDistanceBasedAlgorithm<ChargepointClusterItem>()
|
||||
algo.maxDistanceBetweenClusteredItems = clusterDistance
|
||||
algo.addItems(clusterItems)
|
||||
return algo.getClusters(zoom).map {
|
||||
if (it.size == 1) {
|
||||
it.items.first().charger
|
||||
} else {
|
||||
ChargeLocationCluster(it.size, Coordinate(it.position.latitude, it.position.longitude))
|
||||
}
|
||||
} + clusters
|
||||
}
|
||||
|
||||
private class ChargepointClusterItem(val charger: ChargeLocation) : ClusterItem {
|
||||
override fun getSnippet(): String? = null
|
||||
|
||||
override fun getTitle(): String? = charger.name
|
||||
|
||||
override fun getPosition(): LatLng = LatLng(charger.coordinates.lat, charger.coordinates.lng)
|
||||
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.google.android.gms.maps.model.BitmapDescriptor
|
||||
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
||||
import com.google.android.libraries.maps.model.BitmapDescriptor
|
||||
import com.google.android.libraries.maps.model.BitmapDescriptorFactory
|
||||
import com.google.maps.android.ui.IconGenerator
|
||||
import com.google.maps.android.ui.SquareTextView
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.view.animation.BounceInterpolator
|
||||
import androidx.core.animation.addListener
|
||||
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||
import com.google.android.gms.maps.model.Marker
|
||||
import com.google.android.libraries.maps.model.Marker
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package net.vonforst.evmap.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import java.util.*
|
||||
|
||||
|
||||
class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||
companion object {
|
||||
fun wrap(context: Context, language: String): ContextWrapper {
|
||||
val config: Configuration = context.resources.configuration
|
||||
var sysLocale: Locale? = null
|
||||
sysLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
config.locales.get(0)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
config.locale
|
||||
}
|
||||
var ctx = context
|
||||
if (language != "" && language != "default" && sysLocale.language != language) {
|
||||
val locale = Locale(language)
|
||||
Locale.setDefault(locale)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
config.setLocale(locale)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
config.locale = locale
|
||||
}
|
||||
ctx = context.createConfigurationContext(config)
|
||||
}
|
||||
return LocaleContextWrapper(ctx)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
fun setSystemLocale(config: Configuration, locale: Locale?) {
|
||||
config.setLocale(locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.*
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.android.libraries.maps.model.LatLng
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -10,12 +10,10 @@ import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.Plug
|
||||
import net.vonforst.evmap.storage.PlugRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.storage.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.cast
|
||||
@@ -28,7 +26,9 @@ internal fun mapPowerInverse(power: Int) = powerSteps
|
||||
|
||||
internal fun getFilters(
|
||||
application: Application,
|
||||
plugs: LiveData<List<Plug>>
|
||||
plugs: LiveData<List<Plug>>,
|
||||
networks: LiveData<List<Network>>,
|
||||
chargeCards: LiveData<List<ChargeCard>>
|
||||
): LiveData<List<Filter<FilterValue>>> {
|
||||
return MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
val plugNames = mapOf(
|
||||
@@ -42,35 +42,57 @@ internal fun getFilters(
|
||||
Chargepoint.CEE_BLAU to application.getString(R.string.plug_cee_blau),
|
||||
Chargepoint.CEE_ROT to application.getString(R.string.plug_cee_rot)
|
||||
)
|
||||
addSource(plugs) { plugs ->
|
||||
val plugMap = plugs.map { plug ->
|
||||
plug.name to (plugNames[plug.name] ?: plug.name)
|
||||
}.toMap()
|
||||
value = listOf(
|
||||
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
|
||||
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_power), "min_power",
|
||||
powerSteps.size - 1,
|
||||
mapping = ::mapPower,
|
||||
inverseMapping = ::mapPowerInverse,
|
||||
unit = "kW"
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_connectors), "connectors",
|
||||
plugMap,
|
||||
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO)
|
||||
),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10
|
||||
)
|
||||
)
|
||||
listOf(plugs, networks, chargeCards).forEach { source ->
|
||||
addSource(source) { _ ->
|
||||
buildFilters(plugs, plugNames, networks, chargeCards, application)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
|
||||
plugs: LiveData<List<Plug>>,
|
||||
plugNames: Map<String, String>,
|
||||
networks: LiveData<List<Network>>,
|
||||
chargeCards: LiveData<List<ChargeCard>>,
|
||||
application: Application
|
||||
) {
|
||||
val plugMap = plugs.value?.map { plug ->
|
||||
plug.name to (plugNames[plug.name] ?: plug.name)
|
||||
}?.toMap() ?: return
|
||||
val networkMap = networks.value?.map { it.name to it.name }?.toMap() ?: return
|
||||
val chargecardMap = chargeCards.value?.map { it.id.toString() to it.name }?.toMap() ?: return
|
||||
value = listOf(
|
||||
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
|
||||
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_power), "min_power",
|
||||
powerSteps.size - 1,
|
||||
mapping = ::mapPower,
|
||||
inverseMapping = ::mapPowerInverse,
|
||||
unit = "kW"
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_connectors), "connectors",
|
||||
plugMap,
|
||||
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO)
|
||||
),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_networks), "networks",
|
||||
networkMap, manyChoices = true
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_chargecards), "chargecards",
|
||||
chargecardMap, manyChoices = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal fun filtersWithValue(
|
||||
filters: LiveData<List<Filter<FilterValue>>>,
|
||||
@@ -107,9 +129,14 @@ class FilterViewModel(application: Application, geApiKey: String) :
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
}
|
||||
|
||||
private val networks: LiveData<List<Network>> by lazy {
|
||||
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
|
||||
}
|
||||
private val chargeCards: LiveData<List<ChargeCard>> by lazy {
|
||||
ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards()
|
||||
}
|
||||
private val filters: LiveData<List<Filter<FilterValue>>> by lazy {
|
||||
getFilters(application, plugs)
|
||||
getFilters(application, plugs, networks, chargeCards)
|
||||
}
|
||||
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
@@ -144,7 +171,8 @@ data class MultipleChoiceFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val choices: Map<String, String>,
|
||||
val commonChoices: Set<String>? = null
|
||||
val commonChoices: Set<String>? = null,
|
||||
val manyChoices: Boolean = false
|
||||
) : Filter<MultipleChoiceFilterValue>() {
|
||||
override val valueClass: KClass<MultipleChoiceFilterValue> = MultipleChoiceFilterValue::class
|
||||
override fun defaultValue() = MultipleChoiceFilterValue(key, mutableSetOf(), true)
|
||||
|
||||
@@ -2,24 +2,21 @@ 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.maps.GoogleMap
|
||||
import com.google.android.libraries.maps.model.LatLngBounds
|
||||
import com.google.android.libraries.places.api.model.Place
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointList
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.Plug
|
||||
import net.vonforst.evmap.storage.PlugRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.api.goingelectric.*
|
||||
import net.vonforst.evmap.storage.*
|
||||
import net.vonforst.evmap.ui.cluster
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
|
||||
|
||||
@@ -52,7 +49,13 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
}
|
||||
private val filters = getFilters(application, plugs)
|
||||
private val networks: LiveData<List<Network>> by lazy {
|
||||
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
|
||||
}
|
||||
private val chargeCards: LiveData<List<ChargeCard>> by lazy {
|
||||
ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards()
|
||||
}
|
||||
private val filters = getFilters(application, plugs, networks, chargeCards)
|
||||
|
||||
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
|
||||
filtersWithValue(filters, filterValues, filtersActive)
|
||||
@@ -74,9 +77,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
value = Resource.loading(emptyList())
|
||||
listOf(mapPosition, filtersWithValue).forEach {
|
||||
addSource(it) {
|
||||
val pos = mapPosition.value ?: return@addSource
|
||||
val filters = filtersWithValue.value ?: return@addSource
|
||||
loadChargepoints(pos, filters)
|
||||
reloadChargepoints()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,6 +173,12 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadChargepoints() {
|
||||
val pos = mapPosition.value ?: return
|
||||
val filters = filtersWithValue.value ?: return
|
||||
loadChargepoints(pos, filters)
|
||||
}
|
||||
|
||||
private fun loadChargepoints(
|
||||
mapPosition: MapPosition,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
@@ -191,54 +198,110 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
zoom: Float,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val freecharging =
|
||||
(filters.find { it.value.key == "freecharging" }!!.value as BooleanFilterValue).value
|
||||
val freeparking =
|
||||
(filters.find { it.value.key == "freeparking" }!!.value as BooleanFilterValue).value
|
||||
val minPower =
|
||||
(filters.find { it.value.key == "min_power" }!!.value as SliderFilterValue).value
|
||||
val minConnectors =
|
||||
(filters.find { it.value.key == "min_connectors" }!!.value as SliderFilterValue).value
|
||||
val freecharging = getBooleanValue(filters, "freecharging")
|
||||
val freeparking = getBooleanValue(filters, "freeparking")
|
||||
val minPower = getSliderValue(filters, "min_power")
|
||||
val minConnectors = getSliderValue(filters, "min_connectors")
|
||||
|
||||
val connectorsVal =
|
||||
filters.find { it.value.key == "connectors" }!!.value as MultipleChoiceFilterValue
|
||||
val connectors = if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
|
||||
val connectorsVal = getMultipleChoiceValue(filters, "connectors")
|
||||
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
|
||||
val chargeCardsVal = getMultipleChoiceValue(filters, "chargecards")
|
||||
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
|
||||
// no chargeCards chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val chargeCards = formatMultipleChoice(chargeCardsVal)
|
||||
|
||||
val networksVal = getMultipleChoiceValue(filters, "networks")
|
||||
if (networksVal.values.isEmpty() && !networksVal.all) {
|
||||
// no networks chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val networks = formatMultipleChoice(networksVal)
|
||||
|
||||
// do not use clustering if filters need to be applied locally.
|
||||
val useClustering = minConnectors <= 1 && zoom < 13
|
||||
val useClustering = zoom < 13
|
||||
val geClusteringAvailable = minConnectors <= 1
|
||||
val useGeClustering = useClustering && geClusteringAvailable
|
||||
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 = useClustering, zoom = zoom,
|
||||
clusterDistance = clusterDistance, freecharging = freecharging, minPower = minPower,
|
||||
freeparking = freeparking, plugs = connectors
|
||||
)
|
||||
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Resource.error(response.message(), chargepoints.value?.data)
|
||||
} else {
|
||||
val data = response.body()!!.chargelocations.filter { it ->
|
||||
// apply filters which GoingElectric does not support natively
|
||||
if (it is ChargeLocation) {
|
||||
it.chargepoints
|
||||
.filter { it.power >= minPower }
|
||||
.filter { if (!connectorsVal.all) it.type in connectorsVal.values else true }
|
||||
.sumBy { it.count } >= minConnectors
|
||||
var startkey: Int? = null
|
||||
val data = mutableListOf<ChargepointListItem>()
|
||||
do {
|
||||
// load all pages of the response
|
||||
try {
|
||||
val response = api.getChargepoints(
|
||||
bounds.southwest.latitude,
|
||||
bounds.southwest.longitude,
|
||||
bounds.northeast.latitude,
|
||||
bounds.northeast.longitude,
|
||||
clustering = useGeClustering,
|
||||
zoom = zoom,
|
||||
clusterDistance = clusterDistance,
|
||||
freecharging = freecharging,
|
||||
minPower = minPower,
|
||||
freeparking = freeparking,
|
||||
plugs = connectors,
|
||||
chargecards = chargeCards,
|
||||
networks = networks,
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Resource.error(response.message(), chargepoints.value?.data)
|
||||
} else {
|
||||
true
|
||||
val body = response.body()!!
|
||||
data.addAll(body.chargelocations)
|
||||
startkey = body.startkey
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, chargepoints.value?.data)
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
|
||||
return Resource.success(data)
|
||||
var result = data.filter { it ->
|
||||
// apply filters which GoingElectric does not support natively
|
||||
if (it is ChargeLocation) {
|
||||
it.chargepoints
|
||||
.filter { it.power >= minPower }
|
||||
.filter { if (!connectorsVal.all) it.type in connectorsVal.values else true }
|
||||
.sumBy { it.count } >= minConnectors
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
if (!geClusteringAvailable && useClustering) {
|
||||
// apply local clustering if server side clustering is not available
|
||||
Dispatchers.IO.run {
|
||||
result = cluster(result, zoom, clusterDistance!!)
|
||||
}
|
||||
}
|
||||
|
||||
return Resource.success(result)
|
||||
}
|
||||
|
||||
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =
|
||||
if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
|
||||
|
||||
private fun getBooleanValue(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = (filters.find { it.value.key == key }!!.value as BooleanFilterValue).value
|
||||
|
||||
private fun getSliderValue(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = (filters.find { it.value.key == key }!!.value as SliderFilterValue).value
|
||||
|
||||
private fun getMultipleChoiceValue(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = filters.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
|
||||
|
||||
private suspend fun loadAvailability(charger: ChargeLocation) {
|
||||
availability.value = Resource.loading(null)
|
||||
availability.value = getAvailability(charger)
|
||||
|
||||
9
app/src/main/res/drawable/expand_toggle.xml
Normal file
9
app/src/main/res/drawable/expand_toggle.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?selectableItemBackgroundBorderless"/>
|
||||
<item android:drawable="@drawable/expand_toggle_icon"
|
||||
android:top="4dp"
|
||||
android:left="4dp"
|
||||
android:right="4dp"
|
||||
android:bottom="4dp"/>
|
||||
</layer-list>
|
||||
5
app/src/main/res/drawable/expand_toggle_icon.xml
Normal file
5
app/src/main/res/drawable/expand_toggle_icon.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="false" android:drawable="@drawable/ic_expand" />
|
||||
<item android:state_checked="true" android:drawable="@drawable/ic_collapse" />
|
||||
</selector>
|
||||
10
app/src/main/res/drawable/ic_collapse.xml
Normal file
10
app/src/main/res/drawable/ic_collapse.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="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_edit.xml
Normal file
10
app/src/main/res/drawable/ic_edit.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="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_expand.xml
Normal file
10
app/src/main/res/drawable/ic_expand.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="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_search.xml
Normal file
10
app/src/main/res/drawable/ic_search.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="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||
</vector>
|
||||
@@ -30,7 +30,9 @@
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardCornerRadius="@dimen/detail_corner_radius"
|
||||
android:layout_marginBottom="@dimen/detail_corner_radius_negative"
|
||||
android:paddingBottom="@dimen/detail_corner_radius"
|
||||
app:cardElevation="6dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
92
app/src/main/res/layout/dialog_multi_select.xml
Normal file
92
app/src/main/res/layout/dialog_multi_select.xml
Normal file
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialogTitle"
|
||||
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnAll"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Select Something" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnOK"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilSearch" />
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnAll"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/all"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/dialogTitle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnNone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnOK"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/ok"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCancel"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnOK" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnNone"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/none"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/btnAll"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilSearch"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:hint="@string/search"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btnAll"
|
||||
app:startIconDrawable="@drawable/ic_search">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etSearch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
27
app/src/main/res/layout/dialog_multi_select_item.xml
Normal file
27
app/src/main/res/layout/dialog_multi_select_item.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.fragment.MultiSelectItem" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="MultiSelectItem" />
|
||||
</data>
|
||||
|
||||
<CheckBox
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||
android:layout_marginStart="?attr/dialogPreferredPadding"
|
||||
android:layout_marginEnd="?attr/dialogPreferredPadding"
|
||||
android:paddingStart="20dp"
|
||||
android:ellipsize="marquee"
|
||||
android:text="@{item.name}"
|
||||
android:checked="@={item.selected}"
|
||||
tools:text="Item"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</layout>
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<fragment
|
||||
android:id="@+id/map"
|
||||
android:name="com.google.android.gms.maps.SupportMapFragment"
|
||||
android:name="com.google.android.libraries.maps.SupportMapFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MapsActivity" />
|
||||
|
||||
204
app/src/main/res/layout/item_detail_openinghours.xml
Normal file
204
app/src/main/res/layout/item_detail_openinghours.xml
Normal file
@@ -0,0 +1,204 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.text.util.Linkify" />
|
||||
|
||||
<import type="java.time.DayOfWeek" />
|
||||
|
||||
<import type="android.transition.TransitionManager" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="net.vonforst.evmap.adapter.DetailAdapter.Detail" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="@{item.clickable}"
|
||||
app:selectableItemBackground="@{item.clickable}">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:maxLines="1"
|
||||
android:text="@{item.text}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/expandToggle"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView3"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Lorem ipsum" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:contentDescription="@{item.contentDescription}"
|
||||
android:tint="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@{item.icon}"
|
||||
tools:srcCompat="@drawable/ic_address" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:autoLink="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
|
||||
android:linksClickable="@{item.links}"
|
||||
android:text="@{item.detailText}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:goneUnless="@{item.detailText != null}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/expandToggle"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView9"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Lorem ipsum" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_mon"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:dayOfWeek="@{DayOfWeek.MONDAY}"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView8" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_tue"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.TUESDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_mon" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_wed"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.WEDNESDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_tue" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_thu"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.THURSDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_wed" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_fri"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.FRIDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_thu" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_sat"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.SATURDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_fri" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_sun"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.SUNDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_sat" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_holiday"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{null}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_sun" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/expandToggle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginTop="@{item.detailText != null ? @dimen/expand_toggle_padding_large : @dimen/expand_toggle_padding_small}"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/expand_toggle"
|
||||
android:onClick="@{() -> TransitionManager.beginDelayedTransition(container)}"
|
||||
android:textOff=""
|
||||
android:textOn=""
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
48
app/src/main/res/layout/item_detail_openinghours_item.xml
Normal file
48
app/src/main/res/layout/item_detail_openinghours_item.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="java.time.format.TextStyle" />
|
||||
|
||||
<import type="java.util.Locale" />
|
||||
|
||||
<variable
|
||||
name="hours"
|
||||
type="net.vonforst.evmap.api.goingelectric.OpeningHoursDays" />
|
||||
|
||||
<variable
|
||||
name="dayOfWeek"
|
||||
type="java.time.DayOfWeek" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView24"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{dayOfWeek != null ? dayOfWeek.getDisplayName(TextStyle.FULL, context.resources.configuration.locale) : @string/holiday}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Montag" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView25"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="100dp"
|
||||
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek).toString().equals("closed") ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="07:00-21:00" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="FilterWithValue<MultipleChoiceFilterValue>" />
|
||||
|
||||
<variable
|
||||
name="showingAll"
|
||||
type="boolean" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView17"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{item.filter.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnEdit"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Networks" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnEdit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_edit"
|
||||
android:contentDescription="@string/edit" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView26"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{item.value.all ? @string/all_selected : @string/number_selected(item.value.values.size())}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnEdit"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView17"
|
||||
tools:text="4 selected" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MapViewModel" />
|
||||
|
||||
<import type="com.google.android.gms.maps.GoogleMap" />
|
||||
<import type="com.google.android.libraries.maps.GoogleMap" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
|
||||
8
app/src/main/res/values-de/arrays.xml
Normal file
8
app/src/main/res/values-de/arrays.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_language_names">
|
||||
<item>Gerätesprache verwenden</item>
|
||||
<item>Englisch</item>
|
||||
<item>Deutsch</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -10,6 +10,8 @@
|
||||
<string name="hours">Öffnungszeiten</string>
|
||||
<string name="open_247"><![CDATA[<b>24 Stunden geöffnet</b>]]></string>
|
||||
<string name="closed"><![CDATA[<b>Geschlossen</b>]]></string>
|
||||
<string name="closed_unfmt">Geschlossen</string>
|
||||
<string name="holiday">Feiertag</string>
|
||||
<string name="open_closesat"><![CDATA[<b>Geöffnet</b> · Schließt um %s]]></string>
|
||||
<string name="closed_opensat"><![CDATA[<b>Geschlossen</b> · Öffnet um %s]]></string>
|
||||
<string name="cost">Kosten</string>
|
||||
@@ -75,8 +77,21 @@
|
||||
<string name="map_traffic">Verkehr</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="menu_filters_active">Filter aktiv</string>
|
||||
<string name="filters_activated">Filter aktiviert</string>
|
||||
<string name="filters_deactivated">Filter deaktiviert</string>
|
||||
<string name="menu_edit_filters">Filter bearbeiten…</string>
|
||||
<string name="go_to_chargeprice"><![CDATA[Preisvergleich<br/><small>mit Chargeprice.app</small>]]></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>
|
||||
<string name="filter_networks">Verbünde</string>
|
||||
<string name="filter_chargecards">Ladetarife</string>
|
||||
<string name="all_selected">Alle ausgewählt</string>
|
||||
<string name="number_selected">%d ausgewählt</string>
|
||||
<string name="edit">bearbeiten</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Sprache</string>
|
||||
<string name="pref_language_summary">App-Sprache ändern</string>
|
||||
<string name="connection_error">Ladesäulen konnten nicht geladen werden</string>
|
||||
<string name="retry">Wiederholen</string>
|
||||
</resources>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="AppTheme" parent="AppTheme.Base">
|
||||
<item name="android:navigationBarColor">@android:color/white</item>
|
||||
</style>
|
||||
</resources>
|
||||
13
app/src/main/res/values/arrays.xml
Normal file
13
app/src/main/res/values/arrays.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_language_names">
|
||||
<item>Device default</item>
|
||||
<item>English</item>
|
||||
<item>German</item>
|
||||
</string-array>
|
||||
<string-array name="pref_language_values" tranlatable="false">
|
||||
<item>default</item>
|
||||
<item>en</item>
|
||||
<item>de</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -3,4 +3,8 @@
|
||||
<dimen name="peek_height">72dp</dimen>
|
||||
<dimen name="gallery_height">200dp</dimen>
|
||||
<dimen name="gallery_height_with_margin">208dp</dimen>
|
||||
<dimen name="detail_corner_radius">8dp</dimen>
|
||||
<dimen name="detail_corner_radius_negative">-8dp</dimen>
|
||||
<dimen name="expand_toggle_padding_large">16dp</dimen>
|
||||
<dimen name="expand_toggle_padding_small">8dp</dimen>
|
||||
</resources>
|
||||
@@ -11,6 +11,8 @@
|
||||
<string name="closed"><![CDATA[<b>Closed</b>]]></string>
|
||||
<string name="open_closesat"><![CDATA[<b>Open</b> · Closes at %s]]></string>
|
||||
<string name="closed_opensat"><![CDATA[<b>Closed</b> · Opens at %s]]></string>
|
||||
<string name="closed_unfmt">Closed</string>
|
||||
<string name="holiday">Holiday</string>
|
||||
<string name="cost">Cost</string>
|
||||
<string name="cost_detail"><![CDATA[<b>Charging:</b> %s · <b>Parking:</b> %s]]></string>
|
||||
<string name="free">Free</string>
|
||||
@@ -74,8 +76,21 @@
|
||||
<string name="map_traffic">Traffic</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="menu_filters_active">Filters active</string>
|
||||
<string name="filters_activated">Filters activated</string>
|
||||
<string name="filters_deactivated">Filters deactivated</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>
|
||||
<string name="filter_networks">Networks</string>
|
||||
<string name="filter_chargecards">Payment methods</string>
|
||||
<string name="all_selected">All selected</string>
|
||||
<string name="number_selected">%d selected</string>
|
||||
<string name="edit">edit</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Language</string>
|
||||
<string name="pref_language_summary">Change the app language</string>
|
||||
<string name="connection_error">Could not load charging stations</string>
|
||||
<string name="retry">Retry</string>
|
||||
</resources>
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
android:summaryOff="@string/pref_navigate_use_maps_off"
|
||||
android:defaultValue="true" />
|
||||
|
||||
<ListPreference
|
||||
android:key="language"
|
||||
android:title="@string/pref_language"
|
||||
android:entries="@array/pref_language_names"
|
||||
android:entryValues="@array/pref_language_values"
|
||||
android:defaultValue="default"
|
||||
android:summary="@string/pref_language_summary" />
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
16
app/src/test/java/net/vonforst/evmap/TestUtils.kt
Normal file
16
app/src/test/java/net/vonforst/evmap/TestUtils.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
val notFoundResponse: MockResponse =
|
||||
MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
|
||||
|
||||
fun okResponse(file: String): MockResponse {
|
||||
val body = readResource(file) ?: return notFoundResponse
|
||||
return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody(body)
|
||||
}
|
||||
|
||||
private fun readResource(s: String) =
|
||||
GoingElectricApiTest::class.java.getResource(s)?.readText()
|
||||
@@ -4,6 +4,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.okResponse
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
@@ -37,9 +38,7 @@ class NewMotionAvailabilityDetectorTest {
|
||||
when (urlHead) {
|
||||
"ge/chargepoints" -> {
|
||||
val id = request.requestUrl.queryParameter("ge_id")
|
||||
val body = readResource("/chargers/$id.json") ?: return notFoundResponse
|
||||
return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(body)
|
||||
return okResponse("/chargers/$id.json")
|
||||
}
|
||||
"nm/markers" -> {
|
||||
val urlTail = segments.subList(2, segments.size).joinToString("/")
|
||||
@@ -48,16 +47,11 @@ class NewMotionAvailabilityDetectorTest {
|
||||
"9.444284/9.644283999999999/54.376699/54.576699000000005" -> 18284
|
||||
else -> -1
|
||||
}
|
||||
val body =
|
||||
readResource("/newmotion/$id/markers.json") ?: return notFoundResponse
|
||||
return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(body)
|
||||
return okResponse("/newmotion/$id/markers.json")
|
||||
}
|
||||
"nm/locations" -> {
|
||||
val id = segments.last()
|
||||
val body = readResource("/newmotion/$id.json") ?: return notFoundResponse
|
||||
return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.setBody(body)
|
||||
return okResponse("/newmotion/$id.json")
|
||||
}
|
||||
else -> return notFoundResponse
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package net.vonforst.evmap.api.goingelectric
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.vonforst.evmap.notFoundResponse
|
||||
import net.vonforst.evmap.okResponse
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GoingElectricApiTest {
|
||||
val api: GoingElectricApi
|
||||
val webServer = MockWebServer()
|
||||
|
||||
init {
|
||||
webServer.start()
|
||||
|
||||
val apikey = ""
|
||||
val baseurl = webServer.url("/ge/").toString()
|
||||
api = GoingElectricApi.create(apikey, baseurl)
|
||||
|
||||
webServer.dispatcher = object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
val segments = request.requestUrl.pathSegments()
|
||||
val urlHead = segments.subList(0, 2).joinToString("/")
|
||||
when (urlHead) {
|
||||
"ge/chargepoints" -> {
|
||||
val id = request.requestUrl.queryParameter("ge_id")
|
||||
if (id != null) {
|
||||
return okResponse("/chargers/$id.json")
|
||||
} else {
|
||||
val freeparking =
|
||||
request.requestUrl.queryParameter("freeparking")!!.toBoolean()
|
||||
val freecharging =
|
||||
request.requestUrl.queryParameter("freecharging")!!.toBoolean()
|
||||
return if (freeparking && freecharging) {
|
||||
okResponse("/chargers/list-empty.json")
|
||||
} else if (freecharging) {
|
||||
okResponse("/chargers/list.json")
|
||||
} else {
|
||||
okResponse("/chargers/list-startkey.json")
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> return notFoundResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadChargepointDetail() {
|
||||
val response = api.getChargepointDetail(2105).execute()
|
||||
assertTrue(response.isSuccessful)
|
||||
val body = response.body()!!
|
||||
assertEquals("ok", body.status)
|
||||
assertEquals(null, body.startkey)
|
||||
assertEquals(1, body.chargelocations.size)
|
||||
val charger = body.chargelocations[0] as ChargeLocation
|
||||
assertEquals(2105, charger.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadChargepointList() {
|
||||
val response = runBlocking {
|
||||
api.getChargepoints(1.0, 1.0, 2.0, 2.0, 11f, freecharging = true)
|
||||
}
|
||||
assertTrue(response.isSuccessful)
|
||||
val body = response.body()!!
|
||||
assertEquals("ok", body.status)
|
||||
assertEquals(null, body.startkey)
|
||||
assertEquals(2, body.chargelocations.size)
|
||||
val charger = body.chargelocations[0] as ChargeLocation
|
||||
assertEquals(41161, charger.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadChargepointListEmpty() {
|
||||
val response = runBlocking {
|
||||
api.getChargepoints(1.0, 1.0, 2.0, 2.0, 11f, freeparking = true, freecharging = true)
|
||||
}
|
||||
assertTrue(response.isSuccessful)
|
||||
val body = response.body()!!
|
||||
assertEquals("ok", body.status)
|
||||
assertEquals(null, body.startkey)
|
||||
assertEquals(0, body.chargelocations.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLoadChargepointListStartkey() {
|
||||
val response = runBlocking {
|
||||
api.getChargepoints(1.0, 1.0, 2.0, 2.0, 1f)
|
||||
}
|
||||
assertTrue(response.isSuccessful)
|
||||
val body = response.body()!!
|
||||
assertEquals("ok", body.status)
|
||||
assertEquals(2, body.startkey)
|
||||
assertEquals(2, body.chargelocations.size)
|
||||
val charger = body.chargelocations[0] as ChargeLocation
|
||||
assertEquals(41161, charger.id)
|
||||
}
|
||||
}
|
||||
5
app/src/test/resources/chargers/list-empty.json
Normal file
5
app/src/test/resources/chargers/list-empty.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": "ok",
|
||||
"startkey": false,
|
||||
"chargelocations": []
|
||||
}
|
||||
61
app/src/test/resources/chargers/list-startkey.json
Normal file
61
app/src/test/resources/chargers/list-startkey.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"status": "ok",
|
||||
"chargelocations": [
|
||||
{
|
||||
"chargepoints": [
|
||||
{
|
||||
"type": "Typ2",
|
||||
"power": 22,
|
||||
"count": 2
|
||||
}
|
||||
],
|
||||
"ge_id": 41161,
|
||||
"name": "BMW Autohaus B&K",
|
||||
"address": {
|
||||
"city": "Hamburg",
|
||||
"country": "Deutschland",
|
||||
"postcode": "21073",
|
||||
"street": "Buxtehuder Straße 112"
|
||||
},
|
||||
"coordinates": {
|
||||
"lat": 53.469542,
|
||||
"lng": 9.964063
|
||||
},
|
||||
"network": "Digital Energy Solutions",
|
||||
"url": "//www.goingelectric.de/stromtankstellen/Deutschland/Hamburg/BMW-Autohaus-B-K-Buxtehuder-Strasse-112/41161/",
|
||||
"fault_report": false,
|
||||
"verified": false
|
||||
},
|
||||
{
|
||||
"chargepoints": [
|
||||
{
|
||||
"type": "Typ2",
|
||||
"power": 22,
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "Schuko",
|
||||
"power": 3.6,
|
||||
"count": 2
|
||||
}
|
||||
],
|
||||
"ge_id": 41226,
|
||||
"name": "Saseler Chaussee",
|
||||
"address": {
|
||||
"city": "Hamburg",
|
||||
"country": "Deutschland",
|
||||
"postcode": "22393",
|
||||
"street": "Saseler Chaussee 94a"
|
||||
},
|
||||
"coordinates": {
|
||||
"lat": 53.644021,
|
||||
"lng": 10.099783
|
||||
},
|
||||
"network": "Stromnetz Hamburg",
|
||||
"url": "//www.goingelectric.de/stromtankstellen/Deutschland/Hamburg/Saseler-Chaussee-Saseler-Chaussee-94a/41226/",
|
||||
"fault_report": false,
|
||||
"verified": false
|
||||
}
|
||||
],
|
||||
"startkey": 2
|
||||
}
|
||||
60
app/src/test/resources/chargers/list.json
Normal file
60
app/src/test/resources/chargers/list.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"status": "ok",
|
||||
"chargelocations": [
|
||||
{
|
||||
"chargepoints": [
|
||||
{
|
||||
"type": "Typ2",
|
||||
"power": 22,
|
||||
"count": 2
|
||||
}
|
||||
],
|
||||
"ge_id": 41161,
|
||||
"name": "BMW Autohaus B&K",
|
||||
"address": {
|
||||
"city": "Hamburg",
|
||||
"country": "Deutschland",
|
||||
"postcode": "21073",
|
||||
"street": "Buxtehuder Straße 112"
|
||||
},
|
||||
"coordinates": {
|
||||
"lat": 53.469542,
|
||||
"lng": 9.964063
|
||||
},
|
||||
"network": "Digital Energy Solutions",
|
||||
"url": "//www.goingelectric.de/stromtankstellen/Deutschland/Hamburg/BMW-Autohaus-B-K-Buxtehuder-Strasse-112/41161/",
|
||||
"fault_report": false,
|
||||
"verified": false
|
||||
},
|
||||
{
|
||||
"chargepoints": [
|
||||
{
|
||||
"type": "Typ2",
|
||||
"power": 22,
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"type": "Schuko",
|
||||
"power": 3.6,
|
||||
"count": 2
|
||||
}
|
||||
],
|
||||
"ge_id": 41226,
|
||||
"name": "Saseler Chaussee",
|
||||
"address": {
|
||||
"city": "Hamburg",
|
||||
"country": "Deutschland",
|
||||
"postcode": "22393",
|
||||
"street": "Saseler Chaussee 94a"
|
||||
},
|
||||
"coordinates": {
|
||||
"lat": 53.644021,
|
||||
"lng": 10.099783
|
||||
},
|
||||
"network": "Stromnetz Hamburg",
|
||||
"url": "//www.goingelectric.de/stromtankstellen/Deutschland/Hamburg/Saseler-Chaussee-Saseler-Chaussee-94a/41226/",
|
||||
"fault_report": false,
|
||||
"verified": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.71'
|
||||
ext.kotlin_version = '1.3.72'
|
||||
ext.about_libs_version = '8.1.1'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.0-rc01'
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
|
||||
|
||||
@@ -25,6 +25,9 @@ allprojects {
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
flatDir {
|
||||
dirs 'libs'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user