diff --git a/app/src/commonMain/composeResources/values/strings.xml b/app/src/commonMain/composeResources/values/strings.xml
index 8dead15fb4..ec0afa3315 100644
--- a/app/src/commonMain/composeResources/values/strings.xml
+++ b/app/src/commonMain/composeResources/values/strings.xml
@@ -1121,6 +1121,8 @@
Spring
Disused drinking water object
+ What doctors are present here?
+
Is this still here?
Does this ferry route transport pedestrians?
diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/QuestTypesRegistry.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/QuestTypesRegistry.kt
index a0771b3f1d..b99bbfc843 100644
--- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/QuestTypesRegistry.kt
+++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/QuestTypesRegistry.kt
@@ -61,6 +61,7 @@ import de.westnordost.streetcomplete.quests.camping.AddCampPower
import de.westnordost.streetcomplete.quests.camping.AddCampShower
import de.westnordost.streetcomplete.quests.camping.AddCaravans
import de.westnordost.streetcomplete.quests.camping.AddTents
+import de.westnordost.streetcomplete.quests.doctor_type.AddDoctorType
import de.westnordost.streetcomplete.quests.car_wash_type.AddCarWashType
import de.westnordost.streetcomplete.quests.charge.AddParkingCharge
import de.westnordost.streetcomplete.quests.charging_station_bicycles.AddChargingStationBicycles
@@ -365,6 +366,8 @@ fun questTypeRegistry(
60 to AddGeneralFee(),
+ 200 to AddDoctorType(getCountryInfoByLocation),
+
61 to AddDrinkingWater(), // used by AnyFinder
62 to AddDrinkingWaterType(),
diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/doctor_type/AddDoctorType.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/doctor_type/AddDoctorType.kt
new file mode 100644
index 0000000000..2486f30d4e
--- /dev/null
+++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/doctor_type/AddDoctorType.kt
@@ -0,0 +1,39 @@
+package de.westnordost.streetcomplete.quests.doctor_type
+
+import androidx.compose.runtime.Composable
+import de.westnordost.osmfeatures.Feature
+import de.westnordost.streetcomplete.data.meta.CountryInfo
+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.osmquests.OsmFilterQuestType
+import de.westnordost.streetcomplete.data.osm.osmquests.QuestAction
+import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.LIFESAVER
+import de.westnordost.streetcomplete.data.user.achievements.EditTypeAchievement.CITIZEN
+import de.westnordost.streetcomplete.osm.Tags
+import de.westnordost.streetcomplete.resources.*
+
+class AddDoctorType(getCountryInfoByLocation: (LatLon) -> CountryInfo) : OsmFilterQuestType>() {
+
+ override val elementFilter = """
+ nodes, ways with
+ (
+ amenity=doctors
+ and !healthcare:speciality
+ )
+ """
+ override val changesetComment = "Survey what type of healthcare a doctor provides"
+ override val wikiLink = "Key:healthcare:speciality"
+ override val icon = Res.drawable.quest_restaurant_vegan
+ override val title = Res.string.quest_doctor_type_title
+ override val achievements = listOf(CITIZEN, LIFESAVER)
+
+ @Composable
+ override fun Form(on: (QuestAction>) -> Unit, element: Element, geometry: ElementGeometry, countryInfo: CountryInfo) {
+ AddDoctorTypeForm(on, element)
+ }
+
+ override fun applyAnswerTo(answer: List, tags: Tags, geometry: ElementGeometry, timestampEdited: Long) {
+ tags["healthcare:speciality"] = answer.joinToString(";") { it.tags["healthcare:speciality"] ?: "" }
+ }
+}
diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/doctor_type/AddDoctorTypeForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/doctor_type/AddDoctorTypeForm.kt
new file mode 100644
index 0000000000..25adb29bf8
--- /dev/null
+++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/quests/doctor_type/AddDoctorTypeForm.kt
@@ -0,0 +1,35 @@
+package de.westnordost.streetcomplete.quests.doctor_type
+
+import androidx.compose.runtime.Composable
+import de.westnordost.osmfeatures.Feature
+import de.westnordost.osmfeatures.FeatureDictionary
+import de.westnordost.streetcomplete.data.osm.mapdata.Element
+import de.westnordost.streetcomplete.data.osm.osmquests.QuestAction
+import de.westnordost.streetcomplete.ui.common.quest.FeatureSelectionForm
+import de.westnordost.streetcomplete.util.ktx.geometryType
+import org.koin.compose.koinInject
+
+val POPULAR_DOCTORS_FEATURE_IDS = listOf(
+ // ordered roughly by usage number according to taginfo
+ "amenity/doctors/ophthalmology",
+ "amenity/doctors/paediatrics",
+ "amenity/doctors/gynaecology",
+ "amenity/doctors/psychiatry",
+ "amenity/doctors/orthodontics",
+ "amenity/doctors/dermatology",
+)
+
+@Composable
+fun AddDoctorTypeForm(
+ on: (QuestAction>) -> Unit,
+ element: Element,
+ featureDictionary: FeatureDictionary = koinInject()
+) {
+ FeatureSelectionForm(
+ on = on,
+ featureDictionary = featureDictionary,
+ geometryType = element.geometryType,
+ filterFn = { (it.tags["amenity"] == "doctors" && it.tags["healthcare:speciality"] != null) },
+ codesOfDefaultFeatures = POPULAR_DOCTORS_FEATURE_IDS
+ )
+}
diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/feature/FeatureSearch.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/feature/FeatureSearch.kt
index b82c59c6c1..69e3ac3728 100644
--- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/feature/FeatureSearch.kt
+++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/feature/FeatureSearch.kt
@@ -47,7 +47,6 @@ fun FeatureSearch(
filterFn: (Feature) -> Boolean = { true },
codesOfDefaultFeatures: List = emptyList(),
) {
- val state = rememberLazyListState()
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
@@ -63,7 +62,7 @@ fun FeatureSearch(
languages = languages,
country = countryCode
)
- }
+ }.filter(filterFn)
}
val features = remember(search, featureDictionary, languages, countryCode, geometryType, filterFn, defaultFeatures) {
if (search.isNotEmpty()) {
diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/FeatureSelectionForm.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/FeatureSelectionForm.kt
new file mode 100644
index 0000000000..0eeb1e0bff
--- /dev/null
+++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/ui/common/quest/FeatureSelectionForm.kt
@@ -0,0 +1,116 @@
+package de.westnordost.streetcomplete.ui.common.quest
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import de.westnordost.osmfeatures.Feature
+import de.westnordost.osmfeatures.FeatureDictionary
+import de.westnordost.osmfeatures.GeometryType
+import de.westnordost.streetcomplete.data.osm.osmquests.Answer
+import de.westnordost.streetcomplete.data.osm.osmquests.QuestAction
+import de.westnordost.streetcomplete.resources.*
+import de.westnordost.streetcomplete.ui.common.Button2
+import de.westnordost.streetcomplete.ui.common.feature.FeatureItem
+import de.westnordost.streetcomplete.ui.common.feature.FeatureSearchDialog
+import org.jetbrains.compose.resources.painterResource
+import org.jetbrains.compose.resources.stringResource
+
+/**
+ * A quest form that allows selecting of features via search or from a list of presets.
+ */
+@Composable
+fun FeatureSelectionForm(
+ on: (QuestAction>) -> Unit,
+ featureDictionary: FeatureDictionary,
+ modifier: Modifier = Modifier,
+ initialSelectedFeatures: List = emptyList(),
+ geometryType: GeometryType? = null,
+ countryCode: String? = null,
+ filterFn: (Feature) -> Boolean = { true },
+ codesOfDefaultFeatures: List = emptyList(),
+) {
+ var selectedFeatures by remember { mutableStateOf(initialSelectedFeatures) }
+ var showSearch by remember { mutableStateOf(false) }
+
+ QuestForm(
+ on = on,
+ isComplete = selectedFeatures.isNotEmpty(),
+ onClickOk = { on(Answer(selectedFeatures)) },
+ modifier = modifier,
+ ) {
+ Column {
+ selectedFeatures.forEach { feature ->
+ FeatureDisplay(
+ feature = feature,
+ featureDictionary = featureDictionary,
+ countryCode = countryCode,
+ onRemove = { selectedFeatures = selectedFeatures - feature },
+ modifier = Modifier.padding(vertical = 4.dp)
+ )
+ }
+
+ Button2(
+ onClick = { showSearch = true }
+ ) {
+ Icon(painterResource(Res.drawable.ic_add_24), contentDescription = null)
+ Spacer(Modifier.size(8.dp))
+ Text(stringResource(Res.string.quest_presets_preset_add))
+ }
+
+ if (showSearch) {
+ FeatureSearchDialog(
+ onDismissRequest = { showSearch = false },
+ onSelectedFeature = {
+ selectedFeatures = selectedFeatures + it
+ showSearch = false
+ },
+ featureDictionary = featureDictionary,
+ geometryType = geometryType,
+ countryCode = countryCode,
+ // Ensure a preset cannot be selected twice
+ filterFn = { feature -> feature !in selectedFeatures && filterFn(feature) },
+ codesOfDefaultFeatures = codesOfDefaultFeatures
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun FeatureDisplay(
+ feature: Feature,
+ featureDictionary: FeatureDictionary,
+ countryCode: String?,
+ onRemove: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ FeatureItem(
+ feature = feature,
+ featureDictionary = featureDictionary,
+ countryCode = countryCode,
+ modifier = Modifier.weight(1f)
+ )
+ IconButton(onClick = onRemove) {
+ Icon(painterResource(Res.drawable.ic_delete_24), contentDescription = null)
+ }
+ }
+}