From e106ceef9aa85b8547489284e28f66dbf6454d0a Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 20 Sep 2022 13:57:41 +0300 Subject: [PATCH] Fix the problem with failed downloads... again --- build.gradle.kts | 2 +- maps-kt-compose/build.gradle.kts | 21 ++++++++- .../sciprog/maps/compose/MapTileProvider.kt | 4 +- .../center/sciprog/maps/compose/MapViewJvm.kt | 20 +++++---- .../maps/compose/OpenStreetMapTileProvider.kt | 28 +++++------- .../maps/compose/OsmTileProviderTest.kt | 45 +++++++++++++++++++ 6 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 maps-kt-compose/src/jvmTest/kotlin/center/sciprog/maps/compose/OsmTileProviderTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 77f7973..a606292 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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{ diff --git a/maps-kt-compose/build.gradle.kts b/maps-kt-compose/build.gradle.kts index cfb4aac..db30e02 100644 --- a/maps-kt-compose/build.gradle.kts +++ b/maps-kt-compose/build.gradle.kts @@ -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 { + useJUnitPlatform() } \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt index 6a71511..8d99571 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTileProvider.kt @@ -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 { diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt index 036b96f..3251b1e 100644 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt +++ b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapViewJvm.kt @@ -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,19 +197,21 @@ 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 { - 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" } + try { + mapTiles += deferred.await() + } catch (ex: Exception) { + //displaying the error is maps responsibility + logger.error(ex) { "Failed to load tile with id=$id" } + } } } + } } @@ -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 ) diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/OpenStreetMapTileProvider.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/OpenStreetMapTileProvider.kt index 4a98ae4..10581d2 100644 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/OpenStreetMapTileProvider.kt +++ b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/OpenStreetMapTileProvider.kt @@ -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>(cacheCapacity) + private val cache = LruCache>(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 = async(Dispatchers.IO) { + private fun CoroutineScope.downloadImageAsync(id: TileId): Deferred = 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 { //start image download - val imageDeferred = cache.getOrPut(tileId) { + val imageDeferred: Deferred = 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) } } diff --git a/maps-kt-compose/src/jvmTest/kotlin/center/sciprog/maps/compose/OsmTileProviderTest.kt b/maps-kt-compose/src/jvmTest/kotlin/center/sciprog/maps/compose/OsmTileProviderTest.kt new file mode 100644 index 0000000..4c9049c --- /dev/null +++ b/maps-kt-compose/src/jvmTest/kotlin/center/sciprog/maps/compose/OsmTileProviderTest.kt @@ -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() + } + } + } + } + +} \ No newline at end of file