Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add canvascontrols package. #9

Merged
merged 1 commit into from
Aug 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Add canvascontrols package.
  • Loading branch information
ISeleznev-HORIS committed Aug 5, 2019
commit 61e563d401f106045bd9e17a412671d65295b332
25 changes: 25 additions & 0 deletions livemap/src/commonMain/kotlin/jetbrains/livemap/BaseLiveMap.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package jetbrains.livemap

import jetbrains.datalore.base.observable.event.EventHandler
import jetbrains.datalore.base.observable.event.EventSource
import jetbrains.datalore.base.observable.event.SimpleEventSource
import jetbrains.datalore.base.observable.property.Property
import jetbrains.datalore.base.observable.property.ValueProperty
import jetbrains.datalore.base.registration.Disposable
import jetbrains.datalore.base.registration.Registration
import jetbrains.datalore.visualization.base.canvas.CanvasControl

abstract class BaseLiveMap : EventSource<Throwable>, Disposable {
private val throwableSource = SimpleEventSource<Throwable>()
val isLoading: Property<Boolean> = ValueProperty(true)

abstract fun draw(canvasControl: CanvasControl)

override fun addHandler(handler: EventHandler<in Throwable>): Registration {
return throwableSource.addHandler(handler)
}

protected fun fireThrowable(throwable: Throwable) {
throwableSource.fire(throwable)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.visualization.base.canvas.CanvasControl

internal interface CanvasContent {
fun show(parentControl: CanvasControl)
fun hide()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.visualization.base.canvas.CanvasControl

internal class CanvasContentPresenter {
lateinit var canvasControl: CanvasControl
private var canvasContent: CanvasContent = EMPTY_CANVAS_CONTENT


fun show(content: CanvasContent) {
canvasContent.hide()
canvasContent = content
canvasContent.show(canvasControl)
}

fun clear() {
show(EMPTY_CANVAS_CONTENT)
}

private class EmptyContent : CanvasContent {
override fun show(parentControl: CanvasControl) {}

override fun hide() {}
}

companion object {
private val EMPTY_CANVAS_CONTENT = EmptyContent()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.base.observable.event.EventHandler
import jetbrains.datalore.base.observable.event.EventSource
import jetbrains.datalore.base.registration.Registration
import jetbrains.datalore.visualization.base.canvas.CanvasControl
import jetbrains.livemap.BaseLiveMap

class LiveMapContent(private val liveMap: BaseLiveMap) : CanvasContent, EventSource<Throwable> {

override fun show(parentControl: CanvasControl) {
liveMap.draw(parentControl)
}

override fun hide() {
liveMap.dispose()
}

override fun addHandler(handler: EventHandler<Throwable>): Registration {
return liveMap.addHandler(handler)
}

fun addHandler(handler: (Throwable) -> Unit): Registration {
return liveMap.addHandler(
object : EventHandler<Throwable> {
override fun onEvent(event: Throwable) {
handler(event)
}
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.base.async.Async
import jetbrains.datalore.base.observable.property.Properties
import jetbrains.datalore.base.observable.property.PropertyBinding
import jetbrains.datalore.base.observable.property.ValueProperty
import jetbrains.datalore.base.registration.Disposable
import jetbrains.datalore.base.registration.Registration
import jetbrains.datalore.visualization.base.canvas.CanvasControl
import jetbrains.livemap.BaseLiveMap

class LiveMapPresenter : Disposable {
private val contentPresenter: CanvasContentPresenter
private var registration = Registration.EMPTY
private var isLoadingLiveMapRegistration = Registration.EMPTY
private var removed = false

private val initializing = ValueProperty(true)
private val liveMapIsLoading = ValueProperty(true)
val isLoading = Properties.or(initializing, liveMapIsLoading)

constructor() {
contentPresenter = CanvasContentPresenter()
}

// for tests
internal constructor(presenter: CanvasContentPresenter) {
contentPresenter = presenter
}

fun render(canvasControl: CanvasControl, liveMap: Async<BaseLiveMap>) {
contentPresenter.canvasControl = canvasControl

showSpinner()
liveMap.onResult(::showLiveMap, ::showError)
}

private fun showLiveMap(liveMap: BaseLiveMap) {
if (isLoadingLiveMapRegistration !== Registration.EMPTY) {
throw IllegalStateException("Unexpected")
}

initializing.set(false)
isLoadingLiveMapRegistration = PropertyBinding.bindOneWay(liveMap.isLoading, liveMapIsLoading)

setContent {
LiveMapContent(liveMap).also {
registration = it.addHandler(::showError)
}
}
}

private fun showSpinner() {
initializing.set(true)
setContent(::SpinnerContent)
}

private fun showError(throwable: Throwable) {
initializing.set(false)
liveMapIsLoading.set(false)
val message = throwable.message
setContent { MessageContent(message ?: "Undefined exception") }
}

private fun setContent(canvasContentSupplier: () -> CanvasContent) {
if (removed) {
return
}

contentPresenter.show(canvasContentSupplier())
}

override fun dispose() {
removed = true
registration.dispose()
isLoadingLiveMapRegistration.dispose()
contentPresenter.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.visualization.base.canvas.CanvasControl
import jetbrains.datalore.visualization.base.canvas.CanvasControlUtil.drawLater
import jetbrains.datalore.visualization.base.canvas.Context2d
import jetbrains.datalore.visualization.base.canvas.Context2d.TextAlign
import jetbrains.datalore.visualization.base.canvas.Context2d.TextBaseline
import jetbrains.datalore.visualization.base.canvas.SingleCanvasControl
import kotlin.math.max

internal class MessageContent(private val message: String) : CanvasContent {
private lateinit var canvasControl: SingleCanvasControl

override fun show(parentControl: CanvasControl) {
canvasControl = SingleCanvasControl(parentControl)

with(canvasControl.createCanvas()) {
drawText(context2d, DoubleVector(size.x.toDouble(), size.y.toDouble()))

takeSnapshot()
.onSuccess { snapshot ->
drawLater(parentControl) {
canvasControl.context.drawImage(
snapshot,
0.0,
0.0
)
}
}
}
}

override fun hide() {
canvasControl.dispose()
}

private fun drawText(context: Context2d, dimension: DoubleVector) =
with(context) {
val lines: List<String> = message.split("\n")

save()

setFillColor(BACKGROUND_COLOR)
fillRect(0.0, 0.0, dimension.x, dimension.y)

setTextBaseline(TextBaseline.TOP)
setTextAlign(TextAlign.LEFT)
setFillColor(FONT_COLOR)
setFont("400 " + FONT_SIZE + "px/" + FONT_HEIGHT + "px Helvetica, Arial, sans-serif")

val height = FONT_HEIGHT * lines.size
var width = 0.0

lines.forEach { width = max(width, measureText(it)) }

lines.indices.forEach {
fillText(lines[it], (dimension.x - width) / 2, (dimension.y - height) / 2 + it * FONT_HEIGHT)
}

restore()
}

companion object {
private const val FONT_SIZE = 17.0
private const val FONT_HEIGHT = 21.25
private const val FONT_COLOR = "#B3B3B3"
private const val BACKGROUND_COLOR = "#FFFFFF"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package jetbrains.livemap.canvascontrols

import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.base.registration.Registration
import jetbrains.datalore.visualization.base.canvas.CanvasControl
import jetbrains.datalore.visualization.base.canvas.CanvasControl.AnimationEventHandler
import jetbrains.datalore.visualization.base.canvas.CanvasControlUtil.setAnimationHandler
import jetbrains.datalore.visualization.base.canvas.Context2d
import jetbrains.datalore.visualization.base.canvas.SingleCanvasControl
import kotlin.math.PI

internal class SpinnerContent : CanvasContent {

private lateinit var registration: Registration
private lateinit var canvasControl: SingleCanvasControl
private lateinit var spinnerCenter: DoubleVector

override fun show(parentControl: CanvasControl) {
canvasControl = SingleCanvasControl(parentControl)

with(canvasControl.createCanvas()) {
context2d.drawStaticElements()

registration = setAnimationHandler(
parentControl,
object : AnimationEventHandler {
override fun onEvent(millisTime: Long): Boolean {
context2d.drawSpinner(millisTime)

takeSnapshot()
.onSuccess { canvasControl.context.drawImage(it, 0.0, 0.0) }
return true
}
}
)
}


}

override fun hide() {
canvasControl.dispose()
registration.dispose()
}

private fun Context2d.drawStaticElements() {
save()

setFont("400 " + FONT_SIZE + "px Helvetica, Arial, sans-serif")
val textWidth = measureText(LOADING_TEXT)

val spinnerWidth = 2 * RADIUS + LINE_WIDTH
val width = spinnerWidth + SPACE + textWidth
val dimension = canvasControl.size
spinnerCenter = DoubleVector((dimension.x - width) / 2 + spinnerWidth / 2, dimension.y / 2.0)

setFillColor(BACKGROUND_COLOR)
fillRect(0.0, 0.0, dimension.x.toDouble(), dimension.y.toDouble())

setTextBaseline(Context2d.TextBaseline.MIDDLE)
setTextAlign(Context2d.TextAlign.LEFT)
setFillColor(FONT_COLOR)
fillText(LOADING_TEXT, (dimension.x + width) / 2 - textWidth, dimension.y / 2.0)

restore()
}

private fun Context2d.drawSpinner(time: Long) {
save()

setFillColor(BACKGROUND_COLOR)
fillRect(
spinnerCenter.x - BACK_RADIUS,
spinnerCenter.y - BACK_RADIUS,
2 * BACK_RADIUS,
2 * BACK_RADIUS
)

drawSpinnerArc(CIRCLE_COLOR, 0.0, 2 * PI)

val angle = 2.0 * PI * (time % LOOP_DURATION).toDouble() / LOOP_DURATION
drawSpinnerArc(ARC_COLOR, angle, ARC_LENGTH)

restore()
}

private fun Context2d.drawSpinnerArc(color: String, startAngle: Double, arcAngle: Double) {
setLineWidth(LINE_WIDTH)
setStrokeColor(color)
beginPath()
arc(spinnerCenter.x, spinnerCenter.y, RADIUS, startAngle, startAngle + arcAngle)
stroke()
}

companion object {
private const val BACKGROUND_COLOR = "#FFFFFF"
private const val LINE_WIDTH = 0.9
private const val RADIUS = 11.5
private const val BACK_RADIUS = RADIUS + LINE_WIDTH
private const val CIRCLE_COLOR = "#E8E8E8"
private const val ARC_COLOR = "#00BFFF"
private const val ARC_LENGTH = PI / 2
private const val LOOP_DURATION: Long = 1000
private const val SPACE = 15.0
private const val LOADING_TEXT = "Loading..."
private const val FONT_SIZE = 12.0
private const val FONT_COLOR = "#616161"
}
}