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 package centre.sciprog.maps.compose
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlin.math.floor import kotlin.math.floor
@ -16,7 +17,7 @@ data class MapTile(
) )
interface MapTileProvider { interface MapTileProvider {
fun loadTileAsync(id: TileId): Deferred<MapTile> suspend fun loadTileAsync(id: TileId, scope: CoroutineScope): Deferred<MapTile>
val tileSize: Int get() = DEFAULT_TILE_SIZE val tileSize: Int get() = DEFAULT_TILE_SIZE
@ -28,5 +29,3 @@ interface MapTileProvider {
const val DEFAULT_TILE_SIZE = 256 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.GeodeticMapCoordinates
import centre.sciprog.maps.MapViewPoint import centre.sciprog.maps.MapViewPoint
import centre.sciprog.maps.compose.* import centre.sciprog.maps.compose.*
import io.ktor.client.HttpClient import io.ktor.client.*
import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -33,7 +33,12 @@ fun App() {
} }
val scope = rememberCoroutineScope() 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) } var coordinates by remember { mutableStateOf<GeodeticMapCoordinates?>(null) }
@ -41,8 +46,8 @@ fun App() {
//display click coordinates //display click coordinates
Text(coordinates?.toString() ?: "") Text(coordinates?.toString() ?: "")
MapView( MapView(
mapTileProvider, mapTileProvider = mapTileProvider,
viewPoint, initialViewPoint = viewPoint,
onClick = { gmc -> coordinates = gmc }, onClick = { gmc -> coordinates = gmc },
config = MapViewConfig(inferViewBoxFromFeatures = true) config = MapViewConfig(inferViewBoxFromFeatures = true)
) { ) {

View File

@ -99,10 +99,10 @@ actual fun MapView(
selectRect?.let { rect -> selectRect?.let { rect ->
val offset = dragChange.position val offset = dragChange.position
selectRect = Rect( selectRect = Rect(
kotlin.math.min(offset.x, rect.left), min(offset.x, rect.left),
kotlin.math.min(offset.y, rect.top), min(offset.y, rect.top),
kotlin.math.max(offset.x, rect.right), max(offset.x, rect.right),
kotlin.math.max(offset.y, rect.bottom) max(offset.y, rect.bottom)
) )
} }
} }
@ -116,7 +116,7 @@ actual fun MapView(
viewPointOverride = MapViewPoint( viewPointOverride = MapViewPoint(
centerGmc, centerGmc,
viewPoint.zoom + kotlin.math.min(verticalZoom, horizontalZoom) viewPoint.zoom + min(verticalZoom, horizontalZoom)
) )
selectRect = null selectRect = null
} }
@ -147,33 +147,37 @@ actual fun MapView(
// Load tiles asynchronously // Load tiles asynchronously
LaunchedEffect(viewPoint, canvasSize) { LaunchedEffect(viewPoint, canvasSize) {
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)..mapTileProvider.toIndex(right) val horizontalIndices = mapTileProvider.toIndex(left)
.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)..mapTileProvider.toIndex(top) val verticalIndices = mapTileProvider.toIndex(bottom)
.rangeTo(mapTileProvider.toIndex(top))
.intersect(indexRange)
mapTiles.clear() mapTiles.clear()
val indexRange = 0 until 2.0.pow(zoom).toInt() verticalIndices
.flatMap { j ->
for (j in verticalIndices) { horizontalIndices
for (i in horizontalIndices) { .asSequence()
if (i in indexRange && j in indexRange) { .map { TileId(zoom, it, j) }
val tileId = TileId(zoom, i, j) }
try { .forEach {
launch { try {
val tile = mapTileProvider.loadTileAsync(tileId) launch {
mapTiles.add(tile.await()) mapTiles += mapTileProvider.loadTileAsync(it, this).await()
}
} catch (ex: Exception) {
logger.error(ex) { "Failed to load tile $tileId" }
} }
} 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.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.graphics.toComposeImageBitmap
import io.ktor.client.HttpClient import centre.sciprog.maps.LruCache
import io.ktor.client.request.get import io.ktor.client.*
import io.ktor.client.statement.readBytes import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.sync.Semaphore
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.skia.Image import org.jetbrains.skia.Image
import java.net.URL import java.net.URL
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.* import kotlin.io.path.*
import kotlin.math.pow
/** /**
* A [MapTileProvider] based on Open Street Map API. With in-memory and file cache * A [MapTileProvider] based on Open Street Map API. With in-memory and file cache
*/ */
public class OpenStreetMapTileProvider( class OpenStreetMapTileProvider(
private val scope: CoroutineScope,
private val client: HttpClient, private val client: HttpClient,
private val cacheDirectory: Path, private val cacheDirectory: Path,
parallelism: Int = 1,
cacheCapacity: Int = 200,
) : MapTileProvider { ) : 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") 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 * 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 -> id.cacheFilePath()?.let { path ->
if (path.exists()) { if (path.exists()) {
try { try {
@ -60,23 +63,20 @@ public class OpenStreetMapTileProvider(
Image.makeFromEncoded(byteArray).toComposeImageBitmap() Image.makeFromEncoded(byteArray).toComposeImageBitmap()
} }
override fun loadTileAsync(id: TileId): Deferred<MapTile> { override suspend fun loadTileAsync(id: TileId, scope: CoroutineScope) = scope.async {
val indexRange = indexRange(id.zoom) semaphore.acquire()
if (id.i !in indexRange || id.j !in indexRange) { try {
error("Indices (${id.i}, ${id.j}) are not in index range $indexRange for zoom ${id.zoom}") val image = cache.getOrPut(id) { downloadImageAsync(id) }
}
val image = cache.getOrPut(id) {
downloadImageAsync(id)
}
return scope.async {
MapTile(id, image.await()) MapTile(id, image.await())
} catch (e: Exception) {
cache.remove(id)
throw e
} finally {
semaphore.release()
} }
} }
companion object { companion object {
private val logger = KotlinLogging.logger("OpenStreetMapCache") private val logger = KotlinLogging.logger("OpenStreetMapCache")
private fun indexRange(zoom: Int): IntRange = 0 until 2.0.pow(zoom).toInt()
} }
} }