A lot of small refactoring in html

This commit is contained in:
Alexander Nozik 2020-12-12 10:44:45 +03:00
parent a38d70bade
commit 734d1e1168
14 changed files with 351 additions and 160 deletions

View File

@ -18,6 +18,13 @@ kscience{
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra
kotlin { kotlin {
js{
browser {
webpackTask {
this.outputFileName = "visionforge-solid.js"
}
}
}
afterEvaluate { afterEvaluate {
val jsBrowserDistribution by tasks.getting val jsBrowserDistribution by tasks.getting

View File

@ -6,34 +6,10 @@ import hep.dataforge.vision.client.fetchAndRenderAllVisions
import hep.dataforge.vision.solid.three.ThreePlugin import hep.dataforge.vision.solid.three.ThreePlugin
import kotlinx.browser.window import kotlinx.browser.window
//private class SatDemoApp : Application {
//
// override fun start(state: Map<String, Any>) {
// val element = document.getElementById("canvas") as? HTMLElement
// ?: error("Element with id 'canvas' not found on page")
// val three = Global.plugins.fetch(ThreePlugin)
//
// val sat = visionOfSatellite(
// ySegments = 3,
// )
// three.render(element, sat){
// minSize = 500
// axes{
// size = 500.0
// visible = true
// }
// }
// }
//
//}
//
//fun main() {
// startApplication(::SatDemoApp)
//}
fun main() { fun main() {
//Loading three-js renderer //Loading three-js renderer
Global.plugins.load(ThreePlugin) Global.plugins.load(ThreePlugin)
//Fetch from server and render visions for all outputs
window.onload = { window.onload = {
Global.plugins.fetch(VisionClient).fetchAndRenderAllVisions() Global.plugins.fetch(VisionClient).fetchAndRenderAllVisions()
} }

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Three js demo for particle physics</title>
<script type="text/javascript" src="sat-demo.js"></script>
</head>
<body class="application">
<div id="canvas"></div>
</body>
</html>

View File

@ -2,12 +2,9 @@ package ru.mipt.npm.sat
import hep.dataforge.context.Global import hep.dataforge.context.Global
import hep.dataforge.names.asName
import hep.dataforge.names.toName import hep.dataforge.names.toName
import hep.dataforge.vision.VisionManager import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.server.close import hep.dataforge.vision.server.*
import hep.dataforge.vision.server.serve
import hep.dataforge.vision.server.show
import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.Solid
import hep.dataforge.vision.solid.SolidManager import hep.dataforge.vision.solid.SolidManager
import hep.dataforge.vision.solid.color import hep.dataforge.vision.solid.color
@ -15,8 +12,8 @@ import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.html.div
import kotlinx.html.h1 import kotlinx.html.h1
import kotlinx.html.script
import kotlin.random.Random import kotlin.random.Random
@OptIn(KtorExperimentalAPI::class) @OptIn(KtorExperimentalAPI::class)
@ -30,27 +27,27 @@ fun main() {
} }
val server = context.plugins.fetch(VisionManager).serve { val server = context.plugins.fetch(VisionManager).serve {
header { useScript("visionforge-solid.js")
script { useCss("css/styles.css")
src = "sat-demo.js"
}
}
page { page {
h1 { +"Satellite detector demo" } div("flex-column") {
vision("main".asName(), sat) h1 { +"Satellite detector demo" }
} vision(sat)
launch {
delay(1000)
while (isActive) {
val target = "layer[${Random.nextInt(1,10)}].segment[${Random.nextInt(3)},${Random.nextInt(3)}]".toName()
(sat[target] as? Solid)?.color("red")
delay(300)
(sat[target] as? Solid)?.color = "green"
} }
} }
} }
server.show() server.show()
context.launch {
while (isActive) {
val target = "layer[${Random.nextInt(1,11)}].segment[${Random.nextInt(3)},${Random.nextInt(3)}]".toName()
(sat[target] as? Solid)?.color("red")
delay(300)
(sat[target] as? Solid)?.color = "green"
delay(10)
}
}
println("Press Enter to close server") println("Press Enter to close server")
while (readLine()!="exit"){ while (readLine()!="exit"){
// //

View File

@ -0,0 +1,16 @@
body{
width: 100%;
height: 100%;
overflow: hidden;
}
.flex-column{
width: calc(100% - 15px);
height: calc(100% - 15px);
display: flex;
flex-direction: column;
}
.visionforge-output{
flex-grow: 1;
}

View File

@ -16,12 +16,12 @@ kotlin {
dependencies { dependencies {
api("hep.dataforge:dataforge-context:$dataforgeVersion") api("hep.dataforge:dataforge-context:$dataforgeVersion")
api("org.jetbrains.kotlinx:kotlinx-html:$htmlVersion") api("org.jetbrains.kotlinx:kotlinx-html:$htmlVersion")
api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion")
} }
} }
jsMain { jsMain {
dependencies { dependencies {
api("org.jetbrains:kotlin-extensions:1.0.1-$kotlinWrappersVersion") api("org.jetbrains:kotlin-extensions:1.0.1-$kotlinWrappersVersion")
api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion")
} }
} }
} }

View File

@ -35,6 +35,7 @@ public class VisionChangeBuilder : VisionContainerBuilder<Vision> {
propertyChange, propertyChange,
childrenChange.mapValues { it.value?.isolate(manager) } childrenChange.mapValues { it.value?.isolate(manager) }
) )
//TODO optimize isolation for visions without parents?
} }
private fun Vision.isolate(manager: VisionManager): Vision { private fun Vision.isolate(manager: VisionManager): Vision {
@ -77,12 +78,15 @@ private fun CoroutineScope.collectChange(
} }
} }
coroutineContext[Job]?.invokeOnCompletion {
source.config.removeListener(mutex)
}
if (source is VisionGroup) { if (source is VisionGroup) {
//Subscribe for children changes //Subscribe for children changes
source.children.forEach { (token, child) -> source.children.forEach { (token, child) ->
collectChange(name + token, child, mutex, collector) collectChange(name + token, child, mutex, collector)
} }
//TODO update styles?
//Subscribe for structure change //Subscribe for structure change
if (source is MutableVisionGroup) { if (source is MutableVisionGroup) {
@ -94,6 +98,9 @@ private fun CoroutineScope.collectChange(
} }
collector()[name + token] = after collector()[name + token] = after
} }
coroutineContext[Job]?.invokeOnCompletion {
source.removeStructureChangeListener(mutex)
}
} }
} }
} }
@ -102,23 +109,24 @@ private fun CoroutineScope.collectChange(
public fun Vision.flowChanges( public fun Vision.flowChanges(
manager: VisionManager, manager: VisionManager,
collectionDuration: Duration, collectionDuration: Duration,
scope: CoroutineScope = manager.context,
): Flow<VisionChange> = flow { ): Flow<VisionChange> = flow {
val mutex = Mutex() supervisorScope {
val mutex = Mutex()
var collector = VisionChangeBuilder() var collector = VisionChangeBuilder()
scope.collectChange(Name.EMPTY, this@flowChanges, mutex) { collector } collectChange(Name.EMPTY, this@flowChanges, mutex) { collector }
while (currentCoroutineContext().isActive) { while (currentCoroutineContext().isActive) {
//Wait for changes to accumulate //Wait for changes to accumulate
delay(collectionDuration) delay(collectionDuration)
//Propagate updates only if something is changed //Propagate updates only if something is changed
if (!collector.isEmpty()) { if (!collector.isEmpty()) {
//emit changes //emit changes
mutex.withLock { mutex.withLock {
emit(collector.isolate(manager)) emit(collector.isolate(manager))
//Reset the collector //Reset the collector
collector = VisionChangeBuilder() collector = VisionChangeBuilder()
}
} }
} }
} }

View File

@ -1,5 +1,6 @@
package hep.dataforge.vision.html package hep.dataforge.vision.html
import hep.dataforge.meta.Meta
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import kotlinx.html.FlowContent import kotlinx.html.FlowContent
@ -13,7 +14,7 @@ public class BindingOutputTagConsumer<T, V : Vision>(
private val _bindings = HashMap<Name, V>() private val _bindings = HashMap<Name, V>()
public val bindings: Map<Name, V> get() = _bindings public val bindings: Map<Name, V> get() = _bindings
override fun FlowContent.renderVision(name: Name, vision: V) { override fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta) {
_bindings[name] = vision _bindings[name] = vision
} }
} }

View File

@ -1,18 +1,24 @@
package hep.dataforge.vision.html package hep.dataforge.vision.html
import hep.dataforge.meta.*
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.toName import hep.dataforge.names.toName
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionManager
import kotlinx.html.* import kotlinx.html.*
/** /**
* An HTML div wrapper that includes the output [name] and inherited [render] function * A placeholder object to attach inline vision builders.
*/ */
public class OutputDiv<in V : Vision>( @DFExperimental
private val div: DIV, public class VisionOutput {
public val name: Name, public var meta: Meta = Meta.EMPTY
public val render: (V) -> Unit,
) : HtmlBlockTag by div public inline fun meta(block: MetaBuilder.() -> Unit) {
this.meta = Meta(block)
}
}
/** /**
* Modified [TagConsumer] that allows rendering output fragments and visions in them * Modified [TagConsumer] that allows rendering output fragments and visions in them
@ -26,34 +32,55 @@ public abstract class OutputTagConsumer<R, V : Vision>(
/** /**
* Render a vision inside the output fragment * Render a vision inside the output fragment
* @param name name of the output container
* @param vision an object to be rendered
* @param outputMeta optional configuration for the output container
*/ */
protected abstract fun FlowContent.renderVision(name: Name, vision: V) protected abstract fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta)
/** /**
* Create a placeholder for an output window * Create a placeholder for a vision output with optional [Vision] in it
*/ */
public fun <T> TagConsumer<T>.visionOutput( public fun <T> TagConsumer<T>.vision(
name: Name, name: Name,
block: OutputDiv<V>.() -> Unit = {}, vision: V? = null,
outputMeta: Meta = Meta.EMPTY,
): T = div { ): T = div {
id = resolveId(name) id = resolveId(name)
classes = setOf(OUTPUT_CLASS) classes = setOf(OUTPUT_CLASS)
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString() attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
OutputDiv<V>(this, name) { renderVision(name, it) }.block() if (!outputMeta.isEmpty()) {
} //Hard-code output configuration
script {
public fun <T> TagConsumer<T>.visionOutput( attributes["class"] = OUTPUT_META_CLASS
name: String, unsafe {
block: OutputDiv<V>.() -> Unit = {}, +VisionManager.defaultJson.encodeToString(MetaSerializer, outputMeta)
): T = visionOutput(name.toName(), block) }
}
public fun <T> TagConsumer<T>.vision(name: Name, vision: V): Unit {
visionOutput(name) {
render(vision)
} }
vision?.let { renderVision(name, it, outputMeta) }
} }
@OptIn(DFExperimental::class)
public inline fun <T> TagConsumer<T>.vision(
name: Name,
visionProvider: VisionOutput.() -> V,
): T {
val output = VisionOutput()
val vision = output.visionProvider()
return vision(name, vision, output.meta)
}
@OptIn(DFExperimental::class)
public inline fun <T> TagConsumer<T>.vision(
name: String,
visionProvider: VisionOutput.() -> V,
): T = vision(name.toName(), visionProvider)
public inline fun <T> TagConsumer<T>.vision(
vision: V,
): T = vision("vision[${vision.hashCode()}]".toName(), vision)
/** /**
* Process the resulting object produced by [TagConsumer] * Process the resulting object produced by [TagConsumer]
*/ */
@ -67,6 +94,9 @@ public abstract class OutputTagConsumer<R, V : Vision>(
public companion object { public companion object {
public const val OUTPUT_CLASS: String = "visionforge-output" public const val OUTPUT_CLASS: String = "visionforge-output"
public const val OUTPUT_META_CLASS: String = "visionforge-output-meta"
public const val OUTPUT_DATA_CLASS: String = "visionforge-output-data"
public const val OUTPUT_NAME_ATTRIBUTE: String = "data-output-name" public const val OUTPUT_NAME_ATTRIBUTE: String = "data-output-name"
public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint" public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
public const val DEFAULT_ENDPOINT: String = "." public const val DEFAULT_ENDPOINT: String = "."

View File

@ -1,12 +1,16 @@
package hep.dataforge.vision.html package hep.dataforge.vision.html
import hep.dataforge.meta.Meta
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionManager
import kotlinx.html.FlowContent import kotlinx.html.FlowContent
import kotlinx.html.TagConsumer import kotlinx.html.TagConsumer
import kotlinx.html.script
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
import kotlinx.html.unsafe
public typealias HtmlVisionRenderer<V> = FlowContent.(V) -> Unit public typealias HtmlVisionRenderer<V> = FlowContent.(V, Meta) -> Unit
/** /**
* An [OutputTagConsumer] that directly renders given [Vision] using provided [renderer] * An [OutputTagConsumer] that directly renders given [Vision] using provided [renderer]
@ -16,8 +20,18 @@ public class StaticOutputTagConsumer<R, V : Vision>(
prefix: String? = null, prefix: String? = null,
private val renderer: HtmlVisionRenderer<V>, private val renderer: HtmlVisionRenderer<V>,
) : OutputTagConsumer<R, V>(root, prefix) { ) : OutputTagConsumer<R, V>(root, prefix) {
override fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta): Unit = renderer(vision, outputMeta)
override fun FlowContent.renderVision(name: Name, vision: V): Unit = renderer(vision) public companion object {
public fun embed(manager: VisionManager): HtmlVisionRenderer<Vision> = { vision: Vision, _: Meta ->
script {
attributes["class"] = OUTPUT_DATA_CLASS
unsafe {
+manager.encodeToString(vision)
}
}
}
}
} }
public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject( public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject(
@ -26,5 +40,21 @@ public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject(
renderer: HtmlVisionRenderer<Vision>, renderer: HtmlVisionRenderer<Vision>,
): T = StaticOutputTagConsumer(root, prefix, renderer).apply(content).finalize() ): T = StaticOutputTagConsumer(root, prefix, renderer).apply(content).finalize()
/**
* Render an object to HTML embedding the data as script bodies
*/
public fun <T : Any> HtmlVisionFragment<Vision>.embedToObject(
manager: VisionManager,
root: TagConsumer<T>,
prefix: String? = null,
): T = renderToObject(root, prefix, StaticOutputTagConsumer.embed(manager))
public fun HtmlVisionFragment<Vision>.renderToString(renderer: HtmlVisionRenderer<Vision>): String = public fun HtmlVisionFragment<Vision>.renderToString(renderer: HtmlVisionRenderer<Vision>): String =
renderToObject(createHTML(), null, renderer) renderToObject(createHTML(), null, renderer)
/**
* Convert a fragment to a string, embedding all visions data
*/
public fun HtmlVisionFragment<Vision>.embedToString(manager: VisionManager): String =
embedToObject(manager, createHTML())

View File

@ -1,5 +1,6 @@
package hep.dataforge.vision.html package hep.dataforge.vision.html
import hep.dataforge.meta.DFExperimental
import hep.dataforge.meta.configure import hep.dataforge.meta.configure
import hep.dataforge.meta.set import hep.dataforge.meta.set
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
@ -10,14 +11,18 @@ import kotlin.test.Test
class HtmlTagTest { class HtmlTagTest {
fun OutputDiv<Vision>.visionBase(block: VisionBase.() -> Unit) = @OptIn(DFExperimental::class)
render(VisionBase().apply(block)) fun VisionOutput.base(block: VisionBase.() -> Unit) =
VisionBase().apply(block)
val fragment = buildVisionFragment { val fragment = buildVisionFragment {
div { div {
h1 { +"Head" } h1 { +"Head" }
visionOutput("ddd") { vision("ddd") {
visionBase { meta{
"metaProperty" put 87
}
base {
configure { configure {
set("myProp", 82) set("myProp", 82)
set("otherProp", false) set("otherProp", false)
@ -27,7 +32,7 @@ class HtmlTagTest {
} }
} }
val simpleVisionRenderer: HtmlVisionRenderer<Vision> = { vision -> val simpleVisionRenderer: HtmlVisionRenderer<Vision> = { vision, _ ->
div { div {
h2 { +"Properties" } h2 { +"Properties" }
ul { ul {
@ -41,7 +46,7 @@ class HtmlTagTest {
} }
} }
val groupRenderer: HtmlVisionRenderer<VisionGroup> = { group -> val groupRenderer: HtmlVisionRenderer<VisionGroup> = { group, _ ->
p { +"This is group" } p { +"This is group" }
} }

View File

@ -2,8 +2,7 @@ package hep.dataforge.vision.client
import hep.dataforge.context.* import hep.dataforge.context.*
import hep.dataforge.meta.Meta import hep.dataforge.meta.Meta
import hep.dataforge.meta.get import hep.dataforge.meta.MetaSerializer
import hep.dataforge.meta.node
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionChange import hep.dataforge.vision.VisionChange
import hep.dataforge.vision.VisionManager import hep.dataforge.vision.VisionManager
@ -42,16 +41,31 @@ public class VisionClient : AbstractPlugin() {
public fun findRendererFor(vision: Vision): ElementVisionRenderer? = public fun findRendererFor(vision: Vision): ElementVisionRenderer? =
getRenderers().maxByOrNull { it.rateVision(vision) } getRenderers().maxByOrNull { it.rateVision(vision) }
private fun Element.getEmbeddedData(className: String): String? = getElementsByClassName(className)[0]?.innerHTML
/** /**
* Fetch from server and render a vision, described in a given with [OutputTagConsumer.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) { public fun renderVisionAt(element: Element, requestUpdates: Boolean = true) {
val name = resolveName(element) ?: error("The element is not a vision output") val name = resolveName(element) ?: error("The element is not a vision output")
console.info("Found DF output with name $name") console.info("Found DF output with name $name")
if (!element.classList.contains(OutputTagConsumer.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) val endpoint = resolveEndpoint(element)
console.info("Vision server is resolved to $endpoint") console.info("Vision server is resolved to $endpoint")
val outputMeta = element.getEmbeddedData(OutputTagConsumer.OUTPUT_META_CLASS)?.let {
VisionManager.defaultJson.decodeFromString(MetaSerializer, it)
} ?: Meta.EMPTY
//Trying to render embedded vision
val embeddedVision = element.getEmbeddedData(OutputTagConsumer.OUTPUT_DATA_CLASS)?.let {
visionManager.decodeFromString(it)
}
if (embeddedVision != null) {
val renderer = findRendererFor(embeddedVision) ?: error("Could nof find renderer for $embeddedVision")
renderer.render(element, embeddedVision, outputMeta)
}
val fetchUrl = URL(endpoint).apply { val fetchUrl = URL(endpoint).apply {
searchParams.append("name", name) searchParams.append("name", name)
pathname += "/vision" pathname += "/vision"
@ -63,15 +77,14 @@ public class VisionClient : AbstractPlugin() {
response.text().then { text -> response.text().then { text ->
val vision = visionManager.decodeFromString(text) val vision = visionManager.decodeFromString(text)
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision") val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
val rendererConfiguration = vision.properties[RENDERER_CONFIGURATION_META_KEY].node ?: Meta.EMPTY renderer.render(element, vision, outputMeta)
renderer.render(element, vision, rendererConfiguration)
if (requestUpdates) { if (requestUpdates) {
val wsUrl = URL(endpoint).apply { val wsUrl = URL(endpoint).apply {
pathname += "/ws" pathname += "/ws"
protocol = "ws" protocol = "ws"
searchParams.append("name", name) searchParams.append("name", name)
} }
val ws = WebSocket(wsUrl.toString()).apply { WebSocket(wsUrl.toString()).apply {
onmessage = { messageEvent -> onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String val stringData: String? = messageEvent.data as? String
if (stringData != null) { if (stringData != null) {
@ -106,8 +119,6 @@ public class VisionClient : AbstractPlugin() {
public companion object : PluginFactory<VisionClient> { public companion object : PluginFactory<VisionClient> {
public const val RENDERER_CONFIGURATION_META_KEY: String = "@renderer"
override fun invoke(meta: Meta, context: Context): VisionClient = VisionClient() override fun invoke(meta: Meta, context: Context): VisionClient = VisionClient()
override val tag: PluginTag = PluginTag(name = "vision.client", group = PluginTag.DATAFORGE_GROUP) override val tag: PluginTag = PluginTag(name = "vision.client", group = PluginTag.DATAFORGE_GROUP)
@ -123,7 +134,7 @@ public fun VisionClient.fetchVisionsInChildren(element: Element, requestUpdates:
val elements = element.getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS) val elements = element.getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS)
console.info("Finished search for outputs. Found ${elements.length} items") console.info("Finished search for outputs. Found ${elements.length} items")
elements.asList().forEach { child -> elements.asList().forEach { child ->
fetchAndRenderVision(child, requestUpdates) renderVisionAt(child, requestUpdates)
} }
} }

View File

@ -0,0 +1,67 @@
//package hep.dataforge.vision.export
//
//package kscience.plotly
//
//import kotlinx.html.*
//import kotlinx.html.stream.createHTML
//
///**
// * A custom HTML fragment including plotly container reference
// */
//public class PlotlyFragment(public val render: FlowContent.(renderer: PlotlyRenderer) -> Unit)
//
///**
// * A complete page including headers and title
// */
//public data class PlotlyPage(
// val headers: Collection<HtmlFragment>,
// val fragment: PlotlyFragment,
// val title: String = "Plotly.kt",
// val renderer: PlotlyRenderer = StaticPlotlyRenderer
//) {
// public fun render(): String = createHTML().html {
// head {
// meta {
// charset = "utf-8"
// }
// title(this@PlotlyPage.title)
// headers.distinct().forEach { it.visit(consumer) }
// }
// body {
// fragment.render(this, renderer)
// }
// }
//}
//
//public fun Plotly.fragment(content: FlowContent.(renderer: PlotlyRenderer) -> Unit): PlotlyFragment = PlotlyFragment(content)
//
///**
// * Create a complete page including plots
// */
//public fun Plotly.page(
// vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
// title: String = "Plotly.kt",
// renderer: PlotlyRenderer = StaticPlotlyRenderer,
// content: FlowContent.(renderer: PlotlyRenderer) -> Unit
//): PlotlyPage = PlotlyPage(headers.toList(), fragment(content), title, renderer)
//
///**
// * Convert an html plot fragment to page
// */
//public fun PlotlyFragment.toPage(
// vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
// title: String = "Plotly.kt",
// renderer: PlotlyRenderer = StaticPlotlyRenderer
//): PlotlyPage = PlotlyPage(headers.toList(), this, title, renderer)
//
///**
// * Convert a plot to the sigle-plot page
// */
//public fun Plot.toPage(
// vararg headers: HtmlFragment = arrayOf(cdnPlotlyHeader),
// config: PlotlyConfig = PlotlyConfig.empty(),
// title: String = "Plotly.kt",
// renderer: PlotlyRenderer = StaticPlotlyRenderer
//): PlotlyPage = PlotlyFragment {
// plot(this@toPage, config = config, renderer = renderer)
//}.toPage(*headers, title = title)

View File

@ -1,10 +1,7 @@
package hep.dataforge.vision.server package hep.dataforge.vision.server
import hep.dataforge.context.Context import hep.dataforge.context.Context
import hep.dataforge.meta.Config import hep.dataforge.meta.*
import hep.dataforge.meta.Configurable
import hep.dataforge.meta.boolean
import hep.dataforge.meta.long
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.names.toName import hep.dataforge.names.toName
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
@ -25,21 +22,16 @@ import io.ktor.http.content.static
import io.ktor.http.withCharset import io.ktor.http.withCharset
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.application import io.ktor.routing.*
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.cio.CIO
import io.ktor.server.engine.ApplicationEngine import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.error
import io.ktor.websocket.WebSockets import io.ktor.websocket.WebSockets
import io.ktor.websocket.webSocket import io.ktor.websocket.webSocket
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.onEach
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
import java.awt.Desktop import java.awt.Desktop
@ -91,7 +83,70 @@ public class VisionServer internal constructor(
return visionMap return visionMap
} }
public fun page( /**
* Server a map of visions without providing explicit html page for them
*/
@OptIn(DFExperimental::class)
public fun serveVisions(route: Route, visions: Map<Name, Vision>): Unit = route {
application.log.info("Serving visions $visions at $route")
//Update websocket
webSocket("ws") {
val name: String = call.request.queryParameters["name"]
?: error("Vision name is not defined in parameters")
application.log.debug("Opened server socket for $name")
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
try {
withContext(visionManager.context.coroutineContext) {
vision.flowChanges(visionManager, updateInterval.milliseconds).collect { update ->
val json = VisionManager.defaultJson.encodeToString(
VisionChange.serializer(),
update
)
outgoing.send(Frame.Text(json))
}
}
} catch (t: Throwable) {
application.log.info("WebSocket update channel for $name is closed with exception: $t")
}
}
//Plots in their json representation
get("vision") {
val name: String = call.request.queryParameters["name"]
?: error("Vision name is not defined in parameters")
val vision: Vision? = visions[name.toName()]
if (vision == null) {
call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found")
} else {
call.respondText(
visionManager.encodeToString(vision),
contentType = ContentType.Application.Json,
status = HttpStatusCode.OK
)
}
}
}
/**
* Serv visions in a given [route] without providing a page template
*/
public fun serveVisions(route: String, visions: Map<Name, Vision>): Unit {
application.routing {
route(rootRoute) {
route(route) {
serveVisions(this, visions)
}
}
}
}
/**
* Serve a page, potentially containing any number of visions at a given [route] with given [headers].
*
*/
public fun servePage(
visionFragment: HtmlVisionFragment<Vision>, visionFragment: HtmlVisionFragment<Vision>,
route: String = DEFAULT_PAGE, route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$route'", title: String = "VisionForge server page '$route'",
@ -100,6 +155,7 @@ public class VisionServer internal constructor(
val visions = HashMap<Name, Vision>() val visions = HashMap<Name, Vision>()
val cachedHtml: String? = if (cacheFragments) { val cachedHtml: String? = if (cacheFragments) {
//Create and cache page html and map of visions
createHTML(true).html { createHTML(true).html {
visions.putAll(buildPage(visionFragment, title, headers)) visions.putAll(buildPage(visionFragment, title, headers))
} }
@ -110,43 +166,17 @@ public class VisionServer internal constructor(
application.routing { application.routing {
route(rootRoute) { route(rootRoute) {
route(route) { route(route) {
//Update websocket serveVisions(this, visions)
webSocket("ws") {
val name: String = call.request.queryParameters["name"]
?: error("Vision name is not defined in parameters")
application.log.debug("Opened server socket for $name")
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
vision.flowChanges(visionManager, updateInterval.milliseconds).onEach { update ->
val json = VisionManager.defaultJson.encodeToString(VisionChange.serializer(), update)
outgoing.send(Frame.Text(json))
}.catch { ex ->
application.log.error(ex)
}.launchIn(visionManager.context).join()
}
//Plots in their json representation
get("vision") {
val name: String = call.request.queryParameters["name"]
?: error("Vision name is not defined in parameters")
val vision: Vision? = visions[name.toName()]
if (vision == null) {
call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found")
} else {
call.respondText(
visionManager.encodeToString(vision),
contentType = ContentType.Application.Json,
status = HttpStatusCode.OK
)
}
}
//filled pages //filled pages
get { get {
if (cachedHtml == null) { if (cachedHtml == null) {
//re-create html and vision list on each call
call.respondHtml { call.respondHtml {
visions.clear()
visions.putAll(buildPage(visionFragment, title, headers)) visions.putAll(buildPage(visionFragment, title, headers))
} }
} else { } else {
//Use cached html
call.respondText(cachedHtml, ContentType.Text.Html.withCharset(Charsets.UTF_8)) call.respondText(cachedHtml, ContentType.Text.Html.withCharset(Charsets.UTF_8))
} }
} }
@ -155,13 +185,16 @@ public class VisionServer internal constructor(
} }
} }
/**
* A shortcut method to easily create Complete pages filled with visions
*/
public fun page( public fun page(
route: String = DEFAULT_PAGE, route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$route'", title: String = "VisionForge server page '$route'",
headers: List<HtmlFragment> = emptyList(), headers: List<HtmlFragment> = emptyList(),
content: OutputTagConsumer<*, Vision>.() -> Unit, content: OutputTagConsumer<*, Vision>.() -> Unit,
) { ) {
page(buildVisionFragment(content), route, title, headers) servePage(buildVisionFragment(content), route, title, headers)
} }
@ -171,11 +204,33 @@ public class VisionServer internal constructor(
} }
} }
/**
* Use a script with given [src] as a global header for all pages.
*/
public inline fun VisionServer.useScript(src: String, crossinline block: SCRIPT.() -> Unit = {}) {
header {
script {
type = "text/javascript"
this.src = src
block()
}
}
}
public inline fun VisionServer.useCss(href: String, crossinline block: LINK.() -> Unit = {}) {
header {
link {
rel = "stylesheet"
this.href = href
block()
}
}
}
/** /**
* Attach plotly application to given server * Attach plotly application to given server
*/ */
public fun Application.visionModule(context: Context, route: String = DEFAULT_PAGE): VisionServer { public fun Application.visionServer(context: Context, route: String = DEFAULT_PAGE): VisionServer {
if (featureOrNull(WebSockets) == null) { if (featureOrNull(WebSockets) == null) {
install(WebSockets) install(WebSockets)
} }
@ -209,7 +264,7 @@ public fun VisionManager.serve(
port: Int = 7777, port: Int = 7777,
block: VisionServer.() -> Unit, block: VisionServer.() -> Unit,
): ApplicationEngine = context.embeddedServer(CIO, port, host) { ): ApplicationEngine = context.embeddedServer(CIO, port, host) {
visionModule(context).apply(block) visionServer(context).apply(block)
}.start() }.start()
public fun ApplicationEngine.show() { public fun ApplicationEngine.show() {