Working server prototype

This commit is contained in:
Alexander Nozik 2020-12-08 22:42:08 +03:00
parent a7136d3eff
commit faddb8a393
13 changed files with 117 additions and 95 deletions

View File

@ -3,17 +3,19 @@ package ru.mipt.npm.sat
import hep.dataforge.context.Global
import hep.dataforge.names.asName
import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.get
import hep.dataforge.vision.server.visionModule
import hep.dataforge.vision.server.close
import hep.dataforge.vision.server.serve
import hep.dataforge.vision.server.show
import hep.dataforge.vision.solid.Solid
import hep.dataforge.vision.solid.SolidManager
import hep.dataforge.vision.solid.color
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.html.h1
import kotlinx.html.script
import kotlin.random.Random
@ -23,28 +25,36 @@ fun main() {
ySegments = 3,
)
val context = Global.context("SAT"){
val context = Global.context("SAT") {
plugin(SolidManager)
}
embeddedServer(CIO, 8080, host = "localhost"){
visionModule(context).apply {
val server = context.plugins.fetch(VisionManager).serve {
header {
script {
src = "sat-demo.js"
}
}
page {
h1 { +"Satellite detector demo" }
vision("main".asName(), sat)
}
}
launch {
while (isActive){
while (isActive) {
val currentLayer = Random.nextInt(10)
(sat["layer[$currentLayer]"] as? Solid)?.color(123)
delay(300)
(sat["layer[$currentLayer]"] as? Solid)?.color = null
}
}
}.start(wait = true)
}
server.show()
println("Press Enter to close server")
while (readLine()!="exit"){
//
}
server.close()
}

View File

@ -20,7 +20,7 @@ import org.w3c.dom.Element
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
class ThreeDemoGrid(element: Element, idPrefix: String = "") : Page<Solid> {
class ThreeDemoGrid(element: Element) : Page<Solid> {
private lateinit var navigationElement: HTMLElement
private lateinit var contentElement: HTMLDivElement

View File

@ -5,23 +5,23 @@ import hep.dataforge.vision.Vision
import kotlinx.html.FlowContent
import kotlinx.html.TagConsumer
public class BindingHtmlOutputScope<T, V : Vision>(
public class BindingOutputTagConsumer<T, V : Vision>(
root: TagConsumer<T>,
prefix: String? = null,
) : HtmlOutputScope<T, V>(root, prefix) {
) : OutputTagConsumer<T, V>(root, prefix) {
private val _bindings = HashMap<Name, V>()
public val bindings: Map<Name, V> get() = _bindings
override fun renderVision(htmlOutput: HtmlOutput<V>, vision: V) {
_bindings[htmlOutput.name] = vision
override fun FlowContent.renderVision(name: Name, vision: V) {
_bindings[name] = vision
}
}
public fun <T : Any> TagConsumer<T>.visionFragment(fragment: HtmlVisionFragment<Vision>): Map<Name, Vision> {
return BindingHtmlOutputScope<T, Vision>(this).apply(fragment.content).bindings
return BindingOutputTagConsumer<T, Vision>(this).apply(fragment.content).bindings
}
public fun FlowContent.visionFragment(fragment: HtmlVisionFragment<Vision>): Map<Name, Vision> {
return BindingHtmlOutputScope<Any?, Vision>(consumer).apply(fragment.content).bindings
return BindingOutputTagConsumer<Any?, Vision>(consumer).apply(fragment.content).bindings
}

View File

@ -14,7 +14,7 @@ public fun FlowContent.fragment(fragment: HtmlFragment) {
fragment.content(consumer)
}
public class HtmlVisionFragment<V : Vision>(public val content: HtmlOutputScope<*, V>.() -> Unit)
public class HtmlVisionFragment<V : Vision>(public val content: OutputTagConsumer<*, V>.() -> Unit)
public fun buildVisionFragment(block: HtmlOutputScope<*, Vision>.() -> Unit): HtmlVisionFragment<Vision> =
public fun buildVisionFragment(block: OutputTagConsumer<*, Vision>.() -> Unit): HtmlVisionFragment<Vision> =
HtmlVisionFragment(block)

View File

@ -5,47 +5,52 @@ import hep.dataforge.names.toName
import hep.dataforge.vision.Vision
import kotlinx.html.*
public class HtmlOutput<V : Vision>(
public val outputScope: HtmlOutputScope<*, V>,
/**
* An HTML div wrapper that includes the output [name] and inherited [render] function
*/
public class OutputDiv<in V : Vision>(
private val div: DIV,
public val name: Name,
public val div: DIV,
)
public val render: (V) -> Unit,
) : HtmlBlockTag by div
public abstract class HtmlOutputScope<R, V : Vision>(
/**
* Modified [TagConsumer] that allows rendering output fragments and visions in them
*/
public abstract class OutputTagConsumer<R, V : Vision>(
private val root: TagConsumer<R>,
public val idPrefix: String? = null,
private val idPrefix: String? = null,
) : TagConsumer<R> by root {
public open fun resolveId(name: Name): String = (idPrefix ?: "output:") + name.toString()
/**
* Create a placeholder but do not attach any [Vision] to it
* Render a vision inside the output fragment
*/
public inline fun <T> TagConsumer<T>.visionOutput(
protected abstract fun FlowContent.renderVision(name: Name, vision: V)
/**
* Create a placeholder for an output window
*/
public fun <T> TagConsumer<T>.visionOutput(
name: Name,
crossinline block: HtmlOutput<V>.() -> Unit = {},
block: OutputDiv<V>.() -> Unit = {},
): T = div {
id = resolveId(name)
classes = setOf(OUTPUT_CLASS)
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
@Suppress("UNCHECKED_CAST")
HtmlOutput(this@HtmlOutputScope, name, this).block()
OutputDiv<V>(this, name) { renderVision(name, it) }.block()
}
public inline fun <T> TagConsumer<T>.visionOutput(
public fun <T> TagConsumer<T>.visionOutput(
name: String,
crossinline block: HtmlOutput<V>.() -> Unit = {},
block: OutputDiv<V>.() -> Unit = {},
): T = visionOutput(name.toName(), block)
/**
* Create a placeholder and put a [Vision] in it
*/
public abstract fun renderVision(htmlOutput: HtmlOutput<V>, vision: V)
public fun <T> TagConsumer<T>.vision(name: Name, vision: V): Unit {
visionOutput(name) {
renderVision(this, vision)
render(vision)
}
}

View File

@ -1,5 +1,6 @@
package hep.dataforge.vision.html
import hep.dataforge.names.Name
import hep.dataforge.vision.Vision
import kotlinx.html.FlowContent
import kotlinx.html.TagConsumer
@ -7,22 +8,23 @@ import kotlinx.html.stream.createHTML
public typealias HtmlVisionRenderer<V> = FlowContent.(V) -> Unit
public class StaticHtmlOutputScope<R, V : Vision>(
/**
* An [OutputTagConsumer] that directly renders given [Vision] using provided [renderer]
*/
public class StaticOutputTagConsumer<R, V : Vision>(
root: TagConsumer<R>,
prefix: String? = null,
private val render: HtmlVisionRenderer<V>,
) : HtmlOutputScope<R, V>(root, prefix) {
private val renderer: HtmlVisionRenderer<V>,
) : OutputTagConsumer<R, V>(root, prefix) {
override fun renderVision(htmlOutput: HtmlOutput<V>, vision: V) {
htmlOutput.div.render(vision)
}
override fun FlowContent.renderVision(name: Name, vision: V): Unit = renderer(vision)
}
public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject(
root: TagConsumer<T>,
prefix: String? = null,
renderer: HtmlVisionRenderer<Vision>,
): T = StaticHtmlOutputScope(root, prefix, renderer).apply(content).finalize()
): T = StaticOutputTagConsumer(root, prefix, renderer).apply(content).finalize()
public fun HtmlVisionFragment<Vision>.renderToString(renderer: HtmlVisionRenderer<Vision>): String =
renderToObject(createHTML(), null, renderer)

View File

@ -10,16 +10,17 @@ import kotlin.test.Test
class HtmlTagTest {
fun HtmlOutput<Vision>.vision(block: Vision.() -> Unit) =
outputScope.renderVision(this, VisionBase().apply(block))
fun OutputDiv<Vision>.visionBase(block: VisionBase.() -> Unit) =
render(VisionBase().apply(block))
val fragment = buildVisionFragment {
div {
h1 { +"Head" }
visionOutput("ddd") {
vision {
visionBase {
configure {
set("myProp", 82)
set("otherProp", false)
}
}
}

View File

@ -2,12 +2,14 @@ package hep.dataforge.vision.client
import hep.dataforge.context.*
import hep.dataforge.meta.Meta
import hep.dataforge.meta.get
import hep.dataforge.meta.node
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
import hep.dataforge.vision.html.HtmlOutputScope.Companion.OUTPUT_NAME_ATTRIBUTE
import hep.dataforge.vision.html.OutputTagConsumer
import hep.dataforge.vision.html.OutputTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
import hep.dataforge.vision.html.OutputTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE
import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.Element
@ -41,12 +43,12 @@ public class VisionClient : AbstractPlugin() {
getRenderers().maxByOrNull { it.rateVision(vision) }
/**
* Fetch from server and render a vision, described in a given with [HtmlOutputScope.OUTPUT_CLASS] class.
* Fetch from server and render a vision, described in a given with [OutputTagConsumer.OUTPUT_CLASS] class.
*/
public fun fetchAndRenderVision(element: Element, requestUpdates: Boolean = true) {
val name = resolveName(element) ?: error("The element is not a vision output")
console.info("Found DF output with name $name")
if (!element.classList.contains(HtmlOutputScope.OUTPUT_CLASS)) error("The element $element is not an output element")
if (!element.classList.contains(OutputTagConsumer.OUTPUT_CLASS)) error("The element $element is not an output element")
val endpoint = resolveEndpoint(element)
console.info("Vision server is resolved to $endpoint")
@ -60,9 +62,9 @@ public class VisionClient : AbstractPlugin() {
if (response.ok) {
response.text().then { text ->
val vision = visionManager.decodeFromString(text)
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
renderer.render(element, vision)
val rendererConfiguration = vision.properties[RENDERER_CONFIGURATION_META_KEY].node ?: Meta.EMPTY
renderer.render(element, vision, rendererConfiguration)
if (requestUpdates) {
val wsUrl = URL(endpoint).apply {
pathname += "/ws"
@ -73,9 +75,10 @@ public class VisionClient : AbstractPlugin() {
onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String
if (stringData != null) {
// console.info("Received WS update: $stringData")
val dif = visionManager.jsonFormat
.decodeFromString(VisionChange.serializer(), stringData)
val dif = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(),
stringData
)
vision.update(dif)
} else {
console.error("WebSocket message data is not a string")
@ -103,6 +106,8 @@ public class VisionClient : AbstractPlugin() {
public companion object : PluginFactory<VisionClient> {
public const val RENDERER_CONFIGURATION_META_KEY: String = "@renderer"
override fun invoke(meta: Meta, context: Context): VisionClient = VisionClient()
override val tag: PluginTag = PluginTag(name = "vision.client", group = PluginTag.DATAFORGE_GROUP)
@ -112,10 +117,10 @@ public class VisionClient : AbstractPlugin() {
}
/**
* Fetch and render visions for all elements with [HtmlOutputScope.OUTPUT_CLASS] class inside given [element].
* Fetch and render visions for all elements with [OutputTagConsumer.OUTPUT_CLASS] class inside given [element].
*/
public fun VisionClient.fetchVisionsInChildren(element: Element, requestUpdates: Boolean = true) {
val elements = element.getElementsByClassName(HtmlOutputScope.OUTPUT_CLASS)
val elements = element.getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS)
console.info("Finished search for outputs. Found ${elements.length} items")
elements.asList().forEach { child ->
fetchAndRenderVision(child, requestUpdates)
@ -123,7 +128,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
* Fetch visions from the server for all elements with [OutputTagConsumer.OUTPUT_CLASS] class in the document body
*/
public fun VisionClient.fetchAndRenderAllVisions(requestUpdates: Boolean = true) {
val element = document.body ?: error("Document does not have a body")

View File

@ -1,13 +1,14 @@
package hep.dataforge.vision.client
import hep.dataforge.meta.DFExperimental
import hep.dataforge.meta.Meta
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.provider.Type
import hep.dataforge.vision.Vision
import hep.dataforge.vision.html.BindingHtmlOutputScope
import hep.dataforge.vision.html.HtmlOutputScope
import hep.dataforge.vision.html.BindingOutputTagConsumer
import hep.dataforge.vision.html.HtmlVisionFragment
import hep.dataforge.vision.html.OutputTagConsumer
import kotlinx.browser.document
import kotlinx.html.TagConsumer
import org.w3c.dom.*
@ -25,7 +26,7 @@ public interface ElementVisionRenderer {
/**
* Display the [vision] inside a given [element] replacing its current content
*/
public fun render(element: Element, vision: Vision): Unit
public fun render(element: Element, vision: Vision, meta: Meta = Meta.EMPTY): Unit
public companion object {
public const val TYPE: String = "elementVisionRenderer"
@ -44,11 +45,11 @@ public fun Map<String, Vision>.bind(rendererFactory: (Vision) -> ElementVisionRe
@DFExperimental
public fun Element.renderAllVisions(visionProvider: (Name) -> Vision, rendererFactory: (Vision) -> ElementVisionRenderer) {
val elements = getElementsByClassName(HtmlOutputScope.OUTPUT_CLASS)
val elements = getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS)
elements.asList().forEach { element ->
val name = element.attributes[HtmlOutputScope.OUTPUT_NAME_ATTRIBUTE]?.value
val name = element.attributes[OutputTagConsumer.OUTPUT_NAME_ATTRIBUTE]?.value
if (name == null) {
console.error("Attribute ${HtmlOutputScope.OUTPUT_NAME_ATTRIBUTE} not defined in the output element")
console.error("Attribute ${OutputTagConsumer.OUTPUT_NAME_ATTRIBUTE} not defined in the output element")
return@forEach
}
val vision = visionProvider(name.toName())
@ -65,7 +66,7 @@ public fun Document.renderAllVisions(visionProvider: (Name) -> Vision, rendererF
public fun HtmlVisionFragment<Vision>.renderInDocument(
root: TagConsumer<HTMLElement>,
renderer: ElementVisionRenderer,
): HTMLElement = BindingHtmlOutputScope<HTMLElement, Vision>(root).apply(content).let { scope ->
): HTMLElement = BindingOutputTagConsumer<HTMLElement, Vision>(root).apply(content).let { scope ->
scope.finalize().apply {
scope.bindings.forEach { (name, vision) ->
val id = scope.resolveId(name)

View File

@ -346,7 +346,7 @@ private class GDMLTransformer(val settings: GDMLTransformerSettings) {
final.prototypes {
proto.children.forEach { (token, item) ->
item.parent = null
set(token.asName(), item)
set(token.asName(), item as? Solid)
}
}
styleCache.forEach {

View File

@ -29,10 +29,14 @@ import io.ktor.routing.application
import io.ktor.routing.get
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.error
import io.ktor.websocket.WebSockets
import io.ktor.websocket.webSocket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.html.*
import kotlinx.html.stream.createHTML
@ -47,7 +51,7 @@ public class VisionServer internal constructor(
private val visionManager: VisionManager,
private val application: Application,
private val rootRoute: String,
) : Configurable {
) : Configurable, CoroutineScope by application {
override val config: Config = Config()
public var updateInterval: Long by config.long(300, key = UPDATE_INTERVAL_KEY)
public var cacheFragments: Boolean by config.boolean(true)
@ -78,23 +82,8 @@ public class VisionServer internal constructor(
title(title)
}
body {
// attributes[OUTPUT_ENDPOINT_ATTRIBUTE] = if (rootRoute.endsWith("/")) {
// rootRoute
// } else {
// "$rootRoute/"
// }
//Load the fragment and remember all loaded visions
visionMap = visionFragment(visionFragment)
// //The script runs when all headers already with required libraries are already loaded
// script {
// type = "text/javascript"
//
// val normalizedRoute =
// unsafe {
// //language=JavaScript
// +"fetchAndRenderAllVisions()"
// }
// }
}
return visionMap
@ -171,7 +160,7 @@ public class VisionServer internal constructor(
route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$route'",
headers: List<HtmlFragment> = emptyList(),
content: HtmlOutputScope<*, Vision>.() -> Unit,
content: OutputTagConsumer<*, Vision>.() -> Unit,
) {
page(buildVisionFragment(content), route, title, headers)
}
@ -215,6 +204,15 @@ public fun Application.visionModule(context: Context, route: String = DEFAULT_PA
return VisionServer(visionManager, this, route)
}
@OptIn(KtorExperimentalAPI::class)
public fun VisionManager.serve(
host: String = "localhost",
port: Int = 7777,
block: VisionServer.()->Unit
): ApplicationEngine = context.embeddedServer(CIO, port, host) {
visionModule(context).apply(block)
}.start()
public fun ApplicationEngine.show() {
val connector = environment.connectors.first()
val uri = URI("http", null, connector.host, connector.port, null, null, null)

View File

@ -65,9 +65,9 @@ public class SolidManager(meta: Meta) : AbstractPlugin(meta) {
serializersModule = serializersModuleForSolids
}
internal fun encodeToString(solid: Solid): String = jsonForSolids.encodeToString(PolymorphicSerializer(Vision::class), solid)
public fun encodeToString(solid: Solid): String = jsonForSolids.encodeToString(PolymorphicSerializer(Vision::class), solid)
internal fun decodeFromString(str: String): Vision = jsonForSolids.decodeFromString(PolymorphicSerializer(Vision::class), str).also {
fun decodeFromString(str: String): Solid = jsonForSolids.decodeFromString(PolymorphicSerializer(Solid::class), str).also {
if(it is VisionGroup){
it.attachChildren()
}

View File

@ -133,8 +133,8 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
return if (vision is Solid) ElementVisionRenderer.DEFAULT_RATING else ElementVisionRenderer.ZERO_RATING
}
override fun render(element: Element, vision: Vision) {
createCanvas(element).render(vision as? Solid ?: error("Only solids are rendered"))
override fun render(element: Element, vision: Vision, meta: Meta) {
createCanvas(element, Canvas3DOptions.read(meta)).render(vision as? Solid ?: error("Only solids are rendered"))
}
public companion object : PluginFactory<ThreePlugin> {