Skip to content

Commit

Permalink
Add Bus Stop Name Suggestions, generalize Name Suggestions (#6097)
Browse files Browse the repository at this point in the history
Add Bus Stop Name Suggestions

Resolves #5187

---------

Co-authored-by: Flo Edelmann <[email protected]>
Co-authored-by: Tobias Zwick <[email protected]>
  • Loading branch information
3 people authored Jan 22, 2025
1 parent 05b8ace commit 86256a6
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check failure on line 11 in app/src/main/java/de/westnordost/streetcomplete/osm/address/AddressStreetNameInputViewController.kt

View workflow job for this annotation

GitHub Actions / Kotlin

Unused import
import de.westnordost.streetcomplete.util.ktx.nonBlankTextOrNull
import de.westnordost.streetcomplete.view.controller.AutoCorrectAbbreviationsViewController
import java.util.Locale
Expand All @@ -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?
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LatLon>,
maxDistance: Double,
filter: ElementFilterExpression
): List<List<LocalizedName>> {
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<List<LocalizedName>, 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 }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -21,7 +21,7 @@ class AddAddressStreetForm : AbstractOsmQuestForm<StreetOrPlaceName>() {
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

Expand Down Expand Up @@ -51,7 +51,7 @@ class AddAddressStreetForm : AbstractOsmQuestForm<StreetOrPlaceName>() {
placeNameInput = binding.placeNameInput,
streetNameInputContainer = binding.streetNameInputContainer,
streetNameInput = binding.streetNameInput,
roadNameSuggestionsSource = roadNameSuggestionsSource,
nameSuggestionsSource = nameSuggestionsSource,
abbreviationsByLocale = abbreviationsByLocale,
countryLocale = countryInfo.locale,
startWithPlace = isShowingPlaceName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import de.westnordost.streetcomplete.osm.applyTo

class AddBusStopName : OsmFilterQuestType<BusStopNameAnswer>() {

// 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
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BusStopNameAnswer>() {

private val nameSuggestionsSource: NameSuggestionsSource by inject()

override val contentLayoutResId = R.layout.quest_localizedname
private val binding by contentViewBinding(QuestLocalizednameBinding::bind)

Expand All @@ -20,6 +25,25 @@ class AddBusStopNameForm : AAddLocalizedNameForm<BusStopNameAnswer>() {
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<List<LocalizedName>> =
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<LocalizedName>) {
applyAnswer(BusStopName(names))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,19 +36,28 @@ class AddRoadNameForm : AAddLocalizedNameForm<RoadNameAnswer>() {
)

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<List<LocalizedName>> {
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
)
}

Expand Down Expand Up @@ -134,8 +147,4 @@ class AddRoadNameForm : AAddLocalizedNameForm<RoadNameAnswer>() {
.setNegativeButton(R.string.quest_generic_confirmation_no, null)
.show()
}

companion object {
const val MAX_DIST_FOR_ROAD_NAME_SUGGESTION = 30.0 // m
}
}

This file was deleted.

Loading

0 comments on commit 86256a6

Please sign in to comment.