Fix the problem with failed downloads... again

This commit is contained in:
Alexander Nozik 2022-09-20 13:57:41 +03:00
parent c82f47a786
commit e106ceef9a
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
6 changed files with 89 additions and 31 deletions

View File

@ -10,7 +10,7 @@ val ktorVersion by extra("2.0.3")
allprojects {
group = "center.sciprog"
version = "0.1.0-dev-8"
version = "0.1.0-dev-9"
}
ksciencePublish{

View File

@ -24,11 +24,28 @@ kotlin {
api("io.github.microutils:kotlin-logging:2.1.23")
}
}
val jvmMain by getting
val jvmTest by getting
val jvmMain by getting{
}
val jvmTest by getting{
dependencies {
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation(compose.desktop.currentOs)
implementation(spclibs.kotlinx.coroutines.test)
implementation("ch.qos.logback:logback-classic:1.2.11")
implementation(kotlin("test-junit5"))
implementation("org.junit.jupiter:junit-jupiter:5.8.2")
}
}
}
}
java{
targetCompatibility = space.kscience.gradle.KScienceVersions.JVM_TARGET
}
tasks.withType<Test> {
useJUnitPlatform()
}

View File

@ -1,8 +1,8 @@
package center.sciprog.maps.compose
import androidx.compose.ui.graphics.ImageBitmap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import org.jetbrains.skia.Image
import kotlin.math.floor
public data class TileId(
@ -13,7 +13,7 @@ public data class TileId(
public data class MapTile(
val id: TileId,
val image: ImageBitmap,
val image: Image,
)
public interface MapTileProvider {

View File

@ -14,8 +14,8 @@ import androidx.compose.ui.graphics.drawscope.*
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.unit.*
import center.sciprog.maps.coordinates.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import mu.KotlinLogging
import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint
@ -197,15 +197,15 @@ public actual fun MapView(
for (j in verticalIndices) {
for (i in horizontalIndices) {
val id = TileId(zoom, i, j)
try {
//ensure that failed tiles do not fail the application
supervisorScope {
//start all
val deferred = loadTileAsync(id)
//wait asynchronously for it to finish
launch {
try {
mapTiles += deferred.await()
}
} catch (ex: Exception) {
if (ex !is CancellationException) {
//displaying the error is maps responsibility
logger.error(ex) { "Failed to load tile with id=$id" }
}
@ -213,6 +213,8 @@ public actual fun MapView(
}
}
}
}
}
@ -334,7 +336,7 @@ public actual fun MapView(
(canvasSize.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale.toFloat()).roundToPx()
)
drawImage(
image = image,
image = image.toComposeImageBitmap(),
dstOffset = offset,
dstSize = tileSize
)

View File

@ -1,11 +1,8 @@
package center.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 io.ktor.utils.io.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
@ -26,23 +23,24 @@ public class OpenStreetMapTileProvider(
private val cacheDirectory: Path,
parallelism: Int = 4,
cacheCapacity: Int = 200,
private val osmBaseUrl: String = "https://tile.openstreetmap.org",
) : MapTileProvider {
private val semaphore = Semaphore(parallelism)
private val cache = LruCache<TileId, Deferred<ImageBitmap>>(cacheCapacity)
private val cache = LruCache<TileId, Deferred<Image>>(cacheCapacity)
private fun TileId.osmUrl() = URL("https://tile.openstreetmap.org/${zoom}/${i}/${j}.png")
private fun TileId.osmUrl() = URL("$osmBaseUrl/${zoom}/${i}/${j}.png")
private fun TileId.cacheFilePath() = cacheDirectory.resolve("${zoom}/${i}/${j}.png")
/**
* Download and cache the tile image
*/
private fun CoroutineScope.downloadImageAsync(id: TileId): Deferred<ImageBitmap> = async(Dispatchers.IO) {
private fun CoroutineScope.downloadImageAsync(id: TileId): Deferred<Image> = async(Dispatchers.IO) {
id.cacheFilePath()?.let { path ->
if (path.exists()) {
try {
return@async Image.makeFromEncoded(path.readBytes()).toComposeImageBitmap()
return@async Image.makeFromEncoded(path.readBytes())
} catch (ex: Exception) {
logger.debug { "Failed to load image from $path" }
path.deleteIfExists()
@ -62,7 +60,7 @@ public class OpenStreetMapTileProvider(
path.writeBytes(byteArray)
}
Image.makeFromEncoded(byteArray).toComposeImageBitmap()
Image.makeFromEncoded(byteArray)
}
}
@ -71,21 +69,17 @@ public class OpenStreetMapTileProvider(
): Deferred<MapTile> {
//start image download
val imageDeferred = cache.getOrPut(tileId) {
val imageDeferred: Deferred<Image> = cache.getOrPut(tileId) {
downloadImageAsync(tileId)
}
//collect the result asynchronously
return async {
val image: ImageBitmap = try {
imageDeferred.await()
} catch (ex: Exception) {
val image: Image = runCatching { imageDeferred.await() }.onFailure {
logger.error(it) { "Failed to load tile image with id=$tileId" }
cache.remove(tileId)
if (ex !is CancellationException) {
logger.error(ex) { "Failed to load tile image with id=$tileId" }
}
throw ex
}
}.getOrThrow()
MapTile(tileId, image)
}
}

View File

@ -0,0 +1,45 @@
package center.sciprog.maps.compose
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import java.nio.file.Files
import kotlin.test.assertFails
@OptIn(ExperimentalCoroutinesApi::class)
class OsmTileProviderTest {
// @get:Rule
// val rule = createComposeRule()
@Test
fun testCorrectOsm() = runTest {
val provider = OpenStreetMapTileProvider(HttpClient(CIO), Files.createTempDirectory("mapCache"))
val tileId = TileId(3, 1, 1)
with(provider) {
loadTileAsync(tileId).await()
}
}
@Test
fun testFailedOsm() = runTest {
val provider = OpenStreetMapTileProvider(
HttpClient(CIO),
Files.createTempDirectory("mapCache"),
osmBaseUrl = "https://tile.openstreetmap1.org"
)
val tileId = TileId(3, 1, 1)
supervisorScope {
with(provider) {
val deferred = loadTileAsync(tileId)
assertFails {
deferred.await()
}
}
}
}
}