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..bfda2ba 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}") + println("Click on $ref") //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..51f5df7 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" @@ -55,6 +55,7 @@ fun App() { } draggableMultiLine( pointRefs + pointRefs.first(), + "line" ) } } 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..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 @@ -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 @@ -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..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 @@ -2,6 +2,8 @@ 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.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -16,7 +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 @@ -52,6 +60,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" } @@ -70,29 +79,48 @@ public class ComposeFeatureDrawScope( /** * Create a canvas with extended functionality (e.g., drawing text) */ +@OptIn(FlowPreview::class) @Composable public fun FeatureCanvas( state: CanvasState, - features: FeatureGroup, + featureFlow: StateFlow>>, modifier: Modifier = Modifier, + sampleDuration: Duration = 20.milliseconds, 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.sample(sampleDuration).collectAsState(featureFlow.value) + + val painterCache = features.values + .filterIsInstance>() + .associateWith { it.getPainter() } + Canvas(modifier) { if (state.canvasSize != size.toDpSize()) { state.canvasSize = size.toDpSize() } - ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply { - clipRect { - features.featureMap.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/FeatureGroup.kt b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureGroup.kt deleted file mode 100644 index d218c22..0000000 --- a/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureGroup.kt +++ /dev/null @@ -1,334 +0,0 @@ -package space.kscience.maps.features - -import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.painter.Painter -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 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 - -//@JvmInline -//public value class FeatureId>(public val id: String) - -public class FeatureRef>(public val id: String, public val parent: FeatureGroup) - -@Suppress("UNCHECKED_CAST") -public fun > FeatureRef.resolve(): F = - parent.featureMap[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( - override val space: CoordinateSpace, - public val featureMap: SnapshotStateMap> = mutableStateMapOf(), -) : CoordinateSpace by space, Feature { - - private val attributesState: MutableState = mutableStateOf(Attributes.EMPTY) - - override val attributes: Attributes get() = attributesState.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++}]" - } - - public fun > feature(id: String?, feature: F): FeatureRef { - val safeId = id ?: generateUID(feature) - featureMap[safeId] = feature - return FeatureRef(safeId, this) - } - - public fun removeFeature(id: String) { - featureMap.remove(id) - } - -// public fun > feature(id: FeatureId, feature: F): FeatureId = feature(id.id, feature) - - 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 companion object { - - /** - * Build, but do not remember map feature state - */ - public fun build( - coordinateSpace: CoordinateSpace, - builder: FeatureGroup.() -> Unit = {}, - ): FeatureGroup = FeatureGroup(coordinateSpace).apply(builder) - - /** - * Build and remember map feature state - */ - @Composable - public fun remember( - coordinateSpace: CoordinateSpace, - builder: FeatureGroup.() -> Unit = {}, - ): FeatureGroup = remember { - build(coordinateSpace, builder) - } - - } -} - -/** - * 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 - } - } -} - -/** - * 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( - key: Attribute, - block: FeatureGroup.(id: String, feature: Feature, attributeValue: A) -> Unit, -) { - forEach { id, feature -> - feature.attributes[key]?.let { - block(id, feature, it) - } - } -} - -public fun FeatureGroup.forEachWithAttributeUntil( - key: Attribute, - block: FeatureGroup.(id: String, feature: Feature, attributeValue: A) -> Boolean, -) { - forEachUntil { id, feature -> - feature.attributes[key]?.let { - block(id, feature, it) - } ?: true - } -} - -public inline fun > FeatureGroup.forEachWithType( - crossinline block: (FeatureRef) -> Unit, -) { - forEach { id, feature -> - if (feature is F) block(FeatureRef(id, this)) - } -} - -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( - center: T, - size: Dp = 5.dp, - attributes: Attributes = Attributes.EMPTY, - id: String? = null, -): FeatureRef> = feature( - id, CircleFeature(space, center, size, attributes) -) - -public fun FeatureGroup.rectangle( - centerCoordinates: T, - size: DpSize = DpSize(5.dp, 5.dp), - attributes: Attributes = Attributes.EMPTY, - id: String? = null, -): FeatureRef> = feature( - id, RectangleFeature(space, centerCoordinates, size, attributes) -) - -public fun FeatureGroup.draw( - position: T, - attributes: Attributes = Attributes.EMPTY, - id: String? = null, - draw: DrawScope.() -> Unit, -): FeatureRef> = feature( - id, - DrawFeature(space, position, drawFeature = draw, attributes = attributes) -) - -public fun FeatureGroup.line( - aCoordinates: T, - bCoordinates: T, - attributes: Attributes = Attributes.EMPTY, - id: String? = null, -): FeatureRef> = feature( - id, - LineFeature(space, aCoordinates, bCoordinates, attributes) -) - -public fun FeatureGroup.arc( - oval: Rectangle, - startAngle: Angle, - arcLength: Angle, - attributes: Attributes = Attributes.EMPTY, - id: String? = null, -): FeatureRef> = feature( - id, - ArcFeature(space, oval, startAngle, arcLength, attributes) -) - -public fun FeatureGroup.points( - points: List, - attributes: Attributes = Attributes.EMPTY, - id: String? = null, -): FeatureRef> = feature( - id, - PointsFeature(space, points, attributes) -) - -public fun FeatureGroup.multiLine( - points: List, - attributes: Attributes = Attributes.EMPTY, - id: String? = null, -): FeatureRef> = feature( - id, - MultiLineFeature(space, points, attributes) -) - -public fun FeatureGroup.polygon( - points: List, - attributes: Attributes = Attributes.EMPTY, - id: String? = null, -): FeatureRef> = feature( - id, - PolygonFeature(space, points, attributes) -) - -public fun FeatureGroup.icon( - position: T, - image: ImageVector, - size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), - attributes: Attributes = Attributes.EMPTY, - id: String? = null, -): FeatureRef> = feature( - id, - VectorIconFeature( - space, - position, - size, - image, - attributes - ) -) - -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( - box: Rectangle, - attributes: Attributes = Attributes.EMPTY, - id: String? = null, - painter: @Composable () -> Painter, -): FeatureRef> = feature( - id, - ScalableImageFeature(space, box, painter = painter, attributes = attributes) -) - -public fun FeatureGroup.text( - position: T, - text: String, - font: Font.() -> Unit = { size = 16f }, - attributes: Attributes = Attributes.EMPTY, - id: String? = null, -): FeatureRef> = feature( - id, - TextFeature(space, position, text, fontConfig = font, attributes = attributes) -) - -//public fun StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND { -// val strides = Strides(shape) -// return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }) -//} - -public inline fun Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D { - val strides = Strides(ShapeND(rows, columns)) - return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }).as2D() -} - -public fun FeatureGroup.pixelMap( - rectangle: Rectangle, - pixelMap: Structure2D, - attributes: Attributes = Attributes.EMPTY, - id: String? = null, -): FeatureRef> = feature( - id, - PixelMapFeature(space, rectangle, pixelMap, attributes = attributes) -) - -/** - * Create a pretty tree-like representation of this feature group - */ -public fun FeatureGroup<*>.toPrettyString(): String { - fun StringBuilder.printGroup(id: String, group: FeatureGroup<*>, prefix: String) { - appendLine("${prefix}* [group] $id") - group.featureMap.forEach { (id, feature) -> - if (feature is FeatureGroup<*>) { - printGroup(id, feature, " ") - } else { - appendLine("$prefix * [${feature::class.simpleName}] $id ") - } - } - } - return buildString { - printGroup("root", this@toPrettyString, "") - } -} \ No newline at end of file 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 new file mode 100644 index 0000000..b8f4e89 --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/space/kscience/maps/features/FeatureStore.kt @@ -0,0 +1,406 @@ +package space.kscience.maps.features + +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 +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.generateFeatureId + +//@JvmInline +//public value class FeatureId>(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 ref $this not found") + +public val > FeatureRef.attributes: Attributes get() = resolve().attributes + +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 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] + */ + public fun > updateFeature(id: String, block: (F?) -> 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, +) : CoordinateSpace by space, FeatureBuilder, FeatureSet { + private val _featureFlow = MutableStateFlow>>(emptyMap()) + + public val featureFlow: StateFlow>> get() = _featureFlow + + override val features: Map> get() = featureFlow.value + + override fun > feature(id: String?, feature: F): FeatureRef { + val safeId = id ?: generateFeatureId(feature) + _featureFlow.value += (safeId to feature) + 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)) + + override fun group( + id: String?, + attributes: Attributes, + builder: FeatureGroup.() -> Unit, + ): FeatureRef> { + val safeId: String = id ?: generateFeatureId>() + return feature(safeId, FeatureGroup(this, safeId, attributes).apply(builder)) + } + + override fun removeFeature(id: String) { + _featureFlow.value -= 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() + } + + + public companion object { + + 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 + */ + public fun build( + coordinateSpace: CoordinateSpace, + builder: FeatureStore.() -> Unit = {}, + ): FeatureStore = FeatureStore(coordinateSpace).apply(builder) + + /** + * Build and remember map feature state + */ + @Composable + public fun remember( + coordinateSpace: CoordinateSpace, + 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 ?: 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) + + + override fun group( + id: String?, + attributes: Attributes, + builder: FeatureGroup.() -> Unit, + ): FeatureRef> { + val safeId = id ?: generateFeatureId>() + 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 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 + } +} + +/** + * Process all features with a given attribute from the one with highest [z] to lowest + */ +public inline fun FeatureSet.forEachWithAttribute( + key: Attribute, + block: FeatureSet.(ref: FeatureRef, feature: Feature, attribute: A) -> Unit, +) { + features.forEach { (id, feature) -> + feature.attributes[key]?.let { + block(ref>(id), feature, it) + } + } +} + +public inline fun FeatureSet.forEachWithAttributeUntil( + key: Attribute, + block: FeatureSet.(ref: FeatureRef, feature: Feature, attribute: A) -> Boolean, +) { + features.forEach { (id, feature) -> + feature.attributes[key]?.let { + if (!block(ref>(id), feature, it)) return@forEachWithAttributeUntil + } + } +} + +public inline fun > FeatureSet.forEachWithType( + crossinline block: FeatureSet.(ref: FeatureRef, feature: F) -> Unit, +) { + features.forEach { (id, feature) -> + if (feature is F) block(ref(id), feature) + } +} + +public fun FeatureBuilder.circle( + center: T, + size: Dp = 5.dp, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = feature( + id, CircleFeature(space, center, size, attributes) +) + +public fun FeatureBuilder.rectangle( + centerCoordinates: T, + size: DpSize = DpSize(5.dp, 5.dp), + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = feature( + id, RectangleFeature(space, centerCoordinates, size, attributes) +) + +public fun FeatureBuilder.draw( + position: T, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, + draw: DrawScope.() -> Unit, +): FeatureRef> = feature( + id, + DrawFeature(space, position, drawFeature = draw, attributes = attributes) +) + +public fun FeatureBuilder.line( + aCoordinates: T, + bCoordinates: T, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = feature( + id, + LineFeature(space, aCoordinates, bCoordinates, attributes) +) + +public fun FeatureBuilder.arc( + oval: Rectangle, + startAngle: Angle, + arcLength: Angle, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = feature( + id, + ArcFeature(space, oval, startAngle, arcLength, attributes) +) + +public fun FeatureBuilder.points( + points: List, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = feature( + id, + PointsFeature(space, points, attributes) +) + +public fun FeatureBuilder.multiLine( + points: List, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = feature( + id, + MultiLineFeature(space, points, attributes) +) + +public fun FeatureBuilder.polygon( + points: List, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = feature( + id, + PolygonFeature(space, points, attributes) +) + +public fun FeatureBuilder.icon( + position: T, + image: ImageVector, + size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = feature( + id, + VectorIconFeature( + space, + position, + size, + image, + attributes + ) +) + +public fun FeatureBuilder.scalableImage( + box: Rectangle, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, + painter: @Composable () -> Painter, +): FeatureRef> = feature( + id, + ScalableImageFeature(space, box, painter = painter, attributes = attributes) +) + +public fun FeatureBuilder.text( + position: T, + text: String, + font: Font.() -> Unit = { size = 16f }, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = feature( + id, + TextFeature(space, position, text, fontConfig = font, attributes = attributes) +) + +//public fun StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND { +// val strides = Strides(shape) +// return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }) +//} + +public inline fun Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D { + val strides = Strides(ShapeND(rows, columns)) + return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }).as2D() +} + +public fun FeatureStore.pixelMap( + rectangle: Rectangle, + pixelMap: Structure2D, + attributes: Attributes = Attributes.EMPTY, + id: String? = null, +): FeatureRef> = feature( + id, + PixelMapFeature(space, rectangle, pixelMap, attributes = attributes) +) + +/** + * Create a pretty tree-like representation of this feature group + */ +public fun FeatureGroup<*>.toPrettyString(): String { + fun StringBuilder.printGroup(id: String, group: FeatureGroup<*>, prefix: String) { + appendLine("${prefix}* [group] $id") + group.features.forEach { (id, feature) -> + if (feature is FeatureGroup<*>) { + printGroup(id, feature, " ") + } else { + appendLine("$prefix * [${feature::class.simpleName}] $id ") + } + } + } + return buildString { + printGroup("root", this@toPrettyString, "") + } +} \ No newline at end of file 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..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 @@ -4,28 +4,20 @@ import space.kscience.attributes.Attributes import kotlin.jvm.JvmName -public fun FeatureGroup.draggableLine( +public fun FeatureBuilder.draggableLine( aId: FeatureRef>, 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 { _, _ -> @@ -39,26 +31,18 @@ public fun FeatureGroup.draggableLine( return drawLine() } -public fun FeatureGroup.draggableMultiLine( +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 { @@ -71,7 +55,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..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 @@ -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,7 @@ public fun FeatureDrawScope.drawFeature( } is FeatureGroup -> { - feature.featureMap.values.forEach { - drawFeature( - it.withAttributes { - feature.attributes + this - } - ) - } + //ignore groups } is PathFeature -> { @@ -117,9 +114,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 +126,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-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..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 @@ -17,11 +19,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() } ) @@ -162,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)