Add OSM search provider (#611)

* add openstreetmaps module in data

* fix injection

* retrofit2 implementation

* tokenization

* partial rewrite of OpeningTime.fromOverpassElement

* finish rewrite of OpeningTime.fromOverpassElement

* fix merge x)

* configurable search radius

* add settings section and disable setting explicit timeout values for http-client to alleviate issues during debugging

* settings screen localization

* enable radius slider only when locations are enabled

* fix dayRange parsing, add barebones UI

* add files to git

* add location listener in SearchableItemVM that gets activated by LocationItem

* add heading listener

* Calculations, UI additions

* rename settings to LocationsSettings

* use android location library for bearing calculations

* location fix

* rotation fix, demo UI

* working buttons for launching map and website (if available)

* finish botched UI

* improve overpass query by utilizing regex for fuzzy search results

* add link to documentation for further reference

* localization comments

* remove wikipedia minification setting

* schema version, default radius 1.5km

* move osm-specific opening-time parsing to OsmLocation

* refactor with callbackFlow

* remember flow and set minimum distance update to 1m

* refactor for replacementIcon, add imperial unit option

* 'open until' UI fix

* implement serializers

* catch errors in deserializer

* hacky live sorting by distance

* give max priority to bestmatch determined by SearchVM

* add yards as additional step to metersToLocalizzedString

* move http-client from serializers of osmlocation to companion object for cache updating

* add setting for custom URL

* round yards to int

* add botched map preview

* unbotch map tiles, draw user location in map (proof of concept)

* - create MapTiles Composable
- add border around map
- add indicators for location and userlocation, when on map

* fix default imperial units setting

* fix tint color

* add OSM attribution string

* display loading animation when tiles can't be shown yet

* create compose preview of maptiles

* UI work

* being glad that API's just return null instead of throwing information

* tryStartActivity

* aniimate card row placement

* Text alignment, padding

* Rotation -PI/PI wrap fix

* fix direction arrow rotation when screen is upside down

* more icons

* icons, settings, localization
- consider other tags than "amenity" when determining location category
- add many more location categories with corresponding icons
- add settings to disable map theming and hide search results with
  LocationCategory.OTHER
- add default localizations for settings

* catch errors when deserializing location category

* move location and heading functions to Context.kt in extensions

* fix hideUncategorized criterion

* add pre-sorting by distance for location results in SearchVM by injecting Context into search()

* specify receiver parameters in ktx.Context lambdas

* move pose logic and context dependency to new module devicepose with DevicePoseProvider

* git, add the frickin' module

* search overpass for nodes and ways
include category for parcel_locker
already start searching for queries extending length 2

* make openingTimes immutable

* OsmRepository changes
- include telephone number
- don't try to repeatedly update cache if there is no value to be
  updated to
- deduplicate results with same label by category and distance (100m)
- include fixmeurl to point to openstreetmaps.org/fixthemap

* ask for center in overpass API to compute center coordinates of ways

* search for brand

* add chemist location category

* restaurant / fastfood icon shenanigans

* actually add the icons :|

* add leisure tag for leisure:fitness_centre

* return to 'open until'/'opens in'/'open next'

* adding missing UI features
- bug report dialog
- call button if phone number exists
- grid item popup

* refactor to handle 24/7 locations more comfortably

* hide hours in 'opens_in' when they are zero

* show maptiles such that user is always in view

* drawing adjustments

* cache previous zoom level to speed up tile coordinate calculations

* using remember

* using MutableIntState

* fix logic that determines whether tiles are loading

* fix for numTiles == 9

* one plus one is two plus one makes three quick maths

* animate user location indicator, remember calculations

* fix off by one when determining next opening hour

* second attempt to fix upside down arrow rotation (probably fine now)

* logging

* reconsider declination, inject samplingPeriod

* undoing the merge undo

* move localization string to i18n

* revert reordering by distance

* refactor .distanceTo

* make Location abstract class to override compareTo with cached distance to correct sort order in search results

* when it is if when you could use when

* replace Pair with dataclass

* condition check order

* not creating objects with undefined locations, removing suspend from getCategory()

* inject permissionsmanager as constructor parameter

* Store OSM settings in decentralized datastore

* Update searchable content in database on launch

* Refactor, add mechanism to load updated searchable data lazily

* Cache all OSM data in launcher database

* Add pin to favorites button to location results

* Add sealed class UpdateResult
that is returned by awaiting updatedSelf of DeferredSearchable

- update on success
- set flag on temporarily unavailable (TODO add some UI indication)
- delete and invalidate VM on permanently unavailable (Display some
  message window to user?)

* Move sorting of Locations from OsmRepository
to SearchVM using cached location in DevicePoseProvider, if available

* make use of cached location in code

* make use of DevicePoseProvider in WeatherRepository

* inject via koin

* increase getLocation().timeout() to 10 minutes since we are asking for locations only every hour, so 10 minutes seem reasonable (?)

* poll new location every time

* add icon for cached results where results are temporarily unavailable

* Refactor DeferredSearchable to UpdatableSearchable
that receives a closure to retrieve an updated self.

- moved timestamp (formerly `updatedAt`) to UpdatableSearchable
- moved logic whether to update searchable to `requestUpdatedSearchable`
  in SearchableItemVM, which gets triggered every time the details of
  the item are shown
- keep track in SearchableVM whether we should retry updating, possibly
  bypassing a timestamp value that is not old enough
- show toast upon permanently unavailable
- animate "cached_searchable" icon
- make "cached_searchable" icon clickable to show toast explaining the
  situation

* logging on PermanentlyUnavailable

* refactor OsmRepository.update()

* MapTheming adjustments. There is now darkmode, hooray!

* remove outdated comment

* code tidying

* remove unnecessary LaunchedEffect

* make outdated badge only clickable when actually outdated

* deserialize opening schedule

* set deserialized props to null if strings are blank

* also consider contact:* tagging scheme for website & phone

* tweaks

* git add Result.kt

* don't search for locations if network is not allowed

* merge fixes

* Move location search settings to preferences module

* Change wording and order of location search preferences

* Limit location search results

* Order location search results by distance

* Use a sequence

* Android Studio's suggestion wasn't as fleshed out as one would hope

* Add proguard rules

* Rename TileMapRepository to MapTileLoader

---------

Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
Christoph
2024-03-26 19:06:19 +01:00
committed by GitHub
parent 4b30b13266
commit cba387c832
133 changed files with 4123 additions and 79 deletions

View File

@@ -0,0 +1,21 @@
package de.mm20.launcher2.coroutines
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
fun <T> deferred(block: suspend () -> T): Deferred<T> {
val deferred = CompletableDeferred<T>()
return object : Deferred<T> by deferred {
private val mutex = Mutex()
override suspend fun await(): T {
mutex.withLock {
if (!deferred.isCompleted) {
block().also { deferred.complete(it) }
}
}
return deferred.await()
}
}
}

View File

@@ -0,0 +1,250 @@
package de.mm20.launcher2.search
import android.content.Context
import androidx.core.content.ContextCompat
import de.mm20.launcher2.base.R
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import kotlinx.collections.immutable.ImmutableList
import java.time.DayOfWeek
import java.time.Duration
import java.time.LocalDate
import java.time.LocalTime
import android.location.Location as AndroidLocation
interface Location : SavableSearchable {
val latitude: Double
val longitude: Double
val fixMeUrl: String?
val category: LocationCategory?
val street: String?
val houseNumber: String?
val openingSchedule: OpeningSchedule?
val websiteUrl: String?
val phoneNumber: String?
override val preferDetailsOverLaunch: Boolean
get() = true
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val (resId, bgColor) = when (category) {
LocationCategory.FAST_FOOD, LocationCategory.RESTAURANT -> with(
labelOverride ?: label
) {
when {
contains(
"pizza",
ignoreCase = true
) -> R.drawable.ic_location_pizza to R.color.red
contains(
"ramen",
ignoreCase = true
) -> R.drawable.ic_location_ramen to R.color.orange
contains(
"tapas",
ignoreCase = true
) -> R.drawable.ic_location_tapas to R.color.orange
contains(
"keba" /* b or p, depending on locale */,
ignoreCase = true
) -> R.drawable.ic_location_kebab to R.color.orange
category == LocationCategory.FAST_FOOD -> R.drawable.ic_location_fastfood to R.color.orange
else -> R.drawable.ic_location_restaurant to R.color.red
}
}
LocationCategory.BAR -> R.drawable.ic_location_bar to R.color.amber
LocationCategory.CAFE, LocationCategory.COFFEE_SHOP -> R.drawable.ic_location_cafe to R.color.brown
LocationCategory.HOTEL -> R.drawable.ic_location_hotel to R.color.green
LocationCategory.SUPERMARKET -> R.drawable.ic_location_supermarket to R.color.lightblue
LocationCategory.SCHOOL -> R.drawable.ic_location_school to R.color.purple
LocationCategory.PARKING -> R.drawable.ic_location_parking to R.color.blue
LocationCategory.FUEL -> R.drawable.ic_location_fuel to R.color.teal
LocationCategory.TOILETS -> R.drawable.ic_location_toilets to R.color.blue
LocationCategory.PHARMACY -> R.drawable.ic_location_pharmacy to R.color.pink
LocationCategory.HOSPITAL, LocationCategory.CLINIC -> R.drawable.ic_location_hospital to R.color.red
LocationCategory.POST_OFFICE -> R.drawable.ic_location_post_office to R.color.yellow
LocationCategory.PUB, LocationCategory.BIERGARTEN -> R.drawable.ic_location_pub to R.color.amber
LocationCategory.GRAVE_YARD -> R.drawable.ic_location_grave_yard to R.color.grey
LocationCategory.DOCTORS -> R.drawable.ic_location_doctors to R.color.red
LocationCategory.POLICE -> R.drawable.ic_location_police to R.color.blue
LocationCategory.DENTIST -> R.drawable.ic_location_dentist to R.color.lightblue
LocationCategory.LIBRARY, LocationCategory.BOOKS -> R.drawable.ic_location_library to R.color.brown
LocationCategory.COLLEGE, LocationCategory.UNIVERSITY -> R.drawable.ic_location_college to R.color.purple
LocationCategory.ICE_CREAM -> R.drawable.ic_location_ice_cream to R.color.pink
LocationCategory.THEATRE -> R.drawable.ic_location_theatre to R.color.purple
LocationCategory.PUBLIC_BUILDING -> R.drawable.ic_location_public_building to R.color.bluegrey
LocationCategory.CINEMA -> R.drawable.ic_location_cinema to R.color.purple
LocationCategory.NIGHTCLUB -> R.drawable.ic_location_nightclub to R.color.purple
LocationCategory.CONVENIENCE -> R.drawable.ic_location_convenience to R.color.lightblue
LocationCategory.CLOTHES -> R.drawable.ic_location_clothes to R.color.pink
LocationCategory.HAIRDRESSER, LocationCategory.BEAUTY -> R.drawable.ic_location_hairdresser to R.color.pink
LocationCategory.CAR_REPAIR -> R.drawable.ic_location_car_repair to R.color.blue
LocationCategory.BAKERY -> R.drawable.ic_location_bakery to R.color.brown
LocationCategory.CAR -> R.drawable.ic_location_car to R.color.blue
LocationCategory.MOBILE_PHONE -> R.drawable.ic_location_mobile_phone to R.color.blue
LocationCategory.FURNITURE -> R.drawable.ic_location_furniture to R.color.brown
LocationCategory.ALCOHOL -> R.drawable.ic_location_alcohol to R.color.amber
LocationCategory.FLORIST -> R.drawable.ic_location_florist to R.color.green
LocationCategory.HARDWARE -> R.drawable.ic_location_hardware to R.color.brown
LocationCategory.ELECTRONICS -> R.drawable.ic_location_electronics to R.color.blue
LocationCategory.SHOES -> R.drawable.ic_location_shoes to R.color.pink
LocationCategory.MALL, LocationCategory.DEPARTMENT_STORE, LocationCategory.CHEMIST -> R.drawable.ic_location_mall to R.color.blue
LocationCategory.OPTICIAN -> R.drawable.ic_location_optician to R.color.blue
LocationCategory.JEWELRY -> R.drawable.ic_location_jewelry to R.color.pink
LocationCategory.GIFT -> R.drawable.ic_location_gift to R.color.pink
LocationCategory.BICYCLE -> R.drawable.ic_location_bicycle to R.color.blue
LocationCategory.LAUNDRY -> R.drawable.ic_location_laundry to R.color.blue
LocationCategory.COMPUTER -> R.drawable.ic_location_computer to R.color.blue
LocationCategory.TOBACCO -> R.drawable.ic_location_tobacco to R.color.amber
LocationCategory.WINE -> R.drawable.ic_location_wine to R.color.amber
LocationCategory.PHOTO -> R.drawable.ic_location_photo to R.color.blue
LocationCategory.BANK -> R.drawable.ic_location_bank to R.color.blue
LocationCategory.SOCCER -> R.drawable.ic_location_soccer to R.color.green
LocationCategory.BASKETBALL -> R.drawable.ic_location_basketball to R.color.orange
LocationCategory.TENNIS -> R.drawable.ic_location_tennis to R.color.orange
LocationCategory.FITNESS, LocationCategory.FITNESS_CENTRE -> R.drawable.ic_location_fitness to R.color.orange
LocationCategory.TRAM_STOP -> R.drawable.ic_location_tram_stop to R.color.blue
LocationCategory.RAILWAY_STOP -> R.drawable.ic_location_railway_stop to R.color.lightblue
LocationCategory.BUS_STATION, LocationCategory.BUS_STOP -> R.drawable.ic_location_bus_station to R.color.blue
LocationCategory.ATM -> R.drawable.ic_location_atm to R.color.green
LocationCategory.ART -> R.drawable.ic_location_art to R.color.deeporange
LocationCategory.KIOSK -> R.drawable.ic_location_kiosk to R.color.bluegrey
LocationCategory.MUSEUM -> R.drawable.ic_location_museum to R.color.deeporange
LocationCategory.PARCEL_LOCKER -> R.drawable.ic_location_parcel_locker to R.color.bluegrey
LocationCategory.TRAVEL_AGENCY -> R.drawable.ic_location_travel_agency to R.color.lightblue
else -> R.drawable.ic_location_place to R.color.bluegrey
}
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, resId)!!,
scale = 0.5f,
color = ContextCompat.getColor(context, bgColor)
),
backgroundLayer = ColorLayer(ContextCompat.getColor(context, bgColor))
)
}
fun toAndroidLocation(): AndroidLocation {
val location = AndroidLocation("KvaesitsoLocationProvider")
location.latitude = latitude
location.longitude = longitude
return location
}
fun distanceTo(androidLocation: AndroidLocation): Float {
return androidLocation.distanceTo(this.toAndroidLocation())
}
fun distanceTo(otherLocation: Location): Float =
this.distanceTo(otherLocation.toAndroidLocation())
}
// https://taginfo.openstreetmap.org/tags
// 'amenity', 'shop', 'sport' of which the most important
enum class LocationCategory {
RESTAURANT,
FAST_FOOD,
BAR,
CAFE,
HOTEL,
SUPERMARKET,
OTHER,
SCHOOL,
PARKING,
FUEL,
TOILETS,
PHARMACY,
HOSPITAL,
POST_OFFICE,
PUB,
GRAVE_YARD,
DOCTORS,
POLICE,
DENTIST,
LIBRARY,
COLLEGE,
ICE_CREAM,
THEATRE,
PUBLIC_BUILDING,
CINEMA,
NIGHTCLUB,
BIERGARTEN,
CLINIC,
UNIVERSITY,
DEPARTMENT_STORE,
CLOTHES,
CONVENIENCE,
HAIRDRESSER,
CAR_REPAIR,
BEAUTY,
BOOKS,
BAKERY,
CAR,
MOBILE_PHONE,
FURNITURE,
ALCOHOL,
FLORIST,
HARDWARE,
ELECTRONICS,
SHOES,
MALL,
OPTICIAN,
JEWELRY,
GIFT,
BICYCLE,
LAUNDRY,
COMPUTER,
TOBACCO,
WINE,
PHOTO,
COFFEE_SHOP,
BANK,
SOCCER,
BASKETBALL,
TENNIS,
FITNESS,
TRAM_STOP,
RAILWAY_STOP,
BUS_STATION,
ATM,
ART,
KIOSK,
BUS_STOP,
MUSEUM,
PARCEL_LOCKER,
CHEMIST,
TRAVEL_AGENCY,
FITNESS_CENTRE
}
data class OpeningHours(
val dayOfWeek: DayOfWeek,
val startTime: LocalTime,
val duration: Duration
) {
val isOpen: Boolean
get() = LocalDate.now().dayOfWeek == dayOfWeek &&
LocalTime.now().isAfter(startTime) &&
LocalTime.now().isBefore(startTime.plus(duration))
override fun toString(): String = "$dayOfWeek $startTime-${startTime.plus(duration)}"
}
data class OpeningSchedule(
val isTwentyFourSeven: Boolean,
val openingHours: ImmutableList<OpeningHours>
) {
val isOpen: Boolean
get() = isTwentyFourSeven || openingHours.any { it.isOpen }
}

View File

@@ -1,3 +1,5 @@
package de.mm20.launcher2.search
import kotlinx.coroutines.Deferred
interface Searchable

View File

@@ -0,0 +1,18 @@
package de.mm20.launcher2.search
/**
* Interface that can be implemented by [SavableSearchable]s to provide a way to update itself.
* Consumers of [SavableSearchable]s can check if the [SavableSearchable] implements this interface
* and decide to get an updated version of the [SavableSearchable] by calling [updatedSelf], which
* returns an [UpdateResult] that contains either an up-to-date value or specifies unavailability.
*/
interface UpdatableSearchable<T : SavableSearchable> {
val timestamp: Long
val updatedSelf: (suspend () -> UpdateResult<T>)?
}
sealed class UpdateResult<out T> {
data class Success<out T>(val result: T) : UpdateResult<T>()
data class TemporarilyUnavailable<T>(val cause: Throwable? = null) : UpdateResult<T>()
data class PermanentlyUnavailable<T>(val cause: Throwable? = null) : UpdateResult<T>()
}