Compare commits

...

3 Commits

21 changed files with 562 additions and 468 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")
//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"
@ -55,6 +55,7 @@ fun App() {
} }
draggableMultiLine( draggableMultiLine(
pointRefs + pointRefs.first(), pointRefs + pointRefs.first(),
"line"
) )
} }
} }

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
@ -67,7 +67,7 @@ public fun <T : Any> Modifier.canvasControls(
point point
) )
features.forEachWithAttributeUntil(ClickListenerAttribute) { _, feature, listeners -> features.forEachWithAttributeUntil(ClickListenerAttribute) {_, 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) }
false false

View File

@ -2,6 +2,8 @@ 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.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,7 +18,13 @@ 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.FlowPreview
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.sample
import space.kscience.attributes.Attributes import space.kscience.attributes.Attributes
import space.kscience.attributes.plus
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
/** /**
* An extension of [DrawScope] to include map-specific features * An extension of [DrawScope] to include map-specific features
@ -52,6 +60,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" }
@ -70,29 +79,48 @@ public class ComposeFeatureDrawScope<T : Any>(
/** /**
* Create a canvas with extended functionality (e.g., drawing text) * Create a canvas with extended functionality (e.g., drawing text)
*/ */
@OptIn(FlowPreview::class)
@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,
sampleDuration: Duration = 20.milliseconds,
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.sample(sampleDuration).collectAsState(featureFlow.value)
if (it is FeatureGroup) it.features else listOf(it)
}.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() } val painterCache = features.values
.filterIsInstance<PainterFeature<T>>()
.associateWith { it.getPainter() }
Canvas(modifier) { Canvas(modifier) {
if (state.canvasSize != size.toDpSize()) { if (state.canvasSize != size.toDpSize()) {
state.canvasSize = size.toDpSize() state.canvasSize = size.toDpSize()
} }
ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply { clipRect {
clipRect { ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply {
features.featureMap.values.sortedBy { it.z }
.filter { state.viewPoint.zoom in it.zoomRange } val attributesCache = mutableMapOf<List<String>, Attributes>()
.forEach { feature ->
this@apply.drawFeature(feature) fun computeGroupAttributes(path: List<String>): Attributes = attributesCache.getOrPut(path) {
if (path.isEmpty()) return Attributes.EMPTY
else if (path.size == 1) {
features[path.first()]?.attributes ?: Attributes.EMPTY
} else {
computeGroupAttributes(path.dropLast(1)) + (features[path.first()]?.attributes
?: Attributes.EMPTY)
}
}
features.entries.sortedBy { it.value.z }
.filter { state.viewPoint.zoom in it.value.zoomRange }
.forEach { (id, feature) ->
val path = id.split("/")
drawFeature(feature, computeGroupAttributes(path.dropLast(1)))
} }
} }
} }

View File

@ -1,334 +0,0 @@
package space.kscience.maps.features
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import org.jetbrains.skia.Font
import space.kscience.attributes.Attribute
import space.kscience.attributes.Attributes
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.nd.*
import space.kscience.kmath.structures.Buffer
//@JvmInline
//public value class FeatureId<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>)
@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")
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>(
override val space: CoordinateSpace<T>,
public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(),
) : CoordinateSpace<T> by space, Feature<T> {
private val attributesState: MutableState<Attributes> = mutableStateOf(Attributes.EMPTY)
override val attributes: Attributes get() = attributesState.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++}]"
}
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)
}
public fun removeFeature(id: String) {
featureMap.remove(id)
}
// public fun <F : Feature<T>> feature(id: FeatureId<F>, feature: F): FeatureId<F> = feature(id.id, feature)
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 companion object {
/**
* 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)
/**
* Build and remember map feature state
*/
@Composable
public fun <T : Any> remember(
coordinateSpace: CoordinateSpace<T>,
builder: FeatureGroup<T>.() -> Unit = {},
): FeatureGroup<T> = remember {
build(coordinateSpace, builder)
}
}
}
/**
* 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
}
}
}
/**
* 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(
key: Attribute<A>,
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Unit,
) {
forEach { id, feature ->
feature.attributes[key]?.let {
block(id, feature, it)
}
}
}
public fun <T : Any, A> FeatureGroup<T>.forEachWithAttributeUntil(
key: Attribute<A>,
block: FeatureGroup<T>.(id: String, feature: Feature<T>, attributeValue: A) -> Boolean,
) {
forEachUntil { id, feature ->
feature.attributes[key]?.let {
block(id, feature, it)
} ?: true
}
}
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType(
crossinline block: (FeatureRef<T, F>) -> Unit,
) {
forEach { id, feature ->
if (feature is F) block(FeatureRef(id, this))
}
}
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(
center: T,
size: Dp = 5.dp,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, CircleFeature<T>> = feature(
id, CircleFeature(space, center, size, attributes)
)
public fun <T : Any> FeatureGroup<T>.rectangle(
centerCoordinates: T,
size: DpSize = DpSize(5.dp, 5.dp),
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, RectangleFeature<T>> = feature(
id, RectangleFeature(space, centerCoordinates, size, attributes)
)
public fun <T : Any> FeatureGroup<T>.draw(
position: T,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
draw: DrawScope.() -> Unit,
): FeatureRef<T, DrawFeature<T>> = feature(
id,
DrawFeature(space, position, drawFeature = draw, attributes = attributes)
)
public fun <T : Any> FeatureGroup<T>.line(
aCoordinates: T,
bCoordinates: T,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, LineFeature<T>> = feature(
id,
LineFeature(space, aCoordinates, bCoordinates, attributes)
)
public fun <T : Any> FeatureGroup<T>.arc(
oval: Rectangle<T>,
startAngle: Angle,
arcLength: Angle,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, ArcFeature<T>> = feature(
id,
ArcFeature(space, oval, startAngle, arcLength, attributes)
)
public fun <T : Any> FeatureGroup<T>.points(
points: List<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, PointsFeature<T>> = feature(
id,
PointsFeature(space, points, attributes)
)
public fun <T : Any> FeatureGroup<T>.multiLine(
points: List<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, MultiLineFeature<T>> = feature(
id,
MultiLineFeature(space, points, attributes)
)
public fun <T : Any> FeatureGroup<T>.polygon(
points: List<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, PolygonFeature<T>> = feature(
id,
PolygonFeature(space, points, attributes)
)
public fun <T : Any> FeatureGroup<T>.icon(
position: T,
image: ImageVector,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, VectorIconFeature<T>> = feature(
id,
VectorIconFeature(
space,
position,
size,
image,
attributes
)
)
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(
box: Rectangle<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
painter: @Composable () -> Painter,
): FeatureRef<T, ScalableImageFeature<T>> = feature(
id,
ScalableImageFeature<T>(space, box, painter = painter, attributes = attributes)
)
public fun <T : Any> FeatureGroup<T>.text(
position: T,
text: String,
font: Font.() -> Unit = { size = 16f },
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, TextFeature<T>> = feature(
id,
TextFeature(space, position, text, fontConfig = font, attributes = attributes)
)
//public fun <T> StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND<T> {
// val strides = Strides(shape)
// return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) })
//}
public inline fun <reified T> Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D<T> {
val strides = Strides(ShapeND(rows, columns))
return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }).as2D()
}
public fun <T : Any> FeatureGroup<T>.pixelMap(
rectangle: Rectangle<T>,
pixelMap: Structure2D<Color?>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, PixelMapFeature<T>> = feature(
id,
PixelMapFeature(space, rectangle, pixelMap, attributes = attributes)
)
/**
* Create a pretty tree-like representation of this feature group
*/
public fun FeatureGroup<*>.toPrettyString(): String {
fun StringBuilder.printGroup(id: String, group: FeatureGroup<*>, prefix: String) {
appendLine("${prefix}* [group] $id")
group.featureMap.forEach { (id, feature) ->
if (feature is FeatureGroup<*>) {
printGroup(id, feature, " ")
} else {
appendLine("$prefix * [${feature::class.simpleName}] $id ")
}
}
}
return buildString {
printGroup("root", this@toPrettyString, "")
}
}

View File

@ -0,0 +1,406 @@
package space.kscience.maps.features
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.jetbrains.skia.Font
import space.kscience.attributes.Attribute
import space.kscience.attributes.Attributes
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.nd.*
import space.kscience.kmath.structures.Buffer
import space.kscience.maps.features.FeatureStore.Companion.generateFeatureId
//@JvmInline
//public value class FeatureId<out F : Feature<*>>(public val id: String)
/**
* A reference to a feature inside a [FeatureStore]
*/
public class FeatureRef<T : Any, out F : Feature<T>> internal constructor(
internal val store: FeatureStore<T>,
internal val id: String,
) {
override fun toString(): String = "FeatureRef($id)"
}
@Suppress("UNCHECKED_CAST")
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.resolve(): F =
store.features[id]?.let { it as F } ?: error("Feature with ref $this not found")
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>
/**
* Add or replace feature. If [id] is null, then a unique id is generated
*/
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F>
public fun putFeatures(features: Map<String, Feature<T>?>)
/**
* Update existing feature if it is present and is of type [F]
*/
public fun <F : Feature<T>> updateFeature(id: String, block: (F?) -> 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>,
) : CoordinateSpace<T> by space, FeatureBuilder<T>, FeatureSet<T> {
private val _featureFlow = MutableStateFlow<Map<String, Feature<T>>>(emptyMap())
public val featureFlow: StateFlow<Map<String, Feature<T>>> get() = _featureFlow
override val features: Map<String, Feature<T>> get() = featureFlow.value
override fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> {
val safeId = id ?: generateFeatureId(feature)
_featureFlow.value += (safeId to feature)
return FeatureRef(this, safeId)
}
public override fun putFeatures(features: Map<String, Feature<T>?>) {
_featureFlow.value = _featureFlow.value.toMutableMap().apply {
features.forEach { (key, value) ->
if (value == null) {
remove(key)
} else {
put(key, value)
}
}
}
}
@Suppress("UNCHECKED_CAST")
override fun <F : Feature<T>> updateFeature(id: String, block: (F?) -> F): FeatureRef<T, F> =
feature(id, block(features[id] as? F))
override fun group(
id: String?,
attributes: Attributes,
builder: FeatureGroup<T>.() -> Unit,
): FeatureRef<T, FeatureGroup<T>> {
val safeId: String = id ?: generateFeatureId<FeatureGroup<*>>()
return feature(safeId, FeatureGroup(this, safeId, attributes).apply(builder))
}
override fun removeFeature(id: String) {
_featureFlow.value -= id
}
override fun <F : Feature<T>> ref(id: String): FeatureRef<T, F> = FeatureRef(this, id)
public fun getBoundingBox(zoom: Float = Float.MAX_VALUE): Rectangle<T>? = with(space) {
features.values.mapNotNull { it.getBoundingBox(zoom) }.wrapRectangles()
}
public companion object {
internal fun generateFeatureId(prefix: String): String =
"$prefix[${uuid4().toIndex()}]"
internal fun generateFeatureId(feature: Feature<*>): String =
generateFeatureId(feature::class.simpleName ?: "undefined")
internal inline fun <reified F : Feature<*>> generateFeatureId(): String =
generateFeatureId(F::class.simpleName ?: "undefined")
/**
* Build, but do not remember map feature state
*/
public fun <T : Any> build(
coordinateSpace: CoordinateSpace<T>,
builder: FeatureStore<T>.() -> Unit = {},
): FeatureStore<T> = FeatureStore(coordinateSpace).apply(builder)
/**
* Build and remember map feature state
*/
@Composable
public fun <T : Any> remember(
coordinateSpace: CoordinateSpace<T>,
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 ?: generateFeatureId(feature)}", feature)
public override fun putFeatures(features: Map<String, Feature<T>?>) {
store.putFeatures(features.mapKeys { "$groupId/${it.key}" })
}
override fun <F : Feature<T>> updateFeature(id: String, block: (F?) -> F): FeatureRef<T, F> =
store.updateFeature("$groupId/$id", block)
override fun group(
id: String?,
attributes: Attributes,
builder: FeatureGroup<T>.() -> Unit,
): FeatureRef<T, FeatureGroup<T>> {
val safeId = id ?: generateFeatureId<FeatureGroup<*>>()
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> 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
}
}
/**
* Process all features with a given attribute from the one with highest [z] to lowest
*/
public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttribute(
key: Attribute<A>,
block: FeatureSet<T>.(ref: FeatureRef<T, *>, feature: Feature<T>, attribute: A) -> Unit,
) {
features.forEach { (id, feature) ->
feature.attributes[key]?.let {
block(ref<Feature<T>>(id), feature, it)
}
}
}
public inline fun <T : Any, A> FeatureSet<T>.forEachWithAttributeUntil(
key: Attribute<A>,
block: FeatureSet<T>.(ref: FeatureRef<T, *>, feature: Feature<T>, attribute: A) -> Boolean,
) {
features.forEach { (id, feature) ->
feature.attributes[key]?.let {
if (!block(ref<Feature<T>>(id), feature, it)) return@forEachWithAttributeUntil
}
}
}
public inline fun <T : Any, reified F : Feature<T>> FeatureSet<T>.forEachWithType(
crossinline block: FeatureSet<T>.(ref: FeatureRef<T, F>, feature: F) -> Unit,
) {
features.forEach { (id, feature) ->
if (feature is F) block(ref(id), feature)
}
}
public fun <T : Any> FeatureBuilder<T>.circle(
center: T,
size: Dp = 5.dp,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, CircleFeature<T>> = feature(
id, CircleFeature(space, center, size, attributes)
)
public fun <T : Any> FeatureBuilder<T>.rectangle(
centerCoordinates: T,
size: DpSize = DpSize(5.dp, 5.dp),
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, RectangleFeature<T>> = feature(
id, RectangleFeature(space, centerCoordinates, size, attributes)
)
public fun <T : Any> FeatureBuilder<T>.draw(
position: T,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
draw: DrawScope.() -> Unit,
): FeatureRef<T, DrawFeature<T>> = feature(
id,
DrawFeature(space, position, drawFeature = draw, attributes = attributes)
)
public fun <T : Any> FeatureBuilder<T>.line(
aCoordinates: T,
bCoordinates: T,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, LineFeature<T>> = feature(
id,
LineFeature(space, aCoordinates, bCoordinates, attributes)
)
public fun <T : Any> FeatureBuilder<T>.arc(
oval: Rectangle<T>,
startAngle: Angle,
arcLength: Angle,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, ArcFeature<T>> = feature(
id,
ArcFeature(space, oval, startAngle, arcLength, attributes)
)
public fun <T : Any> FeatureBuilder<T>.points(
points: List<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, PointsFeature<T>> = feature(
id,
PointsFeature(space, points, attributes)
)
public fun <T : Any> FeatureBuilder<T>.multiLine(
points: List<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, MultiLineFeature<T>> = feature(
id,
MultiLineFeature(space, points, attributes)
)
public fun <T : Any> FeatureBuilder<T>.polygon(
points: List<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, PolygonFeature<T>> = feature(
id,
PolygonFeature(space, points, attributes)
)
public fun <T : Any> FeatureBuilder<T>.icon(
position: T,
image: ImageVector,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, VectorIconFeature<T>> = feature(
id,
VectorIconFeature(
space,
position,
size,
image,
attributes
)
)
public fun <T : Any> FeatureBuilder<T>.scalableImage(
box: Rectangle<T>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
painter: @Composable () -> Painter,
): FeatureRef<T, ScalableImageFeature<T>> = feature(
id,
ScalableImageFeature<T>(space, box, painter = painter, attributes = attributes)
)
public fun <T : Any> FeatureBuilder<T>.text(
position: T,
text: String,
font: Font.() -> Unit = { size = 16f },
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, TextFeature<T>> = feature(
id,
TextFeature(space, position, text, fontConfig = font, attributes = attributes)
)
//public fun <T> StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND<T> {
// val strides = Strides(shape)
// return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) })
//}
public inline fun <reified T> Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D<T> {
val strides = Strides(ShapeND(rows, columns))
return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }).as2D()
}
public fun <T : Any> FeatureStore<T>.pixelMap(
rectangle: Rectangle<T>,
pixelMap: Structure2D<Color?>,
attributes: Attributes = Attributes.EMPTY,
id: String? = null,
): FeatureRef<T, PixelMapFeature<T>> = feature(
id,
PixelMapFeature(space, rectangle, pixelMap, attributes = attributes)
)
/**
* Create a pretty tree-like representation of this feature group
*/
public fun FeatureGroup<*>.toPrettyString(): String {
fun StringBuilder.printGroup(id: String, group: FeatureGroup<*>, prefix: String) {
appendLine("${prefix}* [group] $id")
group.features.forEach { (id, feature) ->
if (feature is FeatureGroup<*>) {
printGroup(id, feature, " ")
} else {
appendLine("$prefix * [${feature::class.simpleName}] $id ")
}
}
}
return buildString {
printGroup("root", this@toPrettyString, "")
}
}

View File

@ -4,28 +4,20 @@ 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,
): FeatureRef<T, LineFeature<T>> { ): FeatureRef<T, LineFeature<T>> {
var lineId: FeatureRef<T, LineFeature<T>>? = null val lineId = id ?: FeatureStore.generateFeatureId<LineFeature<*>>()
fun drawLine(): FeatureRef<T, LineFeature<T>> { fun drawLine(): FeatureRef<T, LineFeature<T>> = updateFeature(lineId) { old ->
val currentId = feature( LineFeature(
lineId?.id ?: id, space,
LineFeature( aId.resolve().center,
space, bId.resolve().center,
aId.resolve().center, old?.attributes ?: Attributes(ZAttribute, -10f)
bId.resolve().center,
Attributes<FeatureGroup<T>> {
ZAttribute(-10f)
lineId?.attributes?.let { putAll(it) }
}
)
) )
lineId = currentId
return currentId
} }
aId.draggable { _, _ -> aId.draggable { _, _ ->
@ -39,26 +31,18 @@ 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>> {
var polygonId: FeatureRef<T, MultiLineFeature<T>>? = null val polygonId = id ?: FeatureStore.generateFeatureId("multiline")
fun drawLines(): FeatureRef<T, MultiLineFeature<T>> { fun drawLines(): FeatureRef<T, MultiLineFeature<T>> = updateFeature(polygonId) { old ->
val currentId = feature( MultiLineFeature(
polygonId?.id ?: id, space,
MultiLineFeature( points.map { it.resolve().center },
space, old?.attributes ?: Attributes(ZAttribute, -10f)
points.map { it.resolve().center },
Attributes<FeatureGroup<T>>{
ZAttribute(-10f)
polygonId?.attributes?.let { putAll(it) }
}
)
) )
polygonId = currentId
return currentId
} }
points.forEach { points.forEach {
@ -71,7 +55,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

@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.drawscope.translate
import space.kscience.attributes.Attributes
import space.kscience.attributes.plus import space.kscience.attributes.plus
import space.kscience.kmath.PerformancePitfall import space.kscience.kmath.PerformancePitfall
@ -20,14 +21,16 @@ import space.kscience.kmath.PerformancePitfall
public fun <T : Any> FeatureDrawScope<T>.drawFeature( public fun <T : Any> FeatureDrawScope<T>.drawFeature(
feature: Feature<T>, feature: Feature<T>,
baseAttributes: Attributes,
): Unit { ): Unit {
val color = feature.color ?: Color.Red val attributes = baseAttributes + feature.attributes
val alpha = feature.attributes[AlphaAttribute] ?: 1f val color = attributes[ColorAttribute] ?: Color.Red
val alpha = attributes[AlphaAttribute] ?: 1f
//avoid drawing invisible features //avoid drawing invisible features
if(feature.attributes[VisibleAttribute] == false) return if(attributes[VisibleAttribute] == false) return
when (feature) { when (feature) {
is FeatureSelector -> drawFeature(feature.selector(state.zoom)) is FeatureSelector -> drawFeature(feature.selector(state.zoom), attributes)
is CircleFeature -> drawCircle( is CircleFeature -> drawCircle(
color, color,
feature.radius.toPx(), feature.radius.toPx(),
@ -49,8 +52,8 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
color, color,
feature.a.toOffset(), feature.a.toOffset(),
feature.b.toOffset(), feature.b.toOffset(),
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, strokeWidth = attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pathEffect = feature.attributes[PathEffectAttribute], pathEffect = attributes[PathEffectAttribute],
alpha = alpha alpha = alpha
) )
@ -84,7 +87,7 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
} }
} }
is TextFeature -> drawText(feature.text, feature.position.toOffset(), feature.attributes) is TextFeature -> drawText(feature.text, feature.position.toOffset(), attributes)
is DrawFeature -> { is DrawFeature -> {
val offset = feature.position.toOffset() val offset = feature.position.toOffset()
@ -94,13 +97,7 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
} }
is FeatureGroup -> { is FeatureGroup -> {
feature.featureMap.values.forEach { //ignore groups
drawFeature(
it.withAttributes {
feature.attributes + this
}
)
}
} }
is PathFeature -> { is PathFeature -> {
@ -117,9 +114,9 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
drawPoints( drawPoints(
points = points, points = points,
color = color, color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: 5f, strokeWidth = attributes[StrokeAttribute] ?: 5f,
pointMode = PointMode.Points, pointMode = PointMode.Points,
pathEffect = feature.attributes[PathEffectAttribute], pathEffect = attributes[PathEffectAttribute],
alpha = alpha alpha = alpha
) )
} }
@ -129,9 +126,9 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
drawPoints( drawPoints(
points = points, points = points,
color = color, color = color,
strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth, strokeWidth = attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
pointMode = PointMode.Polygon, pointMode = PointMode.Polygon,
pathEffect = feature.attributes[PathEffectAttribute], pathEffect = attributes[PathEffectAttribute],
alpha = alpha alpha = alpha
) )
} }

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

@ -6,6 +6,8 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.jfree.svg.SVGGraphics2D import org.jfree.svg.SVGGraphics2D
import org.jfree.svg.SVGUtils import org.jfree.svg.SVGUtils
import space.kscience.attributes.Attributes
import space.kscience.attributes.plus
import space.kscience.maps.features.* import space.kscience.maps.features.*
import space.kscience.maps.scheme.XY import space.kscience.maps.scheme.XY
import space.kscience.maps.scheme.XYCanvasState import space.kscience.maps.scheme.XYCanvasState
@ -17,11 +19,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() }
) )
@ -162,10 +162,22 @@ public fun FeatureStateSnapshot<XY>.generateSvg(
val svgScope = SvgDrawScope(svgCanvasState, svgGraphics2D, painterCache) val svgScope = SvgDrawScope(svgCanvasState, svgGraphics2D, painterCache)
svgScope.apply { svgScope.apply {
features.values.sortedBy { it.z } features.entries.sortedBy { it.value.z }
.filter { state.viewPoint.zoom in it.zoomRange } .filter { state.viewPoint.zoom in it.value.zoomRange }
.forEach { feature -> .forEach { (id, feature) ->
this@apply.drawFeature(feature) val attributesCache = mutableMapOf<List<String>, Attributes>()
fun computeGroupAttributes(path: List<String>): Attributes = attributesCache.getOrPut(path){
if (path.isEmpty()) return Attributes.EMPTY
else if (path.size == 1) {
features[path.first()]?.attributes ?: Attributes.EMPTY
} else {
computeGroupAttributes(path.dropLast(1)) + (features[path.first()]?.attributes ?: Attributes.EMPTY)
}
}
val path = id.split("/")
drawFeature(feature, computeGroupAttributes(path.dropLast(1)))
} }
} }
return svgGraphics2D.getSVGElement(id) return svgGraphics2D.getSVGElement(id)