Repository: streetcomplete/StreetComplete (language: kotlin)

Issue:
Cycleway quest applies wrong tags
Likely this is related to #4774, but this happens in in 51.0 beta1 (from GitHub)

* Open the cycleway quest for https://www.openstreetmap.org/way/43943837 (`cycleway=lane`)
* see current situation tagged as "???" (which is expected)
* either: set the available side to "none"
  * see added tags `oneway:bicycle=no`, `cycleway:left=lane`, `cycleway:right=no` (the first two are incorrect)
* or: set the available side to "advisory"
  * see added tags `oneway:bicycle=no`, `cycleway:both=lane`, `cycleway:right:lane=advisory` (the first two are incorrect)
* get the quest again, this time showing both sides
* set new side to "none"
* see tags are now correct


Discussion hints:
Uh, this one is a bugger.

`cycleway=lane` on a `oneway=yes` is interpreted to mean

`cycleway:right=lane`
`cycleway:left` not set (`null`)

So, as the left side is `null`, the left side is also not shown in the UI.

However, as only the right side is **changed** by the user, only `cycleway:right=lane` + `cycleway:right:lane=advisory` is set and whatever the value that was in `cycleway` tag before is now updated to only apply for the left side. This is why we end up with `cycleway:left=lane`.

Probably will need to change the logic that expands `cycleway` into `cycleway:left` and `cycleway:right` in `CyclewayCreator.kt`. Not sure yet what's the cleanest way to do this.

Pull request: Fix #4790
@Helium314 are you interested in reviewing this?

Test command: ./gradlew :app:testDebugUnitTest --tests "de.westnordost.streetcomplete.osm.cycleway.CyclewayCreatorKtTest"

Fix patch:
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreator.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreator.kt
index dbee23c4259..de54fe90137 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreator.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreator.kt
@@ -7,6 +7,7 @@ import de.westnordost.streetcomplete.osm.expandSides
 import de.westnordost.streetcomplete.osm.hasCheckDateForKey
 import de.westnordost.streetcomplete.osm.isInContraflowOfOneway
 import de.westnordost.streetcomplete.osm.isOneway
+import de.westnordost.streetcomplete.osm.isReversedOneway
 import de.westnordost.streetcomplete.osm.mergeSides
 import de.westnordost.streetcomplete.osm.sidewalk.LeftAndRightSidewalk
 import de.westnordost.streetcomplete.osm.sidewalk.Sidewalk
@@ -22,10 +23,12 @@ fun LeftAndRightCycleway.applyTo(tags: Tags, isLeftHandTraffic: Boolean) {
        - cycleway:left=separate
        First separating the values and then later conflating them again, if possible, solves this.
      */
-    tags.expandSides("cycleway")
-    tags.expandSides("cycleway", "lane")
-    tags.expandSides("cycleway", "oneway")
-    tags.expandSides("cycleway", "segregated")
+
+    expandBareTags(tags, isLeftHandTraffic)
+    tags.expandSides("cycleway", null, false)
+    tags.expandSides("cycleway", "lane", false)
+    tags.expandSides("cycleway", "oneway", false)
+    tags.expandSides("cycleway", "segregated", false)
 
     applyOnewayNotForCyclists(tags, isLeftHandTraffic)
     left?.applyTo(tags, false, isLeftHandTraffic)
@@ -49,6 +52,36 @@ fun LeftAndRightCycleway.applyTo(tags: Tags, isLeftHandTraffic: Boolean) {
     ).applyTo(tags)
 }
 
+/* bare cycleway tags are interpreted differently for oneways */
+private fun expandBareTags(tags: Tags, isLeftHandTraffic: Boolean) {
+    val cycleway = tags["cycleway"] ?: return
+    // i.e. they are only expanded into one side. Which side depends on country, direction of oneway
+    // and whether it is an "opposite" tag value
+    val side = if (isOneway(tags)) {
+        val isReverseSideRight = isReversedOneway(tags) xor isLeftHandTraffic
+        val isOpposite = cycleway.startsWith("opposite")
+        if (isOpposite == isReverseSideRight) "right" else "left"
+    } else {
+        "both"
+    }
+    if (!tags.containsKey("cycleway:$side")) {
+        tags["cycleway:$side"] = cycleway
+            .removePrefix("opposite_") // opposite_track -> track etc.
+            .replaceFirst("opposite", "no") // opposite -> no
+    }
+    tags.remove("cycleway")
+    tags.expandBareTagIntoSide("cycleway", "lane", side)
+    tags.expandBareTagIntoSide("cycleway", "oneway", side)
+    tags.expandBareTagIntoSide("cycleway", "segregated", side)
+}
+
+private fun Tags.expandBareTagIntoSide(key: String, suffix: String = "", side: String) {
+    val post = if (suffix.isEmpty()) "" else ":$suffix"
+    val value = get("$key$post") ?: return
+    if (!containsKey("$key:$side$post")) set("$key:$side$post", value)
+    remove("$key$post")
+}
+
 private fun LeftAndRightCycleway.applyOnewayNotForCyclists(tags: Tags, isLeftHandTraffic: Boolean) {
     if (isOneway(tags) && isNotOnewayForCyclistsNow(tags, isLeftHandTraffic)) {
         tags["oneway:bicycle"] = "no"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParser.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParser.kt
index 06770f67c6b..d71f4ed6220 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParser.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayParser.kt
@@ -38,8 +38,11 @@ fun createCyclewaySides(tags: Map<String, String>, isLeftHandTraffic: Boolean):
     *  it that way. */
     val nakedCycleway = createCyclewayForSide(expandedTags, null)
     if (isOneway && nakedCycleway != null && nakedCycleway != NONE) {
-        val isRight = if (isOpposite) isReverseSideRight else !isReverseSideRight
-        if (isRight) right = nakedCycleway else left = nakedCycleway
+        if (isOpposite == isReverseSideRight) {
+            right = nakedCycleway
+        } else {
+            left = nakedCycleway
+        }
     } else {
         left = createCyclewayForSide(expandedTags, false)
         right = createCyclewayForSide(expandedTags, true)


Test patch:
diff --git a/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreatorKtTest.kt b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreatorKtTest.kt
index bf195940931..568eecd1d27 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreatorKtTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/osm/cycleway/CyclewayCreatorKtTest.kt
@@ -697,6 +697,75 @@ class CyclewayCreatorKtTest {
         )
     }
 
+    @Test fun `apply answer for one side in oneway when bare tag was set before`() {
+        verifyAnswer(
+            mapOf("oneway" to "yes", "cycleway" to "track"),
+            cycleway(null, NONE),
+            arrayOf(
+                StringMapEntryDelete("cycleway", "track"),
+                StringMapEntryAdd("cycleway:right", "no")
+            )
+        )
+        verifyAnswer(
+            mapOf("oneway" to "-1", "cycleway" to "opposite"),
+            cycleway(TRACK, null),
+            arrayOf(
+                StringMapEntryDelete("cycleway", "opposite"),
+                StringMapEntryAdd("cycleway:right", "no"),
+                StringMapEntryAdd("cycleway:left", "track")
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "oneway" to "yes",
+                "cycleway" to "opposite_track",
+                "cycleway:segregated" to "yes"
+            ),
+            cycleway(null, NONE),
+            arrayOf(
+                StringMapEntryDelete("cycleway", "opposite_track"),
+                StringMapEntryDelete("cycleway:segregated", "yes"),
+                StringMapEntryAdd("cycleway:left", "track"),
+                StringMapEntryAdd("cycleway:left:segregated", "yes"),
+                StringMapEntryAdd("cycleway:right", "no"),
+                StringMapEntryAdd("oneway:bicycle", "no")
+            )
+        )
+        verifyAnswer(
+            mapOf(
+                "oneway" to "yes",
+                "cycleway" to "opposite_lane",
+                "cycleway:lane" to "advisory",
+                "cycleway:oneway" to "yes"
+            ),
+            cycleway(null, NONE),
+            arrayOf(
+                StringMapEntryDelete("cycleway", "opposite_lane"),
+                StringMapEntryDelete("cycleway:lane", "advisory"),
+                StringMapEntryDelete("cycleway:oneway", "yes"),
+                StringMapEntryAdd("cycleway:left", "lane"),
+                StringMapEntryAdd("cycleway:left:lane", "advisory"),
+                StringMapEntryAdd("cycleway:left:oneway", "yes"),
+                StringMapEntryAdd("cycleway:right", "no")
+            )
+        )
+    }
+
+    @Test fun `expanding bare tags does not overwrite non-bare tags`() {
+        verifyAnswer(
+            mapOf(
+                "oneway" to "yes",
+                "cycleway" to "track",
+                "cycleway:right" to "blubber",
+            ),
+            cycleway(NONE, null),
+            arrayOf(
+                StringMapEntryDelete("cycleway", "track"),
+                StringMapEntryAdd("cycleway:left", "no")
+            )
+        )
+    }
+
     @Test(expected = IllegalArgumentException::class)
     fun `applying invalid left throws exception`() {
         cycleway(INVALID, null).applyTo(StringMapChangesBuilder(mapOf()), false)