limit parallel requests

This commit is contained in:
Lev Shagalov 2022-07-15 09:30:36 +03:00
parent 416328e320
commit 0694cd6a07
4 changed files with 59 additions and 51 deletions

View File

@ -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<MapTile>
suspend fun loadTileAsync(id: TileId, scope: CoroutineScope): Deferred<MapTile>
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()

View File

@ -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<GeodeticMapCoordinates?>(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)
) {

View File

@ -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(
)
}
}
}
}

View File

@ -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<TileId, Deferred<ImageBitmap>>()
private val semaphore = Semaphore(parallelism)
private val cache = LruCache<TileId, Deferred<ImageBitmap>>(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<MapTile> {
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()
}
}
}