Skip to content

Commit

Permalink
street furniture overlay
Browse files Browse the repository at this point in the history
  • Loading branch information
matkoniecz committed Nov 14, 2023
1 parent 611c56e commit 8854bc5
Show file tree
Hide file tree
Showing 13 changed files with 414 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package de.westnordost.streetcomplete.osm

import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression

fun isStreetFurnitureFragment(prefix: String? = null): String {
val amenities = listOf(
"bicycle_parking", "bicycle_rental", "bench", "longer", "bbq", "grit_bin", "toilets",
"public_bookcase", "give_box", "clock", "bicycle_repair_station", "charging_station",
"parcel_locker", "telephone", "drinking_water", "vending_machine",
"atm", "waste_basket", "trolley_bay",
// "post_box", "letter_box", - blocked by https://github.com/streetcomplete/StreetComplete/issues/4916
// waiting for response in https://github.com/ideditor/schema-builder/issues/94
// man_made = street_cabinet and street_cabinet = postal_service
// is also disabled to avoid bad data being added
)
val p = if (prefix != null) "$prefix:" else ""
return ("""(
${p}amenity ~ ${amenities.joinToString("|")}
or (${p}amenity = recycling and recycling_type = container)
or ${p}leisure ~ picnic_table|firepit
or ${p}man_made ~ water_tap|obelisk|cross|monitoring_station|flagpole|carpet_hanger|planter
or ${p}tourism ~ viewpoint|artwork
or (${p}tourism ~ information and information ~ guidepost|board|map|terminal)
or ${p}historic ~ memorial|monument|wayside_shrine|wayside_cross|boundary_stone
or ${p}highway ~ milestone|street_lamp|emergency_access_point
or ${p}emergency ~ fire_hydrant|life_ring|phone|defibrillator|siren|lifeguard|assembly_point|access_point
or ${p}advertising
or ${p}leisure = pitch and sport ~ table_tennis|sport=chess
or ${p}natural ~ tree
or ${p}man_made = street_cabinet and street_cabinet != postal_service
)""")
}
val IS_DISUSED_STREET_FURNITURE_EXPRESSION = """
nodes, ways, relations with
${isStreetFurnitureFragment("disused")}
""".toElementFilterExpression()

val IS_STREET_FURNITURE_INCLUDING_DISUSED_EXPRESSION = """
nodes, ways, relations with
${isStreetFurnitureFragment()}
or ${isStreetFurnitureFragment("disused")}
""".toElementFilterExpression()
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import de.westnordost.streetcomplete.overlays.address.AddressOverlay
import de.westnordost.streetcomplete.overlays.cycleway.CyclewayOverlay
import de.westnordost.streetcomplete.overlays.shops.ShopsOverlay
import de.westnordost.streetcomplete.overlays.sidewalk.SidewalkOverlay
import de.westnordost.streetcomplete.overlays.street_furniture.StreetFurnitureOverlay
import de.westnordost.streetcomplete.overlays.street_parking.StreetParkingOverlay
import de.westnordost.streetcomplete.overlays.surface.SurfaceOverlay
import de.westnordost.streetcomplete.overlays.way_lit.WayLitOverlay
Expand Down Expand Up @@ -56,4 +57,5 @@ fun overlaysRegistry(
2 to StreetParkingOverlay(),
3 to AddressOverlay(getCountryCodeByLocation),
4 to ShopsOverlay(getFeature),
7 to StreetFurnitureOverlay(getFeature),
))
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import de.westnordost.streetcomplete.osm.replaceShop
import de.westnordost.streetcomplete.overlays.AbstractOverlayForm
import de.westnordost.streetcomplete.overlays.AnswerItem
import de.westnordost.streetcomplete.quests.LocalizedNameAdapter
import de.westnordost.streetcomplete.quests.shop_type.topFeatureCodesOfPopularShoplikePOIs
import de.westnordost.streetcomplete.util.getLocalesForFeatureDictionary
import de.westnordost.streetcomplete.util.getLocationLabel
import de.westnordost.streetcomplete.util.ktx.geometryType
Expand Down Expand Up @@ -109,7 +110,8 @@ class ShopsOverlayForm : AbstractOverlayForm() {
countryOrSubdivisionCode,
featureCtrl.feature?.name,
::filterOnlyShops,
::onSelectedFeature
::onSelectedFeature,
topFeatureCodesOfPopularShoplikePOIs(),
).show()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package de.westnordost.streetcomplete.overlays.street_furniture

import de.westnordost.osmfeatures.Feature
import de.westnordost.osmfeatures.GeometryType
import java.util.Locale

data class DummyFeature(
private val id: String,
private val name: String,
private val icon: String,
private val addTags: Map<String, String>
) : Feature {
override fun getId() = id
override fun getTags() = addTags
override fun getGeometry() = listOf(GeometryType.POINT, GeometryType.AREA)
override fun getName() = name
override fun getIcon() = icon
override fun getImageURL() = null
override fun getNames() = listOf(name)
override fun getTerms() = emptyList<String>()
override fun getIncludeCountryCodes() = emptyList<String>()
override fun getExcludeCountryCodes() = emptyList<String>()
override fun isSearchable() = false
override fun getMatchScore() = 1.0
override fun getAddTags() = addTags
override fun getRemoveTags() = emptyMap<String, String>()
override fun getCanonicalNames() = emptyList<String>()
override fun getCanonicalTerms() = emptyList<String>()
override fun isSuggestion() = false
override fun getLocale(): Locale = Locale.getDefault()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package de.westnordost.streetcomplete.overlays.street_furniture

import androidx.core.content.ContentProviderCompat.requireContext
import de.westnordost.osmfeatures.Feature
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.data.osm.mapdata.Element
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
import de.westnordost.streetcomplete.data.osm.mapdata.Node
import de.westnordost.streetcomplete.data.osm.mapdata.filter
import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement
import de.westnordost.streetcomplete.osm.IS_DISUSED_STREET_FURNITURE_EXPRESSION
import de.westnordost.streetcomplete.osm.IS_STREET_FURNITURE_INCLUDING_DISUSED_EXPRESSION
import de.westnordost.streetcomplete.overlays.Color
import de.westnordost.streetcomplete.overlays.Overlay
import de.westnordost.streetcomplete.overlays.PointStyle
import de.westnordost.streetcomplete.overlays.PolygonStyle
import de.westnordost.streetcomplete.util.getNameLabel

class StreetFurnitureOverlay(private val getFeature: (tags: Map<String, String>) -> Feature?) : Overlay {

override val title = R.string.overlay_street_furniture
override val icon = R.drawable.ic_quest_apple // TODO
override val changesetComment = "Survey street furniture and similar objects"
override val wikiLink: String = "Street furniture"
override val achievements = listOf(EditTypeAchievement.CITIZEN)
override val isCreateNodeEnabled = true

override val sceneUpdates = listOf(
"layers.buildings.draw.buildings-style.extrude" to "false",
"layers.buildings.draw.buildings-outline-style.extrude" to "false"
)

override fun getStyledElements(mapData: MapDataWithGeometry) =
mapData
.filter(IS_STREET_FURNITURE_INCLUDING_DISUSED_EXPRESSION)
.mapNotNull { element ->
val feature = getFeature(element.tags)
val iconIdentifier = feature?.icon
if ( iconIdentifier == null) {
if (IS_DISUSED_STREET_FURNITURE_EXPRESSION.matches(element)) {
val icon = "ic_preset_maki_marker_stroked"
val label = null
val style = if (element is Node) {
PointStyle(icon, label)
} else {
PolygonStyle(Color.INVISIBLE, icon, label)
}
element to style
} else {
null
}
} else {
val icon = "ic_preset_" + iconIdentifier.replace('-', '_')
val label = getNameLabel(element.tags)

val style = if (element is Node) {
PointStyle(icon, label)
} else {
PolygonStyle(Color.INVISIBLE, icon, label)
}
element to style
}
}

override fun createForm(element: Element?) = StreetFurnitureOverlayForm()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package de.westnordost.streetcomplete.overlays.street_furniture

import android.os.Bundle
import android.view.View
import androidx.core.content.ContentProviderCompat
import de.westnordost.osmfeatures.Feature
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.data.osm.edits.ElementEditAction
import de.westnordost.streetcomplete.data.osm.edits.create.CreateNodeAction
import de.westnordost.streetcomplete.data.osm.edits.update_tags.StringMapChangesBuilder
import de.westnordost.streetcomplete.data.osm.edits.update_tags.UpdateElementTagsAction
import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
import de.westnordost.streetcomplete.data.osm.mapdata.Element
import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
import de.westnordost.streetcomplete.data.osm.mapdata.Node
import de.westnordost.streetcomplete.databinding.FragmentOverlayStreetFurnitureBinding
import de.westnordost.streetcomplete.osm.IS_DISUSED_STREET_FURNITURE_EXPRESSION
import de.westnordost.streetcomplete.osm.IS_STREET_FURNITURE_INCLUDING_DISUSED_EXPRESSION
import de.westnordost.streetcomplete.overlays.AbstractOverlayForm
import de.westnordost.streetcomplete.util.getLocalesForFeatureDictionary
import de.westnordost.streetcomplete.util.getLocationLabel
import de.westnordost.streetcomplete.util.ktx.geometryType
import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope
import de.westnordost.streetcomplete.view.controller.FeatureViewController
import de.westnordost.streetcomplete.view.dialogs.SearchFeaturesDialog
import kotlinx.coroutines.launch

class StreetFurnitureOverlayForm : AbstractOverlayForm() {

override val contentLayoutResId = R.layout.fragment_overlay_street_furniture
private val binding by contentViewBinding(FragmentOverlayStreetFurnitureBinding::bind)

private var originalFeature: Feature? = null

private lateinit var featureCtrl: FeatureViewController

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val element = element
originalFeature = element?.let {
if (IS_DISUSED_STREET_FURNITURE_EXPRESSION.matches(element)) {
DummyFeature(
"street_furniture/unknown_disused",
requireContext().getString(R.string.unknown_disused_street_furniture),
"ic_preset_maki_marker_stroked",
element.tags
)
} else {
featureDictionary
.byTags(element.tags)
.forLocale(*getLocalesForFeatureDictionary(resources.configuration))
.forGeometry(element.geometryType)
.inCountry(countryOrSubdivisionCode)
.find()
.firstOrNull()
// if not found anything in the iD presets, then something weird happened
?: DummyFeature(
"street_furniture/unknown",
requireContext().getString(R.string.unknown_street_furniture),
"ic_preset_maki_marker_stroked",
element.tags
)
}
}
}

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

// title hint label with name is a duplication, it is displayed in the UI already
setTitleHintLabel(element?.tags?.let { getLocationLabel(it, resources) })
setMarkerIcon(R.drawable.ic_quest_apple)

featureCtrl = FeatureViewController(featureDictionary, binding.featureTextView, binding.featureIconView)
featureCtrl.countryOrSubdivisionCode = countryOrSubdivisionCode
featureCtrl.feature = originalFeature

binding.featureView.setOnClickListener {
SearchFeaturesDialog(
requireContext(),
featureDictionary,
element?.geometryType,
countryOrSubdivisionCode,
featureCtrl.feature?.name,
::filterOnlyStreetFurniture,
::onSelectedFeature,
listOf(
// ordered by popularity, skipping trees as there are multiple variants of them
"highway/street_lamp",
"amenity/bench",
"emergency/fire_hydrant",
"amenity/bicycle_parking",
"amenity/shelter",
"amenity/toilets",
// "amenity/post_box",
// blocked by https://github.com/streetcomplete/StreetComplete/issues/4916
// waiting for response in https://github.com/ideditor/schema-builder/issues/94
"amenity/drinking_water",
"leisure/picnic_table",

// popular, a bit less than some competing entries
// but interesting and worth promoting
"emergency/defibrillator",
)
).show()
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
}

private fun filterOnlyStreetFurniture(feature: Feature): Boolean {
val fakeElement = Node(-1L, LatLon(0.0, 0.0), feature.tags, 0)
return IS_STREET_FURNITURE_INCLUDING_DISUSED_EXPRESSION.matches(fakeElement)
}

private fun onSelectedFeature(feature: Feature) {
featureCtrl.feature = feature
checkIsFormComplete()
}

override fun hasChanges(): Boolean =
originalFeature != featureCtrl.feature

override fun isFormComplete(): Boolean = featureCtrl.feature != null

override fun onClickOk() {
viewLifecycleScope.launch {
applyEdit(createEditAction(
element, geometry,
featureCtrl.feature!!, originalFeature,
))
}
}
}

private suspend fun createEditAction(
element: Element?,
geometry: ElementGeometry,
newFeature: Feature,
previousFeature: Feature?
): ElementEditAction {
val tagChanges = StringMapChangesBuilder(element?.tags ?: emptyMap())

for ((key, value) in previousFeature?.removeTags.orEmpty()) {
tagChanges.remove(key)
}
for ((key, value) in newFeature.addTags) {
tagChanges[key] = value
}

return if (element != null) {
UpdateElementTagsAction(element, tagChanges.create())
} else {
CreateNodeAction(geometry.center, tagChanges)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class ShopGoneDialog(
featureCtrl.feature?.name,
::filterOnlyShops,
::onSelectedFeature,
topFeatureCodesOfPopularShoplikePOIs(),
true
).show()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class ShopTypeForm : AbstractOsmQuestForm<ShopTypeAnswer>() {
countryOrSubdivisionCode,
featureCtrl.feature?.name,
::filterOnlyShops,
::onSelectedFeature
::onSelectedFeature,
topFeatureCodesOfPopularShoplikePOIs(),
).show()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.westnordost.streetcomplete.quests.shop_type

fun topFeatureCodesOfPopularShoplikePOIs(): List<String> {
// ordered by usage number according to taginfo
return listOf(
"amenity/restaurant",
"shop/convenience",
"amenity/cafe",
"shop/supermarket",
"amenity/fast_food",
"amenity/pharmacy",
"shop/clothes",
"shop/hairdresser"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class SearchFeaturesDialog(
text: String? = null,
private val filterFn: (Feature) -> Boolean = { true },
private val onSelectedFeatureFn: (Feature) -> Unit,
private val codesOfDefaultFeatures: List<String>,
private val dismissKeyboardOnClose: Boolean = false,
) : AlertDialog(context) {

Expand All @@ -39,17 +40,7 @@ class SearchFeaturesDialog(
private val searchText: String? get() = binding.searchEditText.nonBlankTextOrNull

private val defaultFeatures: List<Feature> by lazy {
listOf(
// ordered by usage number according to taginfo
"amenity/restaurant",
"shop/convenience",
"amenity/cafe",
"shop/supermarket",
"amenity/fast_food",
"amenity/pharmacy",
"shop/clothes",
"shop/hairdresser"
).mapNotNull {
codesOfDefaultFeatures.mapNotNull {
featureDictionary
.byId(it)
.forLocale(*locales)
Expand Down
Loading

0 comments on commit 8854bc5

Please sign in to comment.