A lot of small refactoring in html
This commit is contained in:
parent
a38d70bade
commit
734d1e1168
@ -18,6 +18,13 @@ kscience{
|
||||
val ktorVersion: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
js{
|
||||
browser {
|
||||
webpackTask {
|
||||
this.outputFileName = "visionforge-solid.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
afterEvaluate {
|
||||
val jsBrowserDistribution by tasks.getting
|
||||
|
||||
|
@ -6,34 +6,10 @@ import hep.dataforge.vision.client.fetchAndRenderAllVisions
|
||||
import hep.dataforge.vision.solid.three.ThreePlugin
|
||||
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() {
|
||||
//Loading three-js renderer
|
||||
Global.plugins.load(ThreePlugin)
|
||||
//Fetch from server and render visions for all outputs
|
||||
window.onload = {
|
||||
Global.plugins.fetch(VisionClient).fetchAndRenderAllVisions()
|
||||
}
|
||||
|
@ -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>
|
@ -2,12 +2,9 @@ package ru.mipt.npm.sat
|
||||
|
||||
|
||||
import hep.dataforge.context.Global
|
||||
import hep.dataforge.names.asName
|
||||
import hep.dataforge.names.toName
|
||||
import hep.dataforge.vision.VisionManager
|
||||
import hep.dataforge.vision.server.close
|
||||
import hep.dataforge.vision.server.serve
|
||||
import hep.dataforge.vision.server.show
|
||||
import hep.dataforge.vision.server.*
|
||||
import hep.dataforge.vision.solid.Solid
|
||||
import hep.dataforge.vision.solid.SolidManager
|
||||
import hep.dataforge.vision.solid.color
|
||||
@ -15,8 +12,8 @@ import io.ktor.util.KtorExperimentalAPI
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.html.div
|
||||
import kotlinx.html.h1
|
||||
import kotlinx.html.script
|
||||
import kotlin.random.Random
|
||||
|
||||
@OptIn(KtorExperimentalAPI::class)
|
||||
@ -30,27 +27,27 @@ fun main() {
|
||||
}
|
||||
|
||||
val server = context.plugins.fetch(VisionManager).serve {
|
||||
header {
|
||||
script {
|
||||
src = "sat-demo.js"
|
||||
}
|
||||
}
|
||||
useScript("visionforge-solid.js")
|
||||
useCss("css/styles.css")
|
||||
page {
|
||||
h1 { +"Satellite detector demo" }
|
||||
vision("main".asName(), 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"
|
||||
div("flex-column") {
|
||||
h1 { +"Satellite detector demo" }
|
||||
vision(sat)
|
||||
}
|
||||
}
|
||||
}
|
||||
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")
|
||||
while (readLine()!="exit"){
|
||||
//
|
||||
|
16
demo/sat-demo/src/jvmMain/resources/css/styles.css
Normal file
16
demo/sat-demo/src/jvmMain/resources/css/styles.css
Normal 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;
|
||||
}
|
@ -16,12 +16,12 @@ kotlin {
|
||||
dependencies {
|
||||
api("hep.dataforge:dataforge-context:$dataforgeVersion")
|
||||
api("org.jetbrains.kotlinx:kotlinx-html:$htmlVersion")
|
||||
api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion")
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
api("org.jetbrains:kotlin-extensions:1.0.1-$kotlinWrappersVersion")
|
||||
api("org.jetbrains:kotlin-css:1.0.0-$kotlinWrappersVersion")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ public class VisionChangeBuilder : VisionContainerBuilder<Vision> {
|
||||
propertyChange,
|
||||
childrenChange.mapValues { it.value?.isolate(manager) }
|
||||
)
|
||||
//TODO optimize isolation for visions without parents?
|
||||
}
|
||||
|
||||
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) {
|
||||
//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) {
|
||||
@ -94,6 +98,9 @@ private fun CoroutineScope.collectChange(
|
||||
}
|
||||
collector()[name + token] = after
|
||||
}
|
||||
coroutineContext[Job]?.invokeOnCompletion {
|
||||
source.removeStructureChangeListener(mutex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,23 +109,24 @@ private fun CoroutineScope.collectChange(
|
||||
public fun Vision.flowChanges(
|
||||
manager: VisionManager,
|
||||
collectionDuration: Duration,
|
||||
scope: CoroutineScope = manager.context,
|
||||
): Flow<VisionChange> = flow {
|
||||
val mutex = Mutex()
|
||||
supervisorScope {
|
||||
val mutex = Mutex()
|
||||
|
||||
var collector = VisionChangeBuilder()
|
||||
scope.collectChange(Name.EMPTY, this@flowChanges, mutex) { collector }
|
||||
var collector = VisionChangeBuilder()
|
||||
collectChange(Name.EMPTY, this@flowChanges, mutex) { collector }
|
||||
|
||||
while (currentCoroutineContext().isActive) {
|
||||
//Wait for changes to accumulate
|
||||
delay(collectionDuration)
|
||||
//Propagate updates only if something is changed
|
||||
if (!collector.isEmpty()) {
|
||||
//emit changes
|
||||
mutex.withLock {
|
||||
emit(collector.isolate(manager))
|
||||
//Reset the collector
|
||||
collector = VisionChangeBuilder()
|
||||
while (currentCoroutineContext().isActive) {
|
||||
//Wait for changes to accumulate
|
||||
delay(collectionDuration)
|
||||
//Propagate updates only if something is changed
|
||||
if (!collector.isEmpty()) {
|
||||
//emit changes
|
||||
mutex.withLock {
|
||||
emit(collector.isolate(manager))
|
||||
//Reset the collector
|
||||
collector = VisionChangeBuilder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package hep.dataforge.vision.html
|
||||
|
||||
import hep.dataforge.meta.Meta
|
||||
import hep.dataforge.names.Name
|
||||
import hep.dataforge.vision.Vision
|
||||
import kotlinx.html.FlowContent
|
||||
@ -13,7 +14,7 @@ public class BindingOutputTagConsumer<T, V : Vision>(
|
||||
private val _bindings = HashMap<Name, V>()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,24 @@
|
||||
package hep.dataforge.vision.html
|
||||
|
||||
import hep.dataforge.meta.*
|
||||
import hep.dataforge.names.Name
|
||||
import hep.dataforge.names.toName
|
||||
import hep.dataforge.vision.Vision
|
||||
import hep.dataforge.vision.VisionManager
|
||||
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>(
|
||||
private val div: DIV,
|
||||
public val name: Name,
|
||||
public val render: (V) -> Unit,
|
||||
) : HtmlBlockTag by div
|
||||
@DFExperimental
|
||||
public class VisionOutput {
|
||||
public var meta: Meta = Meta.EMPTY
|
||||
|
||||
public inline fun meta(block: MetaBuilder.() -> Unit) {
|
||||
this.meta = Meta(block)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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,
|
||||
block: OutputDiv<V>.() -> Unit = {},
|
||||
vision: V? = null,
|
||||
outputMeta: Meta = Meta.EMPTY,
|
||||
): T = div {
|
||||
id = resolveId(name)
|
||||
classes = setOf(OUTPUT_CLASS)
|
||||
attributes[OUTPUT_NAME_ATTRIBUTE] = name.toString()
|
||||
OutputDiv<V>(this, name) { renderVision(name, it) }.block()
|
||||
}
|
||||
|
||||
public fun <T> TagConsumer<T>.visionOutput(
|
||||
name: String,
|
||||
block: OutputDiv<V>.() -> Unit = {},
|
||||
): T = visionOutput(name.toName(), block)
|
||||
|
||||
|
||||
public fun <T> TagConsumer<T>.vision(name: Name, vision: V): Unit {
|
||||
visionOutput(name) {
|
||||
render(vision)
|
||||
if (!outputMeta.isEmpty()) {
|
||||
//Hard-code output configuration
|
||||
script {
|
||||
attributes["class"] = OUTPUT_META_CLASS
|
||||
unsafe {
|
||||
+VisionManager.defaultJson.encodeToString(MetaSerializer, outputMeta)
|
||||
}
|
||||
}
|
||||
}
|
||||
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]
|
||||
*/
|
||||
@ -67,6 +94,9 @@ public abstract class OutputTagConsumer<R, V : Vision>(
|
||||
|
||||
public companion object {
|
||||
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_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
|
||||
public const val DEFAULT_ENDPOINT: String = "."
|
||||
|
@ -1,12 +1,16 @@
|
||||
package hep.dataforge.vision.html
|
||||
|
||||
import hep.dataforge.meta.Meta
|
||||
import hep.dataforge.names.Name
|
||||
import hep.dataforge.vision.Vision
|
||||
import hep.dataforge.vision.VisionManager
|
||||
import kotlinx.html.FlowContent
|
||||
import kotlinx.html.TagConsumer
|
||||
import kotlinx.html.script
|
||||
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]
|
||||
@ -16,8 +20,18 @@ public class StaticOutputTagConsumer<R, V : Vision>(
|
||||
prefix: String? = null,
|
||||
private val renderer: HtmlVisionRenderer<V>,
|
||||
) : 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(
|
||||
@ -26,5 +40,21 @@ public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject(
|
||||
renderer: HtmlVisionRenderer<Vision>,
|
||||
): 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 =
|
||||
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())
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package hep.dataforge.vision.html
|
||||
|
||||
import hep.dataforge.meta.DFExperimental
|
||||
import hep.dataforge.meta.configure
|
||||
import hep.dataforge.meta.set
|
||||
import hep.dataforge.vision.Vision
|
||||
@ -10,14 +11,18 @@ import kotlin.test.Test
|
||||
|
||||
class HtmlTagTest {
|
||||
|
||||
fun OutputDiv<Vision>.visionBase(block: VisionBase.() -> Unit) =
|
||||
render(VisionBase().apply(block))
|
||||
@OptIn(DFExperimental::class)
|
||||
fun VisionOutput.base(block: VisionBase.() -> Unit) =
|
||||
VisionBase().apply(block)
|
||||
|
||||
val fragment = buildVisionFragment {
|
||||
div {
|
||||
h1 { +"Head" }
|
||||
visionOutput("ddd") {
|
||||
visionBase {
|
||||
vision("ddd") {
|
||||
meta{
|
||||
"metaProperty" put 87
|
||||
}
|
||||
base {
|
||||
configure {
|
||||
set("myProp", 82)
|
||||
set("otherProp", false)
|
||||
@ -27,7 +32,7 @@ class HtmlTagTest {
|
||||
}
|
||||
}
|
||||
|
||||
val simpleVisionRenderer: HtmlVisionRenderer<Vision> = { vision ->
|
||||
val simpleVisionRenderer: HtmlVisionRenderer<Vision> = { vision, _ ->
|
||||
div {
|
||||
h2 { +"Properties" }
|
||||
ul {
|
||||
@ -41,7 +46,7 @@ class HtmlTagTest {
|
||||
}
|
||||
}
|
||||
|
||||
val groupRenderer: HtmlVisionRenderer<VisionGroup> = { group ->
|
||||
val groupRenderer: HtmlVisionRenderer<VisionGroup> = { group, _ ->
|
||||
p { +"This is group" }
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,7 @@ 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.meta.MetaSerializer
|
||||
import hep.dataforge.vision.Vision
|
||||
import hep.dataforge.vision.VisionChange
|
||||
import hep.dataforge.vision.VisionManager
|
||||
@ -42,16 +41,31 @@ public class VisionClient : AbstractPlugin() {
|
||||
public fun findRendererFor(vision: Vision): ElementVisionRenderer? =
|
||||
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.
|
||||
*/
|
||||
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")
|
||||
console.info("Found DF output with name $name")
|
||||
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")
|
||||
|
||||
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 {
|
||||
searchParams.append("name", name)
|
||||
pathname += "/vision"
|
||||
@ -63,15 +77,14 @@ public class VisionClient : AbstractPlugin() {
|
||||
response.text().then { text ->
|
||||
val vision = visionManager.decodeFromString(text)
|
||||
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, rendererConfiguration)
|
||||
renderer.render(element, vision, outputMeta)
|
||||
if (requestUpdates) {
|
||||
val wsUrl = URL(endpoint).apply {
|
||||
pathname += "/ws"
|
||||
protocol = "ws"
|
||||
searchParams.append("name", name)
|
||||
}
|
||||
val ws = WebSocket(wsUrl.toString()).apply {
|
||||
WebSocket(wsUrl.toString()).apply {
|
||||
onmessage = { messageEvent ->
|
||||
val stringData: String? = messageEvent.data as? String
|
||||
if (stringData != null) {
|
||||
@ -106,8 +119,6 @@ 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)
|
||||
@ -123,7 +134,7 @@ public fun VisionClient.fetchVisionsInChildren(element: Element, requestUpdates:
|
||||
val elements = element.getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS)
|
||||
console.info("Finished search for outputs. Found ${elements.length} items")
|
||||
elements.asList().forEach { child ->
|
||||
fetchAndRenderVision(child, requestUpdates)
|
||||
renderVisionAt(child, requestUpdates)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
@ -1,10 +1,7 @@
|
||||
package hep.dataforge.vision.server
|
||||
|
||||
import hep.dataforge.context.Context
|
||||
import hep.dataforge.meta.Config
|
||||
import hep.dataforge.meta.Configurable
|
||||
import hep.dataforge.meta.boolean
|
||||
import hep.dataforge.meta.long
|
||||
import hep.dataforge.meta.*
|
||||
import hep.dataforge.names.Name
|
||||
import hep.dataforge.names.toName
|
||||
import hep.dataforge.vision.Vision
|
||||
@ -25,21 +22,16 @@ import io.ktor.http.content.static
|
||||
import io.ktor.http.withCharset
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.response.respondText
|
||||
import io.ktor.routing.application
|
||||
import io.ktor.routing.get
|
||||
import io.ktor.routing.route
|
||||
import io.ktor.routing.routing
|
||||
import io.ktor.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.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.stream.createHTML
|
||||
import java.awt.Desktop
|
||||
@ -91,7 +83,70 @@ public class VisionServer internal constructor(
|
||||
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>,
|
||||
route: String = DEFAULT_PAGE,
|
||||
title: String = "VisionForge server page '$route'",
|
||||
@ -100,6 +155,7 @@ public class VisionServer internal constructor(
|
||||
val visions = HashMap<Name, Vision>()
|
||||
|
||||
val cachedHtml: String? = if (cacheFragments) {
|
||||
//Create and cache page html and map of visions
|
||||
createHTML(true).html {
|
||||
visions.putAll(buildPage(visionFragment, title, headers))
|
||||
}
|
||||
@ -110,43 +166,17 @@ public class VisionServer internal constructor(
|
||||
application.routing {
|
||||
route(rootRoute) {
|
||||
route(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")
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
serveVisions(this, visions)
|
||||
//filled pages
|
||||
get {
|
||||
if (cachedHtml == null) {
|
||||
//re-create html and vision list on each call
|
||||
call.respondHtml {
|
||||
visions.clear()
|
||||
visions.putAll(buildPage(visionFragment, title, headers))
|
||||
}
|
||||
} else {
|
||||
//Use cached html
|
||||
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(
|
||||
route: String = DEFAULT_PAGE,
|
||||
title: String = "VisionForge server page '$route'",
|
||||
headers: List<HtmlFragment> = emptyList(),
|
||||
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
|
||||
*/
|
||||
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) {
|
||||
install(WebSockets)
|
||||
}
|
||||
@ -209,7 +264,7 @@ public fun VisionManager.serve(
|
||||
port: Int = 7777,
|
||||
block: VisionServer.() -> Unit,
|
||||
): ApplicationEngine = context.embeddedServer(CIO, port, host) {
|
||||
visionModule(context).apply(block)
|
||||
visionServer(context).apply(block)
|
||||
}.start()
|
||||
|
||||
public fun ApplicationEngine.show() {
|
||||
|
Loading…
Reference in New Issue
Block a user