Refactor papallel doanwload
This commit is contained in:
parent
96844cd526
commit
7a243780d1
@ -13,5 +13,5 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "maps-kt-compose"
|
rootProject.name = "maps-kt"
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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()
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user