From 62196fc6f5f69fa7b4b1e2fe712a0823a675e110 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 6 Jul 2024 09:54:06 +0300 Subject: [PATCH] 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() } )