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

Sort building type by recently used frequency #3373

Merged
merged 10 commits into from
Oct 21, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import de.westnordost.streetcomplete.view.image_select.GroupableDisplayItem
import de.westnordost.streetcomplete.view.image_select.GroupedImageSelectAdapter
import java.util.LinkedList
import kotlin.math.max
import kotlin.math.min

/**
* Abstract class for quests with a grouped list of images and one to select.
Expand Down Expand Up @@ -92,9 +93,8 @@ abstract class AGroupedImageListQuestAnswerFragment<I,T> : AbstractQuestFormAnsw
}

private fun getInitialItems(): List<GroupableDisplayItem<I>> {
val items = LinkedList(topItems)
favs.moveLastPickedGroupableDisplayItemToFront(javaClass.simpleName, items, allItems)
return items
val validSuggestions = allItems.mapNotNull { it.items }.flatten()
return favs.getWeighted(javaClass.simpleName, 6, 30, topItems, validSuggestions)
}

override fun onClickOk() {
Expand All @@ -115,14 +115,14 @@ abstract class AGroupedImageListQuestAnswerFragment<I,T> : AbstractQuestFormAnsw
.setMessage(R.string.quest_generic_item_confirmation)
.setNegativeButton(R.string.quest_generic_confirmation_no, null)
.setPositiveButton(R.string.quest_generic_confirmation_yes) { _, _ ->
favs.add(javaClass.simpleName, itemValue)
favs.add(javaClass.simpleName, itemValue, allowDuplicates = true)
onClickOk(itemValue)
}
.show()
}
}
else {
favs.add(javaClass.simpleName, itemValue)
favs.add(javaClass.simpleName, itemValue, allowDuplicates = true)
onClickOk(itemValue)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,21 @@ import kotlin.math.min
/** T must be a string or enum - something that distinctly converts toString. */
class LastPickedValuesStore<T> @Inject constructor(private val prefs: SharedPreferences) {

fun add(key: String, newValues: Iterable<T>, max: Int = -1) {
fun add(key: String, newValues: Iterable<T>, max: Int? = null, allowDuplicates: Boolean = false) {
val values = get(key)
for (value in newValues.map { it.toString() }) {
values.remove(value)
values.addFirst(value)
}
val lastValues = if (max != -1) values.subList(0, min(values.size, max)) else values
val unique = if (allowDuplicates) values else values.distinct()
val lastValues = unique.subList(0, min(unique.size, max ?: MAX_ENTRIES))
android.util.Log.d("FAV", "Pref ${key} value: ${lastValues.joinToString(",")}")
prefs.edit {
putString(getKey(key), lastValues.joinToString(","))
}
}

fun add(key: String, value: T, max: Int = -1) {
add(key, listOf(value), max)
fun add(key: String, value: T, max: Int? = null, allowDuplicates: Boolean = false) {
add(key, listOf(value), max, allowDuplicates)
}

fun get(key: String): LinkedList<String> {
Expand All @@ -41,6 +42,44 @@ class LastPickedValuesStore<T> @Inject constructor(private val prefs: SharedPref
private fun getKey(key: String) = Prefs.LAST_PICKED_PREFIX + key
}

private const val MAX_ENTRIES = 100

/* Returns `count` unique items, sorted by how often they appear in the last `historyCount` answers.
* If fewer than `count` unique items are found, look farther back in the history.
* Only returns items in `itemPool` ("valid"), although other answers count towards `historyCount`.
* If there are not enough unique items in the whole history, add unique `defaultItems` as needed.
* Always include the most recent answer, if it is in `itemPool`, but still sorted normally. So, if
* it is not one of the `count` most frequent items, it will replace the last of those.
*
* impl: null represents items not in the item pool
*/
fun <T> LastPickedValuesStore<T>.getWeighted(
key: String,
count: Int,
historyCount: Int,
defaultItems: List<GroupableDisplayItem<T>>,
itemPool: List<GroupableDisplayItem<T>>
): List<GroupableDisplayItem<T>> {
val stringToItem = itemPool.associateBy { it.value.toString() }
val recents = get(key).asSequence().map { stringToItem.get(it) }
val counts = recents.countUniqueNonNull(historyCount, count)
val topRecent = counts.keys.sortedByDescending { counts.get(it) }
val first = recents.take(1).filterNotNull()
val items = (first + topRecent + defaultItems).distinct().take(count)
return items.sortedByDescending { counts.get(it) }.toList()
}

// Counts at least the first `minItems`, keeps going until it finds at least `target` unique values
private fun <T> Sequence<T?>.countUniqueNonNull(minItems: Int, target: Int): Map<T, Int> {
val counts = mutableMapOf<T, Int>()
val items = takeAtLeastWhile(minItems) { counts.size < target }.filterNotNull()
return items.groupingBy { it }.eachCountTo(counts)
}

// Take at least `count` elements, then continue until `predicate` returns false
private fun <T> Sequence<T>.takeAtLeastWhile(count: Int, predicate: (T) -> Boolean): Sequence<T> =
withIndex().takeWhile{ (i, t) -> i < count || predicate(t) }.map { it.value }

fun <T> LastPickedValuesStore<T>.moveLastPickedGroupableDisplayItemToFront(
key: String,
items: LinkedList<GroupableDisplayItem<T>>,
Expand Down