Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cycleway overlay: Differentiate bicycle access on pedestrian roads #6020

Merged
merged 8 commits into from
Nov 25, 2024
14 changes: 7 additions & 7 deletions app/src/main/assets/map_theme/streetcomplete-night.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions app/src/main/assets/map_theme/streetcomplete.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package de.westnordost.streetcomplete.osm.bicycle_in_pedestrian_street

import de.westnordost.streetcomplete.osm.Tags
import de.westnordost.streetcomplete.osm.bicycle_in_pedestrian_street.BicycleInPedestrianStreet.*

enum class BicycleInPedestrianStreet {
/** Pedestrian area also designated for pedestrians (like shared-use path) */
DESIGNATED,
/** Bicycles explicitly allowed in pedestrian area */
ALLOWED,
/** Bicycles explicitly not allowed in pedestrian area */
NOT_ALLOWED,
/** Nothing is signed about bicycles in pedestrian area (probably disallowed, but depends on
* legislation */
NOT_SIGNED
}

fun parseBicycleInPedestrianStreet(tags: Map<String, String>): BicycleInPedestrianStreet? {
val bicycleSigned = tags["bicycle:signed"] == "yes"
return when {
tags["highway"] != "pedestrian" -> null
tags["bicycle"] == "designated" -> DESIGNATED
tags["bicycle"] in yesButNotDesignated && bicycleSigned -> ALLOWED
tags["bicycle"] in noCycling && bicycleSigned -> NOT_ALLOWED
else -> NOT_SIGNED
}
}

private val yesButNotDesignated = setOf(
"yes", "permissive", "private", "destination", "customers", "permit"
)

private val noCycling = setOf(
"no", "dismount"
)

fun BicycleInPedestrianStreet.applyTo(tags: Tags) {
// note the implementation is quite similar to that in SeparateCyclewayCreator
when (this) {
DESIGNATED -> {
tags["bicycle"] = "designated"
// if bicycle:signed is explicitly no, set it to yes
if (tags["bicycle:signed"] == "no") tags["bicycle:signed"] = "yes"
}
ALLOWED -> {
tags["bicycle"] = "yes"
tags["bicycle:signed"] = "yes"
}
NOT_ALLOWED -> {
if (tags["bicycle"] !in noCycling) tags["bicycle"] = "no"
tags["bicycle:signed"] = "yes"
}
NOT_SIGNED -> {
// only remove if designated before, it might still be allowed by legislation!
if (tags["bicycle"] == "designated") tags.remove("bicycle")
tags.remove("bicycle:signed")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,36 @@ private fun SeparateCycleway?.getColor() = when (this) {
private fun getStreetCyclewayStyle(element: Element, countryInfo: CountryInfo): PolylineStyle {
val isLeftHandTraffic = countryInfo.isLeftHandTraffic
val cycleways = parseCyclewaySides(element.tags, isLeftHandTraffic)
val isBicycleBoulevard = parseBicycleBoulevard(element.tags) == BicycleBoulevard.YES
val isNoCyclewayExpectedLeft = { cyclewayTaggingNotExpected(element, false, isLeftHandTraffic) }
val isNoCyclewayExpectedRight = { cyclewayTaggingNotExpected(element, true, isLeftHandTraffic) }

return PolylineStyle(
stroke = if (isBicycleBoulevard) StrokeStyle(Color.GOLD, dashed = true) else null,
stroke = getStreetStrokeStyle(element.tags),
strokeLeft = cycleways?.left?.cycleway.getStyle(countryInfo, isNoCyclewayExpectedLeft),
strokeRight = cycleways?.right?.cycleway.getStyle(countryInfo, isNoCyclewayExpectedRight)
)
}

private fun getStreetStrokeStyle(tags: Map<String, String>): StrokeStyle? {
val isBicycleBoulevard = parseBicycleBoulevard(tags) == BicycleBoulevard.YES
val isPedestrian = tags["highway"] == "pedestrian"
val isBicycleDesignated = tags["bicycle"] == "designated"
val isBicycleOk = tags["bicycle"] == "yes" && tags["bicycle:signed"] == "yes"

return when {
isBicycleBoulevard ->
StrokeStyle(Color.GOLD, dashed = true)
isPedestrian && isBicycleDesignated ->
StrokeStyle(Color.CYAN)
isPedestrian && isBicycleOk ->
StrokeStyle(Color.AQUAMARINE)
isPedestrian ->
StrokeStyle(Color.BLACK)
else ->
null
}
}

private val cyclewayTaggingNotExpectedFilter by lazy { """
ways with
highway ~ track|living_street|pedestrian|service|motorway_link|motorway|busway
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import de.westnordost.streetcomplete.osm.Direction
import de.westnordost.streetcomplete.osm.bicycle_boulevard.BicycleBoulevard
import de.westnordost.streetcomplete.osm.bicycle_boulevard.applyTo
import de.westnordost.streetcomplete.osm.bicycle_boulevard.parseBicycleBoulevard
import de.westnordost.streetcomplete.osm.bicycle_in_pedestrian_street.BicycleInPedestrianStreet
import de.westnordost.streetcomplete.osm.bicycle_in_pedestrian_street.applyTo
import de.westnordost.streetcomplete.osm.bicycle_in_pedestrian_street.parseBicycleInPedestrianStreet
import de.westnordost.streetcomplete.osm.cycleway.Cycleway
import de.westnordost.streetcomplete.osm.cycleway.CyclewayAndDirection
import de.westnordost.streetcomplete.osm.cycleway.LeftAndRightCycleway
Expand All @@ -34,15 +37,20 @@ import kotlinx.serialization.json.Json

class StreetCyclewayOverlayForm : AStreetSideSelectOverlayForm<CyclewayAndDirection>() {

override val contentLayoutResId = R.layout.fragment_overlay_cycleway

override val otherAnswers: List<IAnswerItem> get() =
createSwitchBicycleInPedestrianZoneAnswers() +
listOfNotNull(
createSwitchBicycleBoulevardAnswer(),
createReverseCyclewayDirectionAnswer()
)

private var originalCycleway: LeftAndRightCycleway? = null
private var originalBicycleBoulevard: BicycleBoulevard = BicycleBoulevard.NO
private var originalBicycleInPedestrianStreet: BicycleInPedestrianStreet? = null
private var bicycleBoulevard: BicycleBoulevard = BicycleBoulevard.NO
private var bicycleInPedestrianStreet: BicycleInPedestrianStreet? = null
private var reverseDirection: Boolean = false

// just a shortcut
Expand All @@ -59,17 +67,17 @@ class StreetCyclewayOverlayForm : AStreetSideSelectOverlayForm<CyclewayAndDirect
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

originalCycleway = parseCyclewaySides(element!!.tags, isLeftHandTraffic)?.selectableOrNullValues(countryInfo)
originalBicycleBoulevard = parseBicycleBoulevard(element!!.tags)
val tags = element!!.tags
originalCycleway = parseCyclewaySides(tags, isLeftHandTraffic)?.selectableOrNullValues(countryInfo)
originalBicycleBoulevard = parseBicycleBoulevard(tags)
originalBicycleInPedestrianStreet = parseBicycleInPedestrianStreet(tags)

if (savedInstanceState == null) {
initStateFromTags()
} else {
savedInstanceState.getString(BICYCLE_BOULEVARD)?.let {
bicycleBoulevard = BicycleBoulevard.valueOf(it)
}
onLoadInstanceState(savedInstanceState)
}
updateBicycleBoulevard()
updateStreetSign()

streetSideSelect.transformLastSelection = { item: CyclewayAndDirection, isRight: Boolean ->
if (item.direction == Direction.BOTH) {
Expand All @@ -80,13 +88,23 @@ class StreetCyclewayOverlayForm : AStreetSideSelectOverlayForm<CyclewayAndDirect
}
}

private fun onLoadInstanceState(state: Bundle) {
bicycleBoulevard = state.getString(BICYCLE_BOULEVARD)
?.let { BicycleBoulevard.valueOf(it) }
?: BicycleBoulevard.NO
bicycleInPedestrianStreet = state.getString(BICYCLE_IN_PEDESTRIAN_STREET)
?.let { BicycleInPedestrianStreet.valueOf(it) }
westnordost marked this conversation as resolved.
Show resolved Hide resolved
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(BICYCLE_BOULEVARD, bicycleBoulevard.name)
outState.putString(BICYCLE_IN_PEDESTRIAN_STREET, bicycleInPedestrianStreet?.name)
}

private fun initStateFromTags() {
bicycleBoulevard = originalBicycleBoulevard
bicycleInPedestrianStreet = originalBicycleInPedestrianStreet

val leftItem = originalCycleway?.left?.asStreetSideItem(false, isContraflowInOneway(false), countryInfo)
streetSideSelect.setPuzzleSide(leftItem, false)
Expand All @@ -95,56 +113,71 @@ class StreetCyclewayOverlayForm : AStreetSideSelectOverlayForm<CyclewayAndDirect
streetSideSelect.setPuzzleSide(rightItem, true)
}

/* ----------------------------------- bicycle boulevards ----------------------------------- */
/* ------------------------- pedestrian zone and bicycle boulevards ------------------------- */

private fun createSwitchBicycleBoulevardAnswer(): IAnswerItem? =
if (bicycleBoulevard == BicycleBoulevard.YES) {
AnswerItem2(
getString(R.string.bicycle_boulevard_is_not_a, getString(R.string.bicycle_boulevard)),
::removeBicycleBoulevard
)
} else if (countryInfo.hasBicycleBoulevard) {
AnswerItem2(
getString(R.string.bicycle_boulevard_is_a, getString(R.string.bicycle_boulevard)),
::addBicycleBoulevard
)
} else {
null
}
private fun createSwitchBicycleInPedestrianZoneAnswers(): List<IAnswerItem> {
if (bicycleInPedestrianStreet == null) return listOf()

private fun removeBicycleBoulevard() {
bicycleBoulevard = BicycleBoulevard.NO
updateBicycleBoulevard()
val result = mutableListOf<IAnswerItem>()
if (bicycleInPedestrianStreet != BicycleInPedestrianStreet.DESIGNATED) {
result.add(AnswerItem(R.string.pedestrian_zone_designated) {
bicycleInPedestrianStreet = BicycleInPedestrianStreet.DESIGNATED
updateStreetSign()
})
}
if (bicycleInPedestrianStreet != BicycleInPedestrianStreet.ALLOWED) {
result.add(AnswerItem(R.string.pedestrian_zone_allowed_sign) {
bicycleInPedestrianStreet = BicycleInPedestrianStreet.ALLOWED
updateStreetSign()
})
}
if (bicycleInPedestrianStreet != BicycleInPedestrianStreet.NOT_SIGNED) {
result.add(AnswerItem(R.string.pedestrian_zone_no_sign) {
bicycleInPedestrianStreet = BicycleInPedestrianStreet.NOT_SIGNED
updateStreetSign()
})
}
return result
westnordost marked this conversation as resolved.
Show resolved Hide resolved
}

private fun addBicycleBoulevard() {
bicycleBoulevard = BicycleBoulevard.YES
updateBicycleBoulevard()
}
private fun createSwitchBicycleBoulevardAnswer(): IAnswerItem? =
when (bicycleBoulevard) {
BicycleBoulevard.YES ->
AnswerItem2(getString(R.string.bicycle_boulevard_is_not_a, getString(R.string.bicycle_boulevard))) {
bicycleBoulevard = BicycleBoulevard.NO
updateStreetSign()
}
BicycleBoulevard.NO ->
// don't allow pedestrian roads to be tagged as bicycle roads
// (should rather be R.string.pedestrian_zone_designated
westnordost marked this conversation as resolved.
Show resolved Hide resolved
if (element!!.tags["highway"] != "pedestrian") {
AnswerItem2(getString(R.string.bicycle_boulevard_is_a, getString(R.string.bicycle_boulevard))) {
bicycleBoulevard = BicycleBoulevard.YES
updateStreetSign()
}
} else {
null
}
}

private fun updateBicycleBoulevard() {
val bicycleBoulevardSignView = requireView().findViewById<View>(R.id.signBicycleBoulevard)
if (bicycleBoulevard == BicycleBoulevard.YES) {
if (bicycleBoulevardSignView == null) {
layoutInflater.inflate(
R.layout.sign_bicycle_boulevard,
requireView().findViewById(R.id.content), true
)
}
} else {
(bicycleBoulevardSignView?.parent as? ViewGroup)?.removeView(bicycleBoulevardSignView)
private fun updateStreetSign() {
val signContainer = requireView().findViewById<ViewGroup>(R.id.signContainer)
signContainer.removeAllViews()

if (bicycleInPedestrianStreet == BicycleInPedestrianStreet.ALLOWED) {
layoutInflater.inflate(R.layout.sign_bicycles_ok, signContainer, true)
} else if (bicycleInPedestrianStreet == BicycleInPedestrianStreet.DESIGNATED) {
layoutInflater.inflate(R.layout.sign_bicycle_and_pedestrians, signContainer, true)
} else if (bicycleBoulevard == BicycleBoulevard.YES) {
layoutInflater.inflate(R.layout.sign_bicycle_boulevard, signContainer, true)
}
checkIsFormComplete()
}

/* ------------------------------ reverse cycleway direction -------------------------------- */

private fun createReverseCyclewayDirectionAnswer(): IAnswerItem? =
if (bicycleBoulevard == BicycleBoulevard.YES) {
null
} else {
AnswerItem(R.string.cycleway_reverse_direction, ::selectReverseCyclewayDirection)
}
private fun createReverseCyclewayDirectionAnswer(): IAnswerItem =
AnswerItem(R.string.cycleway_reverse_direction, ::selectReverseCyclewayDirection)

private fun selectReverseCyclewayDirection() {
confirmSelectReverseCyclewayDirection {
Expand Down Expand Up @@ -193,18 +226,12 @@ class StreetCyclewayOverlayForm : AStreetSideSelectOverlayForm<CyclewayAndDirect
}

override fun onClickOk() {
if (bicycleBoulevard == BicycleBoulevard.YES) {
val tags = StringMapChangesBuilder(element!!.tags)
bicycleBoulevard.applyTo(tags, countryInfo.countryCode)
applyEdit(UpdateElementTagsAction(element!!, tags.create()))
// only tag the cycleway if that is what is currently displayed
westnordost marked this conversation as resolved.
Show resolved Hide resolved
val cycleways = LeftAndRightCycleway(streetSideSelect.left?.value, streetSideSelect.right?.value)
if (cycleways.wasNoOnewayForCyclistsButNowItIs(element!!.tags, isLeftHandTraffic)) {
confirmNotOnewayForCyclists { saveAndApplyCycleway(cycleways) }
} else {
// only tag the cycleway if that is what is currently displayed
val cycleways = LeftAndRightCycleway(streetSideSelect.left?.value, streetSideSelect.right?.value)
if (cycleways.wasNoOnewayForCyclistsButNowItIs(element!!.tags, isLeftHandTraffic)) {
confirmNotOnewayForCyclists { saveAndApplyCycleway(cycleways) }
} else {
saveAndApplyCycleway(cycleways)
}
saveAndApplyCycleway(cycleways)
}
}

Expand All @@ -221,6 +248,7 @@ class StreetCyclewayOverlayForm : AStreetSideSelectOverlayForm<CyclewayAndDirect
val tags = StringMapChangesBuilder(element!!.tags)
cycleways.applyTo(tags, countryInfo.isLeftHandTraffic)
bicycleBoulevard.applyTo(tags, countryInfo.countryCode)
bicycleInPedestrianStreet?.applyTo(tags)
applyEdit(UpdateElementTagsAction(element!!, tags.create()))
}

Expand All @@ -229,12 +257,14 @@ class StreetCyclewayOverlayForm : AStreetSideSelectOverlayForm<CyclewayAndDirect
override fun isFormComplete() =
streetSideSelect.left != null ||
streetSideSelect.right != null ||
bicycleBoulevard == BicycleBoulevard.YES
originalBicycleBoulevard != bicycleBoulevard ||
originalBicycleInPedestrianStreet != bicycleInPedestrianStreet

override fun hasChanges(): Boolean =
streetSideSelect.left?.value != originalCycleway?.left ||
streetSideSelect.right?.value != originalCycleway?.right ||
originalBicycleBoulevard != bicycleBoulevard
originalBicycleBoulevard != bicycleBoulevard ||
originalBicycleInPedestrianStreet != bicycleInPedestrianStreet

override fun serialize(item: CyclewayAndDirection) = Json.encodeToString(item)
override fun deserialize(str: String) = Json.decodeFromString<CyclewayAndDirection>(str)
Expand All @@ -249,5 +279,6 @@ class StreetCyclewayOverlayForm : AStreetSideSelectOverlayForm<CyclewayAndDirect

companion object {
private const val BICYCLE_BOULEVARD = "bicycle_boulevard"
private const val BICYCLE_IN_PEDESTRIAN_STREET = "bicycle_in_pedestrian_street"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
<path
android:pathData="m50.35,55.69a21.68,21.68 0,0 1,-21.68 21.68,21.68 21.68,0 0,1 -21.68,-21.68 21.68,21.68 0,0 1,21.68 -21.68,21.68 21.68,0 0,1 21.68,21.68m70.64,-0a21.68,21.68 0,0 1,-21.68 21.68,21.68 21.68,0 0,1 -21.68,-21.68 21.68,21.68 0,0 1,21.68 -21.68,21.68 21.68,0 0,1 21.68,21.68m-80.27,-33.39 l27.4,33.27h31.31l-21.53,-33.27h-37.18m37.18,-7.83 l9.54,-0.02m-19.32,41.12 l9.78,-41.1m-48.93,41.1 l16.31,-48.56h15.66"
android:strokeLineJoin="round"
android:strokeWidth="6.0002"
android:fillColor="#00000000"
android:strokeWidth="6"
android:strokeColor="#fff"
android:strokeLineCap="round"/>
</vector>
15 changes: 15 additions & 0 deletions app/src/main/res/drawable/pedestrian_and_bicycle_white.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="m41.731,54.845c-4.519,0 -8.185,3.665 -8.185,8.185 0,4.52 3.665,8.183 8.185,8.183 4.52,0 8.185,-3.663 8.185,-8.183 0,-4.52 -3.665,-8.185 -8.185,-8.185zM33.169,74.19c-8.281,3.097 -14.667,8.36 -14.738,17.114 0,2.241 1.816,4.056 4.058,4.056 2.242,0 4.059,-1.815 4.059,-4.056 0.067,-2.237 1.016,-4.137 2.402,-5.655 0.044,1.993 0.362,4.231 1.047,6.714 -0.422,0.703 -0.599,1.761 -0.432,3.309 -1.432,8.24 -10.516,13.601 -16.373,16.261 -2.048,0.91 -2.972,3.309 -2.062,5.358 0.911,2.048 3.309,2.972 5.358,2.062 7.772,-4.047 17.656,-10.576 20.502,-19.148 6.456,3.01 12.497,9.771 13.586,16.012 0.316,2.219 2.373,3.759 4.592,3.442 2.219,-0.317 3.761,-2.371 3.444,-4.589 -1.683,-10.015 -8.599,-17.426 -17.224,-21.769 -2.498,-1.073 -2.696,-6.905 -2.555,-9.008 7.568,4.296 15.326,6.215 23.004,2.447 2.005,-1.002 2.816,-3.441 1.814,-5.445 -1.003,-2.004 -3.438,-2.817 -5.442,-1.814 -9.024,4.461 -14.645,-2.371 -19.393,-5.136 -1.699,-1.19 -3.956,-1.193 -5.644,-0.155z"
android:fillColor="#fff"/>
<path
android:pathData="M76.601,45.578A10.905,10.858 0,0 1,65.696 56.436,10.905 10.858,0 0,1 54.792,45.578 10.905,10.858 0,0 1,65.696 34.72,10.905 10.858,0 0,1 76.601,45.578ZM112.138,45.577A10.905,10.858 0,0 1,101.233 56.435,10.905 10.858,0 0,1 90.328,45.577 10.905,10.858 0,0 1,101.233 34.719,10.905 10.858,0 0,1 112.138,45.577ZM71.759,28.854 L85.541,45.518h15.751L90.464,28.854L71.759,28.854M85.541,45.518 L90.464,24.933m0,0 l4.798,-0.015M65.852,45.513 L74.057,21.188h7.876"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:strokeColor="#fff"
android:strokeLineCap="round"/>
</vector>
17 changes: 17 additions & 0 deletions app/src/main/res/layout/fragment_overlay_cycleway.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<include layout="@layout/fragment_overlay_street_side_puzzle_with_last_answer_button"/>

<FrameLayout
android:id="@+id/signContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:scaleX="0.5"
android:scaleY="0.5"
android:alpha="0.75"/>

</RelativeLayout>
Loading