Refactored to use flow instead of snapshot maps

This commit is contained in:
Alexander Nozik 2024-07-06 09:54:06 +03:00
parent 0f5dcf9979
commit 62196fc6f5
20 changed files with 222 additions and 189 deletions

View File

@ -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()

View File

@ -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<XY> = FeatureGroup.remember(XYCoordinateSpace) {
val features = FeatureStore.remember(XYCoordinateSpace) {
background(1600f, 1200f) {
painterResource(Res.drawable.middle_earth)
}

View File

@ -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<Gmc, PolygonFeature<Gmc>> { ref ->
forEachWithType<Gmc, PolygonFeature<Gmc>> { ref, polygon: PolygonFeature<Gmc> ->
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)

View File

@ -24,7 +24,7 @@ fun App() {
val myPolygon: SnapshotStateList<XY> = remember { mutableStateListOf<XY>() }
val featureState: FeatureGroup<XY> = 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"

View File

@ -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<XY> = 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)

View File

@ -22,7 +22,7 @@ private fun Vector2D<out Number>.toXY() = XY(x.toFloat(), y.toFloat())
private val random = Random(123)
fun FeatureGroup<XY>.trajectory(
fun FeatureBuilder<XY>.trajectory(
trajectory: Trajectory2D,
colorPicker: (Trajectory2D) -> Color = { Color.Blue },
): FeatureRef<XY, FeatureGroup<XY>> = group {
@ -54,12 +54,12 @@ fun FeatureGroup<XY>.trajectory(
}
}
fun FeatureGroup<XY>.obstacle(obstacle: Obstacle, colorPicker: (Trajectory2D) -> Color = { Color.Red }) {
fun FeatureBuilder<XY>.obstacle(obstacle: Obstacle, colorPicker: (Trajectory2D) -> Color = { Color.Red }) {
trajectory(obstacle.circumvention, colorPicker)
polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray)
}
fun FeatureGroup<XY>.pose(pose2D: Pose2D) = with(Float64Space2D) {
fun FeatureBuilder<XY>.pose(pose2D: Pose2D) = with(Float64Space2D) {
line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY())
}

View File

@ -33,7 +33,7 @@ private val logger = KotlinLogging.logger("MapView")
public fun MapView(
mapState: MapCanvasState,
mapTileProvider: MapTileProvider,
features: FeatureGroup<Gmc>,
featureStore: FeatureStore<Gmc>,
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<Gmc>,
features: FeatureGroup<Gmc>,
featureStore: FeatureStore<Gmc>,
initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = 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<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null,
modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: FeatureGroup<Gmc>.() -> Unit = {},
buildFeatures: FeatureStore<Gmc>.() -> 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)
}

View File

@ -13,12 +13,12 @@ import space.kscience.maps.features.*
import kotlin.math.ceil
internal fun FeatureGroup<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
internal fun FeatureBuilder<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
GeodeticMapCoordinates.ofDegrees(pair.first.toDouble(), pair.second.toDouble())
public typealias MapFeature = Feature<Gmc>
public fun FeatureGroup<Gmc>.circle(
public fun FeatureBuilder<Gmc>.circle(
centerCoordinates: Pair<Number, Number>,
size: Dp = 5.dp,
id: String? = null,
@ -26,7 +26,7 @@ public fun FeatureGroup<Gmc>.circle(
id, CircleFeature(space, coordinatesOf(centerCoordinates), size)
)
public fun FeatureGroup<Gmc>.rectangle(
public fun FeatureBuilder<Gmc>.rectangle(
centerCoordinates: Pair<Number, Number>,
size: DpSize = DpSize(5.dp, 5.dp),
id: String? = null,
@ -35,7 +35,7 @@ public fun FeatureGroup<Gmc>.rectangle(
)
public fun FeatureGroup<Gmc>.draw(
public fun FeatureBuilder<Gmc>.draw(
position: Pair<Number, Number>,
id: String? = null,
draw: DrawScope.() -> Unit,
@ -45,7 +45,7 @@ public fun FeatureGroup<Gmc>.draw(
)
public fun FeatureGroup<Gmc>.line(
public fun FeatureBuilder<Gmc>.line(
curve: GmcCurve,
id: String? = null,
): FeatureRef<Gmc, LineFeature<Gmc>> = feature(
@ -56,7 +56,7 @@ public fun FeatureGroup<Gmc>.line(
/**
* A segmented geodetic curve
*/
public fun FeatureGroup<Gmc>.geodeticLine(
public fun FeatureBuilder<Gmc>.geodeticLine(
curve: GmcCurve,
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
maxLineDistance: Distance = 100.kilometers,
@ -79,7 +79,7 @@ public fun FeatureGroup<Gmc>.geodeticLine(
multiLine(points.map { it.coordinates }, id = id)
}
public fun FeatureGroup<Gmc>.geodeticLine(
public fun FeatureBuilder<Gmc>.geodeticLine(
from: Gmc,
to: Gmc,
ellipsoid: GeoEllipsoid = GeoEllipsoid.WGS84,
@ -87,7 +87,7 @@ public fun FeatureGroup<Gmc>.geodeticLine(
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> = geodeticLine(ellipsoid.curveBetween(from, to), ellipsoid, maxLineDistance, id)
public fun FeatureGroup<Gmc>.line(
public fun FeatureBuilder<Gmc>.line(
aCoordinates: Pair<Double, Double>,
bCoordinates: Pair<Double, Double>,
id: String? = null,
@ -96,7 +96,7 @@ public fun FeatureGroup<Gmc>.line(
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates))
)
public fun FeatureGroup<Gmc>.arc(
public fun FeatureBuilder<Gmc>.arc(
center: Pair<Double, Double>,
radius: Distance,
startAngle: Angle,
@ -112,17 +112,17 @@ public fun FeatureGroup<Gmc>.arc(
)
)
public fun FeatureGroup<Gmc>.points(
public fun FeatureBuilder<Gmc>.points(
points: List<Pair<Double, Double>>,
id: String? = null,
): FeatureRef<Gmc, PointsFeature<Gmc>> = feature(id, PointsFeature(space, points.map(::coordinatesOf)))
public fun FeatureGroup<Gmc>.multiLine(
public fun FeatureBuilder<Gmc>.multiLine(
points: List<Pair<Double, Double>>,
id: String? = null,
): FeatureRef<Gmc, MultiLineFeature<Gmc>> = feature(id, MultiLineFeature(space, points.map(::coordinatesOf)))
public fun FeatureGroup<Gmc>.icon(
public fun FeatureBuilder<Gmc>.icon(
position: Pair<Double, Double>,
image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp),
@ -137,7 +137,7 @@ public fun FeatureGroup<Gmc>.icon(
)
)
public fun FeatureGroup<Gmc>.text(
public fun FeatureBuilder<Gmc>.text(
position: Pair<Double, Double>,
text: String,
font: Font.() -> Unit = { size = 16f },
@ -147,7 +147,7 @@ public fun FeatureGroup<Gmc>.text(
TextFeature(space, coordinatesOf(position), text, fontConfig = font)
)
public fun FeatureGroup<Gmc>.pixelMap(
public fun FeatureBuilder<Gmc>.pixelMap(
rectangle: Rectangle<Gmc>,
latitudeDelta: Angle,
longitudeDelta: Angle,

View File

@ -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")
}
}

View File

@ -18,7 +18,7 @@ import kotlin.math.min
*/
public fun <T : Any> Modifier.canvasControls(
state: CanvasState<T>,
features: FeatureGroup<T>,
features: FeatureStore<T>,
): Modifier = with(state) {
// //selecting all tapabales ahead of time
@ -36,7 +36,7 @@ public fun <T : Any> 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 <T : Any> 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

View File

@ -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<T : Any>(
) : FeatureDrawScope<T>(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<T : Any>(
@Composable
public fun <T : Any> FeatureCanvas(
state: CanvasState<T>,
features: FeatureGroup<T>,
featureFlow: StateFlow<Map<String, Feature<T>>>,
modifier: Modifier = Modifier,
draw: FeatureDrawScope<T>.() -> Unit = {},
) {
val textMeasurer = rememberTextMeasurer(0)
val painterCache: Map<PainterFeature<T>, Painter> = features.features.flatMap {
if (it is FeatureGroup) it.features else listOf(it)
}.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
val features by featureFlow.collectAsState()
val painterCache = key(features) {
features.values
.filterIsInstance<PainterFeature<T>>()
.associateWith { it.getPainter() }
}
Canvas(modifier) {
if (state.canvasSize != size.toDpSize()) {
@ -89,7 +98,7 @@ public fun <T : Any> 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)

View File

@ -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<out F : Feature<*>>(public val id: String)
public class FeatureRef<T : Any, out F : Feature<T>>(public val id: String, public val parent: FeatureGroup<T>)
public class FeatureRef<T : Any, out F : Feature<T>>(public val store: FeatureStore<T>, public val id: String)
@Suppress("UNCHECKED_CAST")
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.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 <T : Any, F : Feature<T>> FeatureRef<T, F>.attributes: Attributes get() = resolve().attributes
/**
* A group of other features
*/
public data class FeatureGroup<T : Any>(
public fun Uuid.toIndex(): String = leastSignificantBits.toString(16)
public interface FeatureBuilder<T : Any> {
public val space: CoordinateSpace<T>
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F>
public fun group(
id: String? = null,
attributes: Attributes = Attributes.EMPTY,
builder: FeatureGroup<T>.() -> Unit,
): FeatureRef<T, FeatureGroup<T>>
public fun removeFeature(id: String)
}
public interface FeatureSet<T : Any> {
public val features: Map<String, Feature<T>>
/**
* Create a reference
*/
public fun <F: Feature<T>> ref(id: String): FeatureRef<T, F>
}
public class FeatureStore<T : Any>(
override val space: CoordinateSpace<T>,
public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(),
) : CoordinateSpace<T> by space, Feature<T> {
) : CoordinateSpace<T> by space, FeatureBuilder<T>, FeatureSet<T> {
private val _featureFlow = MutableStateFlow<Map<String, Feature<T>>>(emptyMap())
private val attributesState: MutableState<Attributes> = mutableStateOf(Attributes.EMPTY)
public val featureFlow: StateFlow<Map<String, Feature<T>>> get() = _featureFlow
override val attributes: Attributes get() = attributesState.value
override val features: Map<String, Feature<T>> get() = featureFlow.value
//
// @Suppress("UNCHECKED_CAST")
// public operator fun <F : Feature<T>> get(id: FeatureId<F>): F =
// featureMap[id.id]?.let { it as F } ?: error("Feature with id=$id not found")
private var uidCounter = 0
private fun generateUID(feature: Feature<T>?): String = if (feature == null) {
"@group[${uidCounter++}]"
} else {
"@${feature::class.simpleName}[${uidCounter++}]"
override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> {
val safeId = id ?: generateId(feature)
_featureFlow.value += (safeId to feature)
return FeatureRef(this, safeId)
}
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> {
val safeId = id ?: generateUID(feature)
featureMap[safeId] = feature
return FeatureRef(safeId, this)
override fun group(
id: String?,
attributes: Attributes,
builder: FeatureGroup<T>.() -> Unit,
): FeatureRef<T, FeatureGroup<T>> {
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 <F : Feature<T>> feature(id: FeatureId<F>, feature: F): FeatureId<F> = feature(id.id, feature)
override fun <F: Feature<T>> ref(id: String): FeatureRef<T, F> = FeatureRef(this, id)
public val features: Collection<Feature<T>> get() = featureMap.values.sortedByDescending { it.z }
//
// @Suppress("UNCHECKED_CAST")
// public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
// get(id).attributes[key]
override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
featureMap.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
}
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> {
attributesState.value = attributes.modify()
return this
public fun getBoundingBox(zoom: Float = Float.MAX_VALUE): Rectangle<T>? = 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 <T : Any> build(
coordinateSpace: CoordinateSpace<T>,
builder: FeatureGroup<T>.() -> Unit = {},
): FeatureGroup<T> = FeatureGroup(coordinateSpace).apply(builder)
builder: FeatureStore<T>.() -> Unit = {},
): FeatureStore<T> = FeatureStore(coordinateSpace).apply(builder)
/**
* Build and remember map feature state
@ -99,79 +113,100 @@ public data class FeatureGroup<T : Any>(
@Composable
public fun <T : Any> remember(
coordinateSpace: CoordinateSpace<T>,
builder: FeatureGroup<T>.() -> Unit = {},
): FeatureGroup<T> = remember {
builder: FeatureStore<T>.() -> Unit = {},
): FeatureStore<T> = remember {
build(coordinateSpace, builder)
}
}
}
/**
* A group of other features
*/
public data class FeatureGroup<T : Any> internal constructor(
val store: FeatureStore<T>,
val groupId: String,
override val attributes: Attributes,
) : CoordinateSpace<T> by store.space, Feature<T>, FeatureBuilder<T>, FeatureSet<T> {
override val space: CoordinateSpace<T> get() = store.space
override fun withAttributes(modify: Attributes.() -> Attributes): FeatureGroup<T> =
FeatureGroup(store, groupId, modify(attributes))
override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> =
store.feature("$groupId/${id ?: generateId(feature)}", feature)
override fun group(
id: String?,
attributes: Attributes,
builder: FeatureGroup<T>.() -> Unit,
): FeatureRef<T, FeatureGroup<T>> {
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<String, Feature<T>>
get() = store.featureFlow.value
.filterKeys { it.startsWith("$groupId/") }
.mapKeys { it.key.removePrefix("$groupId/") }
.toMap()
override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
}
override fun <F: Feature<T>> ref(id: String): FeatureRef<T, F> = FeatureRef(store, "$groupId/$id")
}
/**
* Recursively search for feature until function returns true
*/
public fun <T : Any> FeatureGroup<T>.forEachUntil(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Boolean) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
if (feature is FeatureGroup<T>) {
feature.forEachUntil(visitor)
} else {
if (!visitor(this, key, feature)) return@forEachUntil
}
public fun <T : Any> FeatureSet<T>.forEachUntil(block: FeatureSet<T>.(ref: FeatureRef<T,*>, feature: Feature<T>) -> Boolean) {
features.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
if (!block(ref<Feature<T>>(key), feature)) return@forEachUntil
}
}
/**
* Recursively visit all features in this group
*/
public fun <T : Any> FeatureGroup<T>.forEach(
visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> 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 <T : Any, A> FeatureGroup<T>.forEachWithAttribute(
public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttribute(
key: Attribute<A>,
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Unit,
block: FeatureSet<T>.(ref: FeatureRef<T,*>, feature: Feature<T>, attribute: A) -> Unit,
) {
forEach { id, feature ->
features.forEach { (id, feature) ->
feature.attributes[key]?.let {
block(id, feature, it)
block(ref<Feature<T>>(id), feature, it)
}
}
}
public fun <T : Any, A> FeatureGroup<T>.forEachWithAttributeUntil(
public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttributeUntil(
key: Attribute<A>,
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Boolean,
block: FeatureSet<T>.(ref: FeatureRef<T,*>, feature: Feature<T>, attribute: A) -> Boolean,
) {
forEachUntil { id, feature ->
features.forEach { (id, feature) ->
feature.attributes[key]?.let {
block(id, feature, it)
} ?: true
if (!block(ref<Feature<T>>(id), feature, it)) return@forEachWithAttributeUntil
}
}
}
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType(
crossinline block: (FeatureRef<T, F>) -> Unit,
public inline fun <T : Any, reified F : Feature<T>> FeatureSet<T>.forEachWithType(
crossinline block: FeatureSet<T>.(ref: FeatureRef<T,F>, 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 <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithTypeUntil(
crossinline block: (FeatureRef<T, F>) -> Boolean,
) {
forEachUntil { id, feature ->
if (feature is F) block(FeatureRef(id, this)) else true
}
}
public fun <T : Any> FeatureGroup<T>.circle(
public fun <T : Any> FeatureBuilder<T>.circle(
center: T,
size: Dp = 5.dp,
attributes: Attributes = Attributes.EMPTY,
@ -180,7 +215,7 @@ public fun <T : Any> FeatureGroup<T>.circle(
id, CircleFeature(space, center, size, attributes)
)
public fun <T : Any> FeatureGroup<T>.rectangle(
public fun <T : Any> FeatureBuilder<T>.rectangle(
centerCoordinates: T,
size: DpSize = DpSize(5.dp, 5.dp),
attributes: Attributes = Attributes.EMPTY,
@ -189,7 +224,7 @@ public fun <T : Any> FeatureGroup<T>.rectangle(
id, RectangleFeature(space, centerCoordinates, size, attributes)
)
public fun <T : Any> FeatureGroup<T>.draw(
public fun <T : Any> FeatureBuilder<T>.draw(
position: T,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
@ -199,7 +234,7 @@ public fun <T : Any> FeatureGroup<T>.draw(
DrawFeature(space, position, drawFeature = draw, attributes = attributes)
)
public fun <T : Any> FeatureGroup<T>.line(
public fun <T : Any> FeatureBuilder<T>.line(
aCoordinates: T,
bCoordinates: T,
attributes: Attributes = Attributes.EMPTY,
@ -209,7 +244,7 @@ public fun <T : Any> FeatureGroup<T>.line(
LineFeature(space, aCoordinates, bCoordinates, attributes)
)
public fun <T : Any> FeatureGroup<T>.arc(
public fun <T : Any> FeatureBuilder<T>.arc(
oval: Rectangle<T>,
startAngle: Angle,
arcLength: Angle,
@ -220,7 +255,7 @@ public fun <T : Any> FeatureGroup<T>.arc(
ArcFeature(space, oval, startAngle, arcLength, attributes)
)
public fun <T : Any> FeatureGroup<T>.points(
public fun <T : Any> FeatureBuilder<T>.points(
points: List<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
@ -229,7 +264,7 @@ public fun <T : Any> FeatureGroup<T>.points(
PointsFeature(space, points, attributes)
)
public fun <T : Any> FeatureGroup<T>.multiLine(
public fun <T : Any> FeatureBuilder<T>.multiLine(
points: List<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
@ -238,7 +273,7 @@ public fun <T : Any> FeatureGroup<T>.multiLine(
MultiLineFeature(space, points, attributes)
)
public fun <T : Any> FeatureGroup<T>.polygon(
public fun <T : Any> FeatureBuilder<T>.polygon(
points: List<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
@ -247,7 +282,7 @@ public fun <T : Any> FeatureGroup<T>.polygon(
PolygonFeature(space, points, attributes)
)
public fun <T : Any> FeatureGroup<T>.icon(
public fun <T : Any> FeatureBuilder<T>.icon(
position: T,
image: ImageVector,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
@ -264,16 +299,7 @@ public fun <T : Any> FeatureGroup<T>.icon(
)
)
public fun <T : Any> FeatureGroup<T>.group(
id: String? = null,
builder: FeatureGroup<T>.() -> Unit,
): FeatureRef<T, FeatureGroup<T>> {
val collection = FeatureGroup(space).apply(builder)
val feature = FeatureGroup(space, collection.featureMap)
return feature(id, feature)
}
public fun <T : Any> FeatureGroup<T>.scalableImage(
public fun <T : Any> FeatureBuilder<T>.scalableImage(
box: Rectangle<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
@ -283,7 +309,7 @@ public fun <T : Any> FeatureGroup<T>.scalableImage(
ScalableImageFeature<T>(space, box, painter = painter, attributes = attributes)
)
public fun <T : Any> FeatureGroup<T>.text(
public fun <T : Any> FeatureBuilder<T>.text(
position: T,
text: String,
font: Font.() -> Unit = { size = 16f },
@ -304,7 +330,7 @@ public inline fun <reified T> Structure2D(rows: Int, columns: Int, initializer:
return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }).as2D()
}
public fun <T : Any> FeatureGroup<T>.pixelMap(
public fun <T : Any> FeatureStore<T>.pixelMap(
rectangle: Rectangle<T>,
pixelMap: Structure2D<Color?>,
attributes: Attributes = Attributes.EMPTY,
@ -320,7 +346,7 @@ public fun <T : Any> FeatureGroup<T>.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 {

View File

@ -4,7 +4,7 @@ import space.kscience.attributes.Attributes
import kotlin.jvm.JvmName
public fun <T : Any> FeatureGroup<T>.draggableLine(
public fun <T : Any> FeatureBuilder<T>.draggableLine(
aId: FeatureRef<T, MarkerFeature<T>>,
bId: FeatureRef<T, MarkerFeature<T>>,
id: String? = null,
@ -39,7 +39,7 @@ public fun <T : Any> FeatureGroup<T>.draggableLine(
return drawLine()
}
public fun <T : Any> FeatureGroup<T>.draggableMultiLine(
public fun <T : Any> FeatureBuilder<T>.draggableMultiLine(
points: List<FeatureRef<T, MarkerFeature<T>>>,
id: String? = null,
): FeatureRef<T, MultiLineFeature<T>> {
@ -71,7 +71,7 @@ public fun <T : Any> FeatureGroup<T>.draggableMultiLine(
}
@JvmName("draggableMultiLineFromPoints")
public fun <T : Any> FeatureGroup<T>.draggableMultiLine(
public fun <T : Any> FeatureBuilder<T>.draggableMultiLine(
points: List<T>,
id: String? = null,
): FeatureRef<T, MultiLineFeature<T>> {

View File

@ -94,7 +94,7 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
}
is FeatureGroup -> {
feature.featureMap.values.forEach {
feature.features.values.forEach {
drawFeature(
it.withAttributes {
feature.attributes + this

View File

@ -55,7 +55,7 @@ public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(
modification: AttributesBuilder<F>.() -> Unit,
): FeatureRef<T, F> {
@Suppress("UNCHECKED_CAST")
parent.feature(
store.feature(
id,
resolve().withAttributes { modified(modification) } as F
)
@ -67,7 +67,7 @@ public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(
value: V,
): FeatureRef<T, F> {
@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 <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(
public fun <T : Any, F : DraggableFeature<T>> FeatureRef<T, F>.draggable(
constraint: ((T) -> T)? = null,
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null,
): FeatureRef<T, F> = with(parent) {
): FeatureRef<T, F> = with(store) {
if (attributes[DraggableAttribute] == null) {
val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
val feature = features[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
start as ViewPoint<T>
end as ViewPoint<T>
if (start in feature) {

View File

@ -12,7 +12,7 @@ import space.kscience.maps.features.*
/**
* Add a single Json geometry to a feature builder
*/
public fun FeatureGroup<Gmc>.geoJsonGeometry(
public fun FeatureBuilder<Gmc>.geoJsonGeometry(
geometry: GeoJsonGeometry,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> = when (geometry) {
@ -50,11 +50,11 @@ public fun FeatureGroup<Gmc>.geoJsonGeometry(
}
}
public fun FeatureGroup<Gmc>.geoJsonFeature(
public fun FeatureBuilder<Gmc>.geoJsonFeature(
geoJson: GeoJsonFeature,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> {
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<Gmc>.geoJsonFeature(
}
}
public fun FeatureGroup<Gmc>.geoJson(
public fun FeatureBuilder<Gmc>.geoJson(
geoJson: GeoJson,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> = when (geoJson) {

View File

@ -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<Gmc>.geoJson(
public fun FeatureBuilder<Gmc>.geoJson(
geoJsonUrl: URL,
id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> {

View File

@ -15,10 +15,10 @@ private val logger = KotlinLogging.logger("SchemeView")
@Composable
public fun SchemeView(
state: XYCanvasState,
features: FeatureGroup<XY>,
featureStore: FeatureStore<XY>,
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<XY>.computeViewPoint(
*/
@Composable
public fun SchemeView(
features: FeatureGroup<XY>,
features: FeatureStore<XY>,
initialViewPoint: ViewPoint<XY>? = null,
initialRectangle: Rectangle<XY>? = null,
config: ViewConfig<XY> = ViewConfig(),
@ -67,14 +67,13 @@ public fun SchemeView(
initialRectangle: Rectangle<XY>? = null,
config: ViewConfig<XY> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(),
buildFeatures: FeatureGroup<XY>.() -> Unit = {},
buildFeatures: FeatureStore<XY>.() -> 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
),
)

View File

@ -15,7 +15,7 @@ import kotlin.math.ceil
internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat())
public fun FeatureGroup<XY>.background(
public fun FeatureBuilder<XY>.background(
width: Float,
height: Float,
offset: XY = XY(0f, 0f),
@ -37,26 +37,26 @@ public fun FeatureGroup<XY>.background(
)
}
public fun FeatureGroup<XY>.circle(
public fun FeatureBuilder<XY>.circle(
centerCoordinates: Pair<Number, Number>,
size: Dp = 5.dp,
id: String? = null,
): FeatureRef<XY, CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), size, id = id)
public fun FeatureGroup<XY>.draw(
public fun FeatureBuilder<XY>.draw(
position: Pair<Number, Number>,
id: String? = null,
draw: DrawScope.() -> Unit,
): FeatureRef<XY, DrawFeature<XY>> = draw(position.toCoordinates(), id = id, draw = draw)
public fun FeatureGroup<XY>.line(
public fun FeatureBuilder<XY>.line(
aCoordinates: Pair<Number, Number>,
bCoordinates: Pair<Number, Number>,
id: String? = null,
): FeatureRef<XY, LineFeature<XY>> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), id = id)
public fun FeatureGroup<XY>.arc(
public fun FeatureBuilder<XY>.arc(
center: Pair<Double, Double>,
radius: Float,
startAngle: Angle,
@ -69,7 +69,7 @@ public fun FeatureGroup<XY>.arc(
id = id
)
public fun FeatureGroup<XY>.image(
public fun FeatureBuilder<XY>.image(
position: Pair<Number, Number>,
image: ImageVector,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
@ -77,13 +77,13 @@ public fun FeatureGroup<XY>.image(
): FeatureRef<XY, VectorIconFeature<XY>> =
icon(position.toCoordinates(), image, size = size, id = id)
public fun FeatureGroup<XY>.text(
public fun FeatureBuilder<XY>.text(
position: Pair<Number, Number>,
text: String,
id: String? = null,
): FeatureRef<XY, TextFeature<XY>> = text(position.toCoordinates(), text, id = id)
public fun FeatureGroup<XY>.pixelMap(
public fun FeatureBuilder<XY>.pixelMap(
rectangle: Rectangle<XY>,
xSize: Float,
ySize: Float,
@ -108,7 +108,7 @@ public fun FeatureGroup<XY>.pixelMap(
)
)
public fun FeatureGroup<XY>.rectanglePolygon(
public fun FeatureBuilder<XY>.rectanglePolygon(
left: Number, right: Number,
bottom: Number, top: Number,
attributes: Attributes = Attributes.EMPTY,
@ -123,7 +123,7 @@ public fun FeatureGroup<XY>.rectanglePolygon(
attributes, id
)
public fun FeatureGroup<XY>.rectanglePolygon(
public fun FeatureBuilder<XY>.rectanglePolygon(
rectangle: Rectangle<XY>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,

View File

@ -17,11 +17,9 @@ public class FeatureStateSnapshot<T : Any>(
)
@Composable
public fun <T : Any> FeatureGroup<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
featureMap,
features.flatMap {
if (it is FeatureGroup) it.features else listOf(it)
}.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
public fun <T : Any> FeatureSet<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
features,
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
)