Skip to content

Commit

Permalink
Merge branch 'resurvey-opening_hours-new' into resurvey-new
Browse files Browse the repository at this point in the history
  • Loading branch information
westnordost committed Aug 29, 2020
2 parents 70cb6cd + 1b04ee3 commit 8b2b7db
Show file tree
Hide file tree
Showing 38 changed files with 1,668 additions and 1,037 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ dependencies {

// config files
implementation 'com.esotericsoftware.yamlbeans:yamlbeans:1.13'

// opening hours parser
implementation "ch.poole:OpeningHoursParser:0.22.0"
}

task generateMetadata(type: Exec) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,16 @@ abstract class AbstractQuestAnswerFragment<T> : AbstractBottomSheetFragment(), I

protected fun setButtonsView(resourceId: Int) {
otherAnswersButton.layoutParams = FlexboxLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
removeButtonsView()
activity?.layoutInflater?.inflate(resourceId, buttonPanel)
}

protected fun removeButtonsView() {
if (buttonPanel.childCount > 1) {
buttonPanel.removeViews(1, buttonPanel.childCount - 1)
}
}

@AnyThread open fun onMapOrientation(rotation: Float, tilt: Float) {
// default empty implementation
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ object QuestModule
AddMaxHeight(o), // OSRM and other routing engines
AddRailwayCrossingBarrier(o, r), // useful for routing
AddPostboxCollectionTimes(o, r),
AddOpeningHours(o, featureDictionaryFuture),
AddOpeningHours(o, featureDictionaryFuture, r),
DetailRoadSurface(o), // used by BRouter, OsmAnd, OSRM, graphhopper
AddBikeParkingCapacity(o, r), // used by cycle map layer on osm.org, OsmAnd
AddOrchardProduce(o),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,32 @@ import de.westnordost.streetcomplete.data.osm.changes.StringMapChangesBuilder
import de.westnordost.streetcomplete.data.elementfilter.ElementFiltersParser
import de.westnordost.streetcomplete.data.elementfilter.getQuestPrintStatement
import de.westnordost.streetcomplete.data.elementfilter.toGlobalOverpassBBox
import de.westnordost.streetcomplete.data.meta.updateWithCheckDate
import de.westnordost.streetcomplete.ktx.containsAny
import de.westnordost.streetcomplete.quests.opening_hours.parser.toOpeningHoursRows
import de.westnordost.streetcomplete.quests.opening_hours.parser.toOpeningHoursRules
import de.westnordost.streetcomplete.settings.ResurveyIntervalsStore
import java.util.concurrent.FutureTask

class AddOpeningHours (
private val overpassApi: OverpassMapDataAndGeometryApi,
private val featureDictionaryFuture: FutureTask<FeatureDictionary>
private val featureDictionaryFuture: FutureTask<FeatureDictionary>,
private val r: ResurveyIntervalsStore
) : OsmElementQuestType<OpeningHoursAnswer> {

/* See also AddWheelchairAccessBusiness and AddPlaceName, which has a similar list and is/should
be ordered in the same way for better overview */
private val filter by lazy { ElementFiltersParser().parse("""
nodes, ways, relations with
(
shop and shop !~ no|vacant
or amenity = bicycle_parking and bicycle_parking = building
or amenity = parking and parking = multi-storey
or amenity = recycling and recycling_type = centre
or tourism = information and information = office
or """.trimIndent() +
(
(
shop and shop !~ no|vacant
or amenity = bicycle_parking and bicycle_parking = building
or amenity = parking and parking = multi-storey
or amenity = recycling and recycling_type = centre
or tourism = information and information = office
or """.trimIndent() +

// The common list is shared by the name quest, the opening hours quest and the wheelchair quest.
// So when adding other tags to the common list keep in mind that they need to be appropriate for all those quests.
Expand Down Expand Up @@ -79,22 +86,32 @@ class AddOpeningHours (
"electronics_repair", "key_cutter", "stonemason"
)
).map { it.key + " ~ " + it.value.joinToString("|") }.joinToString("\n or ") + "\n" + """
)
and !opening_hours
)
or opening_hours older today -${r * 1} years
)
and (name or brand or noname = yes)
and !opening_hours and opening_hours:signed != no
and (access !~ private|no)
and (access !~ private|no)
and (name or brand or noname = yes)
and opening_hours:signed != no
""".trimIndent()
)}

override val commitMessage = "Add opening hours"
override val wikiLink = "Key:opening_hours"
override val icon = R.drawable.ic_quest_opening_hours

override fun getTitle(tags: Map<String, String>) =
if (hasProperName(tags))
R.string.quest_openingHours_name_title
else
R.string.quest_openingHours_no_name_title
override fun getTitle(tags: Map<String, String>): Int {
// treat invalid opening hours like it is not set at all
val hasValidOpeningHours = tags["opening_hours"]?.toOpeningHoursRules() != null
return if (hasValidOpeningHours) {
if (hasProperName(tags)) R.string.quest_openingHours_resurvey_name_title
else R.string.quest_openingHours_resurvey_no_name_title
} else {
if (hasProperName(tags)) R.string.quest_openingHours_name_title
else R.string.quest_openingHours_no_name_title
}
}

override fun getTitleArgs(tags: Map<String, String>, featureName: Lazy<String?>): Array<String> {
val name = tags["name"] ?: tags["brand"] ?: featureName.value
Expand All @@ -103,24 +120,42 @@ class AddOpeningHours (

override fun download(bbox: BoundingBox, handler: (element: Element, geometry: ElementGeometry?) -> Unit): Boolean {
return overpassApi.query(getOverpassQuery(bbox)) { element, geometry ->
// only show places that can be named somehow
if (hasName(element.tags)) handler(element, geometry)
if (isApplicableTo(element)) handler(element, geometry)
}
}

override fun isApplicableTo(element: Element) =
filter.matches(element) && hasName(element.tags)
override fun isApplicableTo(element: Element) : Boolean {
if (!filter.matches(element)) return false
// only show places that can be named somehow
if (!hasName(element.tags)) return false
// no opening_hours yet -> new survey
val oh = element.tags?.get("opening_hours") ?: return true
// invalid opening_hours rules -> applicable because we want to ask for opening hours again
val rules = oh.toOpeningHoursRules() ?: return true
// only display supported rules
return rules.toOpeningHoursRows() != null
}

override fun createForm() = AddOpeningHoursForm()

override fun applyAnswerTo(answer: OpeningHoursAnswer, changes: StringMapChangesBuilder) {
when(answer) {
is RegularOpeningHours -> changes.add("opening_hours", answer.times.joinToString(";"))
is AlwaysOpen -> changes.add("opening_hours", "24/7")
is NoOpeningHoursSign -> changes.add("opening_hours:signed", "no")
is RegularOpeningHours -> {
changes.updateWithCheckDate("opening_hours", answer.hours.toString())
changes.deleteIfPreviously("opening_hours:signed", "no")
}
is AlwaysOpen -> {
changes.updateWithCheckDate("opening_hours", "24/7")
changes.deleteIfPreviously("opening_hours:signed", "no")
}
is DescribeOpeningHours -> {
val text = answer.text.replace("\"","")
changes.add("opening_hours", "\"$text\"")
changes.updateWithCheckDate("opening_hours", "\"$text\"")
changes.deleteIfPreviously("opening_hours:signed", "no")
}
is NoOpeningHoursSign -> {
changes.addOrModify("opening_hours:signed", "no")
// don't delete current opening hours: these may be the correct hours, they are just not visible anywhere on the door
}
}
}
Expand All @@ -136,3 +171,7 @@ class AddOpeningHours (
private fun hasFeatureName(tags: Map<String, String>?): Boolean =
tags?.let { featureDictionaryFuture.get().byTags(it).find().isNotEmpty() } ?: false
}

private fun StringMapChangesBuilder.deleteIfPreviously(key: String, previousValue: String) {
if (getPreviousValue(key) == previousValue) delete(key)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@ import javax.inject.Inject
import de.westnordost.streetcomplete.Injector
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.quests.AbstractQuestFormAnswerFragment
import de.westnordost.streetcomplete.quests.opening_hours.adapter.AddOpeningHoursAdapter
import de.westnordost.streetcomplete.quests.opening_hours.adapter.OpeningMonthsRow
import de.westnordost.streetcomplete.util.AdapterDataChangedWatcher
import de.westnordost.streetcomplete.util.Serializer


import android.view.Menu.NONE
import androidx.core.view.isGone
import androidx.recyclerview.widget.RecyclerView
import de.westnordost.streetcomplete.quests.OtherAnswer
import de.westnordost.streetcomplete.ktx.toObject
import de.westnordost.streetcomplete.quests.opening_hours.adapter.*
import de.westnordost.streetcomplete.quests.opening_hours.parser.toOpeningHoursRows
import de.westnordost.streetcomplete.quests.opening_hours.parser.toOpeningHoursRules
import kotlinx.android.synthetic.main.quest_opening_hours.*

class AddOpeningHoursForm : AbstractQuestFormAnswerFragment<OpeningHoursAnswer>() {
Expand All @@ -35,11 +37,16 @@ class AddOpeningHoursForm : AbstractQuestFormAnswerFragment<OpeningHoursAnswer>(
OtherAnswer(R.string.quest_openingHours_no_sign) { confirmNoSign() },
OtherAnswer(R.string.quest_openingHours_answer_no_regular_opening_hours) { showInputCommentDialog() },
OtherAnswer(R.string.quest_openingHours_answer_247) { showConfirm24_7Dialog() },
OtherAnswer(R.string.quest_openingHours_answer_seasonal_opening_hours) { openingHoursAdapter.changeToMonthsMode() }
OtherAnswer(R.string.quest_openingHours_answer_seasonal_opening_hours) {
setAsResurvey(false)
openingHoursAdapter.changeToMonthsMode()
}
)

private lateinit var openingHoursAdapter: AddOpeningHoursAdapter

private var isDisplayingPreviousOpeningHours: Boolean = false

@Inject internal lateinit var serializer: Serializer

init {
Expand All @@ -49,59 +56,75 @@ class AddOpeningHoursForm : AbstractQuestFormAnswerFragment<OpeningHoursAnswer>(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val isAlsoAddingMonths = savedInstanceState?.getBoolean(IS_ADD_MONTHS_MODE) == true
val viewData = loadOpeningHoursData(savedInstanceState)

openingHoursAdapter = AddOpeningHoursAdapter(viewData, requireContext(), countryInfo)
openingHoursAdapter.isDisplayMonths = isAlsoAddingMonths
openingHoursAdapter = AddOpeningHoursAdapter(requireContext(), countryInfo)
openingHoursAdapter.registerAdapterDataObserver(AdapterDataChangedWatcher { checkIsFormComplete() })
}

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

if (savedInstanceState != null) {
onLoadInstanceState(savedInstanceState)
} else {
val oh = osmElement!!.tags!!["opening_hours"]
val rows = oh?.toOpeningHoursRules()?.toOpeningHoursRows()
if (rows != null) {
openingHoursAdapter.rows = rows.toMutableList()
setAsResurvey(true)
} else {
setAsResurvey(false)
}
}

openingHoursList.layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
openingHoursList.adapter = openingHoursAdapter
openingHoursList.isNestedScrollingEnabled = false
checkIsFormComplete()

addTimesButton.setOnClickListener { this.onClickAddButton(it) }
addTimesButton.setOnClickListener { onClickAddButton(it) }
}

private fun loadOpeningHoursData(savedInstanceState: Bundle?): List<OpeningMonthsRow> =
if (savedInstanceState != null) {
serializer.toObject<ArrayList<OpeningMonthsRow>>(savedInstanceState.getByteArray(OPENING_HOURS_DATA)!!)
} else {
listOf(OpeningMonthsRow())
}

private fun onClickAddButton(v: View) {
if (!openingHoursAdapter.isDisplayMonths) {
openingHoursAdapter.addNewWeekdays()
} else {
val rows = openingHoursAdapter.rows

val addMonthAvailable = rows.any { it is OpeningMonthsRow }
val addTimeAvailable = rows.isNotEmpty() && rows.last() is OpeningWeekdaysRow
val addOffDayAvailable = rows.isNotEmpty() && rows.last() is OpeningWeekdaysRow

if (addMonthAvailable || addTimeAvailable || addOffDayAvailable) {
val popup = PopupMenu(requireContext(), v)
popup.menu.add(NONE, 0, NONE, R.string.quest_openingHours_add_weekdays)
popup.menu.add(NONE, 1, NONE, R.string.quest_openingHours_add_months)
if (addTimeAvailable) popup.menu.add(NONE, 0, NONE, R.string.quest_openingHours_add_hours)
popup.menu.add(NONE, 1, NONE, R.string.quest_openingHours_add_weekdays)
if (addOffDayAvailable) popup.menu.add(NONE, 2, NONE, R.string.quest_openingHours_add_off_days)
if (addMonthAvailable) popup.menu.add(NONE, 3, NONE, R.string.quest_openingHours_add_months)
popup.setOnMenuItemClickListener { item ->
when(item.itemId) {
0 -> openingHoursAdapter.addNewWeekdays()
1 -> openingHoursAdapter.addNewMonths()
0 -> openingHoursAdapter.addNewHours()
1 -> openingHoursAdapter.addNewWeekdays()
2 -> openingHoursAdapter.addNewOffDays()
3 -> openingHoursAdapter.addNewMonths()
}
true
}
popup.show()
} else {
openingHoursAdapter.addNewWeekdays()
}
}

private fun onLoadInstanceState(savedInstanceState: Bundle) {
openingHoursAdapter.rows = serializer.toObject<ArrayList<OpeningHoursRow>>(savedInstanceState.getByteArray(OPENING_HOURS_DATA)!!).toMutableList()
isDisplayingPreviousOpeningHours = savedInstanceState.getBoolean(IS_DISPLAYING_PREVIOUS_HOURS)
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val serializedTimes = serializer.toBytes(ArrayList(openingHoursAdapter.monthsRows))
outState.putByteArray(OPENING_HOURS_DATA, serializedTimes)
outState.putBoolean(IS_ADD_MONTHS_MODE, openingHoursAdapter.isDisplayMonths)
outState.putByteArray(OPENING_HOURS_DATA, serializer.toBytes(ArrayList(openingHoursAdapter.rows)))
outState.putBoolean(IS_DISPLAYING_PREVIOUS_HOURS, isDisplayingPreviousOpeningHours)
}

override fun onClickOk() {
applyAnswer(RegularOpeningHours(openingHoursAdapter.createOpeningMonths()))
applyAnswer(RegularOpeningHours(openingHoursAdapter.createOpeningHours()))
}

private fun showInputCommentDialog() {
Expand All @@ -126,10 +149,31 @@ class AddOpeningHoursForm : AbstractQuestFormAnswerFragment<OpeningHoursAnswer>(
.show()
}

private fun setAsResurvey(resurvey: Boolean) {
openingHoursAdapter.isEnabled = !resurvey
isDisplayingPreviousOpeningHours = resurvey
addTimesButton.isGone = resurvey
if (resurvey) {
setButtonsView(R.layout.quest_buttonpanel_yes_no)
requireView().findViewById<View>(R.id.noButton).setOnClickListener {
setAsResurvey(false)
}
requireView().findViewById<View>(R.id.yesButton).setOnClickListener {
applyAnswer(RegularOpeningHours(
osmElement!!.tags!!["opening_hours"]!!.toOpeningHoursRules()!!
))
}
} else {
removeButtonsView()
}
}

private fun showConfirm24_7Dialog() {
AlertDialog.Builder(requireContext())
.setMessage(R.string.quest_openingHours_24_7_confirmation)
.setPositiveButton(android.R.string.yes) { _, _ -> applyAnswer(AlwaysOpen) }
.setPositiveButton(android.R.string.yes) { _, _ ->
applyAnswer(AlwaysOpen)
}
.setNegativeButton(android.R.string.no, null)
.show()
}
Expand All @@ -142,10 +186,10 @@ class AddOpeningHoursForm : AbstractQuestFormAnswerFragment<OpeningHoursAnswer>(
.show()
}

override fun isFormComplete() = openingHoursAdapter.createOpeningMonths().joinToString(";").isNotEmpty()
override fun isFormComplete() = openingHoursAdapter.rows.isNotEmpty() && !isDisplayingPreviousOpeningHours

companion object {
private const val OPENING_HOURS_DATA = "oh_data"
private const val IS_ADD_MONTHS_MODE = "oh_add_months"
private const val IS_DISPLAYING_PREVIOUS_HOURS = "oh_is_displaying_previous_hours"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package de.westnordost.streetcomplete.quests.opening_hours

import android.content.Context
import androidx.appcompat.app.AlertDialog

import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.quests.opening_hours.model.Months

object MonthsPickerDialog {

fun show(context: Context, months: Months?, callback: (Months) -> Unit): AlertDialog {
val selection = months?.selection ?: BooleanArray(Months.MONTHS_COUNT)

return AlertDialog.Builder(context)
.setTitle(R.string.quest_openingHours_chooseMonthsTitle)
.setMultiChoiceItems(Months.getNames(), selection) { _, _, _ -> }
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok) { _, _ -> callback(Months(selection)) }
.show()
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package de.westnordost.streetcomplete.quests.opening_hours

import de.westnordost.streetcomplete.quests.opening_hours.model.OpeningMonths
import de.westnordost.streetcomplete.quests.opening_hours.model.OpeningHoursRuleList

sealed class OpeningHoursAnswer

data class RegularOpeningHours(val times:List<OpeningMonths>) : OpeningHoursAnswer()
data class RegularOpeningHours(val hours: OpeningHoursRuleList) : OpeningHoursAnswer()
object AlwaysOpen : OpeningHoursAnswer()
object NoOpeningHoursSign : OpeningHoursAnswer()
data class DescribeOpeningHours(val text:String) : OpeningHoursAnswer()
object NoOpeningHoursSign : OpeningHoursAnswer()
Loading

0 comments on commit 8b2b7db

Please sign in to comment.