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:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
250
core/base/src/main/java/de/mm20/launcher2/search/Location.kt
Normal file
250
core/base/src/main/java/de/mm20/launcher2/search/Location.kt
Normal 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 }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
||||
interface Searchable
|
||||
@@ -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>()
|
||||
}
|
||||
Reference in New Issue
Block a user