immutable_features #24
@ -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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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 {
|
||||||
features.featureMap.values.sortedBy { it.z }
|
ComposeFeatureDrawScope(this, state, painterCache, textMeasurer).apply(draw).apply {
|
||||||
.filter { state.viewPoint.zoom in it.zoomRange }
|
|
||||||
.forEach { feature ->
|
val attributesCache = mutableMapOf<List<String>, Attributes>()
|
||||||
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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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, "")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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, "")
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
|
||||||
lineId?.id ?: id,
|
|
||||||
LineFeature(
|
LineFeature(
|
||||||
space,
|
space,
|
||||||
aId.resolve().center,
|
aId.resolve().center,
|
||||||
bId.resolve().center,
|
bId.resolve().center,
|
||||||
Attributes<FeatureGroup<T>> {
|
old?.attributes ?: Attributes(ZAttribute, -10f)
|
||||||
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(
|
|
||||||
polygonId?.id ?: id,
|
|
||||||
MultiLineFeature(
|
MultiLineFeature(
|
||||||
space,
|
space,
|
||||||
points.map { it.resolve().center },
|
points.map { it.resolve().center },
|
||||||
Attributes<FeatureGroup<T>>{
|
old?.attributes ?: Attributes(ZAttribute, -10f)
|
||||||
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>> {
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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>> {
|
||||||
|
@ -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
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user