OSM: search by tags "amenity" and "shop" after delocalizing (#1253)
* OSM: search by tags "amenity" and "shop" after delocalizing * use overpassQL UNION * document Union usage * cleanup unused imports * refine restaurant queries * two more :) * Hardcode string -> tag assignment * Remove log statement --------- Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
parent
ce5495418e
commit
3478411d7e
@ -30,3 +30,9 @@ fun String.normalize(): String {
|
||||
fun String.romanize(): String {
|
||||
return Pinyin.toPinyin(this, "")
|
||||
}
|
||||
|
||||
fun String.stripStartOrNull(s: String): String?
|
||||
= if (startsWith(s)) removePrefix(s) else null
|
||||
|
||||
fun String.stripEndOrNull(s: String): String?
|
||||
= if (endsWith(s)) removeSuffix(s) else null
|
||||
@ -5,16 +5,16 @@ import android.util.Log
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.locations.providers.AndroidLocation
|
||||
import de.mm20.launcher2.locations.providers.LocationProvider
|
||||
import de.mm20.launcher2.openstreetmaps.R
|
||||
import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
||||
import de.mm20.launcher2.search.Location
|
||||
import de.mm20.launcher2.search.ResultScore
|
||||
import de.mm20.launcher2.search.UpdateResult
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
@ -38,7 +38,8 @@ internal class OsmLocationProvider(
|
||||
try {
|
||||
Retrofit.Builder()
|
||||
.client(HttpClient)
|
||||
.baseUrl(it?.takeIf { it.isNotBlank() } ?: LocationSearchSettings.DefaultOverpassUrl)
|
||||
.baseUrl(it?.takeIf { it.isNotBlank() }
|
||||
?: LocationSearchSettings.DefaultOverpassUrl)
|
||||
.addConverterFactory(OverpassQueryConverterFactory())
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
@ -109,44 +110,27 @@ internal class OsmLocationProvider(
|
||||
HttpClient.dispatcher.cancelAll()
|
||||
}
|
||||
|
||||
suspend fun searchByTag(tag: String): OverpassResponse? =
|
||||
overpassApi.first()?.runCatching {
|
||||
this.search(
|
||||
return overpassApi.first()?.runCatching {
|
||||
search(
|
||||
OverpassFuzzyRadiusQuery(
|
||||
tag = tag,
|
||||
query = query,
|
||||
tagGroups = delocalizeToQueryableTags(query),
|
||||
radius = searchRadiusMeters,
|
||||
latitude = userLocation.latitude,
|
||||
longitude = userLocation.longitude,
|
||||
longitude = userLocation.longitude
|
||||
)
|
||||
)
|
||||
}?.onFailure {
|
||||
if (it !is HttpException && it !is CancellationException) {
|
||||
Log.e("OsmLocationProvider", "Failed to search for $tag: $query", it)
|
||||
Log.e("OsmLocationProvider", "Failed to search for: $query", it)
|
||||
}
|
||||
}?.getOrNull()
|
||||
|
||||
val result = awaitAll(
|
||||
// optionally query by "amenity" or "shop" here
|
||||
// if we want to make searching for locations fuzzier
|
||||
// however, this would not account for localized queries like "Bäcker" (shop:bakery)
|
||||
Scope.async { searchByTag("name") },
|
||||
Scope.async { searchByTag("brand") },
|
||||
).flatMap {
|
||||
it?.let {
|
||||
}?.getOrNull()?.let {
|
||||
OsmLocation.fromOverpassResponse(it, context)
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
return result
|
||||
.asSequence()
|
||||
.filter {
|
||||
}?.asSequence()?.filter {
|
||||
(!hideUncategorized || (it.category != null)) && it.distanceTo(userLocation) < searchRadiusMeters
|
||||
}
|
||||
.groupBy {
|
||||
}?.groupBy {
|
||||
it.label.lowercase()
|
||||
}
|
||||
.flatMap { (_, duplicates) ->
|
||||
}?.flatMap { (_, duplicates) ->
|
||||
// deduplicate results with same labels, if
|
||||
// - same category
|
||||
// - distance is less than 100m
|
||||
@ -160,11 +144,110 @@ internal class OsmLocationProvider(
|
||||
it.distanceTo(luckyFirst) > 100.0
|
||||
} + luckyFirst
|
||||
}
|
||||
}
|
||||
.sortedBy {
|
||||
}?.sortedBy {
|
||||
it.distanceTo(userLocation)
|
||||
}?.take(9)?.toImmutableList() ?: emptyList()
|
||||
}
|
||||
.take(7)
|
||||
.toImmutableList()
|
||||
|
||||
private val poiCategories = mapOf(
|
||||
R.string.poi_category_restaurant to "amenity=restaurant",
|
||||
R.string.poi_category_fast_food to "amenity=fast_food",
|
||||
R.string.poi_category_bar to "amenity=bar",
|
||||
R.string.poi_category_cafe to "amenity=cafe",
|
||||
R.string.poi_category_hotel to "tourism=hotel",
|
||||
R.string.poi_category_supermarket to "shop=supermarket",
|
||||
R.string.poi_category_school to "amenity=school",
|
||||
R.string.poi_category_parking to "amenity=parking",
|
||||
R.string.poi_category_fuel to "amenity=fuel",
|
||||
R.string.poi_category_toilets to "amenity=toilets",
|
||||
R.string.poi_category_pharmacy to "amenity=pharmacy",
|
||||
R.string.poi_category_hospital to "amenity=hospital",
|
||||
R.string.poi_category_post_office to "amenity=post_office",
|
||||
R.string.poi_category_pub to "amenity=pub",
|
||||
R.string.poi_category_doctors to "amenity=doctors",
|
||||
R.string.poi_category_police to "amenity=police",
|
||||
R.string.poi_category_dentist to "amenity=dentist",
|
||||
R.string.poi_category_library to "amenity=library",
|
||||
R.string.poi_category_ice_cream to "amenity=ice_cream",
|
||||
R.string.poi_category_theater to "amenity=theatre",
|
||||
R.string.poi_category_cinema to "amenity=cinema",
|
||||
R.string.poi_category_nightclub to "amenity=nightclub",
|
||||
R.string.poi_category_clinic to "amenity=clinic",
|
||||
R.string.poi_category_university to "amenity=university",
|
||||
R.string.poi_category_clothes to "shop=clothes",
|
||||
R.string.poi_category_convenience to "shop=convenience",
|
||||
R.string.poi_category_hairdresser to "shop=hairdresser",
|
||||
R.string.poi_category_books to "shop=books",
|
||||
R.string.poi_category_bakery to "shop=bakery",
|
||||
R.string.poi_category_car_rental to "amenity=car_rental",
|
||||
R.string.poi_category_car_sharing to "amenity=car_sharing",
|
||||
R.string.poi_category_mobile_phone to "shop=mobile_phone",
|
||||
R.string.poi_category_furniture to "shop=furniture",
|
||||
R.string.poi_category_alcohol to "shop=alcohol",
|
||||
R.string.poi_category_florist to "shop=florist",
|
||||
R.string.poi_category_mall to "shop=mall",
|
||||
R.string.poi_category_optician to "shop=optician",
|
||||
R.string.poi_category_jewelry to "shop=jewelry",
|
||||
R.string.poi_category_laundry to "amenity=laundry",
|
||||
R.string.poi_category_bank to "amenity=bank",
|
||||
R.string.poi_category_soccer to "leisure=pitch,sport=soccer",
|
||||
R.string.poi_category_basketball to "leisure=pitch,sport=basketball",
|
||||
R.string.poi_category_tennis to "leisure=pitch,sport=tennis",
|
||||
R.string.poi_category_atm to "amenity=atm",
|
||||
R.string.poi_category_kiosk to "shop=kiosk",
|
||||
R.string.poi_category_museum to "tourism=museum",
|
||||
R.string.poi_category_fitness_center to "leisure=fitness_centre",
|
||||
R.string.poi_category_church to "amenity=place_of_worship,religion=christian",
|
||||
R.string.poi_category_mosque to "amenity=place_of_worship,religion=muslim",
|
||||
R.string.poi_category_buddhist_temple to "amenity=place_of_worship,religion=buddhist",
|
||||
R.string.poi_category_hindu_temple to "amenity=place_of_worship,religion=hindu",
|
||||
R.string.poi_category_synagogue to "amenity=place_of_worship,religion=jewish",
|
||||
R.string.poi_category_pizza_restaurant to "amenity=restaurant,cuisine=pizza",
|
||||
R.string.poi_category_burger_restaurant to "amenity=restaurant,cuisine=burger",
|
||||
R.string.poi_category_place_of_worship to "amenity=place_of_worship",
|
||||
R.string.poi_category_chinese_restaurant to "amenity=restaurant,cuisine=chinese",
|
||||
R.string.poi_category_japanese_restaurant to "amenity=restaurant,cuisine=japanese",
|
||||
R.string.poi_category_kebab_restaurant to "amenity=restaurant,cuisine=kebab",
|
||||
R.string.poi_category_asian_restaurant to "amenity=restaurant,cuisine=asian",
|
||||
R.string.poi_category_ramen_restaurant to "amenity=restaurant,cuisine=ramen",
|
||||
R.string.poi_category_soup_restaurant to "amenity=restaurant,cuisine=soup",
|
||||
R.string.poi_category_brunch_restaurant to "amenity=restaurant,cuisine=brunch",
|
||||
R.string.poi_category_car_wash to "amenity=car_wash",
|
||||
R.string.poi_category_charging_station to "amenity=charging_station",
|
||||
R.string.poi_category_motorcycle_rental to "amenity=motorcycle_rental",
|
||||
R.string.poi_category_gallery to "tourism=gallery",
|
||||
R.string.poi_category_amusement_park to "tourism=theme_park",
|
||||
R.string.poi_category_concert_hall to "amenity=concert_hall",
|
||||
R.string.poi_category_stadium to "leisure=stadium",
|
||||
R.string.poi_category_casino to "amenity=casino",
|
||||
R.string.poi_category_discount_store to "shop=discount",
|
||||
R.string.poi_category_pet to "shop=pet",
|
||||
R.string.poi_category_shopping to "shop=mall",
|
||||
R.string.poi_category_swimming to "leisure=swimming_pool",
|
||||
R.string.poi_category_martial_arts to "leisure=sports_centre,sport=martial_arts",
|
||||
R.string.poi_category_golf to "leisure=golf_course",
|
||||
R.string.poi_category_gymnastics to "leisure=sports_hall,sport=gymnastics",
|
||||
R.string.poi_category_ice_hockey to "leisure=sports_centre,sport=ice_hockey",
|
||||
R.string.poi_category_baseball to "leisure=pitch,sport=baseball",
|
||||
R.string.poi_category_american_football to "leisure=pitch,sport=american_football",
|
||||
R.string.poi_category_handball to "leisure=pitch,sport=handball",
|
||||
R.string.poi_category_volleyball to "leisure=pitch,sport=volleyball",
|
||||
R.string.poi_category_skiing to "leisure=piste",
|
||||
R.string.poi_category_cricket to "leisure=pitch,sport=cricket",
|
||||
R.string.poi_category_park to "leisure=park",
|
||||
R.string.poi_category_monument to "historic=monument",
|
||||
R.string.poi_category_government_building to "building=government",
|
||||
R.string.poi_category_fire_station to "amenity=fire_station",
|
||||
R.string.poi_category_courthouse to "amenity=courthouse",
|
||||
R.string.poi_category_townhall to "amenity=townhall"
|
||||
).mapKeys { context.getString(it.key) }
|
||||
|
||||
private fun delocalizeToQueryableTags(localizedQuery: String): List<String> =
|
||||
poiCategories.mapNotNull { (string, tags) ->
|
||||
val score = ResultScore(
|
||||
localizedQuery,
|
||||
primaryFields = listOf(string)
|
||||
)
|
||||
if (score.score > 0.8f) tags else null
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,13 +9,28 @@ import retrofit2.http.POST
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.math.cos
|
||||
|
||||
/**
|
||||
* Overpass API query builder
|
||||
* Searches for nodes and ways that at least:
|
||||
* - match the query string in their name or brand tag
|
||||
* - match one of the given tag groups
|
||||
*/
|
||||
data class OverpassFuzzyRadiusQuery(
|
||||
val tag: String = "name",
|
||||
/**
|
||||
* Free text query to search for.
|
||||
*/
|
||||
val query: String,
|
||||
/**
|
||||
* Tags groups to search for. Each item represents a group of tags, separated by commas.
|
||||
* The query matches if all tags in a group are present in the element.
|
||||
* For example:
|
||||
* ["amenity=restaurant,cuisine=italian", "amenity=cafe"]
|
||||
* This query will match elements that are either a restaurant with italian cuisine or a cafe.
|
||||
*/
|
||||
val tagGroups: List<String>,
|
||||
val radius: Int,
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val caseInvariant: Boolean = true,
|
||||
)
|
||||
|
||||
data class OverpassIdQuery(
|
||||
@ -50,12 +65,7 @@ interface OverpassApi {
|
||||
|
||||
class OverpassFuzzyRadiusQueryConverter : Converter<OverpassFuzzyRadiusQuery, RequestBody> {
|
||||
override fun convert(value: OverpassFuzzyRadiusQuery): RequestBody {
|
||||
|
||||
// allow other characters in between query words, if there are multiple
|
||||
// https://dev.overpass-api.de/overpass-doc/en/criteria/per_tag.html#regex
|
||||
val escapedQueryName = value
|
||||
.query
|
||||
.split(' ')
|
||||
val encodedQuery = value.query.split(' ')
|
||||
.joinToString(
|
||||
separator = ".*",
|
||||
prefix = "\"",
|
||||
@ -65,11 +75,23 @@ class OverpassFuzzyRadiusQueryConverter : Converter<OverpassFuzzyRadiusQuery, Re
|
||||
val overpassQlBuilder = StringBuilder()
|
||||
val latDegreeChange = value.radius * 0.00001 / 1.11
|
||||
val lonDegreeChange = latDegreeChange / cos(Math.toRadians(value.latitude))
|
||||
val boundingBox = arrayOf(value.latitude - latDegreeChange, value.longitude - lonDegreeChange,
|
||||
value.latitude + latDegreeChange, value.longitude + lonDegreeChange)
|
||||
val boundingBox = arrayOf(
|
||||
value.latitude - latDegreeChange, value.longitude - lonDegreeChange,
|
||||
value.latitude + latDegreeChange, value.longitude + lonDegreeChange
|
||||
)
|
||||
overpassQlBuilder.append("[out:json][timeout:10][bbox:" + boundingBox.joinToString(",") + "];")
|
||||
// nw: node or way
|
||||
overpassQlBuilder.append("nw[", value.tag, "~", escapedQueryName, if (value.caseInvariant) ",i];" else "];")
|
||||
|
||||
overpassQlBuilder.append("(")
|
||||
overpassQlBuilder.append("nw[name~$encodedQuery,i];")
|
||||
overpassQlBuilder.append("nw[brand~$encodedQuery,i];")
|
||||
for (tag in value.tagGroups) {
|
||||
val tags = tag.split(',')
|
||||
|
||||
if (tags.isEmpty()) continue
|
||||
|
||||
overpassQlBuilder.append("nw[${tags.joinToString("][")}];")
|
||||
}
|
||||
overpassQlBuilder.append(");")
|
||||
// center to add the center coordinate of a way to the result, if applicable
|
||||
overpassQlBuilder.append("out center;")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user