immutable_features #24

Merged
altavir merged 7 commits from immutable_features into dev 2024-07-08 11:56:05 +03:00
20 changed files with 222 additions and 189 deletions
Showing only changes of commit 62196fc6f5 - Show all commits

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 ->
altavir marked this conversation as resolved Outdated

Does id argument is really needed here?

Does `id` argument is really needed here?

It should be ref and no, it is probably not needed.

It should be ref and no, it is probably not needed.
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() }
)