forked from kscience/visionforge
Added file export
This commit is contained in:
parent
734d1e1168
commit
70ac2c99dd
@ -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()
|
||||
}
|
||||
}
|
@ -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") {
|
||||
//
|
||||
}
|
||||
|
||||
|
@ -79,3 +79,8 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
|
||||
internal val visionSerializer: PolymorphicSerializer<Vision> = PolymorphicSerializer(Vision::class)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a [VisionManager] from this plugin
|
||||
*/
|
||||
public val Context.visionManager: VisionManager get() = plugins.fetch(VisionManager)
|
@ -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<T, V : Vision>(
|
||||
root: TagConsumer<T>,
|
||||
prefix: String? = null,
|
||||
) : OutputTagConsumer<T, V>(root, prefix) {
|
||||
|
||||
private val _bindings = HashMap<Name, V>()
|
||||
public val bindings: Map<Name, V> get() = _bindings
|
||||
|
||||
override fun FlowContent.renderVision(name: Name, vision: V, outputMeta: Meta) {
|
||||
_bindings[name] = vision
|
||||
}
|
||||
}
|
||||
|
||||
public fun <T : Any> TagConsumer<T>.visionFragment(fragment: HtmlVisionFragment<Vision>): Map<Name, Vision> {
|
||||
return BindingOutputTagConsumer<T, Vision>(this).apply(fragment.content).bindings
|
||||
}
|
||||
|
||||
public fun FlowContent.visionFragment(fragment: HtmlVisionFragment<Vision>): Map<Name, Vision> {
|
||||
return BindingOutputTagConsumer<Any?, Vision>(consumer).apply(fragment.content).bindings
|
||||
}
|
@ -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<V : Vision>(public val content: OutputTagConsumer<*, V>.() -> Unit)
|
||||
|
||||
public fun buildVisionFragment(block: OutputTagConsumer<*, Vision>.() -> Unit): HtmlVisionFragment<Vision> =
|
||||
HtmlVisionFragment(block)
|
||||
public typealias HtmlVisionFragment = VisionTagConsumer<*>.() -> Unit
|
@ -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<V> = FlowContent.(V, Meta) -> Unit
|
||||
|
||||
/**
|
||||
* An [OutputTagConsumer] that directly renders given [Vision] using provided [renderer]
|
||||
*/
|
||||
public class StaticOutputTagConsumer<R, V : Vision>(
|
||||
root: TagConsumer<R>,
|
||||
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)
|
||||
|
||||
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(
|
||||
root: TagConsumer<T>,
|
||||
prefix: String? = null,
|
||||
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())
|
||||
|
@ -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<R, V : Vision>(
|
||||
public abstract class VisionTagConsumer<R>(
|
||||
private val root: TagConsumer<R>,
|
||||
private val idPrefix: String? = null,
|
||||
) : TagConsumer<R> by root {
|
||||
@ -36,14 +36,14 @@ public abstract class OutputTagConsumer<R, V : Vision>(
|
||||
* @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 <T> TagConsumer<T>.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<R, V : Vision>(
|
||||
@OptIn(DFExperimental::class)
|
||||
public inline fun <T> TagConsumer<T>.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<R, V : Vision>(
|
||||
@OptIn(DFExperimental::class)
|
||||
public inline fun <T> TagConsumer<T>.vision(
|
||||
name: String,
|
||||
visionProvider: VisionOutput.() -> V,
|
||||
visionProvider: VisionOutput.() -> Vision,
|
||||
): T = vision(name.toName(), visionProvider)
|
||||
|
||||
public inline fun <T> TagConsumer<T>.vision(
|
||||
vision: V,
|
||||
vision: Vision,
|
||||
): T = vision("vision[${vision.hashCode()}]".toName(), vision)
|
||||
|
||||
/**
|
||||
@ -97,6 +97,9 @@ public abstract class OutputTagConsumer<R, V : Vision>(
|
||||
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 = "."
|
@ -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<Any?>(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 <R> FlowContent.renderVisionFragment(
|
||||
renderer: DIV.(name: Name, vision: Vision, meta: Meta) -> Unit,
|
||||
idPrefix: String? = null,
|
||||
fragment: HtmlVisionFragment,
|
||||
) {
|
||||
val consumer = object : VisionTagConsumer<Any?>(consumer, idPrefix) {
|
||||
override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) = renderer(name, vision, outputMeta)
|
||||
}
|
||||
fragment(consumer)
|
||||
}
|
@ -4,7 +4,7 @@ import hep.dataforge.meta.Meta
|
||||
import hep.dataforge.names.Name
|
||||
import hep.dataforge.vision.Vision
|
||||
|
||||
public interface Output<in V : Vision> {
|
||||
public fun interface Output<in V : Vision> {
|
||||
public fun render(vision: V)
|
||||
}
|
||||
|
||||
|
@ -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> = { vision, _ ->
|
||||
val simpleVisionRenderer: HtmlVisionRenderer = { _, vision, _ ->
|
||||
div {
|
||||
h2 { +"Properties" }
|
||||
ul {
|
||||
@ -46,13 +45,17 @@ class HtmlTagTest {
|
||||
}
|
||||
}
|
||||
|
||||
val groupRenderer: HtmlVisionRenderer<VisionGroup> = { group, _ ->
|
||||
val groupRenderer: HtmlVisionRenderer = { _, group, _ ->
|
||||
p { +"This is group" }
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testStringRender() {
|
||||
println(fragment.renderToString(simpleVisionRenderer))
|
||||
println(
|
||||
createHTML().div {
|
||||
renderVisionFragment<String>(simpleVisionRenderer, fragment = fragment)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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<String, Vision>.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<Vision>.renderInDocument(
|
||||
root: TagConsumer<HTMLElement>,
|
||||
renderer: ElementVisionRenderer,
|
||||
): HTMLElement = BindingOutputTagConsumer<HTMLElement, Vision>(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<String, Vision>.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)
|
||||
//}
|
||||
|
@ -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<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)
|
@ -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
|
||||
//}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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<HtmlFragment> = ArrayList()
|
||||
|
||||
public fun header(block: TagConsumer<*>.() -> Unit) {
|
||||
globalHeaders.add(HtmlFragment(block))
|
||||
globalHeaders.add(block)
|
||||
}
|
||||
|
||||
private fun HTML.buildPage(
|
||||
visionFragment: HtmlVisionFragment<Vision>,
|
||||
visionFragment: HtmlVisionFragment,
|
||||
title: String,
|
||||
headers: List<HtmlFragment>,
|
||||
): Map<Name, Vision> {
|
||||
lateinit var visionMap: Map<Name, Vision>
|
||||
val visionMap = HashMap<Name, Vision>()
|
||||
|
||||
val consumer = object : VisionTagConsumer<Any?>(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<Vision>,
|
||||
public fun page(
|
||||
route: String = DEFAULT_PAGE,
|
||||
title: String = "VisionForge server page '$route'",
|
||||
headers: List<HtmlFragment> = emptyList(),
|
||||
visionFragment: HtmlVisionFragment,
|
||||
) {
|
||||
val visions = HashMap<Name, Vision>()
|
||||
|
||||
@ -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<HtmlFragment> = 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()
|
||||
|
Loading…
Reference in New Issue
Block a user