diff --git a/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt index 94fb02c1..6d19ebb3 100644 --- a/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt +++ b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt @@ -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()}" diff --git a/app/src/main/java/net/vonforst/evmap/MapsActivity.kt b/app/src/main/java/net/vonforst/evmap/MapsActivity.kt index 2ab57a59..b8a10ff5 100644 --- a/app/src/main/java/net/vonforst/evmap/MapsActivity.kt +++ b/app/src/main/java/net/vonforst/evmap/MapsActivity.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt index a9fffcac..97af6f3a 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt @@ -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(getKey: ((T) -> Any)? = null) : diff --git a/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt b/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt index 4333f037..0e55d9d8 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt b/app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt index 1cff62f2..268ad7d4 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt @@ -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>() { diff --git a/app/src/main/java/net/vonforst/evmap/adapter/GalleryAdapter.kt b/app/src/main/java/net/vonforst/evmap/adapter/GalleryAdapter.kt index 64e8ecfd..8a48dde9 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/GalleryAdapter.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/GalleryAdapter.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt b/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt new file mode 100644 index 00000000..9c420f27 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt @@ -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 { + suspend fun getChargepoints( + bounds: LatLngBounds, + zoom: Float, + filters: FilterValues + ): Resource> + + suspend fun getChargepointsRadius( + location: LatLng, + radius: Int, + zoom: Float, + filters: FilterValues + ): Resource> + + suspend fun getChargepointDetail(id: Long): Resource + + suspend fun getReferenceData(): Resource + + fun getFilters(referenceData: T, sp: StringProvider): List> +} + +interface StringProvider { + fun getString(id: Int): String +} + +fun Context.stringProvider() = object : StringProvider { + override fun getString(id: Int): String { + return this@stringProvider.getString(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/Utils.kt b/app/src/main/java/net/vonforst/evmap/api/Utils.kt index 6fe5e2b7..6a4409c3 100644 --- a/app/src/main/java/net/vonforst/evmap/api/Utils.kt +++ b/app/src/main/java/net/vonforst/evmap/api/Utils.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt index d7400725..a90dccd6 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/ChargecloudAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/ChargecloudAvailabilityDetector.kt index 3676c910..393dca61 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/ChargecloudAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/ChargecloudAvailabilityDetector.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt index 350ac189..7a9fedd6 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt index 3294f611..2edf9285 100644 --- a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricAdapters.kt b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricAdapters.kt index 339644d9..7ce2b095 100644 --- a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricAdapters.kt @@ -13,7 +13,7 @@ internal class ChargepointListItemJsonAdapterFactory : JsonAdapter.Factory { annotations: MutableSet, 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() { + JsonAdapter() { private val clusterAdapter = - moshi.adapter( - ChargeLocationCluster::class.java + moshi.adapter( + GEChargeLocationCluster::class.java ) - private val locationAdapter = moshi.adapter( - ChargeLocation::class.java + private val locationAdapter = moshi.adapter( + 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 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 { diff --git a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt index 6c3f502f..af064869 100644 --- a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt @@ -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 + ): Response @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 + ): Response @GET("chargepoints/") - suspend fun getChargepointDetail(@Query("ge_id") id: Long): Response + suspend fun getChargepointDetail(@Query("ge_id") id: Long): Response @GET("chargepoints/pluglist/") - suspend fun getPlugs(): Response + suspend fun getPlugs(): Response @GET("chargepoints/networklist/") - suspend fun getNetworks(): Response + suspend fun getNetworks(): Response @GET("chargepoints/chargecardlist/") - suspend fun getChargeCards(): Response + suspend fun getChargeCards(): Response 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 { + val api = GoingElectricApi.create(apikey, baseurl, context) + override suspend fun getChargepoints( + bounds: LatLngBounds, + zoom: Float, + filters: FilterValues + ): Resource> { + 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() + 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> { + TODO("Not yet implemented") + } + + override suspend fun getChargepointDetail(id: Long): Resource { + 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 = + 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> { + 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 +} + diff --git a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt index 2c50179f..c0789cac 100644 --- a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricModel.kt @@ -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, + val chargelocations: List, @JsonObjectOrFalse val startkey: Int? ) @JsonClass(generateAdapter = true) -data class StringList( +data class GEStringList( val status: String, val result: List ) @JsonClass(generateAdapter = true) -data class ChargeCardList( +data class GEChargeCardList( val status: String, - val result: List + val result: List ) -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, + val coordinates: GECoordinate, + val address: GEAddress, + val chargepoints: List, @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?, - @JsonObjectOrFalse val chargecards: List?, - @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? = null): Double { - return chargepoints.filter { connectors?.contains(it.type) ?: true } - .map { it.power }.maxOrNull() ?: 0.0 - } - - fun isMulti(filteredConnectors: Set? = 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 - 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?, + @JsonObjectOrFalse val chargecards: List?, + 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 -) \ No newline at end of file +) { + fun convert() = ChargeCardId(id) +} + +data class GEReferenceData( + val plugs: List, + val networks: List, + val chargecards: List +) : ReferenceData() \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt index f05a2a58..6c7052b9 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/fragment/GalleryFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/GalleryFragment.kt index e1491cc7..10c16c91 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/GalleryFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/GalleryFragment.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt index a0f04693..d1c7b535 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt new file mode 100644 index 00000000..ee09cc6f --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt @@ -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, + 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?, + val chargecards: List?, + @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? = null): Double { + return chargepoints.filter { connectors?.contains(it.type) ?: true } + .map { it.power }.maxOrNull() ?: 0.0 + } + + fun isMulti(filteredConnectors: Set? = 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 + 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 +) \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/model/FiltersModel.kt b/app/src/main/java/net/vonforst/evmap/model/FiltersModel.kt new file mode 100644 index 00000000..902b037a --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/model/FiltersModel.kt @@ -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 : Equatable { + abstract val name: String + abstract val key: String + abstract val valueClass: KClass + abstract fun defaultValue(): T +} + +data class BooleanFilter(override val name: String, override val key: String) : + Filter() { + override val valueClass: KClass = BooleanFilterValue::class + override fun defaultValue() = BooleanFilterValue(key, false) +} + +data class MultipleChoiceFilter( + override val name: String, + override val key: String, + val choices: Map, + val commonChoices: Set? = null, + val manyChoices: Boolean = false +) : Filter() { + override val valueClass: KClass = 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() { + override val valueClass: KClass = 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, + 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(val filter: Filter, val value: T) : Equatable + +typealias FilterValues = List> + +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 \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/model/ReferenceDataModel.kt b/app/src/main/java/net/vonforst/evmap/model/ReferenceDataModel.kt new file mode 100644 index 00000000..1e6cffb7 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/model/ReferenceDataModel.kt @@ -0,0 +1,4 @@ +package net.vonforst.evmap.model + +open class ReferenceData { +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/ChargeCardDao.kt b/app/src/main/java/net/vonforst/evmap/storage/ChargeCardDao.kt deleted file mode 100644 index 9afd52e3..00000000 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeCardDao.kt +++ /dev/null @@ -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> -} - -class ChargeCardRepository( - private val api: GoingElectricApi, private val scope: CoroutineScope, - private val dao: ChargeCardDao, private val prefs: PreferenceDataSource -) { - fun getChargeCards(): LiveData> { - 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 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt index 24c82a6d..af04786d 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -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 { diff --git a/app/src/main/java/net/vonforst/evmap/storage/Database.kt b/app/src/main/java/net/vonforst/evmap/storage/Database.kt index 011b27ad..a1a02c41 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/Database.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/Database.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/storage/FilterProfileDao.kt b/app/src/main/java/net/vonforst/evmap/storage/FilterProfileDao.kt index b6736221..1bf62915 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/FilterProfileDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/FilterProfileDao.kt @@ -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)] diff --git a/app/src/main/java/net/vonforst/evmap/storage/FilterValueDao.kt b/app/src/main/java/net/vonforst/evmap/storage/FilterValueDao.kt index b715fc6a..3a72701e 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/FilterValueDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/FilterValueDao.kt @@ -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 { diff --git a/app/src/main/java/net/vonforst/evmap/storage/GEReferenceDataDao.kt b/app/src/main/java/net/vonforst/evmap/storage/GEReferenceDataDao.kt new file mode 100644 index 00000000..1e79d08c --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/storage/GEReferenceDataDao.kt @@ -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) { + deleteAllNetworks() + for (network in networks) { + insert(network) + } + } + + @Query("SELECT * FROM network") + abstract fun getAllNetworks(): LiveData> + + // 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) { + deleteAllPlugs() + for (plug in plugs) { + insert(plug) + } + } + + @Query("SELECT * FROM plug") + abstract fun getAllPlugs(): LiveData> + + // 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) { + deleteAllChargeCards() + for (chargeCard in chargeCards) { + insert(chargeCard) + } + } + + @Query("SELECT * FROM chargecard") + abstract fun getAllChargeCards(): LiveData> +} + +class GEReferenceDataRepository( + private val api: GoingElectricApiWrapper, private val scope: CoroutineScope, + private val dao: GEReferenceDataDao, private val prefs: PreferenceDataSource +) { + fun getReferenceData(): LiveData { + scope.launch { + updateData() + } + val plugs = dao.getAllPlugs() + val networks = dao.getAllNetworks() + val chargeCards = dao.getAllChargeCards() + return MediatorLiveData().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() + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt b/app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt deleted file mode 100644 index aceae272..00000000 --- a/app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt +++ /dev/null @@ -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> -} - -class NetworkRepository( - private val api: GoingElectricApi, private val scope: CoroutineScope, - private val dao: NetworkDao, private val prefs: PreferenceDataSource -) { - fun getNetworks(): LiveData> { - 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 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/PlugDao.kt b/app/src/main/java/net/vonforst/evmap/storage/PlugDao.kt deleted file mode 100644 index 93548c11..00000000 --- a/app/src/main/java/net/vonforst/evmap/storage/PlugDao.kt +++ /dev/null @@ -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> -} - -class PlugRepository( - private val api: GoingElectricApi, private val scope: CoroutineScope, - private val dao: PlugDao, private val prefs: PreferenceDataSource -) { - fun getPlugs(): LiveData> { - 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 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt b/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt index 98fb2a76..b15f9bfe 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt @@ -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() } /** diff --git a/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt b/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt index d4a09450..fd72e1c4 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/ui/Clustering.kt b/app/src/main/java/net/vonforst/evmap/ui/Clustering.kt index c2f24445..6c5a361e 100644 --- a/app/src/main/java/net/vonforst/evmap/ui/Clustering.kt +++ b/app/src/main/java/net/vonforst/evmap/ui/Clustering.kt @@ -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( diff --git a/app/src/main/java/net/vonforst/evmap/ui/MarkerUtils.kt b/app/src/main/java/net/vonforst/evmap/ui/MarkerUtils.kt index 38f0f03e..962f3570 100644 --- a/app/src/main/java/net/vonforst/evmap/ui/MarkerUtils.kt +++ b/app/src/main/java/net/vonforst/evmap/ui/MarkerUtils.kt @@ -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( diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt index bf1b11c7..5faabbe0 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt @@ -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.* diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt index d969949d..cf049908 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/FilterProfilesViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/FilterProfilesViewModel.kt index 8a8bfd72..a0555a6b 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/FilterProfilesViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/FilterProfilesViewModel.kt @@ -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 diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt index 0fb16e73..0d484ff2 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/FilterViewModel.kt @@ -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>, - networks: LiveData>, - chargeCards: LiveData> -): LiveData>> { - return MediatorLiveData>>().apply { - listOf(plugs, networks, chargeCards).forEach { source -> - addSource(source) { _ -> - buildFilters(plugs, networks, chargeCards, application) - } - } - } -} - -private fun MediatorLiveData>>.buildFilters( - plugs: LiveData>, - networks: LiveData>, - chargeCards: LiveData>, - 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>>, filterValues: LiveData> @@ -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> by lazy { - PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs() + private val referenceData: LiveData by lazy { + GEReferenceDataRepository( + api, + viewModelScope, + db.geReferenceDataDao(), + prefs + ).getReferenceData() } - private val networks: LiveData> by lazy { - NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks() - } - private val chargeCards: LiveData> by lazy { - ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards() - } - private val filters: LiveData>> by lazy { - getFilters(application, plugs, networks, chargeCards) + private val filters = MediatorLiveData>>().apply { + addSource(referenceData) { data -> + value = api.getFilters(data as GEReferenceData, application.stringProvider()) + } } private val filterValues: LiveData> by lazy { @@ -212,126 +103,4 @@ class FilterViewModel(application: Application, geApiKey: String) : // set selected profile prefs.filterStatus = profileId } -} - -sealed class Filter : Equatable { - abstract val name: String - abstract val key: String - abstract val valueClass: KClass - abstract fun defaultValue(): T -} - -data class BooleanFilter(override val name: String, override val key: String) : - Filter() { - override val valueClass: KClass = BooleanFilterValue::class - override fun defaultValue() = BooleanFilterValue(key, false) -} - -data class MultipleChoiceFilter( - override val name: String, - override val key: String, - val choices: Map, - val commonChoices: Set? = null, - val manyChoices: Boolean = false -) : Filter() { - override val valueClass: KClass = 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() { - override val valueClass: KClass = 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, - 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(val filter: Filter, val value: T) : Equatable - -typealias FilterValues = List> - -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 \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt index b22bf9f6..28edff35 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt @@ -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> by lazy { - PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs() + private val referenceData: LiveData by lazy { + GEReferenceDataRepository( + api, + viewModelScope, + db.geReferenceDataDao(), + prefs + ).getReferenceData() } - private val networks: LiveData> by lazy { - NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks() + private val filters = MediatorLiveData>>().apply { + addSource(referenceData) { data -> + value = api.getFilters(data as GEReferenceData, application.stringProvider()) + } } - private val chargeCards: LiveData> by lazy { - ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards() - } - private val filters = getFilters(application, plugs, networks, chargeCards) private val filtersWithValue: LiveData by lazy { filtersWithValue(filters, filterValues) @@ -78,10 +82,14 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode val chargeCardMap: LiveData> by lazy { MediatorLiveData>().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>, Set?, Set?> { - 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() - 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 } } } diff --git a/app/src/main/res/layout/detail_view.xml b/app/src/main/res/layout/detail_view.xml index 4d284d3c..754f6695 100644 --- a/app/src/main/res/layout/detail_view.xml +++ b/app/src/main/res/layout/detail_view.xml @@ -5,11 +5,11 @@ - + - + - + diff --git a/app/src/main/res/layout/fragment_gallery.xml b/app/src/main/res/layout/fragment_gallery.xml index 5e697208..819c3789 100644 --- a/app/src/main/res/layout/fragment_gallery.xml +++ b/app/src/main/res/layout/fragment_gallery.xml @@ -4,7 +4,7 @@ - + diff --git a/app/src/main/res/layout/item_connector_button.xml b/app/src/main/res/layout/item_connector_button.xml index 4644fb73..34b05ac8 100644 --- a/app/src/main/res/layout/item_connector_button.xml +++ b/app/src/main/res/layout/item_connector_button.xml @@ -5,7 +5,7 @@ - + diff --git a/app/src/main/res/layout/item_detail_openinghours_item.xml b/app/src/main/res/layout/item_detail_openinghours_item.xml index f16c128a..f517ba7b 100644 --- a/app/src/main/res/layout/item_detail_openinghours_item.xml +++ b/app/src/main/res/layout/item_detail_openinghours_item.xml @@ -11,7 +11,7 @@ + type="net.vonforst.evmap.model.OpeningHoursDays" /> - + - + - + - + - + - + - + - + - + - + - + - +