Connect timeout exception handling #16
@ -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: ImageBitmap,
|
||||||
)
|
)
|
||||||
|
|
||||||
public interface MapTileProvider {
|
public interface MapTileProvider {
|
||||||
|
@ -195,20 +195,21 @@ 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)
|
||||||
//start all
|
try {
|
||||||
val deferred = loadTileAsync(id)
|
//start all
|
||||||
//wait asynchronously for it to finish
|
val deferred = loadTileAsync(id)
|
||||||
launch {
|
//wait asynchronously for it to finish
|
||||||
try {
|
launch {
|
||||||
mapTiles += deferred.await()
|
mapTiles += deferred.await()
|
||||||
} catch (ex: Exception) {
|
}
|
||||||
if (ex !is CancellationException) {
|
} catch (ex: Exception) {
|
||||||
//displaying the error is maps responsibility
|
if (ex !is CancellationException) {
|
||||||
logger.error(ex) { "Failed to load tile with id=$id" }
|
//displaying the error is maps responsibility
|
||||||
}
|
logger.error(ex) { "Failed to load tile with id=$id" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -232,6 +233,7 @@ public actual fun MapView(
|
|||||||
feature.size,
|
feature.size,
|
||||||
center = feature.center.toOffset()
|
center = feature.center.toOffset()
|
||||||
)
|
)
|
||||||
|
|
||||||
is MapRectangleFeature -> drawRect(
|
is MapRectangleFeature -> drawRect(
|
||||||
feature.color,
|
feature.color,
|
||||||
topLeft = feature.center.toOffset() - Offset(
|
topLeft = feature.center.toOffset() - Offset(
|
||||||
@ -240,6 +242,7 @@ public actual fun MapView(
|
|||||||
),
|
),
|
||||||
size = feature.size.toSize()
|
size = feature.size.toSize()
|
||||||
)
|
)
|
||||||
|
|
||||||
is MapLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
|
is MapLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
|
||||||
is MapArcFeature -> {
|
is MapArcFeature -> {
|
||||||
val topLeft = feature.oval.topLeft.toOffset()
|
val topLeft = feature.oval.topLeft.toOffset()
|
||||||
@ -252,6 +255,7 @@ public actual fun MapView(
|
|||||||
drawPath(path, color = feature.color, style = Stroke())
|
drawPath(path, color = feature.color, style = Stroke())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is MapBitmapImageFeature -> drawImage(feature.image, feature.position.toOffset())
|
is MapBitmapImageFeature -> drawImage(feature.image, feature.position.toOffset())
|
||||||
is MapVectorImageFeature -> {
|
is MapVectorImageFeature -> {
|
||||||
val offset = feature.position.toOffset()
|
val offset = feature.position.toOffset()
|
||||||
@ -262,6 +266,7 @@ public actual fun MapView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is MapTextFeature -> drawIntoCanvas { canvas ->
|
is MapTextFeature -> drawIntoCanvas { canvas ->
|
||||||
val offset = feature.position.toOffset()
|
val offset = feature.position.toOffset()
|
||||||
canvas.nativeCanvas.drawString(
|
canvas.nativeCanvas.drawString(
|
||||||
@ -272,17 +277,20 @@ public actual fun MapView(
|
|||||||
feature.color.toPaint()
|
feature.color.toPaint()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is MapDrawFeature -> {
|
is MapDrawFeature -> {
|
||||||
val offset = feature.position.toOffset()
|
val offset = feature.position.toOffset()
|
||||||
translate(offset.x, offset.y) {
|
translate(offset.x, offset.y) {
|
||||||
feature.drawFeature(this)
|
feature.drawFeature(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is MapFeatureGroup -> {
|
is MapFeatureGroup -> {
|
||||||
feature.children.values.forEach {
|
feature.children.values.forEach {
|
||||||
drawFeature(zoom, it)
|
drawFeature(zoom, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is MapPointsFeature -> {
|
is MapPointsFeature -> {
|
||||||
val points = feature.points.map { it.toOffset() }
|
val points = feature.points.map { it.toOffset() }
|
||||||
drawPoints(
|
drawPoints(
|
||||||
@ -292,6 +300,7 @@ public actual fun MapView(
|
|||||||
pointMode = feature.pointMode
|
pointMode = feature.pointMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
logger.error { "Unrecognized feature type: ${feature::class}" }
|
logger.error { "Unrecognized feature type: ${feature::class}" }
|
||||||
}
|
}
|
||||||
@ -313,13 +322,11 @@ public actual fun MapView(
|
|||||||
(canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale.toFloat()).roundToPx(),
|
(canvasSize.width / 2 + (mapTileProvider.toCoordinate(id.i).dp - centerCoordinates.x.dp) * tileScale.toFloat()).roundToPx(),
|
||||||
(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()
|
||||||
)
|
)
|
||||||
image?.let {
|
drawImage(
|
||||||
drawImage(
|
image = image,
|
||||||
image = it,
|
dstOffset = offset,
|
||||||
dstOffset = offset,
|
dstSize = tileSize
|
||||||
dstSize = tileSize
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
features.values.filter { zoom in it.zoomRange }.forEach { feature ->
|
features.values.filter { zoom in it.zoomRange }.forEach { feature ->
|
||||||
drawFeature(zoom, feature)
|
drawFeature(zoom, feature)
|
||||||
|
@ -2,11 +2,10 @@ package center.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.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.network.sockets.*
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.statement.readBytes
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.utils.io.CancellationException
|
||||||
import io.ktor.utils.io.*
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -25,11 +24,11 @@ import kotlin.io.path.*
|
|||||||
public class OpenStreetMapTileProvider(
|
public class OpenStreetMapTileProvider(
|
||||||
private val client: HttpClient,
|
private val client: HttpClient,
|
||||||
private val cacheDirectory: Path,
|
private val cacheDirectory: Path,
|
||||||
parallelism: Int = 1,
|
parallelism: Int = 4,
|
||||||
cacheCapacity: Int = 200,
|
cacheCapacity: Int = 200,
|
||||||
) : 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<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")
|
||||||
|
|
||||||
@ -38,7 +37,7 @@ public class OpenStreetMapTileProvider(
|
|||||||
/**
|
/**
|
||||||
* 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<ImageBitmap> = async(Dispatchers.IO) {
|
||||||
|
|
||||||
id.cacheFilePath()?.let { path ->
|
id.cacheFilePath()?.let { path ->
|
||||||
if (path.exists()) {
|
if (path.exists()) {
|
||||||
@ -53,22 +52,17 @@ public class OpenStreetMapTileProvider(
|
|||||||
|
|
||||||
//semaphore works only for actual download
|
//semaphore works only for actual download
|
||||||
semaphore.withPermit {
|
semaphore.withPermit {
|
||||||
try {
|
val url = id.osmUrl()
|
||||||
val url = id.osmUrl()
|
val byteArray = client.get(url).readBytes()
|
||||||
val byteArray = client.get(url).readBytes()
|
logger.debug { "Finished downloading map tile with id $id from $url" }
|
||||||
logger.debug { "Finished downloading map tile with id $id from $url" }
|
id.cacheFilePath()?.let { path ->
|
||||||
id.cacheFilePath()?.let { path ->
|
logger.debug { "Caching map tile $id to $path" }
|
||||||
logger.debug { "Caching map tile $id to $path" }
|
|
||||||
|
|
||||||
path.parent.createDirectories()
|
path.parent.createDirectories()
|
||||||
path.writeBytes(byteArray)
|
path.writeBytes(byteArray)
|
||||||
}
|
|
||||||
|
|
||||||
Image.makeFromEncoded(byteArray).toComposeImageBitmap()
|
|
||||||
} catch (e: ConnectTimeoutException) {
|
|
||||||
logger.error(e) { e.localizedMessage }
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Image.makeFromEncoded(byteArray).toComposeImageBitmap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +77,7 @@ public class OpenStreetMapTileProvider(
|
|||||||
|
|
||||||
//collect the result asynchronously
|
//collect the result asynchronously
|
||||||
return async {
|
return async {
|
||||||
val image = try {
|
val image: ImageBitmap = try {
|
||||||
imageDeferred.await()
|
imageDeferred.await()
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
cache.remove(tileId)
|
cache.remove(tileId)
|
||||||
|
Loading…
Reference in New Issue
Block a user