limit parallel requests
This commit is contained in:
parent
416328e320
commit
0694cd6a07
@ -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()
|
|
@ -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)
|
||||||
) {
|
) {
|
||||||
|
@ -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,31 +147,35 @@ 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)
|
}
|
||||||
|
.forEach {
|
||||||
try {
|
try {
|
||||||
launch {
|
launch {
|
||||||
val tile = mapTileProvider.loadTileAsync(tileId)
|
mapTiles += mapTileProvider.loadTileAsync(it, this).await()
|
||||||
mapTiles.add(tile.await())
|
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
logger.error(ex) { "Failed to load tile $tileId" }
|
logger.error(ex) { "Failed to load tile $it" }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user