Skip to content

Commit

Permalink
Cycleway overlay: Differentiate bicycle access on pedestrian roads (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
westnordost authored Nov 25, 2024
1 parent bc70db8 commit 7e2fd27
Show file tree
Hide file tree
Showing 13 changed files with 443 additions and 110 deletions.
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) }
}

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
}

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
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
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

0 comments on commit 7e2fd27

Please sign in to comment.