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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlin.math.floor
data class TileId(
@ -16,7 +17,7 @@ data class MapTile(
)
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

View File

@ -1,4 +1,4 @@
package centre.sciprog.maps
package centre.sciprog.maps.compose
import kotlin.jvm.Synchronized
@ -9,32 +9,14 @@ internal class LruCache<K, V>(
private val cache = linkedMapOf<K, V>()
@Synchronized
fun getCache() = cache.toMap()
@Synchronized
fun put(key: K, value: V) = internalPut(key, value)
@Synchronized
operator fun get(key: K) = internalGet(key)
@Synchronized
fun remove(key: K) {
cache.remove(key)
fun put(key: K, value: V){
if (cache.size >= capacity) {
cache.remove(cache.iterator().next().key)
}
cache[key] = value
}
@Synchronized
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? {
operator fun get(key: K): V? {
val value = cache[key]
if (value != null) {
cache.remove(key)
@ -43,11 +25,18 @@ internal class LruCache<K, V>(
return value
}
private fun internalPut(key: K, value: V) {
if (cache.size >= capacity) {
cache.remove(cache.iterator().next().key)
}
cache[key] = value
@Synchronized
fun remove(key: K) {
cache.remove(key)
}
@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.unit.*
import centre.sciprog.maps.*
import kotlinx.coroutines.launch
import mu.KotlinLogging
import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint
@ -28,6 +29,8 @@ private fun Color.toPaint(): Paint = Paint().apply {
color = toArgb()
}
private fun IntRange.intersect(other: IntRange) = max(first, other.first)..min(last, other.last)
private val logger = KotlinLogging.logger("MapView")
/**
@ -148,39 +151,40 @@ actual fun MapView(
// Load tiles asynchronously
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 right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
val horizontalIndices = mapTileProvider.toIndex(left)
.rangeTo(mapTileProvider.toIndex(right))
.intersect(indexRange)
val left = centerCoordinates.x - canvasSize.width.value / 2 / tileScale
val right = centerCoordinates.x + canvasSize.width.value / 2 / tileScale
val horizontalIndices: IntRange = (toIndex(left)..toIndex(right)).intersect(indexRange)
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
val verticalIndices = mapTileProvider.toIndex(bottom)
.rangeTo(mapTileProvider.toIndex(top))
.intersect(indexRange)
val top = (centerCoordinates.y + canvasSize.height.value / 2 / tileScale)
val bottom = (centerCoordinates.y - canvasSize.height.value / 2 / tileScale)
val verticalIndices: IntRange = (toIndex(bottom)..toIndex(top)).intersect(indexRange)
mapTiles.clear()
mapTiles.clear()
val tileIds = verticalIndices
.flatMap { j ->
horizontalIndices
.asSequence()
.map { TileId(zoom, it, j) }
for (j in verticalIndices) {
for (i in horizontalIndices) {
val id = TileId(zoom, i, j)
//start all
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(
(canvasSize.width / 2 + (x.dp - centerCoordinates.x.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.toComposeImageBitmap
import centre.sciprog.maps.LruCache
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.readBytes
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import mu.KotlinLogging
import org.jetbrains.skia.Image
import java.net.URL
@ -45,47 +48,42 @@ class OpenStreetMapTileProvider(
}
}
val url = id.osmUrl()
val byteArray = client.get(url).readBytes()
try {
//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 ->
logger.debug { "Caching map tile $id to $path" }
id.cacheFilePath()?.let { path ->
logger.debug { "Caching map tile $id to $path" }
path.parent.createDirectories()
path.writeBytes(byteArray)
path.parent.createDirectories()
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 {
private val logger = KotlinLogging.logger("OpenStreetMapCache")