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 470a77e4010..b7da6a2322f 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/QuestsModule.kt @@ -59,13 +59,14 @@ import de.westnordost.streetcomplete.quests.bus_stop_name.AddBusStopName import de.westnordost.streetcomplete.quests.bus_stop_ref.AddBusStopRef import de.westnordost.streetcomplete.quests.bus_stop_shelter.AddBusStopShelter import de.westnordost.streetcomplete.quests.camera_type.AddCameraType +import de.westnordost.streetcomplete.quests.camping.AddCabins import de.westnordost.streetcomplete.quests.camping.AddCampDrinkingWater import de.westnordost.streetcomplete.quests.camping.AddCampPower import de.westnordost.streetcomplete.quests.camping.AddCampShower -import de.westnordost.streetcomplete.quests.camping.AddTents -import de.westnordost.streetcomplete.quests.camping.AddCabins import de.westnordost.streetcomplete.quests.camping.AddCaravans +import de.westnordost.streetcomplete.quests.camping.AddTents import de.westnordost.streetcomplete.quests.car_wash_type.AddCarWashType +import de.westnordost.streetcomplete.quests.charge.AddParkingCharge import de.westnordost.streetcomplete.quests.charging_station_bicycles.AddChargingStationBicycles import de.westnordost.streetcomplete.quests.charging_station_capacity.AddChargingStationBicycleCapacity import de.westnordost.streetcomplete.quests.charging_station_capacity.AddChargingStationCapacity @@ -292,6 +293,7 @@ fun questTypeRegistry( 17 to AddParkingType(), 18 to AddParkingAccess(), // used by OSM Carto, mapy.cz, OSMand, Sputnik etc 19 to AddParkingFee(), // used by OsmAnd + 199 to AddParkingCharge(), 20 to AddTrafficCalmingType(), diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingCharge.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingCharge.kt new file mode 100644 index 00000000000..299beab045e --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingCharge.kt @@ -0,0 +1,46 @@ +package de.westnordost.streetcomplete.quests.charge + +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry +import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType +import de.westnordost.streetcomplete.data.quest.AndroidQuest +import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CAR +import de.westnordost.streetcomplete.osm.Tags +import de.westnordost.streetcomplete.osm.updateCheckDateForKey +import de.westnordost.streetcomplete.resources.Res +import de.westnordost.streetcomplete.resources.quest_crossing_title2 + +class AddParkingCharge : OsmFilterQuestType(), AndroidQuest { + + override val elementFilter = """ + nodes, ways, relations with amenity = parking + and access ~ yes|customers|public + and fee = yes + and ( + !charge and !charge:conditional + or charge older today -18 months + ) + """ + + override val changesetComment = "Add parking charges" + override val wikiLink = "Key:charge" + override val icon = R.drawable.quest_parking_charge + override val achievements = listOf(CAR) + // for now, this hint is just a proposal + // override val hint = Res.string.quest_parking_charge_hint // TODO FIX THE HINT + + // override val title = Res.string.quest_parking_charge_title // TODO FIX THE TITLE + override val title = Res.string.quest_crossing_title2 + + override fun createForm() = AddParkingChargeForm() + + override fun applyAnswerTo(answer: ParkingChargeAnswer, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) { + when (answer) { + is SimpleCharge -> { + // Format: "1.50 EUR/hour" + tags["charge"] = "${answer.amount} ${answer.currency}/${answer.timeUnit.toOsmValue(false)}" + tags.updateCheckDateForKey("charge") + } + } + } +} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingChargeForm.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingChargeForm.kt new file mode 100644 index 00000000000..bbbcdc21a6c --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/AddParkingChargeForm.kt @@ -0,0 +1,96 @@ +package de.westnordost.streetcomplete.quests.charge + +import android.os.Bundle +import android.view.View +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Surface +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableDoubleStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.databinding.ComposeViewBinding +import de.westnordost.streetcomplete.osm.duration.DurationUnit +import de.westnordost.streetcomplete.quests.AbstractOsmQuestForm +import de.westnordost.streetcomplete.ui.common.ChargeInput +import de.westnordost.streetcomplete.ui.theme.extraLargeInput +import de.westnordost.streetcomplete.ui.util.content +import de.westnordost.streetcomplete.util.locale.CurrencyFormatElements +import de.westnordost.streetcomplete.util.locale.CurrencyFormatter + +class AddParkingChargeForm : AbstractOsmQuestForm() { + + override val contentLayoutResId = R.layout.compose_view + private val binding by contentViewBinding(ComposeViewBinding::bind) + + private lateinit var amountState: MutableState + private lateinit var durationUnitState: MutableState + + private lateinit var showDialogState: MutableState + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.composeViewBase.content { + Surface { + amountState = rememberSaveable { mutableDoubleStateOf(0.0) } + durationUnitState = rememberSaveable { mutableStateOf(DurationUnit.HOURS) } + showDialogState = rememberSaveable { mutableStateOf(false) } + + val currencyFormatInfo = remember(countryInfo) { + CurrencyFormatElements.of(countryInfo.userPreferredLocale) + } + + ProvideTextStyle(MaterialTheme.typography.extraLargeInput) { + ChargeInput( + amount = amountState.value, + onAmountChange = { + if (it != null) { + amountState.value = it + } + checkIsFormComplete() + }, + currencyFormatInfo = currencyFormatInfo, + durationUnit = durationUnitState.value, + onDurationUnitChange = { unit -> + durationUnitState.value = unit + checkIsFormComplete() + }, + perLabel = getString(R.string.quest_parking_charge_time_unit_label), + durationUnitDisplayNames = { unit -> unit.getDisplayName(this@AddParkingChargeForm) }, + modifier = Modifier.padding(16.dp) + ) + } + } + } + } + + override fun isFormComplete(): Boolean { + val amount = amountState.value + return amount != 0.0 && amount > 0.0 + } + + override fun onClickOk() { + val amount = amountState.value + val currency = CurrencyFormatter(countryInfo.userPreferredLocale).currencyCode ?: "???" + /* + val timeUnit = when (durationUnitState.value) { // TODO: This could be removed if DurationUnit implements toOSMValue(). + DurationUnit.HOURS -> "hour" + DurationUnit.DAYS -> "day" + DurationUnit.MINUTES -> "minute" + } + */ + applyAnswer(SimpleCharge(amount, currency, durationUnitState.value)) + } +} + +fun DurationUnit.getDisplayName(form: AddParkingChargeForm): String = when (this) { + DurationUnit.HOURS -> form.getString(R.string.quest_parking_charge_hour) + DurationUnit.DAYS -> form.getString(R.string.quest_parking_charge_day) + DurationUnit.MINUTES -> form.getString(R.string.quest_parking_charge_minute_short) +} diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ChargeInput.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ChargeInput.kt new file mode 100644 index 00000000000..c81d96b1bc8 --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ChargeInput.kt @@ -0,0 +1,123 @@ +package de.westnordost.streetcomplete.ui.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.osm.duration.DurationUnit +import de.westnordost.streetcomplete.ui.common.input.DecimalInput +import de.westnordost.streetcomplete.util.locale.CurrencyFormatElements +// import org.jetbrains.compose.ui.tooling.preview.Preview + +/** A composable for inputting a charge amount with currency symbol and time unit selector */ +@Composable +fun ChargeInput( + amount: Double, + onAmountChange: (Double?) -> Unit, + currencyFormatInfo: CurrencyFormatElements, + durationUnit: DurationUnit, + onDurationUnitChange: (DurationUnit) -> Unit, + perLabel: String, + modifier: Modifier = Modifier, + durationUnitDisplayNames: (DurationUnit) -> String +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (currencyFormatInfo.isSymbolBeforeAmount) { + Text( + text = currencyFormatInfo.symbol, + style = MaterialTheme.typography.h5, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), + modifier = Modifier.padding(end = 8.dp) + ) + } + + DecimalInput( + value = amount, + onValueChange = onAmountChange, + keyboardOptions = KeyboardOptions( + keyboardType = if (currencyFormatInfo.decimalDigits > 0) { + KeyboardType.Decimal + } else { + KeyboardType.Number + } + ), + modifier = Modifier.width(150.dp), + ) + + if (!currencyFormatInfo.isSymbolBeforeAmount) { + Text( + text = currencyFormatInfo.symbol, + style = MaterialTheme.typography.h5, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), + modifier = Modifier.padding(start = 8.dp) + ) + } + + Text( + text = perLabel, + style = MaterialTheme.typography.body1 + ) + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + /* + DurationUnitDropdown( + selectedDuration = durationUnit, + onSelectedDuration = onDurationUnitChange, + )*/ + DropdownButton( + items = DurationUnit.entries, + onSelectedItem = onDurationUnitChange, + itemContent = { unit -> + Text(durationUnitDisplayNames(unit)) + }, + selectedItem = durationUnit, + style = ButtonStyle.Outlined, + ) + } + } +} +/* +@Composable +@Preview +private fun ChargeInputPreview() { + val amount = remember { mutableStateOf("1.50") } + val durationUnit = remember { mutableStateOf(DurationUnit.HOURS) } + + ChargeInput( + amount = amount.value, + onAmountChange = { amount.value = it }, + currencyFormatInfo = CurrencyFormatElements( + symbol = "€", + symbolBeforeAmount = false, + decimalPlaces = 2 + ), + durationUnit = durationUnit.value, + onDurationUnitChange = { durationUnit.value = it }, + perLabel = "per", + durationUnitDisplayNames = { unit -> + when (unit) { + DurationUnit.MINUTES -> Res.string.unit_minutes + DurationUnit.HOURS -> Res.string.unit_hours + DurationUnit.DAYS -> Res.string.unit_days + } as String + } + ) +} +*/ diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ParkingChargeAnswer.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ParkingChargeAnswer.kt new file mode 100644 index 00000000000..2fe931992c7 --- /dev/null +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/charge/ParkingChargeAnswer.kt @@ -0,0 +1,14 @@ +package de.westnordost.streetcomplete.quests.charge + +import de.westnordost.streetcomplete.osm.duration.DurationUnit + +sealed interface ParkingChargeAnswer + +data class SimpleCharge( + // e.g. "1.50" + val amount: Double, + // e.g. "EUR" + val currency: String, + // either "day", "hour" or "minute" + val timeUnit: DurationUnit +) : ParkingChargeAnswer diff --git a/app/src/androidMain/res/drawable/quest_parking_charge.xml b/app/src/androidMain/res/drawable/quest_parking_charge.xml new file mode 100644 index 00000000000..763352ca8bc --- /dev/null +++ b/app/src/androidMain/res/drawable/quest_parking_charge.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + diff --git a/app/src/androidMain/res/values-de/strings.xml b/app/src/androidMain/res/values-de/strings.xml index 228c90d67f4..84d48d57687 100644 --- a/app/src/androidMain/res/values-de/strings.xml +++ b/app/src/androidMain/res/values-de/strings.xml @@ -1504,4 +1504,4 @@ Zum Beispiel 1,3 oder 2b,4,6." "Kann man hier warm duschen?" "Neues OSM-Event in deiner Gegend!" "Neue OSM-Events in deiner Gegend" - \ No newline at end of file + diff --git a/app/src/androidMain/res/values/strings.xml b/app/src/androidMain/res/values/strings.xml index ae6c3464052..811b12592ca 100644 --- a/app/src/androidMain/res/values/strings.xml +++ b/app/src/androidMain/res/values/strings.xml @@ -1416,6 +1416,14 @@ If there are no signs along the whole street which apply for the highlighted sec Any person visiting a specific place (e.g. customers only) Only individual(s) with permission + + How much do you need to pay to park here? + per + hour + day + min + "For simplicity’s sake, it is recommended that you enter the standard amount for different amounts in this quest. For example, if it costs €3 during the day and €1 at night, enter €3. This makes it easier to compare prices from the data later on." + Do you have to pay to park here? Depends on time and day… Yes,… diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/osm/duration/Duration.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/osm/duration/Duration.kt index 3bebf083bb1..6846be80503 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/osm/duration/Duration.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/osm/duration/Duration.kt @@ -4,7 +4,7 @@ import de.westnordost.streetcomplete.osm.duration.DurationUnit.* import de.westnordost.streetcomplete.util.ktx.toShortString import kotlinx.serialization.Serializable -/** A duration, as used in OSM for e.g. maxstay */ +/** A duration, as used in OSM for e.g. maxstay or charge */ @Serializable data class Duration(val value: Double, val unit: DurationUnit) { fun toOsmValue(): String = @@ -18,5 +18,11 @@ data class Duration(val value: Double, val unit: DurationUnit) { enum class DurationUnit { MINUTES, HOURS, - DAYS, + DAYS; + + fun toOsmValue(usePlural: Boolean = false): String = when (this) { + MINUTES -> if (usePlural) "minutes" else "minute" + HOURS -> if (usePlural) "hours" else "hour" + DAYS -> if (usePlural) "days" else "day" + } } diff --git a/app/src/commonTest/kotlin/de/westnordost/streetcomplete/osm/duration/DurationTest.kt b/app/src/commonTest/kotlin/de/westnordost/streetcomplete/osm/duration/DurationTest.kt index 4aa2d7d7b77..cba05eb0a41 100644 --- a/app/src/commonTest/kotlin/de/westnordost/streetcomplete/osm/duration/DurationTest.kt +++ b/app/src/commonTest/kotlin/de/westnordost/streetcomplete/osm/duration/DurationTest.kt @@ -11,5 +11,18 @@ class DurationTest() { assertEquals("12 hours", Duration(12.0, DurationUnit.HOURS).toOsmValue()) assertEquals("1 day", Duration(1.0, DurationUnit.DAYS).toOsmValue()) assertEquals("12 days", Duration(12.0, DurationUnit.DAYS).toOsmValue()) + + assertEquals("day", DurationUnit.DAYS.toOsmValue( + usePlural = false + )) + assertEquals("days", DurationUnit.DAYS.toOsmValue( + usePlural = true + )) + assertEquals("minute", DurationUnit.MINUTES.toOsmValue( + usePlural = false + )) + assertEquals("hours", DurationUnit.HOURS.toOsmValue( + usePlural = true + )) } } diff --git a/res/graphics/quest/parking_charge.svg b/res/graphics/quest/parking_charge.svg new file mode 100644 index 00000000000..9fbb80df368 --- /dev/null +++ b/res/graphics/quest/parking_charge.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + +