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,12 +2,13 @@ package hep.dataforge.vision.html
import hep.dataforge.names.Name import hep.dataforge.names.Name
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import kotlinx.html.FlowContent
import kotlinx.html.TagConsumer import kotlinx.html.TagConsumer
public class BindingHtmlOutputScope<T, V : Vision>( public class BindingHtmlOutputScope<T, V : Vision>(
root: TagConsumer<T>, root: TagConsumer<T>,
prefix: String? = null, prefix: String? = null,
) : HtmlOutputScope<T, V>(root,prefix) { ) : HtmlOutputScope<T, V>(root, prefix) {
private val _bindings = HashMap<Name, V>() private val _bindings = HashMap<Name, V>()
public val bindings: Map<Name, V> get() = _bindings public val bindings: Map<Name, V> get() = _bindings
@ -15,4 +16,12 @@ public class BindingHtmlOutputScope<T, V : Vision>(
override fun renderVision(htmlOutput: HtmlOutput<V>, vision: V) { override fun renderVision(htmlOutput: HtmlOutput<V>, vision: V) {
_bindings[htmlOutput.name] = 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.Name
import hep.dataforge.names.toName import hep.dataforge.names.toName
import hep.dataforge.vision.Vision import hep.dataforge.vision.Vision
import kotlinx.html.DIV import kotlinx.html.*
import kotlinx.html.TagConsumer
import kotlinx.html.div
import kotlinx.html.id
public class HtmlOutput<V : Vision>( public class HtmlOutput<V : Vision>(
public val outputScope: HtmlOutputScope<*, V>, public val outputScope: HtmlOutputScope<*, V>,
@ -28,7 +25,9 @@ public abstract class HtmlOutputScope<R, V : Vision>(
name: Name, name: Name,
crossinline block: HtmlOutput<V>.() -> Unit = {}, crossinline block: HtmlOutput<V>.() -> Unit = {},
): T = div { ): T = div {
this.id = resolveId(name) id = resolveId(name)
classes = setOf(OUTPUT_CLASS)
attributes[NAME_ATTRIBUTE] = name.toString()
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
HtmlOutput(this@HtmlOutputScope, name, this).block() HtmlOutput(this@HtmlOutputScope, name, this).block()
} }
@ -49,4 +48,20 @@ public abstract class HtmlOutputScope<R, V : Vision>(
renderVision(this, 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 package hep.dataforge.vision.html
import hep.dataforge.vision.Vision 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> = public fun TagConsumer<*>.fragment(fragment: HtmlFragment) {
HtmlVisionFragment(visit) 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>, root: TagConsumer<T>,
prefix: String? = null, prefix: String? = null,
renderer: HtmlVisionRenderer<Vision>, 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 = public fun HtmlVisionFragment<Vision>.renderToString(renderer: HtmlVisionRenderer<Vision>): String =
renderToObject(createHTML(), null, renderer) 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 package hep.dataforge.vision.server
//
//import hep.dataforge.meta.* import hep.dataforge.context.Context
//import hep.dataforge.names.Name import hep.dataforge.meta.*
//import hep.dataforge.names.toName import hep.dataforge.names.Name
//import hep.dataforge.vision.server.VisionServer.Companion.DEFAULT_PAGE import hep.dataforge.names.toName
//import io.ktor.application.Application import hep.dataforge.vision.Vision
//import io.ktor.application.featureOrNull import hep.dataforge.vision.VisionManager
//import io.ktor.application.install import hep.dataforge.vision.flowChanges
//import io.ktor.features.CORS import hep.dataforge.vision.html.*
//import io.ktor.http.content.resources import hep.dataforge.vision.server.VisionServer.Companion.DEFAULT_PAGE
//import io.ktor.http.content.static import io.ktor.application.*
//import io.ktor.routing.Routing import io.ktor.features.CORS
//import io.ktor.routing.route import io.ktor.html.respondHtml
//import io.ktor.routing.routing import io.ktor.http.*
//import io.ktor.server.engine.ApplicationEngine import io.ktor.http.cio.websocket.Frame
//import io.ktor.websocket.WebSockets import io.ktor.http.content.resources
//import kotlinx.html.TagConsumer import io.ktor.http.content.static
//import java.awt.Desktop import io.ktor.response.respond
//import java.net.URI import io.ktor.response.respondText
//import kotlin.text.get import io.ktor.routing.*
// import io.ktor.server.engine.ApplicationEngine
//public enum class ServerUpdateMode { import io.ktor.websocket.WebSockets
// NONE, import io.ktor.websocket.webSocket
// PUSH, import kotlinx.html.*
// PULL import kotlinx.html.stream.createHTML
//} import java.awt.Desktop
// import java.net.URI
//public class VisionServer internal constructor( import kotlin.time.milliseconds
// private val routing: Routing,
// private val rootRoute: String, public class VisionServer internal constructor(
//) : Configurable { private val visionManager: VisionManager,
// override val config: Config = Config() private val routing: Routing,
// public var updateMode: ServerUpdateMode by config.enum(ServerUpdateMode.NONE, key = UPDATE_MODE_KEY) private val rootRoute: String,
// public var updateInterval: Long by config.long(300, key = UPDATE_INTERVAL_KEY) ) : Configurable {
// public var embedData: Boolean by config.boolean(false) 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<HtmlFragment>() * a list of headers that should be applied to all pages
// */
// public fun header(block: TagConsumer<*>.() -> Unit) { private val globalHeaders: ArrayList<HtmlFragment> = ArrayList()
// globalHeaders.add(HtmlFragment(block))
// } public fun header(block: TagConsumer<*>.() -> Unit) {
// globalHeaders.add(HtmlFragment(block))
// public fun page( }
// plotlyFragment: PlotlyFragment,
// route: String = DEFAULT_PAGE, private fun HTML.buildPage(
// title: String = "Plotly server page '$route'", visionFragment: HtmlVisionFragment<Vision>,
// headers: List<HtmlFragment> = emptyList(), title: String,
// ) { headers: List<HtmlFragment>,
// routing.createRouteFromPath(rootRoute).apply { ): Map<Name, Vision> {
// val plots = HashMap<String, Plot>() lateinit var result: Map<Name, Vision>
// route(route) {
// //Update websocket head {
// webSocket("ws/{id}") { meta {
// val plotId: String = call.parameters["id"] ?: error("Plot id not defined") charset = "utf-8"
// (globalHeaders + headers).forEach {
// application.log.debug("Opened server socket for $plotId") fragment(it)
// }
// val plot = plots[plotId] ?: error("Plot with id='$plotId' not registered") }
// title(title)
// try { }
// plot.collectUpdates(plotId, this, updateInterval).collect { update -> body {
// val json = update.toJson() result = visionFragment(visionFragment)
// outgoing.send(Frame.Text(json.toString())) script {
// } type = "text/javascript"
// } catch (ex: Exception) {
// application.log.debug("Closed server socket for $plotId") val normalizedRoute = if (rootRoute.endsWith("/")) {
// } rootRoute
// } } else {
// //Plots in their json representation "$rootRoute/"
// get("data/{id}") { }
// val id: String = call.parameters["id"] ?: error("Plot id not defined")
// src = TODO()//"${normalizedRoute}js/plotlyConnect.js"
// val plot: Plot? = plots[id] }
// if (plot == null) { }
// call.respond(HttpStatusCode.NotFound, "Plot with id = $id not found")
// } else { return result
// call.respondText( }
// plot.toJsonString(),
// contentType = ContentType.Application.Json, public fun page(
// status = HttpStatusCode.OK visionFragment: HtmlVisionFragment<Vision>,
// ) route: String = DEFAULT_PAGE,
// } title: String = "VisionForge server page '$route'",
// } headers: List<HtmlFragment> = emptyList(),
// //filled pages ) {
// get { 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 origin = call.request.origin
// val url = URLBuilder().apply { // val url = URLBuilder().apply {
// protocol = URLProtocol.createOrDefault(origin.scheme) // protocol = URLProtocol.createOrDefault(origin.scheme)
@ -95,125 +140,68 @@
// port = origin.port // port = origin.port
// encodedPath = origin.uri // encodedPath = origin.uri
// }.build() // }.build()
// call.respondHtml { if (cachedHtml == null) {
// val normalizedRoute = if (rootRoute.endsWith("/")) { call.respondHtml {
// rootRoute visions.putAll(buildPage(visionFragment, title, headers))
// } else { }
// "$rootRoute/" } else {
// } call.respondText(cachedHtml, ContentType.Text.Html.withCharset(Charsets.UTF_8))
// }
// head { }
// meta { }
// charset = "utf-8" }
// (globalHeaders + headers).forEach { }
// it.visit(consumer)
// } public fun page(
// script { route: String = DEFAULT_PAGE,
// type = "text/javascript" title: String = "Plotly server page '$route'",
// src = "${normalizedRoute}js/plotly.min.js" headers: List<HtmlFragment> = emptyList(),
// } content: HtmlOutputScope<*, Vision>.() -> Unit,
// script { ) {
// type = "text/javascript" page(buildVisionFragment(content), route, title, headers)
// src = "${normalizedRoute}js/plotlyConnect.js" }
// }
// }
// title(title) public companion object {
// } public const val DEFAULT_PAGE: String = "/"
// body { public val UPDATE_MODE_KEY: Name = "update.mode".toName()
// val container = public val UPDATE_INTERVAL_KEY: Name = "update.interval".toName()
// ServerPlotlyRenderer(url, updateMode, updateInterval, embedData) { plotId, plot -> }
// plots[plotId] = plot }
// }
// with(plotlyFragment) {
// render(container) /**
// } * 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) {
// public fun page( install(CORS) {
// route: String = DEFAULT_PAGE, anyHost()
// title: String = "Plotly server page '$route'", }
// headers: List<HtmlFragment> = emptyList(), }
// content: FlowContent.(renderer: PlotlyRenderer) -> Unit,
// ) {
// page(PlotlyFragment(content), route, title, headers) val routing = routing {
// } route(route) {
// static {
// resources()
// 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()
// } val visionManager = context.plugins.fetch(VisionManager)
//}
// return VisionServer(visionManager, routing, route)
// }
///**
// * Attach plotly application to given server public fun ApplicationEngine.show() {
// */ val connector = environment.connectors.first()
//public fun Application.visionModule(route: String = DEFAULT_PAGE): VisionServer { val uri = URI("http", null, connector.host, connector.port, null, null, null)
// if (featureOrNull(WebSockets) == null) { Desktop.getDesktop().browse(uri)
// install(WebSockets) }
// }
// public fun ApplicationEngine.close(): Unit = stop(1000, 5000)
// 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)

View File

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