mirror of
https://github.com/ev-map/EVMap.git
synced 2026-04-20 14:18:20 -04:00
Encapsulate of GoingElectric API to create an interface the OpenChargeMap API can implement
This commit is contained in:
@@ -34,9 +34,10 @@ import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
@@ -518,7 +519,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
chargepointsText.append(
|
||||
"${cp.count}× ${
|
||||
nameForPlugType(
|
||||
carContext,
|
||||
carContext.stringProvider(),
|
||||
cp.type
|
||||
)
|
||||
} ${cp.formatPower()}"
|
||||
|
||||
@@ -19,8 +19,8 @@ import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
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.fragment.MapFragment
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.LocaleContextWrapper
|
||||
import net.vonforst.evmap.utils.getLocationFromIntent
|
||||
|
||||
@@ -15,14 +15,14 @@ import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.chargeprice.ChargePrice
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTag
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.databinding.ItemChargepriceBinding
|
||||
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.ui.CheckableConstraintLayout
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
|
||||
interface Equatable {
|
||||
override fun equals(other: Any?): Boolean;
|
||||
override fun equals(other: Any?): Boolean
|
||||
}
|
||||
|
||||
abstract class DataBindingAdapter<T : Equatable>(getKey: ((T) -> Any)? = null) :
|
||||
|
||||
@@ -3,12 +3,12 @@ package net.vonforst.evmap.adapter
|
||||
import android.content.Context
|
||||
import androidx.core.text.HtmlCompat
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCardId
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.OpeningHoursDays
|
||||
import net.vonforst.evmap.bold
|
||||
import net.vonforst.evmap.joinToSpannedString
|
||||
import net.vonforst.evmap.model.ChargeCard
|
||||
import net.vonforst.evmap.model.ChargeCardId
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.OpeningHoursDays
|
||||
import net.vonforst.evmap.plus
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 net.vonforst.evmap.model.*
|
||||
import kotlin.math.max
|
||||
|
||||
class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
|
||||
@@ -13,7 +13,7 @@ import coil.size.OriginalSize
|
||||
import coil.size.SizeResolver
|
||||
import com.ortiz.touchview.TouchImageView
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
|
||||
|
||||
class GalleryAdapter(
|
||||
@@ -78,13 +78,11 @@ class GalleryAdapter(
|
||||
(holder.view as TouchImageView).resetZoom()
|
||||
}
|
||||
val id = getItem(position).id
|
||||
val url = "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}" +
|
||||
"&id=$id" +
|
||||
if (detailView) {
|
||||
"&size=1000"
|
||||
} else {
|
||||
"&height=${holder.view.height}"
|
||||
}
|
||||
val url = if (detailView) {
|
||||
getItem(position).getUrl(size = 1000)
|
||||
} else {
|
||||
getItem(position).getUrl(height = holder.view.height)
|
||||
}
|
||||
|
||||
holder.view.load(
|
||||
url
|
||||
|
||||
38
app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt
Normal file
38
app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import android.content.Context
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
|
||||
interface ChargepointApi<T : ReferenceData> {
|
||||
suspend fun getChargepoints(
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: FilterValues
|
||||
): Resource<List<ChargepointListItem>>
|
||||
|
||||
suspend fun getChargepointsRadius(
|
||||
location: LatLng,
|
||||
radius: Int,
|
||||
zoom: Float,
|
||||
filters: FilterValues
|
||||
): Resource<List<ChargepointListItem>>
|
||||
|
||||
suspend fun getChargepointDetail(id: Long): Resource<ChargeLocation>
|
||||
|
||||
suspend fun getReferenceData(): Resource<T>
|
||||
|
||||
fun getFilters(referenceData: T, sp: StringProvider): List<Filter<FilterValue>>
|
||||
}
|
||||
|
||||
interface StringProvider {
|
||||
fun getString(id: Int): String
|
||||
}
|
||||
|
||||
fun Context.stringProvider() = object : StringProvider {
|
||||
override fun getString(id: Int): String {
|
||||
return this@stringProvider.getString(id)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.DrawableRes
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Response
|
||||
@@ -56,7 +55,7 @@ private val plugNames = mapOf(
|
||||
Chargepoint.TESLA_ROADSTER_HPC to R.string.plug_roadster_hpc
|
||||
)
|
||||
|
||||
fun nameForPlugType(ctx: Context, type: String): String =
|
||||
fun nameForPlugType(ctx: StringProvider, type: String): String =
|
||||
plugNames[type]?.let {
|
||||
ctx.getString(it)
|
||||
} ?: type
|
||||
|
||||
@@ -6,12 +6,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.api.RateLimitInterceptor
|
||||
import net.vonforst.evmap.api.await
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.viewmodel.FilterValues
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import net.vonforst.evmap.viewmodel.getMultipleChoiceValue
|
||||
import net.vonforst.evmap.viewmodel.getSliderValue
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.iterator
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import okhttp3.OkHttpClient
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@@ -9,7 +9,7 @@ import moe.banana.jsonapi2.JsonApi
|
||||
import moe.banana.jsonapi2.Resource
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
@@ -13,7 +13,7 @@ internal class ChargepointListItemJsonAdapterFactory : JsonAdapter.Factory {
|
||||
annotations: MutableSet<out Annotation>,
|
||||
moshi: Moshi
|
||||
): JsonAdapter<*>? {
|
||||
if (Types.getRawType(type) == ChargepointListItem::class.java) {
|
||||
if (Types.getRawType(type) == GEChargepointListItem::class.java) {
|
||||
return ChargepointListItemJsonAdapter(
|
||||
moshi
|
||||
)
|
||||
@@ -26,18 +26,18 @@ internal class ChargepointListItemJsonAdapterFactory : JsonAdapter.Factory {
|
||||
|
||||
|
||||
internal class ChargepointListItemJsonAdapter(val moshi: Moshi) :
|
||||
JsonAdapter<ChargepointListItem>() {
|
||||
JsonAdapter<GEChargepointListItem>() {
|
||||
private val clusterAdapter =
|
||||
moshi.adapter<ChargeLocationCluster>(
|
||||
ChargeLocationCluster::class.java
|
||||
moshi.adapter<GEChargeLocationCluster>(
|
||||
GEChargeLocationCluster::class.java
|
||||
)
|
||||
|
||||
private val locationAdapter = moshi.adapter<ChargeLocation>(
|
||||
ChargeLocation::class.java
|
||||
private val locationAdapter = moshi.adapter<GEChargeLocation>(
|
||||
GEChargeLocation::class.java
|
||||
)
|
||||
|
||||
@FromJson
|
||||
override fun fromJson(reader: JsonReader): ChargepointListItem {
|
||||
override fun fromJson(reader: JsonReader): GEChargepointListItem {
|
||||
var clustered = false
|
||||
reader.peekJson().use { peeked ->
|
||||
peeked.beginObject()
|
||||
@@ -61,7 +61,7 @@ internal class ChargepointListItemJsonAdapter(val moshi: Moshi) :
|
||||
val CLUSTERED: JsonReader.Options = JsonReader.Options.of("clustered")
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: ChargepointListItem?) {
|
||||
override fun toJson(writer: JsonWriter, value: GEChargepointListItem?) {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
}
|
||||
@@ -94,8 +94,8 @@ internal class JsonObjectOrFalseAdapter<T> private constructor(
|
||||
JsonReader.Token.BOOLEAN -> when (reader.nextBoolean()) {
|
||||
false -> null // Response was false
|
||||
else -> {
|
||||
if (this.clazz == FaultReport::class.java) {
|
||||
FaultReport(null, null) as T
|
||||
if (this.clazz == GEFaultReport::class.java) {
|
||||
GEFaultReport(null, null) as T
|
||||
} else {
|
||||
throw IllegalStateException("Non-false boolean for @JsonObjectOrFalse field")
|
||||
}
|
||||
@@ -126,20 +126,20 @@ internal class HoursAdapter {
|
||||
private val regex = Regex("from (.*) till (.*)")
|
||||
|
||||
@FromJson
|
||||
fun fromJson(str: String): Hours? {
|
||||
fun fromJson(str: String): GEHours? {
|
||||
if (str == "closed") {
|
||||
return Hours(null, null)
|
||||
return GEHours(null, null)
|
||||
} else {
|
||||
val match = regex.find(str)
|
||||
if (match != null) {
|
||||
return Hours(
|
||||
return GEHours(
|
||||
LocalTime.parse(match.groupValues[1]),
|
||||
LocalTime.parse(match.groupValues[2])
|
||||
)
|
||||
} else {
|
||||
// I cannot reproduce this case, but it seems to occur once in a while
|
||||
Log.e("GoingElectricApi", "invalid hours value: " + str)
|
||||
return Hours(
|
||||
return GEHours(
|
||||
LocalTime.MIN, LocalTime.MIN
|
||||
)
|
||||
}
|
||||
@@ -147,7 +147,7 @@ internal class HoursAdapter {
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: Hours): String {
|
||||
fun toJson(value: GEHours): String {
|
||||
if (value.start == null || value.end == null) {
|
||||
return "closed"
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
package net.vonforst.evmap.api.goingelectric
|
||||
|
||||
import android.content.Context
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.StringProvider
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.ui.cluster
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import net.vonforst.evmap.viewmodel.getClusterDistance
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Response
|
||||
@@ -11,6 +24,8 @@ import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
import java.io.IOException
|
||||
import kotlin.math.abs
|
||||
|
||||
interface GoingElectricApi {
|
||||
@GET("chargepoints/")
|
||||
@@ -31,7 +46,7 @@ interface GoingElectricApi {
|
||||
@Query("open_twentyfourseven") open247: Boolean = false,
|
||||
@Query("barrierfree") barrierfree: Boolean = false,
|
||||
@Query("exclude_faults") excludeFaults: Boolean = false
|
||||
): Response<ChargepointList>
|
||||
): Response<GEChargepointList>
|
||||
|
||||
@GET("chargepoints/")
|
||||
suspend fun getChargepointsRadius(
|
||||
@@ -52,19 +67,19 @@ interface GoingElectricApi {
|
||||
@Query("open_twentyfourseven") open247: Boolean = false,
|
||||
@Query("barrierfree") barrierfree: Boolean = false,
|
||||
@Query("exclude_faults") excludeFaults: Boolean = false
|
||||
): Response<ChargepointList>
|
||||
): Response<GEChargepointList>
|
||||
|
||||
@GET("chargepoints/")
|
||||
suspend fun getChargepointDetail(@Query("ge_id") id: Long): Response<ChargepointList>
|
||||
suspend fun getChargepointDetail(@Query("ge_id") id: Long): Response<GEChargepointList>
|
||||
|
||||
@GET("chargepoints/pluglist/")
|
||||
suspend fun getPlugs(): Response<StringList>
|
||||
suspend fun getPlugs(): Response<GEStringList>
|
||||
|
||||
@GET("chargepoints/networklist/")
|
||||
suspend fun getNetworks(): Response<StringList>
|
||||
suspend fun getNetworks(): Response<GEStringList>
|
||||
|
||||
@GET("chargepoints/chargecardlist/")
|
||||
suspend fun getChargeCards(): Response<ChargeCardList>
|
||||
suspend fun getChargeCards(): Response<GEChargeCardList>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 10L * 1024 * 1024 // 10MB
|
||||
@@ -106,3 +121,254 @@ interface GoingElectricApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GoingElectricApiWrapper(
|
||||
val apikey: String,
|
||||
baseurl: String = "https://api.goingelectric.de",
|
||||
context: Context? = null
|
||||
) : ChargepointApi<GEReferenceData> {
|
||||
val api = GoingElectricApi.create(apikey, baseurl, context)
|
||||
override suspend fun getChargepoints(
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: FilterValues
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val freecharging = filters.getBooleanValue("freecharging")
|
||||
val freeparking = filters.getBooleanValue("freeparking")
|
||||
val open247 = filters.getBooleanValue("open_247")
|
||||
val barrierfree = filters.getBooleanValue("barrierfree")
|
||||
val excludeFaults = filters.getBooleanValue("exclude_faults")
|
||||
val minPower = filters.getSliderValue("min_power")
|
||||
val minConnectors = filters.getSliderValue("min_connectors")
|
||||
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
|
||||
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")
|
||||
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
|
||||
// no chargeCards chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val chargeCards = formatMultipleChoice(chargeCardsVal)
|
||||
|
||||
val networksVal = filters.getMultipleChoiceValue("networks")
|
||||
if (networksVal.values.isEmpty() && !networksVal.all) {
|
||||
// no networks chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val networks = formatMultipleChoice(networksVal)
|
||||
|
||||
val categoriesVal = filters.getMultipleChoiceValue("categories")
|
||||
if (categoriesVal.values.isEmpty() && !categoriesVal.all) {
|
||||
// no categories chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val categories = formatMultipleChoice(categoriesVal)
|
||||
|
||||
// do not use clustering if filters need to be applied locally.
|
||||
val useClustering = zoom < 13
|
||||
val geClusteringAvailable = minConnectors <= 1
|
||||
val useGeClustering = useClustering && geClusteringAvailable
|
||||
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
|
||||
|
||||
var startkey: Int? = null
|
||||
val data = mutableListOf<GEChargepointListItem>()
|
||||
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,
|
||||
open247 = open247,
|
||||
barrierfree = barrierfree,
|
||||
excludeFaults = excludeFaults,
|
||||
plugs = connectors,
|
||||
chargecards = chargeCards,
|
||||
networks = networks,
|
||||
categories = categories,
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Resource.error(response.message(), null)
|
||||
} else {
|
||||
val body = response.body()!!
|
||||
data.addAll(body.chargelocations)
|
||||
startkey = body.startkey
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
|
||||
var result = data.filter { it ->
|
||||
// apply filters which GoingElectric does not support natively
|
||||
if (it is GEChargeLocation) {
|
||||
it.chargepoints
|
||||
.filter { it.power >= minPower }
|
||||
.filter { if (!connectorsVal.all) it.type in connectorsVal.values else true }
|
||||
.sumBy { it.count } >= minConnectors
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}.map { it.convert(apikey) } // convert to common model
|
||||
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(",")
|
||||
|
||||
override suspend fun getChargepointsRadius(
|
||||
location: LatLng,
|
||||
radius: Int,
|
||||
zoom: Float,
|
||||
filters: FilterValues
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getChargepointDetail(id: Long): Resource<ChargeLocation> {
|
||||
val response = api.getChargepointDetail(id)
|
||||
return if (response.isSuccessful && response.body()!!.status == "ok" && response.body()!!.chargelocations.size == 1) {
|
||||
Resource.success(
|
||||
(response.body()!!.chargelocations[0] as GEChargeLocation).convert(
|
||||
apikey
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Resource.error(response.message(), null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getReferenceData(): Resource<GEReferenceData> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val plugs = async { api.getPlugs() }
|
||||
val chargeCards = async { api.getChargeCards() }
|
||||
val networks = async { api.getNetworks() }
|
||||
|
||||
val plugsResponse = plugs.await()
|
||||
val chargeCardsResponse = chargeCards.await()
|
||||
val networksResponse = networks.await()
|
||||
|
||||
val responses = listOf(plugsResponse, chargeCardsResponse, networksResponse)
|
||||
|
||||
if (responses.map { it.isSuccessful }.all { it }) {
|
||||
Resource.success(
|
||||
GEReferenceData(
|
||||
plugsResponse.body()!!.result,
|
||||
networksResponse.body()!!.result,
|
||||
chargeCardsResponse.body()!!.result
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Resource.error(responses.find { !it.isSuccessful }!!.message(), null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilters(
|
||||
referenceData: GEReferenceData,
|
||||
sp: StringProvider
|
||||
): List<Filter<FilterValue>> {
|
||||
val plugs = referenceData.plugs
|
||||
val networks = referenceData.networks
|
||||
val chargeCards = referenceData.chargecards
|
||||
|
||||
val plugMap = plugs.map { plug ->
|
||||
plug to nameForPlugType(sp, plug)
|
||||
}.toMap()
|
||||
val networkMap = networks.map { it to it }.toMap()
|
||||
val chargecardMap = chargeCards.map { it.id.toString() to it.name }.toMap()
|
||||
val categoryMap = mapOf(
|
||||
"Autohaus" to sp.getString(R.string.category_car_dealership),
|
||||
"Autobahnraststätte" to sp.getString(R.string.category_service_on_motorway),
|
||||
"Autohof" to sp.getString(R.string.category_service_off_motorway),
|
||||
"Bahnhof" to sp.getString(R.string.category_railway_station),
|
||||
"Behörde" to sp.getString(R.string.category_public_authorities),
|
||||
"Campingplatz" to sp.getString(R.string.category_camping),
|
||||
"Einkaufszentrum" to sp.getString(R.string.category_shopping_mall),
|
||||
"Ferienwohnung" to sp.getString(R.string.category_holiday_home),
|
||||
"Flughafen" to sp.getString(R.string.category_airport),
|
||||
"Freizeitpark" to sp.getString(R.string.category_amusement_park),
|
||||
"Hotel" to sp.getString(R.string.category_hotel),
|
||||
"Kino" to sp.getString(R.string.category_cinema),
|
||||
"Kirche" to sp.getString(R.string.category_church),
|
||||
"Krankenhaus" to sp.getString(R.string.category_hospital),
|
||||
"Museum" to sp.getString(R.string.category_museum),
|
||||
"Parkhaus" to sp.getString(R.string.category_parking_multi),
|
||||
"Parkplatz" to sp.getString(R.string.category_parking),
|
||||
"Privater Ladepunkt" to sp.getString(R.string.category_private_charger),
|
||||
"Rastplatz" to sp.getString(R.string.category_rest_area),
|
||||
"Restaurant" to sp.getString(R.string.category_restaurant),
|
||||
"Schwimmbad" to sp.getString(R.string.category_swimming_pool),
|
||||
"Supermarkt" to sp.getString(R.string.category_supermarket),
|
||||
"Tankstelle" to sp.getString(R.string.category_petrol_station),
|
||||
"Tiefgarage" to sp.getString(R.string.category_parking_underground),
|
||||
"Tierpark" to sp.getString(R.string.category_zoo),
|
||||
"Wohnmobilstellplatz" to sp.getString(R.string.category_caravan_site)
|
||||
)
|
||||
return listOf(
|
||||
BooleanFilter(sp.getString(R.string.filter_free), "freecharging"),
|
||||
BooleanFilter(sp.getString(R.string.filter_free_parking), "freeparking"),
|
||||
BooleanFilter(sp.getString(R.string.filter_open_247), "open_247"),
|
||||
SliderFilter(
|
||||
sp.getString(R.string.filter_min_power), "min_power",
|
||||
powerSteps.size - 1,
|
||||
mapping = ::mapPower,
|
||||
inverseMapping = ::mapPowerInverse,
|
||||
unit = "kW"
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_connectors), "connectors",
|
||||
plugMap,
|
||||
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO),
|
||||
manyChoices = true
|
||||
),
|
||||
SliderFilter(
|
||||
sp.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10,
|
||||
min = 1
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_networks), "networks",
|
||||
networkMap, manyChoices = true
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.categories), "categories",
|
||||
categoryMap,
|
||||
manyChoices = true
|
||||
),
|
||||
BooleanFilter(sp.getString(R.string.filter_barrierfree), "barrierfree"),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_chargecards), "chargecards",
|
||||
chargecardMap, manyChoices = true
|
||||
),
|
||||
BooleanFilter(sp.getString(R.string.filter_exclude_faults), "exclude_faults")
|
||||
)
|
||||
}
|
||||
|
||||
private val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
|
||||
private fun mapPower(i: Int) = powerSteps[i]
|
||||
private fun mapPowerInverse(power: Int) = powerSteps
|
||||
.mapIndexed { index, v -> abs(v - power) to index }
|
||||
.minByOrNull { it.first }?.second ?: 0
|
||||
}
|
||||
|
||||
|
||||
@@ -1,58 +1,47 @@
|
||||
package net.vonforst.evmap.api.goingelectric
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import java.time.DayOfWeek
|
||||
import net.vonforst.evmap.model.*
|
||||
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
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepointList(
|
||||
data class GEChargepointList(
|
||||
val status: String,
|
||||
val chargelocations: List<ChargepointListItem>,
|
||||
val chargelocations: List<GEChargepointListItem>,
|
||||
@JsonObjectOrFalse val startkey: Int?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StringList(
|
||||
data class GEStringList(
|
||||
val status: String,
|
||||
val result: List<String>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCardList(
|
||||
data class GEChargeCardList(
|
||||
val status: String,
|
||||
val result: List<ChargeCard>
|
||||
val result: List<GEChargeCard>
|
||||
)
|
||||
|
||||
sealed class ChargepointListItem
|
||||
sealed class GEChargepointListItem {
|
||||
abstract fun convert(apikey: String): ChargepointListItem
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Entity
|
||||
data class ChargeLocation(
|
||||
@Json(name = "ge_id") @PrimaryKey val id: Long,
|
||||
data class GEChargeLocation(
|
||||
@Json(name = "ge_id") val id: Long,
|
||||
val name: String,
|
||||
@Embedded val coordinates: Coordinate,
|
||||
@Embedded val address: Address,
|
||||
val chargepoints: List<Chargepoint>,
|
||||
val coordinates: GECoordinate,
|
||||
val address: GEAddress,
|
||||
val chargepoints: List<GEChargepoint>,
|
||||
@JsonObjectOrFalse val network: String?,
|
||||
val url: String,
|
||||
@Embedded(prefix = "fault_report_") @JsonObjectOrFalse @Json(name = "fault_report") val faultReport: FaultReport?,
|
||||
@JsonObjectOrFalse @Json(name = "fault_report") val faultReport: GEFaultReport?,
|
||||
val verified: Boolean,
|
||||
@Json(name = "barrierfree") val barrierFree: Boolean?,
|
||||
// only shown in details:
|
||||
@@ -60,260 +49,156 @@ data class ChargeLocation(
|
||||
@JsonObjectOrFalse @Json(name = "general_information") val generalInformation: String?,
|
||||
@JsonObjectOrFalse @Json(name = "ladeweile") val amenities: String?,
|
||||
@JsonObjectOrFalse @Json(name = "location_description") val locationDescription: String?,
|
||||
val photos: List<ChargerPhoto>?,
|
||||
@JsonObjectOrFalse val chargecards: List<ChargeCardId>?,
|
||||
@Embedded val openinghours: OpeningHours?,
|
||||
@Embedded val cost: Cost?
|
||||
) : ChargepointListItem(), Equatable {
|
||||
/**
|
||||
* maximum power available from this charger.
|
||||
*/
|
||||
val maxPower: Double
|
||||
get() {
|
||||
return maxPower()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum power available from certain connectors of this charger.
|
||||
*/
|
||||
fun maxPower(connectors: Set<String>? = null): Double {
|
||||
return chargepoints.filter { connectors?.contains(it.type) ?: true }
|
||||
.map { it.power }.maxOrNull() ?: 0.0
|
||||
}
|
||||
|
||||
fun isMulti(filteredConnectors: Set<String>? = null): Boolean {
|
||||
var chargepoints = chargepointsMerged
|
||||
.filter { filteredConnectors?.contains(it.type) ?: true }
|
||||
if (maxPower(filteredConnectors) >= 43) {
|
||||
// fast charger -> only count fast chargers
|
||||
chargepoints = chargepoints.filter { it.power >= 43 }
|
||||
}
|
||||
val connectors = chargepoints.map { it.type }.distinct().toSet()
|
||||
|
||||
// check if there is more than one plug for any connector type
|
||||
val chargepointsPerConnector =
|
||||
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
|
||||
return chargepointsPerConnector.any { it > 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges chargepoints if they have the same plug and power
|
||||
*
|
||||
* This occurs e.g. for Type2 sockets and plugs, which are distinct on the GE website, but not
|
||||
* separable in the API
|
||||
*/
|
||||
val chargepointsMerged: List<Chargepoint>
|
||||
get() {
|
||||
val variants = chargepoints.distinctBy { it.power to it.type }
|
||||
return variants.map { variant ->
|
||||
val count = chargepoints
|
||||
.filter { it.type == variant.type && it.power == variant.power }
|
||||
.sumBy { it.count }
|
||||
Chargepoint(variant.type, variant.power, count)
|
||||
}
|
||||
}
|
||||
|
||||
val totalChargepoints: Int
|
||||
get() = chargepoints.sumBy { it.count }
|
||||
|
||||
fun formatChargepoints(): String {
|
||||
return chargepointsMerged.map {
|
||||
"${it.count} × ${it.type} ${it.formatPower()}"
|
||||
}.joinToString(" · ")
|
||||
val photos: List<GEChargerPhoto>?,
|
||||
@JsonObjectOrFalse val chargecards: List<GEChargeCardId>?,
|
||||
val openinghours: GEOpeningHours?,
|
||||
val cost: GECost?
|
||||
) : GEChargepointListItem() {
|
||||
override fun convert(apikey: String): ChargeLocation {
|
||||
return ChargeLocation(
|
||||
id,
|
||||
name,
|
||||
coordinates.convert(),
|
||||
address.convert(),
|
||||
chargepoints.map { it.convert() },
|
||||
network,
|
||||
url,
|
||||
faultReport?.convert(),
|
||||
verified,
|
||||
barrierFree,
|
||||
operator,
|
||||
generalInformation,
|
||||
amenities,
|
||||
locationDescription,
|
||||
photos?.map { it.convert(apikey) },
|
||||
chargecards?.map { it.convert() },
|
||||
openinghours?.convert(),
|
||||
cost?.convert()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Cost(
|
||||
data class GECost(
|
||||
val freecharging: Boolean,
|
||||
val freeparking: Boolean,
|
||||
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String?,
|
||||
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String?
|
||||
) {
|
||||
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
|
||||
val charging =
|
||||
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
val parking =
|
||||
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
return if (emoji) {
|
||||
"⚡ $charging · \uD83C\uDD7F️ $parking"
|
||||
} else {
|
||||
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail, charging, parking), 0)
|
||||
}
|
||||
}
|
||||
fun convert() = Cost(freecharging, freeparking, descriptionShort, descriptionLong)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OpeningHours(
|
||||
data class GEOpeningHours(
|
||||
@Json(name = "24/7") val twentyfourSeven: Boolean,
|
||||
@JsonObjectOrFalse val description: String?,
|
||||
@Embedded val days: OpeningHoursDays?
|
||||
val days: GEOpeningHoursDays?
|
||||
) {
|
||||
val isEmpty: Boolean
|
||||
get() = description == "Leider noch keine Informationen zu Öffnungszeiten vorhanden."
|
||||
&& days == null && !twentyfourSeven
|
||||
|
||||
fun getStatusText(ctx: Context): CharSequence {
|
||||
if (twentyfourSeven) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
|
||||
} else if (days != null) {
|
||||
val hours = days.getHoursForDate(LocalDate.now())
|
||||
if (hours.start == null || hours.end == null) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
|
||||
}
|
||||
|
||||
val now = LocalTime.now()
|
||||
if (hours.start.isBefore(now) && hours.end.isAfter(now)) {
|
||||
return HtmlCompat.fromHtml(
|
||||
ctx.getString(
|
||||
R.string.open_closesat,
|
||||
hours.end.toString()
|
||||
), 0
|
||||
)
|
||||
} else if (hours.end.isBefore(now)) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
|
||||
} else {
|
||||
return HtmlCompat.fromHtml(
|
||||
ctx.getString(
|
||||
R.string.closed_opensat,
|
||||
hours.start.toString()
|
||||
), 0
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
fun convert() = OpeningHours(twentyfourSeven, description, days?.convert())
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OpeningHoursDays(
|
||||
@Embedded(prefix = "mo") val monday: Hours,
|
||||
@Embedded(prefix = "tu") val tuesday: Hours,
|
||||
@Embedded(prefix = "we") val wednesday: Hours,
|
||||
@Embedded(prefix = "th") val thursday: Hours,
|
||||
@Embedded(prefix = "fr") val friday: Hours,
|
||||
@Embedded(prefix = "sa") val saturday: Hours,
|
||||
@Embedded(prefix = "su") val sunday: Hours,
|
||||
@Embedded(prefix = "ho") val holiday: Hours
|
||||
data class GEOpeningHoursDays(
|
||||
val monday: GEHours,
|
||||
val tuesday: GEHours,
|
||||
val wednesday: GEHours,
|
||||
val thursday: GEHours,
|
||||
val friday: GEHours,
|
||||
val saturday: GEHours,
|
||||
val sunday: GEHours,
|
||||
val holiday: GEHours
|
||||
) {
|
||||
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 (dayOfWeek) {
|
||||
DayOfWeek.MONDAY -> monday
|
||||
DayOfWeek.TUESDAY -> tuesday
|
||||
DayOfWeek.WEDNESDAY -> wednesday
|
||||
DayOfWeek.THURSDAY -> thursday
|
||||
DayOfWeek.FRIDAY -> friday
|
||||
DayOfWeek.SATURDAY -> saturday
|
||||
DayOfWeek.SUNDAY -> sunday
|
||||
null -> holiday
|
||||
}
|
||||
}
|
||||
fun convert() = OpeningHoursDays(
|
||||
monday.convert(),
|
||||
tuesday.convert(),
|
||||
wednesday.convert(),
|
||||
thursday.convert(),
|
||||
friday.convert(),
|
||||
saturday.convert(),
|
||||
sunday.convert(),
|
||||
holiday.convert()
|
||||
)
|
||||
}
|
||||
|
||||
data class Hours(
|
||||
data class GEHours(
|
||||
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"
|
||||
}
|
||||
}
|
||||
fun convert() = Hours(start, end)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GEChargerPhoto(val id: String) {
|
||||
fun convert(apikey: String): ChargerPhoto = GEChargerPhotoAdapter(id, apikey)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ChargerPhoto(val id: String) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeLocationCluster(
|
||||
val clusterCount: Int,
|
||||
val coordinates: Coordinate
|
||||
) : ChargepointListItem()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Coordinate(val lat: Double, val lng: Double) {
|
||||
fun formatDMS(): String {
|
||||
return "${dms(lat, false)}, ${dms(lng, true)}"
|
||||
}
|
||||
|
||||
private fun dms(value: Double, lon: Boolean): String {
|
||||
val hemisphere = if (lon) {
|
||||
if (value >= 0) "E" else "W"
|
||||
} else {
|
||||
if (value >= 0) "N" else "S"
|
||||
}
|
||||
val d = abs(value)
|
||||
val degrees = floor(d).toInt()
|
||||
val minutes = floor((d - degrees) * 60).toInt()
|
||||
val seconds = ((d - degrees) * 60 - minutes) * 60
|
||||
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
|
||||
}
|
||||
|
||||
fun formatDecimal(): String {
|
||||
return "%.6f, %.6f".format(Locale.ENGLISH, lat, lng)
|
||||
private class GEChargerPhotoAdapter(override val id: String, private val apikey: String) :
|
||||
ChargerPhoto(id) {
|
||||
override fun getUrl(height: Int?, width: Int?, size: Int?): String {
|
||||
return "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}&id=$id" +
|
||||
when {
|
||||
size != null -> "&size=$size"
|
||||
height != null -> "&height=$height"
|
||||
width != null -> "&width=$width"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Address(
|
||||
data class GEChargeLocationCluster(
|
||||
val clusterCount: Int,
|
||||
val coordinates: GECoordinate
|
||||
) : GEChargepointListItem() {
|
||||
override fun convert(apikey: String) =
|
||||
ChargeLocationCluster(clusterCount, coordinates.convert())
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GECoordinate(val lat: Double, val lng: Double) {
|
||||
fun convert() = Coordinate(lat, lng)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GEAddress(
|
||||
@JsonObjectOrFalse val city: String?,
|
||||
@JsonObjectOrFalse val country: String?,
|
||||
@JsonObjectOrFalse val postcode: String?,
|
||||
@JsonObjectOrFalse val street: String?
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "${street ?: ""}, ${postcode ?: ""} ${city ?: ""}"
|
||||
}
|
||||
fun convert() = Address(city, country, postcode, street)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Chargepoint(val type: String, val power: Double, val count: Int) : Equatable {
|
||||
fun formatPower(): String {
|
||||
val powerFmt = if (power - power.toInt() == 0.0) {
|
||||
"%.0f".format(power)
|
||||
} else {
|
||||
"%.1f".format(power)
|
||||
}
|
||||
return "$powerFmt kW"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_1 = "Typ1"
|
||||
const val TYPE_2 = "Typ2"
|
||||
const val TYPE_3 = "Typ3"
|
||||
const val CCS = "CCS"
|
||||
const val SCHUKO = "Schuko"
|
||||
const val CHADEMO = "CHAdeMO"
|
||||
const val SUPERCHARGER = "Tesla Supercharger"
|
||||
const val CEE_BLAU = "CEE Blau"
|
||||
const val CEE_ROT = "CEE Rot"
|
||||
const val TESLA_ROADSTER_HPC = "Tesla HPC"
|
||||
}
|
||||
data class GEChargepoint(val type: String, val power: Double, val count: Int) {
|
||||
fun convert() = Chargepoint(type, power, count)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FaultReport(val created: Instant?, val description: String?)
|
||||
data class GEFaultReport(val created: Instant?, val description: String?) {
|
||||
fun convert() = FaultReport(created, description)
|
||||
}
|
||||
|
||||
@Entity
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCard(
|
||||
@Entity(tableName = "ChargeCard")
|
||||
data class GEChargeCard(
|
||||
@Json(name = "card_id") @PrimaryKey val id: Long,
|
||||
val name: String,
|
||||
val url: String
|
||||
)
|
||||
) {
|
||||
fun convert() = ChargeCard(id, name, url)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCardId(
|
||||
data class GEChargeCardId(
|
||||
val id: Long
|
||||
)
|
||||
) {
|
||||
fun convert() = ChargeCardId(id)
|
||||
}
|
||||
|
||||
data class GEReferenceData(
|
||||
val plugs: List<String>,
|
||||
val networks: List<String>,
|
||||
val chargecards: List<GEChargeCard>
|
||||
) : ReferenceData()
|
||||
@@ -20,10 +20,10 @@ import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.ChargepriceAdapter
|
||||
import net.vonforst.evmap.adapter.CheckableConnectorAdapter
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
@@ -16,8 +16,8 @@ import coil.memory.MemoryCache
|
||||
import com.ortiz.touchview.TouchImageView
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
|
||||
import net.vonforst.evmap.databinding.FragmentGalleryBinding
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
import net.vonforst.evmap.viewmodel.GalleryViewModel
|
||||
|
||||
|
||||
|
||||
@@ -66,12 +66,10 @@ import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.DetailsAdapter
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
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.autocomplete.handleAutocompleteResult
|
||||
import net.vonforst.evmap.autocomplete.launchAutocomplete
|
||||
import net.vonforst.evmap.databinding.FragmentMapBinding
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.ClusterIconGenerator
|
||||
|
||||
286
app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt
Normal file
286
app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt
Normal file
@@ -0,0 +1,286 @@
|
||||
package net.vonforst.evmap.model
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import java.time.DayOfWeek
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
|
||||
sealed class ChargepointListItem
|
||||
|
||||
@Entity
|
||||
data class ChargeLocation(
|
||||
@PrimaryKey val id: Long,
|
||||
val name: String,
|
||||
@Embedded val coordinates: Coordinate,
|
||||
@Embedded val address: Address,
|
||||
val chargepoints: List<Chargepoint>,
|
||||
val network: String?,
|
||||
val url: String,
|
||||
@Embedded(prefix = "fault_report_") val faultReport: FaultReport?,
|
||||
val verified: Boolean,
|
||||
val barrierFree: Boolean?,
|
||||
// only shown in details:
|
||||
val operator: String?,
|
||||
val generalInformation: String?,
|
||||
val amenities: String?,
|
||||
val locationDescription: String?,
|
||||
val photos: List<ChargerPhoto>?,
|
||||
val chargecards: List<ChargeCardId>?,
|
||||
@Embedded val openinghours: OpeningHours?,
|
||||
@Embedded val cost: Cost?
|
||||
) : ChargepointListItem(), Equatable {
|
||||
/**
|
||||
* maximum power available from this charger.
|
||||
*/
|
||||
val maxPower: Double
|
||||
get() {
|
||||
return maxPower()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum power available from certain connectors of this charger.
|
||||
*/
|
||||
fun maxPower(connectors: Set<String>? = null): Double {
|
||||
return chargepoints.filter { connectors?.contains(it.type) ?: true }
|
||||
.map { it.power }.maxOrNull() ?: 0.0
|
||||
}
|
||||
|
||||
fun isMulti(filteredConnectors: Set<String>? = null): Boolean {
|
||||
var chargepoints = chargepointsMerged
|
||||
.filter { filteredConnectors?.contains(it.type) ?: true }
|
||||
if (maxPower(filteredConnectors) >= 43) {
|
||||
// fast charger -> only count fast chargers
|
||||
chargepoints = chargepoints.filter { it.power >= 43 }
|
||||
}
|
||||
val connectors = chargepoints.map { it.type }.distinct().toSet()
|
||||
|
||||
// check if there is more than one plug for any connector type
|
||||
val chargepointsPerConnector =
|
||||
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
|
||||
return chargepointsPerConnector.any { it > 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges chargepoints if they have the same plug and power
|
||||
*
|
||||
* This occurs e.g. for Type2 sockets and plugs, which are distinct on the GE website, but not
|
||||
* separable in the API
|
||||
*/
|
||||
val chargepointsMerged: List<Chargepoint>
|
||||
get() {
|
||||
val variants = chargepoints.distinctBy { it.power to it.type }
|
||||
return variants.map { variant ->
|
||||
val count = chargepoints
|
||||
.filter { it.type == variant.type && it.power == variant.power }
|
||||
.sumBy { it.count }
|
||||
Chargepoint(variant.type, variant.power, count)
|
||||
}
|
||||
}
|
||||
|
||||
val totalChargepoints: Int
|
||||
get() = chargepoints.sumBy { it.count }
|
||||
|
||||
fun formatChargepoints(): String {
|
||||
return chargepointsMerged.map {
|
||||
"${it.count} × ${it.type} ${it.formatPower()}"
|
||||
}.joinToString(" · ")
|
||||
}
|
||||
}
|
||||
|
||||
data class Cost(
|
||||
val freecharging: Boolean,
|
||||
val freeparking: Boolean,
|
||||
val descriptionShort: String?,
|
||||
val descriptionLong: String?
|
||||
) {
|
||||
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
|
||||
val charging =
|
||||
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
val parking =
|
||||
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
return if (emoji) {
|
||||
"⚡ $charging · \uD83C\uDD7F️ $parking"
|
||||
} else {
|
||||
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail, charging, parking), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class OpeningHours(
|
||||
val twentyfourSeven: Boolean,
|
||||
val description: String?,
|
||||
@Embedded val days: OpeningHoursDays?
|
||||
) {
|
||||
val isEmpty: Boolean
|
||||
get() = description == "Leider noch keine Informationen zu Öffnungszeiten vorhanden."
|
||||
&& days == null && !twentyfourSeven
|
||||
|
||||
fun getStatusText(ctx: Context): CharSequence {
|
||||
if (twentyfourSeven) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
|
||||
} else if (days != null) {
|
||||
val hours = days.getHoursForDate(LocalDate.now())
|
||||
if (hours.start == null || hours.end == null) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
|
||||
}
|
||||
|
||||
val now = LocalTime.now()
|
||||
if (hours.start.isBefore(now) && hours.end.isAfter(now)) {
|
||||
return HtmlCompat.fromHtml(
|
||||
ctx.getString(
|
||||
R.string.open_closesat,
|
||||
hours.end.toString()
|
||||
), 0
|
||||
)
|
||||
} else if (hours.end.isBefore(now)) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
|
||||
} else {
|
||||
return HtmlCompat.fromHtml(
|
||||
ctx.getString(
|
||||
R.string.closed_opensat,
|
||||
hours.start.toString()
|
||||
), 0
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class OpeningHoursDays(
|
||||
@Embedded(prefix = "mo") val monday: Hours,
|
||||
@Embedded(prefix = "tu") val tuesday: Hours,
|
||||
@Embedded(prefix = "we") val wednesday: Hours,
|
||||
@Embedded(prefix = "th") val thursday: Hours,
|
||||
@Embedded(prefix = "fr") val friday: Hours,
|
||||
@Embedded(prefix = "sa") val saturday: Hours,
|
||||
@Embedded(prefix = "su") val sunday: Hours,
|
||||
@Embedded(prefix = "ho") val holiday: Hours
|
||||
) {
|
||||
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 (dayOfWeek) {
|
||||
DayOfWeek.MONDAY -> monday
|
||||
DayOfWeek.TUESDAY -> tuesday
|
||||
DayOfWeek.WEDNESDAY -> wednesday
|
||||
DayOfWeek.THURSDAY -> thursday
|
||||
DayOfWeek.FRIDAY -> friday
|
||||
DayOfWeek.SATURDAY -> saturday
|
||||
DayOfWeek.SUNDAY -> sunday
|
||||
null -> holiday
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ChargerPhoto(open val id: String) : Parcelable {
|
||||
abstract fun getUrl(height: Int? = null, width: Int? = null, size: Int? = null): String
|
||||
}
|
||||
|
||||
data class ChargeLocationCluster(
|
||||
val clusterCount: Int,
|
||||
val coordinates: Coordinate
|
||||
) : ChargepointListItem()
|
||||
|
||||
data class Coordinate(val lat: Double, val lng: Double) {
|
||||
fun formatDMS(): String {
|
||||
return "${dms(lat, false)}, ${dms(lng, true)}"
|
||||
}
|
||||
|
||||
private fun dms(value: Double, lon: Boolean): String {
|
||||
val hemisphere = if (lon) {
|
||||
if (value >= 0) "E" else "W"
|
||||
} else {
|
||||
if (value >= 0) "N" else "S"
|
||||
}
|
||||
val d = abs(value)
|
||||
val degrees = floor(d).toInt()
|
||||
val minutes = floor((d - degrees) * 60).toInt()
|
||||
val seconds = ((d - degrees) * 60 - minutes) * 60
|
||||
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
|
||||
}
|
||||
|
||||
fun formatDecimal(): String {
|
||||
return "%.6f, %.6f".format(Locale.ENGLISH, lat, lng)
|
||||
}
|
||||
}
|
||||
|
||||
data class Address(
|
||||
val city: String?,
|
||||
val country: String?,
|
||||
val postcode: String?,
|
||||
val street: String?
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "${street ?: ""}, ${postcode ?: ""} ${city ?: ""}"
|
||||
}
|
||||
}
|
||||
|
||||
data class Chargepoint(val type: String, val power: Double, val count: Int) : Equatable {
|
||||
fun formatPower(): String {
|
||||
val powerFmt = if (power - power.toInt() == 0.0) {
|
||||
"%.0f".format(power)
|
||||
} else {
|
||||
"%.1f".format(power)
|
||||
}
|
||||
return "$powerFmt kW"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_1 = "Typ1"
|
||||
const val TYPE_2 = "Typ2"
|
||||
const val TYPE_3 = "Typ3"
|
||||
const val CCS = "CCS"
|
||||
const val SCHUKO = "Schuko"
|
||||
const val CHADEMO = "CHAdeMO"
|
||||
const val SUPERCHARGER = "Tesla Supercharger"
|
||||
const val CEE_BLAU = "CEE Blau"
|
||||
const val CEE_ROT = "CEE Rot"
|
||||
const val TESLA_ROADSTER_HPC = "Tesla HPC"
|
||||
}
|
||||
}
|
||||
|
||||
data class FaultReport(val created: Instant?, val description: String?)
|
||||
|
||||
@Entity
|
||||
data class ChargeCard(
|
||||
@PrimaryKey val id: Long,
|
||||
val name: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
data class ChargeCardId(
|
||||
val id: Long
|
||||
)
|
||||
130
app/src/main/java/net/vonforst/evmap/model/FiltersModel.kt
Normal file
130
app/src/main/java/net/vonforst/evmap/model/FiltersModel.kt
Normal file
@@ -0,0 +1,130 @@
|
||||
package net.vonforst.evmap.model
|
||||
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
sealed class Filter<out T : FilterValue> : Equatable {
|
||||
abstract val name: String
|
||||
abstract val key: String
|
||||
abstract val valueClass: KClass<out T>
|
||||
abstract fun defaultValue(): T
|
||||
}
|
||||
|
||||
data class BooleanFilter(override val name: String, override val key: String) :
|
||||
Filter<BooleanFilterValue>() {
|
||||
override val valueClass: KClass<BooleanFilterValue> = BooleanFilterValue::class
|
||||
override fun defaultValue() = BooleanFilterValue(key, false)
|
||||
}
|
||||
|
||||
data class MultipleChoiceFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val choices: Map<String, String>,
|
||||
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)
|
||||
}
|
||||
|
||||
data class SliderFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val max: Int,
|
||||
val min: Int = 0,
|
||||
val mapping: ((Int) -> Int) = { it },
|
||||
val inverseMapping: ((Int) -> Int) = { it },
|
||||
val unit: String? = ""
|
||||
) : Filter<SliderFilterValue>() {
|
||||
override val valueClass: KClass<SliderFilterValue> = SliderFilterValue::class
|
||||
override fun defaultValue() = SliderFilterValue(key, min)
|
||||
}
|
||||
|
||||
sealed class FilterValue : BaseObservable(), Equatable {
|
||||
abstract val key: String
|
||||
var profile: Long = FILTERS_CUSTOM
|
||||
}
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class BooleanFilterValue(
|
||||
override val key: String,
|
||||
var value: Boolean
|
||||
) : FilterValue()
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class MultipleChoiceFilterValue(
|
||||
override val key: String,
|
||||
var values: MutableSet<String>,
|
||||
var all: Boolean
|
||||
) : FilterValue() {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other !is MultipleChoiceFilterValue) return false
|
||||
if (key != other.key) return false
|
||||
|
||||
return if (all) {
|
||||
other.all
|
||||
} else {
|
||||
!other.all && values == other.values
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = key.hashCode()
|
||||
result = 31 * result + all.hashCode()
|
||||
result = 31 * result + if (all) 0 else values.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class SliderFilterValue(
|
||||
override val key: String,
|
||||
var value: Int
|
||||
) : FilterValue()
|
||||
|
||||
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
|
||||
|
||||
typealias FilterValues = List<FilterWithValue<out FilterValue>>
|
||||
|
||||
fun FilterValues.getBooleanValue(key: String) =
|
||||
(this.find { it.value.key == key }!!.value as BooleanFilterValue).value
|
||||
|
||||
fun FilterValues.getSliderValue(key: String) =
|
||||
(this.find { it.value.key == key }!!.value as SliderFilterValue).value
|
||||
|
||||
fun FilterValues.getMultipleChoiceFilter(key: String) =
|
||||
this.find { it.value.key == key }!!.filter as MultipleChoiceFilter
|
||||
|
||||
fun FilterValues.getMultipleChoiceValue(key: String) =
|
||||
this.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
|
||||
|
||||
const val FILTERS_DISABLED = -2L
|
||||
const val FILTERS_CUSTOM = -1L
|
||||
@@ -0,0 +1,4 @@
|
||||
package net.vonforst.evmap.model
|
||||
|
||||
open class ReferenceData {
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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.io.IOException
|
||||
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
|
||||
|
||||
try {
|
||||
val response = api.getChargeCards()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (card in response.body()!!.result) {
|
||||
dao.insert(card)
|
||||
}
|
||||
|
||||
prefs.lastChargeCardUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
|
||||
@Dao
|
||||
interface ChargeLocationsDao {
|
||||
|
||||
@@ -7,12 +7,8 @@ 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.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
|
||||
import net.vonforst.evmap.viewmodel.SliderFilterValue
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargeCard
|
||||
import net.vonforst.evmap.model.*
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
@@ -21,9 +17,9 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
|
||||
MultipleChoiceFilterValue::class,
|
||||
SliderFilterValue::class,
|
||||
FilterProfile::class,
|
||||
Plug::class,
|
||||
Network::class,
|
||||
ChargeCard::class
|
||||
GEPlug::class,
|
||||
GENetwork::class,
|
||||
GEChargeCard::class
|
||||
], version = 11
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
@@ -31,9 +27,9 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun chargeLocationsDao(): ChargeLocationsDao
|
||||
abstract fun filterValueDao(): FilterValueDao
|
||||
abstract fun filterProfileDao(): FilterProfileDao
|
||||
abstract fun plugDao(): PlugDao
|
||||
abstract fun networkDao(): NetworkDao
|
||||
abstract fun chargeCardDao(): ChargeCardDao
|
||||
|
||||
// GoingElectric API specific
|
||||
abstract fun geReferenceDataDao(): GEReferenceDataDao
|
||||
|
||||
companion object {
|
||||
private lateinit var context: Context
|
||||
|
||||
@@ -3,7 +3,7 @@ package net.vonforst.evmap.storage
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
|
||||
@Entity(
|
||||
indices = [Index(value = ["name"], unique = true)]
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
import net.vonforst.evmap.model.*
|
||||
|
||||
@Dao
|
||||
abstract class FilterValueDao {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Entity(tableName = "Network")
|
||||
data class GENetwork(@PrimaryKey val name: String)
|
||||
|
||||
@Entity(tableName = "Plug")
|
||||
data class GEPlug(@PrimaryKey val name: String)
|
||||
|
||||
@Dao
|
||||
abstract class GEReferenceDataDao {
|
||||
// NETWORKS
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(vararg networks: GENetwork)
|
||||
|
||||
@Query("DELETE FROM network")
|
||||
abstract fun deleteAllNetworks()
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateNetworks(networks: List<GENetwork>) {
|
||||
deleteAllNetworks()
|
||||
for (network in networks) {
|
||||
insert(network)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM network")
|
||||
abstract fun getAllNetworks(): LiveData<List<GENetwork>>
|
||||
|
||||
// PLUGS
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(vararg plugs: GEPlug)
|
||||
|
||||
@Query("DELETE FROM plug")
|
||||
abstract fun deleteAllPlugs()
|
||||
|
||||
@Transaction
|
||||
open suspend fun updatePlugs(plugs: List<GEPlug>) {
|
||||
deleteAllPlugs()
|
||||
for (plug in plugs) {
|
||||
insert(plug)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM plug")
|
||||
abstract fun getAllPlugs(): LiveData<List<GEPlug>>
|
||||
|
||||
// CHARGE CARDS
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(vararg chargeCards: GEChargeCard)
|
||||
|
||||
@Query("DELETE FROM chargecard")
|
||||
abstract fun deleteAllChargeCards()
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateChargeCards(chargeCards: List<GEChargeCard>) {
|
||||
deleteAllChargeCards()
|
||||
for (chargeCard in chargeCards) {
|
||||
insert(chargeCard)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM chargecard")
|
||||
abstract fun getAllChargeCards(): LiveData<List<GEChargeCard>>
|
||||
}
|
||||
|
||||
class GEReferenceDataRepository(
|
||||
private val api: GoingElectricApiWrapper, private val scope: CoroutineScope,
|
||||
private val dao: GEReferenceDataDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getReferenceData(): LiveData<GEReferenceData> {
|
||||
scope.launch {
|
||||
updateData()
|
||||
}
|
||||
val plugs = dao.getAllPlugs()
|
||||
val networks = dao.getAllNetworks()
|
||||
val chargeCards = dao.getAllChargeCards()
|
||||
return MediatorLiveData<GEReferenceData>().apply {
|
||||
listOf(chargeCards, networks, plugs).map { source ->
|
||||
addSource(source) { _ ->
|
||||
val p = plugs.value ?: return@addSource
|
||||
val n = networks.value ?: return@addSource
|
||||
val cc = chargeCards.value ?: return@addSource
|
||||
value = GEReferenceData(p.map { it.name }, n.map { it.name }, cc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateData() {
|
||||
if (Duration.between(
|
||||
prefs.lastGeReferenceDataUpdate,
|
||||
Instant.now()
|
||||
) < Duration.ofDays(1)
|
||||
) return
|
||||
|
||||
val response = api.getReferenceData()
|
||||
if (response.status == Status.ERROR) return // ignore and retry next time
|
||||
|
||||
|
||||
val data = response.data!!
|
||||
dao.updateNetworks(data.networks.map { GENetwork(it) })
|
||||
dao.updatePlugs(data.plugs.map { GEPlug(it) })
|
||||
dao.updateChargeCards(data.chargecards)
|
||||
|
||||
prefs.lastGeReferenceDataUpdate = Instant.now()
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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.io.IOException
|
||||
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
|
||||
|
||||
try {
|
||||
val response = api.getNetworks()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Network(name))
|
||||
}
|
||||
|
||||
prefs.lastNetworkUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
data class Plug(@PrimaryKey val name: String)
|
||||
|
||||
@Dao
|
||||
interface PlugDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg plugs: Plug)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg plugs: Plug)
|
||||
|
||||
@Query("SELECT * FROM plug")
|
||||
fun getAllPlugs(): LiveData<List<Plug>>
|
||||
}
|
||||
|
||||
class PlugRepository(
|
||||
private val api: GoingElectricApi, private val scope: CoroutineScope,
|
||||
private val dao: PlugDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getPlugs(): LiveData<List<Plug>> {
|
||||
scope.launch {
|
||||
updatePlugs()
|
||||
}
|
||||
return dao.getAllPlugs()
|
||||
}
|
||||
|
||||
private suspend fun updatePlugs() {
|
||||
if (Duration.between(prefs.lastPlugUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
try {
|
||||
val response = api.getPlugs()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Plug(name))
|
||||
}
|
||||
|
||||
prefs.lastPlugUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.car2go.maps.AnyMap
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import java.time.Instant
|
||||
|
||||
class PreferenceDataSource(val context: Context) {
|
||||
@@ -17,22 +17,10 @@ class PreferenceDataSource(val context: Context) {
|
||||
sp.edit().putBoolean("navigate_use_maps", value).apply()
|
||||
}
|
||||
|
||||
var lastPlugUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_plug_update", 0L))
|
||||
var lastGeReferenceDataUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_ge_reference_data_update", 0L))
|
||||
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()
|
||||
sp.edit().putLong("last_ge_reference_data_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,9 +3,9 @@ package net.vonforst.evmap.storage
|
||||
import androidx.room.TypeConverter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCardId
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
|
||||
import net.vonforst.evmap.model.ChargeCardId
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
import java.time.Instant
|
||||
import java.time.LocalTime
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ package net.vonforst.evmap.ui;
|
||||
import com.car2go.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
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargeLocationCluster
|
||||
import net.vonforst.evmap.model.ChargepointListItem
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
|
||||
|
||||
fun cluster(
|
||||
|
||||
@@ -7,7 +7,7 @@ import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||
import com.car2go.maps.model.Marker
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import kotlin.math.max
|
||||
|
||||
fun getMarkerTint(
|
||||
|
||||
@@ -6,8 +6,8 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.banana.jsonapi2.HasOne
|
||||
import net.vonforst.evmap.api.chargeprice.*
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
@@ -10,8 +10,8 @@ import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
@@ -1,125 +1,15 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.lifecycle.*
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.ForeignKey.CASCADE
|
||||
import kotlinx.coroutines.launch
|
||||
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.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.cast
|
||||
|
||||
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
|
||||
internal fun mapPower(i: Int) = powerSteps[i]
|
||||
internal fun mapPowerInverse(power: Int) = powerSteps
|
||||
.mapIndexed { index, v -> abs(v - power) to index }
|
||||
.minByOrNull { it.first }?.second ?: 0
|
||||
|
||||
internal fun getFilters(
|
||||
application: Application,
|
||||
plugs: LiveData<List<Plug>>,
|
||||
networks: LiveData<List<Network>>,
|
||||
chargeCards: LiveData<List<ChargeCard>>
|
||||
): LiveData<List<Filter<FilterValue>>> {
|
||||
return MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
listOf(plugs, networks, chargeCards).forEach { source ->
|
||||
addSource(source) { _ ->
|
||||
buildFilters(plugs, networks, chargeCards, application)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
|
||||
plugs: LiveData<List<Plug>>,
|
||||
networks: LiveData<List<Network>>,
|
||||
chargeCards: LiveData<List<ChargeCard>>,
|
||||
application: Application
|
||||
) {
|
||||
val plugMap = plugs.value?.map { plug ->
|
||||
plug.name to nameForPlugType(application, 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
|
||||
val categoryMap = mapOf(
|
||||
"Autohaus" to application.getString(R.string.category_car_dealership),
|
||||
"Autobahnraststätte" to application.getString(R.string.category_service_on_motorway),
|
||||
"Autohof" to application.getString(R.string.category_service_off_motorway),
|
||||
"Bahnhof" to application.getString(R.string.category_railway_station),
|
||||
"Behörde" to application.getString(R.string.category_public_authorities),
|
||||
"Campingplatz" to application.getString(R.string.category_camping),
|
||||
"Einkaufszentrum" to application.getString(R.string.category_shopping_mall),
|
||||
"Ferienwohnung" to application.getString(R.string.category_holiday_home),
|
||||
"Flughafen" to application.getString(R.string.category_airport),
|
||||
"Freizeitpark" to application.getString(R.string.category_amusement_park),
|
||||
"Hotel" to application.getString(R.string.category_hotel),
|
||||
"Kino" to application.getString(R.string.category_cinema),
|
||||
"Kirche" to application.getString(R.string.category_church),
|
||||
"Krankenhaus" to application.getString(R.string.category_hospital),
|
||||
"Museum" to application.getString(R.string.category_museum),
|
||||
"Parkhaus" to application.getString(R.string.category_parking_multi),
|
||||
"Parkplatz" to application.getString(R.string.category_parking),
|
||||
"Privater Ladepunkt" to application.getString(R.string.category_private_charger),
|
||||
"Rastplatz" to application.getString(R.string.category_rest_area),
|
||||
"Restaurant" to application.getString(R.string.category_restaurant),
|
||||
"Schwimmbad" to application.getString(R.string.category_swimming_pool),
|
||||
"Supermarkt" to application.getString(R.string.category_supermarket),
|
||||
"Tankstelle" to application.getString(R.string.category_petrol_station),
|
||||
"Tiefgarage" to application.getString(R.string.category_parking_underground),
|
||||
"Tierpark" to application.getString(R.string.category_zoo),
|
||||
"Wohnmobilstellplatz" to application.getString(R.string.category_caravan_site)
|
||||
)
|
||||
value = listOf(
|
||||
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
|
||||
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
|
||||
BooleanFilter(application.getString(R.string.filter_open_247), "open_247"),
|
||||
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),
|
||||
manyChoices = true
|
||||
),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10,
|
||||
min = 1
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_networks), "networks",
|
||||
networkMap, manyChoices = true
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.categories), "categories",
|
||||
categoryMap,
|
||||
manyChoices = true
|
||||
),
|
||||
BooleanFilter(application.getString(R.string.filter_barrierfree), "barrierfree"),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_chargecards), "chargecards",
|
||||
chargecardMap, manyChoices = true
|
||||
),
|
||||
BooleanFilter(application.getString(R.string.filter_exclude_faults), "exclude_faults")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal fun filtersWithValue(
|
||||
filters: LiveData<List<Filter<FilterValue>>>,
|
||||
filterValues: LiveData<List<FilterValue>>
|
||||
@@ -140,21 +30,22 @@ internal fun filtersWithValue(
|
||||
|
||||
class FilterViewModel(application: Application, geApiKey: String) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey, context = application)
|
||||
private var api = GoingElectricApiWrapper(geApiKey, context = application)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
private val referenceData: LiveData<out ReferenceData> by lazy {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
viewModelScope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
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, networks, chargeCards)
|
||||
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
addSource(referenceData) { data ->
|
||||
value = api.getFilters(data as GEReferenceData, application.stringProvider())
|
||||
}
|
||||
}
|
||||
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
@@ -212,126 +103,4 @@ class FilterViewModel(application: Application, geApiKey: String) :
|
||||
// set selected profile
|
||||
prefs.filterStatus = profileId
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Filter<out T : FilterValue> : Equatable {
|
||||
abstract val name: String
|
||||
abstract val key: String
|
||||
abstract val valueClass: KClass<out T>
|
||||
abstract fun defaultValue(): T
|
||||
}
|
||||
|
||||
data class BooleanFilter(override val name: String, override val key: String) :
|
||||
Filter<BooleanFilterValue>() {
|
||||
override val valueClass: KClass<BooleanFilterValue> = BooleanFilterValue::class
|
||||
override fun defaultValue() = BooleanFilterValue(key, false)
|
||||
}
|
||||
|
||||
data class MultipleChoiceFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val choices: Map<String, String>,
|
||||
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)
|
||||
}
|
||||
|
||||
data class SliderFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val max: Int,
|
||||
val min: Int = 0,
|
||||
val mapping: ((Int) -> Int) = { it },
|
||||
val inverseMapping: ((Int) -> Int) = { it },
|
||||
val unit: String? = ""
|
||||
) : Filter<SliderFilterValue>() {
|
||||
override val valueClass: KClass<SliderFilterValue> = SliderFilterValue::class
|
||||
override fun defaultValue() = SliderFilterValue(key, min)
|
||||
}
|
||||
|
||||
sealed class FilterValue : BaseObservable(), Equatable {
|
||||
abstract val key: String
|
||||
var profile: Long = FILTERS_CUSTOM
|
||||
}
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class BooleanFilterValue(
|
||||
override val key: String,
|
||||
var value: Boolean
|
||||
) : FilterValue()
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class MultipleChoiceFilterValue(
|
||||
override val key: String,
|
||||
var values: MutableSet<String>,
|
||||
var all: Boolean
|
||||
) : FilterValue() {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other !is MultipleChoiceFilterValue) return false
|
||||
if (key != other.key) return false
|
||||
|
||||
return if (all) {
|
||||
other.all
|
||||
} else {
|
||||
!other.all && values == other.values
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = key.hashCode()
|
||||
result = 31 * result + all.hashCode()
|
||||
result = 31 * result + if (all) 0 else values.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class SliderFilterValue(
|
||||
override val key: String,
|
||||
var value: Int
|
||||
) : FilterValue()
|
||||
|
||||
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
|
||||
|
||||
typealias FilterValues = List<FilterWithValue<out FilterValue>>
|
||||
|
||||
fun FilterValues.getBooleanValue(key: String) =
|
||||
(this.find { it.value.key == key }!!.value as BooleanFilterValue).value
|
||||
|
||||
fun FilterValues.getSliderValue(key: String) =
|
||||
(this.find { it.value.key == key }!!.value as SliderFilterValue).value
|
||||
|
||||
fun FilterValues.getMultipleChoiceFilter(key: String) =
|
||||
this.find { it.value.key == key }!!.filter as MultipleChoiceFilter
|
||||
|
||||
fun FilterValues.getMultipleChoiceValue(key: String) =
|
||||
this.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
|
||||
|
||||
const val FILTERS_DISABLED = -2L
|
||||
const val FILTERS_CUSTOM = -1L
|
||||
}
|
||||
@@ -5,16 +5,17 @@ import androidx.lifecycle.*
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.storage.*
|
||||
import net.vonforst.evmap.ui.cluster
|
||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.GEReferenceDataRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import java.io.IOException
|
||||
|
||||
@@ -33,7 +34,7 @@ internal fun getClusterDistance(zoom: Float): Int? {
|
||||
}
|
||||
|
||||
class MapViewModel(application: Application, geApiKey: String) : AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey, context = application)
|
||||
private var api = GoingElectricApiWrapper(geApiKey, context = application)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
|
||||
@@ -56,16 +57,19 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
}
|
||||
}
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
private val referenceData: LiveData<out ReferenceData> by lazy {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
viewModelScope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
private val networks: LiveData<List<Network>> by lazy {
|
||||
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
|
||||
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
addSource(referenceData) { data ->
|
||||
value = api.getFilters(data as GEReferenceData, application.stringProvider())
|
||||
}
|
||||
}
|
||||
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<FilterValues> by lazy {
|
||||
filtersWithValue(filters, filterValues)
|
||||
@@ -78,10 +82,14 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
val chargeCardMap: LiveData<Map<Long, ChargeCard>> by lazy {
|
||||
MediatorLiveData<Map<Long, ChargeCard>>().apply {
|
||||
value = null
|
||||
addSource(chargeCards) {
|
||||
value = chargeCards.value?.map {
|
||||
it.id to it
|
||||
}?.toMap()
|
||||
addSource(referenceData) { data ->
|
||||
value = if (data is GEReferenceData) {
|
||||
data.chargecards.map {
|
||||
it.id to it.convert()
|
||||
}.toMap()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,10 +294,19 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
|
||||
val mapPosition = data.first
|
||||
val filters = data.second
|
||||
val result = getChargepointsWithFilters(mapPosition.bounds, mapPosition.zoom, filters)
|
||||
filteredConnectors.value = result.second
|
||||
filteredChargeCards.value = result.third
|
||||
chargepoints.value = result.first
|
||||
var result = api.getChargepoints(mapPosition.bounds, mapPosition.zoom, filters)
|
||||
if (result.status == Status.ERROR && result.data == null) {
|
||||
// keep old results if new data could not be loaded
|
||||
result = Resource.error(result.message, chargepoints.value?.data)
|
||||
}
|
||||
chargepoints.value = result
|
||||
|
||||
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")
|
||||
filteredChargeCards.value =
|
||||
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }.toSet()
|
||||
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")
|
||||
filteredConnectors.value = if (connectorsVal.all) null else connectorsVal.values
|
||||
}
|
||||
|
||||
private fun loadChargepoints(
|
||||
@@ -299,125 +316,6 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
chargepointLoader(Pair(mapPosition, filters))
|
||||
}
|
||||
|
||||
private suspend fun getChargepointsWithFilters(
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: FilterValues
|
||||
): Triple<Resource<List<ChargepointListItem>>, Set<String>?, Set<Long>?> {
|
||||
val freecharging = filters.getBooleanValue("freecharging")
|
||||
val freeparking = filters.getBooleanValue("freeparking")
|
||||
val open247 = filters.getBooleanValue("open_247")
|
||||
val barrierfree = filters.getBooleanValue("barrierfree")
|
||||
val excludeFaults = filters.getBooleanValue("exclude_faults")
|
||||
val minPower = filters.getSliderValue("min_power")
|
||||
val minConnectors = filters.getSliderValue("min_connectors")
|
||||
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Triple(Resource.success(emptyList()), null, null)
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
val filteredConnectors = if (connectorsVal.all) null else connectorsVal.values
|
||||
|
||||
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")
|
||||
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
|
||||
// no chargeCards chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, null)
|
||||
}
|
||||
val chargeCards = formatMultipleChoice(chargeCardsVal)
|
||||
val filteredChargeCards =
|
||||
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }.toSet()
|
||||
|
||||
val networksVal = filters.getMultipleChoiceValue("networks")
|
||||
if (networksVal.values.isEmpty() && !networksVal.all) {
|
||||
// no networks chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
val networks = formatMultipleChoice(networksVal)
|
||||
|
||||
val categoriesVal = filters.getMultipleChoiceValue("categories")
|
||||
if (categoriesVal.values.isEmpty() && !categoriesVal.all) {
|
||||
// no categories chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
val categories = formatMultipleChoice(categoriesVal)
|
||||
|
||||
// do not use clustering if filters need to be applied locally.
|
||||
val useClustering = zoom < 13
|
||||
val geClusteringAvailable = minConnectors <= 1
|
||||
val useGeClustering = useClustering && geClusteringAvailable
|
||||
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
|
||||
|
||||
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,
|
||||
open247 = open247,
|
||||
barrierfree = barrierfree,
|
||||
excludeFaults = excludeFaults,
|
||||
plugs = connectors,
|
||||
chargecards = chargeCards,
|
||||
networks = networks,
|
||||
categories = categories,
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Triple(
|
||||
Resource.error(response.message(), chargepoints.value?.data),
|
||||
null,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
val body = response.body()!!
|
||||
data.addAll(body.chargelocations)
|
||||
startkey = body.startkey
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Triple(
|
||||
Resource.error(e.message, chargepoints.value?.data),
|
||||
filteredConnectors,
|
||||
filteredChargeCards
|
||||
)
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
|
||||
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 Triple(Resource.success(result), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
|
||||
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =
|
||||
if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
|
||||
|
||||
private suspend fun loadAvailability(charger: ChargeLocation) {
|
||||
availability.value = Resource.loading(null)
|
||||
availability.value = getAvailability(charger)
|
||||
@@ -427,13 +325,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
chargerDetails.value = Resource.loading(null)
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val response = api.getChargepointDetail(charger.id)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
chargerDetails.value = Resource.error(response.message(), null)
|
||||
} else {
|
||||
chargerDetails.value =
|
||||
Resource.success(response.body()!!.chargelocations[0] as ChargeLocation)
|
||||
}
|
||||
chargerDetails.value = api.getChargepointDetail(charger.id)
|
||||
} catch (e: IOException) {
|
||||
chargerDetails.value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
@@ -446,19 +338,11 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
chargerSparse.value = null
|
||||
viewModelScope.launch {
|
||||
val response = api.getChargepointDetail(chargerId)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
chargerSparse.value = null
|
||||
chargerDetails.value = Resource.error(response.message(), null)
|
||||
chargerDetails.value = response
|
||||
if (response.status == Status.SUCCESS) {
|
||||
chargerSparse.value = response.data
|
||||
} else {
|
||||
val chargers = response.body()!!.chargelocations
|
||||
if (chargers.isNotEmpty()) {
|
||||
val charger = chargers[0] as ChargeLocation
|
||||
chargerDetails.value =
|
||||
Resource.success(charger)
|
||||
chargerSparse.value = charger} else {
|
||||
chargerDetails.value = Resource.error("not found", null)
|
||||
chargerSparse.value = null
|
||||
}
|
||||
chargerSparse.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.ChargeLocation" />
|
||||
<import type="net.vonforst.evmap.model.ChargeLocation" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.Chargepoint" />
|
||||
<import type="net.vonforst.evmap.model.Chargepoint" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.ChargeCard" />
|
||||
<import type="net.vonforst.evmap.model.ChargeCard" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.availability.ChargeLocationStatus" />
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.ChargerPhoto" />
|
||||
<import type="net.vonforst.evmap.model.ChargerPhoto" />
|
||||
|
||||
<import type="java.util.List" />
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.Chargepoint" />
|
||||
<import type="net.vonforst.evmap.model.Chargepoint" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<variable
|
||||
name="hours"
|
||||
type="net.vonforst.evmap.api.goingelectric.OpeningHoursDays" />
|
||||
type="net.vonforst.evmap.model.OpeningHoursDays" />
|
||||
|
||||
<variable
|
||||
name="dayOfWeek"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.BooleanFilter" />
|
||||
<import type="net.vonforst.evmap.model.BooleanFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.BooleanFilterValue" />
|
||||
<import type="net.vonforst.evmap.model.BooleanFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
<import type="net.vonforst.evmap.model.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilter" />
|
||||
<import type="net.vonforst.evmap.model.MultipleChoiceFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue" />
|
||||
<import type="net.vonforst.evmap.model.MultipleChoiceFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
<import type="net.vonforst.evmap.model.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilter" />
|
||||
<import type="net.vonforst.evmap.model.MultipleChoiceFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue" />
|
||||
<import type="net.vonforst.evmap.model.MultipleChoiceFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
<import type="net.vonforst.evmap.model.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.SliderFilter" />
|
||||
<import type="net.vonforst.evmap.model.SliderFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.SliderFilterValue" />
|
||||
<import type="net.vonforst.evmap.model.SliderFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
<import type="net.vonforst.evmap.model.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
|
||||
Reference in New Issue
Block a user