From 70ac2c99dd88a43322ac8098ce45a34645268454 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 12 Dec 2020 18:27:36 +0300 Subject: [PATCH] Added file export --- .../kotlin/ru/mipt/npm/sat/SatDemoApp.kt | 4 +- .../kotlin/ru/mipt/npm/sat/satServer.kt | 20 ++- .../hep/dataforge/vision/VisionManager.kt | 7 +- .../vision/html/BindingOutputTagConsumer.kt | 28 ---- .../vision/html/HtmlVisionFragment.kt | 12 +- .../vision/html/StaticOutputTagConsumer.kt | 60 -------- ...putTagConsumer.kt => VisionTagConsumer.kt} | 17 ++- .../dataforge/vision/html/staticHtmlRender.kt | 42 ++++++ .../hep/dataforge/vision/layout/Page.kt | 2 +- .../hep/dataforge/vision/html/HtmlTagTest.kt | 17 ++- .../dataforge/vision/client/VisionClient.kt | 130 ++++++++-------- .../dataforge/vision/client/elementOutput.kt | 84 +++++------ .../hep/dataforge/vision/export/htmlExport.kt | 67 --------- .../kotlin/hep/dataforge/vision/headers.kt | 140 ++++++++++++++++++ .../kotlin/hep/dataforge/vision/htmlExport.kt | 37 +++++ .../dataforge/vision/server/VisionServer.kt | 40 ++--- 16 files changed, 388 insertions(+), 319 deletions(-) delete mode 100644 visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/BindingOutputTagConsumer.kt delete mode 100644 visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/StaticOutputTagConsumer.kt rename visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/{OutputTagConsumer.kt => VisionTagConsumer.kt} (84%) create mode 100644 visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/staticHtmlRender.kt delete mode 100644 visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/export/htmlExport.kt create mode 100644 visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/headers.kt create mode 100644 visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/htmlExport.kt diff --git a/demo/sat-demo/src/jsMain/kotlin/ru/mipt/npm/sat/SatDemoApp.kt b/demo/sat-demo/src/jsMain/kotlin/ru/mipt/npm/sat/SatDemoApp.kt index a3af18af..c30b2ef8 100644 --- a/demo/sat-demo/src/jsMain/kotlin/ru/mipt/npm/sat/SatDemoApp.kt +++ b/demo/sat-demo/src/jsMain/kotlin/ru/mipt/npm/sat/SatDemoApp.kt @@ -2,7 +2,7 @@ package ru.mipt.npm.sat import hep.dataforge.context.Global import hep.dataforge.vision.client.VisionClient -import hep.dataforge.vision.client.fetchAndRenderAllVisions +import hep.dataforge.vision.client.renderAllVisions import hep.dataforge.vision.solid.three.ThreePlugin import kotlinx.browser.window @@ -11,6 +11,6 @@ fun main() { Global.plugins.load(ThreePlugin) //Fetch from server and render visions for all outputs window.onload = { - Global.plugins.fetch(VisionClient).fetchAndRenderAllVisions() + Global.plugins.fetch(VisionClient).renderAllVisions() } } \ No newline at end of file diff --git a/demo/sat-demo/src/jvmMain/kotlin/ru/mipt/npm/sat/satServer.kt b/demo/sat-demo/src/jvmMain/kotlin/ru/mipt/npm/sat/satServer.kt index 2e86ff22..067a06f0 100644 --- a/demo/sat-demo/src/jvmMain/kotlin/ru/mipt/npm/sat/satServer.kt +++ b/demo/sat-demo/src/jvmMain/kotlin/ru/mipt/npm/sat/satServer.kt @@ -3,11 +3,11 @@ package ru.mipt.npm.sat import hep.dataforge.context.Global import hep.dataforge.names.toName -import hep.dataforge.vision.VisionManager import hep.dataforge.vision.server.* import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.SolidManager import hep.dataforge.vision.solid.color +import hep.dataforge.vision.visionManager import io.ktor.util.KtorExperimentalAPI import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -18,16 +18,21 @@ import kotlin.random.Random @OptIn(KtorExperimentalAPI::class) fun main() { - val sat = visionOfSatellite( - ySegments = 3, - ) + //Create a geometry + val sat = visionOfSatellite(ySegments = 3) val context = Global.context("SAT") { + //need to install solids extension, vision manager is installed automatically plugin(SolidManager) } - val server = context.plugins.fetch(VisionManager).serve { + // fetch vision manager + val visionManager = context.visionManager + + val server = visionManager.serve { + //use client library useScript("visionforge-solid.js") + //use css useCss("css/styles.css") page { div("flex-column") { @@ -36,11 +41,12 @@ fun main() { } } } + server.show() context.launch { while (isActive) { - val target = "layer[${Random.nextInt(1,11)}].segment[${Random.nextInt(3)},${Random.nextInt(3)}]".toName() + 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" @@ -49,7 +55,7 @@ fun main() { } println("Press Enter to close server") - while (readLine()!="exit"){ + while (readLine() != "exit") { // } diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt index c39eb3d9..eb34ed84 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/VisionManager.kt @@ -78,4 +78,9 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) { internal val visionSerializer: PolymorphicSerializer = PolymorphicSerializer(Vision::class) } -} \ No newline at end of file +} + +/** + * Fetch a [VisionManager] from this plugin + */ +public val Context.visionManager: VisionManager get() = plugins.fetch(VisionManager) \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/BindingOutputTagConsumer.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/BindingOutputTagConsumer.kt deleted file mode 100644 index 9998c442..00000000 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/BindingOutputTagConsumer.kt +++ /dev/null @@ -1,28 +0,0 @@ -package hep.dataforge.vision.html - -import hep.dataforge.meta.Meta -import hep.dataforge.names.Name -import hep.dataforge.vision.Vision -import kotlinx.html.FlowContent -import kotlinx.html.TagConsumer - -public class BindingOutputTagConsumer( - root: TagConsumer, - prefix: String? = null, -) : OutputTagConsumer(root, prefix) { - - private val _bindings = HashMap() - public val bindings: Map get() = _bindings - - override fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta) { - _bindings[name] = vision - } -} - -public fun TagConsumer.visionFragment(fragment: HtmlVisionFragment): Map { - return BindingOutputTagConsumer(this).apply(fragment.content).bindings -} - -public fun FlowContent.visionFragment(fragment: HtmlVisionFragment): Map { - return BindingOutputTagConsumer(consumer).apply(fragment.content).bindings -} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/HtmlVisionFragment.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/HtmlVisionFragment.kt index 6ae41f25..9f3999e2 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/HtmlVisionFragment.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/HtmlVisionFragment.kt @@ -1,20 +1,16 @@ package hep.dataforge.vision.html -import hep.dataforge.vision.Vision import kotlinx.html.FlowContent import kotlinx.html.TagConsumer -public class HtmlFragment(public val content: TagConsumer<*>.() -> Unit) +public typealias HtmlFragment = TagConsumer<*>.()->Unit public fun TagConsumer<*>.fragment(fragment: HtmlFragment) { - fragment.content(this) + fragment() } public fun FlowContent.fragment(fragment: HtmlFragment) { - fragment.content(consumer) + fragment(consumer) } -public class HtmlVisionFragment(public val content: OutputTagConsumer<*, V>.() -> Unit) - -public fun buildVisionFragment(block: OutputTagConsumer<*, Vision>.() -> Unit): HtmlVisionFragment = - HtmlVisionFragment(block) +public typealias HtmlVisionFragment = VisionTagConsumer<*>.() -> Unit \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/StaticOutputTagConsumer.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/StaticOutputTagConsumer.kt deleted file mode 100644 index 0e045918..00000000 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/StaticOutputTagConsumer.kt +++ /dev/null @@ -1,60 +0,0 @@ -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 = FlowContent.(V, Meta) -> Unit - -/** - * An [OutputTagConsumer] that directly renders given [Vision] using provided [renderer] - */ -public class StaticOutputTagConsumer( - root: TagConsumer, - prefix: String? = null, - private val renderer: HtmlVisionRenderer, -) : OutputTagConsumer(root, prefix) { - override fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta): Unit = renderer(vision, outputMeta) - - public companion object { - public fun embed(manager: VisionManager): HtmlVisionRenderer = { vision: Vision, _: Meta -> - script { - attributes["class"] = OUTPUT_DATA_CLASS - unsafe { - +manager.encodeToString(vision) - } - } - } - } -} - -public fun HtmlVisionFragment.renderToObject( - root: TagConsumer, - prefix: String? = null, - renderer: HtmlVisionRenderer, -): T = StaticOutputTagConsumer(root, prefix, renderer).apply(content).finalize() - -/** - * Render an object to HTML embedding the data as script bodies - */ -public fun HtmlVisionFragment.embedToObject( - manager: VisionManager, - root: TagConsumer, - prefix: String? = null, -): T = renderToObject(root, prefix, StaticOutputTagConsumer.embed(manager)) - -public fun HtmlVisionFragment.renderToString(renderer: HtmlVisionRenderer): String = - renderToObject(createHTML(), null, renderer) - -/** - * Convert a fragment to a string, embedding all visions data - */ -public fun HtmlVisionFragment.embedToString(manager: VisionManager): String = - embedToObject(manager, createHTML()) - diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/OutputTagConsumer.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/VisionTagConsumer.kt similarity index 84% rename from visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/OutputTagConsumer.kt rename to visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/VisionTagConsumer.kt index 86ea51f6..594011af 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/OutputTagConsumer.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/VisionTagConsumer.kt @@ -12,7 +12,7 @@ import kotlinx.html.* * A placeholder object to attach inline vision builders. */ @DFExperimental -public class VisionOutput { +public class VisionOutput @PublishedApi internal constructor(){ public var meta: Meta = Meta.EMPTY public inline fun meta(block: MetaBuilder.() -> Unit) { @@ -23,7 +23,7 @@ public class VisionOutput { /** * Modified [TagConsumer] that allows rendering output fragments and visions in them */ -public abstract class OutputTagConsumer( +public abstract class VisionTagConsumer( private val root: TagConsumer, private val idPrefix: String? = null, ) : TagConsumer by root { @@ -36,14 +36,14 @@ public abstract class OutputTagConsumer( * @param vision an object to be rendered * @param outputMeta optional configuration for the output container */ - protected abstract fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta) + protected abstract fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) /** * Create a placeholder for a vision output with optional [Vision] in it */ public fun TagConsumer.vision( name: Name, - vision: V? = null, + vision: Vision? = null, outputMeta: Meta = Meta.EMPTY, ): T = div { id = resolveId(name) @@ -64,7 +64,7 @@ public abstract class OutputTagConsumer( @OptIn(DFExperimental::class) public inline fun TagConsumer.vision( name: Name, - visionProvider: VisionOutput.() -> V, + visionProvider: VisionOutput.() -> Vision, ): T { val output = VisionOutput() val vision = output.visionProvider() @@ -74,11 +74,11 @@ public abstract class OutputTagConsumer( @OptIn(DFExperimental::class) public inline fun TagConsumer.vision( name: String, - visionProvider: VisionOutput.() -> V, + visionProvider: VisionOutput.() -> Vision, ): T = vision(name.toName(), visionProvider) public inline fun TagConsumer.vision( - vision: V, + vision: Vision, ): T = vision("vision[${vision.hashCode()}]".toName(), vision) /** @@ -97,6 +97,9 @@ public abstract class OutputTagConsumer( public const val OUTPUT_META_CLASS: String = "visionforge-output-meta" public const val OUTPUT_DATA_CLASS: String = "visionforge-output-data" + public const val OUTPUT_FETCH_VISION_ATTRIBUTE: String = "data-output-fetch-vision" + public const val OUTPUT_FETCH_UPDATE_ATTRIBUTE: String = "data-output-fetch-update" + 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 = "." diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/staticHtmlRender.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/staticHtmlRender.kt new file mode 100644 index 00000000..1ecbdb8a --- /dev/null +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/html/staticHtmlRender.kt @@ -0,0 +1,42 @@ +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.DIV +import kotlinx.html.FlowContent +import kotlinx.html.script +import kotlinx.html.unsafe + + +public fun FlowContent.embedVisionFragment( + manager: VisionManager, + idPrefix: String? = null, + fragment: HtmlVisionFragment, +) { + val consumer = object : VisionTagConsumer(consumer, idPrefix) { + override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) { + script { + attributes["class"] = OUTPUT_DATA_CLASS + unsafe { + +manager.encodeToString(vision) + } + } + } + } + fragment(consumer) +} + +public typealias HtmlVisionRenderer = FlowContent.(name: Name, vision: Vision, meta: Meta) -> Unit + +public fun FlowContent.renderVisionFragment( + renderer: DIV.(name: Name, vision: Vision, meta: Meta) -> Unit, + idPrefix: String? = null, + fragment: HtmlVisionFragment, +) { + val consumer = object : VisionTagConsumer(consumer, idPrefix) { + override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) = renderer(name, vision, outputMeta) + } + fragment(consumer) +} \ No newline at end of file diff --git a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/layout/Page.kt b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/layout/Page.kt index cd66a3d6..848e0dc7 100644 --- a/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/layout/Page.kt +++ b/visionforge-core/src/commonMain/kotlin/hep/dataforge/vision/layout/Page.kt @@ -4,7 +4,7 @@ import hep.dataforge.meta.Meta import hep.dataforge.names.Name import hep.dataforge.vision.Vision -public interface Output { +public fun interface Output { public fun render(vision: V) } diff --git a/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt b/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt index eac0a16a..0d40205a 100644 --- a/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt +++ b/visionforge-core/src/commonTest/kotlin/hep/dataforge/vision/html/HtmlTagTest.kt @@ -3,10 +3,9 @@ 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 import hep.dataforge.vision.VisionBase -import hep.dataforge.vision.VisionGroup import kotlinx.html.* +import kotlinx.html.stream.createHTML import kotlin.test.Test class HtmlTagTest { @@ -15,11 +14,11 @@ class HtmlTagTest { fun VisionOutput.base(block: VisionBase.() -> Unit) = VisionBase().apply(block) - val fragment = buildVisionFragment { + val fragment: HtmlVisionFragment = { div { h1 { +"Head" } vision("ddd") { - meta{ + meta { "metaProperty" put 87 } base { @@ -32,7 +31,7 @@ class HtmlTagTest { } } - val simpleVisionRenderer: HtmlVisionRenderer = { vision, _ -> + val simpleVisionRenderer: HtmlVisionRenderer = { _, vision, _ -> div { h2 { +"Properties" } ul { @@ -46,13 +45,17 @@ class HtmlTagTest { } } - val groupRenderer: HtmlVisionRenderer = { group, _ -> + val groupRenderer: HtmlVisionRenderer = { _, group, _ -> p { +"This is group" } } @Test fun testStringRender() { - println(fragment.renderToString(simpleVisionRenderer)) + println( + createHTML().div { + renderVisionFragment(simpleVisionRenderer, fragment = fragment) + } + ) } } \ No newline at end of file diff --git a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt index 5b51cacd..72cf61f7 100644 --- a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/VisionClient.kt @@ -6,9 +6,10 @@ import hep.dataforge.meta.MetaSerializer import hep.dataforge.vision.Vision import hep.dataforge.vision.VisionChange import hep.dataforge.vision.VisionManager -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 hep.dataforge.vision.html.VisionTagConsumer +import hep.dataforge.vision.html.VisionTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE +import hep.dataforge.vision.html.VisionTagConsumer.Companion.OUTPUT_FETCH_UPDATE_ATTRIBUTE +import hep.dataforge.vision.html.VisionTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE import kotlinx.browser.document import kotlinx.browser.window import org.w3c.dom.Element @@ -43,22 +44,23 @@ public class VisionClient : AbstractPlugin() { private fun Element.getEmbeddedData(className: String): String? = getElementsByClassName(className)[0]?.innerHTML + private fun Element.getFlag(attribute: String): Boolean = attributes[attribute]?.value == "true" + /** - * 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 [VisionTagConsumer.OUTPUT_CLASS] class. */ - public fun renderVisionAt(element: Element, requestUpdates: Boolean = true) { + public fun renderVisionAt(element: Element) { 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") + if (!element.classList.contains(VisionTagConsumer.OUTPUT_CLASS)) error("The element $element is not an output element") - val outputMeta = element.getEmbeddedData(OutputTagConsumer.OUTPUT_META_CLASS)?.let { + + val outputMeta = element.getEmbeddedData(VisionTagConsumer.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 { + val embeddedVision = element.getEmbeddedData(VisionTagConsumer.OUTPUT_DATA_CLASS)?.let { visionManager.decodeFromString(it) } if (embeddedVision != null) { @@ -66,54 +68,60 @@ public class VisionClient : AbstractPlugin() { renderer.render(element, embeddedVision, outputMeta) } - val fetchUrl = URL(endpoint).apply { - searchParams.append("name", name) - pathname += "/vision" - } + if(element.getFlag(VisionTagConsumer.OUTPUT_FETCH_VISION_ATTRIBUTE)) { - console.info("Fetching vision data from $fetchUrl") - window.fetch(fetchUrl).then { response -> - 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, outputMeta) - if (requestUpdates) { - val wsUrl = URL(endpoint).apply { - pathname += "/ws" - protocol = "ws" - searchParams.append("name", name) - } - WebSocket(wsUrl.toString()).apply { - onmessage = { messageEvent -> - val stringData: String? = messageEvent.data as? String - if (stringData != null) { - val dif = visionManager.jsonFormat.decodeFromString( - VisionChange.serializer(), - stringData - ) - vision.update(dif) - } else { - console.error("WebSocket message data is not a string") - } - } - onopen = { - console.info("WebSocket update channel established for output '$name'") - } - onclose = { - console.info("WebSocket update channel closed for output '$name'") - } - onerror = { - console.error("WebSocket update channel error for output '$name'") - } - } + val endpoint = resolveEndpoint(element) + console.info("Vision server is resolved to $endpoint") - } - } - } else { - console.error("Failed to fetch initial vision state from $endpoint") + val fetchUrl = URL(endpoint).apply { + searchParams.append("name", name) + pathname += "/vision" } + console.info("Fetching vision data from $fetchUrl") + window.fetch(fetchUrl).then { response -> + 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, outputMeta) + if (element.getFlag(OUTPUT_FETCH_UPDATE_ATTRIBUTE)) { + val wsUrl = URL(endpoint).apply { + pathname += "/ws" + protocol = "ws" + searchParams.append("name", name) + } + WebSocket(wsUrl.toString()).apply { + onmessage = { messageEvent -> + val stringData: String? = messageEvent.data as? String + if (stringData != null) { + val dif = visionManager.jsonFormat.decodeFromString( + VisionChange.serializer(), + stringData + ) + vision.update(dif) + } else { + console.error("WebSocket message data is not a string") + } + } + onopen = { + console.info("WebSocket update channel established for output '$name'") + } + onclose = { + console.info("WebSocket update channel closed for output '$name'") + } + onerror = { + console.error("WebSocket update channel error for output '$name'") + } + } + + } + } + } else { + console.error("Failed to fetch initial vision state from $endpoint") + } + + } } } @@ -128,20 +136,20 @@ public class VisionClient : AbstractPlugin() { } /** - * Fetch and render visions for all elements with [OutputTagConsumer.OUTPUT_CLASS] class inside given [element]. + * Fetch and render visions for all elements with [VisionTagConsumer.OUTPUT_CLASS] class inside given [element]. */ -public fun VisionClient.fetchVisionsInChildren(element: Element, requestUpdates: Boolean = true) { - val elements = element.getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS) +public fun VisionClient.renderAllVisionsAt(element: Element) { + val elements = element.getElementsByClassName(VisionTagConsumer.OUTPUT_CLASS) console.info("Finished search for outputs. Found ${elements.length} items") elements.asList().forEach { child -> - renderVisionAt(child, requestUpdates) + renderVisionAt(child) } } /** - * Fetch visions from the server for all elements with [OutputTagConsumer.OUTPUT_CLASS] class in the document body + * Fetch visions from the server for all elements with [VisionTagConsumer.OUTPUT_CLASS] class in the document body */ -public fun VisionClient.fetchAndRenderAllVisions(requestUpdates: Boolean = true) { +public fun VisionClient.renderAllVisions() { val element = document.body ?: error("Document does not have a body") - fetchVisionsInChildren(element, requestUpdates) + renderAllVisionsAt(element) } \ No newline at end of file diff --git a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/elementOutput.kt b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/elementOutput.kt index 745754dc..72a80143 100644 --- a/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/elementOutput.kt +++ b/visionforge-core/src/jsMain/kotlin/hep/dataforge/vision/client/elementOutput.kt @@ -1,17 +1,9 @@ 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.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.* +import org.w3c.dom.Element @Type(ElementVisionRenderer.TYPE) public interface ElementVisionRenderer { @@ -34,44 +26,36 @@ public interface ElementVisionRenderer { public const val DEFAULT_RATING: Int = 10 } } - -@DFExperimental -public fun Map.bind(rendererFactory: (Vision) -> ElementVisionRenderer) { - forEach { (id, vision) -> - val element = document.getElementById(id) ?: error("Could not find element with id $id") - rendererFactory(vision).render(element, vision) - } -} - -@DFExperimental -public fun Element.renderAllVisions(visionProvider: (Name) -> Vision, rendererFactory: (Vision) -> ElementVisionRenderer) { - val elements = getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS) - elements.asList().forEach { element -> - val name = element.attributes[OutputTagConsumer.OUTPUT_NAME_ATTRIBUTE]?.value - if (name == null) { - console.error("Attribute ${OutputTagConsumer.OUTPUT_NAME_ATTRIBUTE} not defined in the output element") - return@forEach - } - val vision = visionProvider(name.toName()) - rendererFactory(vision).render(element, vision) - } -} - -@DFExperimental -public fun Document.renderAllVisions(visionProvider: (Name) -> Vision, rendererFactory: (Vision) -> ElementVisionRenderer): Unit { - documentElement?.renderAllVisions(visionProvider,rendererFactory) -} - -@DFExperimental -public fun HtmlVisionFragment.renderInDocument( - root: TagConsumer, - renderer: ElementVisionRenderer, -): HTMLElement = BindingOutputTagConsumer(root).apply(content).let { scope -> - scope.finalize().apply { - scope.bindings.forEach { (name, vision) -> - val id = scope.resolveId(name) - val element = document.getElementById(id) ?: error("Could not find element with name $name and id $id") - renderer.render(element, vision) - } - } -} +// +//@DFExperimental +//public fun Map.bind(rendererFactory: (Vision) -> ElementVisionRenderer) { +// forEach { (id, vision) -> +// val element = document.getElementById(id) ?: error("Could not find element with id $id") +// rendererFactory(vision).render(element, vision) +// } +//} +// +//@DFExperimental +//public fun Element.renderAllVisions( +// visionProvider: (Name) -> Vision, +// rendererFactory: (Vision) -> ElementVisionRenderer, +//) { +// val elements = getElementsByClassName(VisionTagConsumer.OUTPUT_CLASS) +// elements.asList().forEach { element -> +// val name = element.attributes[VisionTagConsumer.OUTPUT_NAME_ATTRIBUTE]?.value +// if (name == null) { +// console.error("Attribute ${VisionTagConsumer.OUTPUT_NAME_ATTRIBUTE} not defined in the output element") +// return@forEach +// } +// val vision = visionProvider(name.toName()) +// rendererFactory(vision).render(element, vision) +// } +//} +// +//@DFExperimental +//public fun Document.renderAllVisions( +// visionProvider: (Name) -> Vision, +// rendererFactory: (Vision) -> ElementVisionRenderer, +//): Unit { +// documentElement?.renderAllVisions(visionProvider, rendererFactory) +//} diff --git a/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/export/htmlExport.kt b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/export/htmlExport.kt deleted file mode 100644 index c7e556ea..00000000 --- a/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/export/htmlExport.kt +++ /dev/null @@ -1,67 +0,0 @@ -//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, -// 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) \ No newline at end of file diff --git a/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/headers.kt b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/headers.kt new file mode 100644 index 00000000..a7da3719 --- /dev/null +++ b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/headers.kt @@ -0,0 +1,140 @@ +package hep.dataforge.vision + +import hep.dataforge.vision.html.HtmlFragment +import kotlinx.html.link +import kotlinx.html.script +import kotlinx.html.unsafe +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption + +/** + * The location of resources for plot. + */ +public enum class ResourceLocation { +// /** +// * Use cdn or other remote source for assets +// */ +// REMOTE, + + /** + * Store assets in a sibling folder `.dataforge/assets` or in a system-wide folder if this is a default temporary file + */ + LOCAL, + + /** + * Store assets in a system-window `~/.dataforge/assets` folder + */ + SYSTEM, + + /** + * Embed the asset into the html. Could produce very large files. + */ + EMBED +} + + +/** + * Check if the asset exists in given local location and put it there if it does not + * @param + */ +internal fun checkOrStoreFile(basePath: Path, filePath: Path, resource: String): Path { + val fullPath = basePath.resolveSibling(filePath).toAbsolutePath() + + if (Files.exists(fullPath)) { + //TODO checksum + } else { + //TODO add logging + + val bytes = VisionManager::class.java.getResourceAsStream(resource).readAllBytes() + Files.createDirectories(fullPath.parent) + Files.write(fullPath, bytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) + } + + return if (basePath.isAbsolute && fullPath.startsWith(basePath)) { + basePath.relativize(fullPath) + } else { + filePath + } +} + +/** + * A header that automatically copies relevant scripts to given path + */ +internal fun fileScriptHeader( + basePath: Path, + scriptPath: Path, + resource: String +): HtmlFragment = { + val relativePath = checkOrStoreFile(basePath, scriptPath, resource) + script { + type = "text/javascript" + src = relativePath.toString() + } +} + +internal fun embedScriptHeader(resource: String): HtmlFragment = { + script { + type = "text/javascript" + unsafe { + val bytes = VisionManager::class.java.getResourceAsStream(resource).readAllBytes() + +bytes.toString(Charsets.UTF_8) + } + } +} + +internal fun fileCssHeader( + basePath: Path, + cssPath: Path, + resource: String +): HtmlFragment = { + val relativePath = checkOrStoreFile(basePath, cssPath, resource) + link { + rel = "stylesheet" + href = relativePath.toString() + } +} + +// +///** +// * A system-wide plotly store location +// */ +//val systemHeader = HtmlFragment { +// val relativePath = checkOrStoreFile( +// Path.of("."), +// Path.of(System.getProperty("user.home")).resolve(".plotly/$assetsDirectory$PLOTLY_SCRIPT_PATH"), +// PLOTLY_SCRIPT_PATH +// ) +// script { +// type = "text/javascript" +// src = relativePath.toString() +// } +//} +// +// +///** +// * embedded plotly script +// */ +//val embededHeader = HtmlFragment { +// script { +// unsafe { +// val bytes = HtmlFragment::class.java.getResourceAsStream(PLOTLY_SCRIPT_PATH).readAllBytes() +// +bytes.toString(Charsets.UTF_8) +// } +// } +//} + + +//internal fun inferPlotlyHeader( +// target: Path?, +// resourceLocation: ResourceLocation +//): HtmlFragment = when (resourceLocation) { +// ResourceLocation.REMOTE -> cdnPlotlyHeader +// ResourceLocation.LOCAL -> if (target != null) { +// localHeader(target) +// } else { +// systemPlotlyHeader +// } +// ResourceLocation.SYSTEM -> systemPlotlyHeader +// ResourceLocation.EMBED -> embededPlotlyHeader +//} diff --git a/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/htmlExport.kt b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/htmlExport.kt new file mode 100644 index 00000000..328425ee --- /dev/null +++ b/visionforge-core/src/jvmMain/kotlin/hep/dataforge/vision/htmlExport.kt @@ -0,0 +1,37 @@ +package hep.dataforge.vision + +import hep.dataforge.vision.html.* +import kotlinx.html.* +import kotlinx.html.stream.createHTML +import java.awt.Desktop +import java.nio.file.Files +import java.nio.file.Path + + + +/** + * Make a file with the embedded vision data + */ +public fun HtmlVisionFragment.makeFile(manager: VisionManager, vararg headers: HtmlFragment, path: Path? = null, show: Boolean = true) { + val actualFile = path ?: Files.createTempFile("tempPlot", ".html") + Files.createDirectories(actualFile.parent) + val htmlString = createHTML().apply { + head { + meta { + charset = "utf-8" + headers.forEach { + fragment(it) + } + } + title(title) + } + body { + embedVisionFragment(manager, fragment = this@makeFile) + } + }.finalize() + + Files.writeString(actualFile, htmlString) + if (show) { + Desktop.getDesktop().browse(actualFile.toFile().toURI()) + } +} \ No newline at end of file diff --git a/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt b/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt index 477f48fa..d67ec24d 100644 --- a/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt +++ b/visionforge-server/src/main/kotlin/hep/dataforge/vision/server/VisionServer.kt @@ -8,7 +8,10 @@ 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.* +import hep.dataforge.vision.html.HtmlFragment +import hep.dataforge.vision.html.HtmlVisionFragment +import hep.dataforge.vision.html.VisionTagConsumer +import hep.dataforge.vision.html.fragment import hep.dataforge.vision.server.VisionServer.Companion.DEFAULT_PAGE import io.ktor.application.* import io.ktor.features.CORS @@ -56,15 +59,25 @@ public class VisionServer internal constructor( private val globalHeaders: ArrayList = ArrayList() public fun header(block: TagConsumer<*>.() -> Unit) { - globalHeaders.add(HtmlFragment(block)) + globalHeaders.add(block) } private fun HTML.buildPage( - visionFragment: HtmlVisionFragment, + visionFragment: HtmlVisionFragment, title: String, headers: List, ): Map { - lateinit var visionMap: Map + val visionMap = HashMap() + + val consumer = object : VisionTagConsumer(consumer) { + override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) { + visionMap[name] = vision + + // Toggle updates + attributes[OUTPUT_FETCH_VISION_ATTRIBUTE] = "true" + attributes[OUTPUT_FETCH_UPDATE_ATTRIBUTE] = "true" + } + } head { meta { @@ -77,7 +90,7 @@ public class VisionServer internal constructor( } body { //Load the fragment and remember all loaded visions - visionMap = visionFragment(visionFragment) + visionFragment(consumer) } return visionMap @@ -146,11 +159,11 @@ public class VisionServer internal constructor( * Serve a page, potentially containing any number of visions at a given [route] with given [headers]. * */ - public fun servePage( - visionFragment: HtmlVisionFragment, + public fun page( route: String = DEFAULT_PAGE, title: String = "VisionForge server page '$route'", headers: List = emptyList(), + visionFragment: HtmlVisionFragment, ) { val visions = HashMap() @@ -185,19 +198,6 @@ 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 = emptyList(), - content: OutputTagConsumer<*, Vision>.() -> Unit, - ) { - servePage(buildVisionFragment(content), route, title, headers) - } - - public companion object { public const val DEFAULT_PAGE: String = "/" public val UPDATE_INTERVAL_KEY: Name = "update.interval".toName()