v0.2.0-dev-22 #47

Merged
altavir merged 158 commits from dev into master 2021-07-17 11:04:22 +03:00
16 changed files with 388 additions and 319 deletions
Showing only changes of commit 70ac2c99dd - Show all commits

View File

@ -2,7 +2,7 @@ package ru.mipt.npm.sat
import hep.dataforge.context.Global import hep.dataforge.context.Global
import hep.dataforge.vision.client.VisionClient 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 hep.dataforge.vision.solid.three.ThreePlugin
import kotlinx.browser.window import kotlinx.browser.window
@ -11,6 +11,6 @@ fun main() {
Global.plugins.load(ThreePlugin) Global.plugins.load(ThreePlugin)
//Fetch from server and render visions for all outputs //Fetch from server and render visions for all outputs
window.onload = { window.onload = {
Global.plugins.fetch(VisionClient).fetchAndRenderAllVisions() Global.plugins.fetch(VisionClient).renderAllVisions()
} }
} }

View File

@ -3,11 +3,11 @@ package ru.mipt.npm.sat
import hep.dataforge.context.Global import hep.dataforge.context.Global
import hep.dataforge.names.toName import hep.dataforge.names.toName
import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.server.* import hep.dataforge.vision.server.*
import hep.dataforge.vision.solid.Solid import hep.dataforge.vision.solid.Solid
import hep.dataforge.vision.solid.SolidManager import hep.dataforge.vision.solid.SolidManager
import hep.dataforge.vision.solid.color import hep.dataforge.vision.solid.color
import hep.dataforge.vision.visionManager
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@ -18,16 +18,21 @@ import kotlin.random.Random
@OptIn(KtorExperimentalAPI::class) @OptIn(KtorExperimentalAPI::class)
fun main() { fun main() {
val sat = visionOfSatellite( //Create a geometry
ySegments = 3, val sat = visionOfSatellite(ySegments = 3)
)
val context = Global.context("SAT") { val context = Global.context("SAT") {
//need to install solids extension, vision manager is installed automatically
plugin(SolidManager) 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") useScript("visionforge-solid.js")
//use css
useCss("css/styles.css") useCss("css/styles.css")
page { page {
div("flex-column") { div("flex-column") {
@ -36,11 +41,12 @@ fun main() {
} }
} }
} }
server.show() server.show()
context.launch { context.launch {
while (isActive) { 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") (sat[target] as? Solid)?.color("red")
delay(300) delay(300)
(sat[target] as? Solid)?.color = "green" (sat[target] as? Solid)?.color = "green"
@ -49,7 +55,7 @@ fun main() {
} }
println("Press Enter to close server") println("Press Enter to close server")
while (readLine()!="exit"){ while (readLine() != "exit") {
// //
} }

View File

@ -78,4 +78,9 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
internal val visionSerializer: PolymorphicSerializer<Vision> = PolymorphicSerializer(Vision::class) internal val visionSerializer: PolymorphicSerializer<Vision> = PolymorphicSerializer(Vision::class)
} }
} }
/**
* Fetch a [VisionManager] from this plugin
*/
public val Context.visionManager: VisionManager get() = plugins.fetch(VisionManager)

View File

@ -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
}

View File

@ -1,20 +1,16 @@
package hep.dataforge.vision.html package hep.dataforge.vision.html
import hep.dataforge.vision.Vision
import kotlinx.html.FlowContent import kotlinx.html.FlowContent
import kotlinx.html.TagConsumer import kotlinx.html.TagConsumer
public class HtmlFragment(public val content: TagConsumer<*>.() -> Unit) public typealias HtmlFragment = TagConsumer<*>.()->Unit
public fun TagConsumer<*>.fragment(fragment: HtmlFragment) { public fun TagConsumer<*>.fragment(fragment: HtmlFragment) {
fragment.content(this) fragment()
} }
public fun FlowContent.fragment(fragment: HtmlFragment) { public fun FlowContent.fragment(fragment: HtmlFragment) {
fragment.content(consumer) fragment(consumer)
} }
public class HtmlVisionFragment<V : Vision>(public val content: OutputTagConsumer<*, V>.() -> Unit) public typealias HtmlVisionFragment = VisionTagConsumer<*>.() -> Unit
public fun buildVisionFragment(block: OutputTagConsumer<*, Vision>.() -> Unit): HtmlVisionFragment<Vision> =
HtmlVisionFragment(block)

View File

@ -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())

View File

@ -12,7 +12,7 @@ import kotlinx.html.*
* A placeholder object to attach inline vision builders. * A placeholder object to attach inline vision builders.
*/ */
@DFExperimental @DFExperimental
public class VisionOutput { public class VisionOutput @PublishedApi internal constructor(){
public var meta: Meta = Meta.EMPTY public var meta: Meta = Meta.EMPTY
public inline fun meta(block: MetaBuilder.() -> Unit) { 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 * 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 root: TagConsumer<R>,
private val idPrefix: String? = null, private val idPrefix: String? = null,
) : TagConsumer<R> by root { ) : TagConsumer<R> by root {
@ -36,14 +36,14 @@ public abstract class OutputTagConsumer<R, V : Vision>(
* @param vision an object to be rendered * @param vision an object to be rendered
* @param outputMeta optional configuration for the output container * @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 * Create a placeholder for a vision output with optional [Vision] in it
*/ */
public fun <T> TagConsumer<T>.vision( public fun <T> TagConsumer<T>.vision(
name: Name, name: Name,
vision: V? = null, vision: Vision? = null,
outputMeta: Meta = Meta.EMPTY, outputMeta: Meta = Meta.EMPTY,
): T = div { ): T = div {
id = resolveId(name) id = resolveId(name)
@ -64,7 +64,7 @@ public abstract class OutputTagConsumer<R, V : Vision>(
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
public inline fun <T> TagConsumer<T>.vision( public inline fun <T> TagConsumer<T>.vision(
name: Name, name: Name,
visionProvider: VisionOutput.() -> V, visionProvider: VisionOutput.() -> Vision,
): T { ): T {
val output = VisionOutput() val output = VisionOutput()
val vision = output.visionProvider() val vision = output.visionProvider()
@ -74,11 +74,11 @@ public abstract class OutputTagConsumer<R, V : Vision>(
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
public inline fun <T> TagConsumer<T>.vision( public inline fun <T> TagConsumer<T>.vision(
name: String, name: String,
visionProvider: VisionOutput.() -> V, visionProvider: VisionOutput.() -> Vision,
): T = vision(name.toName(), visionProvider) ): T = vision(name.toName(), visionProvider)
public inline fun <T> TagConsumer<T>.vision( public inline fun <T> TagConsumer<T>.vision(
vision: V, vision: Vision,
): T = vision("vision[${vision.hashCode()}]".toName(), 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_META_CLASS: String = "visionforge-output-meta"
public const val OUTPUT_DATA_CLASS: String = "visionforge-output-data" 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_NAME_ATTRIBUTE: String = "data-output-name"
public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint" public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
public const val DEFAULT_ENDPOINT: String = "." public const val DEFAULT_ENDPOINT: String = "."

View File

@ -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)
}

View File

@ -4,7 +4,7 @@ import hep.dataforge.meta.Meta
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
public interface Output<in V : Vision> { public fun interface Output<in V : Vision> {
public fun render(vision: V) public fun render(vision: V)
} }

View File

@ -3,10 +3,9 @@ package hep.dataforge.vision.html
import hep.dataforge.meta.DFExperimental import hep.dataforge.meta.DFExperimental
import hep.dataforge.meta.configure import hep.dataforge.meta.configure
import hep.dataforge.meta.set import hep.dataforge.meta.set
import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionBase import hep.dataforge.vision.VisionBase
import hep.dataforge.vision.VisionGroup
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.stream.createHTML
import kotlin.test.Test import kotlin.test.Test
class HtmlTagTest { class HtmlTagTest {
@ -15,11 +14,11 @@ class HtmlTagTest {
fun VisionOutput.base(block: VisionBase.() -> Unit) = fun VisionOutput.base(block: VisionBase.() -> Unit) =
VisionBase().apply(block) VisionBase().apply(block)
val fragment = buildVisionFragment { val fragment: HtmlVisionFragment = {
div { div {
h1 { +"Head" } h1 { +"Head" }
vision("ddd") { vision("ddd") {
meta{ meta {
"metaProperty" put 87 "metaProperty" put 87
} }
base { base {
@ -32,7 +31,7 @@ class HtmlTagTest {
} }
} }
val simpleVisionRenderer: HtmlVisionRenderer<Vision> = { vision, _ -> val simpleVisionRenderer: HtmlVisionRenderer = { _, vision, _ ->
div { div {
h2 { +"Properties" } h2 { +"Properties" }
ul { ul {
@ -46,13 +45,17 @@ class HtmlTagTest {
} }
} }
val groupRenderer: HtmlVisionRenderer<VisionGroup> = { group, _ -> val groupRenderer: HtmlVisionRenderer = { _, group, _ ->
p { +"This is group" } p { +"This is group" }
} }
@Test @Test
fun testStringRender() { fun testStringRender() {
println(fragment.renderToString(simpleVisionRenderer)) println(
createHTML().div {
renderVisionFragment<String>(simpleVisionRenderer, fragment = fragment)
}
)
} }
} }

View File

@ -6,9 +6,10 @@ import hep.dataforge.meta.MetaSerializer
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionChange import hep.dataforge.vision.VisionChange
import hep.dataforge.vision.VisionManager import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.html.OutputTagConsumer import hep.dataforge.vision.html.VisionTagConsumer
import hep.dataforge.vision.html.OutputTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE import hep.dataforge.vision.html.VisionTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
import hep.dataforge.vision.html.OutputTagConsumer.Companion.OUTPUT_NAME_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.document
import kotlinx.browser.window import kotlinx.browser.window
import org.w3c.dom.Element 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.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") val name = resolveName(element) ?: error("The element is not a vision output")
console.info("Found DF output with name $name") console.info("Found DF output with name $name")
if (!element.classList.contains(OutputTagConsumer.OUTPUT_CLASS)) error("The element $element is not an output element") if (!element.classList.contains(VisionTagConsumer.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 {
val outputMeta = element.getEmbeddedData(VisionTagConsumer.OUTPUT_META_CLASS)?.let {
VisionManager.defaultJson.decodeFromString(MetaSerializer, it) VisionManager.defaultJson.decodeFromString(MetaSerializer, it)
} ?: Meta.EMPTY } ?: Meta.EMPTY
//Trying to render embedded vision //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) visionManager.decodeFromString(it)
} }
if (embeddedVision != null) { if (embeddedVision != null) {
@ -66,54 +68,60 @@ public class VisionClient : AbstractPlugin() {
renderer.render(element, embeddedVision, outputMeta) renderer.render(element, embeddedVision, outputMeta)
} }
val fetchUrl = URL(endpoint).apply { if(element.getFlag(VisionTagConsumer.OUTPUT_FETCH_VISION_ATTRIBUTE)) {
searchParams.append("name", name)
pathname += "/vision"
}
console.info("Fetching vision data from $fetchUrl") val endpoint = resolveEndpoint(element)
window.fetch(fetchUrl).then { response -> console.info("Vision server is resolved to $endpoint")
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 fetchUrl = URL(endpoint).apply {
} searchParams.append("name", name)
} else { pathname += "/vision"
console.error("Failed to fetch initial vision state from $endpoint")
} }
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) { public fun VisionClient.renderAllVisionsAt(element: Element) {
val elements = element.getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS) val elements = element.getElementsByClassName(VisionTagConsumer.OUTPUT_CLASS)
console.info("Finished search for outputs. Found ${elements.length} items") console.info("Finished search for outputs. Found ${elements.length} items")
elements.asList().forEach { child -> elements.asList().forEach { child ->
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") val element = document.body ?: error("Document does not have a body")
fetchVisionsInChildren(element, requestUpdates) renderAllVisionsAt(element)
} }

View File

@ -1,17 +1,9 @@
package hep.dataforge.vision.client package hep.dataforge.vision.client
import hep.dataforge.meta.DFExperimental
import hep.dataforge.meta.Meta import hep.dataforge.meta.Meta
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.provider.Type import hep.dataforge.provider.Type
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import hep.dataforge.vision.html.BindingOutputTagConsumer import org.w3c.dom.Element
import hep.dataforge.vision.html.HtmlVisionFragment
import hep.dataforge.vision.html.OutputTagConsumer
import kotlinx.browser.document
import kotlinx.html.TagConsumer
import org.w3c.dom.*
@Type(ElementVisionRenderer.TYPE) @Type(ElementVisionRenderer.TYPE)
public interface ElementVisionRenderer { public interface ElementVisionRenderer {
@ -34,44 +26,36 @@ public interface ElementVisionRenderer {
public const val DEFAULT_RATING: Int = 10 public const val DEFAULT_RATING: Int = 10
} }
} }
//
@DFExperimental //@DFExperimental
public fun Map<String, Vision>.bind(rendererFactory: (Vision) -> ElementVisionRenderer) { //public fun Map<String, Vision>.bind(rendererFactory: (Vision) -> ElementVisionRenderer) {
forEach { (id, vision) -> // forEach { (id, vision) ->
val element = document.getElementById(id) ?: error("Could not find element with id $id") // val element = document.getElementById(id) ?: error("Could not find element with id $id")
rendererFactory(vision).render(element, vision) // rendererFactory(vision).render(element, vision)
} // }
} //}
//
@DFExperimental //@DFExperimental
public fun Element.renderAllVisions(visionProvider: (Name) -> Vision, rendererFactory: (Vision) -> ElementVisionRenderer) { //public fun Element.renderAllVisions(
val elements = getElementsByClassName(OutputTagConsumer.OUTPUT_CLASS) // visionProvider: (Name) -> Vision,
elements.asList().forEach { element -> // rendererFactory: (Vision) -> ElementVisionRenderer,
val name = element.attributes[OutputTagConsumer.OUTPUT_NAME_ATTRIBUTE]?.value //) {
if (name == null) { // val elements = getElementsByClassName(VisionTagConsumer.OUTPUT_CLASS)
console.error("Attribute ${OutputTagConsumer.OUTPUT_NAME_ATTRIBUTE} not defined in the output element") // elements.asList().forEach { element ->
return@forEach // val name = element.attributes[VisionTagConsumer.OUTPUT_NAME_ATTRIBUTE]?.value
} // if (name == null) {
val vision = visionProvider(name.toName()) // console.error("Attribute ${VisionTagConsumer.OUTPUT_NAME_ATTRIBUTE} not defined in the output element")
rendererFactory(vision).render(element, vision) // 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 Document.renderAllVisions(
@DFExperimental // visionProvider: (Name) -> Vision,
public fun HtmlVisionFragment<Vision>.renderInDocument( // rendererFactory: (Vision) -> ElementVisionRenderer,
root: TagConsumer<HTMLElement>, //): Unit {
renderer: ElementVisionRenderer, // documentElement?.renderAllVisions(visionProvider, rendererFactory)
): 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)
}
}
}

View File

@ -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)

View File

@ -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
//}

View File

@ -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())
}
}

View File

@ -8,7 +8,10 @@ import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionChange import hep.dataforge.vision.VisionChange
import hep.dataforge.vision.VisionManager import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.flowChanges 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 hep.dataforge.vision.server.VisionServer.Companion.DEFAULT_PAGE
import io.ktor.application.* import io.ktor.application.*
import io.ktor.features.CORS import io.ktor.features.CORS
@ -56,15 +59,25 @@ public class VisionServer internal constructor(
private val globalHeaders: ArrayList<HtmlFragment> = ArrayList() private val globalHeaders: ArrayList<HtmlFragment> = ArrayList()
public fun header(block: TagConsumer<*>.() -> Unit) { public fun header(block: TagConsumer<*>.() -> Unit) {
globalHeaders.add(HtmlFragment(block)) globalHeaders.add(block)
} }
private fun HTML.buildPage( private fun HTML.buildPage(
visionFragment: HtmlVisionFragment<Vision>, visionFragment: HtmlVisionFragment,
title: String, title: String,
headers: List<HtmlFragment>, headers: List<HtmlFragment>,
): Map<Name, Vision> { ): 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 { head {
meta { meta {
@ -77,7 +90,7 @@ public class VisionServer internal constructor(
} }
body { body {
//Load the fragment and remember all loaded visions //Load the fragment and remember all loaded visions
visionMap = visionFragment(visionFragment) visionFragment(consumer)
} }
return visionMap 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]. * Serve a page, potentially containing any number of visions at a given [route] with given [headers].
* *
*/ */
public fun servePage( public fun page(
visionFragment: HtmlVisionFragment<Vision>,
route: String = DEFAULT_PAGE, route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$route'", title: String = "VisionForge server page '$route'",
headers: List<HtmlFragment> = emptyList(), headers: List<HtmlFragment> = emptyList(),
visionFragment: HtmlVisionFragment,
) { ) {
val visions = HashMap<Name, Vision>() 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 companion object {
public const val DEFAULT_PAGE: String = "/" public const val DEFAULT_PAGE: String = "/"
public val UPDATE_INTERVAL_KEY: Name = "update.interval".toName() public val UPDATE_INTERVAL_KEY: Name = "update.interval".toName()