diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/address/AddressStreetNameInputViewController.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/address/AddressStreetNameInputViewController.kt index 67659e3370e..5484e688e05 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/osm/address/AddressStreetNameInputViewController.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/osm/address/AddressStreetNameInputViewController.kt @@ -2,9 +2,13 @@ package de.westnordost.streetcomplete.osm.address import android.widget.EditText import androidx.core.widget.doAfterTextChanged +import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression import de.westnordost.streetcomplete.data.meta.AbbreviationsByLocale import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.quests.road_name.RoadNameSuggestionsSource +import de.westnordost.streetcomplete.osm.ALL_PATHS +import de.westnordost.streetcomplete.osm.ALL_ROADS +import de.westnordost.streetcomplete.quests.NameSuggestionsSource +import de.westnordost.streetcomplete.quests.road_name.AddRoadNameForm import de.westnordost.streetcomplete.util.ktx.nonBlankTextOrNull import de.westnordost.streetcomplete.view.controller.AutoCorrectAbbreviationsViewController import java.util.Locale @@ -14,12 +18,16 @@ import java.util.Locale * automatically expanded, e.g. "Main st" becomes "Main street" */ class AddressStreetNameInputViewController( private val streetNameInput: EditText, - private val roadNameSuggestionsSource: RoadNameSuggestionsSource, + private val nameSuggestionsSource: NameSuggestionsSource, abbreviationsByLocale: AbbreviationsByLocale, private val countryLocale: Locale ) { private val autoCorrectAbbreviationsViewController: AutoCorrectAbbreviationsViewController + private val roadsWithNamesFilter = + "ways with highway ~ ${(ALL_ROADS + ALL_PATHS).joinToString("|")} and name" + .toElementFilterExpression() + var onInputChanged: (() -> Unit)? = null var streetName: String? @@ -33,12 +41,15 @@ class AddressStreetNameInputViewController( streetNameInput.doAfterTextChanged { onInputChanged?.invoke() } } - /** select the name of the street near the given [position] (ast most [radiusInMeters] from it) + /** select the name of the street near the given [position] (at most [radiusInMeters] from it) * instead of typing it in the edit text */ fun selectStreetAt(position: LatLon, radiusInMeters: Double): Boolean { - val dist = radiusInMeters + 5 - val namesByLocale = roadNameSuggestionsSource - .getNames(listOf(position), dist) + val namesByLocale = nameSuggestionsSource + .getNames( + points = listOf(position), + maxDistance = radiusInMeters, + filter = roadsWithNamesFilter + ) .firstOrNull() ?.associate { it.languageTag to it.name }?.toMutableMap() ?: return false diff --git a/app/src/main/java/de/westnordost/streetcomplete/osm/address/StreetOrPlaceNameViewController.kt b/app/src/main/java/de/westnordost/streetcomplete/osm/address/StreetOrPlaceNameViewController.kt index eb59439fcb5..6e1122c01fd 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/osm/address/StreetOrPlaceNameViewController.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/osm/address/StreetOrPlaceNameViewController.kt @@ -11,7 +11,7 @@ import de.westnordost.streetcomplete.R import de.westnordost.streetcomplete.data.meta.AbbreviationsByLocale import de.westnordost.streetcomplete.data.osm.mapdata.LatLon import de.westnordost.streetcomplete.osm.address.StreetOrPlaceNameViewController.StreetOrPlace.* -import de.westnordost.streetcomplete.quests.road_name.RoadNameSuggestionsSource +import de.westnordost.streetcomplete.quests.NameSuggestionsSource import de.westnordost.streetcomplete.util.ktx.nonBlankTextOrNull import de.westnordost.streetcomplete.view.OnAdapterItemSelectedListener import java.util.Locale @@ -26,13 +26,13 @@ class StreetOrPlaceNameViewController( private val placeNameInput: EditText, private val streetNameInputContainer: View, private val streetNameInput: EditText, - roadNameSuggestionsSource: RoadNameSuggestionsSource, + nameSuggestionsSource: NameSuggestionsSource, abbreviationsByLocale: AbbreviationsByLocale, countryLocale: Locale, startWithPlace: Boolean, ) { private val streetNameInputCtrl = AddressStreetNameInputViewController( - streetNameInput, roadNameSuggestionsSource, abbreviationsByLocale, countryLocale + streetNameInput, nameSuggestionsSource, abbreviationsByLocale, countryLocale ) var onInputChanged: (() -> Unit)? = null diff --git a/app/src/main/java/de/westnordost/streetcomplete/overlays/address/AddressOverlayForm.kt b/app/src/main/java/de/westnordost/streetcomplete/overlays/address/AddressOverlayForm.kt index 569569dfc26..aa29a00097f 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/overlays/address/AddressOverlayForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/overlays/address/AddressOverlayForm.kt @@ -40,7 +40,7 @@ import de.westnordost.streetcomplete.osm.address.streetHouseNumber import de.westnordost.streetcomplete.overlays.AbstractOverlayForm import de.westnordost.streetcomplete.overlays.AnswerItem import de.westnordost.streetcomplete.overlays.IAnswerItem -import de.westnordost.streetcomplete.quests.road_name.RoadNameSuggestionsSource +import de.westnordost.streetcomplete.quests.NameSuggestionsSource import de.westnordost.streetcomplete.screens.main.bottom_sheet.IsMapPositionAware import de.westnordost.streetcomplete.util.getNameAndLocationSpanned import de.westnordost.streetcomplete.util.ktx.dpToPx @@ -57,7 +57,7 @@ class AddressOverlayForm : AbstractOverlayForm(), IsMapPositionAware { private val mapDataWithEditsSource: MapDataWithEditsSource by inject() private val abbreviationsByLocale: AbbreviationsByLocale by inject() - private val roadNameSuggestionsSource: RoadNameSuggestionsSource by inject() + private val nameSuggestionsSource: NameSuggestionsSource by inject() private lateinit var numberOrNameInputCtrl: AddressNumberAndNameInputViewController private lateinit var streetOrPlaceCtrl: StreetOrPlaceNameViewController @@ -149,7 +149,7 @@ class AddressOverlayForm : AbstractOverlayForm(), IsMapPositionAware { placeNameInput = streetOrPlaceBinding.placeNameInput.apply { hint = lastPlaceName }, streetNameInputContainer = streetOrPlaceBinding.streetNameInputContainer, streetNameInput = streetOrPlaceBinding.streetNameInput.apply { hint = lastStreetName }, - roadNameSuggestionsSource = roadNameSuggestionsSource, + nameSuggestionsSource = nameSuggestionsSource, abbreviationsByLocale = abbreviationsByLocale, countryLocale = countryInfo.locale, startWithPlace = isShowingPlaceName diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/NameSuggestionsSource.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/NameSuggestionsSource.kt new file mode 100644 index 00000000000..4e101a03eae --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/NameSuggestionsSource.kt @@ -0,0 +1,54 @@ +package de.westnordost.streetcomplete.quests + +import de.westnordost.streetcomplete.data.elementfilter.ElementFilterExpression +import de.westnordost.streetcomplete.data.osm.edits.MapDataWithEditsSource +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.data.osm.mapdata.filter +import de.westnordost.streetcomplete.osm.LocalizedName +import de.westnordost.streetcomplete.osm.parseLocalizedNames +import de.westnordost.streetcomplete.util.math.distance +import de.westnordost.streetcomplete.util.math.enclosingBoundingBox +import de.westnordost.streetcomplete.util.math.enlargedBy + +class NameSuggestionsSource(private val mapDataSource: MapDataWithEditsSource) { + /** + * Return a list of [LocalizedName]s of elements with name(s), sorted by distance ascending to + * any of the given [points] that have at most a distance of [maxDistance] in m to those. The + * elements can be filtered with the given [filter] expression, to e.g. only find + * roads with names. + */ + fun getNames( + points: List, + maxDistance: Double, + filter: ElementFilterExpression + ): List> { + if (points.isEmpty()) return emptyList() + + /* add 100m radius for bbox query because roads will only be included in the result that + have at least one node in the bounding box around the tap position. This is a problem for + long straight roads (#3797). This doesn't completely solve this issue but mitigates it */ + val bbox = points.enclosingBoundingBox().enlargedBy(maxDistance + 100) + val mapData = mapDataSource.getMapDataWithGeometry(bbox) + val filteredElements = mapData.filter(filter) + // map of localized names -> min distance + val result = mutableMapOf, Double>() + + for (element in filteredElements) { + val geometry = mapData.getGeometry(element.type, element.id) ?: continue + + val minDistance = points.minOf { geometry.distance(it) } + if (minDistance > maxDistance) continue + + val names = parseLocalizedNames(element.tags) ?: continue + + // eliminate duplicates (e.g. same road, different segments, different distances) + val prev = result[names] + if (prev != null && prev < minDistance) continue + + result[names] = minDistance + } + + // return only the road names, sorted by distance ascending + return result.entries.sortedBy { it.value }.map { it.key } + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt index daab2febcc4..75adefc05f2 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/QuestsModule.kt @@ -135,7 +135,6 @@ import de.westnordost.streetcomplete.quests.recycling_material.AddRecyclingConta import de.westnordost.streetcomplete.quests.religion.AddReligionToPlaceOfWorship import de.westnordost.streetcomplete.quests.religion.AddReligionToWaysideShrine import de.westnordost.streetcomplete.quests.road_name.AddRoadName -import de.westnordost.streetcomplete.quests.road_name.RoadNameSuggestionsSource import de.westnordost.streetcomplete.quests.roof_shape.AddRoofShape import de.westnordost.streetcomplete.quests.sanitary_dump_station.AddSanitaryDumpStation import de.westnordost.streetcomplete.quests.seating.AddSeating @@ -186,7 +185,7 @@ import org.koin.core.qualifier.named import org.koin.dsl.module val questsModule = module { - factory { RoadNameSuggestionsSource(get()) } + factory { NameSuggestionsSource(get()) } single { questTypeRegistry( diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddAddressStreetForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddAddressStreetForm.kt index 21a827daa38..bc438c236a8 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddAddressStreetForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/address/AddAddressStreetForm.kt @@ -12,7 +12,7 @@ import de.westnordost.streetcomplete.osm.address.StreetOrPlaceName import de.westnordost.streetcomplete.osm.address.StreetOrPlaceNameViewController import de.westnordost.streetcomplete.quests.AbstractOsmQuestForm import de.westnordost.streetcomplete.quests.AnswerItem -import de.westnordost.streetcomplete.quests.road_name.RoadNameSuggestionsSource +import de.westnordost.streetcomplete.quests.NameSuggestionsSource import de.westnordost.streetcomplete.util.getNameAndLocationSpanned import org.koin.android.ext.android.inject @@ -21,7 +21,7 @@ class AddAddressStreetForm : AbstractOsmQuestForm() { private val binding by contentViewBinding(ViewStreetOrPlaceNameInputBinding::bind) private val abbreviationsByLocale: AbbreviationsByLocale by inject() - private val roadNameSuggestionsSource: RoadNameSuggestionsSource by inject() + private val nameSuggestionsSource: NameSuggestionsSource by inject() private lateinit var streetOrPlaceCtrl: StreetOrPlaceNameViewController @@ -51,7 +51,7 @@ class AddAddressStreetForm : AbstractOsmQuestForm() { placeNameInput = binding.placeNameInput, streetNameInputContainer = binding.streetNameInputContainer, streetNameInput = binding.streetNameInput, - roadNameSuggestionsSource = roadNameSuggestionsSource, + nameSuggestionsSource = nameSuggestionsSource, abbreviationsByLocale = abbreviationsByLocale, countryLocale = countryInfo.locale, startWithPlace = isShowingPlaceName diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopName.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopName.kt index 079a0d34f27..6487664a46f 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopName.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopName.kt @@ -10,14 +10,13 @@ import de.westnordost.streetcomplete.osm.applyTo class AddBusStopName : OsmFilterQuestType() { + // this filter needs to be kept somewhat in sync with the filter in AddBusStopNameForm override val elementFilter = """ nodes, ways, relations with ( public_transport = platform and bus = yes - or (highway = bus_stop and public_transport != stop_position) - or railway = halt - or railway = station - or railway = tram_stop + or highway = bus_stop and public_transport != stop_position + or railway ~ halt|station|tram_stop ) and !name and noname != yes and name:signed != no """ diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopNameForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopNameForm.kt index a871c96315a..1bceeb5377b 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopNameForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/bus_stop_name/AddBusStopNameForm.kt @@ -2,13 +2,18 @@ package de.westnordost.streetcomplete.quests.bus_stop_name import androidx.appcompat.app.AlertDialog import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression import de.westnordost.streetcomplete.databinding.QuestLocalizednameBinding import de.westnordost.streetcomplete.osm.LocalizedName import de.westnordost.streetcomplete.quests.AAddLocalizedNameForm import de.westnordost.streetcomplete.quests.AnswerItem +import de.westnordost.streetcomplete.quests.NameSuggestionsSource +import org.koin.android.ext.android.inject class AddBusStopNameForm : AAddLocalizedNameForm() { + private val nameSuggestionsSource: NameSuggestionsSource by inject() + override val contentLayoutResId = R.layout.quest_localizedname private val binding by contentViewBinding(QuestLocalizednameBinding::bind) @@ -20,6 +25,25 @@ class AddBusStopNameForm : AAddLocalizedNameForm() { AnswerItem(R.string.quest_streetName_answer_cantType) { showKeyboardInfo() } ) + // this filter needs to be kept somewhat in sync with the filter in AddBusStopName + private val busStopsWithNamesFilter = """ + nodes, ways, relations with + ( + public_transport = platform and bus = yes + or highway = bus_stop and public_transport != stop_position + or railway ~ halt|station|tram_stop + ) + and name + """.toElementFilterExpression() + + override fun getLocalizedNameSuggestions(): List> = + nameSuggestionsSource.getNames( + // bus stops are usually not that large, we can just take the center for the dist check + points = listOf(geometry.center), + maxDistance = 250.0, + filter = busStopsWithNamesFilter + ) + override fun onClickOk(names: List) { applyAnswer(BusStopName(names)) } diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/AddRoadNameForm.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/AddRoadNameForm.kt index 5ba3063f8d2..f9e32301943 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/AddRoadNameForm.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/AddRoadNameForm.kt @@ -3,14 +3,18 @@ package de.westnordost.streetcomplete.quests.road_name import android.content.DialogInterface import androidx.appcompat.app.AlertDialog import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.data.elementfilter.toElementFilterExpression import de.westnordost.streetcomplete.data.meta.AbbreviationsByLocale import de.westnordost.streetcomplete.data.osm.geometry.ElementPointGeometry import de.westnordost.streetcomplete.data.osm.geometry.ElementPolygonsGeometry import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry import de.westnordost.streetcomplete.databinding.QuestRoadnameBinding +import de.westnordost.streetcomplete.osm.ALL_PATHS +import de.westnordost.streetcomplete.osm.ALL_ROADS import de.westnordost.streetcomplete.osm.LocalizedName import de.westnordost.streetcomplete.quests.AAddLocalizedNameForm import de.westnordost.streetcomplete.quests.AnswerItem +import de.westnordost.streetcomplete.quests.NameSuggestionsSource import org.koin.android.ext.android.inject import java.lang.IllegalStateException import java.util.LinkedList @@ -32,19 +36,28 @@ class AddRoadNameForm : AAddLocalizedNameForm() { ) private val abbrByLocale: AbbreviationsByLocale by inject() - private val roadNameSuggestionsSource: RoadNameSuggestionsSource by inject() + private val nameSuggestionsSource: NameSuggestionsSource by inject() + + private val roadsWithNamesFilter = + "ways with highway ~ ${(ALL_ROADS + ALL_PATHS).joinToString("|")} and name" + .toElementFilterExpression() override fun getAbbreviationsByLocale(): AbbreviationsByLocale = abbrByLocale override fun getLocalizedNameSuggestions(): List> { - val polyline = when (val geom = geometry) { + val firstAndLast = when (val geom = geometry) { is ElementPolylinesGeometry -> geom.polylines.first() is ElementPolygonsGeometry -> geom.polygons.first() is ElementPointGeometry -> listOf(geom.center) - } - return roadNameSuggestionsSource.getNames( - listOf(polyline.first(), polyline.last()), - MAX_DIST_FOR_ROAD_NAME_SUGGESTION + }.let { listOf(it.first(), it.last()) } + + return nameSuggestionsSource.getNames( + // only first and last point of polyline because a still unnamed section of road is + // usually (if at all) a continuation of a neighbouring road section + points = firstAndLast, + // and hence we can also search only in a very small area only + maxDistance = 30.0, + filter = roadsWithNamesFilter ) } @@ -134,8 +147,4 @@ class AddRoadNameForm : AAddLocalizedNameForm() { .setNegativeButton(R.string.quest_generic_confirmation_no, null) .show() } - - companion object { - const val MAX_DIST_FOR_ROAD_NAME_SUGGESTION = 30.0 // m - } } diff --git a/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/RoadNameSuggestionsSource.kt b/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/RoadNameSuggestionsSource.kt deleted file mode 100644 index 64baa11c8f8..00000000000 --- a/app/src/main/java/de/westnordost/streetcomplete/quests/road_name/RoadNameSuggestionsSource.kt +++ /dev/null @@ -1,57 +0,0 @@ -package de.westnordost.streetcomplete.quests.road_name - -import de.westnordost.streetcomplete.data.osm.edits.MapDataWithEditsSource -import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry -import de.westnordost.streetcomplete.data.osm.mapdata.LatLon -import de.westnordost.streetcomplete.data.osm.mapdata.Way -import de.westnordost.streetcomplete.osm.ALL_PATHS -import de.westnordost.streetcomplete.osm.ALL_ROADS -import de.westnordost.streetcomplete.osm.LocalizedName -import de.westnordost.streetcomplete.osm.parseLocalizedNames -import de.westnordost.streetcomplete.util.math.distanceTo -import de.westnordost.streetcomplete.util.math.enclosingBoundingBox -import de.westnordost.streetcomplete.util.math.enlargedBy - -class RoadNameSuggestionsSource( - private val mapDataSource: MapDataWithEditsSource -) { - - fun getNames(points: List, maxDistance: Double): List> { - if (points.isEmpty()) return emptyList() - - /* add 100m radius for bbox query because roads will only be included in the result that have - at least one node in the bounding box around the tap position. This is a problem for long - straight roads (#3797). This doesn't completely solve this issue but mitigates it */ - val bbox = points.enclosingBoundingBox().enlargedBy(maxDistance + 100) - val mapData = mapDataSource.getMapDataWithGeometry(bbox) - val roadsWithNames = mapData.ways.filter { it.isRoadWithName() } - - val result = mutableMapOf, Double>() - for (road in roadsWithNames) { - val geometry = mapData.getWayGeometry(road.id) as? ElementPolylinesGeometry ?: continue - - val polyline = geometry.polylines.firstOrNull() ?: continue - if (polyline.isEmpty()) continue - - val minDistanceToRoad = points.distanceTo(polyline) - if (minDistanceToRoad > maxDistance) continue - - val names = parseLocalizedNames(road.tags) ?: continue - - // eliminate duplicates (same road, different segments, different distances) - val prev = result[names] - if (prev != null && prev < minDistanceToRoad) continue - - result[names] = minDistanceToRoad - } - // return only the road names, sorted by distance ascending - return result.entries.sortedBy { it.value }.map { it.key } - } - - private fun Way.isRoadWithName(): Boolean = - tags.containsKey("name") && tags["highway"] in ALL_ROADS_AND_PATHS - - companion object { - private val ALL_ROADS_AND_PATHS = ALL_ROADS + ALL_PATHS - } -} diff --git a/app/src/main/java/de/westnordost/streetcomplete/util/math/ElementGeometryMath.kt b/app/src/main/java/de/westnordost/streetcomplete/util/math/ElementGeometryMath.kt index af9935b4708..1724dd36972 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/util/math/ElementGeometryMath.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/util/math/ElementGeometryMath.kt @@ -24,6 +24,25 @@ fun ElementGeometry.intersects(other: ElementGeometry): Boolean { } } +/** Minimum distance to a [point]. If this is a polygon(s), the distance is 0 if [point] is within + * this polygon(s). */ +fun ElementGeometry.distance(point: LatLon): Double = + when (this) { + is ElementPointGeometry -> { + center.distanceTo(point) + } + is ElementPolylinesGeometry -> { + polylines.filter { it.isNotEmpty() }.minOf { point.distanceToArcs(it) } + } + is ElementPolygonsGeometry -> { + if (polygons.any { point.isInPolygon(it) }) { + 0.0 + } else { + polygons.filter { it.isNotEmpty() }.minOf { point.distanceToArcs(it) } + } + } + } + private fun ElementGeometry.asList(): List> = when (this) { is ElementPointGeometry -> listOf(listOf(center)) is ElementPolygonsGeometry -> polygons