-
-
Notifications
You must be signed in to change notification settings - Fork 406
Quest: How much do you have to pay to use this parking spot? #6653
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
base: master
Are you sure you want to change the base?
Changes from 83 commits
6852557
466a533
7ede907
90bd202
1271944
0d0ba52
243ad2f
b9ea69d
4527d4d
d5a62f6
b61ad41
8ecafe5
1089f1d
383a75d
2aa236d
4955a71
f383df8
5150b7e
ab503d1
e57bfa0
93dbd36
08b884d
6e0949f
5da3835
2bf2aec
cacf979
ce92959
307499c
469b062
747a191
93f95bf
d62dfcd
02994da
b11a503
7961d0d
e33df90
d1ae298
2835ce7
e0a512c
9d2affc
7d71454
2d4db4c
e893f1f
c0fb253
820bd7e
ee62f11
1adbb7e
63de645
124f4dc
91a70d8
94e321e
f1adbb5
97c0dd8
5665720
26b7c23
cf170c3
3b8460d
a65ab23
2999a91
93b1950
8b5baf4
9a4aa5d
1f2910d
5b39733
68303d7
821b326
960a1bd
9951ebe
cf0a14e
431c795
dfc846b
ab64fa0
c8686a7
40c6b66
f4f35b1
7b43c3c
679ba8b
ba73b85
ccb659f
2fba209
316c804
3f9c8af
1b6950e
3e2b6c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| 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 | ||
|
|
||
| class AddParkingCharge : OsmFilterQuestType<ParkingChargeAnswer>(), AndroidQuest { | ||
|
|
||
| override val elementFilter = """ | ||
| nodes, ways, relations with amenity = parking | ||
marekkrug marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 = R.string.quest_parking_charge_hint | ||
|
|
||
| override fun getTitle(tags: Map<String, String>) = R.string.quest_parking_charge_title | ||
|
|
||
| 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}" | ||
| tags.updateCheckDateForKey("charge") | ||
| } | ||
| is ItVaries -> { | ||
| tags["charge:conditional"] = answer.conditional | ||
| tags.updateCheckDateForKey("charge:conditional") | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| 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.material.Text | ||
| import androidx.compose.runtime.MutableState | ||
| 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.quests.AnswerItem | ||
| import de.westnordost.streetcomplete.ui.common.ChargeInput | ||
| import de.westnordost.streetcomplete.ui.common.dialogs.TextInputDialog | ||
| import de.westnordost.streetcomplete.ui.theme.extraLargeInput | ||
| import de.westnordost.streetcomplete.ui.util.content | ||
| import de.westnordost.streetcomplete.util.locale.CurrencyFormatElements | ||
| import java.util.Currency | ||
| import java.util.Locale | ||
|
|
||
| class AddParkingChargeForm : AbstractOsmQuestForm<ParkingChargeAnswer>() { | ||
|
|
||
| override val contentLayoutResId = R.layout.compose_view | ||
| private val binding by contentViewBinding(ComposeViewBinding::bind) | ||
|
|
||
| private lateinit var amountState: MutableState<String> | ||
| private lateinit var durationUnitState: MutableState<DurationUnit> | ||
|
|
||
| private lateinit var showDialogState: MutableState<Boolean> | ||
|
|
||
| override val otherAnswers: List<AnswerItem> get() = listOf( | ||
| AnswerItem(R.string.quest_parking_charge_varies) { | ||
| showDialogState.value = true | ||
| } | ||
| ) | ||
|
|
||
| override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| super.onViewCreated(view, savedInstanceState) | ||
|
|
||
| binding.composeViewBase.content { | ||
| Surface { | ||
| amountState = rememberSaveable { mutableStateOf("") } | ||
| durationUnitState = rememberSaveable { mutableStateOf(DurationUnit.HOURS) } | ||
| showDialogState = rememberSaveable { mutableStateOf(false) } | ||
|
|
||
| val currencyCode = getCurrencyForCountry() | ||
| val currencyFormatInfo = remember(currencyCode) { | ||
| CurrencyFormatElements.of(currencyCode) | ||
| } | ||
|
|
||
| ProvideTextStyle(MaterialTheme.typography.extraLargeInput) { | ||
| ChargeInput( | ||
| amount = amountState.value, | ||
| onAmountChange = { | ||
| 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) | ||
| ) | ||
| } | ||
|
|
||
| if (showDialogState.value) { | ||
| TextInputDialog( | ||
| onDismissRequest = { showDialogState.value = false }, | ||
| onConfirmed = { description -> | ||
| applyAnswer(ItVaries(description)) | ||
| }, | ||
| title = { Text(getString(R.string.quest_parking_charge_varies_title)) }, | ||
| textInputLabel = { Text(getString(R.string.quest_parking_charge_varies_description)) } | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun isFormComplete(): Boolean { | ||
| val amount = amountState.value.replace(',', '.') | ||
| return amount.isNotEmpty() && amount.toDoubleOrNull() != null && amount.toDouble() > 0 | ||
| } | ||
|
|
||
| override fun onClickOk() { | ||
| val amount = amountState.value.replace(',', '.') | ||
| val currency = getCurrencyForCountry() | ||
| val timeUnit = when (durationUnitState.value) { // TODO: This could be removed if DurationUnit implements toOSMValue(). | ||
| DurationUnit.HOURS -> "hour" | ||
| DurationUnit.DAYS -> "day" | ||
| DurationUnit.MINUTES -> "minute" | ||
| } | ||
|
Comment on lines
+100
to
+104
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. create an extension function or add a method to |
||
| applyAnswer(SimpleCharge(amount, currency, timeUnit)) | ||
| } | ||
|
|
||
| private fun getCurrencyForCountry(): String = try { | ||
| val locale = Locale.Builder().setRegion(countryInfo.countryCode).build() | ||
| val currency = Currency.getInstance(locale) | ||
| currency.currencyCode | ||
| } catch (_: Exception) { | ||
| "EUR" | ||
| } | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
|
Comment on lines
+109
to
+113
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have a look at how the string resource is created for other quests / composable UI. Could also use the |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| 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.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: String, | ||
| onAmountChange: (String) -> 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.symbolBeforeAmount) { | ||
| Text( | ||
| text = currencyFormatInfo.symbol, | ||
| style = MaterialTheme.typography.h5, | ||
| color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), | ||
| modifier = Modifier.padding(end = 8.dp) | ||
| ) | ||
| } | ||
|
|
||
| TextField( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| value = amount, | ||
| onValueChange = onAmountChange, | ||
| placeholder = { | ||
| // Generate placeholder based on decimal places | ||
| val placeholderValue = when (currencyFormatInfo.decimalPlaces) { | ||
| 0 -> "150" | ||
| 1 -> "15.0" | ||
| else -> "1.50" | ||
| } | ||
| Text(placeholderValue) | ||
| }, | ||
| keyboardOptions = KeyboardOptions( | ||
| keyboardType = if (currencyFormatInfo.decimalPlaces > 0) { | ||
| KeyboardType.Decimal | ||
| } else { | ||
| KeyboardType.Number | ||
| } | ||
| ), | ||
| modifier = Modifier.width(150.dp), | ||
| singleLine = true, | ||
| ) | ||
|
|
||
| if (!currencyFormatInfo.symbolBeforeAmount) { | ||
| 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 | ||
| } | ||
| ) | ||
| } | ||
| */ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package de.westnordost.streetcomplete.quests.charge | ||
|
|
||
| sealed interface ParkingChargeAnswer | ||
|
|
||
| data class SimpleCharge( | ||
| // e.g. "1.50" | ||
| val amount: String, | ||
| // e.g. "EUR" | ||
| val currency: String, | ||
| // either "day", "hour" or "minute" | ||
| val timeUnit: String | ||
|
Comment on lines
+7
to
+11
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless there is a good reason not to do that, at least |
||
| ) : ParkingChargeAnswer | ||
|
|
||
| data class ItVaries( | ||
| val conditional: String | ||
| ) : ParkingChargeAnswer | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package de.westnordost.streetcomplete.util.locale | ||
|
|
||
| import java.text.NumberFormat | ||
| import java.util.Currency | ||
| import java.util.Locale | ||
|
|
||
| /** | ||
| * @param currencyCode the ISO 4217 code of the currency | ||
| * */ | ||
| actual class CurrencyFormatter actual constructor(currencyCode: String?) { | ||
| actual val currencyCode: String? = currencyCode | ||
| /** | ||
| * @param sampleValue the value to format | ||
| * @return the formatted input value | ||
| */ | ||
| actual fun format(sampleValue: Double): String { | ||
| val currency = Currency.getInstance(currencyCode) | ||
|
|
||
| val locale = Locale.getAvailableLocales().firstOrNull { | ||
| try { | ||
| Currency.getInstance(it)?.currencyCode == currencyCode | ||
| } catch (_: Exception) { | ||
| false | ||
| } | ||
| } ?: Locale.getDefault() | ||
|
|
||
| val formatter = NumberFormat.getCurrencyInstance(locale) | ||
| formatter.currency = currency | ||
|
|
||
| val formatted = formatter.format(sampleValue) | ||
|
|
||
| return formatted | ||
| } | ||
| actual fun getCurrencyCodeFromLocale(countryCode: androidx.compose.ui.text.intl.Locale?): String? = try { | ||
| val countryCode = countryCode?.region ?: return null | ||
| val locale = Locale.Builder().setRegion(countryCode).build() | ||
| val currency = java.util.Currency.getInstance(locale) | ||
| currency.currencyCode | ||
| } catch (_: Exception) { | ||
| null | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.