Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement OpenStreetMap data source #290

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
093e1d1
basic implementation of OpenStreetMapApi
johan12345 Jul 2, 2023
0685004
fix getChargepointDetail for offline APIs
johan12345 Jul 9, 2023
3a5bdd1
OpenStreetMap API: implement some first filters
johan12345 Jul 9, 2023
3d97ae6
improve formatting when address or connectors are missing
johan12345 Jul 9, 2023
5d6840a
DB: make getChargeLocationById nullable
johan12345 Jul 9, 2023
0d61122
OSM: implement cost description
johan12345 Jul 9, 2023
82b14b7
OSM: implement address
johan12345 Jul 9, 2023
4f2e90c
OSM: implement getPhotos for images hosted at imgur
johan12345 Jul 9, 2023
003ca9e
OSM: add charger website
johan12345 Jul 9, 2023
a257b98
lazy loading for fullDownload
johan12345 Sep 17, 2023
e4c0dfd
OSM: enable realtime data
johan12345 Sep 17, 2023
6cdcac6
OSM: add tesla_supercharger_ccs
johan12345 Sep 17, 2023
76ed8e5
detail_view layout fixes for missing values
johan12345 Sep 17, 2023
a7b6f18
add total count in OSMDocument
johan12345 Sep 17, 2023
e4af6a5
OSM: implement progress indicator for full download
johan12345 Sep 26, 2023
e78eaa5
fix OnboardingFragment for OSM
johan12345 Nov 11, 2023
ba5381c
OSM: implement ReferenceData and network filter
johan12345 Nov 11, 2023
fc303b4
TeslaAvailabilityDetector: raise exception if chargepoints not known
johan12345 Nov 19, 2023
6c6e107
OSM: adjustments for AA/AAOS app
johan12345 Nov 12, 2023
5cfa2de
OSM: progress indicator improvements
johan12345 Nov 12, 2023
dd609ca
move to production webserver
johan12345 Nov 12, 2023
3b07cb2
Consistency: add "." to strings
johan12345 Nov 16, 2023
ddebe5b
OSM: reduce flashing progress bar when moving map during download
johan12345 Nov 26, 2023
417fbb1
OSM: try to speed up download process
johan12345 Nov 26, 2023
8196052
WIP: clustering in DB
johan12345 Dec 2, 2023
13a06eb
Increase margin to data source text (#361)
Altonss Sep 5, 2024
a76987a
fix imports
johan12345 Sep 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -58,6 +59,27 @@ interface ChargepointApi<out T : ReferenceData> {
* 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.
*/
suspend fun fullDownload(): FullDownloadResult<T>
}

interface StringProvider {
Expand All @@ -79,13 +101,19 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
)
)
}

"goingelectric" -> {
GoingElectricApiWrapper(
ctx.getString(
R.string.goingelectric_key
)
)
}

"openstreetmap" -> {
OpenStreetMapApiWrapper()
}

else -> throw IllegalArgumentException()
}
}
Expand All @@ -100,4 +128,20 @@ data class ChargepointList(val items: List<ChargepointListItem>, val isComplete:
companion object {
fun empty() = ChargepointList(emptyList(), true)
}
}

/**
* 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<out T : ReferenceData> {
val chargers: Sequence<ChargeLocation>
val progress: Float
val referenceData: T
}
1 change: 1 addition & 0 deletions app/src/main/java/net/vonforst/evmap/api/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> JSONArray.iterator(): Iterator<T> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -163,6 +167,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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -162,6 +166,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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -159,6 +160,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(): FullDownloadResult<GEReferenceData> {
throw NotImplementedError()
}

override suspend fun getChargepoints(
referenceData: ReferenceData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -130,6 +131,12 @@ class OpenChargeMapApiWrapper(

override val name = "OpenChargeMap.org"
override val id = "openchargemap"
override val supportsOnlineQueries = true
override val supportsFullDownload = false

override suspend fun fullDownload(): FullDownloadResult<OCMReferenceData> {
throw NotImplementedError()
}

private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
if (value == null || value.all) null else value.values.joinToString(",")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package net.vonforst.evmap.api.openstreetmap

import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
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

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
}
}

internal class OSMConverterFactory(val moshi: Moshi) : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
if (type.rawType != OSMDocument::class.java) return null

val instantAdapter = moshi.adapter(Instant::class.java)
val osmChargingStationAdapter = moshi.adapter(OSMChargingStation::class.java)
val longAdapter = moshi.adapter(Long::class.java)
return Converter<ResponseBody, OSMDocument> { body ->
val reader = JsonReader.of(body.source())
reader.beginObject()

var timestamp: Instant? = null
var doc: Sequence<OSMChargingStation>? = 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()
while (reader.hasNext()) {
yield(osmChargingStationAdapter.fromJson(reader)!!)
}
reader.endArray()
reader.close()
}
break
}
}
}
OSMDocument(timestamp!!, count!!, doc!!)
}
}
}
Loading