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 { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.3.1-dev" version = "0.4.0-dev"
repositories { repositories {
mavenLocal() 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.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import space.kscience.kmath.geometry.Angle 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.ViewConfig
import space.kscience.maps.features.ViewPoint import space.kscience.maps.features.ViewPoint
import space.kscience.maps.features.color import space.kscience.maps.features.color
@ -25,7 +25,7 @@ fun App() {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val features: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) { val features = FeatureStore.remember(XYCoordinateSpace) {
background(1600f, 1200f) { background(1600f, 1200f) {
painterResource(Res.drawable.middle_earth) painterResource(Res.drawable.middle_earth)
} }

View File

@ -73,7 +73,7 @@ fun App() {
geoJson(javaClass.getResource("/moscow.geo.json")!!) geoJson(javaClass.getResource("/moscow.geo.json")!!)
.color(Color.Blue) .color(Color.Blue)
.modifyAttribute(AlphaAttribute, 0.4f) .alpha(0.4f)
icon(pointOne, Icons.Filled.Home) icon(pointOne, Icons.Filled.Home)
@ -166,13 +166,13 @@ fun App() {
}.launchIn(scope) }.launchIn(scope)
//Add click listeners for all polygons //Add click listeners for all polygons
forEachWithType<Gmc, PolygonFeature<Gmc>> { ref -> forEachWithType<Gmc, PolygonFeature<Gmc>> { ref, polygon: PolygonFeature<Gmc> ->
ref.onClick(PointerMatcher.Primary) { ref.onClick(PointerMatcher.Primary) {
println("Click on ${ref.id}") println("Click on ${ref.id}")
//draw in top-level scope //draw in top-level scope
with(this@MapView) { with(this@MapView) {
multiLine( multiLine(
ref.resolve().points, polygon.points,
attributes = Attributes(ZAttribute, 10f), attributes = Attributes(ZAttribute, 10f),
id = "selected", id = "selected",
).modifyAttribute(StrokeAttribute, 4f).color(Color.Magenta) ).modifyAttribute(StrokeAttribute, 4f).color(Color.Magenta)

View File

@ -24,7 +24,7 @@ fun App() {
val myPolygon: SnapshotStateList<XY> = remember { mutableStateListOf<XY>() } val myPolygon: SnapshotStateList<XY> = remember { mutableStateListOf<XY>() }
val featureState: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) { val featureState = FeatureStore.remember(XYCoordinateSpace) {
multiLine( multiLine(
listOf(XY(0f, 0f), XY(0f, 1f), XY(1f, 1f), XY(1f, 0f), XY(0f, 0f)), listOf(XY(0f, 0f), XY(0f, 1f), XY(1f, 1f), XY(1f, 0f), XY(0f, 0f)),
id = "frame" id = "frame"

View File

@ -12,7 +12,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.kmath.geometry.Angle 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.ViewConfig
import space.kscience.maps.features.ViewPoint import space.kscience.maps.features.ViewPoint
import space.kscience.maps.features.color import space.kscience.maps.features.color
@ -29,7 +29,7 @@ fun App() {
MaterialTheme { MaterialTheme {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val features: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) { val features = FeatureStore.remember(XYCoordinateSpace) {
background(1600f, 1200f) { painterResource("middle-earth.jpg") } background(1600f, 1200f) { painterResource("middle-earth.jpg") }
circle(410.52737 to 868.7676).color(Color.Blue) circle(410.52737 to 868.7676).color(Color.Blue)
text(410.52737 to 868.7676, "Shire").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) private val random = Random(123)
fun FeatureGroup<XY>.trajectory( fun FeatureBuilder<XY>.trajectory(
trajectory: Trajectory2D, trajectory: Trajectory2D,
colorPicker: (Trajectory2D) -> Color = { Color.Blue }, colorPicker: (Trajectory2D) -> Color = { Color.Blue },
): FeatureRef<XY, FeatureGroup<XY>> = group { ): 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) trajectory(obstacle.circumvention, colorPicker)
polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray) 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()) line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY())
} }

View File

@ -33,7 +33,7 @@ private val logger = KotlinLogging.logger("MapView")
public fun MapView( public fun MapView(
mapState: MapCanvasState, mapState: MapCanvasState,
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
features: FeatureGroup<Gmc>, featureStore: FeatureStore<Gmc>,
modifier: Modifier, modifier: Modifier,
) { ) {
val mapTiles = remember(mapTileProvider) { 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 val tileScale = mapState.tileScale
clipRect { clipRect {
@ -112,19 +112,19 @@ public fun MapView(
} }
/** /**
* Create a [MapView] with given [features] group. * Create a [MapView] with given [featureStore] group.
*/ */
@Composable @Composable
public fun MapView( public fun MapView(
mapTileProvider: MapTileProvider, mapTileProvider: MapTileProvider,
config: ViewConfig<Gmc>, config: ViewConfig<Gmc>,
features: FeatureGroup<Gmc>, featureStore: FeatureStore<Gmc>,
initialViewPoint: ViewPoint<Gmc>? = null, initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null, initialRectangle: Rectangle<Gmc>? = null,
modifier: Modifier, modifier: Modifier,
) { ) {
val mapState = MapCanvasState.remember(mapTileProvider, config, initialViewPoint, initialRectangle) 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, initialViewPoint: ViewPoint<Gmc>? = null,
initialRectangle: Rectangle<Gmc>? = null, initialRectangle: Rectangle<Gmc>? = null,
modifier: Modifier = Modifier.fillMaxSize(), 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() val computedRectangle = initialRectangle ?: featureState.getBoundingBox()
MapView(mapTileProvider, config, featureState, initialViewPoint, computedRectangle, modifier) MapView(mapTileProvider, config, featureState, initialViewPoint, computedRectangle, modifier)
} }

View File

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

View File

@ -35,5 +35,6 @@ kscience {
api(compose.material) api(compose.material)
api(compose.ui) api(compose.ui)
api("io.github.oshai:kotlin-logging:6.0.3") 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( public fun <T : Any> Modifier.canvasControls(
state: CanvasState<T>, state: CanvasState<T>,
features: FeatureGroup<T>, features: FeatureStore<T>,
): Modifier = with(state) { ): Modifier = with(state) {
// //selecting all tapabales ahead of time // //selecting all tapabales ahead of time
@ -36,7 +36,7 @@ public fun <T : Any> Modifier.canvasControls(
val point = state.space.ViewPoint(coordinates, zoom) val point = state.space.ViewPoint(coordinates, zoom)
if (event.type == PointerEventType.Move) { if (event.type == PointerEventType.Move) {
features.forEachWithAttribute(HoverListenerAttribute) { _, feature, listeners -> features.forEachWithAttribute(HoverListenerAttribute) { id, feature, listeners ->
if (point in feature as DomainFeature) { if (point in feature as DomainFeature) {
listeners.forEach { it.handle(event, point) } listeners.forEach { it.handle(event, point) }
return@forEachWithAttribute return@forEachWithAttribute

View File

@ -2,6 +2,9 @@ package space.kscience.maps.features
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color 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.text.rememberTextMeasurer
import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.DpRect
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.flow.StateFlow
import space.kscience.attributes.Attributes import space.kscience.attributes.Attributes
/** /**
@ -52,6 +56,7 @@ public class ComposeFeatureDrawScope<T : Any>(
) : FeatureDrawScope<T>(state), DrawScope by drawScope { ) : FeatureDrawScope<T>(state), DrawScope by drawScope {
override fun drawText(text: String, position: Offset, attributes: Attributes) { override fun drawText(text: String, position: Offset, attributes: Attributes) {
try { try {
//TODO don't draw text that is not on screen
drawText(textMeasurer ?: error("Text measurer not defined"), text, position) drawText(textMeasurer ?: error("Text measurer not defined"), text, position)
} catch (ex: Exception) { } catch (ex: Exception) {
logger.error(ex) { "Failed to measure text" } logger.error(ex) { "Failed to measure text" }
@ -73,15 +78,19 @@ public class ComposeFeatureDrawScope<T : Any>(
@Composable @Composable
public fun <T : Any> FeatureCanvas( public fun <T : Any> FeatureCanvas(
state: CanvasState<T>, state: CanvasState<T>,
features: FeatureGroup<T>, featureFlow: StateFlow<Map<String, Feature<T>>>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
draw: FeatureDrawScope<T>.() -> Unit = {}, draw: FeatureDrawScope<T>.() -> Unit = {},
) { ) {
val textMeasurer = rememberTextMeasurer(0) val textMeasurer = rememberTextMeasurer(0)
val painterCache: Map<PainterFeature<T>, Painter> = features.features.flatMap { val features by featureFlow.collectAsState()
if (it is FeatureGroup) it.features else listOf(it)
}.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() } val painterCache = key(features) {
features.values
.filterIsInstance<PainterFeature<T>>()
.associateWith { it.getPainter() }
}
Canvas(modifier) { Canvas(modifier) {
if (state.canvasSize != size.toDpSize()) { if (state.canvasSize != size.toDpSize()) {
@ -89,7 +98,7 @@ public fun <T : Any> FeatureCanvas(
} }
ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply { ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply {
clipRect { clipRect {
features.featureMap.values.sortedBy { it.z } features.values.sortedBy { it.z }
.filter { state.viewPoint.zoom in it.zoomRange } .filter { state.viewPoint.zoom in it.zoomRange }
.forEach { feature -> .forEach { feature ->
this@apply.drawFeature(feature) this@apply.drawFeature(feature)

View File

@ -1,7 +1,7 @@
package space.kscience.maps.features package space.kscience.maps.features
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter 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.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp 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 org.jetbrains.skia.Font
import space.kscience.attributes.Attribute import space.kscience.attributes.Attribute
import space.kscience.attributes.Attributes import space.kscience.attributes.Attributes
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.nd.* import space.kscience.kmath.nd.*
import space.kscience.kmath.structures.Buffer import space.kscience.kmath.structures.Buffer
import space.kscience.maps.features.FeatureStore.Companion.generateId
//@JvmInline //@JvmInline
//public value class FeatureId<out F : Feature<*>>(public val id: String) //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") @Suppress("UNCHECKED_CAST")
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.resolve(): F = 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 public val <T : Any, F : Feature<T>> FeatureRef<T, F>.attributes: Attributes get() = resolve().attributes
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>>
/** /**
* A group of other features * Create a reference
*/ */
public data class FeatureGroup<T : Any>( public fun <F: Feature<T>> ref(id: String): FeatureRef<T, F>
}
public class FeatureStore<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(), ) : CoordinateSpace<T> by space, FeatureBuilder<T>, FeatureSet<T> {
) : CoordinateSpace<T> by space, Feature<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
// override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> {
// @Suppress("UNCHECKED_CAST") val safeId = id ?: generateId(feature)
// public operator fun <F : Feature<T>> get(id: FeatureId<F>): F = _featureFlow.value += (safeId to feature)
// featureMap[id.id]?.let { it as F } ?: error("Feature with id=$id not found") return FeatureRef(this, safeId)
private var uidCounter = 0
private fun generateUID(feature: Feature<T>?): String = if (feature == null) {
"@group[${uidCounter++}]"
} else {
"@${feature::class.simpleName}[${uidCounter++}]"
} }
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> { override fun group(
val safeId = id ?: generateUID(feature) id: String?,
featureMap[safeId] = feature attributes: Attributes,
return FeatureRef(safeId, this) 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) { override fun removeFeature(id: String) {
featureMap.remove(id) _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 } public fun getBoundingBox(zoom: Float = Float.MAX_VALUE): Rectangle<T>? = with(space) {
features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
//
// @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 companion object { 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 * Build, but do not remember map feature state
*/ */
public fun <T : Any> build( public fun <T : Any> build(
coordinateSpace: CoordinateSpace<T>, coordinateSpace: CoordinateSpace<T>,
builder: FeatureGroup<T>.() -> Unit = {}, builder: FeatureStore<T>.() -> Unit = {},
): FeatureGroup<T> = FeatureGroup(coordinateSpace).apply(builder) ): FeatureStore<T> = FeatureStore(coordinateSpace).apply(builder)
/** /**
* Build and remember map feature state * Build and remember map feature state
@ -99,79 +113,100 @@ public data class FeatureGroup<T : Any>(
@Composable @Composable
public fun <T : Any> remember( public fun <T : Any> remember(
coordinateSpace: CoordinateSpace<T>, coordinateSpace: CoordinateSpace<T>,
builder: FeatureGroup<T>.() -> Unit = {}, builder: FeatureStore<T>.() -> Unit = {},
): FeatureGroup<T> = remember { ): FeatureStore<T> = remember {
build(coordinateSpace, builder) 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 * Recursively search for feature until function returns true
*/ */
public fun <T : Any> FeatureGroup<T>.forEachUntil(visitor: FeatureGroup<T>.(id: String, feature: Feature<T>) -> Boolean) { public fun <T : Any> FeatureSet<T>.forEachUntil(block: FeatureSet<T>.(ref: FeatureRef<T,*>, feature: Feature<T>) -> Boolean) {
featureMap.entries.sortedByDescending { it.value.z }.forEach { (key, feature) -> features.entries.sortedByDescending { it.value.z }.forEach { (key, feature) ->
if (feature is FeatureGroup<T>) { if (!block(ref<Feature<T>>(key), feature)) return@forEachUntil
feature.forEachUntil(visitor)
} else {
if (!visitor(this, 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 * 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>, 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 { 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>, 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 { feature.attributes[key]?.let {
block(id, feature, it) if (!block(ref<Feature<T>>(id), feature, it)) return@forEachWithAttributeUntil
} ?: true }
} }
} }
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType( public inline fun <T : Any, reified F : Feature<T>> FeatureSet<T>.forEachWithType(
crossinline block: (FeatureRef<T, F>) -> Unit, crossinline block: FeatureSet<T>.(ref: FeatureRef<T,F>, feature: F) -> Unit,
) { ) {
forEach { id, feature -> features.forEach { (id, feature) ->
if (feature is F) block(FeatureRef(id, this)) if (feature is F) block(ref(id), feature)
} }
} }
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithTypeUntil( public fun <T : Any> FeatureBuilder<T>.circle(
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(
center: T, center: T,
size: Dp = 5.dp, size: Dp = 5.dp,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
@ -180,7 +215,7 @@ public fun <T : Any> FeatureGroup<T>.circle(
id, CircleFeature(space, center, size, attributes) id, CircleFeature(space, center, size, attributes)
) )
public fun <T : Any> FeatureGroup<T>.rectangle( public fun <T : Any> FeatureBuilder<T>.rectangle(
centerCoordinates: T, centerCoordinates: T,
size: DpSize = DpSize(5.dp, 5.dp), size: DpSize = DpSize(5.dp, 5.dp),
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
@ -189,7 +224,7 @@ public fun <T : Any> FeatureGroup<T>.rectangle(
id, RectangleFeature(space, centerCoordinates, size, attributes) id, RectangleFeature(space, centerCoordinates, size, attributes)
) )
public fun <T : Any> FeatureGroup<T>.draw( public fun <T : Any> FeatureBuilder<T>.draw(
position: T, position: T,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,
@ -199,7 +234,7 @@ public fun <T : Any> FeatureGroup<T>.draw(
DrawFeature(space, position, drawFeature = draw, attributes = attributes) DrawFeature(space, position, drawFeature = draw, attributes = attributes)
) )
public fun <T : Any> FeatureGroup<T>.line( public fun <T : Any> FeatureBuilder<T>.line(
aCoordinates: T, aCoordinates: T,
bCoordinates: T, bCoordinates: T,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
@ -209,7 +244,7 @@ public fun <T : Any> FeatureGroup<T>.line(
LineFeature(space, aCoordinates, bCoordinates, attributes) LineFeature(space, aCoordinates, bCoordinates, attributes)
) )
public fun <T : Any> FeatureGroup<T>.arc( public fun <T : Any> FeatureBuilder<T>.arc(
oval: Rectangle<T>, oval: Rectangle<T>,
startAngle: Angle, startAngle: Angle,
arcLength: Angle, arcLength: Angle,
@ -220,7 +255,7 @@ public fun <T : Any> FeatureGroup<T>.arc(
ArcFeature(space, oval, startAngle, arcLength, attributes) ArcFeature(space, oval, startAngle, arcLength, attributes)
) )
public fun <T : Any> FeatureGroup<T>.points( public fun <T : Any> FeatureBuilder<T>.points(
points: List<T>, points: List<T>,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,
@ -229,7 +264,7 @@ public fun <T : Any> FeatureGroup<T>.points(
PointsFeature(space, points, attributes) PointsFeature(space, points, attributes)
) )
public fun <T : Any> FeatureGroup<T>.multiLine( public fun <T : Any> FeatureBuilder<T>.multiLine(
points: List<T>, points: List<T>,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,
@ -238,7 +273,7 @@ public fun <T : Any> FeatureGroup<T>.multiLine(
MultiLineFeature(space, points, attributes) MultiLineFeature(space, points, attributes)
) )
public fun <T : Any> FeatureGroup<T>.polygon( public fun <T : Any> FeatureBuilder<T>.polygon(
points: List<T>, points: List<T>,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,
@ -247,7 +282,7 @@ public fun <T : Any> FeatureGroup<T>.polygon(
PolygonFeature(space, points, attributes) PolygonFeature(space, points, attributes)
) )
public fun <T : Any> FeatureGroup<T>.icon( public fun <T : Any> FeatureBuilder<T>.icon(
position: T, position: T,
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), 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( public fun <T : Any> FeatureBuilder<T>.scalableImage(
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(
box: Rectangle<T>, box: Rectangle<T>,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,
@ -283,7 +309,7 @@ public fun <T : Any> FeatureGroup<T>.scalableImage(
ScalableImageFeature<T>(space, box, painter = painter, attributes = attributes) ScalableImageFeature<T>(space, box, painter = painter, attributes = attributes)
) )
public fun <T : Any> FeatureGroup<T>.text( public fun <T : Any> FeatureBuilder<T>.text(
position: T, position: T,
text: String, text: String,
font: Font.() -> Unit = { size = 16f }, 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() 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>, rectangle: Rectangle<T>,
pixelMap: Structure2D<Color?>, pixelMap: Structure2D<Color?>,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
@ -320,7 +346,7 @@ public fun <T : Any> FeatureGroup<T>.pixelMap(
public fun FeatureGroup<*>.toPrettyString(): String { public fun FeatureGroup<*>.toPrettyString(): String {
fun StringBuilder.printGroup(id: String, group: FeatureGroup<*>, prefix: String) { fun StringBuilder.printGroup(id: String, group: FeatureGroup<*>, prefix: String) {
appendLine("${prefix}* [group] $id") appendLine("${prefix}* [group] $id")
group.featureMap.forEach { (id, feature) -> group.features.forEach { (id, feature) ->
if (feature is FeatureGroup<*>) { if (feature is FeatureGroup<*>) {
printGroup(id, feature, " ") printGroup(id, feature, " ")
} else { } else {

View File

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

View File

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

View File

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

View File

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

View File

@ -4,14 +4,14 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import space.kscience.maps.coordinates.Gmc import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.features.Feature 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 space.kscience.maps.features.FeatureRef
import java.net.URL import java.net.URL
/** /**
* Add geojson features from url * Add geojson features from url
*/ */
public fun FeatureGroup<Gmc>.geoJson( public fun FeatureBuilder<Gmc>.geoJson(
geoJsonUrl: URL, geoJsonUrl: URL,
id: String? = null, id: String? = null,
): FeatureRef<Gmc, Feature<Gmc>> { ): FeatureRef<Gmc, Feature<Gmc>> {

View File

@ -15,10 +15,10 @@ private val logger = KotlinLogging.logger("SchemeView")
@Composable @Composable
public fun SchemeView( public fun SchemeView(
state: XYCanvasState, state: XYCanvasState,
features: FeatureGroup<XY>, featureStore: FeatureStore<XY>,
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
): Unit { ): 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 @Composable
public fun SchemeView( public fun SchemeView(
features: FeatureGroup<XY>, features: FeatureStore<XY>,
initialViewPoint: ViewPoint<XY>? = null, initialViewPoint: ViewPoint<XY>? = null,
initialRectangle: Rectangle<XY>? = null, initialRectangle: Rectangle<XY>? = null,
config: ViewConfig<XY> = ViewConfig(), config: ViewConfig<XY> = ViewConfig(),
@ -67,14 +67,13 @@ public fun SchemeView(
initialRectangle: Rectangle<XY>? = null, initialRectangle: Rectangle<XY>? = null,
config: ViewConfig<XY> = ViewConfig(), config: ViewConfig<XY> = ViewConfig(),
modifier: Modifier = Modifier.fillMaxSize(), 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( val mapState: XYCanvasState = XYCanvasState.remember(
config, config,
initialViewPoint = initialViewPoint, initialViewPoint = initialViewPoint,
initialRectangle = initialRectangle ?: featureState.features.computeBoundingBox( initialRectangle = initialRectangle ?: featureState.getBoundingBox(
XYCoordinateSpace,
Float.MAX_VALUE 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()) internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat())
public fun FeatureGroup<XY>.background( public fun FeatureBuilder<XY>.background(
width: Float, width: Float,
height: Float, height: Float,
offset: XY = XY(0f, 0f), 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>, centerCoordinates: Pair<Number, Number>,
size: Dp = 5.dp, size: Dp = 5.dp,
id: String? = null, id: String? = null,
): FeatureRef<XY, CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), size, id = id) ): FeatureRef<XY, CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), size, id = id)
public fun FeatureGroup<XY>.draw( public fun FeatureBuilder<XY>.draw(
position: Pair<Number, Number>, position: Pair<Number, Number>,
id: String? = null, id: String? = null,
draw: DrawScope.() -> Unit, draw: DrawScope.() -> Unit,
): FeatureRef<XY, DrawFeature<XY>> = draw(position.toCoordinates(), id = id, draw = draw) ): 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>, aCoordinates: Pair<Number, Number>,
bCoordinates: Pair<Number, Number>, bCoordinates: Pair<Number, Number>,
id: String? = null, id: String? = null,
): FeatureRef<XY, LineFeature<XY>> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), id = id) ): 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>, center: Pair<Double, Double>,
radius: Float, radius: Float,
startAngle: Angle, startAngle: Angle,
@ -69,7 +69,7 @@ public fun FeatureGroup<XY>.arc(
id = id id = id
) )
public fun FeatureGroup<XY>.image( public fun FeatureBuilder<XY>.image(
position: Pair<Number, Number>, position: Pair<Number, Number>,
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
@ -77,13 +77,13 @@ public fun FeatureGroup<XY>.image(
): FeatureRef<XY, VectorIconFeature<XY>> = ): FeatureRef<XY, VectorIconFeature<XY>> =
icon(position.toCoordinates(), image, size = size, id = id) icon(position.toCoordinates(), image, size = size, id = id)
public fun FeatureGroup<XY>.text( public fun FeatureBuilder<XY>.text(
position: Pair<Number, Number>, position: Pair<Number, Number>,
text: String, text: String,
id: String? = null, id: String? = null,
): FeatureRef<XY, TextFeature<XY>> = text(position.toCoordinates(), text, id = id) ): FeatureRef<XY, TextFeature<XY>> = text(position.toCoordinates(), text, id = id)
public fun FeatureGroup<XY>.pixelMap( public fun FeatureBuilder<XY>.pixelMap(
rectangle: Rectangle<XY>, rectangle: Rectangle<XY>,
xSize: Float, xSize: Float,
ySize: 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, left: Number, right: Number,
bottom: Number, top: Number, bottom: Number, top: Number,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
@ -123,7 +123,7 @@ public fun FeatureGroup<XY>.rectanglePolygon(
attributes, id attributes, id
) )
public fun FeatureGroup<XY>.rectanglePolygon( public fun FeatureBuilder<XY>.rectanglePolygon(
rectangle: Rectangle<XY>, rectangle: Rectangle<XY>,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,

View File

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