Skip to content
Open
Show file tree
Hide file tree
Changes from 83 commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
6852557
refactor localization of weekdays, months, times
westnordost Nov 30, 2025
466a533
remove some old code
westnordost Nov 30, 2025
7ede907
add texts for times, weekdays, months selectors
westnordost Nov 30, 2025
90bd202
add month select and weekday select dialogs
westnordost Dec 2, 2025
1271944
Merge branch 'master' into opening_hours
westnordost Dec 3, 2025
0d0ba52
add tests, fix bugs
westnordost Dec 3, 2025
243ad2f
add time format elements class plus tests
westnordost Dec 4, 2025
b9ea69d
TimePicker mockup
westnordost Dec 4, 2025
4527d4d
finish time picker
westnordost Dec 5, 2025
d5a62f6
add generic CheckboxList, analogous to the RadioGroup
westnordost Dec 9, 2025
b61ad41
rename preview function
westnordost Dec 9, 2025
8ecafe5
make use of checkbox list in weekday and months select dialog
westnordost Dec 9, 2025
1089f1d
forgot default param
westnordost Dec 9, 2025
383a75d
blub
westnordost Dec 9, 2025
2aa236d
add function for holidayselector -> holidays
westnordost Dec 9, 2025
4955a71
actually, easier like this
westnordost Dec 9, 2025
f383df8
more progress...
westnordost Dec 9, 2025
5150b7e
more screwing around
westnordost Dec 12, 2025
ab503d1
make time selectors column deal with both time points and time spans …
westnordost Dec 15, 2025
e57bfa0
handle off days, somewhat
westnordost Dec 15, 2025
93dbd36
off days
westnordost Dec 16, 2025
08b884d
work on the data model
westnordost Dec 17, 2025
6e0949f
work on the data model
westnordost Dec 17, 2025
5da3835
workweek
westnordost Dec 17, 2025
2bf2aec
workweek
westnordost Dec 17, 2025
cacf979
remove some old code
westnordost Dec 17, 2025
ce92959
defaults for new weekdays, new times, new months
westnordost Dec 18, 2025
307499c
note if months/weekdays are not specified
westnordost Dec 19, 2025
469b062
make compile again (lobotomize AddParkingFeeForm), fix tests etc...
westnordost Dec 23, 2025
747a191
update country metadata
westnordost Dec 23, 2025
93f95bf
add missing strings
westnordost Dec 23, 2025
d62dfcd
some aligning, paddingh
westnordost Dec 24, 2025
02994da
refactor to a single add button (untested)
westnordost Jan 1, 2026
b11a503
looks good now, except time selection dialog
westnordost Jan 1, 2026
7961d0d
fix time dialog
westnordost Jan 2, 2026
e33df90
fix crash
westnordost Jan 5, 2026
d1ae298
add comment
westnordost Jan 5, 2026
2835ce7
solve enabling months differently
westnordost Jan 5, 2026
e0a512c
add divider
westnordost Jan 6, 2026
9d2affc
force user to specify months for all rules if he specified months for…
westnordost Jan 7, 2026
7d71454
WIP add parking fee form
westnordost Jan 8, 2026
2d4db4c
implement parking fee
westnordost Jan 12, 2026
e893f1f
don't support opening hours with incomplete months at all; force user…
westnordost Jan 13, 2026
c0fb253
display text for null
westnordost Jan 13, 2026
820bd7e
a bit nicer layout
westnordost Jan 14, 2026
ee62f11
Merge branch 'master' into opening_hours
westnordost Jan 14, 2026
1adbb7e
remove unused styling
westnordost Jan 15, 2026
63de645
correction
westnordost Jan 15, 2026
124f4dc
add comment
westnordost Jan 15, 2026
91a70d8
Merge branch 'master' into opening_hours
westnordost Jan 15, 2026
94e321e
remove own CheckboxList, rename MultipleSelectGroup
westnordost Jan 15, 2026
f1adbb5
fix bugs reported by paulklie
westnordost Jan 18, 2026
97c0dd8
some comments etc
westnordost Jan 18, 2026
5665720
feature: better default guess for initial time in the picker
westnordost Jan 18, 2026
26b7c23
correct title
westnordost Jan 18, 2026
cf170c3
add comments
westnordost Jan 18, 2026
3b8460d
put duration unit dropdown into own file
westnordost Jan 18, 2026
a65ab23
change string to be more generic
westnordost Jan 19, 2026
2999a91
comment about ui thing
westnordost Jan 19, 2026
93b1950
initial commit for parking_charge (mainly copied other quest so far)
Dec 16, 2025
8b5baf4
already implemented some basic things for quest, deleted a few files …
Dec 17, 2025
9a4aa5d
more or less working quest layout, added icon for quest
Dec 17, 2025
1f2910d
removed custom xml file, Quest is now using the standard composeViewB…
Dec 18, 2025
5b39733
changed quest_parking_charge_title
Dec 18, 2025
68303d7
new horizontal task layout with dropdown
Dec 20, 2025
821b326
new horizontal task layout with dropdown
Dec 20, 2025
960a1bd
added check_date:charge logic, now using correct tag charge:condition…
Dec 20, 2025
9951ebe
changed folder name from parking_charge to charge
Dec 20, 2025
cf0a14e
removed duplicate heading, bigger font for input, removed unnecessary…
Jan 18, 2026
431c795
german translations
Jan 18, 2026
dfc846b
new ChargeInput composable
Jan 18, 2026
ab64fa0
new hint proposal (maybe could be changed later on)
Jan 18, 2026
c8686a7
currency formatting: Automatic detection of locales like symbol befor…
Jan 18, 2026
40c6b66
Update app/src/androidMain/res/values/strings.xml
marekkrug Jan 29, 2026
f4f35b1
refactored CurrencyFormatter and introduced CurrencyFormatElements
Jan 29, 2026
7b43c3c
refactored CurrencyFormatter and introduced CurrencyFormatElements
Jan 29, 2026
679ba8b
new tests for CurrencyFormatter and CurrencyFormatElements, added bit…
Jan 30, 2026
ba73b85
Use typographic quotation marks and apostrophes
FloEdelmann Jan 30, 2026
ccb659f
migration to DurationUnit, so far not using DurationUnitDropdown
Jan 30, 2026
2fba209
Merge remote-tracking branch 'github/master'
Jan 30, 2026
316c804
Merge branch 'master' into master
marekkrug Feb 19, 2026
3f9c8af
fixed quest number, now the app is in a runnable state again
Feb 19, 2026
1b6950e
add missing shadow (and make stack of dollars bigger)
westnordost Feb 20, 2026
3e2b6c8
fix all things regarding the currency formatter
westnordost Feb 20, 2026
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 @@ -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
Expand Down Expand Up @@ -293,6 +294,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(),

Expand Down
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
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
Copy link
Copy Markdown
Member

@westnordost westnordost Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create an extension function or add a method to DurationUnit, for example either parametrize the existing toOsmValue function to e.g. toOsmValue(alwaysUseSingular: Boolean = false) or add toOsmValueSingular function

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
Copy link
Copy Markdown
Member

@westnordost westnordost Feb 20, 2026

Choose a reason for hiding this comment

The 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 DurationUnitDropdown but add a parameter that decides whether the unit should be written always in singular or not (analogous to my comment above)

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(
Copy link
Copy Markdown
Member

@westnordost westnordost Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use DecimalInput. This handles the locale aware decimal separator for you, as well as lets you set the maximum number of decimal places, plus the keyboard configuration.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless there is a good reason not to do that, at least amount should be a Double, timeUnitshould be aTimeUnit`.

) : 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
}
}
Loading