diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/StreetFurniture.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/StreetFurniture.kt new file mode 100644 index 0000000000..9f3bada3ab --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/osm/StreetFurniture.kt @@ -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() diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt index 4a3701b341..f23daccde7 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/OverlaysModule.kt @@ -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 @@ -56,4 +57,5 @@ fun overlaysRegistry( 2 to StreetParkingOverlay(), 3 to AddressOverlay(getCountryCodeByLocation), 4 to ShopsOverlay(getFeature), + 7 to StreetFurnitureOverlay(getFeature), )) diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlayForm.kt index e8af01c221..834a8bc2fd 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlayForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/shops/ShopsOverlayForm.kt @@ -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 @@ -109,7 +110,8 @@ class ShopsOverlayForm : AbstractOverlayForm() { countryOrSubdivisionCode, featureCtrl.feature?.name, ::filterOnlyShops, - ::onSelectedFeature + ::onSelectedFeature, + topFeatureCodesOfPopularShoplikePOIs(), ).show() } diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/street_furniture/DummyFeature.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/street_furniture/DummyFeature.kt new file mode 100644 index 0000000000..842c435e47 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/street_furniture/DummyFeature.kt @@ -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 +) : 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() + override fun getIncludeCountryCodes() = emptyList() + override fun getExcludeCountryCodes() = emptyList() + override fun isSearchable() = false + override fun getMatchScore() = 1.0 + override fun getAddTags() = addTags + override fun getRemoveTags() = emptyMap() + override fun getCanonicalNames() = emptyList() + override fun getCanonicalTerms() = emptyList() + override fun isSuggestion() = false + override fun getLocale(): Locale = Locale.getDefault() +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/street_furniture/StreetFurnitureOverlay.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/street_furniture/StreetFurnitureOverlay.kt new file mode 100644 index 0000000000..0eea98590f --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/street_furniture/StreetFurnitureOverlay.kt @@ -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) -> 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() +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/street_furniture/StreetFurnitureOverlayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/street_furniture/StreetFurnitureOverlayForm.kt new file mode 100644 index 0000000000..3332f2f21b --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/street_furniture/StreetFurnitureOverlayForm.kt @@ -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) + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopGoneDialog.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopGoneDialog.kt index efc62f59ac..8bdeeb4a10 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopGoneDialog.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopGoneDialog.kt @@ -56,6 +56,7 @@ class ShopGoneDialog( featureCtrl.feature?.name, ::filterOnlyShops, ::onSelectedFeature, + topFeatureCodesOfPopularShoplikePOIs(), true ).show() } diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopTypeForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopTypeForm.kt index 97b7d0a07f..e6f3cd98b8 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopTypeForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopTypeForm.kt @@ -45,7 +45,8 @@ class ShopTypeForm : AbstractOsmQuestForm() { countryOrSubdivisionCode, featureCtrl.feature?.name, ::filterOnlyShops, - ::onSelectedFeature + ::onSelectedFeature, + topFeatureCodesOfPopularShoplikePOIs(), ).show() } } diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopUtils.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopUtils.kt new file mode 100644 index 0000000000..54637672d6 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/shop_type/ShopUtils.kt @@ -0,0 +1,15 @@ +package de.westnordost.streetcomplete.quests.shop_type + +fun topFeatureCodesOfPopularShoplikePOIs(): List { + // 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" + ) +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/view/dialogs/SearchFeaturesDialog.kt b/app/src/main/java/de/westnordost/streetcomplete/view/dialogs/SearchFeaturesDialog.kt index 2b31bab68f..acc621deab 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/view/dialogs/SearchFeaturesDialog.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/view/dialogs/SearchFeaturesDialog.kt @@ -29,6 +29,7 @@ class SearchFeaturesDialog( text: String? = null, private val filterFn: (Feature) -> Boolean = { true }, private val onSelectedFeatureFn: (Feature) -> Unit, + private val codesOfDefaultFeatures: List, private val dismissKeyboardOnClose: Boolean = false, ) : AlertDialog(context) { @@ -39,17 +40,7 @@ class SearchFeaturesDialog( private val searchText: String? get() = binding.searchEditText.nonBlankTextOrNull private val defaultFeatures: List 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) diff --git a/app/src/main/res/layout/fragment_overlay_street_furniture.xml b/app/src/main/res/layout/fragment_overlay_street_furniture.xml new file mode 100644 index 0000000000..7d2fc859a9 --- /dev/null +++ b/app/src/main/res/layout/fragment_overlay_street_furniture.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f768d0dcc..621d284c32 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1661,6 +1661,9 @@ Partially means that a wheelchair can enter and use the restroom, but no handrai It’s not an entrance… Shops + Street furniture + Disused object + Unknown object Name: Other kind of place diff --git a/app/src/test/java/de/westnordost/streetcomplete/overlays/street_furniture/FlagpoleMatching.kt b/app/src/test/java/de/westnordost/streetcomplete/overlays/street_furniture/FlagpoleMatching.kt new file mode 100644 index 0000000000..27d193db94 --- /dev/null +++ b/app/src/test/java/de/westnordost/streetcomplete/overlays/street_furniture/FlagpoleMatching.kt @@ -0,0 +1,29 @@ +package de.westnordost.streetcomplete.overlays.street_furniture + +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.data.osm.mapdata.Node +import de.westnordost.streetcomplete.osm.IS_STREET_FURNITURE_INCLUDING_DISUSED_EXPRESSION +import kotlin.test.* +import kotlin.test.Test + +class FlagpoleMatching { + + @Test fun `flagpole matches`() { + val fakeElement = Node(-1L, LatLon(0.0, 0.0), mapOf("man_made" to "flagpole"), 0) + assertEquals(true, IS_STREET_FURNITURE_INCLUDING_DISUSED_EXPRESSION.matches(fakeElement)) + } + + @Test fun `specific flagpole matches`() { + // note that in search currently you may need to type "PL - Poland" + val fakeElement = Node(-1L, LatLon(0.0, 0.0), mapOf( + "country" to "PL", + "flag:name" to "Poland", + "flag:type" to "national", + "flag:wikidata" to "Q42436", + "man_made" to "flagpole", + "subject" to "Poland", + "subject:wikidata" to "Q36", + ), 0) + assertEquals(true, IS_STREET_FURNITURE_INCLUDING_DISUSED_EXPRESSION.matches(fakeElement)) + } +}