diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/meta/OsmTaggings.kt b/app/src/main/java/de/westnordost/streetcomplete/data/meta/OsmTaggings.kt index 856319164c..6a7bf6e4c8 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/data/meta/OsmTaggings.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/data/meta/OsmTaggings.kt @@ -32,6 +32,12 @@ val MAXSPEED_TYPE_KEYS = setOf( "zone:traffic" ) +val SIDEWALK_SURFACE_KEYS = setOf( + "sidewalk:both:surface", + "sidewalk:left:surface", + "sidewalk:right:surface" +) + const val SURVEY_MARK_KEY = "check_date" // generated by "make update" from https://github.com/mnalis/StreetComplete-taginfo-categorize/ diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/Sidewalk.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/Sidewalk.kt similarity index 78% rename from app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/Sidewalk.kt rename to app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/Sidewalk.kt index ea4d0d50dc..578646cca4 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/Sidewalk.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/Sidewalk.kt @@ -1,9 +1,9 @@ -package de.westnordost.streetcomplete.quests.sidewalk +package de.westnordost.streetcomplete.osm.sidewalk import de.westnordost.streetcomplete.data.osm.osmquests.Tags -import de.westnordost.streetcomplete.quests.sidewalk.Sidewalk.NO -import de.westnordost.streetcomplete.quests.sidewalk.Sidewalk.SEPARATE -import de.westnordost.streetcomplete.quests.sidewalk.Sidewalk.YES +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.NO +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.SEPARATE +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.YES data class SidewalkSides(val left: Sidewalk, val right: Sidewalk) diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkParser.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkParser.kt new file mode 100644 index 0000000000..7765d06938 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/osm/sidewalk/SidewalkParser.kt @@ -0,0 +1,53 @@ +package de.westnordost.streetcomplete.osm.sidewalk + +import de.westnordost.streetcomplete.ktx.containsAny +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.NO +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.SEPARATE +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.YES + + +data class LeftAndRightSidewalk(val left: Sidewalk?, val right: Sidewalk?) + +/** Returns on which sides are sidewalks. Returns null if tagging is unknown */ +fun createSidewalkSides(tags: Map): LeftAndRightSidewalk? { + if (!tags.keys.containsAny(KNOWN_SIDEWALK_KEYS)) return null + + val sidewalk = createSidewalksDefault(tags) + if (sidewalk != null) return sidewalk + + // alternative tagging + val altSidewalk = createSidewalksAlternative(tags) + if (altSidewalk != null) return altSidewalk + + return null +} + +private fun createSidewalksDefault(tags: Map): LeftAndRightSidewalk? = when(tags["sidewalk"]) { + "left" -> LeftAndRightSidewalk(left = YES, right = NO) + "right" -> LeftAndRightSidewalk(left = NO, right = YES) + "both" -> LeftAndRightSidewalk(left = YES, right = YES) + "no", "none" -> LeftAndRightSidewalk(left = NO, right = NO) + "separate" -> LeftAndRightSidewalk(left = SEPARATE, right = SEPARATE) + else -> null +} + +private fun createSidewalksAlternative(tags: Map): LeftAndRightSidewalk? { + val sidewalkLeft = tags["sidewalk:both"] ?: tags["sidewalk:left"] + val sidewalkRight = tags["sidewalk:both"] ?: tags["sidewalk:right"] + return if (sidewalkLeft != null || sidewalkRight != null) { + LeftAndRightSidewalk(left = createSidewalkSide(sidewalkLeft), right = createSidewalkSide(sidewalkRight)) + } else { + null + } +} + +private fun createSidewalkSide(tag: String?): Sidewalk? = when(tag) { + "yes" -> YES + "no" -> NO + "separate" -> SEPARATE + else -> null +} + +private val KNOWN_SIDEWALK_KEYS = listOf( + "sidewalk", "sidewalk:left", "sidewalk:right", "sidewalk:both" +) diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt index ac2717b70d..a1b71fc096 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt @@ -120,6 +120,7 @@ import de.westnordost.streetcomplete.quests.surface.AddFootwayPartSurface import de.westnordost.streetcomplete.quests.surface.AddPathSurface import de.westnordost.streetcomplete.quests.surface.AddPitchSurface import de.westnordost.streetcomplete.quests.surface.AddRoadSurface +import de.westnordost.streetcomplete.quests.surface.AddSidewalkSurface import de.westnordost.streetcomplete.quests.tactile_paving.AddTactilePavingBusStop import de.westnordost.streetcomplete.quests.tactile_paving.AddTactilePavingCrosswalk import de.westnordost.streetcomplete.quests.tactile_paving.AddTactilePavingKerb @@ -406,6 +407,7 @@ whether the postbox is still there in countries in which it is enabled */ AddCyclewaySegregation(), // Cyclosm, Valhalla, Bike Citizens Bicycle Navigation... AddFootwayPartSurface(), AddCyclewayPartSurface(), + AddSidewalkSurface(), AddCyclewayWidth(arSupportChecker), // should be after cycleway segregation /* should best be after road surface because it excludes unpaved roads, also, need to search diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt index 9ba079aea9..595a895015 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalk.kt @@ -14,6 +14,8 @@ import de.westnordost.streetcomplete.osm.estimateCycleTrackWidth import de.westnordost.streetcomplete.osm.estimateParkingOffRoadWidth import de.westnordost.streetcomplete.osm.estimateRoadwayWidth import de.westnordost.streetcomplete.osm.guessRoadwayWidth +import de.westnordost.streetcomplete.osm.sidewalk.SidewalkSides +import de.westnordost.streetcomplete.osm.sidewalk.applyTo import de.westnordost.streetcomplete.util.isNearAndAligned class AddSidewalk : OsmElementQuestType { diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkForm.kt index 92db3b14f1..448e166327 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/AddSidewalkForm.kt @@ -1,5 +1,7 @@ package de.westnordost.streetcomplete.quests.sidewalk +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk +import de.westnordost.streetcomplete.osm.sidewalk.SidewalkSides import androidx.appcompat.app.AlertDialog import de.westnordost.streetcomplete.R import de.westnordost.streetcomplete.quests.AStreetSideSelectFragment diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/SidewalkItem.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/SidewalkItem.kt index 65d8993aa4..b3522061db 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/SidewalkItem.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/sidewalk/SidewalkItem.kt @@ -1,11 +1,12 @@ package de.westnordost.streetcomplete.quests.sidewalk import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk import de.westnordost.streetcomplete.quests.StreetSideDisplayItem import de.westnordost.streetcomplete.quests.StreetSideItem -import de.westnordost.streetcomplete.quests.sidewalk.Sidewalk.NO -import de.westnordost.streetcomplete.quests.sidewalk.Sidewalk.SEPARATE -import de.westnordost.streetcomplete.quests.sidewalk.Sidewalk.YES +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.NO +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.SEPARATE +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.YES import de.westnordost.streetcomplete.view.image_select.DisplayItem import de.westnordost.streetcomplete.view.image_select.Item diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurface.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurface.kt new file mode 100644 index 0000000000..be89778b51 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurface.kt @@ -0,0 +1,124 @@ +package de.westnordost.streetcomplete.quests.surface + +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.data.meta.SIDEWALK_SURFACE_KEYS +import de.westnordost.streetcomplete.data.meta.hasCheckDateForKey +import de.westnordost.streetcomplete.data.meta.removeCheckDatesForKey +import de.westnordost.streetcomplete.data.meta.updateCheckDateForKey +import de.westnordost.streetcomplete.data.osm.osmquests.OsmFilterQuestType +import de.westnordost.streetcomplete.data.osm.osmquests.Tags +import de.westnordost.streetcomplete.data.user.achievements.QuestTypeAchievement.PEDESTRIAN +import de.westnordost.streetcomplete.data.user.achievements.QuestTypeAchievement.WHEELCHAIR + +class AddSidewalkSurface : OsmFilterQuestType() { + + // Only roads with 'complete' sidewalk tagging (at least one side has sidewalk, other side specified) + override val elementFilter = """ + ways with + highway ~ trunk|trunk_link|primary|primary_link|secondary|secondary_link|tertiary|tertiary_link|unclassified|residential + and area != yes + and motorroad != yes + and ( + sidewalk ~ both|left|right or + sidewalk:both = yes or + (sidewalk:left = yes and sidewalk:right ~ yes|no|separate) or + (sidewalk:right = yes and sidewalk:left ~ yes|no|separate) + ) + and ( + ${SIDEWALK_SURFACE_KEYS.joinToString(" and ") {"!$it"}} + or sidewalk:surface older today -8 years + ) + """ + + override val changesetComment = "Add surface of sidewalks" + override val wikiLink = "Key:sidewalk" + override val icon = R.drawable.ic_quest_sidewalk_surface + override val isSplitWayEnabled = true + override val questTypeAchievements = listOf(PEDESTRIAN, WHEELCHAIR) + override val defaultDisabledMessage = R.string.default_disabled_msg_difficult_and_time_consuming + + override fun getTitle(tags: Map) : Int = + R.string.quest_sidewalk_surface_title + + override fun createForm() = AddSidewalkSurfaceForm() + + override fun applyAnswerTo(answer: SidewalkSurfaceAnswer, tags: Tags, timestampEdited: Long) { + val leftChanged = answer.left?.let { sideSurfaceChanged(it, Side.LEFT, tags) } + val rightChanged = answer.right?.let { sideSurfaceChanged(it, Side.RIGHT, tags) } + + if (leftChanged == true) { + deleteSmoothnessKeys(Side.LEFT, tags) + deleteSmoothnessKeys(Side.BOTH, tags) + } + if (rightChanged == true) { + deleteSmoothnessKeys(Side.RIGHT, tags) + deleteSmoothnessKeys(Side.BOTH, tags) + } + + if (answer.left == answer.right) { + answer.left?.let { applySidewalkSurfaceAnswerTo(it, Side.BOTH, tags) } + deleteSidewalkSurfaceAnswerIfExists(Side.LEFT, tags) + deleteSidewalkSurfaceAnswerIfExists(Side.RIGHT, tags) + } else { + answer.left?.let { applySidewalkSurfaceAnswerTo(it, Side.LEFT, tags) } + answer.right?.let { applySidewalkSurfaceAnswerTo(it, Side.RIGHT, tags) } + deleteSidewalkSurfaceAnswerIfExists(Side.BOTH, tags) + } + deleteSidewalkSurfaceAnswerIfExists(null, tags) + + // only set the check date if nothing was changed or if check date was already set + if (!tags.hasChanges || tags.hasCheckDateForKey("sidewalk:surface")) { + tags.updateCheckDateForKey("sidewalk:surface") + } + } + + private enum class Side(val value: String) { + LEFT("left"), RIGHT("right"), BOTH("both") + } + + private fun sideSurfaceChanged(surface: SurfaceAnswer, side: Side, tags: Tags): Boolean { + val previousSideOsmValue = tags["sidewalk:${side.value}:surface"] + val previousBothOsmValue = tags["sidewalk:both:surface"] + val osmValue = surface.value.osmValue + + return previousSideOsmValue != null && previousSideOsmValue != osmValue + || previousBothOsmValue != null && previousBothOsmValue != osmValue + } + + private fun applySidewalkSurfaceAnswerTo(surface: SurfaceAnswer, side: Side, tags: Tags) + { + val sidewalkKey = "sidewalk:" + side.value + val sidewalkSurfaceKey = "$sidewalkKey:surface" + + tags[sidewalkSurfaceKey] = surface.value.osmValue + + // add/remove note - used to describe generic surfaces + if (surface.note != null) { + tags["$sidewalkSurfaceKey:note"] = surface.note + } else { + tags.remove("$sidewalkSurfaceKey:note") + } + // clean up old source tags - source should be in changeset tags + tags.remove("source:$sidewalkSurfaceKey") + } + + /** clear smoothness tags for the given side*/ + private fun deleteSmoothnessKeys(side: Side, tags: Tags) { + val sidewalkKey = "sidewalk:" + side.value + tags.remove("$sidewalkKey:smoothness") + tags.remove("$sidewalkKey:smoothness:date") + tags.removeCheckDatesForKey("$sidewalkKey:smoothness") + } + + /** clear previous answers for the given side */ + private fun deleteSidewalkSurfaceAnswerIfExists(side: Side?, tags: Tags) { + val sideVal = if (side == null) "" else ":" + side.value + val sidewalkSurfaceKey = "sidewalk$sideVal:surface" + + // only things are cleared that are set by this quest + // for example cycleway:surface should only be cleared by a cycleway surface quest etc. + tags.remove(sidewalkSurfaceKey) + tags.remove("$sidewalkSurfaceKey:note") + } + +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurfaceForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurfaceForm.kt new file mode 100644 index 0000000000..f82fa9119b --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurfaceForm.kt @@ -0,0 +1,242 @@ +package de.westnordost.streetcomplete.quests.surface + +import android.os.Bundle +import android.view.View +import androidx.annotation.AnyThread +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isGone +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry +import de.westnordost.streetcomplete.databinding.QuestStreetSidePuzzleWithLastAnswerButtonBinding +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk +import de.westnordost.streetcomplete.osm.sidewalk.createSidewalkSides +import de.westnordost.streetcomplete.quests.AbstractQuestFormAnswerFragment +import de.westnordost.streetcomplete.quests.StreetSideRotater +import de.westnordost.streetcomplete.quests.sidewalk.imageResId +import de.westnordost.streetcomplete.quests.sidewalk.titleResId +import de.westnordost.streetcomplete.util.normalizeDegrees +import de.westnordost.streetcomplete.view.DrawableImage +import de.westnordost.streetcomplete.view.ResImage +import de.westnordost.streetcomplete.view.ResText +import de.westnordost.streetcomplete.view.RotatedCircleDrawable +import de.westnordost.streetcomplete.view.image_select.ImageListPickerDialog +import kotlin.math.absoluteValue + +class AddSidewalkSurfaceForm : AbstractQuestFormAnswerFragment() { + + override val contentLayoutResId = R.layout.quest_street_side_puzzle_with_last_answer_button + private val binding by contentViewBinding(QuestStreetSidePuzzleWithLastAnswerButtonBinding::bind) + + private val currentSidewalks get() = createSidewalkSides(osmElement!!.tags) + + override val contentPadding = false + + private var streetSideRotater: StreetSideRotater? = null + + private var isDefiningBothSides: Boolean = false + private var isLeftSideNotDefined: Boolean = false + private var isRightSideNotDefined: Boolean = false + + private var leftSide: SurfaceAnswer? = null + private var rightSide: SurfaceAnswer? = null + + private val isLeftHandTraffic get() = countryInfo.isLeftHandTraffic + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.puzzleView.onClickSideListener = { isRight -> showSurfaceSelectionDialog(isRight) } + + val defaultResId = + if (isLeftHandTraffic) R.drawable.ic_street_side_unknown_l + else R.drawable.ic_street_side_unknown + + binding.puzzleView.setLeftSideImage(ResImage(defaultResId)) + binding.puzzleView.setRightSideImage(ResImage(defaultResId)) + + initStateFromTags() + + streetSideRotater = StreetSideRotater( + binding.puzzleView, + binding.littleCompass.root, + elementGeometry as ElementPolylinesGeometry + ) + + showTapHint() + initLastAnswerButton() + checkIsFormComplete() + } + + private fun initStateFromTags() { + val left = currentSidewalks?.left + val right = currentSidewalks?.right + + if (left != null && right != null) { + isDefiningBothSides = (left == Sidewalk.YES) && (right == Sidewalk.YES) + isRightSideNotDefined = (right == Sidewalk.NO) || (right == Sidewalk.SEPARATE) + isLeftSideNotDefined = (left == Sidewalk.NO) || (left == Sidewalk.SEPARATE) + if (right == Sidewalk.NO || right == Sidewalk.SEPARATE) { + binding.puzzleView.setRightSideText(ResText(right.titleResId)) + binding.puzzleView.setRightSideImage(ResImage(right.imageResId)) + binding.puzzleView.onlyLeftSideClickable() + } + if (left == Sidewalk.NO || left == Sidewalk.SEPARATE) { + binding.puzzleView.setLeftSideText(ResText(left.titleResId)) + binding.puzzleView.setLeftSideImage(ResImage(left.imageResId)) + binding.puzzleView.onlyRightSideClickable() + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + rightSide?.let { outState.putString(SIDEWALK_SURFACE_RIGHT, it.value.name) } + leftSide?.let { outState.putString(SIDEWALK_SURFACE_LEFT, it.value.name) } + outState.putBoolean(DEFINE_BOTH_SIDES, isDefiningBothSides) + } + + @AnyThread + override fun onMapOrientation(rotation: Float, tilt: Float) { + streetSideRotater?.onMapOrientation(rotation, tilt) + } + + private fun showTapHint() { + if ((leftSide == null || rightSide == null) && !HAS_SHOWN_TAP_HINT) { + if (leftSide == null && !isLeftSideNotDefined) binding.puzzleView.showLeftSideTapHint() + if (rightSide == null && !isRightSideNotDefined) binding.puzzleView.showRightSideTapHint() + HAS_SHOWN_TAP_HINT = true + } + } + + private fun showBothSides() { + isDefiningBothSides = true + binding.puzzleView.showBothSides() + binding.puzzleView.bothSidesClickable() + updateLastAnswerButtonVisibility() + checkIsFormComplete() + } + + /* ---------------------------------- selection dialog -------------------------------------- */ + + private fun showSurfaceSelectionDialog(isRight: Boolean) { + val ctx = context ?: return + val items = (PAVED_SURFACES + UNPAVED_SURFACES + Surface.WOODCHIPS + GROUND_SURFACES + GENERIC_ROAD_SURFACES).toItems() + ImageListPickerDialog(ctx, items, R.layout.cell_labeled_image_select, 2) { + if (it.value!!.shouldBeDescribed) { + AlertDialog.Builder(requireContext()) + .setMessage(R.string.quest_surface_detailed_answer_impossible_confirmation) + .setPositiveButton(R.string.quest_generic_confirmation_yes) { _, _ -> + DescribeGenericSurfaceDialog(requireContext()) { description -> + onSelectedSide(SurfaceAnswer(it.value!!, description), isRight) + }.show() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } else { + onSelectedSide(SurfaceAnswer(it.value!!), isRight) + } + }.show() + } + + private fun onSelectedSide(surface: SurfaceAnswer, isRight: Boolean) { + val image = DrawableImage(RotatedCircleDrawable(resources.getDrawable(surface.value.asItem().drawableId!!))) + + if (isRight) { + binding.puzzleView.replaceRightSideFloatingIcon(image) + binding.puzzleView.replaceRightSideImage(ResImage(R.drawable.ic_sidewalk_illustration_yes)) + rightSide = surface + } else { + binding.puzzleView.replaceLeftSideFloatingIcon(image) + binding.puzzleView.replaceLeftSideImage(ResImage(R.drawable.ic_sidewalk_illustration_yes)) + leftSide = surface + } + updateLastAnswerButtonVisibility() + checkIsFormComplete() + } + + /* --------------------------------- last answer button ------------------------------------- */ + + private fun initLastAnswerButton() { + updateLastAnswerButtonVisibility() + + lastSelection?.let { + binding.lastAnswerButton.leftSideImageView.setImageResource(it.left.value.asItem().drawableId!!) + binding.lastAnswerButton.rightSideImageView.setImageResource(it.right.value.asItem().drawableId!!) + } + + binding.lastAnswerButton.root.setOnClickListener { applyLastSelection() } + } + + private fun updateLastAnswerButtonVisibility() { + val formIsPrefilled = leftSide != null || rightSide != null + val lastAnswerWasForBothSides = (lastSelection?.left != null && lastSelection?.right != null) + val isDefiningBothSides = isDefiningBothSides && lastAnswerWasForBothSides + + binding.lastAnswerButton.root.isGone = + lastSelection == null || formIsPrefilled || !isDefiningBothSides + } + + private fun saveLastSelection() { + val leftSide = leftSide + val rightSide = rightSide + if (leftSide != null && rightSide != null) { + lastSelection = + if (isRoadDisplayedUpsideDown()) + LastSidewalkSurfaceSelection(rightSide, leftSide) + else + LastSidewalkSurfaceSelection(leftSide, rightSide) + } + } + + private fun applyLastSelection() { + val lastSelection = lastSelection ?: return + if (isRoadDisplayedUpsideDown()) { + onSelectedSide(lastSelection.right, false) + onSelectedSide(lastSelection.left, true) + } else { + onSelectedSide(lastSelection.left, false) + onSelectedSide(lastSelection.right, true) + } + } + + private fun isRoadDisplayedUpsideDown(): Boolean = + binding.puzzleView.streetRotation.normalizeDegrees(-180f).absoluteValue > 90f + + /* --------------------------------------- apply answer ------------------------------------- */ + + override fun onClickOk() { + val leftSide = leftSide + val rightSide = rightSide + + val answer = SidewalkSurfaceAnswer( + left = leftSide, + right = rightSide + ) + + applyAnswer(answer) + + saveLastSelection() + } + + override fun isFormComplete() = ( + if (isDefiningBothSides) leftSide != null && rightSide != null + else leftSide != null || rightSide != null + ) + + override fun isRejectingClose() = (leftSide != null || rightSide != null) + + companion object { + private const val SIDEWALK_SURFACE_LEFT = "sidewalk_surface_left" + private const val SIDEWALK_SURFACE_RIGHT = "sidewalk_surface_right" + private const val DEFINE_BOTH_SIDES = "define_both_sides" + + private var HAS_SHOWN_TAP_HINT = false + + private var lastSelection: LastSidewalkSurfaceSelection? = null + } +} + +private data class LastSidewalkSurfaceSelection( + val left: SurfaceAnswer, + val right: SurfaceAnswer +) diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/surface/SidewalkSurfaceAnswer.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/SidewalkSurfaceAnswer.kt new file mode 100644 index 0000000000..6404191853 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/surface/SidewalkSurfaceAnswer.kt @@ -0,0 +1,9 @@ +package de.westnordost.streetcomplete.quests.surface + +data class SidewalkSurfaceAnswer( + val left: SurfaceAnswer?, + val right: SurfaceAnswer?, +) + +data class SidewalkSurfaceSide(val surface: Surface) + diff --git a/app/src/main/java/de/westnordost/streetcomplete/view/StreetSideSelectPuzzle.kt b/app/src/main/java/de/westnordost/streetcomplete/view/StreetSideSelectPuzzle.kt index 5c940651f0..9e2f6fba0b 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/view/StreetSideSelectPuzzle.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/view/StreetSideSelectPuzzle.kt @@ -203,6 +203,21 @@ class StreetSideSelectPuzzle @JvmOverloads constructor( binding.strut.layoutParams = params } + fun onlyLeftSideClickable() { + binding.leftSideContainer.isClickable = true + binding.rightSideContainer.isClickable = false + } + + fun onlyRightSideClickable() { + binding.rightSideContainer.isClickable = true + binding.leftSideContainer.isClickable = false + } + + fun bothSidesClickable() { + binding.rightSideContainer.isClickable = true + binding.leftSideContainer.isClickable = true + } + private fun replace(image: Image?, imgView: ImageView, flip180Degrees: Boolean) { val width = if (onlyShowingOneSide) binding.rotateContainer.width else binding.rotateContainer.width / 2 if (width == 0) return diff --git a/app/src/main/res/drawable/ic_quest_sidewalk_surface.xml b/app/src/main/res/drawable/ic_quest_sidewalk_surface.xml new file mode 100644 index 0000000000..2fe558498e --- /dev/null +++ b/app/src/main/res/drawable/ic_quest_sidewalk_surface.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 182eeadca8..cb7a01b68c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -229,6 +229,7 @@ However, before uploading your changes, the app checks with a <a href=\"https Woodchips "What is the name of this place? (%s)" "Does this street have a sidewalk?" + "What is the surface of the sidewalk here?" "What is the name of this bus stop?" "What is the name of this streetcar stop?" "What is the reference number of this bus stop?" diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/osm/edits/update_tags/StringMapChangesBuilderTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/osm/edits/update_tags/StringMapChangesBuilderTest.kt index 1eafbcba94..257f52f5e3 100644 --- a/app/src/test/java/de/westnordost/streetcomplete/data/osm/edits/update_tags/StringMapChangesBuilderTest.kt +++ b/app/src/test/java/de/westnordost/streetcomplete/data/osm/edits/update_tags/StringMapChangesBuilderTest.kt @@ -3,6 +3,7 @@ package de.westnordost.streetcomplete.data.osm.edits.update_tags import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Assert.assertFalse import org.junit.Test class StringMapChangesBuilderTest { diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/AddSidewalkTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/AddSidewalkTest.kt index 892fac8b1f..a2707070cb 100644 --- a/app/src/test/java/de/westnordost/streetcomplete/quests/AddSidewalkTest.kt +++ b/app/src/test/java/de/westnordost/streetcomplete/quests/AddSidewalkTest.kt @@ -3,10 +3,10 @@ package de.westnordost.streetcomplete.quests import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry import de.westnordost.streetcomplete.quests.sidewalk.AddSidewalk -import de.westnordost.streetcomplete.quests.sidewalk.Sidewalk.NO -import de.westnordost.streetcomplete.quests.sidewalk.Sidewalk.SEPARATE -import de.westnordost.streetcomplete.quests.sidewalk.Sidewalk.YES -import de.westnordost.streetcomplete.quests.sidewalk.SidewalkSides +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.NO +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.SEPARATE +import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk.YES +import de.westnordost.streetcomplete.osm.sidewalk.SidewalkSides import de.westnordost.streetcomplete.testutils.p import de.westnordost.streetcomplete.testutils.way import de.westnordost.streetcomplete.util.translate diff --git a/app/src/test/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurfaceTest.kt b/app/src/test/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurfaceTest.kt new file mode 100644 index 0000000000..f02cddabfd --- /dev/null +++ b/app/src/test/java/de/westnordost/streetcomplete/quests/surface/AddSidewalkSurfaceTest.kt @@ -0,0 +1,133 @@ +package de.westnordost.streetcomplete.quests.surface + +import de.westnordost.streetcomplete.data.meta.toCheckDateString +import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryAdd +import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryDelete +import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapEntryModify +import de.westnordost.streetcomplete.quests.verifyAnswer +import de.westnordost.streetcomplete.testutils.way +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue + +import org.junit.Test +import java.time.LocalDate + +class AddSidewalkSurfaceTest { + private val questType = AddSidewalkSurface() + + @Test fun `not applicable to road with separate sidewalks`() { + assertIsNotApplicable("sidewalk" to "separate") + } + + @Test fun `not applicable to road with no sidewalks`() { + assertIsNotApplicable("sidewalk" to "no") + } + + @Test fun `applicable to road with sidewalk on both sides`() { + assertIsApplicable("highway" to "residential", "sidewalk" to "both") + } + + @Test fun `applicable to road with sidewalk on only one side`() { + assertIsApplicable("highway" to "residential", "sidewalk" to "left") + + assertIsApplicable("highway" to "residential", "sidewalk" to "right") + } + + @Test fun `applicable to road with sidewalk on one side and separate sidewalk on the other`() { + assertIsApplicable("highway" to "residential", "sidewalk:left" to "yes", "sidewalk:right" to "separate") + + assertIsApplicable("highway" to "residential", "sidewalk:left" to "separate", "sidewalk:right" to "yes") + } + + @Test fun `applicable to road with sidewalk on one side and no sidewalk on the other`() { + assertIsApplicable("highway" to "residential", "sidewalk:left" to "yes", "sidewalk:right" to "no") + + assertIsApplicable("highway" to "residential", "sidewalk:left" to "no", "sidewalk:right" to "yes") + } + + @Test fun `apply asphalt surface on both sides`() { + questType.verifyAnswer( + SidewalkSurfaceAnswer(SurfaceAnswer(Surface.ASPHALT), SurfaceAnswer(Surface.ASPHALT)), + StringMapEntryAdd("sidewalk:both:surface", "asphalt") + ) + } + + @Test fun `apply different surface on each side`() { + questType.verifyAnswer( + SidewalkSurfaceAnswer(SurfaceAnswer(Surface.ASPHALT), SurfaceAnswer(Surface.PAVING_STONES)), + StringMapEntryAdd("sidewalk:left:surface", "asphalt"), + StringMapEntryAdd("sidewalk:right:surface", "paving_stones") + ) + } + + @Test fun `apply generic surface on both sides`() { + questType.verifyAnswer( + SidewalkSurfaceAnswer( + SurfaceAnswer(Surface.PAVED_ROAD, "note"), + SurfaceAnswer(Surface.PAVED_ROAD, "note")), + StringMapEntryAdd("sidewalk:both:surface", "paved"), + StringMapEntryAdd("sidewalk:both:surface:note", "note") + ) + } + + @Test fun `updates check_date`() { + questType.verifyAnswer( + mapOf("sidewalk:both:surface" to "asphalt", "check_date:sidewalk:surface" to "2000-10-10"), + SidewalkSurfaceAnswer(SurfaceAnswer(Surface.ASPHALT), SurfaceAnswer(Surface.ASPHALT)), + StringMapEntryModify("sidewalk:both:surface", "asphalt", "asphalt"), + StringMapEntryModify("check_date:sidewalk:surface", "2000-10-10", LocalDate.now().toCheckDateString()), + ) + } + + @Test fun `sidewalk surface changes to be the same on both sides`() { + questType.verifyAnswer( + mapOf("sidewalk:left:surface" to "asphalt", "sidewalk:right:surface" to "paving_stones"), + SidewalkSurfaceAnswer(SurfaceAnswer(Surface.CONCRETE), SurfaceAnswer(Surface.CONCRETE)), + StringMapEntryDelete("sidewalk:left:surface", "asphalt"), + StringMapEntryDelete("sidewalk:right:surface", "paving_stones"), + StringMapEntryAdd("sidewalk:both:surface", "concrete") + ) + } + + @Test fun `sidewalk surface changes on each side`() { + questType.verifyAnswer( + mapOf("sidewalk:left:surface" to "asphalt", "sidewalk:right:surface" to "paving_stones"), + SidewalkSurfaceAnswer(SurfaceAnswer(Surface.CONCRETE), SurfaceAnswer(Surface.GRAVEL)), + StringMapEntryModify("sidewalk:left:surface", "asphalt", "concrete"), + StringMapEntryModify("sidewalk:right:surface", "paving_stones", "gravel"), + ) + } + + @Test fun `smoothness tag removed when surface changes, same on both sides`() { + questType.verifyAnswer( + mapOf("sidewalk:both:surface" to "asphalt", "sidewalk:both:smoothness" to "excellent"), + SidewalkSurfaceAnswer(SurfaceAnswer(Surface.PAVING_STONES), SurfaceAnswer(Surface.PAVING_STONES)), + StringMapEntryDelete("sidewalk:both:smoothness", "excellent"), + StringMapEntryModify("sidewalk:both:surface", "asphalt","paving_stones") + ) + } + + @Test fun `remove smoothness when surface changes, different on each side`() { + questType.verifyAnswer( + mapOf("sidewalk:left:surface" to "asphalt", + "sidewalk:right:surface" to "concrete", + "sidewalk:left:smoothness" to "excellent", + "sidewalk:right:smoothness" to "good" + ), + SidewalkSurfaceAnswer(SurfaceAnswer(Surface.PAVING_STONES), SurfaceAnswer(Surface.PAVING_STONES)), + StringMapEntryDelete("sidewalk:left:surface", "asphalt"), + StringMapEntryDelete("sidewalk:right:surface", "concrete"), + StringMapEntryDelete("sidewalk:left:smoothness", "excellent"), + StringMapEntryDelete("sidewalk:right:smoothness", "good"), + StringMapEntryAdd("sidewalk:both:surface", "paving_stones") + ) + } + + private fun assertIsApplicable(vararg pairs: Pair) { + assertTrue(questType.isApplicableTo(way(nodes = listOf(1,2,3), tags = mapOf(*pairs)))) + } + + private fun assertIsNotApplicable(vararg pairs: Pair) { + assertFalse(questType.isApplicableTo(way(nodes = listOf(1,2,3), tags = mapOf(*pairs)))) + } +} diff --git a/res/graphics/quest/sidewalk_surface.svg b/res/graphics/quest/sidewalk_surface.svg new file mode 100644 index 0000000000..2a26de7eb6 --- /dev/null +++ b/res/graphics/quest/sidewalk_surface.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +