From d5168f12c625f06e99a17cb810022ed1f881d989 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 6 Jun 2021 21:59:00 +0200 Subject: [PATCH] continue working on OCM API, proof of concept works --- app/build.gradle | 4 +- .../net/vonforst/evmap/api/ChargepointApi.kt | 11 +- .../api/goingelectric/GoingElectricApi.kt | 10 +- .../api/goingelectric/GoingElectricModel.kt | 42 ++++--- .../api/openchargemap/OpenChargeMapApi.kt | 80 ++++++++++++- .../api/openchargemap/OpenChargeMapModel.kt | 68 ++++++++++- .../net/vonforst/evmap/model/ChargersModel.kt | 30 +++-- .../evmap/model/ReferenceDataModel.kt | 3 +- .../vonforst/evmap/viewmodel/MapViewModel.kt | 110 ++++++++++++------ build.gradle | 2 +- 10 files changed, 271 insertions(+), 89 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c9c6c535..3b1dd4ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,7 +113,7 @@ dependencies { implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0' - implementation 'com.squareup.moshi:moshi-kotlin:1.9.2' + implementation 'com.squareup.moshi:moshi-kotlin:1.12.0' implementation 'moe.banana:moshi-jsonapi:3.5.0' implementation 'moe.banana:moshi-jsonapi-retrofit-converter:3.5.0' implementation 'io.coil-kt:coil:1.1.0' @@ -186,7 +186,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' - kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2" + kapt "com.squareup.moshi:moshi-kotlin-codegen:1.12.0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' } diff --git a/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt b/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt index 9c420f27..45fca565 100644 --- a/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt @@ -6,25 +6,30 @@ import com.car2go.maps.model.LatLngBounds import net.vonforst.evmap.model.* import net.vonforst.evmap.viewmodel.Resource -interface ChargepointApi { +interface ChargepointApi { suspend fun getChargepoints( + referenceData: ReferenceData, bounds: LatLngBounds, zoom: Float, filters: FilterValues ): Resource> suspend fun getChargepointsRadius( + referenceData: ReferenceData, location: LatLng, radius: Int, zoom: Float, filters: FilterValues ): Resource> - suspend fun getChargepointDetail(id: Long): Resource + suspend fun getChargepointDetail( + referenceData: ReferenceData, + id: Long + ): Resource suspend fun getReferenceData(): Resource - fun getFilters(referenceData: T, sp: StringProvider): List> + fun getFilters(referenceData: ReferenceData, sp: StringProvider): List> } interface StringProvider { 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 af064869..84e807c6 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 @@ -129,6 +129,7 @@ class GoingElectricApiWrapper( ) : ChargepointApi { 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 { + override suspend fun getChargepointDetail( + referenceData: ReferenceData, + id: Long + ): Resource { 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> { + val referenceData = referenceData as GEReferenceData val plugs = referenceData.plugs val networks = referenceData.networks val chargeCards = referenceData.chargecards 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 c0789cac..37825dff 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 @@ -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) diff --git a/app/src/main/java/net/vonforst/evmap/api/openchargemap/OpenChargeMapApi.kt b/app/src/main/java/net/vonforst/evmap/api/openchargemap/OpenChargeMapApi.kt index 65f36f84..eb4a97b8 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openchargemap/OpenChargeMapApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openchargemap/OpenChargeMapApi.kt @@ -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> - /* @GET("referencedata/") suspend fun getReferenceData(): Response - */ 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 { + val api = OpenChargeMapApi.create(apikey, baseurl, context) + + override suspend fun getChargepoints( + referenceData: ReferenceData, + bounds: LatLngBounds, + zoom: Float, + filters: FilterValues, + ): Resource> { + 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> { + TODO("Not yet implemented") + } + + override suspend fun getChargepointDetail( + referenceData: ReferenceData, + id: Long + ): Resource { + 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 { + 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> { + val referenceData = referenceData as OCMReferenceData + return emptyList() + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/openchargemap/OpenChargeMapModel.kt b/app/src/main/java/net/vonforst/evmap/api/openchargemap/OpenChargeMapModel.kt index c6949a66..12f7fa46 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openchargemap/OpenChargeMapModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openchargemap/OpenChargeMapModel.kt @@ -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, @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, + @Json(name = "Countries") val countries: List +) : 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 ) \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt index ee09cc6f..206656f1 100644 --- a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt +++ b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt @@ -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 "" } } } diff --git a/app/src/main/java/net/vonforst/evmap/model/ReferenceDataModel.kt b/app/src/main/java/net/vonforst/evmap/model/ReferenceDataModel.kt index 1e6cffb7..c1a2f41f 100644 --- a/app/src/main/java/net/vonforst/evmap/model/ReferenceDataModel.kt +++ b/app/src/main/java/net/vonforst/evmap/model/ReferenceDataModel.kt @@ -1,4 +1,3 @@ package net.vonforst.evmap.model -open class ReferenceData { -} \ No newline at end of file +abstract class ReferenceData \ 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 28edff35..fff8404e 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt @@ -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 = 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 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().apply { + viewModelScope.launch { + val referenceData1 = api.getReferenceData() + if (referenceData1.status == Status.SUCCESS) { + value = referenceData1.data + } + } + } + } } private val filters = MediatorLiveData>>().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> by lazy { MediatorLiveData>().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 -> + throttleLatest( + 500L, + viewModelScope + ) { data: Triple -> 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 { + 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 + } + } } - } + }) } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8723f513..176a16e4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.32' + ext.kotlin_version = '1.5.10' ext.about_libs_version = '8.8.5' ext.nav_version = '2.3.5' repositories {