Skip to content

Commit

Permalink
Merge pull request #3841 from streetcomplete/parking-maxstay
Browse files Browse the repository at this point in the history
Add maxstay answer to parking fee quests
  • Loading branch information
westnordost authored Mar 4, 2022
2 parents b9f8e45 + f7c42ed commit e230b76
Show file tree
Hide file tree
Showing 19 changed files with 544 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package de.westnordost.streetcomplete.quests.max_height
import android.os.Bundle
import android.text.InputFilter
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.Spinner
Expand All @@ -22,6 +21,7 @@ import de.westnordost.streetcomplete.osm.LengthInMeters
import de.westnordost.streetcomplete.quests.AbstractQuestFormAnswerFragment
import de.westnordost.streetcomplete.quests.AnswerItem
import de.westnordost.streetcomplete.util.TextChangedWatcher
import de.westnordost.streetcomplete.view.OnAdapterItemSelectedListener

class AddMaxHeightForm : AbstractQuestFormAnswerFragment<MaxHeightAnswer>() {

Expand Down Expand Up @@ -68,12 +68,8 @@ class AddMaxHeightForm : AbstractQuestFormAnswerFragment<MaxHeightAnswer>() {
heightUnitSelect?.isGone = lengthUnits.size == 1
heightUnitSelect?.adapter = ArrayAdapter(requireContext(), R.layout.spinner_item_centered, lengthUnits)
heightUnitSelect?.setSelection(0)
heightUnitSelect?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parentView: AdapterView<*>, selectedItemView: View?, position: Int, id: Long) {
switchLayout(heightUnitSelect?.selectedItem as LengthUnit)
}

override fun onNothingSelected(parentView: AdapterView<*>) {}
heightUnitSelect?.onItemSelectedListener = OnAdapterItemSelectedListener {
switchLayout(heightUnitSelect?.selectedItem as LengthUnit)
}

inchInput?.filters = arrayOf(InputFilter { source, start, end, dest, dstart, dend ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import de.westnordost.streetcomplete.quests.AbstractQuestFormAnswerFragment
import de.westnordost.streetcomplete.quests.AnswerItem
import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningMonthsRow
import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningWeekdaysRow
import de.westnordost.streetcomplete.quests.opening_hours.adapter.RegularOpeningHoursAdapter
import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningHoursAdapter
import de.westnordost.streetcomplete.util.AdapterDataChangedWatcher
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
Expand Down Expand Up @@ -47,14 +47,16 @@ class AddOpeningHoursForm : AbstractQuestFormAnswerFragment<OpeningHoursAnswer>(
}
)

private lateinit var openingHoursAdapter: RegularOpeningHoursAdapter
private lateinit var openingHoursAdapter: OpeningHoursAdapter

private var isDisplayingPreviousOpeningHours: Boolean = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

openingHoursAdapter = RegularOpeningHoursAdapter(requireContext(), countryInfo)
openingHoursAdapter = OpeningHoursAdapter(requireContext())
openingHoursAdapter.firstDayOfWorkweek = countryInfo.firstDayOfWorkweek
openingHoursAdapter.regularShoppingDays = countryInfo.regularShoppingDays
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.recyclerview.widget.RecyclerView
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.data.meta.CountryInfo
import de.westnordost.streetcomplete.databinding.QuestTimesMonthRowBinding
import de.westnordost.streetcomplete.databinding.QuestTimesOffdayRowBinding
import de.westnordost.streetcomplete.databinding.QuestTimesWeekdayRowBinding
Expand All @@ -30,10 +29,8 @@ data class OpeningWeekdaysRow(var weekdays: Weekdays, var timeRange: TimeRange)
@Serializable
data class OffDaysRow(var weekdays: Weekdays) : OpeningHoursRow()

class RegularOpeningHoursAdapter(
private val context: Context,
private val countryInfo: CountryInfo
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class OpeningHoursAdapter(private val context: Context)
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {

var rows: MutableList<OpeningHoursRow> = mutableListOf()
set(value) {
Expand All @@ -47,6 +44,11 @@ class RegularOpeningHoursAdapter(
notifyDataSetChanged()
}

/** Set to change which weekdays are pre-checked in the weekday-select dialog */
var firstDayOfWorkweek: String = "Mo"
/** Set to change which weekdays are pre-checked in the weekday-select dialog */
var regularShoppingDays: Int = 6

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
Expand All @@ -66,7 +68,8 @@ class RegularOpeningHoursAdapter(
}
is WeekdayViewHolder -> {
val prevRow = if (position > 0) rows[position - 1] as? OpeningWeekdaysRow else null
holder.update(row as OpeningWeekdaysRow, prevRow, isEnabled)
val nextRow = if (rows.lastIndex > position) rows[position + 1] as? OpeningWeekdaysRow else null
holder.update(row as OpeningWeekdaysRow, prevRow, nextRow, isEnabled)
}
is OffDaysViewHolder -> {
holder.update(row as OffDaysRow, isEnabled)
Expand Down Expand Up @@ -233,15 +236,16 @@ class RegularOpeningHoursAdapter(
}
}

fun update(row: OpeningWeekdaysRow, rowBefore: OpeningWeekdaysRow?, isEnabled: Boolean) {
fun update(row: OpeningWeekdaysRow, rowBefore: OpeningWeekdaysRow?, nextRow: OpeningWeekdaysRow?, isEnabled: Boolean) {
binding.weekdaysLabel.text =
if (rowBefore != null && row.weekdays == rowBefore.weekdays) ""
else if (row.weekdays.isSelectionEmpty()) "(" + context.resources.getString(R.string.quest_openingHours_unspecified_range) + ")"
else if (rowBefore != null && row.weekdays.isSelectionEmpty()) "(" + context.resources.getString(R.string.quest_openingHours_unspecified_range) + ")"
else row.weekdays.toLocalizedString(context.resources)

binding.weekdaysLabel.setOnClickListener {
openSetWeekdaysDialog(row.weekdays) { weekdays ->
row.weekdays = weekdays
notifyItemChanged(adapterPosition)
notifyItemRangeChanged(adapterPosition, if (nextRow != null) 2 else 1)
}
}

Expand Down Expand Up @@ -292,9 +296,9 @@ class RegularOpeningHoursAdapter(

private fun getWeekdaysSuggestion(isFirst: Boolean): Weekdays {
if (isFirst) {
val firstWorkDayIdx = Weekdays.getWeekdayIndex(countryInfo.firstDayOfWorkweek)
val firstWorkDayIdx = Weekdays.getWeekdayIndex(firstDayOfWorkweek)
val result = BooleanArray(Weekdays.OSM_ABBR_WEEKDAYS.size)
for (i in 0 until countryInfo.regularShoppingDays) {
for (i in 0 until regularShoppingDays) {
result[(i + firstWorkDayIdx) % Weekdays.WEEKDAY_COUNT] = true
}
return Weekdays(result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
import de.westnordost.streetcomplete.data.osm.osmquests.Tags
import de.westnordost.streetcomplete.data.user.achievements.QuestTypeAchievement.BICYCLIST

class AddBikeParkingFee : OsmFilterQuestType<Fee>() {
class AddBikeParkingFee : OsmFilterQuestType<FeeAndMaxStay>() {

// element selection logic by @DerDings in #2507
override val elementFilter = """
Expand All @@ -31,6 +31,6 @@ class AddBikeParkingFee : OsmFilterQuestType<Fee>() {

override fun createForm() = AddParkingFeeForm()

override fun applyAnswerTo(answer: Fee, tags: Tags, timestampEdited: Long) =
override fun applyAnswerTo(answer: FeeAndMaxStay, tags: Tags, timestampEdited: Long) =
answer.applyTo(tags)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType
import de.westnordost.streetcomplete.data.osm.osmquests.Tags
import de.westnordost.streetcomplete.data.user.achievements.QuestTypeAchievement.CAR

class AddParkingFee : OsmFilterQuestType<Fee>() {
class AddParkingFee : OsmFilterQuestType<FeeAndMaxStay>() {

override val elementFilter = """
nodes, ways, relations with amenity = parking
Expand All @@ -25,6 +25,6 @@ class AddParkingFee : OsmFilterQuestType<Fee>() {

override fun createForm() = AddParkingFeeForm()

override fun applyAnswerTo(answer: Fee, tags: Tags, timestampEdited: Long) =
override fun applyAnswerTo(answer: FeeAndMaxStay, tags: Tags, timestampEdited: Long) =
answer.applyTo(tags)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,120 +2,122 @@ package de.westnordost.streetcomplete.quests.parking_fee

import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.core.view.isGone
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.databinding.QuestFeeHoursBinding
import de.westnordost.streetcomplete.databinding.QuestMaxstayBinding
import de.westnordost.streetcomplete.osm.opening_hours.parser.toOpeningHoursRules
import de.westnordost.streetcomplete.quests.AbstractQuestFormAnswerFragment
import de.westnordost.streetcomplete.quests.AnswerItem
import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningHoursRow
import de.westnordost.streetcomplete.quests.opening_hours.adapter.RegularOpeningHoursAdapter
import de.westnordost.streetcomplete.util.AdapterDataChangedWatcher
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import de.westnordost.streetcomplete.quests.parking_fee.AddParkingFeeForm.Mode.*
import de.westnordost.streetcomplete.view.DurationUnit
import de.westnordost.streetcomplete.view.TimeRestriction.AT_ANY_TIME
import de.westnordost.streetcomplete.view.TimeRestriction.EXCEPT_AT_HOURS
import de.westnordost.streetcomplete.view.TimeRestriction.ONLY_AT_HOURS

class AddParkingFeeForm : AbstractQuestFormAnswerFragment<Fee>() {
class AddParkingFeeForm : AbstractQuestFormAnswerFragment<FeeAndMaxStay>() {

override val contentLayoutResId = R.layout.quest_fee_hours
private val binding by contentViewBinding(QuestFeeHoursBinding::bind)
private var binding: ViewBinding? = null

override val buttonPanelAnswers get() =
if (!isDefiningHours) listOf(
AnswerItem(R.string.quest_generic_hasFeature_no) { applyAnswer(HasNoFee) },
AnswerItem(R.string.quest_generic_hasFeature_yes) { applyAnswer(HasFee) }
if (mode == FEE_YES_NO) listOf(
AnswerItem(R.string.quest_generic_hasFeature_no) { applyAnswer(FeeAndMaxStay(HasNoFee)) },
AnswerItem(R.string.quest_generic_hasFeature_yes) { applyAnswer(FeeAndMaxStay(HasFee)) }
)
else emptyList()

override val otherAnswers = listOf(
AnswerItem(R.string.quest_fee_answer_hours) { isDefiningHours = true }
AnswerItem(R.string.quest_fee_answer_hours) { mode = FEE_AT_HOURS },
AnswerItem(R.string.quest_fee_answer_no_but_maxstay) { mode = MAX_STAY },
)

private lateinit var openingHoursAdapter: RegularOpeningHoursAdapter

private var content: ViewGroup? = null

private var isDefiningHours: Boolean = false
private var mode: Mode = FEE_YES_NO
set(value) {
field = value

content?.isGone = !value
binding = when (mode) {
FEE_YES_NO -> null
FEE_AT_HOURS -> QuestFeeHoursBinding.bind(setContentView(R.layout.quest_fee_hours))
MAX_STAY -> QuestMaxstayBinding.bind(setContentView(R.layout.quest_maxstay))
}
onContentViewBound()
updateButtonPanel()
}
private var isFeeOnlyAtHours: Boolean = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

openingHoursAdapter = RegularOpeningHoursAdapter(requireContext(), countryInfo)
openingHoursAdapter.rows = loadOpeningHoursData(savedInstanceState).toMutableList()
openingHoursAdapter.registerAdapterDataObserver(AdapterDataChangedWatcher { checkIsFormComplete() })
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

content = view.findViewById(R.id.content)

// must be read here because setting these values effects the UI
isFeeOnlyAtHours = savedInstanceState?.getBoolean(IS_FEE_ONLY_AT_HOURS, true) ?: true
isDefiningHours = savedInstanceState?.getBoolean(IS_DEFINING_HOURS) ?: false

binding.openingHoursList.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
binding.openingHoursList.adapter = openingHoursAdapter
binding.openingHoursList.isNestedScrollingEnabled = false
mode = savedInstanceState?.getString(MODE)?.let { valueOf(it) } ?: FEE_YES_NO
checkIsFormComplete()
}

binding.addTimesButton.setOnClickListener { openingHoursAdapter.addNewWeekdays() }

val spinnerItems = listOf(
getString(R.string.quest_fee_only_at_hours),
getString(R.string.quest_fee_not_at_hours)
)
binding.selectFeeOnlyAtHours.adapter = ArrayAdapter(requireContext(), R.layout.spinner_item_centered, spinnerItems)
binding.selectFeeOnlyAtHours.setSelection(if (isFeeOnlyAtHours) 0 else 1)
binding.selectFeeOnlyAtHours.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
isFeeOnlyAtHours = position == 0
}

override fun onNothingSelected(parent: AdapterView<*>) {}
private fun onContentViewBound() {
val binding = binding
if (binding is QuestFeeHoursBinding) {
binding.timesView.firstDayOfWorkweek = countryInfo.firstDayOfWorkweek
binding.timesView.regularShoppingDays = countryInfo.regularShoppingDays
binding.timesView.selectableTimeRestrictions = listOf(ONLY_AT_HOURS, EXCEPT_AT_HOURS)
binding.timesView.onInputChanged = { checkIsFormComplete() }
} else if (binding is QuestMaxstayBinding) {
binding.timesView.firstDayOfWorkweek = countryInfo.firstDayOfWorkweek
binding.timesView.regularShoppingDays = countryInfo.regularShoppingDays
binding.timesView.onInputChanged = { checkIsFormComplete() }
binding.durationInput.onInputChanged = { checkIsFormComplete() }
}
}

override fun onDestroyView() {
super.onDestroyView()
content = null
binding = null
}

override fun onClickOk() {
val times = openingHoursAdapter.createOpeningHours()
applyAnswer(if (isFeeOnlyAtHours) HasFeeAtHours(times) else HasFeeExceptAtHours(times))
val binding = binding
if (binding is QuestFeeHoursBinding) {
val hours = binding.timesView.hours.toOpeningHoursRules()
val fee = when (binding.timesView.timeRestriction) {
AT_ANY_TIME -> HasFee
ONLY_AT_HOURS -> HasFeeAtHours(hours)
EXCEPT_AT_HOURS -> HasFeeExceptAtHours(hours)
}
applyAnswer(FeeAndMaxStay(fee))
} else if (binding is QuestMaxstayBinding) {
val duration = MaxstayDuration(
binding.durationInput.durationValue,
when (binding.durationInput.durationUnit) {
DurationUnit.MINUTES -> Maxstay.Unit.MINUTES
DurationUnit.HOURS -> Maxstay.Unit.HOURS
DurationUnit.DAYS -> Maxstay.Unit.DAYS
}
)
val hours = binding.timesView.hours.toOpeningHoursRules()
val maxstay = when (binding.timesView.timeRestriction) {
AT_ANY_TIME -> duration
ONLY_AT_HOURS -> MaxstayAtHours(duration, hours)
EXCEPT_AT_HOURS -> MaxstayExceptAtHours(duration, hours)
}
applyAnswer(FeeAndMaxStay(HasNoFee, maxstay))
}
}

private fun loadOpeningHoursData(savedInstanceState: Bundle?): List<OpeningHoursRow> =
savedInstanceState?.let { Json.decodeFromString(it.getString(OPENING_HOURS_DATA)!!) } ?: emptyList()

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(OPENING_HOURS_DATA, Json.encodeToString(openingHoursAdapter.rows))
outState.putBoolean(IS_DEFINING_HOURS, isDefiningHours)
outState.putBoolean(IS_FEE_ONLY_AT_HOURS, isFeeOnlyAtHours)
outState.putString(MODE, mode.name)
}

override fun isRejectingClose() =
isDefiningHours && openingHoursAdapter.rows.isEmpty()
override fun isRejectingClose() = when (val binding = binding) {
is QuestFeeHoursBinding -> binding.timesView.hours.isNotEmpty()
is QuestMaxstayBinding -> binding.timesView.isComplete || binding.durationInput.durationValue > 0.0
else -> false
}

override fun isFormComplete() =
isDefiningHours && openingHoursAdapter.rows.isNotEmpty()
override fun isFormComplete() = when (val binding = binding) {
is QuestFeeHoursBinding -> binding.timesView.hours.isNotEmpty()
is QuestMaxstayBinding -> binding.timesView.isComplete && binding.durationInput.durationValue > 0.0
else -> false
}

companion object {
private const val OPENING_HOURS_DATA = "oh_data"
private const val IS_FEE_ONLY_AT_HOURS = "oh_fee_only_at"
private const val IS_DEFINING_HOURS = "oh"
private const val MODE = "mode"
}

private enum class Mode { FEE_YES_NO, FEE_AT_HOURS, MAX_STAY }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ sealed class Fee

object HasFee : Fee()
object HasNoFee : Fee()
data class HasFeeAtHours(val openingHours: OpeningHoursRuleList) : Fee()
data class HasFeeExceptAtHours(val openingHours: OpeningHoursRuleList) : Fee()
data class HasFeeAtHours(val hours: OpeningHoursRuleList) : Fee()
data class HasFeeExceptAtHours(val hours: OpeningHoursRuleList) : Fee()

fun Fee.applyTo(tags: Tags) {
when (this) {
Expand All @@ -23,11 +23,11 @@ fun Fee.applyTo(tags: Tags) {
}
is HasFeeAtHours -> {
tags.updateWithCheckDate("fee", "no")
tags["fee:conditional"] = "yes @ ($openingHours)"
tags["fee:conditional"] = "yes @ ($hours)"
}
is HasFeeExceptAtHours -> {
tags.updateWithCheckDate("fee", "yes")
tags["fee:conditional"] = "no @ ($openingHours)"
tags["fee:conditional"] = "no @ ($hours)"
}
}
}
Loading

0 comments on commit e230b76

Please sign in to comment.