From 62196fc6f5f69fa7b4b1e2fe712a0823a675e110 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 6 Jul 2024 09:54:06 +0300 Subject: [PATCH 1/6] Refactored to use flow instead of snapshot maps --- build.gradle.kts | 2 +- demo/maps-wasm/src/wasmJsMain/kotlin/Main.kt | 6 +- demo/maps/src/jvmMain/kotlin/Main.kt | 6 +- .../polygon-editor/src/jvmMain/kotlin/Main.kt | 2 +- demo/scheme/src/jvmMain/kotlin/Main.kt | 4 +- .../src/jvmMain/kotlin/Main.kt | 6 +- .../space/kscience/maps/compose/MapView.kt | 14 +- .../kscience/maps/compose/mapFeatures.kt | 28 +- maps-kt-features/build.gradle.kts | 1 + .../kscience/maps/compose/canvasControls.kt | 6 +- .../maps/features/FeatureDrawScope.kt | 19 +- .../{FeatureGroup.kt => FeatureStore.kt} | 248 ++++++++++-------- .../maps/features/compositeFeatures.kt | 6 +- .../kscience/maps/features/drawFeature.kt | 2 +- .../maps/features/mapFeatureAttributes.kt | 8 +- .../kscience/maps/geojson/geoJsonToMap.kt | 8 +- .../maps/geojson/geoJsonFeatureJvm.kt | 4 +- .../space/kscience/maps/scheme/SchemeView.kt | 13 +- .../kscience/maps/scheme/schemeFeatures.kt | 20 +- .../space/kscience/maps/svg/exportToSvg.kt | 8 +- 20 files changed, 222 insertions(+), 189 deletions(-) rename maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/{FeatureGroup.kt => FeatureStore.kt} (52%) diff --git a/build.gradle.kts b/build.gradle.kts index fdc4c25..62353a8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ val kmathVersion: String by extra("0.4.0") allprojects { group = "space.kscience" - version = "0.3.1-dev" + version = "0.4.0-dev" repositories { mavenLocal() diff --git a/demo/maps-wasm/src/wasmJsMain/kotlin/Main.kt b/demo/maps-wasm/src/wasmJsMain/kotlin/Main.kt index c6aee72..1e51933 100644 --- a/demo/maps-wasm/src/wasmJsMain/kotlin/Main.kt +++ b/demo/maps-wasm/src/wasmJsMain/kotlin/Main.kt @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class) +@file:OptIn(ExperimentalResourceApi::class, ExperimentalComposeUiApi::class) import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi @@ -10,7 +10,7 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource import space.kscience.kmath.geometry.Angle -import space.kscience.maps.features.FeatureGroup +import space.kscience.maps.features.FeatureStore import space.kscience.maps.features.ViewConfig import space.kscience.maps.features.ViewPoint import space.kscience.maps.features.color @@ -25,7 +25,7 @@ fun App() { val scope = rememberCoroutineScope() - val features: FeatureGroup = FeatureGroup.remember(XYCoordinateSpace) { + val features = FeatureStore.remember(XYCoordinateSpace) { background(1600f, 1200f) { painterResource(Res.drawable.middle_earth) } diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index a33267e..7d6d1e3 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -73,7 +73,7 @@ fun App() { geoJson(javaClass.getResource("/moscow.geo.json")!!) .color(Color.Blue) - .modifyAttribute(AlphaAttribute, 0.4f) + .alpha(0.4f) icon(pointOne, Icons.Filled.Home) @@ -166,13 +166,13 @@ fun App() { }.launchIn(scope) //Add click listeners for all polygons - forEachWithType> { ref -> + forEachWithType> { ref, polygon: PolygonFeature -> ref.onClick(PointerMatcher.Primary) { println("Click on ${ref.id}") //draw in top-level scope with(this@MapView) { multiLine( - ref.resolve().points, + polygon.points, attributes = Attributes(ZAttribute, 10f), id = "selected", ).modifyAttribute(StrokeAttribute, 4f).color(Color.Magenta) diff --git a/demo/polygon-editor/src/jvmMain/kotlin/Main.kt b/demo/polygon-editor/src/jvmMain/kotlin/Main.kt index 907be39..4325538 100644 --- a/demo/polygon-editor/src/jvmMain/kotlin/Main.kt +++ b/demo/polygon-editor/src/jvmMain/kotlin/Main.kt @@ -24,7 +24,7 @@ fun App() { val myPolygon: SnapshotStateList = remember { mutableStateListOf() } - val featureState: FeatureGroup = FeatureGroup.remember(XYCoordinateSpace) { + val featureState = FeatureStore.remember(XYCoordinateSpace) { multiLine( listOf(XY(0f, 0f), XY(0f, 1f), XY(1f, 1f), XY(1f, 0f), XY(0f, 0f)), id = "frame" diff --git a/demo/scheme/src/jvmMain/kotlin/Main.kt b/demo/scheme/src/jvmMain/kotlin/Main.kt index 32e1a18..efb18d8 100644 --- a/demo/scheme/src/jvmMain/kotlin/Main.kt +++ b/demo/scheme/src/jvmMain/kotlin/Main.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import space.kscience.kmath.geometry.Angle -import space.kscience.maps.features.FeatureGroup +import space.kscience.maps.features.FeatureStore import space.kscience.maps.features.ViewConfig import space.kscience.maps.features.ViewPoint import space.kscience.maps.features.color @@ -29,7 +29,7 @@ fun App() { MaterialTheme { val scope = rememberCoroutineScope() - val features: FeatureGroup = FeatureGroup.remember(XYCoordinateSpace) { + val features = FeatureStore.remember(XYCoordinateSpace) { 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) diff --git a/demo/trajectory-playground/src/jvmMain/kotlin/Main.kt b/demo/trajectory-playground/src/jvmMain/kotlin/Main.kt index 2c088ce..1f676ce 100644 --- a/demo/trajectory-playground/src/jvmMain/kotlin/Main.kt +++ b/demo/trajectory-playground/src/jvmMain/kotlin/Main.kt @@ -22,7 +22,7 @@ private fun Vector2D.toXY() = XY(x.toFloat(), y.toFloat()) private val random = Random(123) -fun FeatureGroup.trajectory( +fun FeatureBuilder.trajectory( trajectory: Trajectory2D, colorPicker: (Trajectory2D) -> Color = { Color.Blue }, ): FeatureRef> = group { @@ -54,12 +54,12 @@ fun FeatureGroup.trajectory( } } -fun FeatureGroup.obstacle(obstacle: Obstacle, colorPicker: (Trajectory2D) -> Color = { Color.Red }) { +fun FeatureBuilder.obstacle(obstacle: Obstacle, colorPicker: (Trajectory2D) -> Color = { Color.Red }) { trajectory(obstacle.circumvention, colorPicker) polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray) } -fun FeatureGroup.pose(pose2D: Pose2D) = with(Float64Space2D) { +fun FeatureBuilder.pose(pose2D: Pose2D) = with(Float64Space2D) { line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY()) } diff --git a/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapView.kt b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapView.kt index eaef886..7a2c75a 100644 --- a/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapView.kt +++ b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/MapView.kt @@ -33,7 +33,7 @@ private val logger = KotlinLogging.logger("MapView") public fun MapView( mapState: MapCanvasState, mapTileProvider: MapTileProvider, - features: FeatureGroup, + featureStore: FeatureStore, modifier: Modifier, ) { val mapTiles = remember(mapTileProvider) { @@ -87,7 +87,7 @@ public fun MapView( } - FeatureCanvas(mapState, features, modifier = modifier.canvasControls(mapState, features)) { + FeatureCanvas(mapState, featureStore.featureFlow, modifier = modifier.canvasControls(mapState, featureStore)) { val tileScale = mapState.tileScale clipRect { @@ -112,19 +112,19 @@ public fun MapView( } /** - * Create a [MapView] with given [features] group. + * Create a [MapView] with given [featureStore] group. */ @Composable public fun MapView( mapTileProvider: MapTileProvider, config: ViewConfig, - features: FeatureGroup, + featureStore: FeatureStore, initialViewPoint: ViewPoint? = null, initialRectangle: Rectangle? = null, modifier: Modifier, ) { val mapState = MapCanvasState.remember(mapTileProvider, config, initialViewPoint, initialRectangle) - MapView(mapState, mapTileProvider, features, modifier) + MapView(mapState, mapTileProvider, featureStore, modifier) } /** @@ -141,9 +141,9 @@ public fun MapView( initialViewPoint: ViewPoint? = null, initialRectangle: Rectangle? = null, modifier: Modifier = Modifier.fillMaxSize(), - buildFeatures: FeatureGroup.() -> Unit = {}, + buildFeatures: FeatureStore.() -> Unit = {}, ) { - val featureState = FeatureGroup.remember(WebMercatorSpace, buildFeatures) + val featureState = FeatureStore.remember(WebMercatorSpace, buildFeatures) val computedRectangle = initialRectangle ?: featureState.getBoundingBox() MapView(mapTileProvider, config, featureState, initialViewPoint, computedRectangle, modifier) } \ No newline at end of file diff --git a/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/mapFeatures.kt b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/mapFeatures.kt index da9cf36..0e1dec8 100644 --- a/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/mapFeatures.kt +++ b/maps-kt-compose/src/commonMain/kotlin/space/kscience/maps/compose/mapFeatures.kt @@ -13,12 +13,12 @@ import space.kscience.maps.features.* import kotlin.math.ceil -internal fun FeatureGroup.coordinatesOf(pair: Pair) = +internal fun FeatureBuilder.coordinatesOf(pair: Pair) = GeodeticMapCoordinates.ofDegrees(pair.first.toDouble(), pair.second.toDouble()) public typealias MapFeature = Feature -public fun FeatureGroup.circle( +public fun FeatureBuilder.circle( centerCoordinates: Pair, size: Dp = 5.dp, id: String? = null, @@ -26,7 +26,7 @@ public fun FeatureGroup.circle( id, CircleFeature(space, coordinatesOf(centerCoordinates), size) ) -public fun FeatureGroup.rectangle( +public fun FeatureBuilder.rectangle( centerCoordinates: Pair, size: DpSize = DpSize(5.dp, 5.dp), id: String? = null, @@ -35,7 +35,7 @@ public fun FeatureGroup.rectangle( ) -public fun FeatureGroup.draw( +public fun FeatureBuilder.draw( position: Pair, id: String? = null, draw: DrawScope.() -> Unit, @@ -45,7 +45,7 @@ public fun FeatureGroup.draw( ) -public fun FeatureGroup.line( +public fun FeatureBuilder.line( curve: GmcCurve, id: String? = null, ): FeatureRef> = feature( @@ -56,7 +56,7 @@ public fun FeatureGroup.line( /** * A segmented geodetic curve */ -public fun FeatureGroup.geodeticLine( +public fun FeatureBuilder.geodeticLine( curve: GmcCurve, ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84, maxLineDistance: Distance = 100.kilometers, @@ -79,7 +79,7 @@ public fun FeatureGroup.geodeticLine( multiLine(points.map { it.coordinates }, id = id) } -public fun FeatureGroup.geodeticLine( +public fun FeatureBuilder.geodeticLine( from: Gmc, to: Gmc, ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84, @@ -87,7 +87,7 @@ public fun FeatureGroup.geodeticLine( id: String? = null, ): FeatureRef> = geodeticLine(ellipsoid.curveBetween(from, to), ellipsoid, maxLineDistance, id) -public fun FeatureGroup.line( +public fun FeatureBuilder.line( aCoordinates: Pair, bCoordinates: Pair, id: String? = null, @@ -96,7 +96,7 @@ public fun FeatureGroup.line( LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates)) ) -public fun FeatureGroup.arc( +public fun FeatureBuilder.arc( center: Pair, radius: Distance, startAngle: Angle, @@ -112,17 +112,17 @@ public fun FeatureGroup.arc( ) ) -public fun FeatureGroup.points( +public fun FeatureBuilder.points( points: List>, id: String? = null, ): FeatureRef> = feature(id, PointsFeature(space, points.map(::coordinatesOf))) -public fun FeatureGroup.multiLine( +public fun FeatureBuilder.multiLine( points: List>, id: String? = null, ): FeatureRef> = feature(id, MultiLineFeature(space, points.map(::coordinatesOf))) -public fun FeatureGroup.icon( +public fun FeatureBuilder.icon( position: Pair, image: ImageVector, size: DpSize = DpSize(20.dp, 20.dp), @@ -137,7 +137,7 @@ public fun FeatureGroup.icon( ) ) -public fun FeatureGroup.text( +public fun FeatureBuilder.text( position: Pair, text: String, font: Font.() -> Unit = { size = 16f }, @@ -147,7 +147,7 @@ public fun FeatureGroup.text( TextFeature(space, coordinatesOf(position), text, fontConfig = font) ) -public fun FeatureGroup.pixelMap( +public fun FeatureBuilder.pixelMap( rectangle: Rectangle, latitudeDelta: Angle, longitudeDelta: Angle, diff --git a/maps-kt-features/build.gradle.kts b/maps-kt-features/build.gradle.kts index c1e4895..bd3b35c 100644 --- a/maps-kt-features/build.gradle.kts +++ b/maps-kt-features/build.gradle.kts @@ -35,5 +35,6 @@ kscience { api(compose.material) api(compose.ui) api("io.github.oshai:kotlin-logging:6.0.3") + api("com.benasher44:uuid:0.8.4") } } \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt index cc650aa..8e7b006 100644 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt @@ -18,7 +18,7 @@ import kotlin.math.min */ public fun Modifier.canvasControls( state: CanvasState, - features: FeatureGroup, + features: FeatureStore, ): Modifier = with(state) { // //selecting all tapabales ahead of time @@ -36,7 +36,7 @@ public fun Modifier.canvasControls( val point = state.space.ViewPoint(coordinates, zoom) if (event.type == PointerEventType.Move) { - features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners -> + features.forEachWithAttribute(HoverListenerAttribute) { id, feature, listeners -> if (point in feature as DomainFeature) { listeners.forEach { it.handle(event, point) } return@forEachWithAttribute @@ -67,7 +67,7 @@ public fun Modifier.canvasControls( point ) - features.forEachWithAttributeUntil(ClickListenerAttribute) { _, feature, listeners -> + features.forEachWithAttributeUntil(ClickListenerAttribute) {_, feature, listeners -> if (point in (feature as DomainFeature)) { listeners.forEach { it.handle(event, point) } false 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 a8d81be..6ced25b 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 @@ -2,6 +2,9 @@ package space.kscience.maps.features import androidx.compose.foundation.Canvas import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -16,6 +19,7 @@ import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.DpRect import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.flow.StateFlow import space.kscience.attributes.Attributes /** @@ -52,6 +56,7 @@ public class ComposeFeatureDrawScope( ) : FeatureDrawScope(state), DrawScope by drawScope { override fun drawText(text: String, position: Offset, attributes: Attributes) { try { + //TODO don't draw text that is not on screen drawText(textMeasurer ?: error("Text measurer not defined"), text, position) } catch (ex: Exception) { logger.error(ex) { "Failed to measure text" } @@ -73,15 +78,19 @@ public class ComposeFeatureDrawScope( @Composable public fun FeatureCanvas( state: CanvasState, - features: FeatureGroup, + featureFlow: StateFlow>>, modifier: Modifier = Modifier, draw: FeatureDrawScope.() -> Unit = {}, ) { val textMeasurer = rememberTextMeasurer(0) - val painterCache: Map, Painter> = features.features.flatMap { - if (it is FeatureGroup) it.features else listOf(it) - }.filterIsInstance>().associateWith { it.getPainter() } + val features by featureFlow.collectAsState() + + val painterCache = key(features) { + features.values + .filterIsInstance>() + .associateWith { it.getPainter() } + } Canvas(modifier) { if (state.canvasSize != size.toDpSize()) { @@ -89,7 +98,7 @@ public fun FeatureCanvas( } ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply { clipRect { - features.featureMap.values.sortedBy { it.z } + features.values.sortedBy { it.z } .filter { state.viewPoint.zoom in it.zoomRange } .forEach { feature -> this@apply.drawFeature(feature) diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureGroup.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureStore.kt similarity index 52% rename from maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureGroup.kt rename to maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureStore.kt index d218c22..efd4232 100644 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureGroup.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureStore.kt @@ -1,7 +1,7 @@ package space.kscience.maps.features -import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.painter.Painter @@ -9,89 +9,103 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import com.benasher44.uuid.Uuid +import com.benasher44.uuid.uuid4 +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.jetbrains.skia.Font import space.kscience.attributes.Attribute 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 //@JvmInline //public value class FeatureId>(public val id: String) -public class FeatureRef>(public val id: String, public val parent: FeatureGroup) +public class FeatureRef>(public val store: FeatureStore, public val id: String) @Suppress("UNCHECKED_CAST") public fun > FeatureRef.resolve(): F = - parent.featureMap[id]?.let { it as F } ?: error("Feature with id=$id not found") + store.features[id]?.let { it as F } ?: error("Feature with id=$id not found") public val > FeatureRef.attributes: Attributes get() = resolve().attributes -/** - * A group of other features - */ -public data class FeatureGroup( +public fun Uuid.toIndex(): String = leastSignificantBits.toString(16) + +public interface FeatureBuilder { + public val space: CoordinateSpace + public fun > feature(id: String?, feature: F): FeatureRef + + public fun group( + id: String? = null, + attributes: Attributes = Attributes.EMPTY, + builder: FeatureGroup.() -> Unit, + ): FeatureRef> + + public fun removeFeature(id: String) +} + +public interface FeatureSet { + public val features: Map> + + /** + * Create a reference + */ + public fun > ref(id: String): FeatureRef +} + + +public class FeatureStore( override val space: CoordinateSpace, - public val featureMap: SnapshotStateMap> = mutableStateMapOf(), -) : CoordinateSpace by space, Feature { +) : CoordinateSpace by space, FeatureBuilder, FeatureSet { + private val _featureFlow = MutableStateFlow>>(emptyMap()) - private val attributesState: MutableState = mutableStateOf(Attributes.EMPTY) + public val featureFlow: StateFlow>> get() = _featureFlow - override val attributes: Attributes get() = attributesState.value + override val features: Map> get() = featureFlow.value -// -// @Suppress("UNCHECKED_CAST") -// public operator fun > get(id: FeatureId): F = -// featureMap[id.id]?.let { it as F } ?: error("Feature with id=$id not found") - - private var uidCounter = 0 - - private fun generateUID(feature: Feature?): String = if (feature == null) { - "@group[${uidCounter++}]" - } else { - "@${feature::class.simpleName}[${uidCounter++}]" + override fun > feature(id: String?, feature: F): FeatureRef { + val safeId = id ?: generateId(feature) + _featureFlow.value += (safeId to feature) + return FeatureRef(this, safeId) } - public fun > feature(id: String?, feature: F): FeatureRef { - val safeId = id ?: generateUID(feature) - featureMap[safeId] = feature - return FeatureRef(safeId, this) + override fun group( + id: String?, + attributes: Attributes, + builder: FeatureGroup.() -> Unit, + ): FeatureRef> { + val safeId = id ?: generateId(null) + return feature(safeId, FeatureGroup(this, safeId, attributes).apply(builder)) } - public fun removeFeature(id: String) { - featureMap.remove(id) + override fun removeFeature(id: String) { + _featureFlow.value -= id } -// public fun > feature(id: FeatureId, feature: F): FeatureId = feature(id.id, feature) + override fun > ref(id: String): FeatureRef = FeatureRef(this, id) - public val features: Collection> get() = featureMap.values.sortedByDescending { it.z } - - - // -// @Suppress("UNCHECKED_CAST") -// public fun getAttribute(id: FeatureId>, key: Attribute): A? = -// get(id).attributes[key] - - - override fun getBoundingBox(zoom: Float): Rectangle? = with(space) { - featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() - } - - override fun withAttributes(modify: Attributes.() -> Attributes): Feature { - attributesState.value = attributes.modify() - return this + public fun getBoundingBox(zoom: Float = Float.MAX_VALUE): Rectangle? = with(space) { + features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() } public companion object { + internal fun generateId(feature: Feature<*>?): String = if (feature == null) { + "@group[${uuid4().toIndex()}]" + } else { + "${feature::class.simpleName}[${uuid4().toIndex()}]" + } /** * Build, but do not remember map feature state */ public fun build( coordinateSpace: CoordinateSpace, - builder: FeatureGroup.() -> Unit = {}, - ): FeatureGroup = FeatureGroup(coordinateSpace).apply(builder) + builder: FeatureStore.() -> Unit = {}, + ): FeatureStore = FeatureStore(coordinateSpace).apply(builder) /** * Build and remember map feature state @@ -99,79 +113,100 @@ public data class FeatureGroup( @Composable public fun remember( coordinateSpace: CoordinateSpace, - builder: FeatureGroup.() -> Unit = {}, - ): FeatureGroup = remember { + builder: FeatureStore.() -> Unit = {}, + ): FeatureStore = remember { build(coordinateSpace, builder) } - } } +/** + * A group of other features + */ +public data class FeatureGroup internal constructor( + val store: FeatureStore, + val groupId: String, + override val attributes: Attributes, +) : CoordinateSpace by store.space, Feature, FeatureBuilder, FeatureSet { + + override val space: CoordinateSpace get() = store.space + + override fun withAttributes(modify: Attributes.() -> Attributes): FeatureGroup = + FeatureGroup(store, groupId, modify(attributes)) + + + override fun > feature(id: String?, feature: F): FeatureRef = + store.feature("$groupId/${id ?: generateId(feature)}", feature) + + override fun group( + id: String?, + attributes: Attributes, + builder: FeatureGroup.() -> Unit, + ): FeatureRef> { + val safeId = id ?: generateId(null) + return feature(safeId, FeatureGroup(store, "$groupId/$safeId", attributes).apply(builder)) + } + + override fun removeFeature(id: String) { + store.removeFeature("$groupId/$id") + } + + override val features: Map> + get() = store.featureFlow.value + .filterKeys { it.startsWith("$groupId/") } + .mapKeys { it.key.removePrefix("$groupId/") } + .toMap() + + override fun getBoundingBox(zoom: Float): Rectangle? = with(space) { + features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles() + } + + override fun > ref(id: String): FeatureRef = FeatureRef(store, "$groupId/$id") +} + /** * Recursively search for feature until function returns true */ -public fun FeatureGroup.forEachUntil(visitor: FeatureGroup.(id: String, feature: Feature) -> Boolean) { - featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) -> - if (feature is FeatureGroup) { - feature.forEachUntil(visitor) - } else { - if (!visitor(this, key, feature)) return@forEachUntil - } +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 } } -/** - * Recursively visit all features in this group - */ -public fun FeatureGroup.forEach( - visitor: FeatureGroup.(id: String, feature: Feature) -> Unit, -): Unit = forEachUntil { id, feature -> - visitor(id, feature) - true -} - /** * Process all features with a given attribute from the one with highest [z] to lowest */ -public fun FeatureGroup.forEachWithAttribute( +public inline fun FeatureSet.forEachWithAttribute( key: Attribute, - block: FeatureGroup.(id: String, feature: Feature, attributeValue: A) -> Unit, + block: FeatureSet.(ref: FeatureRef, feature: Feature, attribute: A) -> Unit, ) { - forEach { id, feature -> + features.forEach { (id, feature) -> feature.attributes[key]?.let { - block(id, feature, it) + block(ref>(id), feature, it) } } } -public fun FeatureGroup.forEachWithAttributeUntil( +public inline fun FeatureSet.forEachWithAttributeUntil( key: Attribute, - block: FeatureGroup.(id: String, feature: Feature, attributeValue: A) -> Boolean, + block: FeatureSet.(ref: FeatureRef, feature: Feature, attribute: A) -> Boolean, ) { - forEachUntil { id, feature -> + features.forEach { (id, feature) -> feature.attributes[key]?.let { - block(id, feature, it) - } ?: true + if (!block(ref>(id), feature, it)) return@forEachWithAttributeUntil + } } } -public inline fun > FeatureGroup.forEachWithType( - crossinline block: (FeatureRef) -> Unit, +public inline fun > FeatureSet.forEachWithType( + crossinline block: FeatureSet.(ref: FeatureRef, feature: F) -> Unit, ) { - forEach { id, feature -> - if (feature is F) block(FeatureRef(id, this)) + features.forEach { (id, feature) -> + if (feature is F) block(ref(id), feature) } } -public inline fun > FeatureGroup.forEachWithTypeUntil( - crossinline block: (FeatureRef) -> Boolean, -) { - forEachUntil { id, feature -> - if (feature is F) block(FeatureRef(id, this)) else true - } -} - -public fun FeatureGroup.circle( +public fun FeatureBuilder.circle( center: T, size: Dp = 5.dp, attributes: Attributes = Attributes.EMPTY, @@ -180,7 +215,7 @@ public fun FeatureGroup.circle( id, CircleFeature(space, center, size, attributes) ) -public fun FeatureGroup.rectangle( +public fun FeatureBuilder.rectangle( centerCoordinates: T, size: DpSize = DpSize(5.dp, 5.dp), attributes: Attributes = Attributes.EMPTY, @@ -189,7 +224,7 @@ public fun FeatureGroup.rectangle( id, RectangleFeature(space, centerCoordinates, size, attributes) ) -public fun FeatureGroup.draw( +public fun FeatureBuilder.draw( position: T, attributes: Attributes = Attributes.EMPTY, id: String? = null, @@ -199,7 +234,7 @@ public fun FeatureGroup.draw( DrawFeature(space, position, drawFeature = draw, attributes = attributes) ) -public fun FeatureGroup.line( +public fun FeatureBuilder.line( aCoordinates: T, bCoordinates: T, attributes: Attributes = Attributes.EMPTY, @@ -209,7 +244,7 @@ public fun FeatureGroup.line( LineFeature(space, aCoordinates, bCoordinates, attributes) ) -public fun FeatureGroup.arc( +public fun FeatureBuilder.arc( oval: Rectangle, startAngle: Angle, arcLength: Angle, @@ -220,7 +255,7 @@ public fun FeatureGroup.arc( ArcFeature(space, oval, startAngle, arcLength, attributes) ) -public fun FeatureGroup.points( +public fun FeatureBuilder.points( points: List, attributes: Attributes = Attributes.EMPTY, id: String? = null, @@ -229,7 +264,7 @@ public fun FeatureGroup.points( PointsFeature(space, points, attributes) ) -public fun FeatureGroup.multiLine( +public fun FeatureBuilder.multiLine( points: List, attributes: Attributes = Attributes.EMPTY, id: String? = null, @@ -238,7 +273,7 @@ public fun FeatureGroup.multiLine( MultiLineFeature(space, points, attributes) ) -public fun FeatureGroup.polygon( +public fun FeatureBuilder.polygon( points: List, attributes: Attributes = Attributes.EMPTY, id: String? = null, @@ -247,7 +282,7 @@ public fun FeatureGroup.polygon( PolygonFeature(space, points, attributes) ) -public fun FeatureGroup.icon( +public fun FeatureBuilder.icon( position: T, image: ImageVector, size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), @@ -264,16 +299,7 @@ public fun FeatureGroup.icon( ) ) -public fun FeatureGroup.group( - id: String? = null, - builder: FeatureGroup.() -> Unit, -): FeatureRef> { - val collection = FeatureGroup(space).apply(builder) - val feature = FeatureGroup(space, collection.featureMap) - return feature(id, feature) -} - -public fun FeatureGroup.scalableImage( +public fun FeatureBuilder.scalableImage( box: Rectangle, attributes: Attributes = Attributes.EMPTY, id: String? = null, @@ -283,7 +309,7 @@ public fun FeatureGroup.scalableImage( ScalableImageFeature(space, box, painter = painter, attributes = attributes) ) -public fun FeatureGroup.text( +public fun FeatureBuilder.text( position: T, text: String, font: Font.() -> Unit = { size = 16f }, @@ -304,7 +330,7 @@ public inline fun Structure2D(rows: Int, columns: Int, initializer: return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }).as2D() } -public fun FeatureGroup.pixelMap( +public fun FeatureStore.pixelMap( rectangle: Rectangle, pixelMap: Structure2D, attributes: Attributes = Attributes.EMPTY, @@ -320,7 +346,7 @@ public fun FeatureGroup.pixelMap( public fun FeatureGroup<*>.toPrettyString(): String { fun StringBuilder.printGroup(id: String, group: FeatureGroup<*>, prefix: String) { appendLine("${prefix}* [group] $id") - group.featureMap.forEach { (id, feature) -> + group.features.forEach { (id, feature) -> if (feature is FeatureGroup<*>) { printGroup(id, feature, " ") } else { 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 de92173..4a68d02 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 @@ -4,7 +4,7 @@ import space.kscience.attributes.Attributes import kotlin.jvm.JvmName -public fun FeatureGroup.draggableLine( +public fun FeatureBuilder.draggableLine( aId: FeatureRef>, bId: FeatureRef>, id: String? = null, @@ -39,7 +39,7 @@ public fun FeatureGroup.draggableLine( return drawLine() } -public fun FeatureGroup.draggableMultiLine( +public fun FeatureBuilder.draggableMultiLine( points: List>>, id: String? = null, ): FeatureRef> { @@ -71,7 +71,7 @@ public fun FeatureGroup.draggableMultiLine( } @JvmName("draggableMultiLineFromPoints") -public fun FeatureGroup.draggableMultiLine( +public fun FeatureBuilder.draggableMultiLine( points: List, id: String? = null, ): FeatureRef> { 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 6af5d06..f313fde 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 @@ -94,7 +94,7 @@ public fun FeatureDrawScope.drawFeature( } is FeatureGroup -> { - feature.featureMap.values.forEach { + feature.features.values.forEach { drawFeature( it.withAttributes { feature.attributes + this diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/mapFeatureAttributes.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/mapFeatureAttributes.kt index 8c10a05..dcdaa42 100644 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/mapFeatureAttributes.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/mapFeatureAttributes.kt @@ -55,7 +55,7 @@ public fun > FeatureRef.modifyAttributes( modification: AttributesBuilder.() -> Unit, ): FeatureRef { @Suppress("UNCHECKED_CAST") - parent.feature( + store.feature( id, resolve().withAttributes { modified(modification) } as F ) @@ -67,7 +67,7 @@ public fun , V> FeatureRef.modifyAttribute( value: V, ): FeatureRef { @Suppress("UNCHECKED_CAST") - parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F) + store.feature(id, resolve().withAttributes { withAttribute(key, value) } as F) return this } @@ -80,10 +80,10 @@ public fun , V> FeatureRef.modifyAttribute( public fun > FeatureRef.draggable( constraint: ((T) -> T)? = null, listener: (PointerEvent.(from: ViewPoint, to: ViewPoint) -> Unit)? = null, -): FeatureRef = with(parent) { +): FeatureRef = with(store) { if (attributes[DraggableAttribute] == null) { val handle = DragHandle.withPrimaryButton { event, start, end -> - val feature = featureMap[id] as? DraggableFeature ?: return@withPrimaryButton DragResult(end) + val feature = features[id] as? DraggableFeature ?: return@withPrimaryButton DragResult(end) start as ViewPoint end as ViewPoint if (start in feature) { diff --git a/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/geoJsonToMap.kt b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/geoJsonToMap.kt index 352376b..188c41f 100644 --- a/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/geoJsonToMap.kt +++ b/maps-kt-geojson/src/commonMain/kotlin/space/kscience/maps/geojson/geoJsonToMap.kt @@ -12,7 +12,7 @@ import space.kscience.maps.features.* /** * Add a single Json geometry to a feature builder */ -public fun FeatureGroup.geoJsonGeometry( +public fun FeatureBuilder.geoJsonGeometry( geometry: GeoJsonGeometry, id: String? = null, ): FeatureRef> = when (geometry) { @@ -50,11 +50,11 @@ public fun FeatureGroup.geoJsonGeometry( } } -public fun FeatureGroup.geoJsonFeature( +public fun FeatureBuilder.geoJsonFeature( geoJson: GeoJsonFeature, id: String? = null, ): FeatureRef> { - val geometry = geoJson.geometry ?: return group {} + val geometry = geoJson.geometry ?: return group(null) {} val idOverride = id ?: geoJson.getProperty("id")?.jsonPrimitive?.contentOrNull return geoJsonGeometry(geometry, idOverride).modifyAttributes { @@ -72,7 +72,7 @@ public fun FeatureGroup.geoJsonFeature( } } -public fun FeatureGroup.geoJson( +public fun FeatureBuilder.geoJson( geoJson: GeoJson, id: String? = null, ): FeatureRef> = when (geoJson) { diff --git a/maps-kt-geojson/src/jvmMain/kotlin/space/kscience/maps/geojson/geoJsonFeatureJvm.kt b/maps-kt-geojson/src/jvmMain/kotlin/space/kscience/maps/geojson/geoJsonFeatureJvm.kt index 5a1fbe3..2060c92 100644 --- a/maps-kt-geojson/src/jvmMain/kotlin/space/kscience/maps/geojson/geoJsonFeatureJvm.kt +++ b/maps-kt-geojson/src/jvmMain/kotlin/space/kscience/maps/geojson/geoJsonFeatureJvm.kt @@ -4,14 +4,14 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import space.kscience.maps.coordinates.Gmc import space.kscience.maps.features.Feature -import space.kscience.maps.features.FeatureGroup +import space.kscience.maps.features.FeatureBuilder import space.kscience.maps.features.FeatureRef import java.net.URL /** * Add geojson features from url */ -public fun FeatureGroup.geoJson( +public fun FeatureBuilder.geoJson( geoJsonUrl: URL, id: String? = null, ): FeatureRef> { diff --git a/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/SchemeView.kt b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/SchemeView.kt index 2caf3e5..b60d106 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/SchemeView.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/SchemeView.kt @@ -15,10 +15,10 @@ private val logger = KotlinLogging.logger("SchemeView") @Composable public fun SchemeView( state: XYCanvasState, - features: FeatureGroup, + featureStore: FeatureStore, modifier: Modifier = Modifier.fillMaxSize(), ): Unit { - FeatureCanvas(state, features, modifier = modifier.canvasControls(state, features)) + FeatureCanvas(state, featureStore.featureFlow, modifier = modifier.canvasControls(state, featureStore)) } @@ -38,7 +38,7 @@ public fun Rectangle.computeViewPoint( */ @Composable public fun SchemeView( - features: FeatureGroup, + features: FeatureStore, initialViewPoint: ViewPoint? = null, initialRectangle: Rectangle? = null, config: ViewConfig = ViewConfig(), @@ -67,14 +67,13 @@ public fun SchemeView( initialRectangle: Rectangle? = null, config: ViewConfig = ViewConfig(), modifier: Modifier = Modifier.fillMaxSize(), - buildFeatures: FeatureGroup.() -> Unit = {}, + buildFeatures: FeatureStore.() -> Unit = {}, ) { - val featureState = FeatureGroup.remember(XYCoordinateSpace, buildFeatures) + val featureState = FeatureStore.remember(XYCoordinateSpace, buildFeatures) val mapState: XYCanvasState = XYCanvasState.remember( config, initialViewPoint = initialViewPoint, - initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox( - XYCoordinateSpace, + initialRectangle = initialRectangle ?: featureState.getBoundingBox( Float.MAX_VALUE ), ) diff --git a/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/schemeFeatures.kt b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/schemeFeatures.kt index c795eeb..3abeca0 100644 --- a/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/schemeFeatures.kt +++ b/maps-kt-scheme/src/commonMain/kotlin/space/kscience/maps/scheme/schemeFeatures.kt @@ -15,7 +15,7 @@ import kotlin.math.ceil internal fun Pair.toCoordinates(): XY = XY(first.toFloat(), second.toFloat()) -public fun FeatureGroup.background( +public fun FeatureBuilder.background( width: Float, height: Float, offset: XY = XY(0f, 0f), @@ -37,26 +37,26 @@ public fun FeatureGroup.background( ) } -public fun FeatureGroup.circle( +public fun FeatureBuilder.circle( centerCoordinates: Pair, size: Dp = 5.dp, id: String? = null, ): FeatureRef> = circle(centerCoordinates.toCoordinates(), size, id = id) -public fun FeatureGroup.draw( +public fun FeatureBuilder.draw( position: Pair, id: String? = null, draw: DrawScope.() -> Unit, ): FeatureRef> = draw(position.toCoordinates(), id = id, draw = draw) -public fun FeatureGroup.line( +public fun FeatureBuilder.line( aCoordinates: Pair, bCoordinates: Pair, id: String? = null, ): FeatureRef> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), id = id) -public fun FeatureGroup.arc( +public fun FeatureBuilder.arc( center: Pair, radius: Float, startAngle: Angle, @@ -69,7 +69,7 @@ public fun FeatureGroup.arc( id = id ) -public fun FeatureGroup.image( +public fun FeatureBuilder.image( position: Pair, image: ImageVector, size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), @@ -77,13 +77,13 @@ public fun FeatureGroup.image( ): FeatureRef> = icon(position.toCoordinates(), image, size = size, id = id) -public fun FeatureGroup.text( +public fun FeatureBuilder.text( position: Pair, text: String, id: String? = null, ): FeatureRef> = text(position.toCoordinates(), text, id = id) -public fun FeatureGroup.pixelMap( +public fun FeatureBuilder.pixelMap( rectangle: Rectangle, xSize: Float, ySize: Float, @@ -108,7 +108,7 @@ public fun FeatureGroup.pixelMap( ) ) -public fun FeatureGroup.rectanglePolygon( +public fun FeatureBuilder.rectanglePolygon( left: Number, right: Number, bottom: Number, top: Number, attributes: Attributes = Attributes.EMPTY, @@ -123,7 +123,7 @@ public fun FeatureGroup.rectanglePolygon( attributes, id ) -public fun FeatureGroup.rectanglePolygon( +public fun FeatureBuilder.rectanglePolygon( rectangle: Rectangle, attributes: Attributes = Attributes.EMPTY, id: String? = null, 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 0fc4131..137ee9b 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 @@ -17,11 +17,9 @@ public class FeatureStateSnapshot( ) @Composable -public fun FeatureGroup.snapshot(): FeatureStateSnapshot = FeatureStateSnapshot( - featureMap, - features.flatMap { - if (it is FeatureGroup) it.features else listOf(it) - }.filterIsInstance>().associateWith { it.getPainter() } +public fun FeatureSet.snapshot(): FeatureStateSnapshot = FeatureStateSnapshot( + features, + features.values.filterIsInstance>().associateWith { it.getPainter() } ) From 29074a96240d2c799d6b7744cfb354d4551e0e16 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 7 Jul 2024 13:20:57 +0300 Subject: [PATCH 2/6] 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) From 3a4c9133c62ced8202fc652da8fc4907230f6f59 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 8 Jul 2024 09:13:42 +0300 Subject: [PATCH 3/6] Add bulk set for features --- .../maps/features/FeatureDrawScope.kt | 23 +++++++++++-------- .../kscience/maps/features/FeatureStore.kt | 21 +++++++++++++++-- .../kscience/maps/features/drawFeature.kt | 7 ------ 3 files changed, 33 insertions(+), 18 deletions(-) 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 a71d0fa..028fd9f 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 @@ -4,7 +4,6 @@ import androidx.compose.foundation.Canvas import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -19,9 +18,13 @@ import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.DpRect import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.sample import space.kscience.attributes.Attributes import space.kscience.attributes.plus +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds /** * An extension of [DrawScope] to include map-specific features @@ -76,22 +79,23 @@ public class ComposeFeatureDrawScope( /** * Create a canvas with extended functionality (e.g., drawing text) */ +@OptIn(FlowPreview::class) @Composable public fun FeatureCanvas( state: CanvasState, featureFlow: StateFlow>>, modifier: Modifier = Modifier, + sampleDuration: Duration = 20.milliseconds, draw: FeatureDrawScope.() -> Unit = {}, ) { val textMeasurer = rememberTextMeasurer(0) - val features by featureFlow.collectAsState() + val features by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value) + + val painterCache = features.values + .filterIsInstance>() + .associateWith { it.getPainter() } - val painterCache = key(features) { - features.values - .filterIsInstance>() - .associateWith { it.getPainter() } - } Canvas(modifier) { if (state.canvasSize != size.toDpSize()) { @@ -102,12 +106,13 @@ public fun FeatureCanvas( val attributesCache = mutableMapOf, Attributes>() - fun computeGroupAttributes(path: List): Attributes = attributesCache.getOrPut(path){ + 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) + computeGroupAttributes(path.dropLast(1)) + (features[path.first()]?.attributes + ?: Attributes.EMPTY) } } 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 71aaf4e..b8f4e89 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 @@ -46,10 +46,12 @@ public interface FeatureBuilder { public val space: CoordinateSpace /** - * Add or replace feature. If [id] is null, then a unique id is genertated + * Add or replace feature. If [id] is null, then a unique id is generated */ public fun > feature(id: String?, feature: F): FeatureRef + public fun putFeatures(features: Map?>) + /** * Update existing feature if it is present and is of type [F] */ @@ -89,6 +91,18 @@ public class FeatureStore( return FeatureRef(this, safeId) } + public override fun putFeatures(features: Map?>) { + _featureFlow.value = _featureFlow.value.toMutableMap().apply { + features.forEach { (key, value) -> + if (value == null) { + remove(key) + } else { + put(key, value) + } + } + } + } + @Suppress("UNCHECKED_CAST") override fun > updateFeature(id: String, block: (F?) -> F): FeatureRef = feature(id, block(features[id] as? F)) @@ -159,10 +173,13 @@ public data class FeatureGroup internal constructor( override fun withAttributes(modify: Attributes.() -> Attributes): FeatureGroup = FeatureGroup(store, groupId, modify(attributes)) - override fun > feature(id: String?, feature: F): FeatureRef = store.feature("$groupId/${id ?: generateFeatureId(feature)}", feature) + public override fun putFeatures(features: Map?>) { + store.putFeatures(features.mapKeys { "$groupId/${it.key}" }) + } + override fun > updateFeature(id: String, block: (F?) -> F): FeatureRef = store.updateFeature("$groupId/$id", block) 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 5b1de31..1c58f01 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 @@ -98,13 +98,6 @@ public fun FeatureDrawScope.drawFeature( is FeatureGroup -> { //ignore groups -// feature.features.values.forEach { -// drawFeature( -// it.withAttributes { -// feature.attributes + this -// } -// ) -// } } is PathFeature -> { From bf128a3eb9520c6076634f7fec4075f7bc97d1f5 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 8 Jul 2024 14:03:59 +0600 Subject: [PATCH 4/6] extract GroupAttributesCalculator --- .../maps/features/FeatureDrawScope.kt | 16 +++------------- .../maps/utils/GroupAttributesCalculator.kt | 19 +++++++++++++++++++ .../space/kscience/maps/svg/exportToSvg.kt | 14 +++----------- 3 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 maps-kt-features/src/commonMain/kotlin/space/kscience/maps/utils/GroupAttributesCalculator.kt 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 028fd9f..d5d7ce0 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 @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.sample import space.kscience.attributes.Attributes import space.kscience.attributes.plus +import space.kscience.maps.utils.GroupAttributesCalculator import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -103,24 +104,13 @@ public fun FeatureCanvas( } 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) - } - } + val attributesCalculator = GroupAttributesCalculator(features) 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))) + drawFeature(feature, attributesCalculator.computeGroupAttributes(path.dropLast(1))) } } } diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/utils/GroupAttributesCalculator.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/utils/GroupAttributesCalculator.kt new file mode 100644 index 0000000..4b9d35e --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/utils/GroupAttributesCalculator.kt @@ -0,0 +1,19 @@ +package space.kscience.maps.utils + +import space.kscience.attributes.Attributes +import space.kscience.attributes.plus +import space.kscience.maps.features.Feature + +public class GroupAttributesCalculator( + private val features: Map>, + private val attributesCache: MutableMap, Attributes> = mutableMapOf() +) { + public 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) + } + } +} 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 3f6993f..420a819 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 @@ -11,6 +11,7 @@ import space.kscience.attributes.plus import space.kscience.maps.features.* import space.kscience.maps.scheme.XY import space.kscience.maps.scheme.XYCanvasState +import space.kscience.maps.utils.GroupAttributesCalculator public class FeatureStateSnapshot( @@ -165,19 +166,10 @@ public fun FeatureStateSnapshot.generateSvg( 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 attributesCalculator = GroupAttributesCalculator(features) val path = id.split("/") - drawFeature(feature, computeGroupAttributes(path.dropLast(1))) + drawFeature(feature, attributesCalculator.computeGroupAttributes(path.dropLast(1))) } } return svgGraphics2D.getSVGElement(id) From 1119d593a22287a42eb3fed1dbb15e82a1825b36 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 8 Jul 2024 14:29:43 +0600 Subject: [PATCH 5/6] Revert "extract GroupAttributesCalculator" This reverts commit bf128a3eb9520c6076634f7fec4075f7bc97d1f5. --- .../maps/features/FeatureDrawScope.kt | 16 +++++++++++++--- .../maps/utils/GroupAttributesCalculator.kt | 19 ------------------- .../space/kscience/maps/svg/exportToSvg.kt | 14 +++++++++++--- 3 files changed, 24 insertions(+), 25 deletions(-) delete mode 100644 maps-kt-features/src/commonMain/kotlin/space/kscience/maps/utils/GroupAttributesCalculator.kt 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 d5d7ce0..028fd9f 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 @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.sample import space.kscience.attributes.Attributes import space.kscience.attributes.plus -import space.kscience.maps.utils.GroupAttributesCalculator import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -104,13 +103,24 @@ public fun FeatureCanvas( } clipRect { ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply { - val attributesCalculator = GroupAttributesCalculator(features) + + 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, attributesCalculator.computeGroupAttributes(path.dropLast(1))) + drawFeature(feature, computeGroupAttributes(path.dropLast(1))) } } } diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/utils/GroupAttributesCalculator.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/utils/GroupAttributesCalculator.kt deleted file mode 100644 index 4b9d35e..0000000 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/utils/GroupAttributesCalculator.kt +++ /dev/null @@ -1,19 +0,0 @@ -package space.kscience.maps.utils - -import space.kscience.attributes.Attributes -import space.kscience.attributes.plus -import space.kscience.maps.features.Feature - -public class GroupAttributesCalculator( - private val features: Map>, - private val attributesCache: MutableMap, Attributes> = mutableMapOf() -) { - public 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) - } - } -} 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 420a819..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 @@ -11,7 +11,6 @@ import space.kscience.attributes.plus import space.kscience.maps.features.* import space.kscience.maps.scheme.XY import space.kscience.maps.scheme.XYCanvasState -import space.kscience.maps.utils.GroupAttributesCalculator public class FeatureStateSnapshot( @@ -166,10 +165,19 @@ public fun FeatureStateSnapshot.generateSvg( features.entries.sortedBy { it.value.z } .filter { state.viewPoint.zoom in it.value.zoomRange } .forEach { (id, feature) -> - val attributesCalculator = GroupAttributesCalculator(features) + 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, attributesCalculator.computeGroupAttributes(path.dropLast(1))) + drawFeature(feature, computeGroupAttributes(path.dropLast(1))) } } return svgGraphics2D.getSVGElement(id) From 5da7ee7944b95ef1d4d70eee8bcf48f47b48b6e7 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 8 Jul 2024 11:55:11 +0300 Subject: [PATCH 6/6] Remove unnecessary argument --- .../kotlin/space/kscience/maps/compose/canvasControls.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt index 8e7b006..e26b8da 100644 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/compose/canvasControls.kt @@ -36,7 +36,7 @@ public fun Modifier.canvasControls( val point = state.space.ViewPoint(coordinates, zoom) if (event.type == PointerEventType.Move) { - features.forEachWithAttribute(HoverListenerAttribute) { id, feature, listeners -> + features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners -> if (point in feature as DomainFeature) { listeners.forEach { it.handle(event, point) } return@forEachWithAttribute