From 5b95adc64962b546fc0f94be7aa53800bfeec63f Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 25 Dec 2022 11:07:45 +0300 Subject: [PATCH] Generalize feature draw logic --- .../center/sciprog/maps/compose/MapState.kt | 18 +-- .../center/sciprog/maps/compose/MapViewJvm.kt | 124 ++---------------- .../maps/coordinates/WebMercatorProjection.kt | 6 +- .../maps/features/CoordinateViewState.kt | 52 ++++++++ .../center/sciprog/maps/features/Feature.kt | 5 +- .../sciprog/maps/features/ViewConfig.kt | 0 .../sciprog/maps/features/drawFeature.kt | 124 ++++++++++++++++++ .../sciprog/maps/features/mapControls.kt | 41 +----- 8 files changed, 202 insertions(+), 168 deletions(-) create mode 100644 maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewState.kt rename maps-kt-features/src/{jvmMain => commonMain}/kotlin/center/sciprog/maps/features/ViewConfig.kt (100%) create mode 100644 maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapState.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapState.kt index 4fc494e..c9c992c 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapState.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapState.kt @@ -2,10 +2,7 @@ package center.sciprog.maps.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.* import center.sciprog.maps.coordinates.* import center.sciprog.maps.features.* import kotlin.math.* @@ -18,9 +15,6 @@ internal class MapState internal constructor( ) : CoordinateViewState(config, canvasSize, viewPoint) { override val space: CoordinateSpace get() = GmcCoordinateSpace - public val zoom: Int - get() = floor(viewPoint.zoom).toInt() - public val scaleFactor: Double get() = WebMercatorProjection.scaleFactor(viewPoint.zoom) @@ -31,7 +25,7 @@ internal class MapState internal constructor( get() = 2.0.pow(viewPoint.zoom - zoom) private fun DpOffset.toMercator(): WebMercatorCoordinates = WebMercatorCoordinates( - zoom, + floor(zoom).toInt(), (x - canvasSize.width / 2).value / tileScale + centerCoordinates.x, (y - canvasSize.height / 2).value / tileScale + centerCoordinates.y, ) @@ -50,6 +44,12 @@ internal class MapState internal constructor( override fun Gmc.toDpOffset(): DpOffset = WebMercatorProjection.toMercator(this, zoom).toOffset() + override fun Rectangle.toDpRect(): DpRect { + val topLeft = topLeft.toDpOffset() + val bottomRight = bottomRight.toDpOffset() + return DpRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y) + } + override fun viewPointFor(rectangle: Rectangle): ViewPoint { val zoom = log2( min( @@ -80,6 +80,6 @@ internal fun rememberMapState( canvasSize: DpSize, viewPoint: ViewPoint, tileSize: Int, -):MapState = remember { +): MapState = remember { MapState(config, canvasSize, viewPoint, tileSize) } \ 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 eec17b8..e351689 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 @@ -4,22 +4,21 @@ import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.drawscope.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.graphics.vector.VectorPainter import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -import center.sciprog.maps.coordinates.GeodeticMapCoordinates import center.sciprog.maps.coordinates.Gmc -import center.sciprog.maps.coordinates.radians -import center.sciprog.maps.coordinates.toFloat import center.sciprog.maps.features.* import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import mu.KotlinLogging -import org.jetbrains.skia.Font import org.jetbrains.skia.Paint import kotlin.math.* @@ -51,8 +50,6 @@ public actual fun MapView( initialViewPoint, mapTileProvider.tileSize ) - val canvasModifier = modifier.mapControls(state).fillMaxSize() - with(state) { val mapTiles = remember(mapTileProvider) { mutableStateListOf() } @@ -74,7 +71,7 @@ public actual fun MapView( for (j in verticalIndices) { for (i in horizontalIndices) { - val id = TileId(zoom, i, j) + val id = TileId(floor(zoom).toInt(), i, j) //ensure that failed tiles do not fail the application supervisorScope { //start all @@ -96,110 +93,11 @@ public actual fun MapView( } } - val painterCache = key(featuresState) { + val painterCache: Map, VectorPainter> = key(featuresState) { featuresState.features.values.filterIsInstance>().associateWith { it.painter() } } - Canvas(canvasModifier) { - fun GeodeticMapCoordinates.toOffset(): Offset = toOffset(this@Canvas) - - fun DrawScope.drawFeature(zoom: Int, feature: MapFeature) { - when (feature) { - is FeatureSelector -> drawFeature(zoom, feature.selector(zoom)) - is CircleFeature -> drawCircle( - feature.color, - feature.size.toPx(), - center = feature.center.toOffset() - ) - - is RectangleFeature -> drawRect( - feature.color, - topLeft = feature.center.toOffset() - Offset( - feature.size.width.toPx() / 2, - feature.size.height.toPx() / 2 - ), - size = feature.size.toSize() - ) - - is LineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset()) - is ArcFeature -> { - val topLeft = feature.oval.topLeft.toOffset() - val bottomRight = feature.oval.bottomRight.toOffset() - - val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y)) - - drawArc( - color = feature.color, - startAngle = feature.startAngle.radians.degrees.toFloat(), - sweepAngle = feature.arcLength.radians.degrees.toFloat(), - useCenter = false, - topLeft = topLeft, - size = size, - style = Stroke() - ) - - } - - is BitmapImageFeature -> drawImage(feature.image, feature.position.toOffset()) - - is VectorImageFeature -> { - val offset = feature.position.toOffset() - val size = feature.size.toSize() - translate(offset.x - size.width / 2, offset.y - size.height / 2) { - with(painterCache[feature]!!) { - draw(size) - } - } - } - - is TextFeature -> drawIntoCanvas { canvas -> - val offset = feature.position.toOffset() - canvas.nativeCanvas.drawString( - feature.text, - offset.x + 5, - offset.y - 5, - Font().apply(feature.fontConfig), - feature.color.toPaint() - ) - } - - is DrawFeature -> { - val offset = feature.position.toOffset() - translate(offset.x, offset.y) { - feature.drawFeature(this) - } - } - - is FeatureGroup -> { - feature.children.values.forEach { - drawFeature(zoom, it) - } - } - - is PathFeature -> { - TODO("MapPathFeature not implemented") -// val offset = feature.rectangle.center.toOffset() - feature.targetRect.center -// translate(offset.x, offset.y) { -// sca -// drawPath(feature.path, brush = feature.brush, style = feature.style) -// } - } - - is PointsFeature -> { - val points = feature.points.map { it.toOffset() } - drawPoints( - points = points, - color = feature.color, - strokeWidth = feature.stroke, - pointMode = feature.pointMode - ) - } - -// else -> { -// logger.error { "Unrecognized feature type: ${feature::class}" } -// } - } - } + Canvas(modifier = modifier.mapControls(state).fillMaxSize()) { if (canvasSize != size.toDpSize()) { logger.debug { "Recalculate canvas. Size: $size" } @@ -226,7 +124,7 @@ public actual fun MapView( } featuresState.features.values.filter { viewPoint.zoom in it.zoomRange }.forEach { feature -> - drawFeature(zoom, feature) + drawFeature(state, painterCache, feature) } } diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt index d29e4c4..a1de7b4 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt +++ b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt @@ -26,12 +26,12 @@ public object WebMercatorProjection { /** * https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas */ - public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Int): WebMercatorCoordinates { + public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Double): WebMercatorCoordinates { require(abs(gmc.latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" } - val scaleFactor = scaleFactor(zoom.toDouble()) + val scaleFactor = scaleFactor(zoom) return WebMercatorCoordinates( - zoom = zoom, + zoom = floor(zoom).toInt(), x = scaleFactor * (gmc.longitude.radians.value + PI), y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians.value / 2))) ) diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewState.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewState.kt new file mode 100644 index 0000000..430e09b --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateViewState.kt @@ -0,0 +1,52 @@ +package center.sciprog.maps.features + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.* + +public abstract class CoordinateViewState( + public val config: ViewConfig, + canvasSize: DpSize, + viewPoint: ViewPoint, +) { + + public abstract val space: CoordinateSpace + + public var canvasSize: DpSize by mutableStateOf(canvasSize) + protected var viewPointState: MutableState> = mutableStateOf(viewPoint) + + public var viewPoint: ViewPoint + get() = viewPointState.value + set(value) { + config.onViewChange(value) + viewPointState.value = value + } + + public val zoom: Double get() = viewPoint.zoom + + public abstract fun DpOffset.toCoordinates(): T + + public abstract fun T.toDpOffset(): DpOffset + + public fun T.toOffset(density: Density): Offset = with(density){ + val dpOffset = this@toOffset.toDpOffset() + Offset(dpOffset.x.toPx(), dpOffset.y.toPx()) + } + + public abstract fun Rectangle.toDpRect(): DpRect + + public abstract fun ViewPoint.moveBy(x: Dp, y: Dp): ViewPoint + + public abstract fun viewPointFor(rectangle: Rectangle): ViewPoint + + // Selection rectangle. If null - no selection + public var selectRect: DpRect? by mutableStateOf(null) +} + + +public val DpRect.topLeft: DpOffset get() = DpOffset(left, top) + +public val DpRect.bottomRight: DpOffset get() = DpOffset(right, bottom) \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt index 37b63d5..10c2153 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import center.sciprog.maps.features.Feature.Companion.defaultZoomRange -import kotlin.math.floor public typealias DoubleRange = ClosedFloatingPointRange @@ -60,11 +59,11 @@ public fun Iterable>.computeBoundingBox( public class FeatureSelector( override val space: CoordinateSpace, override var attributes: AttributeMap = AttributeMap(), - public val selector: (zoom: Int) -> Feature, + public val selector: (zoom: Double) -> Feature, ) : Feature { override val zoomRange: ClosedFloatingPointRange get() = defaultZoomRange - override fun getBoundingBox(zoom: Double): Rectangle? = selector(floor(zoom).toInt()).getBoundingBox(zoom) + override fun getBoundingBox(zoom: Double): Rectangle? = selector(zoom).getBoundingBox(zoom) } public class PathFeature( diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/ViewConfig.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt similarity index 100% rename from maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/ViewConfig.kt rename to maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewConfig.kt diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt new file mode 100644 index 0000000..e32d49d --- /dev/null +++ b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt @@ -0,0 +1,124 @@ +package center.sciprog.maps.features + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.graphics.vector.VectorPainter +import org.jetbrains.skia.Font +import org.jetbrains.skia.Paint +import kotlin.math.PI + + +internal fun Color.toPaint(): Paint = Paint().apply { + isAntiAlias = true + color = toArgb() +} + +public fun DrawScope.drawFeature( + state: CoordinateViewState, + painterCache: Map, VectorPainter>, + feature: Feature, +): Unit = with(state) { + fun T.toOffset(): Offset = toOffset(this@drawFeature) + + when (feature) { + is FeatureSelector -> drawFeature(state, painterCache, feature.selector(state.zoom)) + is CircleFeature -> drawCircle( + feature.color, + feature.size.toPx(), + center = feature.center.toOffset() + ) + + is RectangleFeature -> drawRect( + feature.color, + topLeft = feature.center.toOffset() - Offset( + feature.size.width.toPx() / 2, + feature.size.height.toPx() / 2 + ), + size = feature.size.toSize() + ) + + is LineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset()) + is ArcFeature -> { + val dpRect = feature.oval.toDpRect().toRect() + + val size = Size(dpRect.width, dpRect.height) + + drawArc( + color = feature.color, + startAngle = feature.startAngle / PI.toFloat() * 180f, + sweepAngle = feature.arcLength / PI.toFloat() * 180f, + useCenter = false, + topLeft = dpRect.topLeft, + size = size, + style = Stroke() + ) + + } + + is BitmapImageFeature -> drawImage(feature.image, feature.position.toOffset()) + + is VectorImageFeature -> { + val offset = feature.position.toOffset() + val size = feature.size.toSize() + translate(offset.x - size.width / 2, offset.y - size.height / 2) { + with(painterCache[feature]!!) { + draw(size) + } + } + } + + is TextFeature -> drawIntoCanvas { canvas -> + val offset = feature.position.toOffset() + canvas.nativeCanvas.drawString( + feature.text, + offset.x + 5, + offset.y - 5, + Font().apply(feature.fontConfig), + feature.color.toPaint() + ) + } + + is DrawFeature -> { + val offset = feature.position.toOffset() + translate(offset.x, offset.y) { + feature.drawFeature(this) + } + } + + is FeatureGroup -> { + feature.children.values.forEach { + drawFeature(state, painterCache, it) + } + } + + is PathFeature -> { + TODO("MapPathFeature not implemented") +// val offset = feature.rectangle.center.toOffset() - feature.targetRect.center +// translate(offset.x, offset.y) { +// sca +// drawPath(feature.path, brush = feature.brush, style = feature.style) +// } + } + + is PointsFeature -> { + val points = feature.points.map { it.toOffset() } + drawPoints( + points = points, + color = feature.color, + strokeWidth = feature.stroke, + pointMode = feature.pointMode + ) + } + + else -> { + //logger.error { "Unrecognized feature type: ${feature::class}" } + } + } +} \ No newline at end of file diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/mapControls.kt b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/mapControls.kt index 0a8eb6c..2ee7c16 100644 --- a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/mapControls.kt +++ b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/mapControls.kt @@ -12,45 +12,6 @@ import kotlin.math.max import kotlin.math.min -public abstract class CoordinateViewState( - public val config: ViewConfig, - canvasSize: DpSize, - viewPoint: ViewPoint, -) { - - public abstract val space: CoordinateSpace - - public var canvasSize: DpSize by mutableStateOf(canvasSize) - protected var viewPointState: MutableState> = mutableStateOf(viewPoint) - - public var viewPoint: ViewPoint - get() = viewPointState.value - set(value) { - config.onViewChange(value) - viewPointState.value = value - } - - public abstract fun DpOffset.toCoordinates(): T - - public abstract fun T.toDpOffset(): DpOffset - - public fun T.toOffset(density: Density): Offset = with(density){ - val dpOffset = this@toOffset.toDpOffset() - Offset(dpOffset.x.toPx(), dpOffset.y.toPx()) - } - - public abstract fun ViewPoint.moveBy(x: Dp, y: Dp): ViewPoint - - public abstract fun viewPointFor(rectangle: Rectangle): ViewPoint - - // Selection rectangle. If null - no selection - public var selectRect: DpRect? by mutableStateOf(null) -} - -public val DpRect.topLeft: DpOffset get() = DpOffset(left, top) -public val DpRect.bottomRight: DpOffset get() = DpOffset(right, bottom) - - @OptIn(ExperimentalComposeUiApi::class) public fun Modifier.mapControls( state: CoordinateViewState, @@ -64,7 +25,7 @@ public fun Modifier.mapControls( event.changes.forEach { change -> val dragStart = change.position - val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp()) + //val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp()) //start selection val selectionStart: Offset? =