Migrate locations module to Ktor
This commit is contained in:
@@ -46,8 +46,7 @@ dependencies {
|
||||
|
||||
implementation(libs.bundles.androidx.lifecycle)
|
||||
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.bundles.retrofit)
|
||||
implementation(libs.bundles.ktor)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.mm20.launcher2.locations.providers.openstreetmaps
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.locations.providers.AndroidLocation
|
||||
import de.mm20.launcher2.locations.providers.LocationProvider
|
||||
@@ -11,89 +10,42 @@ import de.mm20.launcher2.search.Location
|
||||
import de.mm20.launcher2.search.ResultScore
|
||||
import de.mm20.launcher2.search.UpdateResult
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.net.UnknownHostException
|
||||
|
||||
private val Scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
private val HttpClient = OkHttpClient()
|
||||
|
||||
internal class OsmLocationProvider(
|
||||
private val context: Context,
|
||||
settings: LocationSearchSettings
|
||||
private val settings: LocationSearchSettings
|
||||
) : LocationProvider<Long> {
|
||||
|
||||
private val overpassApi = settings.overpassUrl.map {
|
||||
try {
|
||||
Retrofit.Builder()
|
||||
.client(HttpClient)
|
||||
.baseUrl(it?.takeIf { it.isNotBlank() }
|
||||
?: LocationSearchSettings.DefaultOverpassUrl)
|
||||
.addConverterFactory(OverpassQueryConverterFactory())
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(OverpassApi::class.java)
|
||||
} catch (e: Exception) {
|
||||
CrashReporter.logException(e)
|
||||
null
|
||||
}
|
||||
}.stateIn(Scope, SharingStarted.Eagerly, null)
|
||||
private val overpassApi = OverpassApi()
|
||||
|
||||
|
||||
suspend fun update(
|
||||
id: Long
|
||||
): UpdateResult<Location> = overpassApi.first()?.runCatching {
|
||||
this.search(
|
||||
OverpassIdQuery(
|
||||
id = id
|
||||
): UpdateResult<Location> {
|
||||
|
||||
val baseUrl = settings.overpassUrl.first()?.takeIf { it.isNotBlank() }
|
||||
?: LocationSearchSettings.DefaultOverpassUrl
|
||||
|
||||
val response = try {
|
||||
overpassApi.interpreter(
|
||||
baseUrl,
|
||||
OverpassIdQuery(
|
||||
id = id
|
||||
),
|
||||
)
|
||||
).let {
|
||||
OsmLocation.fromOverpassResponse(it, context)
|
||||
}.first().apply {
|
||||
} catch (e: Exception) {
|
||||
CrashReporter.logException(e)
|
||||
return UpdateResult.TemporarilyUnavailable(e)
|
||||
}
|
||||
|
||||
val locations = OsmLocation.fromOverpassResponse(response, context)
|
||||
|
||||
if (locations.isEmpty()) return UpdateResult.PermanentlyUnavailable()
|
||||
|
||||
return UpdateResult.Success(locations.first().apply {
|
||||
updatedSelf = { update(id) }
|
||||
}
|
||||
}?.fold(
|
||||
onSuccess = { UpdateResult.Success(it) },
|
||||
onFailure = {
|
||||
when (it) {
|
||||
is CancellationException, is UnknownHostException -> {
|
||||
// network
|
||||
UpdateResult.TemporarilyUnavailable(it)
|
||||
}
|
||||
|
||||
is HttpException -> when (it.code()) {
|
||||
in 400..499 -> UpdateResult.PermanentlyUnavailable(it)
|
||||
else -> UpdateResult.TemporarilyUnavailable(it)
|
||||
}
|
||||
|
||||
is NoSuchElementException -> {
|
||||
// empty response
|
||||
UpdateResult.PermanentlyUnavailable(it)
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (it is Exception) {
|
||||
CrashReporter.logException(it)
|
||||
}
|
||||
UpdateResult.TemporarilyUnavailable(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
) ?: let {
|
||||
Log.e("OsmProvider", "overpassApi was not initialized")
|
||||
UpdateResult.TemporarilyUnavailable()
|
||||
})
|
||||
}
|
||||
|
||||
override suspend fun search(
|
||||
@@ -107,12 +59,12 @@ internal class OsmLocationProvider(
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
HttpClient.dispatcher.cancelAll()
|
||||
}
|
||||
val baseUrl = settings.overpassUrl.first()?.takeIf { it.isNotBlank() }
|
||||
?: LocationSearchSettings.DefaultOverpassUrl
|
||||
|
||||
return overpassApi.first()?.runCatching {
|
||||
search(
|
||||
val response = try {
|
||||
overpassApi.interpreter(
|
||||
baseUrl,
|
||||
OverpassFuzzyRadiusQuery(
|
||||
query = query,
|
||||
tagGroups = delocalizeToQueryableTags(query),
|
||||
@@ -121,22 +73,24 @@ internal class OsmLocationProvider(
|
||||
longitude = userLocation.longitude
|
||||
)
|
||||
)
|
||||
}?.onFailure {
|
||||
if (it !is HttpException && it !is CancellationException) {
|
||||
Log.e("OsmLocationProvider", "Failed to search for: $query", it)
|
||||
}
|
||||
}?.getOrNull()?.let {
|
||||
OsmLocation.fromOverpassResponse(it, context)
|
||||
}?.asSequence()?.filter {
|
||||
} catch (e: Exception) {
|
||||
CrashReporter.logException(e)
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val locations = OsmLocation.fromOverpassResponse(response, context)
|
||||
|
||||
return locations.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
|
||||
if (duplicates.size < 2) duplicates
|
||||
else {
|
||||
if (duplicates.size < 2) {
|
||||
duplicates
|
||||
} else {
|
||||
val luckyFirst = duplicates.first()
|
||||
duplicates
|
||||
.drop(1)
|
||||
@@ -145,105 +99,117 @@ internal class OsmLocationProvider(
|
||||
it.distanceTo(luckyFirst) > 100.0
|
||||
} + luckyFirst
|
||||
}
|
||||
}?.sortedBy {
|
||||
}.sortedBy {
|
||||
it.distanceTo(userLocation)
|
||||
}?.take(9)?.toImmutableList() ?: emptyList()
|
||||
}.take(9)
|
||||
}
|
||||
|
||||
private val poiCategories = mapOf(
|
||||
R.string.poi_category_restaurant to listOf("amenity=restaurant"),
|
||||
R.string.poi_category_fast_food to listOf("amenity=fast_food"),
|
||||
R.string.poi_category_bar to listOf("amenity=bar"),
|
||||
R.string.poi_category_cafe to listOf("amenity=cafe"),
|
||||
R.string.poi_category_hotel to listOf("tourism=hotel"),
|
||||
R.string.poi_category_supermarket to listOf("shop=supermarket"),
|
||||
R.string.poi_category_school to listOf("amenity=school"),
|
||||
R.string.poi_category_parking to listOf("amenity=parking"),
|
||||
R.string.poi_category_fuel to listOf("amenity=fuel"),
|
||||
R.string.poi_category_toilets to listOf("amenity=toilets"),
|
||||
R.string.poi_category_pharmacy to listOf("amenity=pharmacy"),
|
||||
R.string.poi_category_hospital to listOf("amenity=hospital"),
|
||||
R.string.poi_category_post_office to listOf("amenity=post_office"),
|
||||
R.string.poi_category_pub to listOf("amenity=pub"),
|
||||
R.string.poi_category_doctors to listOf("amenity=doctors"),
|
||||
R.string.poi_category_police to listOf("amenity=police"),
|
||||
R.string.poi_category_dentist to listOf("amenity=dentist"),
|
||||
R.string.poi_category_library to listOf("amenity=library"),
|
||||
R.string.poi_category_ice_cream to listOf("amenity=ice_cream"),
|
||||
R.string.poi_category_theater to listOf("amenity=theatre"),
|
||||
R.string.poi_category_cinema to listOf("amenity=cinema"),
|
||||
R.string.poi_category_nightclub to listOf("amenity=nightclub"),
|
||||
R.string.poi_category_clinic to listOf("amenity=clinic"),
|
||||
R.string.poi_category_university to listOf("amenity=university"),
|
||||
R.string.poi_category_clothes to listOf("shop=clothes"),
|
||||
R.string.poi_category_convenience to listOf("shop=convenience"),
|
||||
R.string.poi_category_hairdresser to listOf("shop=hairdresser"),
|
||||
R.string.poi_category_books to listOf("shop=books"),
|
||||
R.string.poi_category_bakery to listOf("shop=bakery"),
|
||||
R.string.poi_category_car_rental to listOf("amenity=car_rental"),
|
||||
R.string.poi_category_car_sharing to listOf("amenity=car_sharing"),
|
||||
R.string.poi_category_mobile_phone to listOf("shop=mobile_phone"),
|
||||
R.string.poi_category_furniture to listOf("shop=furniture"),
|
||||
R.string.poi_category_alcohol to listOf("shop=alcohol"),
|
||||
R.string.poi_category_florist to listOf("shop=florist"),
|
||||
R.string.poi_category_mall to listOf("shop=mall"),
|
||||
R.string.poi_category_optician to listOf("shop=optician"),
|
||||
R.string.poi_category_jewelry to listOf("shop=jewelry"),
|
||||
R.string.poi_category_laundry to listOf("amenity=laundry"),
|
||||
R.string.poi_category_bank to listOf("amenity=bank"),
|
||||
R.string.poi_category_soccer to listOf("leisure=pitch,sport=soccer"),
|
||||
R.string.poi_category_basketball to listOf("leisure=pitch,sport=basketball"),
|
||||
R.string.poi_category_tennis to listOf("leisure=pitch,sport=tennis"),
|
||||
R.string.poi_category_atm to listOf("amenity=atm"),
|
||||
R.string.poi_category_kiosk to listOf("shop=kiosk"),
|
||||
R.string.poi_category_museum to listOf("tourism=museum"),
|
||||
R.string.poi_category_fitness_center to listOf("leisure=fitness_centre"),
|
||||
R.string.poi_category_church to listOf("amenity=place_of_worship,religion=christian"),
|
||||
R.string.poi_category_mosque to listOf("amenity=place_of_worship,religion=muslim"),
|
||||
R.string.poi_category_buddhist_temple to listOf("amenity=place_of_worship,religion=buddhist"),
|
||||
R.string.poi_category_hindu_temple to listOf("amenity=place_of_worship,religion=hindu"),
|
||||
R.string.poi_category_synagogue to listOf("amenity=place_of_worship,religion=jewish"),
|
||||
R.string.poi_category_pizza_restaurant to listOf("amenity=restaurant,cuisine=pizza"),
|
||||
R.string.poi_category_burger_restaurant to listOf("amenity=restaurant,cuisine=burger"),
|
||||
R.string.poi_category_place_of_worship to listOf("amenity=place_of_worship"),
|
||||
R.string.poi_category_chinese_restaurant to listOf("amenity=restaurant,cuisine=chinese"),
|
||||
R.string.poi_category_japanese_restaurant to listOf("amenity=restaurant,cuisine=japanese"),
|
||||
R.string.poi_category_kebab_restaurant to listOf("amenity=restaurant,cuisine=kebab"),
|
||||
R.string.poi_category_asian_restaurant to listOf("amenity=restaurant,cuisine=asian"),
|
||||
R.string.poi_category_ramen_restaurant to listOf("amenity=restaurant,cuisine=ramen"),
|
||||
R.string.poi_category_soup_restaurant to listOf("amenity=restaurant,cuisine=soup"),
|
||||
R.string.poi_category_brunch_restaurant to listOf("amenity=restaurant,cuisine=brunch"),
|
||||
R.string.poi_category_car_wash to listOf("amenity=car_wash"),
|
||||
R.string.poi_category_charging_station to listOf("amenity=charging_station"),
|
||||
R.string.poi_category_motorcycle_rental to listOf("amenity=motorcycle_rental"),
|
||||
R.string.poi_category_gallery to listOf("tourism=gallery"),
|
||||
R.string.poi_category_amusement_park to listOf("tourism=theme_park"),
|
||||
R.string.poi_category_concert_hall to listOf("amenity=concert_hall"),
|
||||
R.string.poi_category_stadium to listOf("leisure=stadium"),
|
||||
R.string.poi_category_casino to listOf("amenity=casino"),
|
||||
R.string.poi_category_discount_store to listOf("shop=discount"),
|
||||
R.string.poi_category_pet to listOf("shop=pet"),
|
||||
R.string.poi_category_shopping to listOf("shop=mall"),
|
||||
R.string.poi_category_swimming to listOf("leisure=swimming_pool"),
|
||||
R.string.poi_category_martial_arts to listOf("leisure=sports_centre", "leisure=sports_hall").map { "$it,sport=martial_arts" },
|
||||
R.string.poi_category_golf to listOf("leisure=golf_course"),
|
||||
R.string.poi_category_gymnastics to listOf("leisure=sports_hall", "leisure=sports_centre").map { "$it,sport=gymnastics" },
|
||||
R.string.poi_category_ice_hockey to listOf("leisure=sports_hall", "leisure=sports_centre").map { "$it,sport=ice_hockey" },
|
||||
R.string.poi_category_baseball to listOf("leisure=pitch,sport=baseball"),
|
||||
R.string.poi_category_american_football to listOf("leisure=pitch,sport=american_football"),
|
||||
R.string.poi_category_handball to listOf("leisure=pitch,sport=handball"),
|
||||
R.string.poi_category_volleyball to listOf("leisure=pitch,sport=volleyball"),
|
||||
R.string.poi_category_skiing to listOf("leisure=piste"),
|
||||
R.string.poi_category_cricket to listOf("leisure=pitch,sport=cricket"),
|
||||
R.string.poi_category_park to listOf("leisure=park"),
|
||||
R.string.poi_category_monument to listOf("historic=monument"),
|
||||
R.string.poi_category_government_building to listOf("building=government"),
|
||||
R.string.poi_category_fire_station to listOf("amenity=fire_station"),
|
||||
R.string.poi_category_courthouse to listOf("amenity=courthouse"),
|
||||
R.string.poi_category_townhall to listOf("amenity=townhall"),
|
||||
R.string.poi_category_stationery to listOf("shop=stationery"),
|
||||
R.string.poi_category_climbing_gym to listOf("leisure=sports_hall", "leisure=sports_centre").map { "$it,sport=climbing" },
|
||||
R.string.poi_category_hackerspace to listOf("leisure=hackerspace")
|
||||
R.string.poi_category_restaurant to listOf("amenity=restaurant"),
|
||||
R.string.poi_category_fast_food to listOf("amenity=fast_food"),
|
||||
R.string.poi_category_bar to listOf("amenity=bar"),
|
||||
R.string.poi_category_cafe to listOf("amenity=cafe"),
|
||||
R.string.poi_category_hotel to listOf("tourism=hotel"),
|
||||
R.string.poi_category_supermarket to listOf("shop=supermarket"),
|
||||
R.string.poi_category_school to listOf("amenity=school"),
|
||||
R.string.poi_category_parking to listOf("amenity=parking"),
|
||||
R.string.poi_category_fuel to listOf("amenity=fuel"),
|
||||
R.string.poi_category_toilets to listOf("amenity=toilets"),
|
||||
R.string.poi_category_pharmacy to listOf("amenity=pharmacy"),
|
||||
R.string.poi_category_hospital to listOf("amenity=hospital"),
|
||||
R.string.poi_category_post_office to listOf("amenity=post_office"),
|
||||
R.string.poi_category_pub to listOf("amenity=pub"),
|
||||
R.string.poi_category_doctors to listOf("amenity=doctors"),
|
||||
R.string.poi_category_police to listOf("amenity=police"),
|
||||
R.string.poi_category_dentist to listOf("amenity=dentist"),
|
||||
R.string.poi_category_library to listOf("amenity=library"),
|
||||
R.string.poi_category_ice_cream to listOf("amenity=ice_cream"),
|
||||
R.string.poi_category_theater to listOf("amenity=theatre"),
|
||||
R.string.poi_category_cinema to listOf("amenity=cinema"),
|
||||
R.string.poi_category_nightclub to listOf("amenity=nightclub"),
|
||||
R.string.poi_category_clinic to listOf("amenity=clinic"),
|
||||
R.string.poi_category_university to listOf("amenity=university"),
|
||||
R.string.poi_category_clothes to listOf("shop=clothes"),
|
||||
R.string.poi_category_convenience to listOf("shop=convenience"),
|
||||
R.string.poi_category_hairdresser to listOf("shop=hairdresser"),
|
||||
R.string.poi_category_books to listOf("shop=books"),
|
||||
R.string.poi_category_bakery to listOf("shop=bakery"),
|
||||
R.string.poi_category_car_rental to listOf("amenity=car_rental"),
|
||||
R.string.poi_category_car_sharing to listOf("amenity=car_sharing"),
|
||||
R.string.poi_category_mobile_phone to listOf("shop=mobile_phone"),
|
||||
R.string.poi_category_furniture to listOf("shop=furniture"),
|
||||
R.string.poi_category_alcohol to listOf("shop=alcohol"),
|
||||
R.string.poi_category_florist to listOf("shop=florist"),
|
||||
R.string.poi_category_mall to listOf("shop=mall"),
|
||||
R.string.poi_category_optician to listOf("shop=optician"),
|
||||
R.string.poi_category_jewelry to listOf("shop=jewelry"),
|
||||
R.string.poi_category_laundry to listOf("amenity=laundry"),
|
||||
R.string.poi_category_bank to listOf("amenity=bank"),
|
||||
R.string.poi_category_soccer to listOf("leisure=pitch,sport=soccer"),
|
||||
R.string.poi_category_basketball to listOf("leisure=pitch,sport=basketball"),
|
||||
R.string.poi_category_tennis to listOf("leisure=pitch,sport=tennis"),
|
||||
R.string.poi_category_atm to listOf("amenity=atm"),
|
||||
R.string.poi_category_kiosk to listOf("shop=kiosk"),
|
||||
R.string.poi_category_museum to listOf("tourism=museum"),
|
||||
R.string.poi_category_fitness_center to listOf("leisure=fitness_centre"),
|
||||
R.string.poi_category_church to listOf("amenity=place_of_worship,religion=christian"),
|
||||
R.string.poi_category_mosque to listOf("amenity=place_of_worship,religion=muslim"),
|
||||
R.string.poi_category_buddhist_temple to listOf("amenity=place_of_worship,religion=buddhist"),
|
||||
R.string.poi_category_hindu_temple to listOf("amenity=place_of_worship,religion=hindu"),
|
||||
R.string.poi_category_synagogue to listOf("amenity=place_of_worship,religion=jewish"),
|
||||
R.string.poi_category_pizza_restaurant to listOf("amenity=restaurant,cuisine=pizza"),
|
||||
R.string.poi_category_burger_restaurant to listOf("amenity=restaurant,cuisine=burger"),
|
||||
R.string.poi_category_place_of_worship to listOf("amenity=place_of_worship"),
|
||||
R.string.poi_category_chinese_restaurant to listOf("amenity=restaurant,cuisine=chinese"),
|
||||
R.string.poi_category_japanese_restaurant to listOf("amenity=restaurant,cuisine=japanese"),
|
||||
R.string.poi_category_kebab_restaurant to listOf("amenity=restaurant,cuisine=kebab"),
|
||||
R.string.poi_category_asian_restaurant to listOf("amenity=restaurant,cuisine=asian"),
|
||||
R.string.poi_category_ramen_restaurant to listOf("amenity=restaurant,cuisine=ramen"),
|
||||
R.string.poi_category_soup_restaurant to listOf("amenity=restaurant,cuisine=soup"),
|
||||
R.string.poi_category_brunch_restaurant to listOf("amenity=restaurant,cuisine=brunch"),
|
||||
R.string.poi_category_car_wash to listOf("amenity=car_wash"),
|
||||
R.string.poi_category_charging_station to listOf("amenity=charging_station"),
|
||||
R.string.poi_category_motorcycle_rental to listOf("amenity=motorcycle_rental"),
|
||||
R.string.poi_category_gallery to listOf("tourism=gallery"),
|
||||
R.string.poi_category_amusement_park to listOf("tourism=theme_park"),
|
||||
R.string.poi_category_concert_hall to listOf("amenity=concert_hall"),
|
||||
R.string.poi_category_stadium to listOf("leisure=stadium"),
|
||||
R.string.poi_category_casino to listOf("amenity=casino"),
|
||||
R.string.poi_category_discount_store to listOf("shop=discount"),
|
||||
R.string.poi_category_pet to listOf("shop=pet"),
|
||||
R.string.poi_category_shopping to listOf("shop=mall"),
|
||||
R.string.poi_category_swimming to listOf("leisure=swimming_pool"),
|
||||
R.string.poi_category_martial_arts to listOf(
|
||||
"leisure=sports_centre",
|
||||
"leisure=sports_hall"
|
||||
).map { "$it,sport=martial_arts" },
|
||||
R.string.poi_category_golf to listOf("leisure=golf_course"),
|
||||
R.string.poi_category_gymnastics to listOf(
|
||||
"leisure=sports_hall",
|
||||
"leisure=sports_centre"
|
||||
).map { "$it,sport=gymnastics" },
|
||||
R.string.poi_category_ice_hockey to listOf(
|
||||
"leisure=sports_hall",
|
||||
"leisure=sports_centre"
|
||||
).map { "$it,sport=ice_hockey" },
|
||||
R.string.poi_category_baseball to listOf("leisure=pitch,sport=baseball"),
|
||||
R.string.poi_category_american_football to listOf("leisure=pitch,sport=american_football"),
|
||||
R.string.poi_category_handball to listOf("leisure=pitch,sport=handball"),
|
||||
R.string.poi_category_volleyball to listOf("leisure=pitch,sport=volleyball"),
|
||||
R.string.poi_category_skiing to listOf("leisure=piste"),
|
||||
R.string.poi_category_cricket to listOf("leisure=pitch,sport=cricket"),
|
||||
R.string.poi_category_park to listOf("leisure=park"),
|
||||
R.string.poi_category_monument to listOf("historic=monument"),
|
||||
R.string.poi_category_government_building to listOf("building=government"),
|
||||
R.string.poi_category_fire_station to listOf("amenity=fire_station"),
|
||||
R.string.poi_category_courthouse to listOf("amenity=courthouse"),
|
||||
R.string.poi_category_townhall to listOf("amenity=townhall"),
|
||||
R.string.poi_category_stationery to listOf("shop=stationery"),
|
||||
R.string.poi_category_climbing_gym to listOf(
|
||||
"leisure=sports_hall",
|
||||
"leisure=sports_centre"
|
||||
).map { "$it,sport=climbing" },
|
||||
R.string.poi_category_hackerspace to listOf("leisure=hackerspace")
|
||||
).mapKeys { context.getString(it.key) }
|
||||
|
||||
private fun delocalizeToQueryableTags(localizedQuery: String): List<String> =
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
package de.mm20.launcher2.locations.providers.openstreetmaps
|
||||
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import java.lang.reflect.Type
|
||||
import de.mm20.launcher2.serialization.Json
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.path
|
||||
import io.ktor.http.takeFrom
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.math.cos
|
||||
|
||||
internal sealed interface OverpassQuery {
|
||||
fun toQueryString(): String
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
internal data class OverpassFuzzyRadiusQuery(
|
||||
/**
|
||||
* Free text query to search for.
|
||||
*/
|
||||
@@ -31,41 +38,9 @@ data class OverpassFuzzyRadiusQuery(
|
||||
val radius: Int,
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
)
|
||||
|
||||
data class OverpassIdQuery(
|
||||
val id: Long,
|
||||
)
|
||||
|
||||
data class OverpassResponse(
|
||||
val elements: List<OverpassResponseElement>,
|
||||
)
|
||||
|
||||
data class OverpassResponseElementCenter(
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
)
|
||||
|
||||
data class OverpassResponseElement(
|
||||
val type: String,
|
||||
val id: Long,
|
||||
val lat: Double?,
|
||||
val lon: Double?,
|
||||
val center: OverpassResponseElementCenter?,
|
||||
val tags: Map<String, String>?,
|
||||
)
|
||||
|
||||
interface OverpassApi {
|
||||
@POST("api/interpreter")
|
||||
suspend fun search(@Body data: OverpassFuzzyRadiusQuery): OverpassResponse
|
||||
|
||||
@POST("api/interpreter")
|
||||
suspend fun search(@Body data: OverpassIdQuery): OverpassResponse
|
||||
}
|
||||
|
||||
class OverpassFuzzyRadiusQueryConverter : Converter<OverpassFuzzyRadiusQuery, RequestBody> {
|
||||
override fun convert(value: OverpassFuzzyRadiusQuery): RequestBody {
|
||||
val encodedQuery = value.query.split(' ')
|
||||
) : OverpassQuery {
|
||||
override fun toQueryString(): String {
|
||||
val encodedQuery = query.split(' ')
|
||||
.joinToString(
|
||||
separator = ".*",
|
||||
prefix = "\"",
|
||||
@@ -73,18 +48,18 @@ class OverpassFuzzyRadiusQueryConverter : Converter<OverpassFuzzyRadiusQuery, Re
|
||||
) { Regex.escapeReplacement(it) }
|
||||
|
||||
val overpassQlBuilder = StringBuilder()
|
||||
val latDegreeChange = value.radius * 0.00001 / 1.11
|
||||
val lonDegreeChange = latDegreeChange / cos(Math.toRadians(value.latitude))
|
||||
val latDegreeChange = radius * 0.00001 / 1.11
|
||||
val lonDegreeChange = latDegreeChange / cos(Math.toRadians(latitude))
|
||||
val boundingBox = arrayOf(
|
||||
value.latitude - latDegreeChange, value.longitude - lonDegreeChange,
|
||||
value.latitude + latDegreeChange, value.longitude + lonDegreeChange
|
||||
latitude - latDegreeChange, longitude - lonDegreeChange,
|
||||
latitude + latDegreeChange, longitude + lonDegreeChange
|
||||
)
|
||||
overpassQlBuilder.append("[out:json][timeout:10][bbox:" + boundingBox.joinToString(",") + "];")
|
||||
|
||||
overpassQlBuilder.append("(")
|
||||
overpassQlBuilder.append("nw[name~$encodedQuery,i];")
|
||||
overpassQlBuilder.append("nw[brand~$encodedQuery,i];")
|
||||
for (tag in value.tagGroups) {
|
||||
for (tag in tagGroups) {
|
||||
val tags = tag.split(',')
|
||||
|
||||
if (tags.isEmpty()) continue
|
||||
@@ -95,32 +70,67 @@ class OverpassFuzzyRadiusQueryConverter : Converter<OverpassFuzzyRadiusQuery, Re
|
||||
// center to add the center coordinate of a way to the result, if applicable
|
||||
overpassQlBuilder.append("out center;")
|
||||
|
||||
return overpassQlBuilder.toString().toRequestBody()
|
||||
return overpassQlBuilder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
class OverpassIdQueryConverter : Converter<OverpassIdQuery, RequestBody> {
|
||||
override fun convert(value: OverpassIdQuery): RequestBody = """
|
||||
[out:json];
|
||||
nw(${value.id});
|
||||
out center;
|
||||
""".trimIndent().toRequestBody()
|
||||
}
|
||||
|
||||
class OverpassQueryConverterFactory : Converter.Factory() {
|
||||
override fun requestBodyConverter(
|
||||
type: Type,
|
||||
parameterAnnotations: Array<out Annotation>,
|
||||
methodAnnotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
): Converter<*, RequestBody>? {
|
||||
if (type == OverpassFuzzyRadiusQuery::class.java)
|
||||
return OverpassFuzzyRadiusQueryConverter()
|
||||
|
||||
if (type == OverpassIdQuery::class.java)
|
||||
return OverpassIdQueryConverter()
|
||||
|
||||
return null
|
||||
internal data class OverpassIdQuery(
|
||||
val id: Long,
|
||||
) : OverpassQuery {
|
||||
override fun toQueryString(): String {
|
||||
return """
|
||||
[out:json];
|
||||
nw($id);
|
||||
out center;
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class OverpassResponse(
|
||||
val elements: List<OverpassResponseElement>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class OverpassResponseElementCenter(
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class OverpassResponseElement(
|
||||
val type: String,
|
||||
val id: Long,
|
||||
val lat: Double?,
|
||||
val lon: Double?,
|
||||
val center: OverpassResponseElementCenter?,
|
||||
val tags: Map<String, String>?,
|
||||
)
|
||||
|
||||
internal class OverpassApi {
|
||||
|
||||
private val httpClient by lazy {
|
||||
HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json.Lenient)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun interpreter(
|
||||
baseUrl: String,
|
||||
query: OverpassQuery,
|
||||
): OverpassResponse {
|
||||
return httpClient.post {
|
||||
url {
|
||||
takeFrom(baseUrl)
|
||||
if (pathSegments.isEmpty()) {
|
||||
path("api", "interpreter")
|
||||
}
|
||||
}
|
||||
setBody(query.toQueryString())
|
||||
}.body()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user