diff --git a/build.gradle.kts b/build.gradle.kts index 97b1ecf..77f7973 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ val ktorVersion by extra("2.0.3") allprojects { group = "center.sciprog" - version = "0.1.0-dev-7" + version = "0.1.0-dev-8" } ksciencePublish{ diff --git a/demo/scheme/src/jvmMain/kotlin/Main.kt b/demo/scheme/src/jvmMain/kotlin/Main.kt index 6e5cc86..7051add 100644 --- a/demo/scheme/src/jvmMain/kotlin/Main.kt +++ b/demo/scheme/src/jvmMain/kotlin/Main.kt @@ -2,7 +2,6 @@ import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource @@ -17,26 +16,17 @@ import kotlinx.coroutines.launch @Preview fun App() { MaterialTheme { - //create a view point - val viewPoint = remember { - SchemeViewPoint( - SchemeCoordinates(0f, 0f), - 1f - ) - } val scope = rememberCoroutineScope() SchemeView( - viewPoint, config = SchemeViewConfig( - inferViewBoxFromFeatures = true, onClick = { println("${focus.x}, ${focus.y}") } ) ) { - background(painterResource("middle-earth.jpg")) + background(1600f, 1200f) { painterResource("middle-earth.jpg") } circle(410.52737 to 868.7676, color = Color.Blue) text(410.52737 to 868.7676, "Shire", color = Color.Blue) circle(1132.0881 to 394.99127, color = Color.Red) 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 d7fa804..036b96f 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 @@ -216,8 +216,9 @@ public actual fun MapView( } } - val painterCache = featuresState.features().values.filterIsInstance() - .associateWith { it.painter() } + val painterCache = key(featuresState) { + featuresState.features().values.filterIsInstance().associateWith { it.painter() } + } Canvas(canvasModifier) { fun WebMercatorCoordinates.toOffset(): Offset = Offset( diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeCoordinates.kt b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeCoordinates.kt index 6090ed8..6d8f631 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeCoordinates.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeCoordinates.kt @@ -1,37 +1,54 @@ package center.sciprog.maps.scheme +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp import kotlin.math.abs import kotlin.math.max import kotlin.math.min data class SchemeCoordinates(val x: Float, val y: Float) -data class SchemeCoordinateBox( +data class SchemeRectangle( val a: SchemeCoordinates, val b: SchemeCoordinates, ) -val SchemeCoordinateBox.top get() = max(a.y, b.y) -val SchemeCoordinateBox.bottom get() = min(a.y, b.y) +val SchemeRectangle.top get() = max(a.y, b.y) +val SchemeRectangle.bottom get() = min(a.y, b.y) -val SchemeCoordinateBox.right get() = max(a.x, b.x) -val SchemeCoordinateBox.left get() = min(a.x, b.x) +val SchemeRectangle.right get() = max(a.x, b.x) +val SchemeRectangle.left get() = min(a.x, b.x) -val SchemeCoordinateBox.width get() = abs(a.x - b.x) -val SchemeCoordinateBox.height get() = abs(a.y - b.y) +val SchemeRectangle.width get() = abs(a.x - b.x) +val SchemeRectangle.height get() = abs(a.y - b.y) -val SchemeCoordinateBox.center get() = SchemeCoordinates((a.x + b.x) / 2, (a.y + b.y) / 2) +val SchemeRectangle.center get() = SchemeCoordinates((a.x + b.x) / 2, (a.y + b.y) / 2) +public val SchemeRectangle.topLeft: SchemeCoordinates get() = SchemeCoordinates(top, left) +public val SchemeRectangle.bottomRight: SchemeCoordinates get() = SchemeCoordinates(bottom, right) -fun Collection.wrapAll(): SchemeCoordinateBox? { +fun Collection.wrapAll(): SchemeRectangle? { if (isEmpty()) return null val minX = minOf { it.left } val maxX = maxOf { it.right } val minY = minOf { it.bottom } val maxY = maxOf { it.top } - return SchemeCoordinateBox( + return SchemeRectangle( SchemeCoordinates(minX, minY), SchemeCoordinates(maxX, maxY) ) +} + +internal val defaultCanvasSize = DpSize(512.dp, 512.dp) + +public fun SchemeRectangle.computeViewPoint( + canvasSize: DpSize = defaultCanvasSize, +): SchemeViewPoint { + val scale = min( + canvasSize.width.value / width, + canvasSize.height.value / height + ) + + return SchemeViewPoint(center, scale) } \ No newline at end of file diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeFeature.kt b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeFeature.kt index 8362531..7a2bdd0 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeFeature.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeFeature.kt @@ -15,7 +15,7 @@ import center.sciprog.maps.scheme.SchemeFeature.Companion.defaultScaleRange internal typealias FloatRange = ClosedFloatingPointRange sealed class SchemeFeature(val scaleRange: FloatRange) { - abstract fun getBoundingBox(scale: Float): SchemeCoordinateBox? + abstract fun getBoundingBox(scale: Float): SchemeRectangle? companion object { val defaultScaleRange = 0f..Float.MAX_VALUE @@ -23,27 +23,31 @@ sealed class SchemeFeature(val scaleRange: FloatRange) { } -fun Iterable.computeBoundingBox(scale: Float): SchemeCoordinateBox? = +fun Iterable.computeBoundingBox(scale: Float): SchemeRectangle? = mapNotNull { it.getBoundingBox(scale) }.wrapAll() internal fun Pair.toCoordinates() = SchemeCoordinates(first.toFloat(), second.toFloat()) +interface PainterFeature { + val painter: @Composable () -> Painter +} + /** * A background image that is bound to scheme coordinates and is scaled together with them * - * @param position the size of background in scheme size units. The screen units to scheme units ratio equals scale. + * @param rectangle the size of background in scheme size units. The screen units to scheme units ratio equals scale. */ class SchemeBackgroundFeature( - val position: SchemeCoordinateBox, - val painter: Painter, + val rectangle: SchemeRectangle, scaleRange: FloatRange = defaultScaleRange, -) : SchemeFeature(scaleRange) { - override fun getBoundingBox(scale: Float): SchemeCoordinateBox = position + override val painter: @Composable () -> Painter, +) : SchemeFeature(scaleRange), PainterFeature { + override fun getBoundingBox(scale: Float): SchemeRectangle = rectangle } class SchemeFeatureSelector(val selector: (scale: Float) -> SchemeFeature) : SchemeFeature(defaultScaleRange) { - override fun getBoundingBox(scale: Float): SchemeCoordinateBox? = selector(scale).getBoundingBox(scale) + override fun getBoundingBox(scale: Float): SchemeRectangle? = selector(scale).getBoundingBox(scale) } class SchemeDrawFeature( @@ -51,7 +55,7 @@ class SchemeDrawFeature( scaleRange: FloatRange = defaultScaleRange, val drawFeature: DrawScope.() -> Unit, ) : SchemeFeature(scaleRange) { - override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position) + override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position) } class SchemeCircleFeature( @@ -60,7 +64,7 @@ class SchemeCircleFeature( val size: Float = 5f, val color: Color = Color.Red, ) : SchemeFeature(scaleRange) { - override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(center, center) + override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(center, center) } class SchemeLineFeature( @@ -69,7 +73,21 @@ class SchemeLineFeature( scaleRange: FloatRange = defaultScaleRange, val color: Color = Color.Red, ) : SchemeFeature(scaleRange) { - override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(a, b) + override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(a, b) +} + +/** + * @param startAngle the angle in radians from parallel downwards for the start of the arc + * @param arcLength arc length in radians + */ +public class SchemeArcFeature( + public val oval: SchemeRectangle, + public val startAngle: Float, + public val arcLength: Float, + scaleRange: FloatRange = defaultScaleRange, + public val color: Color = Color.Red, +) : SchemeFeature(scaleRange) { + override fun getBoundingBox(scale: Float): SchemeRectangle = oval } class SchemeTextFeature( @@ -78,7 +96,7 @@ class SchemeTextFeature( scaleRange: FloatRange = defaultScaleRange, val color: Color = Color.Red, ) : SchemeFeature(scaleRange) { - override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position) + override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position) } class SchemeBitmapFeature( @@ -87,25 +105,24 @@ class SchemeBitmapFeature( val size: IntSize = IntSize(15, 15), scaleRange: FloatRange = defaultScaleRange, ) : SchemeFeature(scaleRange) { - override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position) + override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position) } class SchemeImageFeature( val position: SchemeCoordinates, - val painter: Painter, val size: DpSize, scaleRange: FloatRange = defaultScaleRange, -) : SchemeFeature(scaleRange) { - override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position) + override val painter: @Composable () -> Painter, +) : SchemeFeature(scaleRange), PainterFeature { + override fun getBoundingBox(scale: Float): SchemeRectangle = SchemeRectangle(position, position) } -@Composable -fun SchemeVectorImageFeature( +fun SchemeImageFeature( position: SchemeCoordinates, image: ImageVector, size: DpSize = DpSize(20.dp, 20.dp), scaleRange: FloatRange = defaultScaleRange, -): SchemeImageFeature = SchemeImageFeature(position, rememberVectorPainter(image), size, scaleRange) +): SchemeImageFeature = SchemeImageFeature(position, size, scaleRange) { rememberVectorPainter(image) } /** * A group of other features @@ -114,6 +131,6 @@ class SchemeFeatureGroup( val children: Map, scaleRange: FloatRange = defaultScaleRange, ) : SchemeFeature(scaleRange) { - override fun getBoundingBox(scale: Float): SchemeCoordinateBox? = + override fun getBoundingBox(scale: Float): SchemeRectangle? = children.values.mapNotNull { it.getBoundingBox(scale) }.wrapAll() } diff --git a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeFeatureBuilder.kt b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeFeatureBuilder.kt index af1e273..89693d3 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeFeatureBuilder.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/center/sciprog/maps/scheme/SchemeFeatureBuilder.kt @@ -3,7 +3,6 @@ package center.sciprog.maps.scheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.painter.Painter @@ -14,55 +13,88 @@ import center.sciprog.maps.scheme.SchemeFeature.Companion.defaultScaleRange typealias FeatureId = String -interface SchemeFeatureBuilder { - fun addFeature(id: FeatureId?, feature: SchemeFeature): FeatureId - fun build(): SnapshotStateMap -} +public class SchemeFeaturesState internal constructor( + private val features: MutableMap, + private val attributes: MutableMap, in Any?>>, +) { + public interface Attribute -internal class SchemeFeatureBuilderImpl( - initialFeatures: Map, -) : SchemeFeatureBuilder { + public fun features(): Map = features - private val content: SnapshotStateMap = - mutableStateMapOf().apply { - putAll(initialFeatures) - } private fun generateID(feature: SchemeFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]" - override fun addFeature(id: FeatureId?, feature: SchemeFeature): FeatureId { + public fun addFeature(id: FeatureId?, feature: SchemeFeature): FeatureId { val safeId = id ?: generateID(feature) - content[id ?: generateID(feature)] = feature + features[id ?: generateID(feature)] = feature return safeId } - override fun build(): SnapshotStateMap = content + public fun setAttribute(id: FeatureId, key: Attribute, value: T) { + attributes.getOrPut(id) { mutableStateMapOf() }[key] = value + } + + @Suppress("UNCHECKED_CAST") + public fun getAttribute(id: FeatureId, key: Attribute): T? = + attributes[id]?.get(key)?.let { it as T } + + @Suppress("UNCHECKED_CAST") + public fun findAllWithAttribute(key: Attribute, condition: (T) -> Boolean): Set { + return attributes.filterValues { + condition(it[key] as T) + }.keys + } + + public companion object { + + /** + * Build, but do not remember map feature state + */ + public fun build( + builder: SchemeFeaturesState.() -> Unit = {}, + ): SchemeFeaturesState = SchemeFeaturesState( + mutableStateMapOf(), + mutableStateMapOf() + ).apply(builder) + + /** + * Build and remember map feature state + */ + @Composable + public fun remember( + builder: SchemeFeaturesState.() -> Unit = {}, + ): SchemeFeaturesState = androidx.compose.runtime.remember(builder) { + build(builder) + } + + } } -fun SchemeFeatureBuilder.background( - painter: Painter, - box: SchemeCoordinateBox, +fun SchemeFeaturesState.background( + box: SchemeRectangle, id: FeatureId? = null, + painter: @Composable () -> Painter, ): FeatureId = addFeature( id, - SchemeBackgroundFeature(box, painter) + SchemeBackgroundFeature(box, painter = painter) ) -fun SchemeFeatureBuilder.background( - painter: Painter, - size: Size = painter.intrinsicSize, +fun SchemeFeaturesState.background( + width: Float, + height: Float, offset: SchemeCoordinates = SchemeCoordinates(0f, 0f), id: FeatureId? = null, + painter: @Composable () -> Painter, ): FeatureId { - val box = SchemeCoordinateBox( + val box = SchemeRectangle( offset, - SchemeCoordinates(size.width + offset.x, size.height + offset.y) + SchemeCoordinates(width + offset.x, height + offset.y) ) - return background(painter, box, id) + return background(box, id, painter = painter) } -fun SchemeFeatureBuilder.circle( +fun SchemeFeaturesState.circle( center: SchemeCoordinates, scaleRange: FloatRange = defaultScaleRange, size: Float = 5f, @@ -72,7 +104,7 @@ fun SchemeFeatureBuilder.circle( id, SchemeCircleFeature(center, scaleRange, size, color) ) -fun SchemeFeatureBuilder.circle( +fun SchemeFeaturesState.circle( centerCoordinates: Pair, scaleRange: FloatRange = defaultScaleRange, size: Float = 5f, @@ -82,14 +114,14 @@ fun SchemeFeatureBuilder.circle( id, SchemeCircleFeature(centerCoordinates.toCoordinates(), scaleRange, size, color) ) -fun SchemeFeatureBuilder.draw( +fun SchemeFeaturesState.draw( position: Pair, scaleRange: FloatRange = defaultScaleRange, id: FeatureId? = null, drawFeature: DrawScope.() -> Unit, ) = addFeature(id, SchemeDrawFeature(position.toCoordinates(), scaleRange, drawFeature)) -fun SchemeFeatureBuilder.line( +fun SchemeFeaturesState.line( aCoordinates: Pair, bCoordinates: Pair, scaleRange: FloatRange = defaultScaleRange, @@ -97,7 +129,7 @@ fun SchemeFeatureBuilder.line( id: FeatureId? = null, ) = addFeature(id, SchemeLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), scaleRange, color)) -fun SchemeFeatureBuilder.text( +fun SchemeFeaturesState.text( position: SchemeCoordinates, text: String, scaleRange: FloatRange = defaultScaleRange, @@ -105,7 +137,7 @@ fun SchemeFeatureBuilder.text( id: FeatureId? = null, ) = addFeature(id, SchemeTextFeature(position, text, scaleRange, color)) -fun SchemeFeatureBuilder.text( +fun SchemeFeaturesState.text( position: Pair, text: String, scaleRange: FloatRange = defaultScaleRange, @@ -113,21 +145,20 @@ fun SchemeFeatureBuilder.text( id: FeatureId? = null, ) = addFeature(id, SchemeTextFeature(position.toCoordinates(), text, scaleRange, color)) -@Composable -fun SchemeFeatureBuilder.image( +fun SchemeFeaturesState.image( position: Pair, image: ImageVector, size: DpSize = DpSize(20.dp, 20.dp), scaleRange: FloatRange = defaultScaleRange, id: FeatureId? = null, -) = addFeature(id, SchemeVectorImageFeature(position.toCoordinates(), image, size, scaleRange)) +) = addFeature(id, SchemeImageFeature(position.toCoordinates(), image, size, scaleRange)) -fun SchemeFeatureBuilder.group( +fun SchemeFeaturesState.group( scaleRange: FloatRange = defaultScaleRange, id: FeatureId? = null, - builder: SchemeFeatureBuilder.() -> Unit, + builder: SchemeFeaturesState.() -> Unit, ): FeatureId { - val map = SchemeFeatureBuilderImpl(emptyMap()).apply(builder).build() - val feature = SchemeFeatureGroup(map, scaleRange) + val groupBuilder = SchemeFeaturesState.build(builder) + val feature = SchemeFeatureGroup(groupBuilder.features(), scaleRange) return addFeature(id, feature) } \ No newline at end of file diff --git a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt b/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt index b7ea5d3..5d4c6cf 100644 --- a/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt +++ b/maps-kt-scheme/src/jvmMain/kotlin/center/sciprog/maps/scheme/SchemeView.kt @@ -9,11 +9,8 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.drawscope.* -import androidx.compose.ui.graphics.nativeCanvas -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.* import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpSize @@ -36,40 +33,29 @@ private val logger = KotlinLogging.logger("SchemeView") data class SchemeViewConfig( val zoomSpeed: Float = 1f / 3f, - val inferViewBoxFromFeatures: Boolean = false, val onClick: SchemeViewPoint.() -> Unit = {}, val onViewChange: SchemeViewPoint.() -> Unit = {}, - val onSelect: (SchemeCoordinateBox) -> Unit = {}, + val onSelect: (SchemeRectangle) -> Unit = {}, val zoomOnSelect: Boolean = true, ) @Composable public fun SchemeView( - computeViewPoint: (canvasSize: DpSize) -> SchemeViewPoint, - features: Map, + initialViewPoint: SchemeViewPoint, + featuresState: SchemeFeaturesState, config: SchemeViewConfig = SchemeViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), ) { + var canvasSize by remember { mutableStateOf(defaultCanvasSize) } - var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) } + var viewPoint by remember { mutableStateOf(initialViewPoint) } - var viewPointInternal: SchemeViewPoint? by remember { - mutableStateOf(null) + + fun setViewPoint(newViewPoint: SchemeViewPoint) { + config.onViewChange(newViewPoint) + viewPoint = newViewPoint } - val viewPoint: SchemeViewPoint by derivedStateOf { - viewPointInternal ?: if (config.inferViewBoxFromFeatures) { - features.values.computeBoundingBox(1f)?.let { box -> - val scale = min( - canvasSize.width.value / box.width, - canvasSize.height.value / box.height - ) - SchemeViewPoint(box.center, scale) - } ?: computeViewPoint(canvasSize) - } else { - computeViewPoint(canvasSize) - } - } fun DpOffset.toCoordinates(): SchemeCoordinates = SchemeCoordinates( (x - canvasSize.width / 2).value / viewPoint.scale + viewPoint.focus.x, @@ -104,7 +90,7 @@ public fun SchemeView( } selectRect?.let { rect -> //Use selection override if it is defined - val box = SchemeCoordinateBox( + val box = SchemeRectangle( rect.topLeft.toDpOffset().toCoordinates(), rect.bottomRight.toDpOffset().toCoordinates() ) @@ -117,8 +103,7 @@ public fun SchemeView( val newViewPoint = SchemeViewPoint(box.center, newScale) - config.onViewChange(newViewPoint) - viewPointInternal = newViewPoint + setViewPoint(newViewPoint) } selectRect = null } @@ -132,8 +117,7 @@ public fun SchemeView( -dragAmount.x.toDp().value / viewPoint.scale, dragAmount.y.toDp().value / viewPoint.scale ) - config.onViewChange(newViewPoint) - viewPointInternal = newViewPoint + setViewPoint(newViewPoint) } } } @@ -146,10 +130,13 @@ public fun SchemeView( //compute invariant point of translation val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates() val newViewPoint = viewPoint.zoom(-change.scrollDelta.y * config.zoomSpeed, invariant) - config.onViewChange(newViewPoint) - viewPointInternal = newViewPoint + setViewPoint(newViewPoint) }.fillMaxSize() + val painterCache = key(featuresState){ + featuresState.features().values.filterIsInstance().associateWith { it.painter() } + } + Canvas(canvasModifier) { fun SchemeCoordinates.toOffset(): Offset = Offset( (canvasSize.width / 2 + (x.dp - viewPoint.focus.x.dp) * viewPoint.scale).toPx(), @@ -160,36 +147,55 @@ public fun SchemeView( fun DrawScope.drawFeature(scale: Float, feature: SchemeFeature) { when (feature) { is SchemeBackgroundFeature -> { - val offset = SchemeCoordinates(feature.position.left, feature.position.top).toOffset() + val offset = SchemeCoordinates(feature.rectangle.left, feature.rectangle.top).toOffset() val backgroundSize = DpSize( - (feature.position.width * scale).dp, - (feature.position.height * scale).dp + (feature.rectangle.width * scale).dp, + (feature.rectangle.height * scale).dp ).toSize() translate(offset.x, offset.y) { - with(feature.painter) { + with(painterCache[feature]!!) { draw(backgroundSize) } } } + is SchemeFeatureSelector -> drawFeature(scale, feature.selector(scale)) is SchemeCircleFeature -> drawCircle( feature.color, feature.size, center = feature.center.toOffset() ) + is SchemeLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset()) + is SchemeArcFeature -> { + val topLeft = feature.oval.topLeft.toOffset() + val bottomRight = feature.oval.bottomRight.toOffset() + + val path = Path().apply { + addArcRad( + Rect(topLeft, bottomRight), + feature.startAngle, + feature.arcLength + ) + } + + drawPath(path, color = feature.color, style = Stroke()) + + } + is SchemeBitmapFeature -> drawImage(feature.image, feature.position.toOffset()) is SchemeImageFeature -> { val offset = feature.position.toOffset() val imageSize = feature.size.toSize() translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) { - with(feature.painter) { + with(painterCache[feature]!!) { draw(imageSize) } } } + is SchemeTextFeature -> drawIntoCanvas { canvas -> val offset = feature.position.toOffset() canvas.nativeCanvas.drawString( @@ -200,12 +206,14 @@ public fun SchemeView( feature.color.toPaint() ) } + is SchemeDrawFeature -> { val offset = feature.position.toOffset() translate(offset.x, offset.y) { feature.drawFeature(this) } } + is SchemeFeatureGroup -> { feature.children.values.forEach { drawFeature(scale, it) @@ -219,10 +227,10 @@ public fun SchemeView( logger.debug { "Recalculate canvas. Size: $size" } } clipRect { - features.values.filterIsInstance().forEach { background -> + featuresState.features().values.filterIsInstance().forEach { background -> drawFeature(viewPoint.scale, background) } - features.values.filter { + featuresState.features().values.filter { it !is SchemeBackgroundFeature && viewPoint.scale in it.scaleRange }.forEach { feature -> drawFeature(viewPoint.scale, feature) @@ -243,20 +251,82 @@ public fun SchemeView( } } + +/** + * A builder for a Scheme with static features. + */ @Composable -fun SchemeView( - initialViewPoint: SchemeViewPoint, - features: Map = emptyMap(), +public fun SchemeView( + initialViewPoint: SchemeViewPoint? = null, + initialRectangle: SchemeRectangle? = null, + featureMap: Map, config: SchemeViewConfig = SchemeViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), - buildFeatures: @Composable (SchemeFeatureBuilder.() -> Unit) = {}, ) { - val featuresBuilder = SchemeFeatureBuilderImpl(features) - featuresBuilder.buildFeatures() + val featuresState = key(featureMap) { + SchemeFeaturesState.build { + featureMap.forEach(::addFeature) + } + } + + val viewPointOverride: SchemeViewPoint = remember(initialViewPoint, initialRectangle) { + initialViewPoint + ?: initialRectangle?.computeViewPoint() + ?: featureMap.values.computeBoundingBox(1f)?.computeViewPoint() + ?: SchemeViewPoint(SchemeCoordinates(0f, 0f)) + } + + SchemeView(viewPointOverride, featuresState, config, modifier) +} + +/** + * Draw a map using convenient parameters. If neither [initialViewPoint], noe [initialRectangle] is defined, + * use map features to infer view region. + * @param initialViewPoint The view point of the map using center and zoom. Is used if provided + * @param initialRectangle The rectangle to be used for view point computation. Used if [initialViewPoint] is not defined. + * @param buildFeatures - a builder for features + */ +@Composable +public fun SchemeView( + initialViewPoint: SchemeViewPoint? = null, + initialRectangle: SchemeRectangle? = null, + config: SchemeViewConfig = SchemeViewConfig(), + modifier: Modifier = Modifier.fillMaxSize(), + buildFeatures: SchemeFeaturesState.() -> Unit = {}, +) { + val featureState = SchemeFeaturesState.remember(buildFeatures) + + val features = featureState.features() + + val viewPointOverride: SchemeViewPoint = remember(initialViewPoint, initialRectangle) { + initialViewPoint + ?: initialRectangle?.computeViewPoint() + ?: features.values.computeBoundingBox(1f)?.computeViewPoint() + ?: SchemeViewPoint(SchemeCoordinates(0f, 0f)) + } + +// val featureDrag = DragHandle.withPrimaryButton { _, start, end -> +// val zoom = start.zoom +// featureState.findAllWithAttribute(DraggableAttribute) { it }.forEach { id -> +// val feature = features[id] as? DraggableMapFeature ?: return@forEach +// val boundingBox = feature.getBoundingBox(zoom) ?: return@forEach +// if (start.focus in boundingBox) { +// featureState.addFeature(id, feature.withCoordinates(end.focus)) +// return@withPrimaryButton false +// } +// } +// return@withPrimaryButton true +// } +// +// +// val newConfig = config.copy( +// dragHandle = DragHandle.combine(featureDrag, config.dragHandle) +// ) + SchemeView( - { initialViewPoint }, - featuresBuilder.build(), - config, - modifier + initialViewPoint = viewPointOverride, + featuresState = featureState, + config = config, + modifier = modifier, ) }