Refactor pages

This commit is contained in:
Alexander Nozik 2022-11-17 21:49:14 +03:00
parent 960d17855b
commit 3c51060e2e
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
32 changed files with 252 additions and 98 deletions

View File

@ -6,6 +6,7 @@
- MeshLine for thick lines - MeshLine for thick lines
### Changed ### Changed
- API update for server and pages
- Edges moved to solids module for easier construction - Edges moved to solids module for easier construction
- Visions **must** be rooted in order to subscribe to updates. - Visions **must** be rooted in order to subscribe to updates.
- Visions use flows instead of direct subscriptions. - Visions use flows instead of direct subscriptions.

View File

@ -1,3 +1,7 @@
import space.kscience.gradle.isInDevelopment
import space.kscience.gradle.useApache2Licence
import space.kscience.gradle.useSPCTeam
plugins { plugins {
id("space.kscience.gradle.project") id("space.kscience.gradle.project")
// id("org.jetbrains.kotlinx.kover") version "0.5.0" // id("org.jetbrains.kotlinx.kover") version "0.5.0"
@ -8,7 +12,7 @@ val fxVersion by extra("11")
allprojects { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.3.0-dev-2" version = "0.3.0-dev-3"
} }
subprojects { subprojects {
@ -29,8 +33,18 @@ subprojects {
} }
ksciencePublish { ksciencePublish {
github("visionforge") pom("https://github.com/SciProgCentre/visionforge") {
space() useApache2Licence()
useSPCTeam()
}
github(githubProject = "visionforge", githubOrg = "SciProgCentre")
space(
if (isInDevelopment) {
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
} else {
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
}
)
sonatype() sonatype()
} }
@ -39,8 +53,3 @@ apiValidation {
} }
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md") readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
//rootProject.extensions.configure<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension> {
// versions.webpackCli.version = "4.10.0"
//}

View File

@ -218,6 +218,7 @@ private object RootDecoder {
var value: Any? = null var value: Any? = null
@Suppress("UNCHECKED_CAST")
fun <T> getOrPutValue(builder: (JsonElement) -> T): T { fun <T> getOrPutValue(builder: (JsonElement) -> T): T {
if (value == null) { if (value == null) {
value = builder(element) value = builder(element)

View File

@ -4,7 +4,7 @@ import kotlinx.html.*
import space.kscience.dataforge.context.Global import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.fetch import space.kscience.dataforge.context.fetch
import space.kscience.visionforge.VisionManager import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.Page import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.formFragment import space.kscience.visionforge.html.formFragment
import space.kscience.visionforge.onPropertyChange import space.kscience.visionforge.onPropertyChange
import space.kscience.visionforge.server.close import space.kscience.visionforge.server.close
@ -15,7 +15,7 @@ fun main() {
val visionManager = Global.fetch(VisionManager) val visionManager = Global.fetch(VisionManager)
val server = visionManager.serve { val server = visionManager.serve {
page(header = Page.scriptHeader("js/visionforge-playground.js")) { page(VisionPage.scriptHeader("js/visionforge-playground.js")) {
val form = formFragment("form") { val form = formFragment("form") {
label { label {
htmlFor = "fname" htmlFor = "fname"

View File

@ -1,15 +1,92 @@
package space.kscience.visionforge.examples package space.kscience.visionforge.examples
import space.kscience.plotly.scatter import space.kscience.dataforge.meta.Value
import space.kscience.plotly.layout
import space.kscience.plotly.models.*
import space.kscience.visionforge.html.ResourceLocation import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.plotly.plotly import space.kscience.visionforge.plotly.plotly
fun main() = makeVisionFile(resourceLocation = ResourceLocation.SYSTEM) { fun main() = makeVisionFile(resourceLocation = ResourceLocation.SYSTEM) {
vision { vision {
val trace1 = Violin {
text("sample length: 32")
marker {
line {
width = 2
color("#bebada")
}
symbol = Symbol.valueOf("line-ns")
}
orientation = Orientation.h
hoveron = ViolinHoveron.`points+kde`
meanline {
visible = true
}
legendgroup = "F"
scalegroup = "F"
points = ViolinPoints.all
pointpos = 1.2
jitter = 0
box {
visible = true
}
scalemode = ViolinScaleMode.count
showlegend = false
side = ViolinSide.positive
y0 = Value.of(0)
line {
color("#bebada")
}
name = "F"
x(10.07, 34.83, 10.65, 12.43, 24.08, 13.42, 12.48, 29.8, 14.52, 11.38,
20.27, 11.17, 12.26, 18.26, 8.51, 10.33, 14.15, 13.16, 17.47, 27.05, 16.43,
8.35, 18.64, 11.87, 19.81, 43.11, 13.0, 12.74, 13.0, 16.4, 16.47, 18.78)
}
val trace2 = Violin {
text("sample length: 32")
marker {
line {
width = 2
color("#8dd3c7")
}
symbol = Symbol.valueOf("line-ns")
}
orientation = Orientation.h
hoveron = ViolinHoveron.`points+kde`
meanline {
visible = true
}
legendgroup = "M"
scalegroup = "M"
points = ViolinPoints.all
pointpos = -1.2
jitter = 0
box {
visible = true
}
scalemode = ViolinScaleMode.count
showlegend = false
side = ViolinSide.negative
y0 = Value.of(0)
line {
color("#8dd3c7")
}
name = "M"
x(27.2, 22.76, 17.29, 19.44, 16.66, 32.68, 15.98, 13.03, 18.28, 24.71,
21.16, 11.69, 14.26, 15.95, 8.52, 22.82, 19.08, 16.0, 34.3, 41.19, 9.78,
7.51, 28.44, 15.48, 16.58, 7.56, 10.34, 13.51, 18.71, 20.53)
}
plotly { plotly {
scatter { traces(trace1, trace2)
x(1, 2, 3) layout {
y(5, 8, 7) width = 800
height = 800
title = "Advanced Violin Plot"
} }
} }
} }

View File

@ -2,8 +2,8 @@ package space.kscience.visionforge.examples
import space.kscience.dataforge.context.Global import space.kscience.dataforge.context.Global
import space.kscience.visionforge.html.HtmlVisionFragment import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.Page
import space.kscience.visionforge.html.ResourceLocation import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.importScriptHeader import space.kscience.visionforge.html.importScriptHeader
import space.kscience.visionforge.makeFile import space.kscience.visionforge.makeFile
import java.awt.Desktop import java.awt.Desktop
@ -16,10 +16,10 @@ public fun makeVisionFile(
show: Boolean = true, show: Boolean = true,
content: HtmlVisionFragment, content: HtmlVisionFragment,
): Unit { ): Unit {
val actualPath = Page(Global, content = content).makeFile(path) { actualPath -> val actualPath = VisionPage(Global, content = content).makeFile(path) { actualPath ->
mapOf( mapOf(
"title" to Page.title(title), "title" to VisionPage.title(title),
"playground" to Page.importScriptHeader("js/visionforge-playground.js", resourceLocation, actualPath), "playground" to VisionPage.importScriptHeader("js/visionforge-playground.js", resourceLocation, actualPath),
) )
} }
if (show) Desktop.getDesktop().browse(actualPath.toFile().toURI()) if (show) Desktop.getDesktop().browse(actualPath.toFile().toURI())

View File

@ -10,8 +10,8 @@ import space.kscience.dataforge.meta.Null
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Colors import space.kscience.visionforge.Colors
import space.kscience.visionforge.html.Page import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.plus import space.kscience.visionforge.server.DataServeMode
import space.kscience.visionforge.server.close import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.serve import space.kscience.visionforge.server.serve
@ -37,7 +37,8 @@ fun main() {
} }
val server = satContext.visionManager.serve { val server = satContext.visionManager.serve {
page(header = Page.threeJsHeader + Page.styleSheetHeader("css/styles.css")) { dataMode = DataServeMode.UPDATE
page(VisionPage.threeJsHeader, VisionPage.styleSheetHeader("css/styles.css")) {
div("flex-column") { div("flex-column") {
h1 { +"Satellite detector demo" } h1 { +"Satellite detector demo" }
vision { sat } vision { sat }

View File

@ -6,4 +6,4 @@ kotlin.incremental.js.ir=true
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4G org.gradle.jvmargs=-Xmx4G
toolsVersion=0.12.0-kotlin-1.7.20-Beta toolsVersion=0.13.3-kotlin-1.7.20

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -49,7 +49,7 @@ public abstract class JupyterPluginBase(final override val context: Context) : J
} }
render<Page> { page -> render<VisionPage> { page ->
HTML(page.render(createHTML()), true) HTML(page.render(createHTML()), true)
} }

View File

@ -8,7 +8,7 @@ import kotlinx.css.padding
import kotlinx.css.properties.border import kotlinx.css.properties.border
import kotlinx.css.px import kotlinx.css.px
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import org.w3c.dom.events.Event import kotlinx.html.org.w3c.dom.events.Event
import org.w3c.files.Blob import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag import org.w3c.files.BlobPropertyBag
import react.FC import react.FC

View File

@ -3,7 +3,6 @@ package space.kscience.visionforge.bootstrap
import org.w3c.dom.Element import org.w3c.dom.Element
import react.RBuilder import react.RBuilder
import react.dom.client.createRoot import react.dom.client.createRoot
import react.key
import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.isEmpty import space.kscience.dataforge.meta.isEmpty
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision

View File

@ -3,7 +3,7 @@ package space.kscience.visionforge.react
import kotlinx.css.Align import kotlinx.css.Align
import kotlinx.css.alignItems import kotlinx.css.alignItems
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import org.w3c.dom.events.Event import kotlinx.html.org.w3c.dom.events.Event
import react.* import react.*
import react.dom.a import react.dom.a
import react.dom.attrs import react.dom.attrs

View File

@ -1,10 +1,10 @@
package space.kscience.visionforge.react package space.kscience.visionforge.react
import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onChangeFunction
import kotlinx.html.org.w3c.dom.events.Event
import org.w3c.dom.HTMLOptionElement import org.w3c.dom.HTMLOptionElement
import org.w3c.dom.HTMLSelectElement import org.w3c.dom.HTMLSelectElement
import org.w3c.dom.asList import org.w3c.dom.asList
import org.w3c.dom.events.Event
import react.FC import react.FC
import react.dom.attrs import react.dom.attrs
import react.dom.option import react.dom.option

View File

@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
import kotlinx.css.* import kotlinx.css.*
import kotlinx.css.properties.TextDecoration import kotlinx.css.properties.TextDecoration
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import org.w3c.dom.events.Event import kotlinx.html.org.w3c.dom.events.Event
import react.* import react.*
import react.dom.attrs import react.dom.attrs
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*

View File

@ -4,8 +4,8 @@ import kotlinx.css.pct
import kotlinx.css.width import kotlinx.css.width
import kotlinx.html.InputType import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onChangeFunction
import kotlinx.html.org.w3c.dom.events.Event
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
import react.FC import react.FC
import react.dom.attrs import react.dom.attrs
import react.fc import react.fc

View File

@ -7,7 +7,7 @@ import kotlinx.css.cursor
import kotlinx.css.properties.TextDecorationLine import kotlinx.css.properties.TextDecorationLine
import kotlinx.css.properties.textDecoration import kotlinx.css.properties.textDecoration
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import org.w3c.dom.events.Event import kotlinx.html.org.w3c.dom.events.Event
import react.* import react.*
import react.dom.attrs import react.dom.attrs
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name

View File

@ -7,9 +7,9 @@ import kotlinx.css.width
import kotlinx.html.InputType import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onKeyDownFunction import kotlinx.html.js.onKeyDownFunction
import kotlinx.html.org.w3c.dom.events.Event
import org.w3c.dom.HTMLInputElement import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLSelectElement import org.w3c.dom.HTMLSelectElement
import org.w3c.dom.events.Event
import react.FC import react.FC
import react.Props import react.Props
import react.dom.attrs import react.dom.attrs

View File

@ -4,7 +4,6 @@ import org.w3c.dom.Element
import react.RBuilder import react.RBuilder
import react.dom.client.createRoot import react.dom.client.createRoot
import react.dom.p import react.dom.p
import react.key
import ringui.Island import ringui.Island
import ringui.SmartTabs import ringui.SmartTabs
import ringui.Tab import ringui.Tab

View File

@ -8,7 +8,7 @@ import kotlinx.css.padding
import kotlinx.css.properties.border import kotlinx.css.properties.border
import kotlinx.css.px import kotlinx.css.px
import kotlinx.html.js.onClickFunction import kotlinx.html.js.onClickFunction
import org.w3c.dom.events.Event import kotlinx.html.org.w3c.dom.events.Event
import org.w3c.files.Blob import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag import org.w3c.files.BlobPropertyBag
import react.FC import react.FC

View File

@ -9,7 +9,7 @@ kotlin {
commonMain { commonMain {
dependencies { dependencies {
api("space.kscience:dataforge-context:$dataforgeVersion") api("space.kscience:dataforge-context:$dataforgeVersion")
api(npmlibs.kotlinx.html) api("org.jetbrains.kotlinx:kotlinx-html:0.8.0")
api("org.jetbrains.kotlin-wrappers:kotlin-css") api("org.jetbrains.kotlin-wrappers:kotlin-css")
} }
} }

View File

@ -118,14 +118,14 @@ private fun CoroutineScope.collectChange(
name: Name, name: Name,
source: Vision, source: Vision,
mutex: Mutex, mutex: Mutex,
collector: () -> VisionChangeBuilder, collector: VisionChangeBuilder,
) { ) {
//Collect properties change //Collect properties change
source.onPropertyChange(this) { propertyName -> source.properties.changes.onEach { propertyName ->
val newItem = source.properties.own?.get(propertyName) val newItem = source.properties.own?.get(propertyName)
collector().propertyChanged(name, propertyName, newItem) collector.propertyChanged(name, propertyName, newItem)
} }.launchIn(this)
val children = source.children val children = source.children
//Subscribe for children changes //Subscribe for children changes
@ -141,7 +141,7 @@ private fun CoroutineScope.collectChange(
collectChange(fullName, after, mutex, collector) collectChange(fullName, after, mutex, collector)
} }
mutex.withLock { mutex.withLock {
collector().setChild(fullName, after) collector.setChild(fullName, after)
} }
}?.launchIn(this) }?.launchIn(this)
} }
@ -156,7 +156,7 @@ public fun Vision.flowChanges(
coroutineScope { coroutineScope {
val collector = VisionChangeBuilder() val collector = VisionChangeBuilder()
val mutex = Mutex() val mutex = Mutex()
collectChange(Name.EMPTY, this@flowChanges, mutex) { collector } collectChange(Name.EMPTY, this@flowChanges, mutex, collector)
//Send initial vision state //Send initial vision state
val initialChange = VisionChange(vision = deepCopy(manager)) val initialChange = VisionChange(vision = deepCopy(manager))
@ -167,10 +167,10 @@ public fun Vision.flowChanges(
delay(collectionDuration) delay(collectionDuration)
//Propagate updates only if something is changed //Propagate updates only if something is changed
if (!collector.isEmpty()) { if (!collector.isEmpty()) {
mutex.withLock {
//emit changes //emit changes
emit(collector.deepCopy(manager)) emit(collector.deepCopy(manager))
//Reset the collector //Reset the collector
mutex.withLock {
collector.reset() collector.reset()
} }
} }

View File

@ -3,9 +3,14 @@ package space.kscience.visionforge.html
import kotlinx.html.* import kotlinx.html.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
public data class Page( /**
* A structure representing a single page with Visions to be rendered.
*
* @param pageHeaders static headers for this page.
*/
public data class VisionPage(
public val context: Context, public val context: Context,
public val headers: Map<String, HtmlFragment> = emptyMap(), public val pageHeaders: Map<String, HtmlFragment> = emptyMap(),
public val content: HtmlVisionFragment, public val content: HtmlVisionFragment,
) { ) {
public fun <R> render(root: TagConsumer<R>): R = root.apply { public fun <R> render(root: TagConsumer<R>): R = root.apply {
@ -13,7 +18,7 @@ public data class Page(
meta { meta {
charset = "utf-8" charset = "utf-8"
} }
headers.values.forEach { pageHeaders.values.forEach {
fragment(it) fragment(it)
} }
} }

View File

@ -114,7 +114,7 @@ internal fun fileCssHeader(
/** /**
* Make a script header from a resource file, automatically copying file to appropriate location * Make a script header from a resource file, automatically copying file to appropriate location
*/ */
public fun Page.Companion.importScriptHeader( public fun VisionPage.Companion.importScriptHeader(
scriptResource: String, scriptResource: String,
resourceLocation: ResourceLocation, resourceLocation: ResourceLocation,
htmlPath: Path? = null, htmlPath: Path? = null,

View File

@ -3,7 +3,7 @@ package space.kscience.visionforge
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.html.HtmlFragment import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.Page import space.kscience.visionforge.html.VisionPage
import java.awt.Desktop import java.awt.Desktop
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@ -54,8 +54,11 @@ import java.nio.file.Path
// } // }
//} //}
/**
* Export a [VisionPage] to a file
*/
@DFExperimental @DFExperimental
public fun Page.makeFile( public fun VisionPage.makeFile(
path: Path?, path: Path?,
defaultHeaders: ((Path) -> Map<String, HtmlFragment>)? = null, defaultHeaders: ((Path) -> Map<String, HtmlFragment>)? = null,
): Path { ): Path {
@ -64,7 +67,7 @@ public fun Page.makeFile(
} ?: Files.createTempFile("tempPlot", ".html") } ?: Files.createTempFile("tempPlot", ".html")
val actualDefaultHeaders = defaultHeaders?.invoke(actualFile) val actualDefaultHeaders = defaultHeaders?.invoke(actualFile)
val actualPage = if (actualDefaultHeaders == null) this else copy(headers = actualDefaultHeaders + headers) val actualPage = if (actualDefaultHeaders == null) this else copy(pageHeaders = actualDefaultHeaders + pageHeaders)
val htmlString = actualPage.render(createHTML()) val htmlString = actualPage.render(createHTML())
@ -73,7 +76,7 @@ public fun Page.makeFile(
} }
@DFExperimental @DFExperimental
public fun Page.show(path: Path? = null) { public fun VisionPage.show(path: Path? = null) {
val actualPath = makeFile(path) val actualPath = makeFile(path)
Desktop.getDesktop().browse(actualPath.toFile().toURI()) Desktop.getDesktop().browse(actualPath.toFile().toURI())
} }

View File

@ -0,0 +1,5 @@
package space.kscience.visionforge.markup
import space.kscience.visionforge.VisionPlugin
public expect class MarkupPlugin: VisionPlugin

View File

@ -16,7 +16,7 @@ import space.kscience.visionforge.markup.VisionOfMarkup.Companion.COMMONMARK_FOR
import space.kscience.visionforge.markup.VisionOfMarkup.Companion.GFM_FORMAT import space.kscience.visionforge.markup.VisionOfMarkup.Companion.GFM_FORMAT
import kotlin.reflect.KClass import kotlin.reflect.KClass
public class MarkupPlugin : VisionPlugin(), ElementVisionRenderer { public actual class MarkupPlugin : VisionPlugin(), ElementVisionRenderer {
public val visionClient: VisionClient by require(VisionClient) public val visionClient: VisionClient by require(VisionClient)
override val tag: PluginTag get() = Companion.tag override val tag: PluginTag get() = Companion.tag
override val visionSerializersModule: SerializersModule get() = markupSerializersModule override val visionSerializersModule: SerializersModule get() = markupSerializersModule

View File

@ -0,0 +1,24 @@
package space.kscience.visionforge.markup
import kotlinx.serialization.modules.SerializersModule
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.visionforge.VisionPlugin
import kotlin.reflect.KClass
public actual class MarkupPlugin : VisionPlugin() {
override val visionSerializersModule: SerializersModule get() = markupSerializersModule
override val tag: PluginTag get() = Companion.tag
public companion object : PluginFactory<MarkupPlugin> {
override val tag: PluginTag = PluginTag("vision.plotly", PluginTag.DATAFORGE_GROUP)
override val type: KClass<out MarkupPlugin> = MarkupPlugin::class
override fun build(context: Context, meta: Meta): MarkupPlugin = MarkupPlugin()
}
}

View File

@ -1,7 +1,10 @@
package space.kscience.visionforge.server package space.kscience.visionforge.server
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.application.log
import io.ktor.server.cio.CIO import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
@ -24,7 +27,6 @@ import kotlinx.coroutines.withContext
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionChange import space.kscience.visionforge.VisionChange
@ -32,6 +34,7 @@ import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.flowChanges import space.kscience.visionforge.flowChanges
import space.kscience.visionforge.html.HtmlFragment import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.HtmlVisionFragment import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.visionFragment import space.kscience.visionforge.html.visionFragment
import space.kscience.visionforge.server.VisionServer.Companion.DEFAULT_PAGE import space.kscience.visionforge.server.VisionServer.Companion.DEFAULT_PAGE
import java.awt.Desktop import java.awt.Desktop
@ -39,6 +42,23 @@ import java.net.URI
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
public enum class DataServeMode {
/**
* Embed the initial state of the vision inside its html tag.
*/
EMBED,
/**
* Fetch data on vision load. Do not embed data.
*/
FETCH,
/**
* Connect to server to get pushes. The address of the server is embedded in the tag.
*/
UPDATE
}
/** /**
* A ktor plugin container with given [routing] * A ktor plugin container with given [routing]
* @param serverUrl a server url including root route * @param serverUrl a server url including root route
@ -63,25 +83,20 @@ public class VisionServer internal constructor(
*/ */
public var cacheFragments: Boolean by meta.boolean(true) public var cacheFragments: Boolean by meta.boolean(true)
/** public var dataMode: DataServeMode by meta.enum(DataServeMode.UPDATE)
* Embed the initial state of the vision inside its html tag. Default: `true`
*/ private val serverHeaders: MutableMap<String, HtmlFragment> = mutableMapOf()
public var dataEmbed: Boolean by meta.boolean(true, Name.parse("data.embed"))
/** /**
* Fetch data on vision load. Overrides embedded data. Default: `false` * Set up a default header that is automatically added to all pages on this server
*/ */
public var dataFetch: Boolean by meta.boolean(false, Name.parse("data.fetch")) public fun header(key: String, block: HtmlFragment) {
serverHeaders[key] = block
/** }
* Connect to server to get pushes. The address of the server is embedded in the tag. Default: `true`
*/
public var dataUpdate: Boolean by meta.boolean(true, Name.parse("data.update"))
private fun HTML.visionPage( private fun HTML.visionPage(
title: String,
pagePath: String, pagePath: String,
header: HtmlFragment, headers: Map<String, HtmlFragment>,
visionFragment: HtmlVisionFragment, visionFragment: HtmlVisionFragment,
): Map<Name, Vision> { ): Map<Name, Vision> {
var visionMap: Map<Name, Vision>? = null var visionMap: Map<Name, Vision>? = null
@ -89,17 +104,18 @@ public class VisionServer internal constructor(
head { head {
meta { meta {
charset = "utf-8" charset = "utf-8"
header()
} }
title(title) (serverHeaders + headers).values.forEach {
consumer.header() consumer.it()
}
} }
body { body {
//Load the fragment and remember all loaded visions //Load the fragment and remember all loaded visions
visionMap = visionFragment( visionMap = visionFragment(
context = visionManager.context, context = visionManager.context,
embedData = true, embedData = dataMode == DataServeMode.EMBED,
fetchUpdatesUrl = "$serverUrl$pagePath/ws", fetchDataUrl = if (dataMode != DataServeMode.EMBED) "$serverUrl$pagePath/data" else null,
fetchUpdatesUrl = if (dataMode == DataServeMode.UPDATE) "$serverUrl$pagePath/ws" else null,
fragment = visionFragment fragment = visionFragment
) )
} }
@ -110,7 +126,6 @@ public class VisionServer internal constructor(
/** /**
* Server a map of visions without providing explicit html page for them * Server a map of visions without providing explicit html page for them
*/ */
@OptIn(DFExperimental::class)
private fun serveVisions(route: Route, visions: Map<Name, Vision>): Unit = route { private fun serveVisions(route: Route, visions: Map<Name, Vision>): Unit = route {
application.log.info("Serving visions $visions at $route") application.log.info("Serving visions $visions at $route")
@ -191,12 +206,11 @@ public class VisionServer internal constructor(
}.finalize() }.finalize()
/** /**
* Serve a page, potentially containing any number of visions at a given [pagePath] with given [headers]. * Serve a page, potentially containing any number of visions at a given [route] with given [header].
*/ */
public fun page( public fun page(
pagePath: String = DEFAULT_PAGE, route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$pagePath'", headers: Map<String, HtmlFragment>,
header: HtmlFragment = {},
visionFragment: HtmlVisionFragment, visionFragment: HtmlVisionFragment,
) { ) {
val visions = HashMap<Name, Vision>() val visions = HashMap<Name, Vision>()
@ -204,13 +218,13 @@ public class VisionServer internal constructor(
val cachedHtml: String? = if (cacheFragments) { val cachedHtml: String? = if (cacheFragments) {
//Create and cache page html and map of visions //Create and cache page html and map of visions
createHTML(true).html { createHTML(true).html {
visions.putAll(visionPage(title, pagePath, header, visionFragment)) visions.putAll(visionPage(route, headers, visionFragment))
} }
} else { } else {
null null
} }
root.route(pagePath) { root.route(route) {
serveVisions(this, visions) serveVisions(this, visions)
//filled pages //filled pages
get { get {
@ -218,7 +232,7 @@ public class VisionServer internal constructor(
//re-create html and vision list on each call //re-create html and vision list on each call
call.respondHtml { call.respondHtml {
visions.clear() visions.clear()
visions.putAll(visionPage(title, pagePath, header, visionFragment)) visions.putAll(visionPage(route, headers, visionFragment))
} }
} else { } else {
//Use cached html //Use cached html
@ -226,8 +240,22 @@ public class VisionServer internal constructor(
} }
} }
} }
}
public fun page(
vararg headers: HtmlFragment,
route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$route'",
visionFragment: HtmlVisionFragment,
) {
page(route, mapOf("title" to VisionPage.title(title)) + headers.associateBy { it.hashCode().toString() }, visionFragment)
}
/**
* Render given [VisionPage] at server
*/
public fun page(route: String, page: VisionPage) {
page(route = route, headers = page.pageHeaders, visionFragment = page.content)
} }
public companion object { public companion object {

View File

@ -3,6 +3,7 @@ plugins {
} }
kotlin{ kotlin{
explicitApi = org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode.Disabled
js{ js{
binaries.library() binaries.library()
} }

View File

@ -24,6 +24,9 @@ public object ThreeCanvasLabelFactory : ThreeFactory<SolidLabel> {
override fun build(three: ThreePlugin, vision: SolidLabel, observe: Boolean): Object3D { override fun build(three: ThreePlugin, vision: SolidLabel, observe: Boolean): Object3D {
val canvas = document.createElement("canvas") as HTMLCanvasElement val canvas = document.createElement("canvas") as HTMLCanvasElement
canvas.width = 200
canvas.height = 200
canvas.getContext("2d").apply { canvas.getContext("2d").apply {
this as CanvasRenderingContext2D this as CanvasRenderingContext2D
font = "Bold ${vision.fontSize}pt ${vision.fontFamily}" font = "Bold ${vision.fontSize}pt ${vision.fontFamily}"
@ -43,15 +46,13 @@ public object ThreeCanvasLabelFactory : ThreeFactory<SolidLabel> {
val texture = Texture(canvas) val texture = Texture(canvas)
texture.needsUpdate = true texture.needsUpdate = true
val material = MeshBasicMaterial().apply { val mesh = Mesh(
PlaneGeometry(canvas.width, canvas.height),
MeshBasicMaterial().apply {
map = texture map = texture
side = DoubleSide side = DoubleSide
transparent = true transparent = true
} }
val mesh = Mesh(
PlaneGeometry(canvas.width, canvas.height),
material
) )
mesh.updatePosition(vision) mesh.updatePosition(vision)

View File

@ -8,7 +8,7 @@ import java.awt.Desktop
import java.nio.file.Path import java.nio.file.Path
public val Page.Companion.threeJsHeader: HtmlFragment get() = scriptHeader("js/visionforge-three.js") public val VisionPage.Companion.threeJsHeader: HtmlFragment get() = scriptHeader("js/visionforge-three.js")
@DFExperimental @DFExperimental
@ -19,10 +19,10 @@ public fun makeThreeJsFile(
show: Boolean = true, show: Boolean = true,
content: HtmlVisionFragment, content: HtmlVisionFragment,
): Unit { ): Unit {
val actualPath = Page(Global, content = content).makeFile(path) { actualPath -> val actualPath = VisionPage(Global, content = content).makeFile(path) { actualPath ->
mapOf( mapOf(
"title" to Page.title(title), "title" to VisionPage.title(title),
"threeJs" to Page.importScriptHeader("js/visionforge-three.js", resourceLocation, actualPath) "threeJs" to VisionPage.importScriptHeader("js/visionforge-three.js", resourceLocation, actualPath)
) )
} }
if (show) Desktop.getDesktop().browse(actualPath.toFile().toURI()) if (show) Desktop.getDesktop().browse(actualPath.toFile().toURI())