Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Bus Stop Name Suggestions, generalize Name Suggestions #6097

Merged
merged 15 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import de.westnordost.streetcomplete.quests.bus_stop_bench.AddBenchStatusOnBusSt
import de.westnordost.streetcomplete.quests.bus_stop_bin.AddBinStatusOnBusStop
import de.westnordost.streetcomplete.quests.bus_stop_lit.AddBusStopLit
import de.westnordost.streetcomplete.quests.bus_stop_name.AddBusStopName
import de.westnordost.streetcomplete.quests.bus_stop_name.BusStopNameSuggestionsSource
import de.westnordost.streetcomplete.quests.bus_stop_ref.AddBusStopRef
import de.westnordost.streetcomplete.quests.bus_stop_shelter.AddBusStopShelter
import de.westnordost.streetcomplete.quests.camera_type.AddCameraType
Expand Down Expand Up @@ -187,6 +188,7 @@ import org.koin.dsl.module

val questsModule = module {
factory { RoadNameSuggestionsSource(get()) }
factory { BusStopNameSuggestionsSource(get()) }

single {
questTypeRegistry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package de.westnordost.streetcomplete.quests.bus_stop_name

import androidx.appcompat.app.AlertDialog
import de.westnordost.streetcomplete.R
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.QuestLocalizednameBinding
import de.westnordost.streetcomplete.osm.LocalizedName
import de.westnordost.streetcomplete.quests.AAddLocalizedNameForm
import de.westnordost.streetcomplete.quests.AnswerItem
import org.koin.android.ext.android.inject

class AddBusStopNameForm : AAddLocalizedNameForm<BusStopNameAnswer>() {

Expand All @@ -15,11 +19,25 @@ class AddBusStopNameForm : AAddLocalizedNameForm<BusStopNameAnswer>() {
override val addLanguageButton get() = binding.addLanguageButton
override val namesList get() = binding.namesList

override val adapterRowLayoutResId = R.layout.quest_localizedname_row

override val otherAnswers = listOf(
AnswerItem(R.string.quest_placeName_no_name_answer) { confirmNoName() },
AnswerItem(R.string.quest_streetName_answer_cantType) { showKeyboardInfo() }
)
private val busStopNameSuggestionsSource: BusStopNameSuggestionsSource by inject()

override fun getLocalizedNameSuggestions(): List<List<LocalizedName>> {
val polyline = when (val geom = geometry) {
is ElementPolylinesGeometry -> geom.polylines.first()
is ElementPolygonsGeometry -> geom.polygons.first()
is ElementPointGeometry -> listOf(geom.center)
}
return busStopNameSuggestionsSource.getNames(
listOf(polyline.first(), polyline.last()),
MAX_DIST_FOR_BUS_STOP_NAME_SUGGESTION
)
kmpoppe marked this conversation as resolved.
Show resolved Hide resolved
}
override fun onClickOk(names: List<LocalizedName>) {
applyAnswer(BusStopName(names))
}
Expand All @@ -31,4 +49,8 @@ class AddBusStopNameForm : AAddLocalizedNameForm<BusStopNameAnswer>() {
.setNegativeButton(R.string.quest_generic_confirmation_no, null)
.show()
}

companion object {
const val MAX_DIST_FOR_BUS_STOP_NAME_SUGGESTION = 250.0 // m
}
}
kmpoppe marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package de.westnordost.streetcomplete.quests.bus_stop_name

import de.westnordost.streetcomplete.data.osm.edits.MapDataWithEditsSource
import de.westnordost.streetcomplete.data.osm.geometry.ElementPointGeometry
import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry
import de.westnordost.streetcomplete.data.osm.mapdata.LatLon
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 BusStopNameSuggestionsSource(
private val mapDataSource: MapDataWithEditsSource
) {

fun getNames(points: List<LatLon>, maxDistance: Double): 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 busStopWaysWithNames = mapData.ways.filter { busStopFilter(it.tags) }
val busStopNodesWithNames = mapData.nodes.filter { busStopFilter(it.tags) }

val result = mutableMapOf<List<LocalizedName>, Double>()

for (busStop in busStopWaysWithNames) {
val geometry = mapData.getWayGeometry(busStop.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(busStop.tags) ?: continue

// eliminate duplicates (same road, different segments, different distances)
val prev = result[names]
if (prev != null && prev < minDistanceToRoad) continue

result[names] = minDistanceToRoad
}

for (busStop in busStopNodesWithNames) {
val geometry = mapData.getNodeGeometry(busStop.id) ?: continue

val minDistanceToRoad = points.distanceTo(listOf(geometry.center))
if (minDistanceToRoad > maxDistance) continue

val names = parseLocalizedNames(busStop.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 busStopFilter(tags: Map<String, String>) =
tags.containsKey("name")
&& (
(tags["highway"] == "bus_stop" && tags["public_transport"] != "stop_position")
|| (tags["public_transport"] == "platform" && tags["bus"] == "yes")
|| tags["railway"] == "halt"
|| tags["railway"] == "station"
|| tags["railway"] == "tram_stop"
)
kmpoppe marked this conversation as resolved.
Show resolved Hide resolved
}
68 changes: 68 additions & 0 deletions app/src/main/res/layout/quest_localizedname_row.xml
kmpoppe marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">

<TextView
android:id="@+id/languageButton"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textAllCaps="false"
android:layout_width="50dp"
android:fontFamily="monospace"
android:gravity="center"
android:textAlignment="center"
tools:text="de"
android:layout_height="wrap_content"/>

<me.grantland.widget.AutofitLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_toEndOf="@id/languageButton"
android:layout_toStartOf="@+id/nameSuggestionsButton"
android:layout_centerVertical="true"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingStart="14dp"
android:paddingEnd="14dp"
>

<EditText
android:id="@+id/autoCorrectInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="@dimen/large_input"
android:textAlignment="center"
android:maxLines="1"
android:inputType="text|textNoSuggestions|textCapSentences"
tools:text="Sesame Street"
tools:ignore="RtlCompat,SpUsage" />

</me.grantland.widget.AutofitLayout>

<ImageView
android:id="@+id/nameSuggestionsButton"
android:layout_width="50dp"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_arrow_expand_down_24dp"
android:layout_toStartOf="@+id/deleteButton"
android:layout_centerVertical="true"
android:layout_marginEnd="8dp"
style="@style/Base.Widget.AppCompat.Button.Borderless" />

<ImageView
android:id="@+id/deleteButton"
app:srcCompat="@drawable/ic_delete_24dp"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
style="@style/Base.Widget.AppCompat.Button.Borderless"
tools:ignore="HardcodedText"/>

</RelativeLayout>