Skip to content

Commit

Permalink
Replay edits done during quests download (#5473)
Browse files Browse the repository at this point in the history
MapDataWithEditsSource: call onUpdated to listeners again after the call onReplacedForBBox to listeners completed for updates that happened while the latter was being executed (fixes #4258)
  • Loading branch information
westnordost authored Feb 5, 2024
1 parent dedc68c commit e2621c7
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import de.westnordost.streetcomplete.data.osm.mapdata.MapDataController
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataRepository
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataUpdates
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometry
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataWithGeometryUpdates
import de.westnordost.streetcomplete.data.osm.mapdata.MutableMapData
import de.westnordost.streetcomplete.data.osm.mapdata.MutableMapDataWithGeometry
import de.westnordost.streetcomplete.data.osm.mapdata.Node
Expand Down Expand Up @@ -61,6 +62,15 @@ class MapDataWithEditsSource internal constructor(
private val updatedElements = HashMap<ElementKey, Element>()
private val updatedGeometries = HashMap<ElementKey, ElementGeometry?>()

// onReplacedForBBox may not be called in parallel
private val onReplacedForBBoxLock = Any()

// access to isReplacingForBBox is atomic (didn't want to pull in kotlinx-atomicfu dependency just for this)
private val isReplacingForBBoxLock = Any()
private var isReplacingForBBox: Boolean = false

private val updatesWhileReplacingBBox = MapDataWithGeometryUpdates()

private val mapDataListener = object : MapDataController.Listener {

override fun onUpdated(updated: MutableMapDataWithGeometry, deleted: Collection<ElementKey>) {
Expand Down Expand Up @@ -136,12 +146,21 @@ class MapDataWithEditsSource internal constructor(
}

override fun onReplacedForBBox(bbox: BoundingBox, mapDataWithGeometry: MutableMapDataWithGeometry) {
synchronized(this) {
rebuildLocalChanges()
modifyBBoxMapData(bbox, mapDataWithGeometry)
}
synchronized(onReplacedForBBoxLock) {
synchronized(isReplacingForBBoxLock) { isReplacingForBBox = true }

synchronized(this) {
rebuildLocalChanges()
modifyBBoxMapData(bbox, mapDataWithGeometry)
}

callOnReplacedForBBox(bbox, mapDataWithGeometry)

callOnReplacedForBBox(bbox, mapDataWithGeometry)
synchronized(isReplacingForBBoxLock) { isReplacingForBBox = false }

callOnUpdated(updatesWhileReplacingBBox.updated, updatesWhileReplacingBBox.deleted)
updatesWhileReplacingBBox.clear()
}
}

override fun onCleared() {
Expand Down Expand Up @@ -515,9 +534,15 @@ class MapDataWithEditsSource internal constructor(
listeners.remove(listener)
}

private fun callOnUpdated(updated: MapDataWithGeometry = MutableMapDataWithGeometry(), deleted: Collection<ElementKey> = emptyList()) {
private fun callOnUpdated(updated: MapDataWithGeometry, deleted: Collection<ElementKey>) {
if (updated.size == 0 && deleted.isEmpty()) return
listeners.forEach { it.onUpdated(updated, deleted) }

synchronized(isReplacingForBBoxLock) {
if (isReplacingForBBox) {
updatesWhileReplacingBBox.add(updated, deleted)
}
}
}
private fun callOnReplacedForBBox(bbox: BoundingBox, mapDataWithGeometry: MapDataWithGeometry) {
if (mapDataWithGeometry.size == 0) return
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.westnordost.streetcomplete.data.osm.mapdata

data class MapDataWithGeometryUpdates(
val updated: MutableMapDataWithGeometry = MutableMapDataWithGeometry(),
val deleted: MutableList<ElementKey> = ArrayList()
) {
fun add(updated: MapDataWithGeometry, deleted: Collection<ElementKey>) {
this.deleted.removeAll(updated.map { it.key })
this.deleted.addAll(deleted)

this.updated.removeAll(deleted)
this.updated.putAll(updated)
}

fun clear() {
updated.clear()
deleted.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ class MutableMapDataWithGeometry() : MapDataWithGeometry {
}
}

fun putAll(other: MapDataWithGeometry) {
for (node in other.nodes) {
put(node, other.getNodeGeometry(node.id))
}
for (way in other.ways) {
put(way, other.getWayGeometry(way.id))
}
for (relation in other.relations) {
put(relation, other.getRelationGeometry(relation.id))
}
}

fun putElement(element: Element) {
val id = element.id
when (element) {
Expand All @@ -79,6 +91,22 @@ class MutableMapDataWithGeometry() : MapDataWithGeometry {
}
}


Check failure on line 94 in app/src/main/java/de/westnordost/streetcomplete/data/osm/mapdata/MutableMapDataWithGeometry.kt

View workflow job for this annotation

GitHub Actions / Kotlin

Needless blank line(s)
fun removeAll(deleted: Collection<ElementKey>) {
for (key in deleted) {
remove(key.type, key.id)
}
}

fun clear() {
nodesById.clear()
waysById.clear()
relationsById.clear()
nodeGeometriesById.clear()
wayGeometriesById.clear()
relationGeometriesById.clear()
}

override fun iterator(): Iterator<Element> {
return (nodes.asSequence() + ways.asSequence() + relations.asSequence()).iterator()
}
Expand Down
21 changes: 9 additions & 12 deletions app/src/main/java/de/westnordost/streetcomplete/util/Listeners.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,20 @@ package de.westnordost.streetcomplete.util

/** Lightweight wrapper around `HashSet` for storing listeners in a thread-safe way */
class Listeners<T> {
private val listeners = HashSet<T>()
private var listeners: Set<T> = HashSet()

fun add(element: T): Boolean {
synchronized(this) {
return listeners.add(element)
}
fun add(element: T) {
synchronized(this) { listeners = listeners + element }
}

fun remove(element: T): Boolean {
synchronized(this) {
return listeners.remove(element)
}
fun remove(element: T) {
synchronized(this) { listeners = listeners - element }
}

fun forEach(action: (T) -> Unit) {
synchronized(this) {
listeners.forEach(action)
}
val listeners = synchronized(this) { listeners }
// the executing of the action itself is not synchronized, only the access to the set,
// because it should be possible to call several Listeners::forEach at the same time
listeners.forEach(action)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import de.westnordost.streetcomplete.testutils.rel
import de.westnordost.streetcomplete.testutils.way
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull

class MutableMapDataWithGeometryTest {
Expand Down Expand Up @@ -65,4 +66,56 @@ class MutableMapDataWithGeometryTest {
assertEquals(rel, m.getRelation(rel.id))
assertEquals(geom, m.getRelationGeometry(rel.id))
}

@Test fun clear() {
val m = MutableMapDataWithGeometry()
m.put(node(0), ElementPointGeometry(p()))
m.put(way(0), ElementPolylinesGeometry(listOf(), p()))
m.put(rel(0), ElementPolygonsGeometry(listOf(), p()))

assertEquals(3, m.size)

m.clear()
assertEquals(0, m.size)
assertEquals(0, m.nodes.size)
assertEquals(0, m.ways.size)
assertEquals(0, m.relations.size)
assertNull(m.getNodeGeometry(0))
assertNull(m.getWayGeometry(0))
assertNull(m.getRelationGeometry(0))
}

@Test fun putAll() {
val m = MutableMapDataWithGeometry()
m.put(node(0), ElementPointGeometry(p()))
m.put(way(0), ElementPolylinesGeometry(listOf(), p()))
m.put(rel(0), ElementPolygonsGeometry(listOf(), p()))

val n = MutableMapDataWithGeometry()
n.putAll(m)

assertEquals(3, n.size)
assertEquals(1, m.nodes.size)
assertEquals(1, m.ways.size)
assertEquals(1, m.relations.size)
assertNotNull(m.getNodeGeometry(0))
assertNotNull(m.getWayGeometry(0))
assertNotNull(m.getRelationGeometry(0))
}

@Test fun removeAll() {
val m = MutableMapDataWithGeometry()
m.put(node(0), ElementPointGeometry(p()))
m.put(node(1), ElementPointGeometry(p()))
m.put(way(0), ElementPolylinesGeometry(listOf(), p()))

m.removeAll(listOf(ElementKey(ElementType.NODE, 0), ElementKey(ElementType.WAY, 0)))

assertNull(m.getNode(0))
assertNull(m.getNodeGeometry(0))
assertNull(m.getWay(0))
assertNull(m.getWayGeometry(0))
assertNotNull(m.getNode(1))
assertNotNull(m.getNodeGeometry(1))
}
}

0 comments on commit e2621c7

Please sign in to comment.