First server iteration.

This commit is contained in:
Alexander Nozik 2020-11-22 21:44:03 +03:00
parent 2be4576495
commit ebb7bf72d1
9 changed files with 301 additions and 261 deletions

View File

@ -2,6 +2,7 @@ package hep.dataforge.vision.html
import hep.dataforge.names.Name
import hep.dataforge.vision.Vision
import kotlinx.html.FlowContent
import kotlinx.html.TagConsumer
public class BindingHtmlOutputScope<T, V : Vision>(
@ -16,3 +17,11 @@ public class BindingHtmlOutputScope<T, V : Vision>(
_bindings[htmlOutput.name] = vision
}
}
public fun <T : Any> TagConsumer<T>.visionFragment(fragment: HtmlVisionFragment<Vision>): Map<Name, Vision> {
return BindingHtmlOutputScope<T, Vision>(this).apply(fragment.content).bindings
}
public fun FlowContent.visionFragment(fragment: HtmlVisionFragment<Vision>): Map<Name, Vision> {
return BindingHtmlOutputScope<Any?, Vision>(consumer).apply(fragment.content).bindings
}

View File

@ -3,10 +3,7 @@ package hep.dataforge.vision.html
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.vision.Vision
import kotlinx.html.DIV
import kotlinx.html.TagConsumer
import kotlinx.html.div
import kotlinx.html.id
import kotlinx.html.*
public class HtmlOutput<V : Vision>(
public val outputScope: HtmlOutputScope<*, V>,
@ -28,7 +25,9 @@ public abstract class HtmlOutputScope<R, V : Vision>(
name: Name,
crossinline block: HtmlOutput<V>.() -> Unit = {},
): T = div {
this.id = resolveId(name)
id = resolveId(name)
classes = setOf(OUTPUT_CLASS)
attributes[NAME_ATTRIBUTE] = name.toString()
@Suppress("UNCHECKED_CAST")
HtmlOutput(this@HtmlOutputScope, name, this).block()
}
@ -49,4 +48,20 @@ public abstract class HtmlOutputScope<R, V : Vision>(
renderVision(this, vision)
}
}
/**
* Process the resulting object produced by [TagConsumer]
*/
protected open fun processResult(result: R) {
//do nothing by default
}
override fun finalize(): R {
return root.finalize().also { processResult(it) }
}
public companion object {
public const val OUTPUT_CLASS: String = "visionforge-output"
public const val NAME_ATTRIBUTE: String = "data-output-name"
}
}

View File

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

View File

@ -22,7 +22,7 @@ public fun <T : Any> HtmlVisionFragment<Vision>.renderToObject(
root: TagConsumer<T>,
prefix: String? = null,
renderer: HtmlVisionRenderer<Vision>,
): T = StaticHtmlOutputScope(root, prefix, renderer).apply(layout).finalize()
): T = StaticHtmlOutputScope(root, prefix, renderer).apply(content).finalize()
public fun HtmlVisionFragment<Vision>.renderToString(renderer: HtmlVisionRenderer<Vision>): String =
renderToObject(createHTML(), null, renderer)

View File

@ -1,31 +0,0 @@
package hep.dataforge.vision.html
import hep.dataforge.vision.Vision
import kotlinx.browser.document
import kotlinx.html.TagConsumer
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
public interface HtmlVisionBinding<in V: Vision>{
public fun bind(element: Element, vision: V): Unit
}
public fun <V: Vision> Map<String, V>.bind(binder: HtmlVisionBinding<V>){
forEach { (id, vision) ->
val element = document.getElementById(id) ?: error("Could not find element with id $id")
binder.bind(element, vision)
}
}
public fun HtmlVisionFragment<Vision>.bindToDocument(
root: TagConsumer<HTMLElement>,
binder: HtmlVisionBinding<Vision>,
): HTMLElement = BindingHtmlOutputScope<HTMLElement, Vision>(root).apply(layout).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")
binder.bind(element, vision)
}
}
}

View File

@ -0,0 +1,53 @@
package hep.dataforge.vision.html
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.vision.Vision
import kotlinx.browser.document
import kotlinx.html.TagConsumer
import org.w3c.dom.*
public interface ElementVisionRenderer<in V : Vision> {
public fun render(element: Element, vision: V): Unit
}
public fun <V : Vision> Map<String, V>.bind(renderer: ElementVisionRenderer<V>) {
forEach { (id, vision) ->
val element = document.getElementById(id) ?: error("Could not find element with id $id")
renderer.render(element, vision)
}
}
public fun <V : Vision> Element.renderVisions(renderer: ElementVisionRenderer<V>, visionProvider: (Name) -> V?) {
val elements = getElementsByClassName(HtmlOutputScope.OUTPUT_CLASS)
elements.asList().forEach { element ->
val name = element.attributes[HtmlOutputScope.NAME_ATTRIBUTE]?.value
if (name == null) {
console.error("Attribute ${HtmlOutputScope.NAME_ATTRIBUTE} not defined in the output element")
return@forEach
}
val vision = visionProvider(name.toName())
if (vision == null) {
console.error("Vision with name $name is not resolved")
return@forEach
}
renderer.render(element, vision)
}
}
public fun <V : Vision> Document.renderVisions(renderer: ElementVisionRenderer<V>, visionProvider: (Name) -> V?): Unit {
documentElement?.renderVisions(renderer, visionProvider)
}
public fun HtmlVisionFragment<Vision>.renderInDocument(
root: TagConsumer<HTMLElement>,
renderer: ElementVisionRenderer<Vision>,
): HTMLElement = BindingHtmlOutputScope<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,92 +1,137 @@
//package hep.dataforge.vision.server
//
//import hep.dataforge.meta.*
//import hep.dataforge.names.Name
//import hep.dataforge.names.toName
//import hep.dataforge.vision.server.VisionServer.Companion.DEFAULT_PAGE
//import io.ktor.application.Application
//import io.ktor.application.featureOrNull
//import io.ktor.application.install
//import io.ktor.features.CORS
//import io.ktor.http.content.resources
//import io.ktor.http.content.static
//import io.ktor.routing.Routing
//import io.ktor.routing.route
//import io.ktor.routing.routing
//import io.ktor.server.engine.ApplicationEngine
//import io.ktor.websocket.WebSockets
//import kotlinx.html.TagConsumer
//import java.awt.Desktop
//import java.net.URI
//import kotlin.text.get
//
//public enum class ServerUpdateMode {
// NONE,
// PUSH,
// PULL
//}
//
//public class VisionServer internal constructor(
// private val routing: Routing,
// private val rootRoute: String,
//) : Configurable {
// override val config: Config = Config()
// public var updateMode: ServerUpdateMode by config.enum(ServerUpdateMode.NONE, key = UPDATE_MODE_KEY)
// public var updateInterval: Long by config.long(300, key = UPDATE_INTERVAL_KEY)
// public var embedData: Boolean by config.boolean(false)
//
// /**
// * a list of headers that should be applied to all pages
// */
// private val globalHeaders: ArrayList<HtmlFragment> = ArrayList<HtmlFragment>()
//
// public fun header(block: TagConsumer<*>.() -> Unit) {
// globalHeaders.add(HtmlFragment(block))
// }
//
// public fun page(
// plotlyFragment: PlotlyFragment,
// route: String = DEFAULT_PAGE,
// title: String = "Plotly server page '$route'",
// headers: List<HtmlFragment> = emptyList(),
// ) {
// routing.createRouteFromPath(rootRoute).apply {
// val plots = HashMap<String, Plot>()
// route(route) {
// //Update websocket
// webSocket("ws/{id}") {
// val plotId: String = call.parameters["id"] ?: error("Plot id not defined")
//
// application.log.debug("Opened server socket for $plotId")
//
// val plot = plots[plotId] ?: error("Plot with id='$plotId' not registered")
//
// try {
// plot.collectUpdates(plotId, this, updateInterval).collect { update ->
// val json = update.toJson()
// outgoing.send(Frame.Text(json.toString()))
// }
// } catch (ex: Exception) {
// application.log.debug("Closed server socket for $plotId")
// }
// }
// //Plots in their json representation
// get("data/{id}") {
// val id: String = call.parameters["id"] ?: error("Plot id not defined")
//
// val plot: Plot? = plots[id]
// if (plot == null) {
// call.respond(HttpStatusCode.NotFound, "Plot with id = $id not found")
// } else {
// call.respondText(
// plot.toJsonString(),
// contentType = ContentType.Application.Json,
// status = HttpStatusCode.OK
// )
// }
// }
// //filled pages
// get {
package hep.dataforge.vision.server
import hep.dataforge.context.Context
import hep.dataforge.meta.*
import hep.dataforge.names.Name
import hep.dataforge.names.toName
import hep.dataforge.vision.Vision
import hep.dataforge.vision.VisionManager
import hep.dataforge.vision.flowChanges
import hep.dataforge.vision.html.*
import hep.dataforge.vision.server.VisionServer.Companion.DEFAULT_PAGE
import io.ktor.application.*
import io.ktor.features.CORS
import io.ktor.html.respondHtml
import io.ktor.http.*
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.content.resources
import io.ktor.http.content.static
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.*
import io.ktor.server.engine.ApplicationEngine
import io.ktor.websocket.WebSockets
import io.ktor.websocket.webSocket
import kotlinx.html.*
import kotlinx.html.stream.createHTML
import java.awt.Desktop
import java.net.URI
import kotlin.time.milliseconds
public class VisionServer internal constructor(
private val visionManager: VisionManager,
private val routing: Routing,
private val rootRoute: String,
) : Configurable {
override val config: Config = Config()
public var updateInterval: Long by config.long(300, key = UPDATE_INTERVAL_KEY)
public var cacheFragments: Boolean by config.boolean(true)
/**
* a list of headers that should be applied to all pages
*/
private val globalHeaders: ArrayList<HtmlFragment> = ArrayList()
public fun header(block: TagConsumer<*>.() -> Unit) {
globalHeaders.add(HtmlFragment(block))
}
private fun HTML.buildPage(
visionFragment: HtmlVisionFragment<Vision>,
title: String,
headers: List<HtmlFragment>,
): Map<Name, Vision> {
lateinit var result: Map<Name, Vision>
head {
meta {
charset = "utf-8"
(globalHeaders + headers).forEach {
fragment(it)
}
}
title(title)
}
body {
result = visionFragment(visionFragment)
script {
type = "text/javascript"
val normalizedRoute = if (rootRoute.endsWith("/")) {
rootRoute
} else {
"$rootRoute/"
}
src = TODO()//"${normalizedRoute}js/plotlyConnect.js"
}
}
return result
}
public fun page(
visionFragment: HtmlVisionFragment<Vision>,
route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$route'",
headers: List<HtmlFragment> = emptyList(),
) {
val visions = HashMap<Name, Vision>()
val cachedHtml: String? = if (cacheFragments) {
createHTML(true).html {
visions.putAll(buildPage(visionFragment, title, headers))
}
} else {
null
}
routing.createRouteFromPath(rootRoute).apply {
route(route) {
//Update websocket
webSocket("ws") {
val name: String = call.request.queryParameters["name"]
?: error("Vision name is not defined in parameters")
application.log.debug("Opened server socket for $name")
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
try {
vision.flowChanges(this, updateInterval.milliseconds).collect { update ->
val json = visionManager.encodeToString(update)
outgoing.send(Frame.Text(json))
}
} catch (ex: Exception) {
application.log.debug("Closed server socket for $name")
}
}
//Plots in their json representation
get("vision") {
val name: String = call.request.queryParameters["name"]
?: error("Vision name is not defined in parameters")
val vision: Vision? = visions[name.toName()]
if (vision == null) {
call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found")
} else {
call.respondText(
visionManager.encodeToString(vision),
contentType = ContentType.Application.Json,
status = HttpStatusCode.OK
)
}
}
//filled pages
get {
// val origin = call.request.origin
// val url = URLBuilder().apply {
// protocol = URLProtocol.createOrDefault(origin.scheme)
@ -95,125 +140,68 @@
// port = origin.port
// encodedPath = origin.uri
// }.build()
// call.respondHtml {
// val normalizedRoute = if (rootRoute.endsWith("/")) {
// rootRoute
// } else {
// "$rootRoute/"
// }
//
// head {
// meta {
// charset = "utf-8"
// (globalHeaders + headers).forEach {
// it.visit(consumer)
// }
// script {
// type = "text/javascript"
// src = "${normalizedRoute}js/plotly.min.js"
// }
// script {
// type = "text/javascript"
// src = "${normalizedRoute}js/plotlyConnect.js"
// }
// }
// title(title)
// }
// body {
// val container =
// ServerPlotlyRenderer(url, updateMode, updateInterval, embedData) { plotId, plot ->
// plots[plotId] = plot
// }
// with(plotlyFragment) {
// render(container)
// }
// }
// }
// }
// }
// }
// }
//
// public fun page(
// route: String = DEFAULT_PAGE,
// title: String = "Plotly server page '$route'",
// headers: List<HtmlFragment> = emptyList(),
// content: FlowContent.(renderer: PlotlyRenderer) -> Unit,
// ) {
// page(PlotlyFragment(content), route, title, headers)
// }
//
//
// public companion object {
// public const val DEFAULT_PAGE: String = "/"
// public val UPDATE_MODE_KEY: Name = "update.mode".toName()
// public val UPDATE_INTERVAL_KEY: Name = "update.interval".toName()
// }
//}
//
//
///**
// * Attach plotly application to given server
// */
//public fun Application.visionModule(route: String = DEFAULT_PAGE): VisionServer {
// if (featureOrNull(WebSockets) == null) {
// install(WebSockets)
// }
//
// if (featureOrNull(CORS) == null) {
// install(CORS) {
// anyHost()
// }
// }
//
//
// val routing = routing {
// route(route) {
// static {
// resources()
// }
// }
// }
//
// return VisionServer(routing, route)
//}
//
//
///**
// * Configure server to start sending updates in push mode. Does not affect loaded pages
// */
//public fun VisionServer.pushUpdates(interval: Long = 100): VisionServer = apply {
// updateMode = ServerUpdateMode.PUSH
// updateInterval = interval
//}
//
///**
// * Configure client to request regular updates from server. Pull updates are more expensive than push updates since
// * they contain the full plot data and server can't decide what to send.
// */
//public fun VisionServer.pullUpdates(interval: Long = 1000): VisionServer = apply {
// updateMode = ServerUpdateMode.PULL
// updateInterval = interval
//}
//
/////**
//// * Start static server (updates via reload)
//// */
////@OptIn(KtorExperimentalAPI::class)
////public fun Plotly.serve(
//// scope: CoroutineScope = GlobalScope,
//// host: String = "localhost",
//// port: Int = 7777,
//// block: PlotlyServer.() -> Unit,
////): ApplicationEngine = scope.embeddedServer(io.ktor.server.cio.CIO, port, host) {
//// plotlyModule().apply(block)
////}.start()
//
//
//public fun ApplicationEngine.show() {
// val connector = environment.connectors.first()
// val uri = URI("http", null, connector.host, connector.port, null, null, null)
// Desktop.getDesktop().browse(uri)
//}
//
//public fun ApplicationEngine.close(): Unit = stop(1000, 5000)
if (cachedHtml == null) {
call.respondHtml {
visions.putAll(buildPage(visionFragment, title, headers))
}
} else {
call.respondText(cachedHtml, ContentType.Text.Html.withCharset(Charsets.UTF_8))
}
}
}
}
}
public fun page(
route: String = DEFAULT_PAGE,
title: String = "Plotly server page '$route'",
headers: List<HtmlFragment> = emptyList(),
content: HtmlOutputScope<*, Vision>.() -> Unit,
) {
page(buildVisionFragment(content), route, title, headers)
}
public companion object {
public const val DEFAULT_PAGE: String = "/"
public val UPDATE_MODE_KEY: Name = "update.mode".toName()
public val UPDATE_INTERVAL_KEY: Name = "update.interval".toName()
}
}
/**
* Attach plotly application to given server
*/
public fun Application.visionModule(context: Context, route: String = DEFAULT_PAGE): VisionServer {
if (featureOrNull(WebSockets) == null) {
install(WebSockets)
}
if (featureOrNull(CORS) == null) {
install(CORS) {
anyHost()
}
}
val routing = routing {
route(route) {
static {
resources()
}
}
}
val visionManager = context.plugins.fetch(VisionManager)
return VisionServer(visionManager, routing, route)
}
public fun ApplicationEngine.show() {
val connector = environment.connectors.first()
val uri = URI("http", null, connector.host, connector.port, null, null, null)
Desktop.getDesktop().browse(uri)
}
public fun ApplicationEngine.close(): Unit = stop(1000, 5000)

View File

@ -14,11 +14,5 @@ kotlin {
api(project(":visionforge-core"))
}
}
jsMain {
dependencies {
implementation(npm("three", "0.122.0"))
implementation(npm("three-csg-ts", "1.0.1"))
}
}
}
}

View File

@ -4,7 +4,7 @@ import hep.dataforge.context.*
import hep.dataforge.meta.Meta
import hep.dataforge.names.*
import hep.dataforge.vision.Vision
import hep.dataforge.vision.html.HtmlVisionBinding
import hep.dataforge.vision.html.ElementVisionRenderer
import hep.dataforge.vision.solid.*
import hep.dataforge.vision.solid.specifications.Canvas3DOptions
import hep.dataforge.vision.visible
@ -15,7 +15,7 @@ import kotlin.collections.set
import kotlin.reflect.KClass
import info.laht.threekt.objects.Group as ThreeGroup
public class ThreePlugin : AbstractPlugin(), HtmlVisionBinding<Solid> {
public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer<Solid> {
override val tag: PluginTag get() = Companion.tag
public val solidManager: SolidManager by require(SolidManager)
@ -122,7 +122,7 @@ public class ThreePlugin : AbstractPlugin(), HtmlVisionBinding<Solid> {
attach(element)
}
override fun bind(element: Element, vision: Solid) {
override fun render(element: Element, vision: Solid) {
createCanvas(element).render(vision)
}