From 29074a96240d2c799d6b7744cfb354d4551e0e16 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 7 Jul 2024 13:20:57 +0300 Subject: [PATCH] Fix feature update --- demo/maps/src/jvmMain/kotlin/Main.kt | 2 +- .../polygon-editor/src/jvmMain/kotlin/Main.kt | 1 + .../maps/features/FeatureDrawScope.kt | 26 +++++-- .../kscience/maps/features/FeatureStore.kt | 67 +++++++++++++------ .../maps/features/compositeFeatures.kt | 42 ++++-------- .../kscience/maps/features/drawFeature.kt | 40 ++++++----- .../space/kscience/maps/svg/exportToSvg.kt | 22 ++++-- 7 files changed, 123 insertions(+), 77 deletions(-) diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index 7d6d1e3..bfda2ba 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -168,7 +168,7 @@ fun App() { //Add click listeners for all polygons forEachWithType> { ref, polygon: PolygonFeature -> ref.onClick(PointerMatcher.Primary) { - println("Click on ${ref.id}") + println("Click on $ref") //draw in top-level scope with(this@MapView) { multiLine( diff --git a/demo/polygon-editor/src/jvmMain/kotlin/Main.kt b/demo/polygon-editor/src/jvmMain/kotlin/Main.kt index 4325538..51f5df7 100644 --- a/demo/polygon-editor/src/jvmMain/kotlin/Main.kt +++ b/demo/polygon-editor/src/jvmMain/kotlin/Main.kt @@ -55,6 +55,7 @@ fun App() { } draggableMultiLine( pointRefs + pointRefs.first(), + "line" ) } } diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt index 6ced25b..a71d0fa 100644 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureDrawScope.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.unit.DpRect import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.flow.StateFlow import space.kscience.attributes.Attributes +import space.kscience.attributes.plus /** * An extension of [DrawScope] to include map-specific features @@ -96,12 +97,25 @@ public fun FeatureCanvas( if (state.canvasSize != size.toDpSize()) { state.canvasSize = size.toDpSize() } - ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply { - clipRect { - features.values.sortedBy { it.z } - .filter { state.viewPoint.zoom in it.zoomRange } - .forEach { feature -> - this@apply.drawFeature(feature) + clipRect { + ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply { + + val attributesCache = mutableMapOf, Attributes>() + + fun computeGroupAttributes(path: List): Attributes = attributesCache.getOrPut(path){ + if (path.isEmpty()) return Attributes.EMPTY + else if (path.size == 1) { + features[path.first()]?.attributes ?: Attributes.EMPTY + } else { + computeGroupAttributes(path.dropLast(1)) + (features[path.first()]?.attributes ?: Attributes.EMPTY) + } + } + + features.entries.sortedBy { it.value.z } + .filter { state.viewPoint.zoom in it.value.zoomRange } + .forEach { (id, feature) -> + val path = id.split("/") + drawFeature(feature, computeGroupAttributes(path.dropLast(1))) } } } diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureStore.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureStore.kt index efd4232..71aaf4e 100644 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureStore.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureStore.kt @@ -19,16 +19,24 @@ import space.kscience.attributes.Attributes import space.kscience.kmath.geometry.Angle import space.kscience.kmath.nd.* import space.kscience.kmath.structures.Buffer -import space.kscience.maps.features.FeatureStore.Companion.generateId +import space.kscience.maps.features.FeatureStore.Companion.generateFeatureId //@JvmInline //public value class FeatureId>(public val id: String) -public class FeatureRef>(public val store: FeatureStore, public val id: String) +/** + * A reference to a feature inside a [FeatureStore] + */ +public class FeatureRef> internal constructor( + internal val store: FeatureStore, + internal val id: String, +) { + override fun toString(): String = "FeatureRef($id)" +} @Suppress("UNCHECKED_CAST") public fun > FeatureRef.resolve(): F = - store.features[id]?.let { it as F } ?: error("Feature with id=$id not found") + store.features[id]?.let { it as F } ?: error("Feature with ref $this not found") public val > FeatureRef.attributes: Attributes get() = resolve().attributes @@ -36,8 +44,17 @@ public fun Uuid.toIndex(): String = leastSignificantBits.toString(16) public interface FeatureBuilder { public val space: CoordinateSpace + + /** + * Add or replace feature. If [id] is null, then a unique id is genertated + */ public fun > feature(id: String?, feature: F): FeatureRef + /** + * Update existing feature if it is present and is of type [F] + */ + public fun > updateFeature(id: String, block: (F?) -> F): FeatureRef + public fun group( id: String? = null, attributes: Attributes = Attributes.EMPTY, @@ -53,7 +70,7 @@ public interface FeatureSet { /** * Create a reference */ - public fun > ref(id: String): FeatureRef + public fun > ref(id: String): FeatureRef } @@ -67,17 +84,21 @@ public class FeatureStore( override val features: Map> get() = featureFlow.value override fun > feature(id: String?, feature: F): FeatureRef { - val safeId = id ?: generateId(feature) + val safeId = id ?: generateFeatureId(feature) _featureFlow.value += (safeId to feature) return FeatureRef(this, safeId) } + @Suppress("UNCHECKED_CAST") + override fun > updateFeature(id: String, block: (F?) -> F): FeatureRef = + feature(id, block(features[id] as? F)) + override fun group( id: String?, attributes: Attributes, builder: FeatureGroup.() -> Unit, ): FeatureRef> { - val safeId = id ?: generateId(null) + val safeId: String = id ?: generateFeatureId>() return feature(safeId, FeatureGroup(this, safeId, attributes).apply(builder)) } @@ -85,7 +106,7 @@ public class FeatureStore( _featureFlow.value -= id } - override fun > ref(id: String): FeatureRef = FeatureRef(this, id) + override fun > ref(id: String): FeatureRef = FeatureRef(this, id) public fun getBoundingBox(zoom: Float = Float.MAX_VALUE): Rectangle? = with(space) { features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() @@ -93,11 +114,15 @@ public class FeatureStore( public companion object { - internal fun generateId(feature: Feature<*>?): String = if (feature == null) { - "@group[${uuid4().toIndex()}]" - } else { - "${feature::class.simpleName}[${uuid4().toIndex()}]" - } + + internal fun generateFeatureId(prefix: String): String = + "$prefix[${uuid4().toIndex()}]" + + internal fun generateFeatureId(feature: Feature<*>): String = + generateFeatureId(feature::class.simpleName ?: "undefined") + + internal inline fun > generateFeatureId(): String = + generateFeatureId(F::class.simpleName ?: "undefined") /** * Build, but do not remember map feature state @@ -136,14 +161,18 @@ public data class FeatureGroup internal constructor( override fun > feature(id: String?, feature: F): FeatureRef = - store.feature("$groupId/${id ?: generateId(feature)}", feature) + store.feature("$groupId/${id ?: generateFeatureId(feature)}", feature) + + override fun > updateFeature(id: String, block: (F?) -> F): FeatureRef = + store.updateFeature("$groupId/$id", block) + override fun group( id: String?, attributes: Attributes, builder: FeatureGroup.() -> Unit, ): FeatureRef> { - val safeId = id ?: generateId(null) + val safeId = id ?: generateFeatureId>() return feature(safeId, FeatureGroup(store, "$groupId/$safeId", attributes).apply(builder)) } @@ -161,13 +190,13 @@ public data class FeatureGroup internal constructor( features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() } - override fun > ref(id: String): FeatureRef = FeatureRef(store, "$groupId/$id") + override fun > ref(id: String): FeatureRef = FeatureRef(store, "$groupId/$id") } /** * Recursively search for feature until function returns true */ -public fun FeatureSet.forEachUntil(block: FeatureSet.(ref: FeatureRef, feature: Feature) -> Boolean) { +public fun FeatureSet.forEachUntil(block: FeatureSet.(ref: FeatureRef, feature: Feature) -> Boolean) { features.entries.sortedByDescending { it.value.z }.forEach { (key, feature) -> if (!block(ref>(key), feature)) return@forEachUntil } @@ -178,7 +207,7 @@ public fun FeatureSet.forEachUntil(block: FeatureSet.(ref: Featu */ public inline fun FeatureSet.forEachWithAttribute( key: Attribute, - block: FeatureSet.(ref: FeatureRef, feature: Feature, attribute: A) -> Unit, + block: FeatureSet.(ref: FeatureRef, feature: Feature, attribute: A) -> Unit, ) { features.forEach { (id, feature) -> feature.attributes[key]?.let { @@ -189,7 +218,7 @@ public inline fun FeatureSet.forEachWithAttribute( public inline fun FeatureSet.forEachWithAttributeUntil( key: Attribute, - block: FeatureSet.(ref: FeatureRef, feature: Feature, attribute: A) -> Boolean, + block: FeatureSet.(ref: FeatureRef, feature: Feature, attribute: A) -> Boolean, ) { features.forEach { (id, feature) -> feature.attributes[key]?.let { @@ -199,7 +228,7 @@ public inline fun FeatureSet.forEachWithAttributeUntil( } public inline fun > FeatureSet.forEachWithType( - crossinline block: FeatureSet.(ref: FeatureRef, feature: F) -> Unit, + crossinline block: FeatureSet.(ref: FeatureRef, feature: F) -> Unit, ) { features.forEach { (id, feature) -> if (feature is F) block(ref(id), feature) diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/compositeFeatures.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/compositeFeatures.kt index 4a68d02..7c8e1a4 100644 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/compositeFeatures.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/compositeFeatures.kt @@ -9,23 +9,15 @@ public fun FeatureBuilder.draggableLine( bId: FeatureRef>, id: String? = null, ): FeatureRef> { - var lineId: FeatureRef>? = null + val lineId = id ?: FeatureStore.generateFeatureId>() - fun drawLine(): FeatureRef> { - val currentId = feature( - lineId?.id ?: id, - LineFeature( - space, - aId.resolve().center, - bId.resolve().center, - Attributes> { - ZAttribute(-10f) - lineId?.attributes?.let { putAll(it) } - } - ) + fun drawLine(): FeatureRef> = updateFeature(lineId) { old -> + LineFeature( + space, + aId.resolve().center, + bId.resolve().center, + old?.attributes ?: Attributes(ZAttribute, -10f) ) - lineId = currentId - return currentId } aId.draggable { _, _ -> @@ -43,22 +35,14 @@ public fun FeatureBuilder.draggableMultiLine( points: List>>, id: String? = null, ): FeatureRef> { - var polygonId: FeatureRef>? = null + val polygonId = id ?: FeatureStore.generateFeatureId("multiline") - fun drawLines(): FeatureRef> { - val currentId = feature( - polygonId?.id ?: id, - MultiLineFeature( - space, - points.map { it.resolve().center }, - Attributes>{ - ZAttribute(-10f) - polygonId?.attributes?.let { putAll(it) } - } - ) + fun drawLines(): FeatureRef> = updateFeature(polygonId) { old -> + MultiLineFeature( + space, + points.map { it.resolve().center }, + old?.attributes ?: Attributes(ZAttribute, -10f) ) - polygonId = currentId - return currentId } points.forEach { diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/drawFeature.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/drawFeature.kt index f313fde..5b1de31 100644 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/drawFeature.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/drawFeature.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.translate +import space.kscience.attributes.Attributes import space.kscience.attributes.plus import space.kscience.kmath.PerformancePitfall @@ -20,14 +21,16 @@ import space.kscience.kmath.PerformancePitfall public fun FeatureDrawScope.drawFeature( feature: Feature, + baseAttributes: Attributes, ): Unit { - val color = feature.color ?: Color.Red - val alpha = feature.attributes[AlphaAttribute] ?: 1f + val attributes = baseAttributes + feature.attributes + val color = attributes[ColorAttribute] ?: Color.Red + val alpha = attributes[AlphaAttribute] ?: 1f //avoid drawing invisible features - if(feature.attributes[VisibleAttribute] == false) return + if(attributes[VisibleAttribute] == false) return when (feature) { - is FeatureSelector -> drawFeature(feature.selector(state.zoom)) + is FeatureSelector -> drawFeature(feature.selector(state.zoom), attributes) is CircleFeature -> drawCircle( color, feature.radius.toPx(), @@ -49,8 +52,8 @@ public fun FeatureDrawScope.drawFeature( color, feature.a.toOffset(), feature.b.toOffset(), - strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, - pathEffect = feature.attributes[PathEffectAttribute], + strokeWidth = attributes[StrokeAttribute] ?: Stroke.HairlineWidth, + pathEffect = attributes[PathEffectAttribute], alpha = alpha ) @@ -84,7 +87,7 @@ public fun FeatureDrawScope.drawFeature( } } - is TextFeature -> drawText(feature.text, feature.position.toOffset(), feature.attributes) + is TextFeature -> drawText(feature.text, feature.position.toOffset(), attributes) is DrawFeature -> { val offset = feature.position.toOffset() @@ -94,13 +97,14 @@ public fun FeatureDrawScope.drawFeature( } is FeatureGroup -> { - feature.features.values.forEach { - drawFeature( - it.withAttributes { - feature.attributes + this - } - ) - } + //ignore groups +// feature.features.values.forEach { +// drawFeature( +// it.withAttributes { +// feature.attributes + this +// } +// ) +// } } is PathFeature -> { @@ -117,9 +121,9 @@ public fun FeatureDrawScope.drawFeature( drawPoints( points = points, color = color, - strokeWidth = feature.attributes[StrokeAttribute] ?: 5f, + strokeWidth = attributes[StrokeAttribute] ?: 5f, pointMode = PointMode.Points, - pathEffect = feature.attributes[PathEffectAttribute], + pathEffect = attributes[PathEffectAttribute], alpha = alpha ) } @@ -129,9 +133,9 @@ public fun FeatureDrawScope.drawFeature( drawPoints( points = points, color = color, - strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, + strokeWidth = attributes[StrokeAttribute] ?: Stroke.HairlineWidth, pointMode = PointMode.Polygon, - pathEffect = feature.attributes[PathEffectAttribute], + pathEffect = attributes[PathEffectAttribute], alpha = alpha ) } diff --git a/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt index 137ee9b..3f6993f 100644 --- a/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt +++ b/maps-kt-scheme/src/jvmMain/kotlin/space/kscience/maps/svg/exportToSvg.kt @@ -6,6 +6,8 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import org.jfree.svg.SVGGraphics2D import org.jfree.svg.SVGUtils +import space.kscience.attributes.Attributes +import space.kscience.attributes.plus import space.kscience.maps.features.* import space.kscience.maps.scheme.XY import space.kscience.maps.scheme.XYCanvasState @@ -160,10 +162,22 @@ public fun FeatureStateSnapshot.generateSvg( val svgScope = SvgDrawScope(svgCanvasState, svgGraphics2D, painterCache) svgScope.apply { - features.values.sortedBy { it.z } - .filter { state.viewPoint.zoom in it.zoomRange } - .forEach { feature -> - this@apply.drawFeature(feature) + features.entries.sortedBy { it.value.z } + .filter { state.viewPoint.zoom in it.value.zoomRange } + .forEach { (id, feature) -> + val attributesCache = mutableMapOf, Attributes>() + + fun computeGroupAttributes(path: List): Attributes = attributesCache.getOrPut(path){ + if (path.isEmpty()) return Attributes.EMPTY + else if (path.size == 1) { + features[path.first()]?.attributes ?: Attributes.EMPTY + } else { + computeGroupAttributes(path.dropLast(1)) + (features[path.first()]?.attributes ?: Attributes.EMPTY) + } + } + + val path = id.split("/") + drawFeature(feature, computeGroupAttributes(path.dropLast(1))) } } return svgGraphics2D.getSVGElement(id)