From 04723a5c588cb82e536c43907bd1f7f8c476814b Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Fri, 21 Jan 2022 00:41:03 +0100 Subject: [PATCH 1/8] Add OSMChargingStation model This allows deserializing a single model from JSON. --- .../api/openstreetmap/OpenStreetMapModel.kt | 87 +++++++++++++++++++ .../net/vonforst/evmap/model/ChargersModel.kt | 3 + .../openstreetmap/OpenStreetMapModelTest.kt | 57 ++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt create mode 100644 app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt 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 new file mode 100644 index 00000000..56082cc9 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt @@ -0,0 +1,87 @@ +package net.vonforst.evmap.api.openstreetmap + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import net.vonforst.evmap.model.* +import java.time.Instant +import java.time.ZonedDateTime + +@JsonClass(generateAdapter = true) +data class OSMChargingStation( + // Unique numeric ID + val id: Long, + // Latitude (WGS84) + val lat: Double, + // Longitude (WGS84) + val lon: Double, + // Timestamp of last update + @Json(name = "timestamp") val lastUpdateTimestamp: ZonedDateTime, + // Numeric, monotonically increasing version number + val version: Int, + // User that last modified this POI + val user: String, + // Raw key-value OSM tags + val tags: Map, +) { + /** + * Convert the [OSMChargingStation] to a generic [ChargeLocation]. + * + * The [dataFetchTimestamp] should be set to the timestamp when the data was last + * refreshed / fetched from OSM. It will always be later than the [lastUpdateTimestamp], + * which contains the timestamp when the data was last _edited_ in OSM. + */ + fun convert(dataFetchTimestamp: Instant) = ChargeLocation( + id, + "openstreetmap", + getName(), + Coordinate(lat, lon), + Address("", "", "", ""), // TODO: Can we determine this with overpass? + getChargepoints(), + tags["network"], + "https://www.openstreetmap.org/node/$id", + "https://www.openstreetmap.org/edit?node=$id", + null, + false, // We don't know + null, // What does this entail? + tags["operator"], + tags["description"], + null, + null, + null, + null, + getOpeningHours(), + null, + "© OpenStreetMap contributors", + null, + dataFetchTimestamp, + true, + ) + + /** + * Return the name for this charging station. + */ + private fun getName(): String { + // Ideally this station has a name. + // If not, fall back to the operator. + // If that is missing as well, use a generic "Charging Station" string. + return tags["name"] + ?: tags["operator"] + ?: "Charging Station"; + } + + /** + * Return the chargepoints for this charging station. + */ + private fun getChargepoints(): List { + // TODO + return emptyList() + } + + private fun getOpeningHours(): OpeningHours? { + if (tags["opening_hours"] == "24/7") { + return OpeningHours(true, null, null) + } + // TODO: Try to convert other formats as well + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt index 522e0733..3a3ca34d 100644 --- a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt +++ b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt @@ -307,6 +307,9 @@ data class Address( } } +/** + * One socket with a certain power, which may be available multiple times at a ChargeLocation. + */ @Parcelize @JsonClass(generateAdapter = true) data class Chargepoint( diff --git a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt new file mode 100644 index 00000000..607ea38d --- /dev/null +++ b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt @@ -0,0 +1,57 @@ +package net.vonforst.evmap.api.openstreetmap + +import com.squareup.moshi.Moshi +import net.vonforst.evmap.api.openchargemap.ZonedDateTimeAdapter +import org.junit.Assert +import org.junit.Test +import java.time.Month +import java.time.ZoneOffset + +const val JSON_SINGLE = "{\n" + +" \"id\": 9084665785,\n" + +" \"lat\": 46.1137872,\n" + +" \"lon\": 7.0778715,\n" + +" \"timestamp\": \"2021-09-12T19:36:56Z\",\n" + +" \"version\": 1,\n" + +" \"user\": \"Voonosm\",\n" + +" \"tags\": {\n" + +" \"amenity\": \"charging_station\",\n" + +" \"authentication:app\": \"yes\",\n" + +" \"authentication:contactless\": \"yes\",\n" + +" \"bicycle\": \"no\",\n" + +" \"capacity\": \"2\",\n" + +" \"cover\": \"no\",\n" + +" \"fee\": \"yes\",\n" + +" \"motorcar\": \"yes\",\n" + +" \"network\": \"Swisscharge\",\n" + +" \"opening_hours\": \"24/7\",\n" + +" \"operator\": \"GOFAST\",\n" + +" \"parking:fee\": \"no\",\n" + +" \"payment:credit_cards\": \"yes\",\n" + +" \"socket:chademo\": \"2\",\n" + +" \"socket:chademo:output\": \"60 kW\",\n" + +" \"socket:type2\": \"1\",\n" + +" \"socket:type2:output\": \"22 kW\",\n" + +" \"socket:type2_combo\": \"2\",\n" + +" \"socket:type2_combo:output\": \"150 kW\"\n" + +" }\n" + +"}" + +class OpenStreetMapModelTest { + @Test + fun parseFromJson() { + val moshi = Moshi.Builder() + .add(ZonedDateTimeAdapter()) + .build() + val deserialized = moshi + .adapter(OSMChargingStation::class.java) + .fromJson(JSON_SINGLE)!! + Assert.assertEquals(9084665785, deserialized.id) + Assert.assertEquals(1, deserialized.version) + Assert.assertEquals(12, deserialized.lastUpdateTimestamp.dayOfMonth) + Assert.assertEquals(Month.SEPTEMBER, deserialized.lastUpdateTimestamp.month) + Assert.assertEquals(36, deserialized.lastUpdateTimestamp.minute) + Assert.assertEquals(ZoneOffset.UTC, deserialized.lastUpdateTimestamp.offset) + Assert.assertEquals("Swisscharge", deserialized.tags["network"]) + } +} \ No newline at end of file From 392c2ecc9af7cea6f4e0653e5d0e446517f32b6c Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sun, 23 Jan 2022 01:26:37 +0100 Subject: [PATCH 2/8] OSM: First incomplete parsing of chargepoints --- .../api/openstreetmap/OpenStreetMapModel.kt | 66 +++++++++++++++++-- .../openstreetmap/OpenStreetMapModelTest.kt | 39 ++++++++--- 2 files changed, 93 insertions(+), 12 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 56082cc9..792e2111 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 @@ -3,9 +3,43 @@ package net.vonforst.evmap.api.openstreetmap import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import net.vonforst.evmap.model.* +import okhttp3.internal.immutableListOf import java.time.Instant import java.time.ZonedDateTime +// List of all OSM socket types that are relevant for EVs: +// https://wiki.openstreetmap.org/wiki/Key:socket +val SOCKET_TYPES = immutableListOf( + // Type 1 + "type1", + "type1_combo", + + // Type 2 + "type2", // Type2 socket + "type2_cable", // Type2 with a fixed attached cable + "type2_combo", // CCS + + // CHAdeMO + "chademo", + + // Tesla + "tesla_standard", + "tesla_supercharger", + + // CEE + "cee_blue", // Also known as "caravan socket" + "cee_red_16a", + "cee_red_32a", + "cee_red_63a", + "cee_red_125a", + + // Switzerland + "sev1011_t13", + "sev1011_t15", + "sev1011_t23", + "sev1011_t25", +) + @JsonClass(generateAdapter = true) data class OSMChargingStation( // Unique numeric ID @@ -73,15 +107,39 @@ data class OSMChargingStation( * Return the chargepoints for this charging station. */ private fun getChargepoints(): List { - // TODO - return emptyList() + // Note: In OSM, the chargepoints are mapped as "socket: = " + val chargepoints = mutableListOf() + for (socket in SOCKET_TYPES) { + val count = try { + (this.tags["socket:$socket"] ?: "0").toInt() + } catch (e: NumberFormatException) { + 0 + } + if (count > 0) { + chargepoints.add(Chargepoint(socket, 42.0, count)) + // TODO: Power parsing + } + } + return chargepoints } private fun getOpeningHours(): OpeningHours? { - if (tags["opening_hours"] == "24/7") { + val rawOpeningHours = tags["opening_hours"] ?: return null + + // Handle the simple 24/7 case + if (rawOpeningHours == "24/7") { return OpeningHours(true, null, null) } - // TODO: Try to convert other formats as well + + // TODO: Try to convert other formats as well? + // + // Note: The current {@link OpeningHours} format is not flexible enough to handle + // all rules that OSM can represent and might need to be updated. + // This library could help: https://github.com/simonpoole/OpeningHoursParser + // + // Alternatively, with the opening-hours-evaluator library + // https://github.com/leonardehrenfried/opening-hours-evaluator + // we could implement an "open now" feature. return null } } \ No newline at end of file diff --git a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt index 607ea38d..78bf2c45 100644 --- a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt +++ b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt @@ -2,8 +2,9 @@ package net.vonforst.evmap.api.openstreetmap import com.squareup.moshi.Moshi import net.vonforst.evmap.api.openchargemap.ZonedDateTimeAdapter -import org.junit.Assert +import org.junit.Assert.assertEquals import org.junit.Test +import java.time.Instant import java.time.Month import java.time.ZoneOffset @@ -46,12 +47,34 @@ class OpenStreetMapModelTest { val deserialized = moshi .adapter(OSMChargingStation::class.java) .fromJson(JSON_SINGLE)!! - Assert.assertEquals(9084665785, deserialized.id) - Assert.assertEquals(1, deserialized.version) - Assert.assertEquals(12, deserialized.lastUpdateTimestamp.dayOfMonth) - Assert.assertEquals(Month.SEPTEMBER, deserialized.lastUpdateTimestamp.month) - Assert.assertEquals(36, deserialized.lastUpdateTimestamp.minute) - Assert.assertEquals(ZoneOffset.UTC, deserialized.lastUpdateTimestamp.offset) - Assert.assertEquals("Swisscharge", deserialized.tags["network"]) + assertEquals(9084665785, deserialized.id) + assertEquals(1, deserialized.version) + assertEquals(12, deserialized.lastUpdateTimestamp.dayOfMonth) + assertEquals(Month.SEPTEMBER, deserialized.lastUpdateTimestamp.month) + assertEquals(36, deserialized.lastUpdateTimestamp.minute) + assertEquals(ZoneOffset.UTC, deserialized.lastUpdateTimestamp.offset) + assertEquals("Swisscharge", deserialized.tags["network"]) + } + + @Test + fun convert() { + val osmChargingStation = Moshi.Builder() + .add(ZonedDateTimeAdapter()) + .build() + .adapter(OSMChargingStation::class.java) + .fromJson(JSON_SINGLE)!! + val now = Instant.now() + val chargeLocation = osmChargingStation.convert(now) + + // Basics + assertEquals("openstreetmap", chargeLocation.dataSource) + assertEquals("https://www.openstreetmap.org/node/9084665785", chargeLocation.url) + assertEquals(true, chargeLocation.openinghours?.twentyfourSeven) + assertEquals("GOFAST", chargeLocation.name) // Fallback to operator because name is not set + assertEquals(now, chargeLocation.timeRetrieved) + + // Chargepoints + assertEquals(3, chargeLocation.chargepoints.size) + assertEquals(5, chargeLocation.chargepoints.sumOf { it.count }) } } \ No newline at end of file From 9548748a64856c7357592d34cd0aadee6d0c469e Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Wed, 26 Jan 2022 23:37:09 +0100 Subject: [PATCH 3/8] OSM: Map socket types to EVMap Chargepoint types --- .../api/openstreetmap/OpenStreetMapModel.kt | 60 ++++++++++++------- .../openstreetmap/OpenStreetMapModelTest.kt | 8 ++- 2 files changed, 47 insertions(+), 21 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 792e2111..69dfda00 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 @@ -7,37 +7,55 @@ import okhttp3.internal.immutableListOf import java.time.Instant import java.time.ZonedDateTime +private data class OsmSocket( + // The OSM socket name (e.g. "type2_combo") + val osmSocketName: String, + // The socket identifier used in EVMap. + // TODO: This should probably be a separate enum-like type, not a string. + val evmapKey: String?, +) { + /** + * Return the OSM socket base tag (e.g. "socket:type2_combo"). + */ + fun osmSocketBaseTag(): String { + return "socket:${this.osmSocketName}" + } +} + // List of all OSM socket types that are relevant for EVs: // https://wiki.openstreetmap.org/wiki/Key:socket -val SOCKET_TYPES = immutableListOf( +private val SOCKET_TYPES = immutableListOf( // Type 1 - "type1", - "type1_combo", + OsmSocket("type1", Chargepoint.TYPE_1), + OsmSocket("type1_combo", Chargepoint.CCS_TYPE_1), // Type 2 - "type2", // Type2 socket - "type2_cable", // Type2 with a fixed attached cable - "type2_combo", // CCS + OsmSocket("type2", Chargepoint.TYPE_2_SOCKET), // Type2 socket (or unknown) + OsmSocket("type2_cable", Chargepoint.TYPE_2_PLUG), // Type2 plug + OsmSocket("type2_combo", Chargepoint.CCS_TYPE_2), // CCS // CHAdeMO - "chademo", + OsmSocket("chademo", Chargepoint.CHADEMO), // Tesla - "tesla_standard", - "tesla_supercharger", + OsmSocket("tesla_standard", null), + OsmSocket("tesla_supercharger", Chargepoint.SUPERCHARGER), // CEE - "cee_blue", // Also known as "caravan socket" - "cee_red_16a", - "cee_red_32a", - "cee_red_63a", - "cee_red_125a", + OsmSocket("cee_blue", Chargepoint.CEE_BLAU), // Also known as "caravan socket" + OsmSocket("cee_red_16a", Chargepoint.CEE_ROT), + OsmSocket("cee_red_32a", Chargepoint.CEE_ROT), + OsmSocket("cee_red_63a", Chargepoint.CEE_ROT), + OsmSocket("cee_red_125a", Chargepoint.CEE_ROT), + + // Europe + OsmSocket("schuko", Chargepoint.SCHUKO), // Switzerland - "sev1011_t13", - "sev1011_t15", - "sev1011_t23", - "sev1011_t25", + OsmSocket("sev1011_t13", null), + OsmSocket("sev1011_t15", null), + OsmSocket("sev1011_t23", null), + OsmSocket("sev1011_t25", null), ) @JsonClass(generateAdapter = true) @@ -111,12 +129,14 @@ data class OSMChargingStation( val chargepoints = mutableListOf() for (socket in SOCKET_TYPES) { val count = try { - (this.tags["socket:$socket"] ?: "0").toInt() + (this.tags[socket.osmSocketBaseTag()] ?: "0").toInt() } catch (e: NumberFormatException) { 0 } if (count > 0) { - chargepoints.add(Chargepoint(socket, 42.0, count)) + if (socket.evmapKey != null) { + chargepoints.add(Chargepoint(socket.evmapKey, null, count)) + } // TODO: Power parsing } } diff --git a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt index 78bf2c45..979b0acf 100644 --- a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt +++ b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt @@ -2,6 +2,7 @@ package net.vonforst.evmap.api.openstreetmap import com.squareup.moshi.Moshi import net.vonforst.evmap.api.openchargemap.ZonedDateTimeAdapter +import net.vonforst.evmap.model.Chargepoint import org.junit.Assert.assertEquals import org.junit.Test import java.time.Instant @@ -75,6 +76,11 @@ class OpenStreetMapModelTest { // Chargepoints assertEquals(3, chargeLocation.chargepoints.size) - assertEquals(5, chargeLocation.chargepoints.sumOf { it.count }) + val ccs = chargeLocation.chargepoints.single { it.type == Chargepoint.CCS_TYPE_2 } + val type2 = chargeLocation.chargepoints.single { it.type == Chargepoint.TYPE_2_SOCKET } + val chademo = chargeLocation.chargepoints.single { it.type == Chargepoint.CHADEMO } + assertEquals(2, ccs.count) + assertEquals(1, type2.count) + assertEquals(2, chademo.count) } } \ No newline at end of file From 038da0856ee41163d4c83a25e325cbd608cc6bf8 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Fri, 28 Jan 2022 23:38:26 +0100 Subject: [PATCH 4/8] OSM: Implement output power parsing --- .../api/openstreetmap/OpenStreetMapModel.kt | 27 +++++++++++++++++-- .../openstreetmap/OpenStreetMapModelTest.kt | 27 +++++++++++++++++++ 2 files changed, 52 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 69dfda00..d2801a6b 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 @@ -135,9 +135,9 @@ data class OSMChargingStation( } if (count > 0) { if (socket.evmapKey != null) { - chargepoints.add(Chargepoint(socket.evmapKey, null, count)) + val outputPower = parseOutputPower(this.tags["${socket.osmSocketBaseTag()}:output"]) + chargepoints.add(Chargepoint(socket.evmapKey, outputPower, count)) } - // TODO: Power parsing } } return chargepoints @@ -162,4 +162,27 @@ data class OSMChargingStation( // we could implement an "open now" feature. return null } + + companion object { + /** + * Parse raw OSM output power. + * + * The proper format to map output power for an EV charging station is " kW", + * for example "22 kW" or "3.7 kW". Some fields in the wild are tagged with the unit "kVA" + * instead of "kW", those can be treated as equivalent. + * + * Sometimes people also mapped plain numbers (e.g. 7000, I assume that's 7 kW), + * ranges (5,5 - 11 kW, huh?) or even current (32 A), which is wrong. If we cannot parse, + * just ignore the field. + */ + fun parseOutputPower(rawOutput: String?): Double? { + if (rawOutput == null) { + return null; + } + val pattern = Regex("([0-9.,]+)\\s*(kW|kVA)", setOf(RegexOption.IGNORE_CASE)) + val matchResult = pattern.matchEntire(rawOutput) ?: return null + val numberString = matchResult.groupValues[1].replace(',', '.') + return numberString.toDoubleOrNull() + } + } } \ No newline at end of file diff --git a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt index 979b0acf..0fd888cc 100644 --- a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt +++ b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt @@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi import net.vonforst.evmap.api.openchargemap.ZonedDateTimeAdapter import net.vonforst.evmap.model.Chargepoint import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Test import java.time.Instant import java.time.Month @@ -80,7 +81,33 @@ class OpenStreetMapModelTest { val type2 = chargeLocation.chargepoints.single { it.type == Chargepoint.TYPE_2_SOCKET } val chademo = chargeLocation.chargepoints.single { it.type == Chargepoint.CHADEMO } assertEquals(2, ccs.count) + assertEquals(150.0, ccs.power) assertEquals(1, type2.count) + assertEquals(22.0, type2.power) assertEquals(2, chademo.count) + assertEquals(60.0, chademo.power) + } + + @Test + fun parseOutputPower() { + // Null input -> null output + assertNull(OSMChargingStation.parseOutputPower(null)) + + // Invalid input -> null output + assertNull(OSMChargingStation.parseOutputPower("")) + assertNull(OSMChargingStation.parseOutputPower("a")) + assertNull(OSMChargingStation.parseOutputPower("22 A")) + + // Invalid number -> null output + assertNull(OSMChargingStation.parseOutputPower("22.0.1 kW")) + + // Valid output power values + assertEquals(22.0, OSMChargingStation.parseOutputPower("22 kW")) + assertEquals(22.0, OSMChargingStation.parseOutputPower("22 kVA")) + assertEquals(22.0, OSMChargingStation.parseOutputPower("22. kW")) + assertEquals(22.0, OSMChargingStation.parseOutputPower("22.0 kW")) + assertEquals(22.0, OSMChargingStation.parseOutputPower("22,0 kW")) + assertEquals(22.0, OSMChargingStation.parseOutputPower("22kW")) + assertEquals(22.0, OSMChargingStation.parseOutputPower("22 kW")) } } \ No newline at end of file From 322d6bea07e62be73048761281b6381cd4fc4619 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sat, 29 Jan 2022 00:07:12 +0100 Subject: [PATCH 5/8] OSM: Parse cost --- .../api/openstreetmap/OpenStreetMapModel.kt | 16 +++++++++++++++- .../api/openstreetmap/OpenStreetMapModelTest.kt | 4 ++++ 2 files changed, 19 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 d2801a6b..f6d0f3ea 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 @@ -102,7 +102,7 @@ data class OSMChargingStation( null, null, getOpeningHours(), - null, + getCost(), "© OpenStreetMap contributors", null, dataFetchTimestamp, @@ -163,6 +163,20 @@ data class OSMChargingStation( return null } + private fun getCost(): Cost? { + val freecharging = when (tags["fee"]?.lowercase()) { + "yes", "y" -> false + "no", "n" -> true + else -> null + } + val freeparking = when (tags["parking:fee"]?.lowercase()) { + "no", "n" -> true + "yes", "y", "interval" -> false + else -> null + } + return Cost(freecharging, freeparking) + } + companion object { /** * Parse raw OSM output power. diff --git a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt index 0fd888cc..ae2a2eb7 100644 --- a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt +++ b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt @@ -75,6 +75,10 @@ class OpenStreetMapModelTest { assertEquals("GOFAST", chargeLocation.name) // Fallback to operator because name is not set assertEquals(now, chargeLocation.timeRetrieved) + // Cost + assertEquals(false, chargeLocation.cost?.freecharging) + assertEquals(true, chargeLocation.cost?.freeparking) + // Chargepoints assertEquals(3, chargeLocation.chargepoints.size) val ccs = chargeLocation.chargepoints.single { it.type == Chargepoint.CCS_TYPE_2 } From ab30cd6eabbc3970174c71f5a9e6d52e5d2a0eab Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sun, 30 Jan 2022 13:05:31 +0100 Subject: [PATCH 6/8] OSM: Set barrierFree to proper value --- .../net/vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt | 2 +- .../vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt | 1 + 2 files changed, 2 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 f6d0f3ea..9cf8a933 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 @@ -94,7 +94,7 @@ data class OSMChargingStation( "https://www.openstreetmap.org/edit?node=$id", null, false, // We don't know - null, // What does this entail? + tags["authentication:none"] == "yes", tags["operator"], tags["description"], null, diff --git a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt index ae2a2eb7..1a79b585 100644 --- a/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt +++ b/app/src/test/java/net/vonforst/evmap/api/openstreetmap/OpenStreetMapModelTest.kt @@ -73,6 +73,7 @@ class OpenStreetMapModelTest { assertEquals("https://www.openstreetmap.org/node/9084665785", chargeLocation.url) assertEquals(true, chargeLocation.openinghours?.twentyfourSeven) assertEquals("GOFAST", chargeLocation.name) // Fallback to operator because name is not set + assertEquals(false, chargeLocation.barrierFree) // False because `authentication:none` isn't set assertEquals(now, chargeLocation.timeRetrieved) // Cost From d5aa1da7ba4590317233ae25081efa61a105ef81 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sun, 30 Jan 2022 13:14:20 +0100 Subject: [PATCH 7/8] Document all fields of ChargeLocation --- .../net/vonforst/evmap/model/ChargersModel.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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 3a3ca34d..97cea276 100644 --- a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt +++ b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt @@ -28,6 +28,28 @@ sealed class ChargepointListItem /** * A whole charging site (potentially with multiple chargepoints). * + * @param id A unique number per charging site + * @param dataSource The name of the data source + * @param coordinates The latitude / longitude of this charge location + * @param address The charge location address + * @param chargepoints List of chargepoints at this location + * @param network The charging network (Mobility Service Provider, MSP) + * @param url A link to this charging site + * @param editUrl A link to a website where this charging site can be edited + * @param faultReport Set this if the charging site is reported to be out of service + * @param verified For crowdsourced data sources, this means that the data has been verified + * by an independent person + * @param barrierFree Whether this charge location can be used without prior registration + * @param operator The operator of this charge location (Charge Point Operator, CPO) + * @param generalInformation General information about this charging site that does not fit anywhere else + * @param amenities Description of amenities available at or near the charging site (toilets, food, accommodation, landmarks, etc.) + * @param locationDescription Directions on how to find the charger (e.g. "In the parking garage on level 5") + * @param photos List of photos of this charging site + * @param chargecards List of charge cards accepted here + * @param openinghours List of times when this charging site can be accessed / used + * @param cost The cost for charging and/or parking + * @param license How the data about this chargepoint is licensed + * @param chargepriceData Additional data needed for the Chargeprice implementation * @param timeRetrieved Time when this information was retrieved from the data source * @param isDetailed Whether this data includes all available details (for many data sources, * API calls that return a list may only give a compact representation) From f082165369534bd332e0c059b083dabf58e5f1b1 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Sun, 30 Jan 2022 23:23:50 +0100 Subject: [PATCH 8/8] Make Chargepoint.address nullable --- app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt | 4 ++-- .../main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt | 4 ++-- .../vonforst/evmap/api/openstreetmap/OpenStreetMapModel.kt | 2 +- app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt | 2 +- app/src/main/res/layout/detail_view.xml | 1 + app/src/main/res/layout/item_favorite.xml | 1 + 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt index d1f69fc4..30c3fbb2 100644 --- a/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt +++ b/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt @@ -105,7 +105,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole chargers?.take(maxRows)?.let { chargerList -> val builder = ItemList.Builder() // only show the city if not all chargers are in the same city - val showCity = chargerList.map { it.address.city }.distinct().size > 1 + val showCity = chargerList.map { it.address?.city }.distinct().size > 1 chargerList.forEach { charger -> builder.addItem(formatCharger(charger, showCity)) } @@ -177,7 +177,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole return Row.Builder().apply { // only show the city if not all chargers are in the same city (-> showCity == true) // and the city is not already contained in the charger name - if (showCity && charger.address.city != null && charger.address.city !in charger.name) { + if (showCity && charger.address?.city != null && charger.address.city !in charger.name) { setTitle( CarText.Builder("${charger.name} · ${charger.address.city}") .addVariant(charger.name) 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 31d57535..1e365702 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt @@ -44,13 +44,13 @@ fun buildDetails( if (loc == null) return emptyList() return listOfNotNull( - DetailsAdapter.Detail( + if (loc.address != null) DetailsAdapter.Detail( R.drawable.ic_address, R.string.address, loc.address.toString(), loc.locationDescription, clickable = true - ), + ) else null, if (loc.operator != null) DetailsAdapter.Detail( R.drawable.ic_operator, R.string.operator, 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 9cf8a933..73b58960 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 @@ -87,7 +87,7 @@ data class OSMChargingStation( "openstreetmap", getName(), Coordinate(lat, lon), - Address("", "", "", ""), // TODO: Can we determine this with overpass? + null, // TODO: Can we determine this with overpass? getChargepoints(), tags["network"], "https://www.openstreetmap.org/node/$id", 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 97cea276..b2b6be7d 100644 --- a/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt +++ b/app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt @@ -61,7 +61,7 @@ data class ChargeLocation( val dataSource: String, val name: String, @Embedded val coordinates: Coordinate, - @Embedded val address: Address, + @Embedded val address: Address?, val chargepoints: List, val network: String?, val url: String, diff --git a/app/src/main/res/layout/detail_view.xml b/app/src/main/res/layout/detail_view.xml index 1fc73a53..59730141 100644 --- a/app/src/main/res/layout/detail_view.xml +++ b/app/src/main/res/layout/detail_view.xml @@ -100,6 +100,7 @@ android:maxLines="1" android:text="@{charger.data.address.toString()}" android:textAppearance="@style/TextAppearance.Material3.BodySmall" + app:invisibleUnless="@{charger.data.address != null}" app:layout_constraintEnd_toStartOf="@+id/guideline2" app:layout_constraintStart_toStartOf="@+id/guideline" app:layout_constraintTop_toBottomOf="@+id/txtName" diff --git a/app/src/main/res/layout/item_favorite.xml b/app/src/main/res/layout/item_favorite.xml index e1be543e..b99442f8 100644 --- a/app/src/main/res/layout/item_favorite.xml +++ b/app/src/main/res/layout/item_favorite.xml @@ -67,6 +67,7 @@ android:maxLines="1" android:text="@{item.charger.address.toString()}" android:textAppearance="@style/TextAppearance.Material3.BodySmall" + app:invisibleUnless="@{item.charger.address != null}" app:layout_constraintEnd_toStartOf="@+id/textView7" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView15"