-
Notifications
You must be signed in to change notification settings - Fork 53
/
Copy pathFigureToHtml.kt
276 lines (235 loc) · 10.8 KB
/
FigureToHtml.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/*
* Copyright (c) 2023. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.dom.createElement
import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle
import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.commons.geometry.Vector
import org.jetbrains.letsPlot.commons.registration.CompositeRegistration
import org.jetbrains.letsPlot.commons.registration.Registration
import org.jetbrains.letsPlot.core.canvasFigure.CanvasFigure
import org.jetbrains.letsPlot.core.interact.event.ToolEventDispatcher
import org.jetbrains.letsPlot.core.platf.dom.DomMouseEventMapper
import org.jetbrains.letsPlot.core.plot.builder.FigureBuildInfo
import org.jetbrains.letsPlot.core.plot.builder.GeomLayer
import org.jetbrains.letsPlot.core.plot.builder.PlotContainer
import org.jetbrains.letsPlot.core.plot.builder.PlotSvgRoot
import org.jetbrains.letsPlot.core.plot.builder.interact.CompositeToolEventDispatcher
import org.jetbrains.letsPlot.core.plot.builder.subPlots.CompositeFigureSvgRoot
import org.jetbrains.letsPlot.core.plot.livemap.CursorServiceConfig
import org.jetbrains.letsPlot.core.plot.livemap.LiveMapProviderUtil
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgNodeContainer
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgSvgElement
import org.jetbrains.letsPlot.platf.w3c.canvas.DomCanvasControl
import org.jetbrains.letsPlot.platf.w3c.dom.css.*
import org.jetbrains.letsPlot.platf.w3c.dom.css.enumerables.CssCursor
import org.jetbrains.letsPlot.platf.w3c.dom.css.enumerables.CssPosition
import org.jetbrains.letsPlot.platf.w3c.mapping.svg.SvgRootDocumentMapper
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import org.w3c.dom.Node
import org.w3c.dom.svg.SVGSVGElement
internal class FigureToHtml(
private val buildInfo: FigureBuildInfo,
// private val containerElement: HTMLElement,
private val parentElement: HTMLElement,
) {
// private val parentElement: HTMLElement = if (buildInfo.isComposite) {
// // The `containerElement` may also contain "computation messages".
// // Container for a composite figure must be another `div`
// // because it is going to have "relative" positioning.
// document.createElement("div") {
// containerElement.appendChild(this)
// } as HTMLElement
// } else {
// containerElement
// }
fun eval(isRoot: Boolean): Result {
val buildInfo = buildInfo.layoutedByOuterSize()
// containerElement.style.apply {
// width = "${buildInfo.layoutInfo.figureSize.x}px"
// height = "${buildInfo.layoutInfo.figureSize.y}px"
// }
buildInfo.injectLiveMapProvider { tiles: List<List<GeomLayer>>, spec: Map<String, Any> ->
val cursorServiceConfig = CursorServiceConfig()
LiveMapProviderUtil.injectLiveMapProvider(tiles, spec, cursorServiceConfig)
cursorServiceConfig
}
val svgRoot = buildInfo.createSvgRoot()
if (isRoot) {
// Setup fixed dimensions for plot wrapper element.
setupRootHTMLElement(
parentElement,
svgRoot.bounds.dimension
)
}
val (toolEventDispatcher, eventsRegistration) = if (svgRoot is CompositeFigureSvgRoot) {
processCompositeFigure(
svgRoot,
origin = null, // The topmost SVG
parentElement = parentElement,
)
} else {
processPlotFigure(
svgRoot = svgRoot as PlotSvgRoot,
parentElement = parentElement,
// eventArea = buildInfo.bounds
eventArea = DoubleRectangle(DoubleVector.ZERO, buildInfo.bounds.dimension)
)
}
val domCleanupRegistration = object : Registration() {
override fun doRemove() {
while (parentElement.firstChild != null) {
parentElement.removeChild(parentElement.firstChild!!)
}
}
}
return Result(
toolEventDispatcher,
CompositeRegistration().add(
eventsRegistration,
domCleanupRegistration
)
)
}
data class Result(
val toolEventDispatcher: ToolEventDispatcher,
val figureRegistration: Registration
)
companion object {
private fun processPlotFigure(
svgRoot: PlotSvgRoot,
parentElement: HTMLElement,
eventArea: DoubleRectangle
): Pair<ToolEventDispatcher, Registration> {
val plotContainer = PlotContainer(svgRoot)
val (rootSVG, cleanupRegistration) = buildPlotFigureSVG(plotContainer, parentElement, eventArea)
rootSVG.style.setCursor(CssCursor.CROSSHAIR)
// Livemap cursor pointer
if (svgRoot.isLiveMap) {
val cursorServiceConfig = svgRoot.liveMapCursorServiceConfig as CursorServiceConfig
cursorServiceConfig.defaultSetter { rootSVG.style.setCursor(CssCursor.CROSSHAIR) }
cursorServiceConfig.pointerSetter { rootSVG.style.setCursor(CssCursor.POINTER) }
}
parentElement.appendChild(rootSVG)
return plotContainer.toolEventDispatcher to cleanupRegistration
}
private fun processCompositeFigure(
svgRoot: CompositeFigureSvgRoot,
origin: DoubleVector?,
parentElement: HTMLElement,
): Pair<ToolEventDispatcher, Registration> {
svgRoot.ensureContentBuilt()
val rootSvgSvg: SvgSvgElement = svgRoot.svg
val domSVGSVG: SVGSVGElement = mapSvgToSVG(rootSvgSvg)
val rootNode: Node = if (origin == null) {
domSVGSVG
} else {
// Not a root - put in "container" with absolute positioning.
createContainerElement(origin).apply {
appendChild(domSVGSVG)
}
}
parentElement.appendChild(rootNode)
@Suppress("NAME_SHADOWING")
val origin = origin ?: DoubleVector.ZERO
// Sub-figures
val elementToolEventDispatchers = ArrayList<ToolEventDispatcher>()
val elementRegistractions = CompositeRegistration()
for (figureSvgRoot in svgRoot.elements) {
val elementOrigin = figureSvgRoot.bounds.origin.add(origin)
val (toolEventDispatcher, registration) = if (figureSvgRoot is PlotSvgRoot) {
// Create "container" with absolute positioning.
val figureContainer = createContainerElement(elementOrigin)
parentElement.appendChild(figureContainer)
processPlotFigure(
svgRoot = figureSvgRoot,
parentElement = figureContainer,
eventArea = DoubleRectangle(DoubleVector.ZERO, figureSvgRoot.bounds.dimension)
)
} else {
figureSvgRoot as CompositeFigureSvgRoot
processCompositeFigure(figureSvgRoot, elementOrigin, parentElement)
}
elementToolEventDispatchers.add(toolEventDispatcher)
elementRegistractions.add(registration)
}
return CompositeToolEventDispatcher(elementToolEventDispatchers) to elementRegistractions
}
fun setupRootHTMLElement(element: HTMLElement, size: DoubleVector) {
// val style = "position: relative;" < -- ggbunch doesn't work without setting the container's width/height.
val style = "position: relative; width: ${size.x}px; height: ${size.y}px;"
element.setAttribute("style", style)
}
fun createContainerElement(origin: DoubleVector): HTMLElement {
return document.createElement("div") {
setAttribute(
"style",
"position: absolute; left: ${origin.x}px; top: ${origin.y}px;"
)
} as HTMLElement
}
private fun mapSvgToSVG(svg: SvgSvgElement): SVGSVGElement {
val mapper = SvgRootDocumentMapper(svg)
SvgNodeContainer(svg)
mapper.attachRoot()
return mapper.target
}
private fun buildPlotFigureSVG(
plotContainer: PlotContainer,
parentElement: Element,
eventArea: DoubleRectangle,
): Pair<SVGSVGElement, Registration> {
val svg: SVGSVGElement = mapSvgToSVG(plotContainer.svg)
if (plotContainer.isLiveMap) {
svg.style.run {
setPosition(CssPosition.RELATIVE)
}
}
val plotMouseEventMapper = DomMouseEventMapper(parentElement, eventArea)
val eventsRegistration = CompositeRegistration()
eventsRegistration.add(Registration.from(plotMouseEventMapper))
plotContainer.mouseEventPeer.addEventSource(plotMouseEventMapper)
plotContainer.liveMapFigures.forEach { liveMapFigure ->
val bounds = (liveMapFigure as CanvasFigure).bounds().get()
val liveMapDiv = document.createElement("div") as HTMLElement
liveMapDiv.style.run {
setLeft(bounds.origin.x.toDouble())
setTop(bounds.origin.y.toDouble())
setWidth(bounds.dimension.x)
setPosition(CssPosition.RELATIVE)
}
val canvasMouseEventMapper = DomMouseEventMapper(
parentElement,
DoubleRectangle(
eventArea.origin.add(bounds.origin.toDoubleVector()),
bounds.dimension.toDoubleVector()
)
)
eventsRegistration.add(Registration.from(canvasMouseEventMapper))
val canvasControl = DomCanvasControl(
myRootElement = liveMapDiv,
size = Vector(bounds.dimension.x, bounds.dimension.y),
mouseEventSource = canvasMouseEventMapper
)
val liveMapReg = liveMapFigure.mapToCanvas(canvasControl)
parentElement.appendChild(liveMapDiv)
liveMapDiv.onDisconnect(liveMapReg::dispose)
}
return svg to eventsRegistration
}
private fun Node.onDisconnect(onDisconnected: () -> Unit): Int {
fun checkConnection() {
if (!isConnected) {
onDisconnected()
} else {
window.requestAnimationFrame { checkConnection() }
}
}
return window.requestAnimationFrame { checkConnection() }
}
}
}