Added file export

This commit is contained in:
Alexander Nozik 2020-12-12 18:27:36 +03:00
parent 734d1e1168
commit 70ac2c99dd
16 changed files with 388 additions and 319 deletions

View File

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

View File

@ -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") {
//
}

View File

@ -78,4 +78,9 @@ 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)

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

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.
*/
@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 = "."

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.vision.Vision
public interface Output<in V : Vision> {
public fun interface Output<in V : Vision> {
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.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)
}
)
}
}

View File

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

View File

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

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