OpenChargeMapApi: implement conversion of plug types

This commit is contained in:
johan12345
2021-06-20 17:17:05 +02:00
parent d5168f12c6
commit 7f8403cfb4
15 changed files with 178 additions and 49 deletions

View File

@@ -51,4 +51,13 @@ fun String.bold(): CharSequence {
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
}
}
}
fun <T> Collection<Iterable<T>>.cartesianProduct(): Set<Set<T>> =
/**
Returns all possible combinations of entries of a list
*/
if (isEmpty()) emptySet()
else drop(1).fold(first().map(::setOf)) { acc, iterable ->
acc.flatMap { list -> iterable.map(list::plus) }
}.toSet()

View File

@@ -44,9 +44,13 @@ suspend fun Call.await(): Response {
private val plugNames = mapOf(
Chargepoint.TYPE_1 to R.string.plug_type_1,
Chargepoint.TYPE_2 to R.string.plug_type_2,
Chargepoint.TYPE_2_UNKNOWN to R.string.plug_type_2,
Chargepoint.TYPE_2_PLUG to R.string.plug_type_2,
Chargepoint.TYPE_2_SOCKET to R.string.plug_type_2,
Chargepoint.TYPE_3 to R.string.plug_type_3,
Chargepoint.CCS to R.string.plug_ccs,
Chargepoint.CCS_UNKNOWN to R.string.plug_ccs,
Chargepoint.CCS_TYPE_1 to R.string.plug_ccs,
Chargepoint.CCS_TYPE_2 to R.string.plug_ccs,
Chargepoint.SCHUKO to R.string.plug_schuko,
Chargepoint.CHADEMO to R.string.plug_chademo,
Chargepoint.SUPERCHARGER to R.string.plug_supercharger,
@@ -60,14 +64,38 @@ fun nameForPlugType(ctx: StringProvider, type: String): String =
ctx.getString(it)
} ?: type
fun equivalentPlugTypes(type: String): Set<String> {
return when (type) {
Chargepoint.CCS_TYPE_1 -> setOf(Chargepoint.CCS_UNKNOWN, Chargepoint.CCS_TYPE_1)
Chargepoint.CCS_TYPE_2 -> setOf(Chargepoint.CCS_UNKNOWN, Chargepoint.CCS_TYPE_2)
Chargepoint.CCS_UNKNOWN -> setOf(
Chargepoint.CCS_UNKNOWN,
Chargepoint.CCS_TYPE_1,
Chargepoint.CCS_TYPE_2
)
Chargepoint.TYPE_2_PLUG -> setOf(Chargepoint.TYPE_2_UNKNOWN, Chargepoint.TYPE_2_PLUG)
Chargepoint.TYPE_2_SOCKET -> setOf(Chargepoint.TYPE_2_UNKNOWN, Chargepoint.TYPE_2_SOCKET)
Chargepoint.TYPE_2_UNKNOWN -> setOf(
Chargepoint.TYPE_2_UNKNOWN,
Chargepoint.TYPE_2_PLUG,
Chargepoint.TYPE_2_SOCKET
)
else -> setOf(type)
}
}
@DrawableRes
fun iconForPlugType(type: String): Int =
when (type) {
Chargepoint.CCS -> R.drawable.ic_connector_ccs
Chargepoint.CCS_TYPE_2 -> R.drawable.ic_connector_ccs
Chargepoint.CCS_UNKNOWN -> R.drawable.ic_connector_ccs
Chargepoint.CCS_TYPE_1 -> 0 // TODO: add CCS Type 1
Chargepoint.CHADEMO -> R.drawable.ic_connector_chademo
Chargepoint.SCHUKO -> R.drawable.ic_connector_schuko
Chargepoint.SUPERCHARGER -> R.drawable.ic_connector_supercharger
Chargepoint.TYPE_2 -> R.drawable.ic_connector_typ2
Chargepoint.TYPE_2_UNKNOWN -> R.drawable.ic_connector_typ2
Chargepoint.TYPE_2_SOCKET -> R.drawable.ic_connector_typ2
Chargepoint.TYPE_2_PLUG -> R.drawable.ic_connector_typ2
Chargepoint.CEE_BLAU -> R.drawable.ic_connector_cee_blau
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1

View File

@@ -6,6 +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.equivalentPlugTypes
import net.vonforst.evmap.cartesianProduct
import net.vonforst.evmap.model.*
import net.vonforst.evmap.viewmodel.Resource
import okhttp3.JavaNetCookieJar
@@ -64,8 +66,9 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
): Map<Chargepoint, Set<Long>> {
// iterate over each connector type
val types = connectors.map { it.value.second }.distinct().toSet()
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
val geTypes = chargepoints.map { it.type }.distinct().toSet()
if (types != geTypes) throw AvailabilityDetectorException("chargepoints do not match")
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
return types.flatMap { type ->
// find connectors of this type
val connsOfType = connectors.filter { it.value.second == type }
@@ -73,13 +76,14 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
val powers = connsOfType.map { it.value.first }.distinct().sorted()
// find corresponding powers in GE data
val gePowers =
chargepoints.filter { it.type == type }.map { it.power }.distinct().sorted()
chargepoints.filter { equivalentPlugTypes(it.type).any { it == type } }
.map { it.power }.distinct().sorted()
// if the distinct number of powers is the same, try to match.
if (powers.size == gePowers.size) {
gePowers.zip(powers).map { (gePower, power) ->
val chargepoint =
chargepoints.find { it.type == type && it.power == gePower }!!
chargepoints.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
val ids = connsOfType.filter { it.value.first == power }.keys
if (chargepoint.count != ids.size) {
throw AvailabilityDetectorException("chargepoints do not match")
@@ -120,7 +124,12 @@ data class ChargeLocationStatus(
val minPower = filters.getSliderValue("min_power")
val statusFiltered = status.filterKeys {
(connectorsVal.all || it.type in connectorsVal.values) && it.power > minPower
(connectorsVal == null || connectorsVal.all || connectorsVal.values.map {
equivalentPlugTypes(
it
)
}.any { equivalent -> it.type in equivalent })
&& (minPower == null || it.power > minPower)
}
return this.copy(status = statusFiltered)
}

View File

@@ -85,9 +85,9 @@ class ChargecloudAvailabilityDetector(
private fun getType(string: String): String {
return when (string) {
"IEC_62196_T2" -> Chargepoint.TYPE_2
"IEC_62196_T2" -> Chargepoint.TYPE_2_UNKNOWN
"DOMESTIC_F" -> Chargepoint.SCHUKO
"IEC_62196_T2_COMBO" -> Chargepoint.CCS
"IEC_62196_T2_COMBO" -> Chargepoint.CCS_TYPE_2
"CHADEMO" -> Chargepoint.CHADEMO
else -> throw IllegalArgumentException("unrecognized type $string")
}

View File

@@ -141,11 +141,11 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
val power = connector.electricalProperties.getPower()
val type = when (connector.connectorType.toLowerCase(Locale.ROOT)) {
"type3" -> Chargepoint.TYPE_3
"type2" -> Chargepoint.TYPE_2
"type2" -> Chargepoint.TYPE_2_UNKNOWN
"type1" -> Chargepoint.TYPE_1
"domestic" -> Chargepoint.SCHUKO
"type1combo" -> Chargepoint.CCS // US CCS, aka type1_combo
"type2combo" -> Chargepoint.CCS // EU CCS, aka type2_combo
"type1combo" -> Chargepoint.CCS_TYPE_1 // US CCS, aka type1_combo
"type2combo" -> Chargepoint.CCS_TYPE_2 // EU CCS, aka type2_combo
"tepcochademo" -> Chargepoint.CHADEMO
"unspecified" -> "unknown"
"unknown" -> "unknown"

View File

@@ -134,36 +134,39 @@ class GoingElectricApiWrapper(
zoom: Float,
filters: FilterValues
): Resource<List<ChargepointListItem>> {
val freecharging = filters.getBooleanValue("freecharging")
val freeparking = filters.getBooleanValue("freeparking")
val open247 = filters.getBooleanValue("open_247")
val barrierfree = filters.getBooleanValue("barrierfree")
val excludeFaults = filters.getBooleanValue("exclude_faults")
val minPower = filters.getSliderValue("min_power")
val minConnectors = filters.getSliderValue("min_connectors")
val 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")
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
}
connectorsVal.values = connectorsVal.values.mapNotNull {
GEChargepoint.convertType(it)
}.toMutableSet()
val connectors = formatMultipleChoice(connectorsVal)
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")
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")
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")
val categoriesVal = filters.getMultipleChoiceValue("categories")!!
if (categoriesVal.values.isEmpty() && !categoriesVal.all) {
// no categories chosen
return Resource.success(emptyList())
@@ -344,7 +347,11 @@ class GoingElectricApiWrapper(
MultipleChoiceFilter(
sp.getString(R.string.filter_connectors), "connectors",
plugMap,
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO),
commonChoices = setOf(
Chargepoint.TYPE_2_UNKNOWN,
Chargepoint.CCS_UNKNOWN,
Chargepoint.CHADEMO
),
manyChoices = true
),
SliderFilter(

View File

@@ -170,7 +170,42 @@ data class GEAddress(
@JsonClass(generateAdapter = true)
data class GEChargepoint(val type: String, val power: Double, val count: Int) {
fun convert() = Chargepoint(type, power, count)
fun convert() = Chargepoint(convertType(type), power, count)
private fun convertType(type: String): String {
return when (type) {
"Typ1" -> Chargepoint.TYPE_1
"Typ2" -> Chargepoint.TYPE_2_UNKNOWN
"Typ3" -> Chargepoint.TYPE_3
"CCS" -> Chargepoint.CCS_UNKNOWN
"Schuko" -> Chargepoint.SCHUKO
"CHAdeMO" -> Chargepoint.CHADEMO
"Tesla Supercharger" -> Chargepoint.SUPERCHARGER
"CEE Blau" -> Chargepoint.CEE_BLAU
"CEE Rot" -> Chargepoint.CEE_ROT
"Tesla HPC" -> Chargepoint.TESLA_ROADSTER_HPC
else -> type
}
}
companion object {
fun convertType(type: String): String? {
return when (type) {
Chargepoint.TYPE_1 -> "Typ1"
Chargepoint.TYPE_2_UNKNOWN -> "Typ2"
Chargepoint.TYPE_3 -> "Typ3"
Chargepoint.CCS_UNKNOWN -> "CCS"
Chargepoint.CCS_TYPE_2 -> "Typ2"
Chargepoint.SCHUKO -> "Schuko"
Chargepoint.CHADEMO -> "CHAdeMO"
Chargepoint.SUPERCHARGER -> "Tesla Supercharger"
Chargepoint.CEE_BLAU -> "CEE Blau"
Chargepoint.CEE_ROT -> "CEE Rot"
Chargepoint.TESLA_ROADSTER_HPC -> "Tesla HPC"
else -> null
}
}
}
}
@JsonClass(generateAdapter = true)

View File

@@ -5,11 +5,14 @@ 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 net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.StringProvider
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
@@ -100,7 +103,17 @@ class OpenChargeMapApiWrapper(
return Resource.error(response.message(), null)
}
val result = response.body()!!.map { it.convert(referenceData) }
var result = response.body()!!.map { it.convert(referenceData) }
.distinct() as List<ChargepointListItem>
val useClustering = zoom < 13
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
if (useClustering) {
Dispatchers.IO.run {
result = cluster(result, zoom, clusterDistance!!)
}
}
return Resource.success(result)
}

View File

@@ -43,7 +43,7 @@ data class OCMChargepoint(
null, // TODO: MediaItems,
null,
null,
Cost(descriptionLong = cost)
cost?.let { Cost(descriptionShort = it) }
)
}
@@ -77,15 +77,34 @@ data class OCMConnection(
@Json(name = "ConnectionTypeID") val connectionTypeId: Long,
@Json(name = "Amps") val amps: Int?,
@Json(name = "Voltage") val voltage: Int?,
@Json(name = "PowerKW") val power: Double,
@Json(name = "PowerKW") val power: Double?,
@Json(name = "Quantity") val quantity: Int?,
@Json(name = "Comments") val comments: String?
) {
fun convert(refData: OCMReferenceData) = Chargepoint(
refData.connectionTypes.find { it.id == connectionTypeId }!!.title,
power,
convertConnectionType(connectionTypeId, refData),
power ?: 0.0,
quantity ?: 0
)
private fun convertConnectionType(id: Long, refData: OCMReferenceData): String {
val title = refData.connectionTypes.find { it.id == id }!!.title
return when (title) {
"CCS (Type 2)" -> Chargepoint.CCS_TYPE_2
"CHAdeMO" -> Chargepoint.CHADEMO
"CEE 3 Pin" -> Chargepoint.CEE_BLAU
"CEE 5 Pin" -> Chargepoint.CEE_ROT
"CEE 7/4 - Schuko - Type F" -> Chargepoint.SCHUKO
"Tesla (Roadster)" -> Chargepoint.TESLA_ROADSTER_HPC
"Tesla Supercharger" -> Chargepoint.SUPERCHARGER
"Type 2 (Socket Only)" -> Chargepoint.TYPE_2_SOCKET
"Type 2 (Tethered Connector) " -> Chargepoint.TYPE_2_PLUG
"Type 1 (J1772)" -> Chargepoint.TYPE_1
"SCAME Type 3A (Low Power)" -> Chargepoint.TYPE_3
"SCAME Type 3C (Schneider-Legrand)" -> Chargepoint.TYPE_3
else -> title
}
}
}
@JsonClass(generateAdapter = true)

View File

@@ -8,6 +8,8 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.StringProvider
import net.vonforst.evmap.api.nameForPlugType
import java.time.DayOfWeek
import java.time.Instant
import java.time.LocalDate
@@ -93,9 +95,9 @@ data class ChargeLocation(
val totalChargepoints: Int
get() = chargepoints.sumBy { it.count }
fun formatChargepoints(): String {
fun formatChargepoints(sp: StringProvider): String {
return chargepointsMerged.map {
"${it.count} × ${it.type} ${it.formatPower()}"
"${it.count} × ${nameForPlugType(sp, it.type)} ${it.formatPower()}"
}.joinToString(" · ")
}
}
@@ -267,10 +269,14 @@ data class Chargepoint(val type: String, val power: Double, val count: Int) : Eq
}
companion object {
const val TYPE_1 = "Typ1"
const val TYPE_2 = "Typ2"
const val TYPE_3 = "Typ3"
const val CCS = "CCS"
const val TYPE_1 = "Type 1"
const val TYPE_2_UNKNOWN = "Type 2 (either plug or socket)"
const val TYPE_2_SOCKET = "Type 2 socket"
const val TYPE_2_PLUG = "Type 2 plug"
const val TYPE_3 = "Type 3"
const val CCS_TYPE_2 = "CCS Type 2"
const val CCS_TYPE_1 = "CCS Type 1"
const val CCS_UNKNOWN = "CCS (either Type 1 or Type 2)"
const val SCHUKO = "Schuko"
const val CHADEMO = "CHAdeMO"
const val SUPERCHARGER = "Tesla Supercharger"

View File

@@ -115,16 +115,16 @@ data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T)
typealias FilterValues = List<FilterWithValue<out FilterValue>>
fun FilterValues.getBooleanValue(key: String) =
(this.find { it.value.key == key }!!.value as BooleanFilterValue).value
(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
(this.find { it.value.key == key }?.value as SliderFilterValue?)?.value
fun FilterValues.getMultipleChoiceFilter(key: String) =
this.find { it.value.key == key }!!.filter as MultipleChoiceFilter
this.find { it.value.key == key }?.filter as MultipleChoiceFilter?
fun FilterValues.getMultipleChoiceValue(key: String) =
this.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
this.find { it.value.key == key }?.value as MultipleChoiceFilterValue?
const val FILTERS_DISABLED = -2L
const val FILTERS_CUSTOM = -1L

View File

@@ -36,12 +36,12 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
Chargepoint.CEE_ROT,
Chargepoint.SCHUKO,
Chargepoint.TYPE_1,
Chargepoint.TYPE_2
Chargepoint.TYPE_2_UNKNOWN
)
private val plugMapping = mapOf(
"ccs" to Chargepoint.CCS,
"ccs" to Chargepoint.CCS_UNKNOWN,
"tesla_suc" to Chargepoint.SUPERCHARGER,
"tesla_ccs" to Chargepoint.CCS,
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
"chademo" to Chargepoint.CHADEMO
)
val vehicleCompatibleConnectors: LiveData<List<String>> by lazy {

View File

@@ -335,12 +335,12 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
chargepoints.value = result
if (api is GoingElectricApiWrapper) {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
filteredChargeCards.value =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
.toSet()
val connectorsVal = filters.getMultipleChoiceValue("connectors")
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value = if (connectorsVal.all) null else connectorsVal.values
}
}

View File

@@ -23,6 +23,8 @@
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="net.vonforst.evmap.api.ChargepointApiKt" />
<variable
name="charger"
type="Resource&lt;ChargeLocation&gt;" />
@@ -140,7 +142,7 @@
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{charger.data.formatChargepoints()}"
android:text="@{charger.data.formatChargepoints(ChargepointApiKt.stringProvider(context))}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/txtDistance"
app:layout_constraintStart_toStartOf="@+id/guideline"

View File

@@ -8,6 +8,7 @@
<import type="net.vonforst.evmap.api.UtilsKt" />
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<import type="net.vonforst.evmap.api.ChargepointApiKt" />
<variable
name="item"
@@ -51,7 +52,7 @@
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{item.charger.formatChargepoints()}"
android:text="@{item.charger.formatChargepoints(ChargepointApiKt.stringProvider(context))}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/textView7"
app:layout_constraintStart_toStartOf="@+id/textView2"