diff --git a/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt index e53cc4fa..c20f6d18 100644 --- a/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt +++ b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt @@ -33,7 +33,6 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus import net.vonforst.evmap.api.availability.ChargepointStatus import net.vonforst.evmap.api.availability.getAvailability import net.vonforst.evmap.api.createApi -import net.vonforst.evmap.api.goingelectric.GoingElectricApi import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper import net.vonforst.evmap.api.nameForPlugType import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper @@ -436,7 +435,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole LatLng.fromLocation(location), searchRadius, zoom = 16f, - emptyList() + null ) chargers = response.data?.filterIsInstance(ChargeLocation::class.java) chargers?.let { @@ -519,9 +518,10 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : var photo: Bitmap? = null private var availability: ChargeLocationStatus? = null - val apikey = ctx.getString(R.string.goingelectric_key) + val prefs = PreferenceDataSource(ctx) + private val db = AppDatabase.getInstance(carContext) private val api by lazy { - GoingElectricApi.create(apikey, context = ctx) + createApi(prefs.dataSource, ctx) } private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64) @@ -656,14 +656,13 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : private fun loadCharger() { lifecycleScope.launch { try { - val response = api.getChargepointDetail(chargerSparse.id) - charger = response.body()?.chargelocations?.get(0) as ChargeLocation + val response = api.getChargepointDetail(getReferenceData(), chargerSparse.id) + charger = response.data!! val photo = charger?.photos?.firstOrNull() photo?.let { val size = (carContext.resources.displayMetrics.density * 64).roundToInt() - val url = "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}" + - "&id=${photo.id}&size=${size}" + val url = photo.getUrl(size = size) val request = ImageRequest.Builder(carContext).data(url).build() this@ChargerDetailScreen.photo = (carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap @@ -680,6 +679,31 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : } } } + + private suspend fun getReferenceData(): ReferenceData { + val api = api + return when (api) { + is GoingElectricApiWrapper -> { + GEReferenceDataRepository( + api, + lifecycleScope, + db.geReferenceDataDao(), + prefs + ).getReferenceData().await() + } + is OpenChargeMapApiWrapper -> { + OCMReferenceDataRepository( + api, + lifecycleScope, + db.ocmReferenceDataDao(), + prefs + ).getReferenceData().await() + } + else -> { + throw RuntimeException("no reference data implemented") + } + } + } } fun carAvailabilityColor(status: List): CarColor { 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 cdb5dc54..ce8f8672 100644 --- a/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt @@ -14,7 +14,7 @@ interface ChargepointApi { referenceData: ReferenceData, bounds: LatLngBounds, zoom: Float, - filters: FilterValues + filters: FilterValues? ): Resource> suspend fun getChargepointsRadius( @@ -22,7 +22,7 @@ interface ChargepointApi { location: LatLng, radius: Int, zoom: Float, - filters: FilterValues + filters: FilterValues? ): Resource> suspend fun getChargepointDetail( 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 bc4da685..9b00508b 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 @@ -132,42 +132,44 @@ class GoingElectricApiWrapper( referenceData: ReferenceData, bounds: LatLngBounds, zoom: Float, - filters: FilterValues + filters: FilterValues? ): Resource> { - val freecharging = filters.getBooleanValue("freecharging")!! - val freeparking = filters.getBooleanValue("freeparking")!! - val open247 = filters.getBooleanValue("open_247")!! - val barrierfree = filters.getBooleanValue("barrierfree")!! - val excludeFaults = filters.getBooleanValue("exclude_faults")!! - val minPower = filters.getSliderValue("min_power")!! - val minConnectors = filters.getSliderValue("min_connectors")!! + val freecharging = filters?.getBooleanValue("freecharging") + val freeparking = filters?.getBooleanValue("freeparking") + val open247 = filters?.getBooleanValue("open_247") + val barrierfree = filters?.getBooleanValue("barrierfree") + val excludeFaults = filters?.getBooleanValue("exclude_faults") + val minPower = filters?.getSliderValue("min_power") + val minConnectors = filters?.getSliderValue("min_connectors") - val connectorsVal = filters.getMultipleChoiceValue("connectors")!! - if (connectorsVal.values.isEmpty() && !connectorsVal.all) { - // no connectors chosen - return Resource.success(emptyList()) + val connectorsVal = filters?.getMultipleChoiceValue("connectors") + if (connectorsVal != null) { + if (connectorsVal.values.isEmpty() && !connectorsVal.all) { + // no connectors chosen + return Resource.success(emptyList()) + } + connectorsVal.values = connectorsVal.values.mapNotNull { + GEChargepoint.convertTypeToGE(it) + }.toMutableSet() } - connectorsVal.values = connectorsVal.values.mapNotNull { - GEChargepoint.convertTypeToGE(it) - }.toMutableSet() val connectors = formatMultipleChoice(connectorsVal) - val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!! - if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) { + val chargeCardsVal = filters?.getMultipleChoiceValue("chargecards") + if (chargeCardsVal != null && chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) { // no chargeCards chosen return Resource.success(emptyList()) } val chargeCards = formatMultipleChoice(chargeCardsVal) - val networksVal = filters.getMultipleChoiceValue("networks")!! - if (networksVal.values.isEmpty() && !networksVal.all) { + val networksVal = filters?.getMultipleChoiceValue("networks") + if (networksVal != null && networksVal.values.isEmpty() && !networksVal.all) { // no networks chosen return Resource.success(emptyList()) } val networks = formatMultipleChoice(networksVal) - val categoriesVal = filters.getMultipleChoiceValue("categories")!! - if (categoriesVal.values.isEmpty() && !categoriesVal.all) { + val categoriesVal = filters?.getMultipleChoiceValue("categories") + if (categoriesVal != null && categoriesVal.values.isEmpty() && !categoriesVal.all) { // no categories chosen return Resource.success(emptyList()) } @@ -175,7 +177,7 @@ class GoingElectricApiWrapper( // do not use clustering if filters need to be applied locally. val useClustering = zoom < 13 - val geClusteringAvailable = minConnectors <= 1 + val geClusteringAvailable = minConnectors == null || minConnectors <= 1 val useGeClustering = useClustering && geClusteringAvailable val clusterDistance = if (useClustering) getClusterDistance(zoom) else null @@ -192,12 +194,12 @@ class GoingElectricApiWrapper( clustering = useGeClustering, zoom = zoom, clusterDistance = clusterDistance, - freecharging = freecharging, - minPower = minPower, - freeparking = freeparking, - open247 = open247, - barrierfree = barrierfree, - excludeFaults = excludeFaults, + freecharging = freecharging ?: false, + minPower = minPower ?: 0, + freeparking = freeparking ?: false, + open247 = open247 ?: false, + barrierfree = barrierfree ?: false, + excludeFaults = excludeFaults ?: false, plugs = connectors, chargecards = chargeCards, networks = networks, @@ -216,38 +218,136 @@ class GoingElectricApiWrapper( } } while (startkey != null && startkey < 10000) - var result = data.filter { it -> - // apply filters which GoingElectric does not support natively - if (it is GEChargeLocation) { - it.chargepoints - .filter { it.power >= minPower } - .filter { if (!connectorsVal.all) it.type in connectorsVal.values else true } - .sumOf { it.count } >= minConnectors - } else { - true - } - }.map { it.convert(apikey) } // convert to common model - if (!geClusteringAvailable && useClustering) { - // apply local clustering if server side clustering is not available - Dispatchers.IO.run { - result = cluster(result, zoom, clusterDistance!!) - } - } + var result = postprocessResult(data, minPower, connectorsVal, minConnectors, zoom) return Resource.success(result) } - private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) = - if (connectorsVal.all) null else connectorsVal.values.joinToString(",") + private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) = + if (value == null || value.all) null else value.values.joinToString(",") override suspend fun getChargepointsRadius( referenceData: ReferenceData, location: LatLng, radius: Int, zoom: Float, - filters: FilterValues + filters: FilterValues? ): Resource> { - TODO("Not yet implemented") + val freecharging = filters?.getBooleanValue("freecharging") + val freeparking = filters?.getBooleanValue("freeparking") + val open247 = filters?.getBooleanValue("open_247") + val barrierfree = filters?.getBooleanValue("barrierfree") + val excludeFaults = filters?.getBooleanValue("exclude_faults") + val minPower = filters?.getSliderValue("min_power") + val minConnectors = filters?.getSliderValue("min_connectors") + + val connectorsVal = filters?.getMultipleChoiceValue("connectors") + if (connectorsVal != null) { + if (connectorsVal.values.isEmpty() && !connectorsVal.all) { + // no connectors chosen + return Resource.success(emptyList()) + } + connectorsVal.values = connectorsVal.values.mapNotNull { + GEChargepoint.convertTypeToGE(it) + }.toMutableSet() + } + val connectors = formatMultipleChoice(connectorsVal) + + val chargeCardsVal = filters?.getMultipleChoiceValue("chargecards") + if (chargeCardsVal != null && chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) { + // no chargeCards chosen + return Resource.success(emptyList()) + } + val chargeCards = formatMultipleChoice(chargeCardsVal) + + val networksVal = filters?.getMultipleChoiceValue("networks") + if (networksVal != null && networksVal.values.isEmpty() && !networksVal.all) { + // no networks chosen + return Resource.success(emptyList()) + } + val networks = formatMultipleChoice(networksVal) + + val categoriesVal = filters?.getMultipleChoiceValue("categories") + if (categoriesVal != null && categoriesVal.values.isEmpty() && !categoriesVal.all) { + // no categories chosen + return Resource.success(emptyList()) + } + val categories = formatMultipleChoice(categoriesVal) + + // do not use clustering if filters need to be applied locally. + val useClustering = zoom < 13 + val geClusteringAvailable = minConnectors == null || minConnectors <= 1 + val useGeClustering = useClustering && geClusteringAvailable + val clusterDistance = if (useClustering) getClusterDistance(zoom) else null + + var startkey: Int? = null + val data = mutableListOf() + do { + // load all pages of the response + try { + val response = api.getChargepointsRadius( + location.latitude, location.longitude, radius, + clustering = useGeClustering, + zoom = zoom, + clusterDistance = clusterDistance, + freecharging = freecharging ?: false, + minPower = minPower ?: 0, + freeparking = freeparking ?: false, + open247 = open247 ?: false, + barrierfree = barrierfree ?: false, + excludeFaults = excludeFaults ?: false, + plugs = connectors, + chargecards = chargeCards, + networks = networks, + categories = categories, + startkey = startkey + ) + if (!response.isSuccessful || response.body()!!.status != "ok") { + return Resource.error(response.message(), null) + } else { + val body = response.body()!! + data.addAll(body.chargelocations) + startkey = body.startkey + } + } catch (e: IOException) { + return Resource.error(e.message, null) + } + } while (startkey != null && startkey < 10000) + + val result = postprocessResult(data, minPower, connectorsVal, minConnectors, zoom) + return Resource.success(result) + } + + private fun postprocessResult( + chargers: List, + minPower: Int?, + connectorsVal: MultipleChoiceFilterValue?, + minConnectors: Int?, + zoom: Float + ): List { + // apply filters which GoingElectric does not support natively + var result = chargers.filter { it -> + if (it is GEChargeLocation) { + it.chargepoints + .filter { it.power >= (minPower ?: 0) } + .filter { if (connectorsVal != null && !connectorsVal.all) it.type in connectorsVal.values else true } + .sumOf { it.count } >= (minConnectors ?: 0) + } else { + true + } + }.map { it.convert(apikey) } + + // apply clustering + val useClustering = zoom < 13 + val geClusteringAvailable = minConnectors == null || minConnectors <= 1 + val clusterDistance = if (useClustering) getClusterDistance(zoom) else null + if (!geClusteringAvailable && useClustering) { + // apply local clustering if server side clustering is not available + Dispatchers.IO.run { + result = cluster(result, zoom, clusterDistance!!) + } + } + return result } override suspend fun getChargepointDetail( 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 8bf4c4a8..ca2eef7c 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 @@ -33,6 +33,20 @@ interface OpenChargeMapApi { @Query("maxresults") maxresults: Int = 500, ): Response> + @GET("poi/") + suspend fun getChargepointsRadius( + @Query("latitude") latitude: Double, + @Query("longitude") longitude: Double, + @Query("distance") distance: Double, + @Query("distanceunit") distanceUnit: String = "KM", + @Query("connectiontypeid") plugs: String? = null, + @Query("minpowerkw") minPower: Double? = null, + @Query("operatorid") operators: String? = null, + @Query("compact") compact: Boolean = true, + @Query("statustypeid") statusType: String? = null, + @Query("maxresults") maxresults: Int = 500, + ): Response> + @GET("poi/") suspend fun getChargepointDetail( @Query("chargepointid") id: Long, @@ -91,8 +105,8 @@ class OpenChargeMapApiWrapper( override fun getName() = "OpenChargeMap.org" - private fun formatMultipleChoice(value: MultipleChoiceFilterValue) = - if (value.all) null else value.values.joinToString(",") + private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) = + if (value == null || value.all) null else value.values.joinToString(",") // Unknown, Currently Available, Currently In Use, Operational, Partly Operational private val noFaultStatuses = "0,10,20,50,75" @@ -101,23 +115,23 @@ class OpenChargeMapApiWrapper( referenceData: ReferenceData, bounds: LatLngBounds, zoom: Float, - filters: FilterValues, + filters: FilterValues?, ): Resource> { val referenceData = referenceData as OCMReferenceData - val minPower = filters.getSliderValue("min_power")!!.toDouble() - val minConnectors = filters.getSliderValue("min_connectors")!! - val excludeFaults = filters.getBooleanValue("exclude_faults")!! + val minPower = filters?.getSliderValue("min_power")?.toDouble() + val minConnectors = filters?.getSliderValue("min_connectors") + val excludeFaults = filters?.getBooleanValue("exclude_faults") - val connectorsVal = filters.getMultipleChoiceValue("connectors")!! - if (connectorsVal.values.isEmpty() && !connectorsVal.all) { + val connectorsVal = filters?.getMultipleChoiceValue("connectors") + if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) { // no connectors chosen return Resource.success(emptyList()) } val connectors = formatMultipleChoice(connectorsVal) - val operatorsVal = filters.getMultipleChoiceValue("operators")!! - if (operatorsVal.values.isEmpty() && !operatorsVal.all) { + val operatorsVal = filters?.getMultipleChoiceValue("operators")!! + if (operatorsVal != null && operatorsVal.values.isEmpty() && !operatorsVal.all) { // no operators chosen return Resource.success(emptyList()) } @@ -131,28 +145,20 @@ class OpenChargeMapApiWrapper( minPower = minPower, plugs = connectors, operators = operators, - statusType = if (excludeFaults) noFaultStatuses else null + statusType = if (excludeFaults == true) noFaultStatuses else null ) if (!response.isSuccessful) { return Resource.error(response.message(), null) } - var result = response.body()!!.filter { it -> - // apply filters which OCM does not support natively - it.connections - .filter { it.power == null || it.power >= minPower } - .filter { if (!connectorsVal.all) it.connectionTypeId in connectorsVal.values.map { it.toLong() } else true } - .sumOf { it.quantity ?: 0 } >= minConnectors - }.map { it.convert(referenceData) }.distinct() as List - - val useClustering = zoom < 13 - val clusterDistance = if (useClustering) getClusterDistance(zoom) else null - if (useClustering) { - Dispatchers.IO.run { - result = cluster(result, zoom, clusterDistance!!) - } - } - + var result = postprocessResult( + response.body()!!, + minPower, + connectorsVal, + minConnectors, + referenceData, + zoom + ) return Resource.success(result) } @@ -161,9 +167,76 @@ class OpenChargeMapApiWrapper( location: LatLng, radius: Int, zoom: Float, - filters: FilterValues + filters: FilterValues? ): Resource> { - TODO("Not yet implemented") + val referenceData = referenceData as OCMReferenceData + + val minPower = filters?.getSliderValue("min_power")?.toDouble() + val minConnectors = filters?.getSliderValue("min_connectors") + val excludeFaults = filters?.getBooleanValue("exclude_faults") + + val connectorsVal = filters?.getMultipleChoiceValue("connectors") + if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) { + // no connectors chosen + return Resource.success(emptyList()) + } + val connectors = formatMultipleChoice(connectorsVal) + + val operatorsVal = filters?.getMultipleChoiceValue("operators") + if (operatorsVal != null && operatorsVal.values.isEmpty() && !operatorsVal.all) { + // no operators chosen + return Resource.success(emptyList()) + } + val operators = formatMultipleChoice(operatorsVal) + + val response = api.getChargepointsRadius( + location.latitude, location.longitude, + radius.toDouble(), + minPower = minPower, + plugs = connectors, + operators = operators, + statusType = if (excludeFaults == true) noFaultStatuses else null + ) + if (!response.isSuccessful) { + return Resource.error(response.message(), null) + } + + val result = postprocessResult( + response.body()!!, + minPower, + connectorsVal, + minConnectors, + referenceData, + zoom + ) + return Resource.success(result) + } + + private fun postprocessResult( + chargers: List, + minPower: Double?, + connectorsVal: MultipleChoiceFilterValue?, + minConnectors: Int?, + referenceData: OCMReferenceData, + zoom: Float + ): List { + // apply filters which OCM does not support natively + var result = chargers.filter { it -> + it.connections + .filter { it.power == null || it.power >= (minPower ?: 0.0) } + .filter { if (connectorsVal != null && !connectorsVal.all) it.connectionTypeId in connectorsVal.values.map { it.toLong() } else true } + .sumOf { it.quantity ?: 0 } >= (minConnectors ?: 0) + }.map { it.convert(referenceData) }.distinct() as List + + // apply clustering + val useClustering = zoom < 13 + if (useClustering) { + val clusterDistance = getClusterDistance(zoom) + Dispatchers.IO.run { + result = cluster(result, zoom, clusterDistance!!) + } + } + return result } override suspend fun getChargepointDetail(