Using specialized VisionChange

This commit is contained in:
Alexander Nozik 2020-12-08 18:10:00 +03:00
parent 8a4779e9c4
commit a7136d3eff
14 changed files with 271 additions and 205 deletions

View File

@ -3,6 +3,8 @@
![Gradle build](https://github.com/mipt-npm/visionforge/workflows/Gradle%20build/badge.svg)
[![Slack](https://img.shields.io/badge/slack-channel-green?logo=slack)](https://kotlinlang.slack.com/archives/CEXV2QWNM)
# DataForge Visualization Platform
## Table of Contents

View File

@ -27,9 +27,11 @@ import org.apache.commons.math3.random.JDKRandomGenerator
import ru.mipt.npm.muon.monitor.Model
import ru.mipt.npm.muon.monitor.sim.Cos2TrackGenerator
import ru.mipt.npm.muon.monitor.sim.simulateOne
import io.ktor.response.respondText
import java.awt.Desktop
import java.io.File
import java.net.URI
import io.ktor.http.HttpStatusCode
private val generator = Cos2TrackGenerator(JDKRandomGenerator(223))
@ -42,7 +44,7 @@ fun Application.module(context: Context = Global) {
install(DefaultHeaders)
install(CallLogging)
install(ContentNegotiation) {
json(solidManager.visionManager.jsonFormat, ContentType.Application.Json)
json()
}
install(Routing) {
get("/event") {
@ -50,7 +52,11 @@ fun Application.module(context: Context = Global) {
call.respond(event)
}
get("/geometry") {
call.respond(Model.buildGeometry())
call.respondText(
solidManager.visionManager.encodeToString(Model.buildGeometry()),
contentType = ContentType.Application.Json,
status = HttpStatusCode.OK
)
}
static("/") {
resources()

View File

@ -66,7 +66,7 @@ public interface Vision : Configurable, Described {
/**
* Update this vision using external meta. Children are not updated.
*/
public fun update(change: Vision)
public fun update(change: VisionChange)
override val descriptor: NodeDescriptor?

View File

@ -46,12 +46,13 @@ public open class VisionBase : Vision {
* The config is initialized and assigned on-demand.
* To avoid unnecessary allocations, one should access [getAllProperties] via [getProperty] instead.
*/
override val config: Config
get() = properties ?: Config().also { config ->
override val config: Config by lazy {
properties ?: Config().also { config ->
properties = config.also {
it.onChange(this) { name, _, _ -> propertyChanged(name) }
}
}
}
@Transient
private val listeners = HashSet<PropertyListener>()
@ -101,9 +102,9 @@ public open class VisionBase : Vision {
properties = null
}
override fun update(change: Vision) {
if (change.properties != null) {
config.update(change.config)
override fun update(change: VisionChange) {
change.propertyChange[Name.EMPTY]?.let {
config.update(it)
}
}

View File

@ -0,0 +1,123 @@
package hep.dataforge.vision
import hep.dataforge.meta.*
import hep.dataforge.names.Name
import hep.dataforge.names.plus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.*
import kotlin.time.Duration
/**
* An update for a [Vision] or a [VisionGroup]
*/
public class VisionChangeBuilder: VisionContainerBuilder<Vision> {
private val propertyChange = HashMap<Name, Config>()
private val childrenChange = HashMap<Name, Vision?>()
public fun isEmpty(): Boolean = propertyChange.isEmpty() && childrenChange.isEmpty()
public fun propertyChanged(visionName: Name, propertyName: Name, item: MetaItem<*>?) {
propertyChange.getOrPut(visionName) { Config() }.setItem(propertyName, item)
}
override fun set(name: Name, child: Vision?) {
childrenChange[name] = child
}
/**
* Isolate collected changes by creating detached copies of given visions
*/
public fun isolate(manager: VisionManager): VisionChange = VisionChange(
propertyChange,
childrenChange.mapValues { it.value?.isolate(manager) }
)
}
private fun Vision.isolate(manager: VisionManager): Vision {
//TODO replace by efficient deep copy
val json = manager.encodeToJsonElement(this)
return manager.decodeFromJson(json)
}
@Serializable
public data class VisionChange(
val propertyChange: Map<Name, @Serializable(MetaSerializer::class) Meta>,
val childrenChange: Map<Name, Vision?>) {
public fun isEmpty(): Boolean = propertyChange.isEmpty() && childrenChange.isEmpty()
/**
* A shortcut to the top level property dif
*/
public val properties: Meta? get() = propertyChange[Name.EMPTY]
}
public inline fun VisionChange(manager: VisionManager, block: VisionChangeBuilder.() -> Unit): VisionChange =
VisionChangeBuilder().apply(block).isolate(manager)
private fun CoroutineScope.collectChange(
name: Name,
source: Vision,
mutex: Mutex,
collector: ()->VisionChangeBuilder,
) {
//Collect properties change
source.config.onChange(mutex) { propertyName, oldItem, newItem ->
if (oldItem != newItem) {
launch {
mutex.withLock {
collector().propertyChanged(name, propertyName, newItem)
}
}
}
}
if (source is VisionGroup) {
//Subscribe for children changes
source.children.forEach { (token, child) ->
collectChange(name + token, child, mutex, collector)
}
//TODO update styles?
//Subscribe for structure change
if (source is MutableVisionGroup) {
source.onStructureChange(mutex) { token, before, after ->
before?.removeChangeListener(mutex)
(before as? MutableVisionGroup)?.removeStructureChangeListener(mutex)
if (after != null) {
collectChange(name + token, after, mutex, collector)
}
collector()[name + token] = after
}
}
}
}
@DFExperimental
public fun Vision.flowChanges(
manager: VisionManager,
collectionDuration: Duration,
scope: CoroutineScope = manager.context,
): Flow<VisionChange> = flow {
val mutex = Mutex()
var collector = VisionChangeBuilder()
scope.collectChange(Name.EMPTY, this@flowChanges, mutex){collector}
while (true) {
//Wait for changes to accumulate
kotlinx.coroutines.delay(collectionDuration)
//Propagate updates only if something is changed
if (!collector.isEmpty()) {
//emit changes
emit(collector.isolate(manager))
//Reset the collector
collector = VisionChangeBuilder()
}
}
}

View File

@ -1,5 +1,6 @@
package hep.dataforge.vision
import hep.dataforge.meta.configure
import hep.dataforge.names.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -95,6 +96,9 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup {
attachChild(NameToken("@static", index = child.hashCode().toString()), child)
}
/**
* Create a vision group of the same type as this vision group. Do not attach it.
*/
protected open fun createGroup(): VisionGroupBase = VisionGroupBase()
/**
@ -158,22 +162,19 @@ public open class VisionGroupBase : VisionBase(), MutableVisionGroup {
}
}
override fun update(change: Vision) {
if (change is VisionGroup) {
override fun update(change: VisionChange) {
//update stylesheet
val changeStyleSheet = change.styleSheet
if (changeStyleSheet != null) {
styleSheet {
update(changeStyleSheet)
}
}
change.children.forEach { (token, child) ->
when {
child is NullVision -> removeChild(token)
children.containsKey(token) -> children[token]!!.update(child)
else -> attachChild(token, child)
}
// val changeStyleSheet = change.styleSheet
// if (changeStyleSheet != null) {
// styleSheet {
// update(changeStyleSheet)
// }
// }
change.propertyChange.forEach {(childName,configChange)->
get(childName)?.configure(configChange)
}
change.childrenChange.forEach { (name, child) ->
set(name, child)
}
super.update(change)
}

View File

@ -25,7 +25,7 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
}
}
private val jsonFormat: Json
public val jsonFormat: Json
get() = Json(defaultJson) {
serializersModule = this@VisionManager.serializersModule
}

View File

@ -1,153 +0,0 @@
package hep.dataforge.vision
import hep.dataforge.meta.*
import hep.dataforge.meta.descriptors.NodeDescriptor
import hep.dataforge.names.Name
import hep.dataforge.names.isEmpty
import hep.dataforge.names.plus
import hep.dataforge.vision.VisionManager.Companion.visionSerializer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.time.Duration
public abstract class EmptyVision : Vision {
@Suppress("SetterBackingFieldAssignment", "UNUSED_PARAMETER")
override var parent: VisionGroup? = null
set(value) {
//do nothing
}
override val properties: Config? = null
override val allProperties: Laminate
get() = Laminate(Meta.EMPTY)
override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? = null
override fun propertyChanged(name: Name) {}
override fun onPropertyChange(owner: Any?, action: (Name) -> Unit) {}
override fun removeChangeListener(owner: Any?) {}
override fun update(change: Vision) {
error("Null vision should be removed, not updated")
}
override val config: Config get() = Config()
override val descriptor: NodeDescriptor? get() = null
}
/**
* An empty vision existing only for Vision tree change representation. [NullVision] should not be used outside update logic.
*/
@Serializable
@SerialName("vision.null")
public object NullVision : EmptyVision()
/**
* Serialization proxy is used to create immutable reference for a given vision
*/
@Serializable(VisionSerializationProxy.Companion::class)
private class VisionSerializationProxy(val ref: Vision) : EmptyVision() {
companion object : KSerializer<VisionSerializationProxy> {
override val descriptor: SerialDescriptor = visionSerializer.descriptor
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(encoder: Encoder, value: VisionSerializationProxy) {
val serializer = encoder.serializersModule.getPolymorphic(Vision::class, value.ref)
?: error("The polymorphic serializer is not provided for ")
serializer.serialize(encoder, value.ref)
}
override fun deserialize(decoder: Decoder): VisionSerializationProxy =
VisionSerializationProxy(visionSerializer.deserialize(decoder))
}
}
private fun MutableVisionGroup.getOrCreate(name: Name): Vision {
if (name.isEmpty()) return this
val existing = get(name)
return existing ?: VisionGroupBase().also { set(name, it) }
}
private fun CoroutineScope.collectChange(
name: Name,
source: Vision,
mutex: Mutex,
target: () -> MutableVisionGroup,
) {
//Collect properties change
source.config.onChange(mutex){propertyName, oldItem, newItem->
if(oldItem!= newItem){
launch {
mutex.withLock {
target().getOrCreate(name).setProperty(propertyName, newItem)
}
}
}
}
// source.onPropertyChange(mutex) { propertyName ->
// launch {
// mutex.withLock {
// target().getOrCreate(name).setProperty(propertyName, source.getProperty(propertyName,false))
// }
// }
// }
val targetVision: Vision = target().getOrCreate(name)
if (source is VisionGroup) {
check(targetVision is MutableVisionGroup) { "Collector for a group should be a group" }
//Subscribe for children changes
source.children.forEach { (token, child) ->
collectChange(name + token, child, mutex, target)
}
//TODO update styles?
//Subscribe for structure change
if (source is MutableVisionGroup) {
source.onStructureChange(mutex) { token, before, after ->
before?.removeChangeListener(mutex)
(before as? MutableVisionGroup)?.removeStructureChangeListener(mutex)
if (after != null) {
targetVision[token] = VisionSerializationProxy(after)
collectChange(name + token, after, mutex, target)
} else {
targetVision[token] = NullVision
}
}
}
}
}
@DFExperimental
public fun Vision.flowChanges(scope: CoroutineScope, collectionDuration: Duration): Flow<Vision> = flow {
val mutex = Mutex()
var collector = VisionGroupBase()
scope.collectChange(Name.EMPTY, this@flowChanges, mutex) { collector }
while (true) {
//Wait for changes to accumulate
kotlinx.coroutines.delay(collectionDuration)
//Propagate updates only if something is changed
if (collector.children.isNotEmpty() || collector.properties?.isEmpty() != false) {
//emit changes
emit(collector)
//Reset the collector
collector = VisionGroupBase()
}
}
}

View File

@ -3,6 +3,7 @@ package hep.dataforge.vision.client
import hep.dataforge.context.*
import hep.dataforge.meta.Meta
import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionChange
import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.html.HtmlOutputScope
import hep.dataforge.vision.html.HtmlOutputScope.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
@ -36,7 +37,8 @@ public class VisionClient : AbstractPlugin() {
private fun getRenderers() = context.gather<ElementVisionRenderer>(ElementVisionRenderer.TYPE).values
public fun findRendererFor(vision: Vision): ElementVisionRenderer? = getRenderers().maxByOrNull { it.rateVision(vision) }
public fun findRendererFor(vision: Vision): ElementVisionRenderer? =
getRenderers().maxByOrNull { it.rateVision(vision) }
/**
* Fetch from server and render a vision, described in a given with [HtmlOutputScope.OUTPUT_CLASS] class.
@ -71,8 +73,10 @@ public class VisionClient : AbstractPlugin() {
onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String
if (stringData != null) {
val update = visionManager.decodeFromString(text)
vision.update(update)
// console.info("Received WS update: $stringData")
val dif = visionManager.jsonFormat
.decodeFromString(VisionChange.serializer(), stringData)
vision.update(dif)
} else {
console.error("WebSocket message data is not a string")
}
@ -121,7 +125,7 @@ public fun VisionClient.fetchVisionsInChildren(element: Element, requestUpdates:
/**
* Fetch visions from the server for all elements with [HtmlOutputScope.OUTPUT_CLASS] class in the document body
*/
public fun VisionClient.fetchAndRenderAllVisions(requestUpdates: Boolean = true){
public fun VisionClient.fetchAndRenderAllVisions(requestUpdates: Boolean = true) {
val element = document.body ?: error("Document does not have a body")
fetchVisionsInChildren(element, requestUpdates)
}

View File

@ -8,6 +8,7 @@ import hep.dataforge.meta.long
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionChange
import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.flowChanges
import hep.dataforge.vision.html.*
@ -126,8 +127,8 @@ public class VisionServer internal constructor(
application.log.debug("Opened server socket for $name")
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
try {
vision.flowChanges(this, updateInterval.milliseconds).collect { update ->
val json = visionManager.encodeToString(update)
vision.flowChanges(visionManager, updateInterval.milliseconds).collect { update ->
val json = VisionManager.defaultJson.encodeToString(VisionChange.serializer(), update)
outgoing.send(Frame.Text(json))
}
} catch (ex: Throwable) {
@ -197,10 +198,11 @@ public fun Application.visionModule(context: Context, route: String = DEFAULT_PA
}
}
if(featureOrNull(CallLogging) == null){
if (featureOrNull(CallLogging) == null) {
install(CallLogging)
}
val visionManager = context.plugins.fetch(VisionManager)
routing {
route(route) {
@ -210,8 +212,6 @@ public fun Application.visionModule(context: Context, route: String = DEFAULT_PA
}
}
val visionManager = context.plugins.fetch(VisionManager)
return VisionServer(visionManager, this, route)
}

View File

@ -33,7 +33,7 @@ public class Composite(
get() = null
}
public inline fun MutableVisionGroup.composite(
public inline fun VisionContainerBuilder<Solid>.composite(
type: CompositeType,
name: String = "",
builder: SolidGroup.() -> Unit
@ -58,11 +58,11 @@ public inline fun MutableVisionGroup.composite(
}
}
public inline fun MutableVisionGroup.union(name: String = "", builder: SolidGroup.() -> Unit): Composite =
public inline fun VisionContainerBuilder<Solid>.union(name: String = "", builder: SolidGroup.() -> Unit): Composite =
composite(CompositeType.UNION, name, builder = builder)
public inline fun MutableVisionGroup.subtract(name: String = "", builder: SolidGroup.() -> Unit): Composite =
public inline fun VisionContainerBuilder<Solid>.subtract(name: String = "", builder: SolidGroup.() -> Unit): Composite =
composite(CompositeType.SUBTRACT, name, builder = builder)
public inline fun MutableVisionGroup.intersect(name: String = "", builder: SolidGroup.() -> Unit): Composite =
public inline fun VisionContainerBuilder<Solid>.intersect(name: String = "", builder: SolidGroup.() -> Unit): Composite =
composite(CompositeType.INTERSECT, name, builder = builder)

View File

@ -5,8 +5,8 @@ import hep.dataforge.meta.descriptors.NodeDescriptor
import hep.dataforge.meta.float
import hep.dataforge.meta.get
import hep.dataforge.meta.node
import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionBase
import hep.dataforge.vision.VisionChange
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -21,16 +21,20 @@ public open class SolidBase : VisionBase(), Solid {
override var scale: Point3D? = null
override fun update(change: Vision) {
fun Meta.toVector(default: Float = 0f) = Point3D(
this[Solid.X_KEY].float ?: default,
this[Solid.Y_KEY].float ?: default,
this[Solid.Z_KEY].float ?: default
)
change.properties[Solid.POSITION_KEY].node?.toVector()?.let { position = it }
change.properties[Solid.ROTATION].node?.toVector()?.let { rotation = it }
change.properties[Solid.SCALE_KEY].node?.toVector(1f)?.let { scale = it }
override fun update(change: VisionChange) {
updatePosition(change.properties)
super.update(change)
}
}
internal fun Meta.toVector(default: Float = 0f) = Point3D(
this[Solid.X_KEY].float ?: default,
this[Solid.Y_KEY].float ?: default,
this[Solid.Z_KEY].float ?: default
)
internal fun Solid.updatePosition(meta: Meta?) {
meta[Solid.POSITION_KEY].node?.toVector()?.let { position = it }
meta[Solid.ROTATION].node?.toVector()?.let { rotation = it }
meta[Solid.SCALE_KEY].node?.toVector(1f)?.let { scale = it }
}

View File

@ -38,7 +38,7 @@ public class SolidGroup : VisionGroupBase(), Solid, PrototypeHolder {
/**
* Create or edit prototype node as a group
*/
public fun prototypes(builder: MutableVisionGroup.() -> Unit): Unit {
public fun prototypes(builder: VisionContainerBuilder<Solid>.() -> Unit): Unit {
(prototypes ?: Prototypes().also {
prototypes = it
it.parent = this
@ -65,6 +65,10 @@ public class SolidGroup : VisionGroupBase(), Solid, PrototypeHolder {
override fun createGroup(): SolidGroup = SolidGroup()
override fun update(change: VisionChange) {
updatePosition(change.properties)
super.update(change)
}
public companion object {
// val PROTOTYPES_KEY = NameToken("@prototypes")
@ -82,13 +86,13 @@ public fun SolidGroup(block: SolidGroup.() -> Unit): SolidGroup {
public tailrec fun PrototypeHolder.getPrototype(name: Name): Solid? =
prototypes?.get(name) as? Solid ?: (parent as? PrototypeHolder)?.getPrototype(name)
public fun MutableVisionGroup.group(name: Name = Name.EMPTY, action: SolidGroup.() -> Unit = {}): SolidGroup =
public fun VisionContainerBuilder<Vision>.group(name: Name = Name.EMPTY, action: SolidGroup.() -> Unit = {}): SolidGroup =
SolidGroup().apply(action).also { set(name, it) }
/**
* Define a group with given [name], attach it to this parent and return it.
*/
public fun MutableVisionGroup.group(name: String, action: SolidGroup.() -> Unit = {}): SolidGroup =
public fun VisionContainerBuilder<Vision>.group(name: String, action: SolidGroup.() -> Unit = {}): SolidGroup =
SolidGroup().apply(action).also { set(name, it) }
/**

View File

@ -0,0 +1,74 @@
package hep.dataforge.vision.solid
import hep.dataforge.context.Global
import hep.dataforge.meta.MetaItem
import hep.dataforge.names.toName
import hep.dataforge.vision.VisionChange
import hep.dataforge.vision.get
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class VisionUpdateTest {
val solidManager = Global.plugins.fetch(SolidManager)
val visionManager = solidManager.visionManager
@Test
fun testVisionUpdate(){
val targetVision = SolidGroup {
box(200,200,200, name = "origin")
}
val dif = VisionChange(visionManager){
group("top") {
color(123)
box(100,100,100)
}
propertyChanged("top".toName(), SolidMaterial.MATERIAL_COLOR_KEY, MetaItem.of("red"))
propertyChanged("origin".toName(), SolidMaterial.MATERIAL_COLOR_KEY, MetaItem.of("red"))
}
targetVision.update(dif)
assertTrue { targetVision["top"] is SolidGroup }
assertEquals("red", (targetVision["origin"] as Solid).color) // Should work
assertEquals("#00007b", (targetVision["top"] as SolidGroup).color) // new item always takes precedence
}
@Test
fun testVisionChangeSerialization(){
val change = VisionChange(visionManager){
group("top") {
color(123)
box(100,100,100)
}
propertyChanged("top".toName(), SolidMaterial.MATERIAL_COLOR_KEY, MetaItem.of("red"))
propertyChanged("origin".toName(), SolidMaterial.MATERIAL_COLOR_KEY, MetaItem.of("red"))
}
val serialized = visionManager.jsonFormat.encodeToString(VisionChange.serializer(), change)
println(serialized)
val reconstructed = visionManager.jsonFormat.decodeFromString(VisionChange.serializer(), serialized)
assertEquals(change.propertyChange,reconstructed.propertyChange)
}
@Test
fun testDeserialization(){
val str = """
{
"propertyChange": {
"layer[4]": {
"material": {
"color": 123
}
},
"layer[2]": {
"material": {
}
}
},
"childrenChange": {
}
}
""".trimIndent()
val reconstructed = visionManager.jsonFormat.decodeFromString(VisionChange.serializer(), str)
}
}