continue working on OCM API, proof of concept works

This commit is contained in:
johan12345
2021-06-06 21:59:00 +02:00
parent 9b94bbf098
commit d5168f12c6
10 changed files with 271 additions and 89 deletions

View File

@@ -6,25 +6,30 @@ import com.car2go.maps.model.LatLngBounds
import net.vonforst.evmap.model.*
import net.vonforst.evmap.viewmodel.Resource
interface ChargepointApi<T : ReferenceData> {
interface ChargepointApi<out T : ReferenceData> {
suspend fun getChargepoints(
referenceData: ReferenceData,
bounds: LatLngBounds,
zoom: Float,
filters: FilterValues
): Resource<List<ChargepointListItem>>
suspend fun getChargepointsRadius(
referenceData: ReferenceData,
location: LatLng,
radius: Int,
zoom: Float,
filters: FilterValues
): Resource<List<ChargepointListItem>>
suspend fun getChargepointDetail(id: Long): Resource<ChargeLocation>
suspend fun getChargepointDetail(
referenceData: ReferenceData,
id: Long
): Resource<ChargeLocation>
suspend fun getReferenceData(): Resource<T>
fun getFilters(referenceData: T, sp: StringProvider): List<Filter<FilterValue>>
fun getFilters(referenceData: ReferenceData, sp: StringProvider): List<Filter<FilterValue>>
}
interface StringProvider {

View File

@@ -129,6 +129,7 @@ class GoingElectricApiWrapper(
) : ChargepointApi<GEReferenceData> {
val api = GoingElectricApi.create(apikey, baseurl, context)
override suspend fun getChargepoints(
referenceData: ReferenceData,
bounds: LatLngBounds,
zoom: Float,
filters: FilterValues
@@ -237,6 +238,7 @@ class GoingElectricApiWrapper(
if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
override suspend fun getChargepointsRadius(
referenceData: ReferenceData,
location: LatLng,
radius: Int,
zoom: Float,
@@ -245,7 +247,10 @@ class GoingElectricApiWrapper(
TODO("Not yet implemented")
}
override suspend fun getChargepointDetail(id: Long): Resource<ChargeLocation> {
override suspend fun getChargepointDetail(
referenceData: ReferenceData,
id: Long
): Resource<ChargeLocation> {
val response = api.getChargepointDetail(id)
return if (response.isSuccessful && response.body()!!.status == "ok" && response.body()!!.chargelocations.size == 1) {
Resource.success(
@@ -284,9 +289,10 @@ class GoingElectricApiWrapper(
}
override fun getFilters(
referenceData: GEReferenceData,
referenceData: ReferenceData,
sp: StringProvider
): List<Filter<FilterValue>> {
val referenceData = referenceData as GEReferenceData
val plugs = referenceData.plugs
val networks = referenceData.networks
val chargeCards = referenceData.chargecards

View File

@@ -54,28 +54,26 @@ data class GEChargeLocation(
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()
)
}
override fun convert(apikey: String) = 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)

View File

@@ -1,9 +1,15 @@
package net.vonforst.evmap.api.openchargemap
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 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.viewmodel.Resource
import okhttp3.Cache
import okhttp3.OkHttpClient
import retrofit2.Response
@@ -28,10 +34,8 @@ interface OpenChargeMapApi {
@Query("compact") compact: Boolean = false
): Response<List<OCMChargepoint>>
/*
@GET("referencedata/")
suspend fun getReferenceData(): Response<OCMReferenceData>
*/
companion object {
private val cacheSize = 10L * 1024 * 1024 // 10MB
@@ -42,7 +46,7 @@ interface OpenChargeMapApi {
fun create(
apikey: String,
baseurl: String = "https://api.openchargemap.io/v3",
baseurl: String = "https://api.openchargemap.io/v3/",
context: Context? = null
): OpenChargeMapApi {
val client = OkHttpClient.Builder().apply {
@@ -71,3 +75,73 @@ interface OpenChargeMapApi {
}
}
}
class OpenChargeMapApiWrapper(
apikey: String,
baseurl: String = "https://api.openchargemap.io/v3/",
context: Context? = null
) : ChargepointApi<OCMReferenceData> {
val api = OpenChargeMapApi.create(apikey, baseurl, context)
override suspend fun getChargepoints(
referenceData: ReferenceData,
bounds: LatLngBounds,
zoom: Float,
filters: FilterValues,
): Resource<List<ChargepointListItem>> {
val referenceData = referenceData as OCMReferenceData
val response = api.getChargepoints(
OCMBoundingBox(
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude
)
)
if (!response.isSuccessful) {
return Resource.error(response.message(), null)
}
val result = response.body()!!.map { it.convert(referenceData) }
return Resource.success(result)
}
override suspend fun getChargepointsRadius(
referenceData: ReferenceData,
location: LatLng,
radius: Int,
zoom: Float,
filters: FilterValues
): Resource<List<ChargepointListItem>> {
TODO("Not yet implemented")
}
override suspend fun getChargepointDetail(
referenceData: ReferenceData,
id: Long
): Resource<ChargeLocation> {
val referenceData = referenceData as OCMReferenceData
val response = api.getChargepointDetail(id)
if (response.isSuccessful) {
return Resource.success(response.body()!![0].convert(referenceData))
} else {
return Resource.error(response.message(), null)
}
}
override suspend fun getReferenceData(): Resource<OCMReferenceData> {
val response = api.getReferenceData()
if (response.isSuccessful) {
return Resource.success(response.body()!!)
} else {
return Resource.error(response.message(), null)
}
}
override fun getFilters(
referenceData: ReferenceData,
sp: StringProvider
): List<Filter<FilterValue>> {
val referenceData = referenceData as OCMReferenceData
return emptyList()
}
}

View File

@@ -2,6 +2,7 @@ package net.vonforst.evmap.api.openchargemap
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import net.vonforst.evmap.model.*
import java.time.ZonedDateTime
data class OCMBoundingBox(
@@ -23,7 +24,28 @@ data class OCMChargepoint(
@Json(name = "Connections") val connections: List<OCMConnection>,
@Json(name = "NumberOfPoints") val numPoints: Int,
@Json(name = "GeneralComments") val generalComments: String?
)
) {
fun convert(refData: OCMReferenceData) = ChargeLocation(
id,
addressInfo.title,
Coordinate(addressInfo.latitude, addressInfo.longitude),
addressInfo.toAddress(refData),
connections.map { it.convert(refData) },
null,
"https://openchargemap.org/site/poi/details/$id",
null,
recentlyVerified,
null,
null, //TODO: OperatorInfo
generalComments,
null,
addressInfo.accessComments,
null, // TODO: MediaItems,
null,
null,
Cost(descriptionLong = cost)
)
}
@JsonClass(generateAdapter = true)
data class OCMAddressInfo(
@@ -41,14 +63,50 @@ data class OCMAddressInfo(
@Json(name = "ContactEmail") val contactEmail: String?,
@Json(name = "AccessComments") val accessComments: String?,
@Json(name = "RelatedURL") val relatedUrl: String?
)
) {
fun toAddress(refData: OCMReferenceData) = Address(
town,
refData.countries.find { it.id == countryId }!!.title,
postcode,
listOfNotNull(addressLine1, addressLine2).joinToString(", ")
)
}
@JsonClass(generateAdapter = true)
data class OCMConnection(
@Json(name = "ConnectionTypeID") val connectionTypeId: Long,
@Json(name = "Amps") val amps: Int,
@Json(name = "Voltage") val voltage: Int,
@Json(name = "Amps") val amps: Int?,
@Json(name = "Voltage") val voltage: Int?,
@Json(name = "PowerKW") val power: Double,
@Json(name = "Quantity") val quantity: Int,
@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,
quantity ?: 0
)
}
@JsonClass(generateAdapter = true)
data class OCMReferenceData(
@Json(name = "ConnectionTypes") val connectionTypes: List<OCMConnectionType>,
@Json(name = "Countries") val countries: List<OCMCountry>
) : ReferenceData()
@JsonClass(generateAdapter = true)
data class OCMConnectionType(
@Json(name = "ID") val id: Long,
@Json(name = "Title") val title: String,
@Json(name = "FormalName") val formalName: String?,
@Json(name = "IsDiscontinued") val discontinued: Boolean?,
@Json(name = "IsObsolete") val obsolete: Boolean?
)
@JsonClass(generateAdapter = true)
data class OCMCountry(
@Json(name = "ID") val id: Long,
@Json(name = "ISOCode") val isoCode: String,
@Json(name = "ContinentCode") val continentCode: String?,
@Json(name = "Title") val title: String
)

View File

@@ -101,20 +101,28 @@ data class ChargeLocation(
}
data class Cost(
val freecharging: Boolean,
val freeparking: Boolean,
val descriptionShort: String?,
val descriptionLong: String?
val freecharging: Boolean? = null,
val freeparking: Boolean? = null,
val descriptionShort: String? = null,
val descriptionLong: String? = null
) {
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"
if (freecharging != null && freeparking != null) {
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)
}
} else if (descriptionShort != null) {
return descriptionShort
} else if (descriptionLong != null) {
return descriptionLong
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail, charging, parking), 0)
return ""
}
}
}

View File

@@ -1,4 +1,3 @@
package net.vonforst.evmap.model
open class ReferenceData {
}
abstract class ReferenceData

View File

@@ -6,10 +6,13 @@ import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
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.goingelectric.GEReferenceData
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
@@ -34,7 +37,13 @@ internal fun getClusterDistance(zoom: Float): Int? {
}
class MapViewModel(application: Application, geApiKey: String) : AndroidViewModel(application) {
private var api = GoingElectricApiWrapper(geApiKey, context = application)
private var api: ChargepointApi<ReferenceData> = OpenChargeMapApiWrapper(
application.getString(
R.string.openchargemap_key
)
)
// = GoingElectricApiWrapper(geApiKey, context = application)
private var db = AppDatabase.getInstance(application)
private var prefs = PreferenceDataSource(application)
@@ -58,16 +67,30 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
private val referenceData: LiveData<out ReferenceData> by lazy {
GEReferenceDataRepository(
api,
viewModelScope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
val api = api
if (api is GoingElectricApiWrapper) {
GEReferenceDataRepository(
api,
viewModelScope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
} else {
// TODO: create repository
MutableLiveData<ReferenceData>().apply {
viewModelScope.launch {
val referenceData1 = api.getReferenceData()
if (referenceData1.status == Status.SUCCESS) {
value = referenceData1.data
}
}
}
}
}
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { data ->
value = api.getFilters(data as GEReferenceData, application.stringProvider())
val api = api
value = api.getFilters(data, application.stringProvider())
}
}
@@ -127,11 +150,15 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
addSource(chargerSparse) { charger ->
if (charger != null) {
loadChargerDetails(charger)
} else {
value = null
listOf(chargerSparse, referenceData).forEach {
addSource(it) { _ ->
val charger = chargerSparse.value
val refData = referenceData.value
if (charger != null && refData != null) {
loadChargerDetails(charger, refData)
} else {
value = null
}
}
}
}
@@ -283,49 +310,51 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
fun reloadChargepoints() {
val pos = mapPosition.value ?: return
val filters = filtersWithValue.value ?: return
loadChargepoints(pos, filters)
val referenceData = referenceData.value ?: return
chargepointLoader(Triple(pos, filters, referenceData))
}
private var chargepointLoader =
throttleLatest(500L, viewModelScope) { data: Pair<MapPosition, FilterValues> ->
throttleLatest(
500L,
viewModelScope
) { data: Triple<MapPosition, FilterValues, ReferenceData> ->
chargepoints.value = Resource.loading(chargepoints.value?.data)
filteredConnectors.value = null
filteredChargeCards.value = null
val mapPosition = data.first
val filters = data.second
var result = api.getChargepoints(mapPosition.bounds, mapPosition.zoom, filters)
val api = api
val refData = data.third
var result = api.getChargepoints(refData, 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()
if (api is GoingElectricApiWrapper) {
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
val connectorsVal = filters.getMultipleChoiceValue("connectors")
filteredConnectors.value = if (connectorsVal.all) null else connectorsVal.values
}
}
private fun loadChargepoints(
mapPosition: MapPosition,
filters: FilterValues
) {
chargepointLoader(Pair(mapPosition, filters))
}
private suspend fun loadAvailability(charger: ChargeLocation) {
availability.value = Resource.loading(null)
availability.value = getAvailability(charger)
}
private fun loadChargerDetails(charger: ChargeLocation) {
private fun loadChargerDetails(charger: ChargeLocation, referenceData: ReferenceData) {
chargerDetails.value = Resource.loading(null)
viewModelScope.launch {
try {
chargerDetails.value = api.getChargepointDetail(charger.id)
chargerDetails.value = api.getChargepointDetail(referenceData, charger.id)
} catch (e: IOException) {
chargerDetails.value = Resource.error(e.message, null)
e.printStackTrace()
@@ -336,14 +365,19 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
fun loadChargerById(chargerId: Long) {
chargerDetails.value = Resource.loading(null)
chargerSparse.value = null
viewModelScope.launch {
val response = api.getChargepointDetail(chargerId)
chargerDetails.value = response
if (response.status == Status.SUCCESS) {
chargerSparse.value = response.data
} else {
chargerSparse.value = null
referenceData.observeForever(object : Observer<ReferenceData> {
override fun onChanged(refData: ReferenceData) {
referenceData.removeObserver(this)
viewModelScope.launch {
val response = api.getChargepointDetail(refData, chargerId)
chargerDetails.value = response
if (response.status == Status.SUCCESS) {
chargerSparse.value = response.data
} else {
chargerSparse.value = null
}
}
}
}
})
}
}