From 4b512d0967622e76ecea34990d430f18181417ec Mon Sep 17 00:00:00 2001 From: "a.kalmakhanov" Date: Sun, 21 Aug 2022 22:36:56 +0600 Subject: [PATCH 1/2] ConnectTimeoutException handling enabled TextFeature multiplatform capability enabled --- .../center/sciprog/maps/compose/MapFeature.kt | 12 +++++- .../sciprog/maps/compose/MapFeatureBuilder.kt | 20 +++++++++- .../maps/compose/MapTextFeatureFont.kt | 5 +++ .../sciprog/maps/compose/MapTileProvider.kt | 2 +- .../sciprog/maps/compose/MapTextFeature.kt | 35 ---------------- .../maps/compose/MapTextFeatureFontJvm.kt | 5 +++ .../center/sciprog/maps/compose/MapViewJvm.kt | 12 +++--- .../maps/compose/OpenStreetMapTileProvider.kt | 40 ++++++++++--------- 8 files changed, 70 insertions(+), 61 deletions(-) create mode 100644 maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFont.kt delete mode 100644 maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt create mode 100644 maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFontJvm.kt diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt index 202e30b..f235db8 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeature.kt @@ -132,4 +132,14 @@ public class MapFeatureGroup( override val zoomRange: IntRange = defaultZoomRange, ) : MapFeature { override fun getBoundingBox(zoom: Int): GmcBox? = children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll() -} \ No newline at end of file +} + +public class MapTextFeature( + public val position: GeodeticMapCoordinates, + public val text: String, + override val zoomRange: IntRange = defaultZoomRange, + public val color: Color, + public val fontConfig: MapTextFeatureFont.() -> Unit, +) : MapFeature { + override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(position, position) +} diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeatureBuilder.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeatureBuilder.kt index e916f12..c11a0b6 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeatureBuilder.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapFeatureBuilder.kt @@ -143,4 +143,22 @@ public fun MapFeatureBuilder.group( val map = MapFeatureBuilderImpl(emptyMap()).apply(builder).build() val feature = MapFeatureGroup(map, zoomRange) return addFeature(id, feature) -} \ No newline at end of file +} + +public fun MapFeatureBuilder.text( + position: GeodeticMapCoordinates, + text: String, + zoomRange: IntRange = defaultZoomRange, + color: Color = Color.Red, + font: MapTextFeatureFont.() -> Unit = { size = 16f }, + id: FeatureId? = null, +): FeatureId = addFeature(id, MapTextFeature(position, text, zoomRange, color, font)) + +public fun MapFeatureBuilder.text( + position: Pair, + text: String, + zoomRange: IntRange = defaultZoomRange, + color: Color = Color.Red, + font: MapTextFeatureFont.() -> Unit = { size = 16f }, + id: FeatureId? = null, +): FeatureId = addFeature(id, MapTextFeature(position.toCoordinates(), text, zoomRange, color, font)) diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFont.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFont.kt new file mode 100644 index 0000000..defe478 --- /dev/null +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFont.kt @@ -0,0 +1,5 @@ +package center.sciprog.maps.compose + +public expect class MapTextFeatureFont { + public var size: Float +} \ 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..f205993 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 @@ -13,7 +13,7 @@ public data class TileId( public data class MapTile( val id: TileId, - val image: ImageBitmap, + val image: ImageBitmap?, ) public interface MapTileProvider { diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt deleted file mode 100644 index fef40fb..0000000 --- a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeature.kt +++ /dev/null @@ -1,35 +0,0 @@ -package center.sciprog.maps.compose - -import androidx.compose.ui.graphics.Color -import center.sciprog.maps.coordinates.GeodeticMapCoordinates -import center.sciprog.maps.coordinates.GmcBox -import org.jetbrains.skia.Font - - -public class MapTextFeature( - public val position: GeodeticMapCoordinates, - public val text: String, - override val zoomRange: IntRange = defaultZoomRange, - public val color: Color, - public val fontConfig: Font.() -> Unit, -) : MapFeature { - override fun getBoundingBox(zoom: Int): GmcBox = GmcBox(position, position) -} - -public fun MapFeatureBuilder.text( - position: GeodeticMapCoordinates, - text: String, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - font: Font.() -> Unit = { size = 16f }, - id: FeatureId? = null, -): FeatureId = addFeature(id, MapTextFeature(position, text, zoomRange, color, font)) - -public fun MapFeatureBuilder.text( - position: Pair, - text: String, - zoomRange: IntRange = defaultZoomRange, - color: Color = Color.Red, - font: Font.() -> Unit = { size = 16f }, - id: FeatureId? = null, -): FeatureId = addFeature(id, MapTextFeature(position.toCoordinates(), text, zoomRange, color, font)) diff --git a/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFontJvm.kt b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFontJvm.kt new file mode 100644 index 0000000..1a2023a --- /dev/null +++ b/maps-kt-compose/src/jvmMain/kotlin/center/sciprog/maps/compose/MapTextFeatureFontJvm.kt @@ -0,0 +1,5 @@ +package center.sciprog.maps.compose + +import org.jetbrains.skia.Font + +public actual typealias MapTextFeatureFont = Font \ No newline at end of file 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 27f9496..f080b3b 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 @@ -313,11 +313,13 @@ public actual fun MapView( (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() ) - drawImage( - image = image, - dstOffset = offset, - dstSize = tileSize - ) + image?.let { + drawImage( + image = it, + dstOffset = offset, + dstSize = tileSize + ) + } } features.values.filter { zoom in it.zoomRange }.forEach { feature -> drawFeature(zoom, feature) 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 4929254..f350279 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 @@ -2,10 +2,11 @@ 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 io.ktor.client.* +import io.ktor.client.network.sockets.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.utils.io.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers @@ -28,7 +29,7 @@ public class OpenStreetMapTileProvider( cacheCapacity: Int = 200, ) : 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") @@ -37,7 +38,7 @@ public class OpenStreetMapTileProvider( /** * Download and cache the tile image */ - private fun CoroutineScope.downloadImageAsync(id: TileId) = async(Dispatchers.IO) { + private fun CoroutineScope.downloadImageAsync(id: TileId): Deferred = async(Dispatchers.IO) { id.cacheFilePath()?.let { path -> if (path.exists()) { @@ -52,19 +53,22 @@ public class OpenStreetMapTileProvider( //semaphore works only for actual download semaphore.withPermit { - val url = id.osmUrl() - val byteArray = client.get(url).readBytes() + try { + val url = id.osmUrl() + val byteArray = client.get(url).readBytes() + logger.debug { "Finished downloading map tile with id $id from $url" } + id.cacheFilePath()?.let { path -> + logger.debug { "Caching map tile $id to $path" } - logger.debug { "Finished downloading map tile with id $id from $url" } + path.parent.createDirectories() + path.writeBytes(byteArray) + } - id.cacheFilePath()?.let { path -> - logger.debug { "Caching map tile $id to $path" } - - path.parent.createDirectories() - path.writeBytes(byteArray) + Image.makeFromEncoded(byteArray).toComposeImageBitmap() + } catch (e: ConnectTimeoutException) { + logger.error(e) { e.localizedMessage } + null } - - Image.makeFromEncoded(byteArray).toComposeImageBitmap() } } @@ -83,7 +87,7 @@ public class OpenStreetMapTileProvider( imageDeferred.await() } catch (ex: Exception) { cache.remove(tileId) - if(ex !is CancellationException) { + if (ex !is CancellationException) { logger.error(ex) { "Failed to load tile image with id=$tileId" } } throw ex @@ -96,4 +100,4 @@ public class OpenStreetMapTileProvider( public companion object { private val logger = KotlinLogging.logger("OpenStreetMapCache") } -} +} \ No newline at end of file From 3467a6dbe03953299f9f25d28a2d893e2ba5376c Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 26 Aug 2022 14:22:17 +0300 Subject: [PATCH 2/2] Refactor error management --- .../sciprog/maps/compose/MapTileProvider.kt | 2 +- .../center/sciprog/maps/compose/MapViewJvm.kt | 41 +++++++++++-------- .../maps/compose/OpenStreetMapTileProvider.kt | 40 ++++++++---------- 3 files changed, 42 insertions(+), 41 deletions(-) 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 f205993..6a71511 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 @@ -13,7 +13,7 @@ public data class TileId( public data class MapTile( val id: TileId, - val image: ImageBitmap?, + val image: ImageBitmap, ) 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 f080b3b..9c6443f 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 @@ -195,20 +195,21 @@ public actual fun MapView( for (j in verticalIndices) { for (i in horizontalIndices) { val id = TileId(zoom, i, j) - //start all - val deferred = loadTileAsync(id) - //wait asynchronously for it to finish - launch { - try { + try { + //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" } - } + } + } catch (ex: Exception) { + if (ex !is CancellationException) { + //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, center = feature.center.toOffset() ) + is MapRectangleFeature -> drawRect( feature.color, topLeft = feature.center.toOffset() - Offset( @@ -240,6 +242,7 @@ public actual fun MapView( ), size = feature.size.toSize() ) + is MapLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset()) is MapArcFeature -> { val topLeft = feature.oval.topLeft.toOffset() @@ -252,6 +255,7 @@ public actual fun MapView( drawPath(path, color = feature.color, style = Stroke()) } + is MapBitmapImageFeature -> drawImage(feature.image, feature.position.toOffset()) is MapVectorImageFeature -> { val offset = feature.position.toOffset() @@ -262,6 +266,7 @@ public actual fun MapView( } } } + is MapTextFeature -> drawIntoCanvas { canvas -> val offset = feature.position.toOffset() canvas.nativeCanvas.drawString( @@ -272,17 +277,20 @@ public actual fun MapView( feature.color.toPaint() ) } + is MapDrawFeature -> { val offset = feature.position.toOffset() translate(offset.x, offset.y) { feature.drawFeature(this) } } + is MapFeatureGroup -> { feature.children.values.forEach { drawFeature(zoom, it) } } + is MapPointsFeature -> { val points = feature.points.map { it.toOffset() } drawPoints( @@ -292,6 +300,7 @@ public actual fun MapView( pointMode = feature.pointMode ) } + else -> { 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.height / 2 + (mapTileProvider.toCoordinate(id.j).dp - centerCoordinates.y.dp) * tileScale.toFloat()).roundToPx() ) - image?.let { - drawImage( - image = it, - dstOffset = offset, - dstSize = tileSize - ) - } + drawImage( + image = image, + dstOffset = offset, + dstSize = tileSize + ) } features.values.filter { zoom in it.zoomRange }.forEach { feature -> drawFeature(zoom, feature) 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 f350279..4a98ae4 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 @@ -2,11 +2,10 @@ package center.sciprog.maps.compose import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.toComposeImageBitmap -import io.ktor.client.* -import io.ktor.client.network.sockets.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.utils.io.* +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 @@ -25,11 +24,11 @@ import kotlin.io.path.* public class OpenStreetMapTileProvider( private val client: HttpClient, private val cacheDirectory: Path, - parallelism: Int = 1, + parallelism: Int = 4, cacheCapacity: Int = 200, ) : 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") @@ -38,7 +37,7 @@ public class OpenStreetMapTileProvider( /** * 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()) { @@ -53,22 +52,17 @@ public class OpenStreetMapTileProvider( //semaphore works only for actual download semaphore.withPermit { - try { - val url = id.osmUrl() - val byteArray = client.get(url).readBytes() - logger.debug { "Finished downloading map tile with id $id from $url" } - id.cacheFilePath()?.let { path -> - logger.debug { "Caching map tile $id to $path" } + val url = id.osmUrl() + val byteArray = client.get(url).readBytes() + logger.debug { "Finished downloading map tile with id $id from $url" } + id.cacheFilePath()?.let { path -> + logger.debug { "Caching map tile $id to $path" } - path.parent.createDirectories() - path.writeBytes(byteArray) - } - - Image.makeFromEncoded(byteArray).toComposeImageBitmap() - } catch (e: ConnectTimeoutException) { - logger.error(e) { e.localizedMessage } - null + path.parent.createDirectories() + path.writeBytes(byteArray) } + + Image.makeFromEncoded(byteArray).toComposeImageBitmap() } } @@ -83,7 +77,7 @@ public class OpenStreetMapTileProvider( //collect the result asynchronously return async { - val image = try { + val image: ImageBitmap = try { imageDeferred.await() } catch (ex: Exception) { cache.remove(tileId)