From 7dd59dbf2ab139f6e0fd8f1cfee296b3d5c69512 Mon Sep 17 00:00:00 2001 From: Lev Shagalov Date: Fri, 15 Jul 2022 09:05:57 +0300 Subject: [PATCH 1/6] LruCache --- .../kotlin/centre/sciprog/maps/LruCache.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt diff --git a/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt b/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt new file mode 100644 index 0000000..9619eb0 --- /dev/null +++ b/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt @@ -0,0 +1,61 @@ +package centre.sciprog.maps + +import kotlin.jvm.Synchronized + + +class LruCache( + private var capacity: Int, +) { + + private val cache = mutableMapOf() + private val order = mutableListOf() + + @Synchronized + fun getCache() = cache.toMap() + + @Synchronized + fun getOrder() = order.toList() + + @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) + order.remove(key) + } + + @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() + order.clear() + capacity = newCapacity ?: capacity + } + + private fun internalGet(key: K): V? { + val value = cache[key] + if (value != null) { + order.remove(key) + order.add(key) + } + return value + } + + private fun internalPut(key: K, value: V) { + if (cache.size >= capacity) { + cache.remove(order.removeAt(0)) + } + cache[key] = value + order.add(key) + } + +} From 0694cd6a07a63b89415bc10ff21983533799fa04 Mon Sep 17 00:00:00 2001 From: Lev Shagalov Date: Fri, 15 Jul 2022 09:30:36 +0300 Subject: [PATCH 2/6] limit parallel requests --- .../sciprog/maps/compose/MapTileProvider.kt | 5 +- src/jvmMain/kotlin/Main.kt | 15 ++++-- .../centre/sciprog/maps/compose/MapViewJvm.kt | 48 ++++++++++--------- .../maps/compose/OpenStreetMapTileProvider.kt | 42 ++++++++-------- 4 files changed, 59 insertions(+), 51 deletions(-) diff --git a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt index 20366b0..37e3ca3 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt @@ -1,6 +1,7 @@ package centre.sciprog.maps.compose import androidx.compose.ui.graphics.ImageBitmap +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlin.math.floor @@ -16,7 +17,7 @@ data class MapTile( ) interface MapTileProvider { - fun loadTileAsync(id: TileId): Deferred + suspend fun loadTileAsync(id: TileId, scope: CoroutineScope): Deferred val tileSize: Int get() = DEFAULT_TILE_SIZE @@ -28,5 +29,3 @@ interface MapTileProvider { const val DEFAULT_TILE_SIZE = 256 } } - -suspend fun MapTileProvider.loadTile(id: TileId): MapTile = loadTileAsync(id).await() \ No newline at end of file diff --git a/src/jvmMain/kotlin/Main.kt b/src/jvmMain/kotlin/Main.kt index 5c19ea9..6ca12d5 100644 --- a/src/jvmMain/kotlin/Main.kt +++ b/src/jvmMain/kotlin/Main.kt @@ -12,8 +12,8 @@ import androidx.compose.ui.window.application import centre.sciprog.maps.GeodeticMapCoordinates import centre.sciprog.maps.MapViewPoint import centre.sciprog.maps.compose.* -import io.ktor.client.HttpClient -import io.ktor.client.engine.cio.CIO +import io.ktor.client.* +import io.ktor.client.engine.cio.* import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -33,7 +33,12 @@ fun App() { } val scope = rememberCoroutineScope() - val mapTileProvider = remember { OpenStreetMapTileProvider(scope, HttpClient(CIO), Path.of("mapCache")) } + val mapTileProvider = remember { + OpenStreetMapTileProvider( + client = HttpClient(CIO), + cacheDirectory = Path.of("mapCache") + ) + } var coordinates by remember { mutableStateOf(null) } @@ -41,8 +46,8 @@ fun App() { //display click coordinates Text(coordinates?.toString() ?: "") MapView( - mapTileProvider, - viewPoint, + mapTileProvider = mapTileProvider, + initialViewPoint = viewPoint, onClick = { gmc -> coordinates = gmc }, config = MapViewConfig(inferViewBoxFromFeatures = true) ) { diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt index 2bdf8eb..8e07084 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt @@ -99,10 +99,10 @@ actual fun MapView( selectRect?.let { rect -> val offset = dragChange.position selectRect = Rect( - kotlin.math.min(offset.x, rect.left), - kotlin.math.min(offset.y, rect.top), - kotlin.math.max(offset.x, rect.right), - kotlin.math.max(offset.y, rect.bottom) + min(offset.x, rect.left), + min(offset.y, rect.top), + max(offset.x, rect.right), + max(offset.y, rect.bottom) ) } } @@ -116,7 +116,7 @@ actual fun MapView( viewPointOverride = MapViewPoint( centerGmc, - viewPoint.zoom + kotlin.math.min(verticalZoom, horizontalZoom) + viewPoint.zoom + min(verticalZoom, horizontalZoom) ) selectRect = null } @@ -147,33 +147,37 @@ actual fun MapView( // Load tiles asynchronously LaunchedEffect(viewPoint, canvasSize) { + 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)..mapTileProvider.toIndex(right) + val horizontalIndices = mapTileProvider.toIndex(left) + .rangeTo(mapTileProvider.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)..mapTileProvider.toIndex(top) + val verticalIndices = mapTileProvider.toIndex(bottom) + .rangeTo(mapTileProvider.toIndex(top)) + .intersect(indexRange) mapTiles.clear() - val indexRange = 0 until 2.0.pow(zoom).toInt() - - for (j in verticalIndices) { - for (i in horizontalIndices) { - if (i in indexRange && j in indexRange) { - val tileId = TileId(zoom, i, j) - try { - launch { - val tile = mapTileProvider.loadTileAsync(tileId) - mapTiles.add(tile.await()) - } - } catch (ex: Exception) { - logger.error(ex) { "Failed to load tile $tileId" } + verticalIndices + .flatMap { j -> + horizontalIndices + .asSequence() + .map { TileId(zoom, it, j) } + } + .forEach { + try { + launch { + mapTiles += mapTileProvider.loadTileAsync(it, this).await() } + } catch (ex: Exception) { + logger.error(ex) { "Failed to load tile $it" } } } - } } @@ -259,4 +263,4 @@ actual fun MapView( ) } } -} \ No newline at end of file +} diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt index 351a2c0..4590994 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt @@ -2,29 +2,32 @@ package centre.sciprog.maps.compose import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.toComposeImageBitmap -import io.ktor.client.HttpClient -import io.ktor.client.request.get -import io.ktor.client.statement.readBytes +import centre.sciprog.maps.LruCache +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async +import kotlinx.coroutines.sync.Semaphore import mu.KotlinLogging import org.jetbrains.skia.Image import java.net.URL import java.nio.file.Path import kotlin.io.path.* -import kotlin.math.pow /** * A [MapTileProvider] based on Open Street Map API. With in-memory and file cache */ -public class OpenStreetMapTileProvider( - private val scope: CoroutineScope, +class OpenStreetMapTileProvider( private val client: HttpClient, private val cacheDirectory: Path, + parallelism: Int = 1, + cacheCapacity: Int = 200, ) : MapTileProvider { - private val cache = HashMap>() + private val semaphore = Semaphore(parallelism) + private val cache = LruCache>(cacheCapacity) private fun TileId.osmUrl() = URL("https://tile.openstreetmap.org/${zoom}/${i}/${j}.png") @@ -33,7 +36,7 @@ public class OpenStreetMapTileProvider( /** * Download and cache the tile image */ - private fun downloadImageAsync(id: TileId) = scope.async(Dispatchers.IO) { + private fun CoroutineScope.downloadImageAsync(id: TileId) = async(Dispatchers.IO) { id.cacheFilePath()?.let { path -> if (path.exists()) { try { @@ -60,23 +63,20 @@ public class OpenStreetMapTileProvider( Image.makeFromEncoded(byteArray).toComposeImageBitmap() } - override fun loadTileAsync(id: TileId): Deferred { - val indexRange = indexRange(id.zoom) - if (id.i !in indexRange || id.j !in indexRange) { - error("Indices (${id.i}, ${id.j}) are not in index range $indexRange for zoom ${id.zoom}") - } - - val image = cache.getOrPut(id) { - downloadImageAsync(id) - } - - return scope.async { + override suspend fun loadTileAsync(id: TileId, scope: CoroutineScope) = scope.async { + semaphore.acquire() + try { + val image = cache.getOrPut(id) { downloadImageAsync(id) } MapTile(id, image.await()) + } catch (e: Exception) { + cache.remove(id) + throw e + } finally { + semaphore.release() } } companion object { private val logger = KotlinLogging.logger("OpenStreetMapCache") - private fun indexRange(zoom: Int): IntRange = 0 until 2.0.pow(zoom).toInt() } -} \ No newline at end of file +} From fc0f2237667ea4fc6505f650d16575b51f160482 Mon Sep 17 00:00:00 2001 From: Lev Shagalov Date: Fri, 15 Jul 2022 10:17:42 +0300 Subject: [PATCH 3/6] LruCache with linkedMapOf --- .../kotlin/centre/sciprog/maps/LruCache.kt | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt b/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt index 9619eb0..3a2f0a9 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt @@ -6,16 +6,11 @@ import kotlin.jvm.Synchronized class LruCache( private var capacity: Int, ) { - - private val cache = mutableMapOf() - private val order = mutableListOf() + private val cache = linkedMapOf() @Synchronized fun getCache() = cache.toMap() - @Synchronized - fun getOrder() = order.toList() - @Synchronized fun put(key: K, value: V) = internalPut(key, value) @@ -25,7 +20,6 @@ class LruCache( @Synchronized fun remove(key: K) { cache.remove(key) - order.remove(key) } @Synchronized @@ -37,25 +31,23 @@ class LruCache( @Synchronized fun clear(newCapacity: Int? = null) { cache.clear() - order.clear() capacity = newCapacity ?: capacity } private fun internalGet(key: K): V? { val value = cache[key] if (value != null) { - order.remove(key) - order.add(key) + cache.remove(key) + cache[key] = value } return value } private fun internalPut(key: K, value: V) { if (cache.size >= capacity) { - cache.remove(order.removeAt(0)) + cache.remove(cache.iterator().next().key) } cache[key] = value - order.add(key) } } From 5d3db81c4ff91c449fd14cbb75acdc8640cd1532 Mon Sep 17 00:00:00 2001 From: Lev Shagalov Date: Fri, 15 Jul 2022 10:39:04 +0300 Subject: [PATCH 4/6] loadTileAsync refactor loadTileAsync consumes all tileIds --- .../sciprog/maps/compose/MapTileProvider.kt | 3 +- .../centre/sciprog/maps/compose/MapViewJvm.kt | 47 +++++++++---------- .../maps/compose/OpenStreetMapTileProvider.kt | 41 ++++++++++------ 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt index 37e3ca3..6c91a99 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt @@ -2,7 +2,6 @@ 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( @@ -17,7 +16,7 @@ data class MapTile( ) interface MapTileProvider { - suspend fun loadTileAsync(id: TileId, scope: CoroutineScope): Deferred + suspend fun loadTileAsync(tileIds: List, scope: CoroutineScope, onTileLoad: (mapTile: MapTile) -> Unit) val tileSize: Int get() = DEFAULT_TILE_SIZE diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt index 8e07084..1e254ca 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt @@ -17,7 +17,6 @@ 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 @@ -46,21 +45,23 @@ actual fun MapView( ) { var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) } - var viewPointOverride by remember { mutableStateOf( - if(config.inferViewBoxFromFeatures){ - features.values.computeBoundingBox(1)?.let { box -> - val zoom = log2( - min( - canvasSize.width.value / box.width, - canvasSize.height.value / box.height - ) * PI / mapTileProvider.tileSize - ) - MapViewPoint(box.center, zoom) + var viewPointOverride by remember { + mutableStateOf( + if (config.inferViewBoxFromFeatures) { + features.values.computeBoundingBox(1)?.let { box -> + val zoom = log2( + min( + canvasSize.width.value / box.width, + canvasSize.height.value / box.height + ) * PI / mapTileProvider.tileSize + ) + MapViewPoint(box.center, zoom) + } + } else { + null } - } else { - null - } - ) } + ) + } val viewPoint by derivedStateOf { viewPointOverride ?: computeViewPoint(canvasSize) } @@ -163,21 +164,17 @@ actual fun MapView( mapTiles.clear() - verticalIndices + val tileIds = verticalIndices .flatMap { j -> horizontalIndices .asSequence() .map { TileId(zoom, it, j) } } - .forEach { - try { - launch { - mapTiles += mapTileProvider.loadTileAsync(it, this).await() - } - } catch (ex: Exception) { - logger.error(ex) { "Failed to load tile $it" } - } - } + + mapTileProvider.loadTileAsync( + tileIds = tileIds, + scope = this + ) { mapTiles += it } } diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt index 4590994..04daea8 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt @@ -6,10 +6,7 @@ import centre.sciprog.maps.LruCache import io.ktor.client.* import io.ktor.client.request.* import io.ktor.client.statement.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Semaphore import mu.KotlinLogging import org.jetbrains.skia.Image @@ -63,17 +60,31 @@ class OpenStreetMapTileProvider( Image.makeFromEncoded(byteArray).toComposeImageBitmap() } - override suspend fun loadTileAsync(id: TileId, scope: CoroutineScope) = scope.async { - semaphore.acquire() - try { - val image = cache.getOrPut(id) { downloadImageAsync(id) } - MapTile(id, image.await()) - } catch (e: Exception) { - cache.remove(id) - throw e - } finally { - semaphore.release() - } + override suspend fun loadTileAsync( + tileIds: List, + 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 { From 96844cd526683915521aa0bafc2b6d017e3b4a9e Mon Sep 17 00:00:00 2001 From: Lev Shagalov Date: Fri, 15 Jul 2022 10:52:26 +0300 Subject: [PATCH 5/6] LruCache internal --- src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt b/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt index 3a2f0a9..f9726af 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt @@ -3,7 +3,7 @@ package centre.sciprog.maps import kotlin.jvm.Synchronized -class LruCache( +internal class LruCache( private var capacity: Int, ) { private val cache = linkedMapOf() From 7a243780d1053cdc1f83744d7a6647b7f18d6320 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 16 Jul 2022 19:22:22 +0300 Subject: [PATCH 6/6] Refactor papallel doanwload --- settings.gradle.kts | 2 +- .../sciprog/maps/compose/MapTileProvider.kt | 3 +- .../centre/sciprog/maps/compose}/LruCache.kt | 49 +++++------- .../centre/sciprog/maps/compose/MapViewJvm.kt | 54 +++++++------ .../maps/compose/OpenStreetMapTileProvider.kt | 76 +++++++++---------- 5 files changed, 88 insertions(+), 96 deletions(-) rename src/{commonMain/kotlin/centre/sciprog/maps => jvmMain/kotlin/centre/sciprog/maps/compose}/LruCache.kt (55%) diff --git a/settings.gradle.kts b/settings.gradle.kts index 1fed0e7..374b7cd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,5 +13,5 @@ pluginManagement { } } -rootProject.name = "maps-kt-compose" +rootProject.name = "maps-kt" diff --git a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt index 6c91a99..c4d90f8 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt +++ b/src/commonMain/kotlin/centre/sciprog/maps/compose/MapTileProvider.kt @@ -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, scope: CoroutineScope, onTileLoad: (mapTile: MapTile) -> Unit) + fun CoroutineScope.loadTileAsync(tileId: TileId): Deferred val tileSize: Int get() = DEFAULT_TILE_SIZE diff --git a/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/LruCache.kt similarity index 55% rename from src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt rename to src/jvmMain/kotlin/centre/sciprog/maps/compose/LruCache.kt index f9726af..bf8d3fc 100644 --- a/src/commonMain/kotlin/centre/sciprog/maps/LruCache.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/LruCache.kt @@ -1,4 +1,4 @@ -package centre.sciprog.maps +package centre.sciprog.maps.compose import kotlin.jvm.Synchronized @@ -9,32 +9,14 @@ internal class LruCache( private val cache = linkedMapOf() @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( 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 } } diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt index b4bf970..9403e85 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/MapViewJvm.kt @@ -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() diff --git a/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt b/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt index 04daea8..7c1adf5 100644 --- a/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt +++ b/src/jvmMain/kotlin/centre/sciprog/maps/compose/OpenStreetMapTileProvider.kt @@ -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 { + //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, - 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")