Refactor papallel doanwload

This commit is contained in:
Alexander Nozik 2022-07-16 19:22:22 +03:00
parent 96844cd526
commit 7a243780d1
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
5 changed files with 88 additions and 96 deletions

View File

@ -13,5 +13,5 @@ pluginManagement {
} }
} }
rootProject.name = "maps-kt-compose" rootProject.name = "maps-kt"

View File

@ -2,6 +2,7 @@ package centre.sciprog.maps.compose
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlin.math.floor import kotlin.math.floor
data class TileId( data class TileId(
@ -16,7 +17,7 @@ data class MapTile(
) )
interface MapTileProvider { interface MapTileProvider {
suspend fun loadTileAsync(tileIds: List<TileId>, scope: CoroutineScope, onTileLoad: (mapTile: MapTile) -> Unit) fun CoroutineScope.loadTileAsync(tileId: TileId): Deferred<MapTile>
val tileSize: Int get() = DEFAULT_TILE_SIZE val tileSize: Int get() = DEFAULT_TILE_SIZE

View File

@ -1,4 +1,4 @@
package centre.sciprog.maps package centre.sciprog.maps.compose
import kotlin.jvm.Synchronized import kotlin.jvm.Synchronized
@ -9,32 +9,14 @@ internal class LruCache<K, V>(
private val cache = linkedMapOf<K, V>() private val cache = linkedMapOf<K, V>()
@Synchronized @Synchronized
fun getCache() = cache.toMap() fun put(key: K, value: V){
if (cache.size >= capacity) {
@Synchronized cache.remove(cache.iterator().next().key)
fun put(key: K, value: V) = internalPut(key, value) }
cache[key] = value
@Synchronized
operator fun get(key: K) = internalGet(key)
@Synchronized
fun remove(key: K) {
cache.remove(key)
} }
@Synchronized operator fun get(key: K): V? {
fun getOrPut(key: K, callback: () -> V): V {
val internalGet = internalGet(key)
return internalGet ?: callback().also { internalPut(key, it) }
}
@Synchronized
fun clear(newCapacity: Int? = null) {
cache.clear()
capacity = newCapacity ?: capacity
}
private fun internalGet(key: K): V? {
val value = cache[key] val value = cache[key]
if (value != null) { if (value != null) {
cache.remove(key) cache.remove(key)
@ -43,11 +25,18 @@ internal class LruCache<K, V>(
return value return value
} }
private fun internalPut(key: K, value: V) { @Synchronized
if (cache.size >= capacity) { fun remove(key: K) {
cache.remove(cache.iterator().next().key) cache.remove(key)
} }
cache[key] = value
@Synchronized
fun getOrPut(key: K, factory: () -> V): V = get(key) ?: factory().also { put(key, it) }
@Synchronized
fun reset(newCapacity: Int? = null) {
cache.clear()
capacity = newCapacity ?: capacity
} }
} }

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.* import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import centre.sciprog.maps.* import centre.sciprog.maps.*
import kotlinx.coroutines.launch
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.skia.Font import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint import org.jetbrains.skia.Paint
@ -28,6 +29,8 @@ private fun Color.toPaint(): Paint = Paint().apply {
color = toArgb() color = toArgb()
} }
private fun IntRange.intersect(other: IntRange) = max(first, other.first)..min(last, other.last)
private val logger = KotlinLogging.logger("MapView") private val logger = KotlinLogging.logger("MapView")
/** /**
@ -148,39 +151,40 @@ actual fun MapView(
// Load tiles asynchronously // Load tiles asynchronously
LaunchedEffect(viewPoint, canvasSize) { LaunchedEffect(viewPoint, canvasSize) {
val indexRange = 0 until 2.0.pow(zoom).toInt() with(mapTileProvider) {
val indexRange = 0 until 2.0.pow(zoom).toInt()
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
val horizontalIndices = mapTileProvider.toIndex(left) val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
.rangeTo(mapTileProvider.toIndex(right))
.intersect(indexRange)
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale) val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale) val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
val verticalIndices = mapTileProvider.toIndex(bottom) val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
.rangeTo(mapTileProvider.toIndex(top))
.intersect(indexRange)
mapTiles.clear() mapTiles.clear()
val tileIds = verticalIndices for (j in verticalIndices) {
.flatMap { j -> for (i in horizontalIndices) {
horizontalIndices val id = TileId(zoom, i, j)
.asSequence() //start all
.map { TileId(zoom, it, j) } val deferred = loadTileAsync(id)
//wait asynchronously for it to finish
launch {
try {
mapTiles += deferred.await()
} catch (ex: Exception) {
//displaying the error is maps responsibility
logger.error(ex) { "Failed to load tile with id=$id" }
}
}
}
} }
}
mapTileProvider.loadTileAsync(
tileIds = tileIds,
scope = this
) { mapTiles += it }
} }
// d
Canvas(canvasModifier) {
Canvas(canvasModifier) {
fun WebMercatorCoordinates.toOffset(): Offset = Offset( fun WebMercatorCoordinates.toOffset(): Offset = Offset(
(canvasSize.width / 2 + (x.dp - centerCoordinates.x.dp) * tileScale.toFloat()).toPx(), (canvasSize.width / 2 + (x.dp - centerCoordinates.x.dp) * tileScale.toFloat()).toPx(),
(canvasSize.height / 2 + (y.dp - centerCoordinates.y.dp) * tileScale.toFloat()).toPx() (canvasSize.height / 2 + (y.dp - centerCoordinates.y.dp) * tileScale.toFloat()).toPx()

View File

@ -2,12 +2,15 @@ package centre.sciprog.maps.compose
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.graphics.toComposeImageBitmap
import centre.sciprog.maps.LruCache import io.ktor.client.HttpClient
import io.ktor.client.* import io.ktor.client.request.get
import io.ktor.client.request.* import io.ktor.client.statement.readBytes
import io.ktor.client.statement.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.* import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.skia.Image import org.jetbrains.skia.Image
import java.net.URL import java.net.URL
@ -45,47 +48,42 @@ class OpenStreetMapTileProvider(
} }
} }
val url = id.osmUrl() try {
val byteArray = client.get(url).readBytes() //semaphore works only for actual download
semaphore.withPermit {
val url = id.osmUrl()
val byteArray = client.get(url).readBytes()
logger.debug { "Finished downloading map tile with id $id from $url" } logger.debug { "Finished downloading map tile with id $id from $url" }
id.cacheFilePath()?.let { path -> id.cacheFilePath()?.let { path ->
logger.debug { "Caching map tile $id to $path" } logger.debug { "Caching map tile $id to $path" }
path.parent.createDirectories() path.parent.createDirectories()
path.writeBytes(byteArray) path.writeBytes(byteArray)
}
Image.makeFromEncoded(byteArray).toComposeImageBitmap()
}
} catch (ex: Exception){
//if loading is failed for some reason, clear the cache
cache.remove(id)
throw ex
}
}
override fun CoroutineScope.loadTileAsync(
tileId: TileId,
): Deferred<MapTile> {
//start image download
val image = cache.getOrPut(tileId) {
downloadImageAsync(tileId)
} }
Image.makeFromEncoded(byteArray).toComposeImageBitmap() //collect the result asynchronously
return async { MapTile(tileId, image.await()) }
} }
override suspend fun loadTileAsync(
tileIds: List<TileId>,
scope: CoroutineScope,
onTileLoad: (mapTile: MapTile) -> Unit,
) {
tileIds
.forEach { id ->
try {
scope.launch {
semaphore.acquire()
try {
val image = cache.getOrPut(id) { downloadImageAsync(id) }
val result = MapTile(id, image.await())
onTileLoad(result)
} catch (e: Exception) {
cache.remove(id)
throw e
} finally {
semaphore.release()
}
}
} catch (ex: Exception) {
logger.error(ex) { "Failed to load tile $id" }
}
}
}
companion object { companion object {
private val logger = KotlinLogging.logger("OpenStreetMapCache") private val logger = KotlinLogging.logger("OpenStreetMapCache")