diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt index 7f812c6600d..b2fefd15506 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt @@ -69,6 +69,7 @@ import de.westnordost.streetcomplete.quests.charging_station_bicycles.AddChargin import de.westnordost.streetcomplete.quests.charging_station_capacity.AddChargingStationBicycleCapacity import de.westnordost.streetcomplete.quests.charging_station_capacity.AddChargingStationCapacity import de.westnordost.streetcomplete.quests.charging_station_operator.AddChargingStationOperator +import de.westnordost.streetcomplete.quests.charging_station_socket.AddChargingStationSocket import de.westnordost.streetcomplete.quests.clothing_bin_operator.AddClothingBinOperator import de.westnordost.streetcomplete.quests.construction.MarkCompletedBuildingConstruction import de.westnordost.streetcomplete.quests.construction.MarkCompletedHighwayConstruction @@ -429,6 +430,7 @@ fun questTypeRegistry( 87 to AddChargingStationCapacity(), // after question for bicycles because user has possibility to answer that it is only for bicycles 179 to AddChargingStationBicycleCapacity(), 88 to AddChargingStationOperator(), + 197 to AddChargingStationSocket(), // postboxes (collection times are further up, see comment) 89 to AddPostboxRoyalCypher(), // can be glanced across the road (if postbox facing the right way) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt new file mode 100644 index 00000000000..3d0b22dfe2b --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocket.kt @@ -0,0 +1,135 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression +import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.Element +import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry +import de.westnordost.streetcomplete.data.osm.mapdata.Way +import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType +import de.westnordost.streetcomplete.data.quest.AndroidQuest +import de.westnordost.streetcomplete.data.quest.NoCountriesExcept +import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR +import de.westnordost.streetcomplete.osm.Tags +import de.westnordost.streetcomplete.util.math.contains + +class AddChargingStationSocket : + OsmElementQuestType>, + AndroidQuest { + + private val filter by lazy { + """ + nodes, ways with + amenity = charging_station + and bicycle != yes + and motorcar != no + """.toElementFilterExpression() + } + + override val enabledInCountries = NoCountriesExcept( + "AT","BE","BG","HR","CY","CZ","DE","DK","EE","ES","FI","FR","GB", + "GR","HU","IE","IS","IT","LI","LT","LU","LV","MT","NL","NO", + "PL","PT","RO","SE","SI","SK" + ) + + override val changesetComment = "Specify charging station sockets" + override val wikiLink = "Key:socket" + override val icon = R.drawable.quest_charger_socket + override val achievements = listOf(CAR) + + override fun getTitle(tags: Map) = + R.string.quest_charging_station_socket_title + + override fun createForm() = AddChargingStationSocketForm() + + override fun getApplicableElements(mapData: MapDataWithGeometry): Iterable { + return mapData + .filter { element -> filter.matches(element) } + .filter { element -> isApplicableTo(element, mapData) } + .asIterable() + } + + override fun isApplicableTo(element: Element): Boolean? { + // This variant must exist because OsmElementQuestType requires it. + // But we delegate real logic to the overloaded version. + return null + } + + private fun isApplicableTo( + element: Element, + mapData: MapDataWithGeometry + ): Boolean { + + if (!filter.matches(element)) return false + + // Skip charge_point nodes completely + if (element.tags["man_made"] == "charge_point") return false + + // Skip charging_station areas that contain charge_points + if (element is Way) { + + val geometry = mapData.getGeometry(element.type, element.id) + ?: return true + + val bounds = geometry.bounds + + val hasChargePointsInside = mapData + .filter { it.tags["man_made"] == "charge_point" } + .any { cp -> + val cpGeometry = mapData.getGeometry(cp.type, cp.id) + ?: return@any false + + bounds.contains(cpGeometry.center) + } + + if (hasChargePointsInside) return false + } + + val socketTags = element.tags + .filterKeys { it.startsWith("socket:") } + + if (socketTags.isEmpty()) return true + if (socketTags.keys.any { isDeprecatedSocketKey(it) }) return true + if (socketTags.values.any { it == "yes" }) return true + + return false + } + + override fun applyAnswerTo( + answer: Map, + tags: Tags, + geometry: ElementGeometry, + timestampEdited: Long + ) { + + // Cleanup deprecated keys + tags.keys + .filter { isDeprecatedSocketKey(it) } + .toList() + .forEach { tags.remove(it) } + + // Remove old socket:* keys + tags.keys + .filter { it.startsWith("socket:") } + .toList() + .forEach { tags.remove(it) } + + // Apply new values + answer.forEach { (type, count) -> + tags["socket:${type.osmKey}"] = count.toString() + } + + // type2/type2_cable=no logic + if (answer.containsKey(SocketType.TYPE2) + && !answer.containsKey(SocketType.TYPE2_CABLE) + ) { + tags["socket:type2_cable"] = "no" + } + } + + private fun isDeprecatedSocketKey(key: String): Boolean = + key.startsWith("socket:tesla") || + key == "socket:css" || + key == "socket:unknown" || + key == "socket:type" +} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt new file mode 100644 index 00000000000..2a599b96d7b --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/AddChargingStationSocketForm.kt @@ -0,0 +1,64 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.compose.material.Surface +import androidx.compose.runtime.mutableStateMapOf +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.databinding.ComposeViewBinding +import de.westnordost.streetcomplete.quests.AbstractOsmQuestForm +import de.westnordost.streetcomplete.ui.util.content + +class AddChargingStationSocketForm : + AbstractOsmQuestForm>() { + + override val contentLayoutResId = R.layout.compose_view + private val binding by contentViewBinding(ComposeViewBinding::bind) + + private val counts = mutableStateMapOf() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.composeViewBase.content { + Surface { + SocketTypeAndCountForm( + counts = counts, + onCountsChanged = { + counts.clear() + counts.putAll(it) + checkIsFormComplete() + } + ) + } + } + } + + override fun onClickOk() { + if (counts.values.any { it !in 1..50 }) { + confirmUnusualInput() + } else { + applyAnswer(counts.toMap()) + } + } + + private fun confirmUnusualInput() { + activity?.let { + AlertDialog.Builder(it) + .setTitle(R.string.quest_generic_confirmation_title) + .setMessage(R.string.quest_maxweight_unusualInput_confirmation_description) + .setPositiveButton(R.string.quest_generic_confirmation_yes) { _, _ -> + applyAnswer(counts.toMap()) + } + .setNegativeButton(R.string.quest_generic_confirmation_no, null) + .show() + } + } + + override fun isFormComplete() = + counts.isNotEmpty() + + override fun isRejectingClose() = + counts.isNotEmpty() +} diff --git a/app/src/androidMain/res/drawable/quest_charger_socket.xml b/app/src/androidMain/res/drawable/quest_charger_socket.xml new file mode 100644 index 00000000000..e0ca2b17cf2 --- /dev/null +++ b/app/src/androidMain/res/drawable/quest_charger_socket.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/app/src/androidMain/res/values/strings.xml b/app/src/androidMain/res/values/strings.xml index 7faad547e69..145dc0761c9 100644 --- a/app/src/androidMain/res/values/strings.xml +++ b/app/src/androidMain/res/values/strings.xml @@ -1056,6 +1056,13 @@ A level counts as a roof level when its windows are in the roof. Subsequently, r Who is the operator of this charging station? + How many sockets does this charging station have? + Type 2 (Mennekes) + Type 2 (Mennekes) Cable + Type 2 Combo (CCS) + CHAdeMO + Household-plug + Who accepts donations for this clothing bin? Is this building completed? diff --git a/app/src/commonMain/composeResources/drawable/socket_chademo.xml b/app/src/commonMain/composeResources/drawable/socket_chademo.xml new file mode 100644 index 00000000000..8378c3ee720 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_chademo.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_domestic.xml b/app/src/commonMain/composeResources/drawable/socket_domestic.xml new file mode 100644 index 00000000000..e3c4323f26b --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_domestic.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_eu_chademo.xml b/app/src/commonMain/composeResources/drawable/socket_eu_chademo.xml new file mode 100644 index 00000000000..3812e59e4b7 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_eu_chademo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_eu_domestic.xml b/app/src/commonMain/composeResources/drawable/socket_eu_domestic.xml new file mode 100644 index 00000000000..3812e59e4b7 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_eu_domestic.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_eu_type2.xml b/app/src/commonMain/composeResources/drawable/socket_eu_type2.xml new file mode 100644 index 00000000000..3812e59e4b7 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_eu_type2.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_eu_type2_combo.xml b/app/src/commonMain/composeResources/drawable/socket_eu_type2_combo.xml new file mode 100644 index 00000000000..3812e59e4b7 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_eu_type2_combo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_type2.xml b/app/src/commonMain/composeResources/drawable/socket_type2.xml new file mode 100644 index 00000000000..c59048ae418 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_type2.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_type2_cable.xml b/app/src/commonMain/composeResources/drawable/socket_type2_cable.xml new file mode 100644 index 00000000000..a1fa6545156 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_type2_cable.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/app/src/commonMain/composeResources/drawable/socket_type2_combo.xml b/app/src/commonMain/composeResources/drawable/socket_type2_combo.xml new file mode 100644 index 00000000000..9b5e7e1b2f8 --- /dev/null +++ b/app/src/commonMain/composeResources/drawable/socket_type2_combo.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/app/src/commonMain/composeResources/values/strings.xml b/app/src/commonMain/composeResources/values/strings.xml index 7faad547e69..145dc0761c9 100644 --- a/app/src/commonMain/composeResources/values/strings.xml +++ b/app/src/commonMain/composeResources/values/strings.xml @@ -1056,6 +1056,13 @@ A level counts as a roof level when its windows are in the roof. Subsequently, r Who is the operator of this charging station? + How many sockets does this charging station have? + Type 2 (Mennekes) + Type 2 (Mennekes) Cable + Type 2 Combo (CCS) + CHAdeMO + Household-plug + Who accepts donations for this clothing bin? Is this building completed? diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/meta/CountryInfo.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/meta/CountryInfo.kt index aad1232b4a6..588f529500b 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/meta/CountryInfo.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/meta/CountryInfo.kt @@ -41,6 +41,7 @@ data class IncompleteCountryInfo( val atmOperators: List? = null, val centerLineStyle: String? = null, val chargingStationOperators: List? = null, + val chargingStationSocket: List? = null, val clothesContainerOperators: List? = null, val edgeLineStyle: String? = null, val exclusiveCycleLaneStyle: String? = null, @@ -159,6 +160,8 @@ data class CountryInfo(private val infos: List) { get() = infos.firstNotNullOfOrNull { it.atmOperators } val chargingStationOperators: List? get() = infos.firstNotNullOfOrNull { it.chargingStationOperators } + val chargingStationSocket: List? + get() = infos.firstNotNullOfOrNull { it.chargingStationSocket } val clothesContainerOperators: List? get() = infos.firstNotNullOfOrNull { it.clothesContainerOperators } val livingStreetSignStyle: String? diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt new file mode 100644 index 00000000000..96c93d1d3ba --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketType.kt @@ -0,0 +1,57 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +import de.westnordost.streetcomplete.resources.* +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.StringResource + +enum class SocketType(val osmKey: String) { + TYPE2("type2"), + TYPE2_CABLE("type2_cable"), + TYPE2_COMBO("type2_combo"), + CHADEMO("chademo"), + DOMESTIC("domestic"); + + companion object { + val selectableValues = entries + } +} + +/* ---------------------------------------------------------- + Primary socket icon + ---------------------------------------------------------- */ + +val SocketType.icon: DrawableResource + get() = when (this) { + SocketType.TYPE2 -> Res.drawable.socket_type2 + SocketType.TYPE2_CABLE -> Res.drawable.socket_type2_cable + SocketType.TYPE2_COMBO -> Res.drawable.socket_type2_combo + SocketType.CHADEMO -> Res.drawable.socket_chademo + SocketType.DOMESTIC -> Res.drawable.socket_domestic + } + +/* ---------------------------------------------------------- + EU compatibility label (hexagon symbol) + Each socket gets its own EU-label icon. + ---------------------------------------------------------- */ + +val SocketType.euLabel: DrawableResource + get() = when (this) { + SocketType.TYPE2 -> Res.drawable.socket_eu_type2 + SocketType.TYPE2_CABLE -> Res.drawable.socket_eu_type2 + SocketType.TYPE2_COMBO -> Res.drawable.socket_eu_type2_combo + SocketType.CHADEMO -> Res.drawable.socket_eu_chademo + SocketType.DOMESTIC -> Res.drawable.socket_eu_domestic + } + +/* ---------------------------------------------------------- + Localized title + ---------------------------------------------------------- */ + +val SocketType.title: StringResource + get() = when (this) { + SocketType.TYPE2 -> Res.string.quest_charging_station_socket_type2 + SocketType.TYPE2_CABLE -> Res.string.quest_charging_station_socket_type2_cable + SocketType.TYPE2_COMBO -> Res.string.quest_charging_station_socket_type2_combo + SocketType.CHADEMO -> Res.string.quest_charging_station_socket_chademo + SocketType.DOMESTIC -> Res.string.quest_charging_station_socket_domestic + } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt new file mode 100644 index 00000000000..10f76f3d3d3 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/charging_station_socket/SocketTypeAndCountForm.kt @@ -0,0 +1,135 @@ +package de.westnordost.streetcomplete.quests.charging_station_socket + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.ui.common.StepperButton +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import androidx.compose.material.Icon + +@Composable +fun SocketTypeAndCountForm( + counts: Map, + onCountsChanged: (Map) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + SocketType.selectableValues.forEach { type -> + + val count = counts[type] ?: 0 + + SocketRow( + type = type, + count = count, + onIncrease = { + val newCount = count + 1 + if (newCount <= 50) { + val newMap = counts.toMutableMap() + newMap[type] = newCount + onCountsChanged(newMap) + } + }, + onDecrease = { + val newMap = counts.toMutableMap() + if (count <= 1) { + newMap.remove(type) + } else { + newMap[type] = count - 1 + } + onCountsChanged(newMap) + } + ) + } + } +} + +@Composable +private fun SocketRow( + type: SocketType, + count: Int, + onIncrease: () -> Unit, + onDecrease: () -> Unit +) { + Surface { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + // LEFT: SOCKET ICON + EU HEX + LABEL + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + + // Socket icon (bigger) + Icon( + painter = painterResource(type.icon), + contentDescription = null, + modifier = Modifier.size(60.dp) + ) + + Spacer(Modifier.width(4.dp)) + + // EU Hex (smaller) + Icon( + painter = painterResource(type.euLabel), + contentDescription = null, + modifier = Modifier.size(32.dp) + ) + + Spacer(Modifier.width(12.dp)) + + Text( + text = stringResource(type.title), + style = MaterialTheme.typography.body1 + ) + } + + // RIGHT: COUNT + STEPPER + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + + // Number with Border + Box( + modifier = Modifier + .border( + width = 1.dp, + color = if (count > 0) MaterialTheme.colors.primary else Color.DarkGray, + shape = RoundedCornerShape(6.dp) + ) + .padding(horizontal = 16.dp, vertical = 20.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = count.toString(), + style = MaterialTheme.typography.h6 + ) + } + + StepperButton( + onIncrease = onIncrease, + onDecrease = onDecrease, + increaseEnabled = count < 50, + decreaseEnabled = count > 0 + ) + } + } + } +} diff --git a/res/graphics/charging station socket/chademo.svg b/res/graphics/charging station socket/chademo.svg new file mode 100644 index 00000000000..7e147384c5d --- /dev/null +++ b/res/graphics/charging station socket/chademo.svg @@ -0,0 +1,26 @@ + + + + + + + + M + + + + + N + + + + + + + + + + + + + diff --git a/res/graphics/charging station socket/domestic.svg b/res/graphics/charging station socket/domestic.svg new file mode 100644 index 00000000000..8ee54acadca --- /dev/null +++ b/res/graphics/charging station socket/domestic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/graphics/charging station socket/socket_eu_type2.svg b/res/graphics/charging station socket/socket_eu_type2.svg new file mode 100644 index 00000000000..5b5eb67ed0b --- /dev/null +++ b/res/graphics/charging station socket/socket_eu_type2.svg @@ -0,0 +1,8 @@ + + + + + + C + + \ No newline at end of file diff --git a/res/graphics/charging station socket/type2.svg b/res/graphics/charging station socket/type2.svg new file mode 100644 index 00000000000..4fab45b0b66 --- /dev/null +++ b/res/graphics/charging station socket/type2.svg @@ -0,0 +1,17 @@ + + + + C + + + + + + + + + + + + + diff --git a/res/graphics/charging station socket/type2_cable.svg b/res/graphics/charging station socket/type2_cable.svg new file mode 100644 index 00000000000..8433fb5a569 --- /dev/null +++ b/res/graphics/charging station socket/type2_cable.svg @@ -0,0 +1,20 @@ + + + + + + + C + + + + + + + + + + + + + diff --git a/res/graphics/charging station socket/type2_combo.svg b/res/graphics/charging station socket/type2_combo.svg new file mode 100644 index 00000000000..7befc6cb4d2 --- /dev/null +++ b/res/graphics/charging station socket/type2_combo.svg @@ -0,0 +1,29 @@ + + + + + + L + + + + + + K + + + + + + + + + + + + + + + + + diff --git a/res/graphics/quest/charger_socket.svg b/res/graphics/quest/charger_socket.svg new file mode 100644 index 00000000000..17332c8545c --- /dev/null +++ b/res/graphics/quest/charger_socket.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + +