Fix the problem with failed downloads... again
This commit is contained in:
parent
c82f47a786
commit
e106ceef9a
@ -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{
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user