OpenChargeMap: implement filters

This commit is contained in:
johan12345
2021-07-03 20:26:05 +02:00
parent e098c70684
commit be071cfa3a
11 changed files with 190 additions and 70 deletions

View File

@@ -11,6 +11,7 @@ import okhttp3.Response
import org.json.JSONArray
import java.io.IOException
import kotlin.coroutines.resumeWithException
import kotlin.math.abs
operator fun <T> JSONArray.iterator(): Iterator<T> =
(0 until length()).asSequence().map {
@@ -101,4 +102,10 @@ fun iconForPlugType(type: String): Int =
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
// TODO: add other connectors
else -> 0
}
}
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
fun mapPower(i: Int) = powerSteps[i]
fun mapPowerInverse(power: Int) = powerSteps
.mapIndexed { index, v -> abs(v - power) to index }
.minByOrNull { it.first }?.second ?: 0

View File

@@ -10,9 +10,7 @@ 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.api.*
import net.vonforst.evmap.model.*
import net.vonforst.evmap.ui.cluster
import net.vonforst.evmap.viewmodel.Resource
@@ -25,7 +23,6 @@ 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/")
@@ -151,7 +148,7 @@ class GoingElectricApiWrapper(
return Resource.success(emptyList())
}
connectorsVal.values = connectorsVal.values.mapNotNull {
GEChargepoint.convertType(it)
GEChargepoint.convertTypeToGE(it)
}.toMutableSet()
val connectors = formatMultipleChoice(connectorsVal)
@@ -225,7 +222,7 @@ class GoingElectricApiWrapper(
it.chargepoints
.filter { it.power >= minPower }
.filter { if (!connectorsVal.all) it.type in connectorsVal.values else true }
.sumBy { it.count } >= minConnectors
.sumOf { it.count } >= minConnectors
} else {
true
}
@@ -380,11 +377,5 @@ class GoingElectricApiWrapper(
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
}

View File

@@ -173,26 +173,10 @@ data class GEAddress(
@JsonClass(generateAdapter = true)
data class GEChargepoint(val type: String, val power: Double, val count: Int) {
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
}
}
fun convert() = Chargepoint(convertTypeFromGE(type), power, count)
companion object {
fun convertType(type: String): String? {
fun convertTypeToGE(type: String): String? {
return when (type) {
Chargepoint.TYPE_1 -> "Typ1"
Chargepoint.TYPE_2_UNKNOWN -> "Typ2"
@@ -208,6 +192,22 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
else -> null
}
}
fun convertTypeFromGE(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
}
}
}
}

View File

@@ -7,8 +7,8 @@ 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.R
import net.vonforst.evmap.api.*
import net.vonforst.evmap.model.*
import net.vonforst.evmap.ui.cluster
import net.vonforst.evmap.viewmodel.Resource
@@ -27,8 +27,10 @@ interface OpenChargeMapApi {
@Query("boundingbox") boundingbox: OCMBoundingBox,
@Query("connectiontypeid") plugs: String? = null,
@Query("minpowerkw") minPower: Double? = null,
@Query("operatorid") operators: String? = null,
@Query("compact") compact: Boolean = true,
@Query("maxresults") maxresults: Int = 100
@Query("statustypeid") statusType: String? = null,
@Query("maxresults") maxresults: Int = 100,
): Response<List<OCMChargepoint>>
@GET("poi/")
@@ -89,6 +91,12 @@ class OpenChargeMapApiWrapper(
override fun getName() = "OpenChargeMap.org"
private fun formatMultipleChoice(value: MultipleChoiceFilterValue) =
if (value.all) null else value.values.joinToString(",")
// Unknown, Currently Available, Currently In Use, Operational, Partly Operational
private val noFaultStatuses = "0,10,20,50,75"
override suspend fun getChargepoints(
referenceData: ReferenceData,
bounds: LatLngBounds,
@@ -96,18 +104,46 @@ class OpenChargeMapApiWrapper(
filters: FilterValues,
): Resource<List<ChargepointListItem>> {
val referenceData = referenceData as OCMReferenceData
val minPower = filters.getSliderValue("min_power")!!.toDouble()
val minConnectors = filters.getSliderValue("min_connectors")!!
val excludeFaults = filters.getBooleanValue("exclude_faults")!!
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
}
val connectors = formatMultipleChoice(connectorsVal)
val operatorsVal = filters.getMultipleChoiceValue("operators")!!
if (operatorsVal.values.isEmpty() && !operatorsVal.all) {
// no operators chosen
return Resource.success(emptyList())
}
val operators = formatMultipleChoice(operatorsVal)
val response = api.getChargepoints(
OCMBoundingBox(
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude
)
),
minPower = minPower,
plugs = connectors,
operators = operators,
statusType = if (excludeFaults) noFaultStatuses else null
)
if (!response.isSuccessful) {
return Resource.error(response.message(), null)
}
var result = response.body()!!.map { it.convert(referenceData) }
.distinct() as List<ChargepointListItem>
var result = response.body()!!.filter { it ->
// apply filters which OCM does not support natively
it.connections
.filter { it.power == null || it.power >= minPower }
.filter { if (!connectorsVal.all) it.connectionTypeId in connectorsVal.values.map { it.toLong() } else true }
.sumOf { it.quantity ?: 0 } >= minConnectors
}.map { it.convert(referenceData) }.distinct() as List<ChargepointListItem>
val useClustering = zoom < 13
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
@@ -157,7 +193,46 @@ class OpenChargeMapApiWrapper(
sp: StringProvider
): List<Filter<FilterValue>> {
val referenceData = referenceData as OCMReferenceData
return emptyList()
val operatorsMap = referenceData.operators.map { it.id.toString() to it.title }.toMap()
val plugMap = referenceData.connectionTypes.map { it.id.toString() to it.title }.toMap()
return listOf(
// supported by OCM API
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(
"1", // Type 1 (J1772)
"25", // Type 2 (Socket only)
"1036", // Type 2 (Tethered connector)
"32", // CCS (Type 1)
"33", // CCS (Type 2)
"2" // CHAdeMO
),
manyChoices = true
),
MultipleChoiceFilter(
sp.getString(R.string.filter_operators), "operators",
operatorsMap, manyChoices = true
),
BooleanFilter(sp.getString(R.string.filter_exclude_faults), "exclude_faults"),
// local filters
SliderFilter(
sp.getString(R.string.filter_min_connectors),
"min_connectors",
10,
min = 1
),
)
}
}

View File

@@ -100,27 +100,30 @@ data class OCMConnection(
@Json(name = "Comments") val comments: String?
) {
fun convert(refData: OCMReferenceData) = Chargepoint(
convertConnectionType(connectionTypeId, refData),
convertConnectionTypeFromOCM(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 ?: ""
companion object {
fun convertConnectionTypeFromOCM(id: Long, refData: OCMReferenceData): String {
val title = refData.connectionTypes.find { it.id == id }?.title
return when (id) {
32L -> Chargepoint.CCS_TYPE_1
33L -> Chargepoint.CCS_TYPE_2
2L -> Chargepoint.CHADEMO
16L -> Chargepoint.CEE_BLAU
17L -> Chargepoint.CEE_ROT
28L -> Chargepoint.SCHUKO
8L -> Chargepoint.TESLA_ROADSTER_HPC
27L -> Chargepoint.SUPERCHARGER
25L -> Chargepoint.TYPE_2_SOCKET
1036L -> Chargepoint.TYPE_2_PLUG
1L -> Chargepoint.TYPE_1
36L -> Chargepoint.TYPE_3
26L -> Chargepoint.TYPE_3
else -> title ?: ""
}
}
}
}
@@ -128,7 +131,8 @@ data class OCMConnection(
@JsonClass(generateAdapter = true)
data class OCMReferenceData(
@Json(name = "ConnectionTypes") val connectionTypes: List<OCMConnectionType>,
@Json(name = "Countries") val countries: List<OCMCountry>
@Json(name = "Countries") val countries: List<OCMCountry>,
@Json(name = "Operators") val operators: List<OCMOperator>
) : ReferenceData()
@JsonClass(generateAdapter = true)
@@ -159,8 +163,9 @@ data class OCMDataProvider(
)
@JsonClass(generateAdapter = true)
@Entity
data class OCMOperator(
@Json(name = "ID") val id: Long,
@Json(name = "ID") @PrimaryKey val id: Long,
@Json(name = "WebsiteURL") val websiteUrl: String?,
@Json(name = "Title") val title: String,
@Json(name = "ContactEmail") val contactEmail: String?,

View File

@@ -10,6 +10,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import net.vonforst.evmap.api.goingelectric.GEChargeCard
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
import net.vonforst.evmap.api.openchargemap.OCMCountry
import net.vonforst.evmap.api.openchargemap.OCMOperator
import net.vonforst.evmap.model.*
@Database(
@@ -23,8 +24,9 @@ import net.vonforst.evmap.model.*
GENetwork::class,
GEChargeCard::class,
OCMConnectionType::class,
OCMCountry::class
], version = 14
OCMCountry::class,
OCMOperator::class
], version = 15
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
@@ -45,7 +47,7 @@ abstract class AppDatabase : RoomDatabase() {
.addMigrations(
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
MIGRATION_12, MIGRATION_13, MIGRATION_14
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -221,5 +223,11 @@ abstract class AppDatabase : RoomDatabase() {
}
}
private val MIGRATION_15 = object : Migration(14, 15) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `OCMOperator` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))");
}
}
}
}

View File

@@ -5,10 +5,7 @@ import androidx.lifecycle.MediatorLiveData
import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
import net.vonforst.evmap.api.openchargemap.OCMCountry
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.openchargemap.*
import net.vonforst.evmap.viewmodel.Status
import java.time.Duration
import java.time.Instant
@@ -50,6 +47,24 @@ abstract class OCMReferenceDataDao {
@Query("SELECT * FROM ocmcountry")
abstract fun getAllCountries(): LiveData<List<OCMCountry>>
// OPERATORS
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insert(vararg operators: OCMOperator)
@Query("DELETE FROM ocmoperator")
abstract fun deleteAllOperators()
@Transaction
open suspend fun updateOperators(operators: List<OCMOperator>) {
deleteAllOperators()
for (operator in operators) {
insert(operator)
}
}
@Query("SELECT * FROM ocmoperator")
abstract fun getAllOperators(): LiveData<List<OCMOperator>>
}
class OCMReferenceDataRepository(
@@ -62,13 +77,15 @@ class OCMReferenceDataRepository(
}
val connectionTypes = dao.getAllConnectionTypes()
val countries = dao.getAllCountries()
val operators = dao.getAllOperators()
return MediatorLiveData<OCMReferenceData>().apply {
listOf(countries, connectionTypes).map { source ->
listOf(countries, connectionTypes, operators).map { source ->
addSource(source) { _ ->
val ct = connectionTypes.value
val c = countries.value
if (ct.isNullOrEmpty() || c.isNullOrEmpty()) return@addSource
value = OCMReferenceData(ct, c)
val o = operators.value
if (ct.isNullOrEmpty() || c.isNullOrEmpty() || o.isNullOrEmpty()) return@addSource
value = OCMReferenceData(ct, c, o)
}
}
}
@@ -88,6 +105,7 @@ class OCMReferenceDataRepository(
val data = response.data!!
dao.updateConnectionTypes(data.connectionTypes)
dao.updateCountries(data.countries)
dao.updateOperators(data.operators)
prefs.lastOcmReferenceDataUpdate = Instant.now()
}

View File

@@ -5,7 +5,6 @@ import androidx.lifecycle.*
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.goingelectric.GEReferenceData
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.stringProvider
@@ -63,7 +62,7 @@ class FilterViewModel(application: Application, geApiKey: String) :
}
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { data ->
value = api.getFilters(data as GEReferenceData, application.stringProvider())
value = api.getFilters(data, application.stringProvider())
}
}

View File

@@ -10,8 +10,11 @@ import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.goingelectric.GEChargepoint
import net.vonforst.evmap.api.goingelectric.GEReferenceData
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OCMConnection
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.*
@@ -341,7 +344,19 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
.toSet()
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value = if (connectorsVal.all) null else connectorsVal.values
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
GEChargepoint.convertTypeFromGE(it)
}.toSet()
} else if (api is OpenChargeMapApiWrapper) {
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
if (connectorsVal.all) null else connectorsVal.values.map {
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
refData as OCMReferenceData
)
}.toSet()
}
}