Fix feature update

This commit is contained in:
Alexander Nozik 2024-07-07 13:20:57 +03:00
parent 62196fc6f5
commit 29074a9624
7 changed files with 123 additions and 77 deletions

View File

@ -168,7 +168,7 @@ fun App() {
//Add click listeners for all polygons //Add click listeners for all polygons
forEachWithType<Gmc, PolygonFeature<Gmc>> { ref, polygon: PolygonFeature<Gmc> -> forEachWithType<Gmc, PolygonFeature<Gmc>> { ref, polygon: PolygonFeature<Gmc> ->
ref.onClick(PointerMatcher.Primary) { ref.onClick(PointerMatcher.Primary) {
println("Click on ${ref.id}") println("Click on $ref")
//draw in top-level scope //draw in top-level scope
with(this@MapView) { with(this@MapView) {
multiLine( multiLine(

View File

@ -55,6 +55,7 @@ fun App() {
} }
draggableMultiLine( draggableMultiLine(
pointRefs + pointRefs.first(), pointRefs + pointRefs.first(),
"line"
) )
} }
} }

View File

@ -21,6 +21,7 @@ import androidx.compose.ui.unit.DpRect
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import space.kscience.attributes.Attributes import space.kscience.attributes.Attributes
import space.kscience.attributes.plus
/** /**
* An extension of [DrawScope] to include map-specific features * An extension of [DrawScope] to include map-specific features
@ -96,12 +97,25 @@ public fun <T : Any> FeatureCanvas(
if (state.canvasSize != size.toDpSize()) { if (state.canvasSize != size.toDpSize()) {
state.canvasSize = size.toDpSize() state.canvasSize = size.toDpSize()
} }
ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply {
clipRect { clipRect {
features.values.sortedBy { it.z } ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply {
.filter { state.viewPoint.zoom in it.zoomRange }
.forEach { feature -> val attributesCache = mutableMapOf<List<String>, Attributes>()
this@apply.drawFeature(feature)
fun computeGroupAttributes(path: List<String>): 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)))
} }
} }
} }

View File

@ -19,16 +19,24 @@ import space.kscience.attributes.Attributes
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.nd.* import space.kscience.kmath.nd.*
import space.kscience.kmath.structures.Buffer import space.kscience.kmath.structures.Buffer
import space.kscience.maps.features.FeatureStore.Companion.generateId import space.kscience.maps.features.FeatureStore.Companion.generateFeatureId
//@JvmInline //@JvmInline
//public value class FeatureId<out F : Feature<*>>(public val id: String) //public value class FeatureId<out F : Feature<*>>(public val id: String)
public class FeatureRef<T : Any, out F : Feature<T>>(public val store: FeatureStore<T>, public val id: String) /**
* A reference to a feature inside a [FeatureStore]
*/
public class FeatureRef<T : Any, out F : Feature<T>> internal constructor(
internal val store: FeatureStore<T>,
internal val id: String,
) {
override fun toString(): String = "FeatureRef($id)"
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.resolve(): F = public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.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 <T : Any, F : Feature<T>> FeatureRef<T, F>.attributes: Attributes get() = resolve().attributes public val <T : Any, F : Feature<T>> FeatureRef<T, F>.attributes: Attributes get() = resolve().attributes
@ -36,8 +44,17 @@ public fun Uuid.toIndex(): String = leastSignificantBits.toString(16)
public interface FeatureBuilder<T : Any> { public interface FeatureBuilder<T : Any> {
public val space: CoordinateSpace<T> public val space: CoordinateSpace<T>
/**
* Add or replace feature. If [id] is null, then a unique id is genertated
*/
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F>
/**
* Update existing feature if it is present and is of type [F]
*/
public fun <F : Feature<T>> updateFeature(id: String, block: (F?) -> F): FeatureRef<T, F>
public fun group( public fun group(
id: String? = null, id: String? = null,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
@ -53,7 +70,7 @@ public interface FeatureSet<T : Any> {
/** /**
* Create a reference * Create a reference
*/ */
public fun <F: Feature<T>> ref(id: String): FeatureRef<T, F> public fun <F : Feature<T>> ref(id: String): FeatureRef<T, F>
} }
@ -67,17 +84,21 @@ public class FeatureStore<T : Any>(
override val features: Map<String, Feature<T>> get() = featureFlow.value override val features: Map<String, Feature<T>> get() = featureFlow.value
override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> { override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> {
val safeId = id ?: generateId(feature) val safeId = id ?: generateFeatureId(feature)
_featureFlow.value += (safeId to feature) _featureFlow.value += (safeId to feature)
return FeatureRef(this, safeId) return FeatureRef(this, safeId)
} }
@Suppress("UNCHECKED_CAST")
override fun <F : Feature<T>> updateFeature(id: String, block: (F?) -> F): FeatureRef<T, F> =
feature(id, block(features[id] as? F))
override fun group( override fun group(
id: String?, id: String?,
attributes: Attributes, attributes: Attributes,
builder: FeatureGroup<T>.() -> Unit, builder: FeatureGroup<T>.() -> Unit,
): FeatureRef<T, FeatureGroup<T>> { ): FeatureRef<T, FeatureGroup<T>> {
val safeId = id ?: generateId(null) val safeId: String = id ?: generateFeatureId<FeatureGroup<*>>()
return feature(safeId, FeatureGroup(this, safeId, attributes).apply(builder)) return feature(safeId, FeatureGroup(this, safeId, attributes).apply(builder))
} }
@ -85,7 +106,7 @@ public class FeatureStore<T : Any>(
_featureFlow.value -= id _featureFlow.value -= id
} }
override fun <F: Feature<T>> ref(id: String): FeatureRef<T, F> = FeatureRef(this, id) override fun <F : Feature<T>> ref(id: String): FeatureRef<T, F> = FeatureRef(this, id)
public fun getBoundingBox(zoom: Float = Float.MAX_VALUE): Rectangle<T>? = with(space) { public fun getBoundingBox(zoom: Float = Float.MAX_VALUE): Rectangle<T>? = with(space) {
features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
@ -93,11 +114,15 @@ public class FeatureStore<T : Any>(
public companion object { public companion object {
internal fun generateId(feature: Feature<*>?): String = if (feature == null) {
"@group[${uuid4().toIndex()}]" internal fun generateFeatureId(prefix: String): String =
} else { "$prefix[${uuid4().toIndex()}]"
"${feature::class.simpleName}[${uuid4().toIndex()}]"
} internal fun generateFeatureId(feature: Feature<*>): String =
generateFeatureId(feature::class.simpleName ?: "undefined")
internal inline fun <reified F : Feature<*>> generateFeatureId(): String =
generateFeatureId(F::class.simpleName ?: "undefined")
/** /**
* Build, but do not remember map feature state * Build, but do not remember map feature state
@ -136,14 +161,18 @@ public data class FeatureGroup<T : Any> internal constructor(
override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> = override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> =
store.feature("$groupId/${id ?: generateId(feature)}", feature) store.feature("$groupId/${id ?: generateFeatureId(feature)}", feature)
override fun <F : Feature<T>> updateFeature(id: String, block: (F?) -> F): FeatureRef<T, F> =
store.updateFeature("$groupId/$id", block)
override fun group( override fun group(
id: String?, id: String?,
attributes: Attributes, attributes: Attributes,
builder: FeatureGroup<T>.() -> Unit, builder: FeatureGroup<T>.() -> Unit,
): FeatureRef<T, FeatureGroup<T>> { ): FeatureRef<T, FeatureGroup<T>> {
val safeId = id ?: generateId(null) val safeId = id ?: generateFeatureId<FeatureGroup<*>>()
return feature(safeId, FeatureGroup(store, "$groupId/$safeId", attributes).apply(builder)) return feature(safeId, FeatureGroup(store, "$groupId/$safeId", attributes).apply(builder))
} }
@ -161,13 +190,13 @@ public data class FeatureGroup<T : Any> internal constructor(
features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
} }
override fun <F: Feature<T>> ref(id: String): FeatureRef<T, F> = FeatureRef(store, "$groupId/$id") override fun <F : Feature<T>> ref(id: String): FeatureRef<T, F> = FeatureRef(store, "$groupId/$id")
} }
/** /**
* Recursively search for feature until function returns true * Recursively search for feature until function returns true
*/ */
public fun <T : Any> FeatureSet<T>.forEachUntil(block: FeatureSet<T>.(ref: FeatureRef<T,*>, feature: Feature<T>) -> Boolean) { public fun <T : Any> FeatureSet<T>.forEachUntil(block: FeatureSet<T>.(ref: FeatureRef<T, *>, feature: Feature<T>) -> Boolean) {
features.entries.sortedByDescending { it.value.z }.forEach { (key, feature) -> features.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
if (!block(ref<Feature<T>>(key), feature)) return@forEachUntil if (!block(ref<Feature<T>>(key), feature)) return@forEachUntil
} }
@ -178,7 +207,7 @@ public fun <T : Any> FeatureSet<T>.forEachUntil(block: FeatureSet<T>.(ref: Featu
*/ */
public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttribute( public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttribute(
key: Attribute<A>, key: Attribute<A>,
block: FeatureSet<T>.(ref: FeatureRef<T,*>, feature: Feature<T>, attribute: A) -> Unit, block: FeatureSet<T>.(ref: FeatureRef<T, *>, feature: Feature<T>, attribute: A) -> Unit,
) { ) {
features.forEach { (id, feature) -> features.forEach { (id, feature) ->
feature.attributes[key]?.let { feature.attributes[key]?.let {
@ -189,7 +218,7 @@ public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttribute(
public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttributeUntil( public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttributeUntil(
key: Attribute<A>, key: Attribute<A>,
block: FeatureSet<T>.(ref: FeatureRef<T,*>, feature: Feature<T>, attribute: A) -> Boolean, block: FeatureSet<T>.(ref: FeatureRef<T, *>, feature: Feature<T>, attribute: A) -> Boolean,
) { ) {
features.forEach { (id, feature) -> features.forEach { (id, feature) ->
feature.attributes[key]?.let { feature.attributes[key]?.let {
@ -199,7 +228,7 @@ public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttributeUntil(
} }
public inline fun <T : Any, reified F : Feature<T>> FeatureSet<T>.forEachWithType( public inline fun <T : Any, reified F : Feature<T>> FeatureSet<T>.forEachWithType(
crossinline block: FeatureSet<T>.(ref: FeatureRef<T,F>, feature: F) -> Unit, crossinline block: FeatureSet<T>.(ref: FeatureRef<T, F>, feature: F) -> Unit,
) { ) {
features.forEach { (id, feature) -> features.forEach { (id, feature) ->
if (feature is F) block(ref(id), feature) if (feature is F) block(ref(id), feature)

View File

@ -9,23 +9,15 @@ public fun <T : Any> FeatureBuilder<T>.draggableLine(
bId: FeatureRef<T, MarkerFeature<T>>, bId: FeatureRef<T, MarkerFeature<T>>,
id: String? = null, id: String? = null,
): FeatureRef<T, LineFeature<T>> { ): FeatureRef<T, LineFeature<T>> {
var lineId: FeatureRef<T, LineFeature<T>>? = null val lineId = id ?: FeatureStore.generateFeatureId<LineFeature<*>>()
fun drawLine(): FeatureRef<T, LineFeature<T>> { fun drawLine(): FeatureRef<T, LineFeature<T>> = updateFeature(lineId) { old ->
val currentId = feature(
lineId?.id ?: id,
LineFeature( LineFeature(
space, space,
aId.resolve().center, aId.resolve().center,
bId.resolve().center, bId.resolve().center,
Attributes<FeatureGroup<T>> { old?.attributes ?: Attributes(ZAttribute, -10f)
ZAttribute(-10f)
lineId?.attributes?.let { putAll(it) }
}
) )
)
lineId = currentId
return currentId
} }
aId.draggable { _, _ -> aId.draggable { _, _ ->
@ -43,22 +35,14 @@ public fun <T : Any> FeatureBuilder<T>.draggableMultiLine(
points: List<FeatureRef<T, MarkerFeature<T>>>, points: List<FeatureRef<T, MarkerFeature<T>>>,
id: String? = null, id: String? = null,
): FeatureRef<T, MultiLineFeature<T>> { ): FeatureRef<T, MultiLineFeature<T>> {
var polygonId: FeatureRef<T, MultiLineFeature<T>>? = null val polygonId = id ?: FeatureStore.generateFeatureId("multiline")
fun drawLines(): FeatureRef<T, MultiLineFeature<T>> { fun drawLines(): FeatureRef<T, MultiLineFeature<T>> = updateFeature(polygonId) { old ->
val currentId = feature(
polygonId?.id ?: id,
MultiLineFeature( MultiLineFeature(
space, space,
points.map { it.resolve().center }, points.map { it.resolve().center },
Attributes<FeatureGroup<T>>{ old?.attributes ?: Attributes(ZAttribute, -10f)
ZAttribute(-10f)
polygonId?.attributes?.let { putAll(it) }
}
) )
)
polygonId = currentId
return currentId
} }
points.forEach { points.forEach {

View File

@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.drawscope.translate
import space.kscience.attributes.Attributes
import space.kscience.attributes.plus import space.kscience.attributes.plus
import space.kscience.kmath.PerformancePitfall import space.kscience.kmath.PerformancePitfall
@ -20,14 +21,16 @@ import space.kscience.kmath.PerformancePitfall
public fun <T : Any> FeatureDrawScope<T>.drawFeature( public fun <T : Any> FeatureDrawScope<T>.drawFeature(
feature: Feature<T>, feature: Feature<T>,
baseAttributes: Attributes,
): Unit { ): Unit {
val color = feature.color ?: Color.Red val attributes = baseAttributes + feature.attributes
val alpha = feature.attributes[AlphaAttribute] ?: 1f val color = attributes[ColorAttribute] ?: Color.Red
val alpha = attributes[AlphaAttribute] ?: 1f
//avoid drawing invisible features //avoid drawing invisible features
if(feature.attributes[VisibleAttribute] == false) return if(attributes[VisibleAttribute] == false) return
when (feature) { when (feature) {
is FeatureSelector -> drawFeature(feature.selector(state.zoom)) is FeatureSelector -> drawFeature(feature.selector(state.zoom), attributes)
is CircleFeature -> drawCircle( is CircleFeature -> drawCircle(
color, color,
feature.radius.toPx(), feature.radius.toPx(),
@ -49,8 +52,8 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
color, color,
feature.a.toOffset(), feature.a.toOffset(),
feature.b.toOffset(), feature.b.toOffset(),
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, strokeWidth = attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pathEffect = feature.attributes[PathEffectAttribute], pathEffect = attributes[PathEffectAttribute],
alpha = alpha alpha = alpha
) )
@ -84,7 +87,7 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
} }
} }
is TextFeature -> drawText(feature.text, feature.position.toOffset(), feature.attributes) is TextFeature -> drawText(feature.text, feature.position.toOffset(), attributes)
is DrawFeature -> { is DrawFeature -> {
val offset = feature.position.toOffset() val offset = feature.position.toOffset()
@ -94,13 +97,14 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
} }
is FeatureGroup -> { is FeatureGroup -> {
feature.features.values.forEach { //ignore groups
drawFeature( // feature.features.values.forEach {
it.withAttributes { // drawFeature(
feature.attributes + this // it.withAttributes {
} // feature.attributes + this
) // }
} // )
// }
} }
is PathFeature -> { is PathFeature -> {
@ -117,9 +121,9 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
drawPoints( drawPoints(
points = points, points = points,
color = color, color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: 5f, strokeWidth = attributes[StrokeAttribute] ?: 5f,
pointMode = PointMode.Points, pointMode = PointMode.Points,
pathEffect = feature.attributes[PathEffectAttribute], pathEffect = attributes[PathEffectAttribute],
alpha = alpha alpha = alpha
) )
} }
@ -129,9 +133,9 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
drawPoints( drawPoints(
points = points, points = points,
color = color, color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, strokeWidth = attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Polygon, pointMode = PointMode.Polygon,
pathEffect = feature.attributes[PathEffectAttribute], pathEffect = attributes[PathEffectAttribute],
alpha = alpha alpha = alpha
) )
} }

View File

@ -6,6 +6,8 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.jfree.svg.SVGGraphics2D import org.jfree.svg.SVGGraphics2D
import org.jfree.svg.SVGUtils import org.jfree.svg.SVGUtils
import space.kscience.attributes.Attributes
import space.kscience.attributes.plus
import space.kscience.maps.features.* import space.kscience.maps.features.*
import space.kscience.maps.scheme.XY import space.kscience.maps.scheme.XY
import space.kscience.maps.scheme.XYCanvasState import space.kscience.maps.scheme.XYCanvasState
@ -160,10 +162,22 @@ public fun FeatureStateSnapshot<XY>.generateSvg(
val svgScope = SvgDrawScope(svgCanvasState, svgGraphics2D, painterCache) val svgScope = SvgDrawScope(svgCanvasState, svgGraphics2D, painterCache)
svgScope.apply { svgScope.apply {
features.values.sortedBy { it.z } features.entries.sortedBy { it.value.z }
.filter { state.viewPoint.zoom in it.zoomRange } .filter { state.viewPoint.zoom in it.value.zoomRange }
.forEach { feature -> .forEach { (id, feature) ->
this@apply.drawFeature(feature) val attributesCache = mutableMapOf<List<String>, Attributes>()
fun computeGroupAttributes(path: List<String>): 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) return svgGraphics2D.getSVGElement(id)