From 093e1d116fc1c0fb3fab319eb8f70d695718a402 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 2 Jul 2023 20:41:54 +0200 Subject: [PATCH 01/27] basic implementation of OpenStreetMapApi #97 --- .../net/vonforst/evmap/api/ChargepointApi.kt | 35 ++++ .../api/goingelectric/GoingElectricApi.kt | 6 + .../api/openchargemap/OpenChargeMapApi.kt | 6 + .../openstreetmap/OpenStreetMapAdapters.kt | 20 ++ .../api/openstreetmap/OpenStreetMapApi.kt | 128 +++++++++++++ .../api/openstreetmap/OpenStreetMapModel.kt | 6 + .../evmap/fragment/DataSourceSelectDialog.kt | 3 + .../evmap/fragment/OnboardingFragment.kt | 3 + .../evmap/storage/ChargeLocationsDao.kt | 173 ++++++++++++------ .../main/res/layout/data_source_select.xml | 18 ++ app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/arrays.xml | 2 + app/src/main/res/values/strings.xml | 2 + 13 files changed, 349 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt create mode 100644 app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt 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 37c252aa0..da14af40e 100644 --- a/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt @@ -6,6 +6,7 @@ import com.car2go.maps.model.LatLngBounds import net.vonforst.evmap.R import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper +import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper import net.vonforst.evmap.model.* import net.vonforst.evmap.viewmodel.Resource import java.time.Duration @@ -58,6 +59,34 @@ interface ChargepointApi { * Duration we are limited to if there is a required API local cache time limit. */ val cacheLimit: Duration + + /** + * Whether this API supports querying for chargers at the backend + * + * This determines whether the getChargepoints, getChargepointsRadius and getChargepointDetail functions are supported. + */ + val supportsOnlineQueries: Boolean + + /** + * Whether this API supports downloading the whole dataset into local storage + * + * This determines whether the getAllChargepoints function is supported. + */ + val supportsFullDownload: Boolean + + /** + * Fetches all available chargers from this API. + * + * This may take a long time and should only be used when the user explicitly wants to download all chargers. + * + * TODO: add an optional callback parameter to this function to be able to receive updates on the download progress? + * TODO: Should this also include getting the ReferenceData, instead of taking it as an argument? + * ReferenceData typically includes information that is needed to create the filter options, e.g. + * mappings between IDs and readable names (for operators etc.). So probably for OSM it makes sense + * to generate that within this function (e.g. build the list of available operators using all the + * operators found in the dataset). + */ + suspend fun fullDownload(referenceData: ReferenceData): List } interface StringProvider { @@ -79,6 +108,7 @@ fun createApi(type: String, ctx: Context): ChargepointApi { ) ) } + "goingelectric" -> { GoingElectricApiWrapper( ctx.getString( @@ -86,6 +116,11 @@ fun createApi(type: String, ctx: Context): ChargepointApi { ) ) } + + "openstreetmap" -> { + OpenStreetMapApiWrapper() + } + else -> throw IllegalArgumentException() } } 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 d4ae126a3..a53f34964 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 @@ -159,6 +159,12 @@ class GoingElectricApiWrapper( override val name = "GoingElectric.de" override val id = "goingelectric" override val cacheLimit = Duration.ofDays(1) + override val supportsOnlineQueries = true + override val supportsFullDownload = false + + override suspend fun fullDownload(referenceData: ReferenceData): List { + throw NotImplementedError() + } override suspend fun getChargepoints( referenceData: ReferenceData, 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 ac10ab9a4..1a9530ee7 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 @@ -130,6 +130,12 @@ class OpenChargeMapApiWrapper( override val name = "OpenChargeMap.org" override val id = "openchargemap" + override val supportsOnlineQueries = true + override val supportsFullDownload = false + + override suspend fun fullDownload(referenceData: ReferenceData): List { + throw NotImplementedError() + } private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) = if (value == null || value.all) null else value.values.joinToString(",") diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt new file mode 100644 index 000000000..1a7b07f6d --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt @@ -0,0 +1,20 @@ +package net.vonforst.evmap.api.openstreetmap + +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson +import java.time.Instant +import kotlin.math.floor + +internal class InstantAdapter { + @FromJson + fun fromJson(value: Double?): Instant? = value?.let { + val seconds = floor(it).toLong() + val nanos = ((value - seconds) * 1e9).toLong() + Instant.ofEpochSecond(seconds, nanos) + } + + @ToJson + fun toJson(value: Instant?): Double? = value?.let { + it.epochSecond.toDouble() + it.nano / 1e9 + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt new file mode 100644 index 000000000..81b83b467 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt @@ -0,0 +1,128 @@ +package net.vonforst.evmap.api.openstreetmap + +import com.car2go.maps.model.LatLng +import com.car2go.maps.model.LatLngBounds +import com.squareup.moshi.Moshi +import net.vonforst.evmap.BuildConfig +import net.vonforst.evmap.addDebugInterceptors +import net.vonforst.evmap.api.ChargepointApi +import net.vonforst.evmap.api.ChargepointList +import net.vonforst.evmap.api.FiltersSQLQuery +import net.vonforst.evmap.api.StringProvider +import net.vonforst.evmap.api.openchargemap.ZonedDateTimeAdapter +import net.vonforst.evmap.model.ChargeLocation +import net.vonforst.evmap.model.Filter +import net.vonforst.evmap.model.FilterValue +import net.vonforst.evmap.model.FilterValues +import net.vonforst.evmap.model.ReferenceData +import net.vonforst.evmap.viewmodel.Resource +import okhttp3.OkHttpClient +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.http.GET +import java.io.IOException +import java.time.Duration +import java.time.Instant + +interface OpenStreetMapApi { + @GET("charging-stations-osm.json") + suspend fun getAllChargingStations(): Response + + companion object { + private val moshi = Moshi.Builder() + .add(ZonedDateTimeAdapter()) + .add(InstantAdapter()) + .build() + + fun create( + baseurl: String = "https://evmap-dev.vonforst.net" + ): OpenStreetMapApi { + val client = OkHttpClient.Builder().apply { + if (BuildConfig.DEBUG) addDebugInterceptors() + }.build() + + val retrofit = Retrofit.Builder() + .baseUrl(baseurl) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .client(client) + .build() + return retrofit.create(OpenStreetMapApi::class.java) + } + } + +} + +class OpenStreetMapApiWrapper(baseurl: String = "https://evmap-dev.vonforst.net") : + ChargepointApi { + override val name = "OpenStreetMap" + override val id = "openstreetmap" + override val cacheLimit = Duration.ofDays(300L) + override val supportsOnlineQueries = false + override val supportsFullDownload = true + + val api = OpenStreetMapApi.create(baseurl) + + override suspend fun getChargepoints( + referenceData: ReferenceData, + bounds: LatLngBounds, + zoom: Float, + useClustering: Boolean, + filters: FilterValues? + ): Resource { + throw NotImplementedError() + } + + override suspend fun getChargepointsRadius( + referenceData: ReferenceData, + location: LatLng, + radius: Int, + zoom: Float, + useClustering: Boolean, + filters: FilterValues? + ): Resource { + throw NotImplementedError() + } + + override suspend fun getChargepointDetail( + referenceData: ReferenceData, + id: Long + ): Resource { + throw NotImplementedError() + } + + override suspend fun getReferenceData(): Resource { + TODO("Not yet implemented") + } + + override fun getFilters( + referenceData: ReferenceData, + sp: StringProvider + ): List> { + return emptyList() + } + + override fun convertFiltersToSQL( + filters: FilterValues, + referenceData: ReferenceData + ): FiltersSQLQuery { + TODO("Not yet implemented") + } + + override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean { + return true + } + + override suspend fun fullDownload(referenceData: ReferenceData): List { + val response = api.getAllChargingStations() + if (!response.isSuccessful) { + throw IOException(response.message()) + } else { + val body = response.body()!! + val time = body.timestamp + return body.elements.map { it.convert(time) } + } + } +} + +data class OSMReferenceData(val test: String) : ReferenceData() diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt index 2fc4c5cf8..75d744378 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt @@ -58,6 +58,12 @@ private val SOCKET_TYPES = immutableListOf( OsmSocket("sev1011_t25", null), ) +@JsonClass(generateAdapter = true) +data class OSMDocument( + val timestamp: Instant, + val elements: List +) + @JsonClass(generateAdapter = true) data class OSMChargingStation( // Unique numeric ID diff --git a/app/src/main/java/net/vonforst/evmap/fragment/DataSourceSelectDialog.kt b/app/src/main/java/net/vonforst/evmap/fragment/DataSourceSelectDialog.kt index 71eada813..6d3808f17 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/DataSourceSelectDialog.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/DataSourceSelectDialog.kt @@ -54,6 +54,7 @@ class DataSourceSelectDialog : MaterialDialogFragment() { when (prefs.dataSource) { "goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true "openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true + "openstreetmap" -> binding.rgDataSource.rbOpenStreetMap.isChecked = true } } @@ -65,6 +66,8 @@ class DataSourceSelectDialog : MaterialDialogFragment() { "goingelectric" } else if (binding.rgDataSource.rbOpenChargeMap.isChecked) { "openchargemap" + } else if (binding.rgDataSource.rbOpenStreetMap.isChecked) { + "openstreetmap" } else { return@setOnClickListener } diff --git a/app/src/main/java/net/vonforst/evmap/fragment/OnboardingFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/OnboardingFragment.kt index c05a50b7a..68c93588c 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/OnboardingFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/OnboardingFragment.kt @@ -254,6 +254,7 @@ class DataSourceSelectFragment : OnboardingPageFragment() { when (prefs.dataSource) { "goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true "openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true + "openstreetmap" -> binding.rgDataSource.rbOpenStreetMap.isChecked = true } } @@ -272,6 +273,8 @@ class DataSourceSelectFragment : OnboardingPageFragment() { "goingelectric" } else if (binding.rgDataSource.rbOpenChargeMap.isChecked) { "openchargemap" + } else if (binding.rgDataSource.rbOpenStreetMap.isChecked) { + "openstreetmap" } else { return@setOnClickListener } 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 d0177e2ab..e1ab4e358 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -16,7 +16,9 @@ import net.vonforst.evmap.api.ChargepointList import net.vonforst.evmap.api.StringProvider import net.vonforst.evmap.api.goingelectric.GEReferenceData import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper +import net.vonforst.evmap.api.openchargemap.OCMReferenceData import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper +import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper import net.vonforst.evmap.model.* import net.vonforst.evmap.ui.cluster import net.vonforst.evmap.viewmodel.Resource @@ -122,6 +124,7 @@ class ChargeLocationsRepository( prefs ).getReferenceData() } + is OpenChargeMapApiWrapper -> { OCMReferenceDataRepository( api, @@ -130,6 +133,19 @@ class ChargeLocationsRepository( prefs ).getReferenceData() } + + is OpenStreetMapApiWrapper -> { + liveData { + emit( + OCMReferenceData( + emptyList(), + emptyList(), + emptyList() + ) + ) + } // TODO: add OSM reference data + } + else -> { throw RuntimeException("no reference data implemented") } @@ -146,8 +162,7 @@ class ChargeLocationsRepository( overrideCache: Boolean = false ): LiveData>> { val api = api.value!! - - val dbResult = if (filters == null) { + val dbResult = if (filters.isNullOrEmpty()) { chargeLocationsDao.getChargeLocationsInBounds( bounds.southwest.latitude, bounds.northeast.latitude, @@ -174,37 +189,46 @@ class ChargeLocationsRepository( requiresDetail ) val useClustering = shouldUseServerSideClustering(zoom) - val apiResult = liveData { - val refData = referenceData.await() - val time = Instant.now() - val result = api.getChargepoints(refData, bounds, zoom, useClustering, filters) - emit(applyLocalClustering(result, zoom)) - if (result.status == Status.SUCCESS) { - val chargers = result.data!!.items.filterIsInstance() - chargeLocationsDao.insertOrReplaceIfNoDetailedExists( - cacheLimitDate(api), *chargers.toTypedArray() - ) - if (chargers.size == result.data.items.size && result.data.isComplete) { - val region = Mbr( - bounds.southwest.longitude, - bounds.southwest.latitude, - bounds.northeast.longitude, - bounds.northeast.latitude, 4326 - ).asPolygon() - savedRegionDao.insert( - SavedRegion( - region, api.id, time, - filtersSerialized, - false - ) + if (api.supportsOnlineQueries) { + val apiResult = liveData { + val refData = referenceData.await() + val time = Instant.now() + val result = api.getChargepoints(refData, bounds, zoom, useClustering, filters) + emit(applyLocalClustering(result, zoom)) + if (result.status == Status.SUCCESS) { + val chargers = result.data!!.items.filterIsInstance() + chargeLocationsDao.insertOrReplaceIfNoDetailedExists( + cacheLimitDate(api), *chargers.toTypedArray() ) + if (chargers.size == result.data.items.size && result.data.isComplete) { + val region = Mbr( + bounds.southwest.longitude, + bounds.southwest.latitude, + bounds.northeast.longitude, + bounds.northeast.latitude, 4326 + ).asPolygon() + savedRegionDao.insert( + SavedRegion( + region, api.id, time, + filtersSerialized, + false + ) + ) + } } } - } - return if (overrideCache) { - apiResult + return if (overrideCache) { + apiResult + } else { + CacheLiveData(dbResult, apiResult, savedRegionResult).distinctUntilChanged() + } } else { - CacheLiveData(dbResult, apiResult, savedRegionResult).distinctUntilChanged() + return liveData { + if (!savedRegionResult.await()) { + fullDownload() + } + emit(Resource.success(dbResult.await())) + } } } @@ -217,7 +241,7 @@ class ChargeLocationsRepository( val api = api.value!! val radiusMeters = radius.toDouble() * 1000 - val dbResult = if (filters == null) { + val dbResult = if (filters.isNullOrEmpty()) { chargeLocationsDao.getChargeLocationsRadius( location.latitude, location.longitude, @@ -242,36 +266,52 @@ class ChargeLocationsRepository( requiresDetail ) val useClustering = shouldUseServerSideClustering(zoom) - val apiResult = liveData { - val refData = referenceData.await() - val time = Instant.now() - val result = - api.getChargepointsRadius(refData, location, radius, zoom, useClustering, filters) - emit(applyLocalClustering(result, zoom)) - if (result.status == Status.SUCCESS) { - val chargers = result.data!!.items.filterIsInstance() - chargeLocationsDao.insertOrReplaceIfNoDetailedExists( - cacheLimitDate(api), *chargers.toTypedArray() - ) - if (chargers.size == result.data.items.size && result.data.isComplete) { - val region = Polygon( - savedRegionDao.makeCircle( - location.latitude, - location.longitude, - radiusMeters - ) + if (api.supportsOnlineQueries) { + val apiResult = liveData { + val refData = referenceData.await() + val time = Instant.now() + val result = + api.getChargepointsRadius( + refData, + location, + radius, + zoom, + useClustering, + filters ) - savedRegionDao.insert( - SavedRegion( - region, api.id, time, - filtersSerialized, - false - ) + emit(applyLocalClustering(result, zoom)) + if (result.status == Status.SUCCESS) { + val chargers = result.data!!.items.filterIsInstance() + chargeLocationsDao.insertOrReplaceIfNoDetailedExists( + cacheLimitDate(api), *chargers.toTypedArray() ) + if (chargers.size == result.data.items.size && result.data.isComplete) { + val region = Polygon( + savedRegionDao.makeCircle( + location.latitude, + location.longitude, + radiusMeters + ) + ) + savedRegionDao.insert( + SavedRegion( + region, api.id, time, + filtersSerialized, + false + ) + ) + } } } + return CacheLiveData(dbResult, apiResult, savedRegionResult).distinctUntilChanged() + } else { + return liveData { + if (!savedRegionResult.await()) { + fullDownload() + } + emit(Resource.success(dbResult.await())) + } } - return CacheLiveData(dbResult, apiResult, savedRegionResult).distinctUntilChanged() } private fun applyLocalClustering( @@ -421,6 +461,29 @@ class ChargeLocationsRepository( } } + private suspend fun fullDownload() { + val api = api.value!! + if (!api.supportsFullDownload) return + + val refData = referenceData.await() + val time = Instant.now() + val result = api.fullDownload(refData) + chargeLocationsDao.insert(*result.toTypedArray()) + val region = Mbr( + -180.0, + -90.0, + 180.0, + 90.0, 4326 + ).asPolygon() + savedRegionDao.insert( + SavedRegion( + region, api.id, time, + null, + true + ) + ) + } + private fun cacheLimitDate(api: ChargepointApi): Long { val cacheLimit = api.cacheLimit diff --git a/app/src/main/res/layout/data_source_select.xml b/app/src/main/res/layout/data_source_select.xml index 67e4b9c5d..c719127cf 100644 --- a/app/src/main/res/layout/data_source_select.xml +++ b/app/src/main/res/layout/data_source_select.xml @@ -41,4 +41,22 @@ android:layout_marginStart="32dp" android:text="@string/data_source_openchargemap_desc" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 910fdc03a..e6b122184 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -223,8 +223,10 @@ Bitte wähle eine Datenquelle für Ladestationen aus. Du kannst sie später in den Einstellungen der App ändern. GoingElectric.de Open Charge Map + OpenStreetMap Sehr gute Abdeckung in den deutschsprachigen Ländern. Beschreibungen in Deutsch. Von der Community gepflegt. + Experimentelle Unterstützung in EVMap, nicht alle Funktionen nutzbar weiter Los geht\'s Alles klar diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 38f4fa1a2..7eff322c5 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -27,10 +27,12 @@ @string/data_source_goingelectric @string/data_source_openchargemap + @string/data_source_openstreetmap goingelectric openchargemap + openstreetmap @string/pref_units_default diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea40374ba..2a4cb83d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -222,9 +222,11 @@ Unknown operator Please pick a data source for charging stations. It can later be changed in the app settings. GoingElectric.de + OpenStreetMap Open Charge Map Great in the German-speaking countries. Descriptions in German. Community-maintained. + Experimental support in EVMap, not all features available next Get started Got it From 0685004cb903b912e00f66891aaa5f3c5b8d0250 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 9 Jul 2023 16:47:12 +0200 Subject: [PATCH 02/27] fix getChargepointDetail for offline APIs --- .../evmap/storage/ChargeLocationsDao.kt | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) 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 e1ab4e358..bbab1fdc6 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -353,24 +353,29 @@ class ChargeLocationsRepository( id: Long, overrideCache: Boolean = false ): LiveData> { + val api = api.value!! val dbResult = chargeLocationsDao.getChargeLocationById( id, prefs.dataSource, - cacheLimitDate(api.value!!) + cacheLimitDate(api) ) - val apiResult = liveData { - emit(Resource.loading(null)) - val refData = referenceData.await() - val result = api.value!!.getChargepointDetail(refData, id) - emit(result) - if (result.status == Status.SUCCESS) { - chargeLocationsDao.insert(result.data!!) + if (api.supportsOnlineQueries) { + val apiResult = liveData { + emit(Resource.loading(null)) + val refData = referenceData.await() + val result = api.getChargepointDetail(refData, id) + emit(result) + if (result.status == Status.SUCCESS) { + chargeLocationsDao.insert(result.data!!) + } + } + return if (overrideCache) { + apiResult + } else { + PreferCacheLiveData(dbResult, apiResult, cacheSoftLimit) } - } - return if (overrideCache) { - apiResult } else { - PreferCacheLiveData(dbResult, apiResult, cacheSoftLimit) + return dbResult.map { Resource.success(it) } } } From 3a5bdd10824eaa3e163405b9149e22d58b4f9af0 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 9 Jul 2023 16:53:25 +0200 Subject: [PATCH 03/27] OpenStreetMap API: implement some first filters --- .../api/openstreetmap/OpenStreetMapApi.kt | 104 +++++++++++++++++- .../net/vonforst/evmap/storage/Database.kt | 12 +- 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt index 81b83b467..f046c01b3 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt @@ -1,20 +1,34 @@ package net.vonforst.evmap.api.openstreetmap +import android.database.DatabaseUtils import com.car2go.maps.model.LatLng import com.car2go.maps.model.LatLngBounds import com.squareup.moshi.Moshi import net.vonforst.evmap.BuildConfig +import net.vonforst.evmap.R import net.vonforst.evmap.addDebugInterceptors import net.vonforst.evmap.api.ChargepointApi import net.vonforst.evmap.api.ChargepointList import net.vonforst.evmap.api.FiltersSQLQuery import net.vonforst.evmap.api.StringProvider +import net.vonforst.evmap.api.goingelectric.GEChargepoint +import net.vonforst.evmap.api.mapPower +import net.vonforst.evmap.api.mapPowerInverse +import net.vonforst.evmap.api.nameForPlugType import net.vonforst.evmap.api.openchargemap.ZonedDateTimeAdapter +import net.vonforst.evmap.api.powerSteps +import net.vonforst.evmap.model.BooleanFilter import net.vonforst.evmap.model.ChargeLocation +import net.vonforst.evmap.model.Chargepoint import net.vonforst.evmap.model.Filter import net.vonforst.evmap.model.FilterValue import net.vonforst.evmap.model.FilterValues +import net.vonforst.evmap.model.MultipleChoiceFilter import net.vonforst.evmap.model.ReferenceData +import net.vonforst.evmap.model.SliderFilter +import net.vonforst.evmap.model.getBooleanValue +import net.vonforst.evmap.model.getMultipleChoiceValue +import net.vonforst.evmap.model.getSliderValue import net.vonforst.evmap.viewmodel.Resource import okhttp3.OkHttpClient import retrofit2.Response @@ -99,14 +113,100 @@ class OpenStreetMapApiWrapper(baseurl: String = "https://evmap-dev.vonforst.net" referenceData: ReferenceData, sp: StringProvider ): List> { - return emptyList() + + val plugs = listOf( + Chargepoint.TYPE_1, + Chargepoint.CCS_TYPE_1, + Chargepoint.TYPE_2_SOCKET, + Chargepoint.TYPE_2_PLUG, + Chargepoint.CCS_TYPE_2, + Chargepoint.CHADEMO, + Chargepoint.SUPERCHARGER, + Chargepoint.CEE_BLAU, + Chargepoint.CEE_ROT, + Chargepoint.SCHUKO + ) + val plugMap = plugs.associateWith { plug -> + nameForPlugType(sp, plug) + } + + 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_1, + Chargepoint.TYPE_2_SOCKET, + Chargepoint.TYPE_2_PLUG, + Chargepoint.CCS_TYPE_1, + Chargepoint.CCS_TYPE_2, + Chargepoint.CHADEMO + ), + manyChoices = true + ), + SliderFilter( + sp.getString(R.string.filter_min_connectors), + "min_connectors", + 10, + min = 1 + ) + ) } override fun convertFiltersToSQL( filters: FilterValues, referenceData: ReferenceData ): FiltersSQLQuery { - TODO("Not yet implemented") + if (filters.isEmpty()) return FiltersSQLQuery("", false, false) + var requiresChargepointQuery = false + + val result = StringBuilder() + if (filters.getBooleanValue("freecharging") == true) { + result.append(" AND freecharging IS 1") + } + if (filters.getBooleanValue("freeparking") == true) { + result.append(" AND freeparking IS 1") + } + if (filters.getBooleanValue("open_247") == true) { + result.append(" AND twentyfourSeven IS 1") + } + + val minPower = filters.getSliderValue("min_power") + if (minPower != null && minPower > 0) { + result.append(" AND json_extract(cp.value, '$.power') >= ${minPower}") + requiresChargepointQuery = true + } + + val connectors = filters.getMultipleChoiceValue("connectors") + if (connectors != null && !connectors.all) { + val connectorsList = if (connectors.values.size == 0) { + "" + } else { + connectors.values.joinToString(",") { + DatabaseUtils.sqlEscapeString(it) + } + } + result.append(" AND json_extract(cp.value, '$.type') IN (${connectorsList})") + requiresChargepointQuery = true + } + + val minConnectors = filters.getSliderValue("min_connectors") + if (minConnectors != null && minConnectors > 1) { + result.append(" GROUP BY ChargeLocation.id HAVING COUNT(1) >= $minConnectors") + requiresChargepointQuery = true + } + + return FiltersSQLQuery(result.toString(), requiresChargepointQuery, false) } override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean { 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 5b855c2e9..988f6faa1 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/Database.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/Database.kt @@ -34,7 +34,7 @@ import net.vonforst.evmap.model.* OCMCountry::class, OCMOperator::class, SavedRegion::class - ], version = 22 + ], version = 23 ) @TypeConverters(Converters::class, GeometryConverters::class) abstract class AppDatabase : RoomDatabase() { @@ -75,13 +75,14 @@ abstract class AppDatabase : RoomDatabase() { MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11, MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16, MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21, - MIGRATION_22 + MIGRATION_22, MIGRATION_23 ) .addCallback(object : Callback() { override fun onCreate(db: SupportSQLiteDatabase) { // create default filter profile for each data source db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('goingelectric', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)") db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openchargemap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)") + db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openstreetmap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)") // initialize spatialite columns db.query("SELECT RecoverGeometryColumn('ChargeLocation', 'coordinates', 4326, 'POINT', 'XY');") .moveToNext() @@ -459,6 +460,13 @@ abstract class AppDatabase : RoomDatabase() { db.execSQL("DELETE FROM savedregion") } } + + private val MIGRATION_23 = object : Migration(22, 23) { + override fun migrate(db: SupportSQLiteDatabase) { + // API openstreetmap added + db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openstreetmap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)") + } + } } /** From 3d97ae610e4d832935aa6b8bbc20d357fbcacabb Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 9 Jul 2023 17:10:36 +0200 Subject: [PATCH 04/27] improve formatting when address or connectors are missing --- .../main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt | 1 + app/src/main/res/layout/detail_view.xml | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) 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 31f79f813..94451fe2b 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt @@ -14,6 +14,7 @@ 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.Coordinate import net.vonforst.evmap.model.OpeningHoursDays import net.vonforst.evmap.plus import net.vonforst.evmap.ui.currency diff --git a/app/src/main/res/layout/detail_view.xml b/app/src/main/res/layout/detail_view.xml index 15f8f6aef..6b042f096 100644 --- a/app/src/main/res/layout/detail_view.xml +++ b/app/src/main/res/layout/detail_view.xml @@ -21,6 +21,8 @@ + + @@ -120,9 +122,9 @@ android:layout_height="wrap_content" android:ellipsize="end" android:maxLines="1" - android:text="@{charger.data.address.toString()}" + android:text="@{charger.data != null ? (charger.data.address != null ? charger.data.address.toString() : LocationUtilsKt.formatDMS(charger.data.coordinates)) : null }" android:textAppearance="@style/TextAppearance.Material3.BodySmall" - app:invisibleUnless="@{charger.data.address != null}" + app:invisibleUnless="@{charger.data != null}" app:layout_constraintEnd_toStartOf="@+id/guideline2" app:layout_constraintStart_toStartOf="@+id/guideline" app:layout_constraintTop_toBottomOf="@+id/txtName" From 5d6840a7710b528984329b68497f95d2063abe2c Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 9 Jul 2023 17:25:26 +0200 Subject: [PATCH 05/27] DB: make getChargeLocationById nullable --- app/src/main/java/net/vonforst/evmap/storage/CacheLiveData.kt | 2 +- .../main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/vonforst/evmap/storage/CacheLiveData.kt b/app/src/main/java/net/vonforst/evmap/storage/CacheLiveData.kt index c352c9358..338395ca3 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/CacheLiveData.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/CacheLiveData.kt @@ -95,7 +95,7 @@ class CacheLiveData( * reload from the API. */ class PreferCacheLiveData( - cache: LiveData, + cache: LiveData, val api: LiveData>, cacheSoftLimit: Duration ) : 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 bbab1fdc6..642630299 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -63,7 +63,7 @@ abstract class ChargeLocationsDao { id: Long, dataSource: String, after: Long - ): LiveData + ): LiveData @SkipQueryVerification @Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND Within(coordinates, BuildMbr(:lng1, :lat1, :lng2, :lat2)) AND timeRetrieved > :after") From 0d611222e7df0c1aa836b2fa6f4c92a11d6cf98c Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 9 Jul 2023 17:35:37 +0200 Subject: [PATCH 06/27] OSM: implement cost description --- .../vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt index 75d744378..99786c43f 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt @@ -171,7 +171,7 @@ data class OSMChargingStation( return null } - private fun getCost(): Cost? { + private fun getCost(): Cost { val freecharging = when (tags["fee"]?.lowercase()) { "yes", "y" -> false "no", "n" -> true @@ -182,7 +182,9 @@ data class OSMChargingStation( "yes", "y", "interval" -> false else -> null } - return Cost(freecharging, freeparking) + val description = listOfNotNull(tags["charge"], tags["charge:conditional"]).ifEmpty { null } + ?.joinToString("\n") + return Cost(freecharging, freeparking, null, description) } companion object { From 82b14b7992695d59bf31f7d86156ff8b786b825e Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 9 Jul 2023 17:35:57 +0200 Subject: [PATCH 07/27] OSM: implement address --- .../evmap/api/openstreetmap/OpenStreetMapModel.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt index 99786c43f..c5da1ea86 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt @@ -93,7 +93,7 @@ data class OSMChargingStation( "openstreetmap", getName(), Coordinate(lat, lon), - null, // TODO: Can we determine this with overpass? + getAddress(), getChargepoints(), tags["network"], "https://www.openstreetmap.org/node/$id", @@ -117,6 +117,19 @@ data class OSMChargingStation( true, ) + private fun getAddress(): Address? { + val city = tags["addr:city"] + val country = tags["addr:country"] + val postcode = tags["addr:postcode"] + val street = tags["addr:street"] + val housenumber = tags["addr:housenumber"] ?: tags["addr:housename"] + return if (listOf(city, country, postcode, street, housenumber).any { it != null }) { + Address(city, country, postcode, "$street $housenumber") + } else { + null + } + } + /** * Return the name for this charging station. */ From 4f2e90cdb66067be8deeef5aa896aac6e382fb68 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 9 Jul 2023 18:25:51 +0200 Subject: [PATCH 08/27] OSM: implement getPhotos for images hosted at imgur --- .../api/openstreetmap/OpenStreetMapModel.kt | 46 ++++++++++++++++++- .../vonforst/evmap/storage/TypeConverters.kt | 2 + 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt index c5da1ea86..bafc3867f 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt @@ -2,6 +2,7 @@ package net.vonforst.evmap.api.openstreetmap import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize import net.vonforst.evmap.model.* import okhttp3.internal.immutableListOf import java.time.Instant @@ -105,7 +106,7 @@ data class OSMChargingStation( tags["description"], null, null, - null, + getPhotos(), null, getOpeningHours(), getCost(), @@ -200,6 +201,25 @@ data class OSMChargingStation( return Cost(freecharging, freeparking, null, description) } + private fun getPhotos(): List { + val photos = mutableListOf() + for (i in -1..9) { + val url = tags["image" + if (i >= 0) ":$i" else ""] + if (url != null) { + if (url.startsWith("https://i.imgur.com")) { + ImgurChargerPhoto.create(url)?.let { photos.add(it) } + } + /* + TODO: Imgur seems to be by far the most common image hoster (650 images), + followed by Mapillary (450, requires an API key to retrieve images) + Other than that, we have Google Photos, Wikimedia Commons (100-150 images each). + And there are some other links to various sites, but not all are valid links pointing directly to a JPEG file... + */ + } + } + return photos + } + companion object { /** * Parse raw OSM output power. @@ -222,4 +242,26 @@ data class OSMChargingStation( return numberString.toDoubleOrNull() } } -} \ No newline at end of file +} + +@Parcelize +@JsonClass(generateAdapter = true) +class ImgurChargerPhoto(override val id: String) : ChargerPhoto(id) { + override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String { + return if (allowOriginal) { + "https://i.imgur.com/$id.jpg" + } else { + val value = width ?: size ?: height + "https://i.imgur.com/${id}_d.jpg?maxwidth=$value" + } + } + + companion object { + private val regex = Regex("https?://i.imgur.com/([\\w\\d]+)(?:_d)?.(?:webp|jpg)") + + fun create(url: String): ImgurChargerPhoto? { + val id = regex.find(url)?.groups?.get(1)?.value + return id?.let { ImgurChargerPhoto(it) } + } + } +} 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 f2fe6d9a6..466b46eec 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/TypeConverters.kt @@ -9,6 +9,7 @@ import com.squareup.moshi.Types import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory import net.vonforst.evmap.api.goingelectric.GEChargerPhotoAdapter import net.vonforst.evmap.api.openchargemap.OCMChargerPhotoAdapter +import net.vonforst.evmap.api.openstreetmap.ImgurChargerPhoto import net.vonforst.evmap.autocomplete.AutocompletePlaceType import net.vonforst.evmap.model.ChargeCardId import net.vonforst.evmap.model.Chargepoint @@ -23,6 +24,7 @@ class Converters { PolymorphicJsonAdapterFactory.of(ChargerPhoto::class.java, "type") .withSubtype(GEChargerPhotoAdapter::class.java, "goingelectric") .withSubtype(OCMChargerPhotoAdapter::class.java, "openchargemap") + .withSubtype(ImgurChargerPhoto::class.java, "imgur") .withDefaultValue(null) ) .build() From 003ca9e00944266bd81eb99f39bddfe30c1fbdcf Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 9 Jul 2023 18:29:21 +0200 Subject: [PATCH 09/27] OSM: add charger website --- .../net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt index bafc3867f..27862f0ef 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt @@ -113,7 +113,7 @@ data class OSMChargingStation( "© OpenStreetMap contributors", null, null, - null, + tags["website"], dataFetchTimestamp, true, ) From a257b98985da7ea1bd6b13d4d9ca4dac39cef8ae Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 17 Sep 2023 20:40:14 +0200 Subject: [PATCH 10/27] lazy loading for fullDownload --- .../net/vonforst/evmap/api/ChargepointApi.kt | 2 +- .../api/goingelectric/GoingElectricApi.kt | 2 +- .../api/openchargemap/OpenChargeMapApi.kt | 2 +- .../openstreetmap/OpenStreetMapAdapters.kt | 46 +++++++++++++++++++ .../api/openstreetmap/OpenStreetMapApi.kt | 14 ++++-- .../api/openstreetmap/OpenStreetMapModel.kt | 3 +- .../evmap/storage/ChargeLocationsDao.kt | 4 +- 7 files changed, 63 insertions(+), 10 deletions(-) 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 da14af40e..a218e3279 100644 --- a/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt @@ -86,7 +86,7 @@ interface ChargepointApi { * to generate that within this function (e.g. build the list of available operators using all the * operators found in the dataset). */ - suspend fun fullDownload(referenceData: ReferenceData): List + suspend fun fullDownload(referenceData: ReferenceData): Sequence } 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 a53f34964..3c275d074 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 @@ -162,7 +162,7 @@ class GoingElectricApiWrapper( override val supportsOnlineQueries = true override val supportsFullDownload = false - override suspend fun fullDownload(referenceData: ReferenceData): List { + override suspend fun fullDownload(referenceData: ReferenceData): Sequence { throw NotImplementedError() } 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 1a9530ee7..13d9a8a22 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 @@ -133,7 +133,7 @@ class OpenChargeMapApiWrapper( override val supportsOnlineQueries = true override val supportsFullDownload = false - override suspend fun fullDownload(referenceData: ReferenceData): List { + override suspend fun fullDownload(referenceData: ReferenceData): Sequence { throw NotImplementedError() } diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt index 1a7b07f6d..f105489b9 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt @@ -1,7 +1,16 @@ package net.vonforst.evmap.api.openstreetmap import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import com.squareup.moshi.Moshi import com.squareup.moshi.ToJson +import com.squareup.moshi.rawType +import okhttp3.ResponseBody +import retrofit2.Converter +import retrofit2.Retrofit +import java.lang.reflect.Type import java.time.Instant import kotlin.math.floor @@ -17,4 +26,41 @@ internal class InstantAdapter { fun toJson(value: Instant?): Double? = value?.let { it.epochSecond.toDouble() + it.nano / 1e9 } +} + +internal class OSMConverterFactory(val moshi: Moshi) : Converter.Factory() { + override fun responseBodyConverter( + type: Type, + annotations: Array, + retrofit: Retrofit + ): Converter? { + if (type.rawType != OSMDocument::class.java) return null + + val instantAdapter = moshi.adapter(Instant::class.java) + val osmChargingStationAdapter = moshi.adapter(OSMChargingStation::class.java) + return Converter { body -> + val reader = JsonReader.of(body.source()) + reader.beginObject() + + var timestamp: Instant? = null + var doc: Sequence? = null + while (reader.hasNext()) { + when (reader.nextName()) { + "timestamp" -> timestamp = instantAdapter.fromJson(reader)!! + "elements" -> { + doc = sequence { + reader.beginArray() + while (reader.hasNext()) { + yield(osmChargingStationAdapter.fromJson(reader)!!) + } + reader.endArray() + reader.close() + } + break + } + } + } + OSMDocument(timestamp!!, doc!!) + } + } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt index f046c01b3..9260e00d6 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt @@ -3,6 +3,8 @@ package net.vonforst.evmap.api.openstreetmap import android.database.DatabaseUtils import com.car2go.maps.model.LatLng import com.car2go.maps.model.LatLngBounds +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader import com.squareup.moshi.Moshi import net.vonforst.evmap.BuildConfig import net.vonforst.evmap.R @@ -11,7 +13,6 @@ import net.vonforst.evmap.api.ChargepointApi import net.vonforst.evmap.api.ChargepointList import net.vonforst.evmap.api.FiltersSQLQuery import net.vonforst.evmap.api.StringProvider -import net.vonforst.evmap.api.goingelectric.GEChargepoint import net.vonforst.evmap.api.mapPower import net.vonforst.evmap.api.mapPowerInverse import net.vonforst.evmap.api.nameForPlugType @@ -31,6 +32,7 @@ import net.vonforst.evmap.model.getMultipleChoiceValue import net.vonforst.evmap.model.getSliderValue import net.vonforst.evmap.viewmodel.Resource import okhttp3.OkHttpClient +import okhttp3.Request import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory @@ -58,7 +60,7 @@ interface OpenStreetMapApi { val retrofit = Retrofit.Builder() .baseUrl(baseurl) - .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addConverterFactory(OSMConverterFactory(moshi)) .client(client) .build() return retrofit.create(OpenStreetMapApi::class.java) @@ -213,14 +215,18 @@ class OpenStreetMapApiWrapper(baseurl: String = "https://evmap-dev.vonforst.net" return true } - override suspend fun fullDownload(referenceData: ReferenceData): List { + override suspend fun fullDownload(referenceData: ReferenceData): Sequence { val response = api.getAllChargingStations() if (!response.isSuccessful) { throw IOException(response.message()) } else { val body = response.body()!! val time = body.timestamp - return body.elements.map { it.convert(time) } + return sequence { + body.elements.forEach { + yield(it.convert(time)) + } + } } } } diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt index 27862f0ef..0ab6ec5e4 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt @@ -59,10 +59,9 @@ private val SOCKET_TYPES = immutableListOf( OsmSocket("sev1011_t25", null), ) -@JsonClass(generateAdapter = true) data class OSMDocument( val timestamp: Instant, - val elements: List + val elements: Sequence ) @JsonClass(generateAdapter = true) 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 642630299..a86736315 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -473,7 +473,9 @@ class ChargeLocationsRepository( val refData = referenceData.await() val time = Instant.now() val result = api.fullDownload(refData) - chargeLocationsDao.insert(*result.toTypedArray()) + result.chunked(100).forEach { + chargeLocationsDao.insert(*it.toTypedArray()) + } val region = Mbr( -180.0, -90.0, From e4c0dfd5e31fb50dab4b2717d7adca4f54fd90b6 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 17 Sep 2023 22:32:54 +0200 Subject: [PATCH 11/27] OSM: enable realtime data --- .../evmap/api/availability/EnBwAvailabilityDetector.kt | 10 ++++++++-- .../api/availability/NewMotionAvailabilityDetector.kt | 1 + .../api/availability/TeslaGuestAvailabilityDetector.kt | 1 + .../api/availability/TeslaOwnerAvailabilityDetector.kt | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/EnBwAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/EnBwAvailabilityDetector.kt index 58c3bf9b1..2cb4b86eb 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/EnBwAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/EnBwAvailabilityDetector.kt @@ -242,8 +242,8 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) : } override fun isChargerSupported(charger: ChargeLocation): Boolean { - val country = charger.chargepriceData?.country - ?: charger.address?.country ?: return false + val country = charger.chargepriceData?.country ?: charger.address?.country + return when (charger.dataSource) { // list of countries as of 2023/04/14, according to // https://www.enbw.com/elektromobilitaet/produkte/ladetarife @@ -285,6 +285,12 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) : "ES", "CZ" ) && charger.chargepriceData?.network !in listOf("23", "3534") + /* TODO: OSM usually does not have the country tagged. Therefore we currently just use + a bounding box to determine whether the charger is roughly in Europe */ + "openstreetmap" -> charger.coordinates.lat in 35.0..72.0 + && charger.coordinates.lng in 25.0..65.0 + && charger.operator !in listOf("Tesla, Inc.", "Tesla") + else -> false } } 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 33731a381..b7ca0bc16 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 @@ -221,6 +221,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul return when (charger.dataSource) { "goingelectric" -> charger.network != "Tesla Supercharger" "openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534") + "openstreetmap" -> charger.operator !in listOf("Tesla, Inc.", "Tesla") else -> false } } diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt index b0dd7f084..1b42006e9 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt @@ -163,6 +163,7 @@ class TeslaGuestAvailabilityDetector( return when (charger.dataSource) { "goingelectric" -> charger.network == "Tesla Supercharger" "openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534") + "openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla") else -> false } } diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt index ad7566679..092b4ea5c 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt @@ -162,6 +162,7 @@ class TeslaOwnerAvailabilityDetector( return when (charger.dataSource) { "goingelectric" -> charger.network == "Tesla Supercharger" "openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534") + "openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla") else -> false } } From 6cdcac624e667a5f9ffe49fea873a8b65e597866 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 17 Sep 2023 22:37:29 +0200 Subject: [PATCH 12/27] OSM: add tesla_supercharger_ccs --- .../net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt index 0ab6ec5e4..182e18c99 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt @@ -41,6 +41,7 @@ private val SOCKET_TYPES = immutableListOf( // Tesla OsmSocket("tesla_standard", null), OsmSocket("tesla_supercharger", Chargepoint.SUPERCHARGER), + OsmSocket("tesla_supercharger_ccs", Chargepoint.CCS_UNKNOWN), // CEE OsmSocket("cee_blue", Chargepoint.CEE_BLAU), // Also known as "caravan socket" From 76ed8e5c81d8528b948ac85ffdfea871942d75ba Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 17 Sep 2023 22:51:54 +0200 Subject: [PATCH 13/27] detail_view layout fixes for missing values --- app/src/main/res/layout/detail_view.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/detail_view.xml b/app/src/main/res/layout/detail_view.xml index 6b042f096..ad2e91ffd 100644 --- a/app/src/main/res/layout/detail_view.xml +++ b/app/src/main/res/layout/detail_view.xml @@ -187,6 +187,7 @@ app:layout_constraintEnd_toStartOf="@+id/guideline2" app:layout_constraintStart_toStartOf="@+id/guideline" app:layout_constraintTop_toBottomOf="@+id/textView7" + app:goneUnless="@{charger.data.chargepointsMerged.size() > 0}" tools:itemCount="3" tools:layoutManager="LinearLayoutManager" tools:listitem="@layout/item_connector" @@ -201,6 +202,7 @@ android:text="@string/connectors" android:textAppearance="@style/TextAppearance.Material3.TitleSmall" android:textColor="?colorPrimary" + app:goneUnless="@{charger.data.chargepointsMerged.size() > 0}" app:layout_constraintEnd_toStartOf="@+id/btnRefreshLiveData" app:layout_constraintStart_toStartOf="@+id/guideline" app:layout_constraintTop_toBottomOf="@+id/txtConnectors" /> @@ -320,6 +322,7 @@ android:gravity="right|end" android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : availability.message == "not signed in" ? @string/realtime_data_login_needed : @string/realtime_data_unavailable}" android:textAppearance="@style/TextAppearance.Material3.BodySmall" + app:goneUnless="@{charger.data.chargepointsMerged.size() > 0}" app:layout_constraintEnd_toStartOf="@+id/btnLogin" app:layout_constraintStart_toStartOf="@+id/guideline" app:layout_constraintTop_toBottomOf="@+id/connectors" @@ -350,7 +353,7 @@ android:layout_height="1dp" android:layout_marginTop="8dp" android:background="?android:attr/listDivider" - app:goneUnless="@{charger.data != null && ChargepriceApi.isChargerSupported(charger.data)}" + app:goneUnless="@{charger.data != null && (ChargepriceApi.isChargerSupported(charger.data) || charger.data.chargerUrl != null)}" app:layout_constraintTop_toBottomOf="@+id/buttonsScroller" /> Date: Sun, 17 Sep 2023 23:27:38 +0200 Subject: [PATCH 14/27] add total count in OSMDocument https://github.com/ev-map/evmap-osm/commit/5d7b07b24327d40ccb87e99c9257da803d19f6aa --- .../evmap/api/openstreetmap/OpenStreetMapAdapters.kt | 7 ++++--- .../vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt index f105489b9..0a2852898 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapAdapters.kt @@ -1,9 +1,7 @@ package net.vonforst.evmap.api.openstreetmap import com.squareup.moshi.FromJson -import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader -import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.ToJson import com.squareup.moshi.rawType @@ -38,15 +36,18 @@ internal class OSMConverterFactory(val moshi: Moshi) : Converter.Factory() { val instantAdapter = moshi.adapter(Instant::class.java) val osmChargingStationAdapter = moshi.adapter(OSMChargingStation::class.java) + val longAdapter = moshi.adapter(Long::class.java) return Converter { body -> val reader = JsonReader.of(body.source()) reader.beginObject() var timestamp: Instant? = null var doc: Sequence? = null + var count: Long? = null while (reader.hasNext()) { when (reader.nextName()) { "timestamp" -> timestamp = instantAdapter.fromJson(reader)!! + "count" -> count = longAdapter.fromJson(reader)!! "elements" -> { doc = sequence { reader.beginArray() @@ -60,7 +61,7 @@ internal class OSMConverterFactory(val moshi: Moshi) : Converter.Factory() { } } } - OSMDocument(timestamp!!, doc!!) + OSMDocument(timestamp!!, count!!, doc!!) } } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt index 182e18c99..749fa7f77 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt @@ -62,6 +62,7 @@ private val SOCKET_TYPES = immutableListOf( data class OSMDocument( val timestamp: Instant, + val count: Long, val elements: Sequence ) From e4af6a5be8758b652fef3ac6ede5ed7dfcb3fa25 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Tue, 26 Sep 2023 17:38:35 +0200 Subject: [PATCH 15/27] OSM: implement progress indicator for full download --- .../net/vonforst/evmap/api/ChargepointApi.kt | 15 ++-- .../main/java/net/vonforst/evmap/api/Utils.kt | 1 + .../api/goingelectric/GoingElectricApi.kt | 2 +- .../api/openchargemap/OpenChargeMapApi.kt | 2 +- .../api/openstreetmap/OpenStreetMapApi.kt | 32 ++++++--- .../evmap/storage/ChargeLocationsDao.kt | 69 ++++++++++++++----- .../net/vonforst/evmap/viewmodel/Utils.kt | 15 ++-- app/src/main/res/layout/fragment_map.xml | 4 +- 8 files changed, 94 insertions(+), 46 deletions(-) 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 a218e3279..09ab3b561 100644 --- a/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt @@ -78,15 +78,8 @@ interface ChargepointApi { * Fetches all available chargers from this API. * * This may take a long time and should only be used when the user explicitly wants to download all chargers. - * - * TODO: add an optional callback parameter to this function to be able to receive updates on the download progress? - * TODO: Should this also include getting the ReferenceData, instead of taking it as an argument? - * ReferenceData typically includes information that is needed to create the filter options, e.g. - * mappings between IDs and readable names (for operators etc.). So probably for OSM it makes sense - * to generate that within this function (e.g. build the list of available operators using all the - * operators found in the dataset). */ - suspend fun fullDownload(referenceData: ReferenceData): Sequence + suspend fun fullDownload(): FullDownloadResult } interface StringProvider { @@ -135,4 +128,10 @@ data class ChargepointList(val items: List, val isComplete: companion object { fun empty() = ChargepointList(emptyList(), true) } +} + +interface FullDownloadResult { + val chargers: Sequence + val progress: Float + val referenceData: T } \ 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 1bf358142..19705016a 100644 --- a/app/src/main/java/net/vonforst/evmap/api/Utils.kt +++ b/app/src/main/java/net/vonforst/evmap/api/Utils.kt @@ -11,6 +11,7 @@ import okhttp3.Response import org.json.JSONArray import java.io.IOException import kotlin.coroutines.resumeWithException +import kotlin.experimental.ExperimentalTypeInference import kotlin.math.abs operator fun JSONArray.iterator(): Iterator = 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 3c275d074..1cfab8c0a 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 @@ -162,7 +162,7 @@ class GoingElectricApiWrapper( override val supportsOnlineQueries = true override val supportsFullDownload = false - override suspend fun fullDownload(referenceData: ReferenceData): Sequence { + override suspend fun fullDownload(): FullDownloadResult { throw NotImplementedError() } 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 13d9a8a22..79fb44630 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 @@ -133,7 +133,7 @@ class OpenChargeMapApiWrapper( override val supportsOnlineQueries = true override val supportsFullDownload = false - override suspend fun fullDownload(referenceData: ReferenceData): Sequence { + override suspend fun fullDownload(): FullDownloadResult { throw NotImplementedError() } diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt index 9260e00d6..107ca2d89 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt @@ -3,8 +3,6 @@ package net.vonforst.evmap.api.openstreetmap import android.database.DatabaseUtils import com.car2go.maps.model.LatLng import com.car2go.maps.model.LatLngBounds -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.JsonReader import com.squareup.moshi.Moshi import net.vonforst.evmap.BuildConfig import net.vonforst.evmap.R @@ -12,6 +10,7 @@ import net.vonforst.evmap.addDebugInterceptors import net.vonforst.evmap.api.ChargepointApi import net.vonforst.evmap.api.ChargepointList import net.vonforst.evmap.api.FiltersSQLQuery +import net.vonforst.evmap.api.FullDownloadResult import net.vonforst.evmap.api.StringProvider import net.vonforst.evmap.api.mapPower import net.vonforst.evmap.api.mapPowerInverse @@ -32,14 +31,11 @@ import net.vonforst.evmap.model.getMultipleChoiceValue import net.vonforst.evmap.model.getSliderValue import net.vonforst.evmap.viewmodel.Resource import okhttp3.OkHttpClient -import okhttp3.Request import retrofit2.Response import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.http.GET import java.io.IOException import java.time.Duration -import java.time.Instant interface OpenStreetMapApi { @GET("charging-stations-osm.json") @@ -108,7 +104,7 @@ class OpenStreetMapApiWrapper(baseurl: String = "https://evmap-dev.vonforst.net" } override suspend fun getReferenceData(): Resource { - TODO("Not yet implemented") + throw NotImplementedError() } override fun getFilters( @@ -215,20 +211,34 @@ class OpenStreetMapApiWrapper(baseurl: String = "https://evmap-dev.vonforst.net" return true } - override suspend fun fullDownload(referenceData: ReferenceData): Sequence { + override suspend fun fullDownload(): FullDownloadResult { val response = api.getAllChargingStations() if (!response.isSuccessful) { throw IOException(response.message()) } else { val body = response.body()!! + return OSMFullDownloadResult(body) + } + } +} + +data class OSMReferenceData(val test: String) : ReferenceData() + +class OSMFullDownloadResult(private val body: OSMDocument) : FullDownloadResult { + private var downloadProgress = 0f + override val chargers: Sequence + get() { val time = body.timestamp return sequence { - body.elements.forEach { + body.elements.forEachIndexed { i, it -> yield(it.convert(time)) + downloadProgress = i.toFloat() / body.count } } } - } -} + override val progress: Float + get() = downloadProgress + override val referenceData: OSMReferenceData + get() = TODO("Not yet implemented") -data class OSMReferenceData(val test: String) : ReferenceData() +} 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 a86736315..25129be0c 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -11,6 +11,13 @@ import com.car2go.maps.model.LatLngBounds import com.car2go.maps.util.SphericalUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.cancel +import kotlinx.coroutines.launch import net.vonforst.evmap.api.ChargepointApi import net.vonforst.evmap.api.ChargepointList import net.vonforst.evmap.api.StringProvider @@ -154,6 +161,8 @@ class ChargeLocationsRepository( private val chargeLocationsDao = db.chargeLocationsDao() private val savedRegionDao = db.savedRegionDao() + private var fullDownloadJob: Job? = null + private var fullDownloadProgress: MutableStateFlow = MutableStateFlow(null) fun getChargepoints( bounds: LatLngBounds, @@ -225,7 +234,16 @@ class ChargeLocationsRepository( } else { return liveData { if (!savedRegionResult.await()) { - fullDownload() + val job = fullDownloadJob ?: scope.launch { + fullDownload() + }.also { fullDownloadJob = it } + val progressJob = scope.launch { + fullDownloadProgress.collect { + emit(Resource.loading(null, it)) + } + } + job.join() + progressJob.cancelAndJoin() } emit(Resource.success(dbResult.await())) } @@ -307,7 +325,16 @@ class ChargeLocationsRepository( } else { return liveData { if (!savedRegionResult.await()) { - fullDownload() + val job = fullDownloadJob ?: scope.launch { + fullDownload() + }.also { fullDownloadJob = it } + val progressJob = scope.launch { + fullDownloadProgress.collect { + emit(Resource.loading(null, it)) + } + } + job.join() + progressJob.cancelAndJoin() } emit(Resource.success(dbResult.await())) } @@ -470,25 +497,29 @@ class ChargeLocationsRepository( val api = api.value!! if (!api.supportsFullDownload) return - val refData = referenceData.await() val time = Instant.now() - val result = api.fullDownload(refData) - result.chunked(100).forEach { - chargeLocationsDao.insert(*it.toTypedArray()) - } - val region = Mbr( - -180.0, - -90.0, - 180.0, - 90.0, 4326 - ).asPolygon() - savedRegionDao.insert( - SavedRegion( - region, api.id, time, - null, - true + val result = api.fullDownload() + try { + result.chargers.chunked(100).forEach { + chargeLocationsDao.insert(*it.toTypedArray()) + fullDownloadProgress.value = result.progress + } + val region = Mbr( + -180.0, + -90.0, + 180.0, + 90.0, 4326 + ).asPolygon() + savedRegionDao.insert( + SavedRegion( + region, api.id, time, + null, + true + ) ) - ) + } finally { + fullDownloadProgress.value = null + } } diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/Utils.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/Utils.kt index 7847a5822..176c64831 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/Utils.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/Utils.kt @@ -39,19 +39,24 @@ enum class Status { * trying to write it to a Parcel if the type parameter does not implement Parcelable. */ @Parcelize -data class Resource(val status: Status, val data: @RawValue T?, val message: String?) : +data class Resource( + val status: Status, + val data: @RawValue T?, + val message: String?, + val progress: Float? = null +) : Parcelable { companion object { fun success(data: T?): Resource { - return Resource(Status.SUCCESS, data, null) + return Resource(Status.SUCCESS, data, null, null) } fun error(msg: String?, data: T?): Resource { - return Resource(Status.ERROR, data, msg) + return Resource(Status.ERROR, data, msg, null) } - fun loading(data: T?): Resource { - return Resource(Status.LOADING, data, null) + fun loading(data: T?, progress: Float? = null): Resource { + return Resource(Status.LOADING, data, null, progress) } } } diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index ca9d4568f..cd57a0b5c 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -131,7 +131,9 @@ android:layout_height="wrap_content" android:layout_marginTop="-10dp" android:layout_marginBottom="-7dp" - android:indeterminate="true" + android:indeterminate="@{ vm.chargepoints.progress == null }" + android:progress="@{ vm.chargepoints.progress != null ? Math.round(vm.chargepoints.progress * 100f) : 0 }" + android:max="100" android:visibility="visible" app:goneUnless="@{ vm.chargepoints.status == Status.LOADING }" /> From e78eaa547fe471274e0e74302a31d2d6940709a3 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sat, 11 Nov 2023 19:24:05 +0100 Subject: [PATCH 16/27] fix OnboardingFragment for OSM --- .../java/net/vonforst/evmap/fragment/OnboardingFragment.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/net/vonforst/evmap/fragment/OnboardingFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/OnboardingFragment.kt index 68c93588c..ceb94f804 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/OnboardingFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/OnboardingFragment.kt @@ -211,6 +211,8 @@ class DataSourceSelectFragment : OnboardingPageFragment() { binding.rgDataSource.textView27, binding.rgDataSource.rbOpenChargeMap, binding.rgDataSource.textView28, + binding.rgDataSource.rbOpenStreetMap, + binding.rgDataSource.textView29, binding.dataSourceHint, binding.cbAcceptPrivacy ) @@ -239,7 +241,8 @@ class DataSourceSelectFragment : OnboardingPageFragment() { for (rb in listOf( binding.rgDataSource.rbGoingElectric, - binding.rgDataSource.rbOpenChargeMap + binding.rgDataSource.rbOpenChargeMap, + binding.rgDataSource.rbOpenStreetMap )) { rb.setOnCheckedChangeListener { _, _ -> if (binding.btnGetStarted.visibility == View.INVISIBLE) { From ba5381c01b4dae4ab888df52b2b829e5ca27c487 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sat, 11 Nov 2023 19:47:23 +0100 Subject: [PATCH 17/27] OSM: implement ReferenceData and network filter --- .../net/vonforst/evmap/api/ChargepointApi.kt | 10 ++++ .../api/openstreetmap/OpenStreetMapApi.kt | 32 +++++++++-- .../evmap/storage/ChargeLocationsDao.kt | 19 +++---- .../net/vonforst/evmap/storage/Database.kt | 5 ++ .../evmap/storage/OSMReferenceDataDao.kt | 53 +++++++++++++++++++ 5 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/net/vonforst/evmap/storage/OSMReferenceDataDao.kt 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 09ab3b561..abfe1bfaf 100644 --- a/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt @@ -130,6 +130,16 @@ data class ChargepointList(val items: List, val isComplete: } } +/** + * Result returned from fullDownload() function. + * + * Note that [chargers] is implemented as a [Sequence] so that downloaded chargers can be saved + * while they are being parsed instead of having to keep all of them in RAM at once. + * + * [progress] is updated regularly to indicate the current download progress. + * [referenceData] will typically only be available once the download is completed, i.e. you have + * iterated over the whole sequence of [chargers]. + */ interface FullDownloadResult { val chargers: Sequence val progress: Float diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt index 107ca2d89..937ef1827 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt @@ -12,6 +12,7 @@ import net.vonforst.evmap.api.ChargepointList import net.vonforst.evmap.api.FiltersSQLQuery import net.vonforst.evmap.api.FullDownloadResult import net.vonforst.evmap.api.StringProvider +import net.vonforst.evmap.api.goingelectric.GEReferenceData import net.vonforst.evmap.api.mapPower import net.vonforst.evmap.api.mapPowerInverse import net.vonforst.evmap.api.nameForPlugType @@ -128,6 +129,9 @@ class OpenStreetMapApiWrapper(baseurl: String = "https://evmap-dev.vonforst.net" nameForPlugType(sp, plug) } + val refData = referenceData as OSMReferenceData + val networkMap = refData.networks.associateWith { it } + return listOf( BooleanFilter(sp.getString(R.string.filter_free), "freecharging"), BooleanFilter(sp.getString(R.string.filter_free_parking), "freeparking"), @@ -152,6 +156,10 @@ class OpenStreetMapApiWrapper(baseurl: String = "https://evmap-dev.vonforst.net" ), manyChoices = true ), + MultipleChoiceFilter( + sp.getString(R.string.filter_networks), "networks", + networkMap, manyChoices = true + ), SliderFilter( sp.getString(R.string.filter_min_connectors), "min_connectors", @@ -204,6 +212,16 @@ class OpenStreetMapApiWrapper(baseurl: String = "https://evmap-dev.vonforst.net" requiresChargepointQuery = true } + val networks = filters.getMultipleChoiceValue("networks") + if (networks != null && !networks.all) { + val networksList = if (networks.values.size == 0) { + "" + } else { + networks.values.joinToString(",") { DatabaseUtils.sqlEscapeString(it) } + } + result.append(" AND network IN (${networksList})") + } + return FiltersSQLQuery(result.toString(), requiresChargepointQuery, false) } @@ -222,23 +240,31 @@ class OpenStreetMapApiWrapper(baseurl: String = "https://evmap-dev.vonforst.net" } } -data class OSMReferenceData(val test: String) : ReferenceData() +data class OSMReferenceData(val networks: List) : ReferenceData() class OSMFullDownloadResult(private val body: OSMDocument) : FullDownloadResult { private var downloadProgress = 0f + private var refData: OSMReferenceData? = null + override val chargers: Sequence get() { val time = body.timestamp + val networks = mutableListOf() + return sequence { body.elements.forEachIndexed { i, it -> - yield(it.convert(time)) + val charger = it.convert(time) + yield(charger) downloadProgress = i.toFloat() / body.count + charger.network?.let { networks.add(it) } } + refData = OSMReferenceData(networks) } } override val progress: Float get() = downloadProgress override val referenceData: OSMReferenceData - get() = TODO("Not yet implemented") + get() = refData + ?: throw UnsupportedOperationException("referenceData is only available once download is complete") } 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 25129be0c..ae5b95021 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -25,6 +25,7 @@ import net.vonforst.evmap.api.goingelectric.GEReferenceData import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper import net.vonforst.evmap.api.openchargemap.OCMReferenceData import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper +import net.vonforst.evmap.api.openstreetmap.OSMReferenceData import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper import net.vonforst.evmap.model.* import net.vonforst.evmap.ui.cluster @@ -142,15 +143,7 @@ class ChargeLocationsRepository( } is OpenStreetMapApiWrapper -> { - liveData { - emit( - OCMReferenceData( - emptyList(), - emptyList(), - emptyList() - ) - ) - } // TODO: add OSM reference data + OSMReferenceDataRepository(db.osmReferenceDataDao()).getReferenceData() } else -> { @@ -517,6 +510,14 @@ class ChargeLocationsRepository( true ) ) + + when (api) { + is OpenStreetMapApiWrapper -> { + val refData = result.referenceData + OSMReferenceDataRepository(db.osmReferenceDataDao()).updateReferenceData(refData as OSMReferenceData) + } + } + } finally { fullDownloadProgress.value = null } 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 988f6faa1..95fe6a014 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/Database.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/Database.kt @@ -33,6 +33,7 @@ import net.vonforst.evmap.model.* OCMConnectionType::class, OCMCountry::class, OCMOperator::class, + OSMNetwork::class, SavedRegion::class ], version = 23 ) @@ -51,6 +52,9 @@ abstract class AppDatabase : RoomDatabase() { // OpenChargeMap API specific abstract fun ocmReferenceDataDao(): OCMReferenceDataDao + // OpenStreetMap API specific + abstract fun osmReferenceDataDao(): OSMReferenceDataDao + companion object { private lateinit var context: Context private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { @@ -465,6 +469,7 @@ abstract class AppDatabase : RoomDatabase() { override fun migrate(db: SupportSQLiteDatabase) { // API openstreetmap added db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openstreetmap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)") + db.execSQL("CREATE TABLE IF NOT EXISTS `OSMNetwork` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))") } } } diff --git a/app/src/main/java/net/vonforst/evmap/storage/OSMReferenceDataDao.kt b/app/src/main/java/net/vonforst/evmap/storage/OSMReferenceDataDao.kt new file mode 100644 index 000000000..c95d5a947 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/storage/OSMReferenceDataDao.kt @@ -0,0 +1,53 @@ +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.openchargemap.* +import net.vonforst.evmap.api.openstreetmap.OSMReferenceData +import net.vonforst.evmap.viewmodel.Status +import java.time.Duration +import java.time.Instant + +@Entity +data class OSMNetwork(@PrimaryKey val name: String) + +@Dao +abstract class OSMReferenceDataDao { + // NETWORKS + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insert(vararg networks: OSMNetwork) + + @Query("DELETE FROM osmnetwork") + abstract fun deleteAllNetworks() + + @Transaction + open suspend fun updateNetworks(networks: List) { + deleteAllNetworks() + for (network in networks) { + insert(network) + } + } + + @Query("SELECT * FROM osmnetwork") + abstract fun getAllNetworks(): LiveData> +} + +class OSMReferenceDataRepository(private val dao: OSMReferenceDataDao) { + fun getReferenceData(): LiveData { + val networks = dao.getAllNetworks() + return MediatorLiveData().apply { + value = null + addSource(networks) { _ -> + val n = networks.value ?: return@addSource + value = OSMReferenceData(n.map { it.name }) + } + } + } + + suspend fun updateReferenceData(refData: OSMReferenceData) { + dao.updateNetworks(refData.networks.map { OSMNetwork(it) }) + } +} \ No newline at end of file From fc303b42d79fdf7da3a6652ca6bddb7d6038137a Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 19 Nov 2023 19:04:16 +0100 Subject: [PATCH 18/27] TeslaAvailabilityDetector: raise exception if chargepoints not known --- .../evmap/api/availability/TeslaGuestAvailabilityDetector.kt | 4 ++++ .../evmap/api/availability/TeslaOwnerAvailabilityDetector.kt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt index 1b42006e9..d8e2d5976 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaGuestAvailabilityDetector.kt @@ -23,6 +23,10 @@ class TeslaGuestAvailabilityDetector( private var api = TeslaChargingGuestGraphQlApi.create(client, baseUrl) override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus { + if (location.chargepoints.isEmpty() || location.chargepoints.any { !it.hasKnownPower() }) { + throw AvailabilityDetectorException("no candidates found.") + } + val results = cuaApi.getTeslaLocations() val result = diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt index 092b4ea5c..a55dae010 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaOwnerAvailabilityDetector.kt @@ -29,6 +29,10 @@ class TeslaOwnerAvailabilityDetector( } override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus { + if (location.chargepoints.isEmpty() || location.chargepoints.any { !it.hasKnownPower() }) { + throw AvailabilityDetectorException("no candidates found.") + } + val api = initApi() val req = TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesRequest( TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesVariables( From 6c6e107c6004efea7a69dd2c16789e924e546054 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 12 Nov 2023 15:14:54 +0100 Subject: [PATCH 19/27] OSM: adjustments for AA/AAOS app --- .../evmap/auto/ChargerDetailScreen.kt | 3 +- .../java/net/vonforst/evmap/auto/MapScreen.kt | 40 +++++- .../vonforst/evmap/auto/SettingsScreens.kt | 115 +++++++++++++----- .../evmap/storage/ChargeLocationsDao.kt | 3 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 129 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt b/app/src/main/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt index 7313d4ec7..a07aa5f47 100644 --- a/app/src/main/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt +++ b/app/src/main/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt @@ -60,6 +60,7 @@ import net.vonforst.evmap.storage.ChargeLocationsRepository import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.ui.ChargerIconGenerator import net.vonforst.evmap.ui.getMarkerTint +import net.vonforst.evmap.utils.formatDMS import net.vonforst.evmap.viewmodel.Status import net.vonforst.evmap.viewmodel.awaitFinished import java.time.ZoneId @@ -245,7 +246,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : // Row 1: address + chargepoints rows.add(Row.Builder().apply { - setTitle(charger.address.toString()) + setTitle(charger.address?.toString() ?: charger.coordinates.formatDMS()) if (photo == null) { // show just the icon diff --git a/app/src/main/java/net/vonforst/evmap/auto/MapScreen.kt b/app/src/main/java/net/vonforst/evmap/auto/MapScreen.kt index 7426ceef3..d914314aa 100644 --- a/app/src/main/java/net/vonforst/evmap/auto/MapScreen.kt +++ b/app/src/main/java/net/vonforst/evmap/auto/MapScreen.kt @@ -6,6 +6,7 @@ import android.location.Location import androidx.activity.OnBackPressedCallback import androidx.car.app.AppManager import androidx.car.app.CarContext +import androidx.car.app.CarToast import androidx.car.app.Screen import androidx.car.app.annotations.ExperimentalCarApi import androidx.car.app.annotations.RequiresCarApi @@ -30,6 +31,8 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.IconCompat import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import com.car2go.maps.AnyMap import com.car2go.maps.OnMapReadyCallback @@ -56,6 +59,8 @@ import net.vonforst.evmap.storage.ChargeLocationsRepository import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.ui.MarkerManager import net.vonforst.evmap.utils.distanceBetween +import net.vonforst.evmap.utils.headingDiff +import net.vonforst.evmap.viewmodel.Resource import net.vonforst.evmap.viewmodel.Status import net.vonforst.evmap.viewmodel.await import net.vonforst.evmap.viewmodel.awaitFinished @@ -67,6 +72,9 @@ import java.time.Instant import java.time.ZonedDateTime import kotlin.collections.set import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.time.DurationUnit +import kotlin.time.TimeSource /** * Main map screen showing either nearby chargers or favorites. @@ -425,12 +433,15 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) : } this@MapScreen.chargers = chargers } else { - val response = repo.getChargepoints( + val responseLiveData = repo.getChargepoints( map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom, filtersWithValue, false - ).awaitFinished() + ) + val observer = setupProgressToasts(responseLiveData) + val response = responseLiveData.awaitFinished() + responseLiveData.removeObserver(observer) if (response.status == Status.ERROR || response.data == null) { loadingError = true this@MapScreen.chargers = null @@ -454,6 +465,31 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) : } } + private fun setupProgressToasts( + responseLiveData: LiveData>> + ): Observer>> { + var lastTime = TimeSource.Monotonic.markNow() + val observer = + Observer>> { value -> + if (value.progress != null && lastTime.elapsedNow().toDouble( + DurationUnit.SECONDS + ) > 2 + ) { + CarToast.makeText( + carContext, + carContext.getString( + R.string.downloading_chargers_percent, + value.progress * 100 + ), + CarToast.LENGTH_SHORT + ).show() + lastTime = TimeSource.Monotonic.markNow() + } + } + responseLiveData.observe(this@MapScreen, observer) + return observer + } + private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) { val isUpdate = this.energyLevel == null this.energyLevel = energyLevel diff --git a/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt b/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt index e15074f9a..7a12f8a5b 100644 --- a/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt +++ b/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt @@ -30,6 +30,8 @@ import androidx.car.app.model.Toggle import androidx.core.content.IntentCompat import androidx.core.graphics.drawable.IconCompat import androidx.core.text.HtmlCompat +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import kotlinx.coroutines.launch @@ -156,7 +158,7 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) { } } -class DataSettingsScreen(ctx: CarContext) : Screen(ctx) { +class DataSettingsScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver { val prefs = PreferenceDataSource(ctx) val encryptedPrefs = EncryptedPreferenceDataStore(ctx) val db = AppDatabase.getInstance(ctx) @@ -174,11 +176,15 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) { var teslaLoggingIn = false + init { + lifecycle.addObserver(this) + } + override fun onGetTemplate(): Template { return ListTemplate.Builder().apply { setTitle(carContext.getString(R.string.settings_data_sources)) setHeaderAction(Action.BACK) - setSingleList(ItemList.Builder().apply { + addSectionedList(SectionedItemList.create(ItemList.Builder().apply { addItem(Row.Builder().apply { setTitle(carContext.getString(R.string.pref_data_source)) setBrowsable(true) @@ -194,6 +200,41 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) { ) } }.build()) + addItem( + Row.Builder() + .setTitle(carContext.getString(R.string.pref_prediction_enabled)) + .addText(carContext.getString(R.string.pref_prediction_enabled_summary)) + .setToggle(Toggle.Builder { + prefs.predictionEnabled = it + }.setChecked(prefs.predictionEnabled).build()) + .build() + ) + addItem(Row.Builder().apply { + setTitle(carContext.getString(R.string.pref_tesla_account)) + addText( + if (encryptedPrefs.teslaRefreshToken != null) { + carContext.getString( + R.string.pref_tesla_account_enabled, + encryptedPrefs.teslaEmail + ) + } else if (teslaLoggingIn) { + carContext.getString(R.string.logging_in) + } else { + carContext.getString(R.string.pref_tesla_account_disabled) + } + ) + if (encryptedPrefs.teslaRefreshToken != null) { + setOnClickListener { + teslaLogout() + } + } else { + setOnClickListener(ParkedOnlyOnClickListener.create { + teslaLogin() + }) + } + }.build()) + }.build(), carContext.getString(R.string.settings_charger_data))) + addSectionedList(SectionedItemList.create(ItemList.Builder().apply { addItem(Row.Builder().apply { setTitle(carContext.getString(R.string.pref_search_provider)) setBrowsable(true) @@ -242,43 +283,54 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) { } } }.build()) - addItem( - Row.Builder() - .setTitle(carContext.getString(R.string.pref_prediction_enabled)) - .addText(carContext.getString(R.string.pref_prediction_enabled_summary)) - .setToggle(Toggle.Builder { - prefs.predictionEnabled = it - }.setChecked(prefs.predictionEnabled).build()) - .build() - ) + }.build(), carContext.getString(R.string.settings_map))) + addSectionedList(SectionedItemList.create(ItemList.Builder().apply { addItem(Row.Builder().apply { - setTitle(carContext.getString(R.string.pref_tesla_account)) - addText( - if (encryptedPrefs.teslaRefreshToken != null) { - carContext.getString( - R.string.pref_tesla_account_enabled, - encryptedPrefs.teslaEmail + setTitle(carContext.getString(R.string.settings_cache_count)) + cacheCount?.let { count -> + cacheSize?.let { size -> + val sizeMb = size.toFloat() / 1024 / 1024 + addText( + carContext.getString( + R.string.settings_cache_count_summary, + count, + sizeMb + ) ) - } else if (teslaLoggingIn) { - carContext.getString(R.string.logging_in) - } else { - carContext.getString(R.string.pref_tesla_account_disabled) } - ) - if (encryptedPrefs.teslaRefreshToken != null) { - setOnClickListener { - teslaLogout() + } + }.build()) + addItem(Row.Builder().apply { + setTitle(carContext.getString(R.string.settings_cache_clear)) + addText(carContext.getString(R.string.settings_cache_clear_summary)) + setOnClickListener { + lifecycleScope.launch { + db.savedRegionDao().deleteAll() + db.chargeLocationsDao().deleteAllIfNotFavorite() + loadCacheSize() } - } else { - setOnClickListener(ParkedOnlyOnClickListener.create { - teslaLogin() - }) } }.build()) - }.build()) + }.build(), carContext.getString(R.string.settings_caching))) }.build() } + var cacheCount: Long? = null + var cacheSize: Long? = null + + private suspend fun loadCacheSize() { + cacheCount = db.chargeLocationsDao().getCountAsync() + cacheSize = db.chargeLocationsDao().getSize() + invalidate() + } + + override fun onStart(owner: LifecycleOwner) { + super.onStart(owner) + lifecycleScope.launch { + loadCacheSize() + } + } + private fun teslaLogin() { val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier() val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier) @@ -398,7 +450,8 @@ class ChooseDataSourceScreen( val descriptions = when (type) { Type.CHARGER_DATA_SOURCE -> listOf( carContext.getString(R.string.data_source_goingelectric_desc), - carContext.getString(R.string.data_source_openchargemap_desc) + carContext.getString(R.string.data_source_openchargemap_desc), + carContext.getString(R.string.data_source_openstreetmap_desc) ) Type.SEARCH_PROVIDER -> null Type.MAP_PROVIDER -> null 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 ae5b95021..15082432c 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -100,6 +100,9 @@ abstract class ChargeLocationsDao { @Query("SELECT COUNT(*) FROM chargelocation") abstract fun getCount(): LiveData + @Query("SELECT COUNT(*) FROM chargelocation") + abstract suspend fun getCountAsync(): Long + @SkipQueryVerification @Query("SELECT SUM(pgsize) FROM dbstat WHERE name == \"ChargeLocation\"") abstract suspend fun getSize(): Long diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e6b122184..c9857269d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -360,6 +360,7 @@ Tesla Daten konnten nicht geladen werden In Zwischenablage kopiert + Lade Ladestations-Datenbank herunter… %.0f%% Verfügbar Besetzt Lädt diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a4cb83d5..12cc54214 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -360,6 +360,7 @@ Tesla Could not load data Copied to clipboard + Downloading charger database… %.0f%% Available Occupied Charging From 5cfa2de661033ab9cc8ca96cd08d9dba04a6996d Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 12 Nov 2023 15:22:35 +0100 Subject: [PATCH 20/27] OSM: progress indicator improvements --- app/src/main/res/layout/fragment_map.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index cd57a0b5c..19a09b6e1 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -92,7 +92,7 @@ android:ellipsize="end" android:background="@null" android:gravity="center_vertical" - android:hint="@string/search" + android:hint="@{vm.chargepoints.progress != null ? @string/downloading_chargers_percent(vm.chargepoints.progress * 100) : @string/search}" android:textSize="18sp" android:textAppearance="@style/TextAppearance.Material3.BodyLarge" android:textColorHint="@color/hint_text_color" diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c9857269d..d844db107 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -360,7 +360,7 @@ Tesla Daten konnten nicht geladen werden In Zwischenablage kopiert - Lade Ladestations-Datenbank herunter… %.0f%% + Lade herunter… %.0f%% Verfügbar Besetzt Lädt diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 12cc54214..36ba24092 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -360,7 +360,7 @@ Tesla Could not load data Copied to clipboard - Downloading charger database… %.0f%% + Downloading… %.0f%% Available Occupied Charging From dd609ca2134eef8c0c2102cc9b75ec5db9c32f12 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 12 Nov 2023 21:40:45 +0100 Subject: [PATCH 21/27] move to production webserver --- .../net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt index 937ef1827..443d3441e 100644 --- a/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapApi.kt @@ -49,7 +49,7 @@ interface OpenStreetMapApi { .build() fun create( - baseurl: String = "https://evmap-dev.vonforst.net" + baseurl: String = "https://osm.ev-map.app/" ): OpenStreetMapApi { val client = OkHttpClient.Builder().apply { if (BuildConfig.DEBUG) addDebugInterceptors() @@ -66,7 +66,7 @@ interface OpenStreetMapApi { } -class OpenStreetMapApiWrapper(baseurl: String = "https://evmap-dev.vonforst.net") : +class OpenStreetMapApiWrapper(baseurl: String = "https://osm.ev-map.app/") : ChargepointApi { override val name = "OpenStreetMap" override val id = "openstreetmap" From 3b07cb2130a83b53a21fb53215eed4bc456137ed Mon Sep 17 00:00:00 2001 From: Johan von Forstner <5310424+johan12345@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:52:16 +0100 Subject: [PATCH 22/27] Consistency: add "." to strings Co-authored-by: Danilo Bargen --- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d844db107..017476c71 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -226,7 +226,7 @@ OpenStreetMap Sehr gute Abdeckung in den deutschsprachigen Ländern. Beschreibungen in Deutsch. Von der Community gepflegt. - Experimentelle Unterstützung in EVMap, nicht alle Funktionen nutzbar + Experimentelle Unterstützung in EVMap, nicht alle Funktionen nutzbar. weiter Los geht\'s Alles klar diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36ba24092..7722298af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -226,7 +226,7 @@ Open Charge Map Great in the German-speaking countries. Descriptions in German. Community-maintained. - Experimental support in EVMap, not all features available + Experimental support in EVMap, not all features available. next Get started Got it From ddebe5b3a656a4f8a3e76031690f8b1707d54c28 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 26 Nov 2023 19:51:34 +0100 Subject: [PATCH 23/27] OSM: reduce flashing progress bar when moving map during download --- .../main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt | 3 +++ 1 file changed, 3 insertions(+) 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 15082432c..56f162d79 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -229,6 +229,9 @@ class ChargeLocationsRepository( } } else { return liveData { + if (fullDownloadJob != null) { + fullDownloadProgress.value?.let { emit(Resource.loading(null, it)) } + } if (!savedRegionResult.await()) { val job = fullDownloadJob ?: scope.launch { fullDownload() From 417fbb108a6dcde793e66ed18e7fc7c06a3bc259 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sun, 26 Nov 2023 20:01:51 +0100 Subject: [PATCH 24/27] OSM: try to speed up download process --- .../net/vonforst/evmap/storage/ChargeLocationsDao.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 56f162d79..9328b4414 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.cancel import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import net.vonforst.evmap.api.ChargepointApi import net.vonforst.evmap.api.ChargepointList import net.vonforst.evmap.api.StringProvider @@ -499,8 +500,14 @@ class ChargeLocationsRepository( val time = Instant.now() val result = api.fullDownload() try { - result.chargers.chunked(100).forEach { - chargeLocationsDao.insert(*it.toTypedArray()) + var insertJob: Job? = null + result.chargers.chunked(1024).forEach { + insertJob?.join() + insertJob = withContext(Dispatchers.IO) { + scope.launch { + chargeLocationsDao.insert(*it.toTypedArray()) + } + } fullDownloadProgress.value = result.progress } val region = Mbr( From 819605206060fb35574afb77dea876f4ff009623 Mon Sep 17 00:00:00 2001 From: johan12345 Date: Sat, 2 Dec 2023 23:23:26 +0100 Subject: [PATCH 25/27] WIP: clustering in DB --- .../net/vonforst/evmap/model/ChargersModel.kt | 5 +++-- .../evmap/storage/ChargeLocationsDao.kt | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) 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 7967b3753..d84949ed7 100644 --- a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt +++ b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt @@ -3,6 +3,7 @@ package net.vonforst.evmap.model import android.content.Context import android.os.Parcelable import androidx.core.text.HtmlCompat +import androidx.room.ColumnInfo import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey @@ -353,8 +354,8 @@ abstract class ChargerPhoto(open val id: String) : Parcelable { } data class ChargeLocationCluster( - val clusterCount: Int, - val coordinates: Coordinate, + @ColumnInfo("clusterCount") val clusterCount: Int, + @ColumnInfo("coordinates") val coordinates: Coordinate, val items: List? = null ) : ChargepointListItem() 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 9328b4414..53b82026b 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -98,6 +98,18 @@ abstract class ChargeLocationsDao { @RawQuery(observedEntities = [ChargeLocation::class]) abstract fun getChargeLocationsCustom(query: SupportSQLiteQuery): LiveData> + @SkipQueryVerification + @Query("SELECT SUM(1) AS clusterCount, MakePoint(AVG(X(coordinates)), AVG(Y(coordinates)), 4326) as center, SnapToGrid(coordinates, :precision) AS snapped FROM chargelocation WHERE dataSource == :dataSource AND Within(coordinates, BuildMbr(:lng1, :lat1, :lng2, :lat2)) AND timeRetrieved > :after GROUP BY snapped") + abstract fun getChargeLocationClusters( + lat1: Double, + lat2: Double, + lng1: Double, + lng2: Double, + dataSource: String, + after: Long, + precision: Double + ): LiveData> + @Query("SELECT COUNT(*) FROM chargelocation") abstract fun getCount(): LiveData @@ -109,6 +121,12 @@ abstract class ChargeLocationsDao { abstract suspend fun getSize(): Long } +data class ChargeLocationClusterSimple( + @ColumnInfo("clusterCount") val clusterCount: Int, + @ColumnInfo("center") val center: Coordinate, + @ColumnInfo("snapped") val snapped: Coordinate, +) + /** * The ChargeLocationsRepository wraps the ChargepointApi and the DB to provide caching * and clustering functionality. From 13a06eb8a2b8c8dcae132a279e02cfff6943721d Mon Sep 17 00:00:00 2001 From: Altonss <66519591+Altonss@users.noreply.github.com> Date: Thu, 5 Sep 2024 19:14:15 +0200 Subject: [PATCH 26/27] Increase margin to data source text (#361) * Update fragment_onboarding_data_source.xml Increase margin to data source text * Update fragment_onboarding_data_source.xml Increase margin to data source text * use margin instead of padding --------- Co-authored-by: johan12345 --- .../main/res/layout-land/fragment_onboarding_data_source.xml | 4 ++-- app/src/main/res/layout/fragment_onboarding_data_source.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout-land/fragment_onboarding_data_source.xml b/app/src/main/res/layout-land/fragment_onboarding_data_source.xml index d27c2fba7..7e1e67acf 100644 --- a/app/src/main/res/layout-land/fragment_onboarding_data_source.xml +++ b/app/src/main/res/layout-land/fragment_onboarding_data_source.xml @@ -44,7 +44,7 @@ android:layout_height="wrap_content" android:layout_marginStart="32dp" android:layout_marginEnd="32dp" - android:layout_marginBottom="8dp" + android:layout_marginBottom="16dp" android:breakStrategy="balanced" android:text="@string/data_sources_description" android:textAppearance="@style/TextAppearance.Material3.BodyMedium" @@ -92,4 +92,4 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/scroll" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_onboarding_data_source.xml b/app/src/main/res/layout/fragment_onboarding_data_source.xml index bf4bf7d70..4fe642a47 100644 --- a/app/src/main/res/layout/fragment_onboarding_data_source.xml +++ b/app/src/main/res/layout/fragment_onboarding_data_source.xml @@ -18,7 +18,7 @@ android:layout_height="wrap_content" android:layout_marginStart="24dp" android:layout_marginEnd="24dp" - android:layout_marginBottom="16dp" + android:layout_marginBottom="32dp" app:layout_constraintBottom_toTopOf="@+id/dataSourceHint" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" @@ -103,4 +103,4 @@ tools:text="@string/accept_privacy" /> - \ No newline at end of file + From a76987a46cc66b8bf554c2a2bf2acf624c0cc361 Mon Sep 17 00:00:00 2001 From: Johan von Forstner Date: Sat, 14 Sep 2024 17:17:46 +0200 Subject: [PATCH 27/27] fix imports --- .../net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt | 1 + .../net/vonforst/evmap/api/openchargemap/OpenChargeMapApi.kt | 1 + 2 files changed, 2 insertions(+) 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 1cfab8c0a..4a558905c 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 @@ -15,6 +15,7 @@ import net.vonforst.evmap.addDebugInterceptors import net.vonforst.evmap.api.ChargepointApi import net.vonforst.evmap.api.ChargepointList import net.vonforst.evmap.api.FiltersSQLQuery +import net.vonforst.evmap.api.FullDownloadResult import net.vonforst.evmap.api.StringProvider import net.vonforst.evmap.api.mapPower import net.vonforst.evmap.api.mapPowerInverse 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 79fb44630..c4ceb5a2f 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 @@ -11,6 +11,7 @@ import net.vonforst.evmap.addDebugInterceptors import net.vonforst.evmap.api.ChargepointApi import net.vonforst.evmap.api.ChargepointList import net.vonforst.evmap.api.FiltersSQLQuery +import net.vonforst.evmap.api.FullDownloadResult import net.vonforst.evmap.api.StringProvider import net.vonforst.evmap.api.mapPower import net.vonforst.evmap.api.mapPowerInverse