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 { allprojects {
group = "center.sciprog" group = "center.sciprog"
version = "0.1.0-dev-8" version = "0.1.0-dev-9"
} }
ksciencePublish{ ksciencePublish{

View File

@ -24,11 +24,28 @@ kotlin {
api("io.github.microutils:kotlin-logging:2.1.23") api("io.github.microutils:kotlin-logging:2.1.23")
} }
} }
val jvmMain by getting val jvmMain by getting{
val jvmTest 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{ java{
targetCompatibility = space.kscience.gradle.KScienceVersions.JVM_TARGET targetCompatibility = space.kscience.gradle.KScienceVersions.JVM_TARGET
} }
tasks.withType<Test> {
useJUnitPlatform()
}

View File

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

View File

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