Refactor network simulator towards MVC model

* Reordered functions in the network map visualiser as a part of an ongoing refactor.
* Started separating concerns out of the NetworkMapVisualiser.
* Moved more view logic to the view class.
* Split out some of the progress tracker visual logic out into the view.
* Finished partial refactor to push model data into the model class.
* Moved some more view and model logic from controller to model.
This commit is contained in:
Clinton Alexander 2016-07-14 13:47:31 +01:00 committed by Ross Nicoll
parent ba22990938
commit f76c7c9cf9
5 changed files with 713 additions and 610 deletions

View File

@ -22,16 +22,9 @@ import javafx.application.Application
import javafx.application.Platform import javafx.application.Platform
import javafx.beans.property.SimpleDoubleProperty import javafx.beans.property.SimpleDoubleProperty
import javafx.beans.value.WritableValue import javafx.beans.value.WritableValue
import javafx.collections.FXCollections
import javafx.event.EventHandler
import javafx.geometry.Insets import javafx.geometry.Insets
import javafx.geometry.Pos import javafx.geometry.Pos
import javafx.scene.Group
import javafx.scene.Node
import javafx.scene.Scene
import javafx.scene.control.* import javafx.scene.control.*
import javafx.scene.image.Image
import javafx.scene.image.ImageView
import javafx.scene.input.KeyCode import javafx.scene.input.KeyCode
import javafx.scene.input.KeyCodeCombination import javafx.scene.input.KeyCodeCombination
import javafx.scene.layout.* import javafx.scene.layout.*
@ -39,7 +32,6 @@ import javafx.scene.paint.Color
import javafx.scene.shape.Circle import javafx.scene.shape.Circle
import javafx.scene.shape.Line import javafx.scene.shape.Line
import javafx.scene.shape.Polygon import javafx.scene.shape.Polygon
import javafx.scene.text.Font
import javafx.stage.Stage import javafx.stage.Stage
import javafx.util.Duration import javafx.util.Duration
import rx.Scheduler import rx.Scheduler
@ -52,24 +44,20 @@ import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.concurrent.scheduleAtFixedRate import kotlin.concurrent.scheduleAtFixedRate
import kotlin.system.exitProcess import kotlin.system.exitProcess
import com.r3cev.corda.netmap.VisualiserModel.Style
fun <T : Any> WritableValue<T>.keyValue(endValue: T, interpolator: Interpolator = Interpolator.EASE_OUT) = KeyValue(this, endValue, interpolator) fun <T : Any> WritableValue<T>.keyValue(endValue: T, interpolator: Interpolator = Interpolator.EASE_OUT) = KeyValue(this, endValue, interpolator)
// TODO: This code is all horribly ugly. Refactor to use TornadoFX to clean it up. // TODO: This code is all horribly ugly. Refactor to use TornadoFX to clean it up.
class NetworkMapVisualiser : Application() { class NetworkMapVisualiser : Application() {
//========================
// Enums
//========================
enum class NodeType { enum class NodeType {
BANK, SERVICE BANK, SERVICE
} }
enum class Style {
MAP, CIRCLE;
override fun toString(): String {
return name.toLowerCase().capitalize()
}
}
enum class RunPauseButtonLabel { enum class RunPauseButtonLabel {
RUN, PAUSE; RUN, PAUSE;
@ -78,6 +66,9 @@ class NetworkMapVisualiser : Application() {
} }
} }
//========================
// Classes
//========================
sealed class RunningPausedState { sealed class RunningPausedState {
class Running(val tickTimer: TimerTask): RunningPausedState() class Running(val tickTimer: TimerTask): RunningPausedState()
class Paused(): RunningPausedState() class Paused(): RunningPausedState()
@ -91,39 +82,32 @@ class NetworkMapVisualiser : Application() {
} }
} }
val stageWidth = 1024.0 //========================
val stageHeight = 768.0 // Object variables
var defaultZoom = 0.7 //========================
private val view = VisualiserView()
private val model = VisualiserModel()
private lateinit var stage: Stage val timer = Timer()
private lateinit var root: Pane val uiThread: Scheduler = Schedulers.from { Platform.runLater(it) }
val bitmapWidth = 1900.0
val bitmapHeight = 1900.0
var stepDuration = Duration.millis(500.0)
//========================
// Init and shutdown
//========================
init { init {
BriefLogFormatter.initVerbose(InMemoryMessagingNetwork.MESSAGES_LOG_NAME) BriefLogFormatter.initVerbose(InMemoryMessagingNetwork.MESSAGES_LOG_NAME)
} }
var simulation = IRSSimulation(true, false, null) // Manually pumped.
val timer = Timer()
val uiThread: Scheduler = Schedulers.from { Platform.runLater(it) }
var displayStyle: Style = Style.MAP
var bankCount: Int = 0
var serviceCount: Int = 0
override fun start(stage: Stage) { override fun start(stage: Stage) {
this.stage = stage model.view = view
model.presentationMode = "--presentation-mode" in parameters.raw
buildScene(stage) buildScene(stage)
model.displayStyle = if ("--circle" in parameters.raw) { Style.CIRCLE } else { model.displayStyle }
// Update the white-backgrounded label indicating what protocol step it's up to. // Update the white-backgrounded label indicating what protocol step it's up to.
simulation.allProtocolSteps.observeOn(uiThread).subscribe { step: Pair<Simulation.SimulatedNode, ProgressTracker.Change> -> model.simulation.allProtocolSteps.observeOn(uiThread).subscribe { step: Pair<Simulation.SimulatedNode, ProgressTracker.Change> ->
val (node, change) = step val (node, change) = step
val label = nodesToWidgets[node]!!.statusLabel val label = model.nodesToWidgets[node]!!.statusLabel
if (change is ProgressTracker.Change.Position) { if (change is ProgressTracker.Change.Position) {
// Fade in the status label if it's our first step. // Fade in the status label if it's our first step.
if (label.text == "") { if (label.text == "") {
@ -150,23 +134,20 @@ class NetworkMapVisualiser : Application() {
} }
} }
// Fire the message bullets between nodes. // Fire the message bullets between nodes.
simulation.network.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer -> model.simulation.network.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer ->
val senderNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.sender.myAddress) val senderNode: MockNetwork.MockNode = model.simulation.network.addressToNode(msg.sender.myAddress)
val destNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.recipients as SingleMessageRecipient) val destNode: MockNetwork.MockNode = model.simulation.network.addressToNode(msg.recipients as SingleMessageRecipient)
if (transferIsInteresting(msg)) { if (transferIsInteresting(msg)) {
nodesToWidgets[senderNode]!!.pulseAnim.play() model.nodesToWidgets[senderNode]!!.pulseAnim.play()
fireBulletBetweenNodes(senderNode, destNode, "bank", "bank") model.fireBulletBetweenNodes(senderNode, destNode, "bank", "bank")
} }
} }
// Pulse all parties in a trade when the trade completes // Pulse all parties in a trade when the trade completes
simulation.doneSteps.observeOn(uiThread).subscribe { nodes: Collection<Simulation.SimulatedNode> -> model.simulation.doneSteps.observeOn(uiThread).subscribe { nodes: Collection<Simulation.SimulatedNode> ->
nodes.forEach { nodesToWidgets[it]!!.longPulseAnim.play() } nodes.forEach { model.nodesToWidgets[it]!!.longPulseAnim.play() }
} }
if ("--circle" in parameters.raw)
updateDisplayStyle(Style.CIRCLE)
stage.setOnCloseRequest { exitProcess(0) } stage.setOnCloseRequest { exitProcess(0) }
//stage.isMaximized = true //stage.isMaximized = true
stage.show() stage.show()
@ -188,59 +169,15 @@ class NetworkMapVisualiser : Application() {
} }
} }
// -23.2031,29.8406,33.0469,64.3209 //========================
private val mapImage = ImageView(Image(NetworkMapVisualiser::class.java.getResourceAsStream("Europe.jpg"))) // View functions
private var scrollPane: ScrollPane? = null //========================
private lateinit var splitter: SplitPane
private fun buildScene(stage: Stage) { private fun buildScene(stage: Stage) {
NetworkMapVisualiser::class.java.getResourceAsStream("SourceSansPro-Regular.otf").use { view.stage = stage
Font.loadFont(it, 120.0) view.setup(model.runningPausedState, model.displayStyle, model.presentationMode)
} bindSidebar()
when (displayStyle) { bindTopbar()
Style.MAP -> { model.createNodes()
mapImage.fitWidth = bitmapWidth * defaultZoom
mapImage.fitHeight = bitmapHeight * defaultZoom
mapImage.onZoom = EventHandler<javafx.scene.input.ZoomEvent> { event ->
event.consume()
mapImage.fitWidth = mapImage.fitWidth * event.zoomFactor
mapImage.fitHeight = mapImage.fitHeight * event.zoomFactor
repositionNodes()
}
}
Style.CIRCLE -> {
val scaleRatio = Math.min(stageWidth / bitmapWidth, stageHeight / bitmapHeight)
mapImage.fitWidth = bitmapWidth * scaleRatio
mapImage.fitHeight = bitmapHeight * scaleRatio
}
}
val backgroundColor: Color = mapImage.image.pixelReader.getColor(0, 0)
root = Pane(mapImage)
root.background = Background(BackgroundFill(backgroundColor, CornerRadii.EMPTY, Insets.EMPTY))
createNodes()
scrollPane = buildScrollPane(backgroundColor)
val vbox = makeTopBar()
StackPane.setAlignment(vbox, Pos.TOP_CENTER)
// Now build the sidebar
val defaultSplitterPosition = 0.3
splitter = SplitPane(buildSidebar(), scrollPane)
splitter.styleClass += "splitter"
Platform.runLater {
splitter.dividers[0].position = defaultSplitterPosition
}
VBox.setVgrow(splitter, Priority.ALWAYS)
// And the left hide button.
val hideButton = makeHideButton(defaultSplitterPosition)
val screenStack = VBox(vbox, StackPane(splitter, hideButton))
screenStack.styleClass += "root-pane"
stage.scene = Scene(screenStack, backgroundColor)
// Spacebar advances simulation by one step. // Spacebar advances simulation by one step.
stage.scene.accelerators[KeyCodeCombination(KeyCode.SPACE)] = Runnable { onNextInvoked() } stage.scene.accelerators[KeyCodeCombination(KeyCode.SPACE)] = Runnable { onNextInvoked() }
@ -248,178 +185,172 @@ class NetworkMapVisualiser : Application() {
reloadStylesheet(stage) reloadStylesheet(stage)
stage.focusedProperty().addListener { value, old, new -> stage.focusedProperty().addListener { value, old, new ->
if (new) if (new) {
reloadStylesheet(stage) reloadStylesheet(stage)
}
stage.width = 1024.0
stage.height = 768.0
}
private val hideButton = Button("«").apply { styleClass += "hide-sidebar-button" }
fun bindHideButtonPosition() {
hideButton.translateXProperty().unbind()
hideButton.translateXProperty().bind(splitter.dividers[0].positionProperty().multiply(splitter.widthProperty()).subtract(hideButton.widthProperty()))
}
private fun makeHideButton(defaultSplitterPosition: Double): Button {
var hideButtonToggled = false
hideButton.isFocusTraversable = false
hideButton.setOnAction {
if (!hideButtonToggled) {
hideButton.translateXProperty().unbind()
Timeline(
KeyFrame(Duration.millis(500.0),
splitter.dividers[0].positionProperty().keyValue(0.0),
hideButton.translateXProperty().keyValue(0.0),
hideButton.rotateProperty().keyValue(180.0)
)
).play()
} else {
bindHideButtonPosition()
Timeline(
KeyFrame(Duration.millis(500.0),
splitter.dividers[0].positionProperty().keyValue(defaultSplitterPosition),
hideButton.rotateProperty().keyValue(0.0)
)
).play()
} }
hideButtonToggled = !hideButtonToggled
}
bindHideButtonPosition()
StackPane.setAlignment(hideButton, Pos.TOP_LEFT)
return hideButton
}
var started = false
private fun startSimulation() {
if (!started) {
simulation.start()
started = true
} }
} }
private fun makeTopBar(): VBox { private fun bindTopbar() {
val nextButton = Button("Next").apply { view.resetButton.setOnAction({reset()})
styleClass += "button" view.nextButton.setOnAction {
styleClass += "next-button" if (!view.simulateInitialisationCheckbox.isSelected && !model.simulation.networkInitialisationFinished.isDone) {
}
var runningPausedState: RunningPausedState = RunningPausedState.Paused()
val runPauseButton = Button(runningPausedState.buttonLabel.toString()).apply {
styleClass += "button"
styleClass += "run-button"
}
val simulateInitialisationCheckbox = CheckBox("Simulate initialisation")
val resetButton = Button("Reset").apply {
setOnAction {
reset()
}
styleClass += "button"
styleClass += "reset-button"
}
nextButton.setOnAction {
if (!simulateInitialisationCheckbox.isSelected && !simulation.networkInitialisationFinished.isDone) {
skipNetworkInitialisation() skipNetworkInitialisation()
} else { } else {
onNextInvoked() onNextInvoked()
} }
} }
model.simulation.networkInitialisationFinished.then {
simulation.networkInitialisationFinished.then { view.simulateInitialisationCheckbox.isVisible = false
simulateInitialisationCheckbox.isVisible = false
} }
view.runPauseButton.setOnAction {
runPauseButton.setOnAction { val oldRunningPausedState = model.runningPausedState
val oldRunningPausedState = runningPausedState
val newRunningPausedState = when (oldRunningPausedState) { val newRunningPausedState = when (oldRunningPausedState) {
is RunningPausedState.Running -> { is NetworkMapVisualiser.RunningPausedState.Running -> {
oldRunningPausedState.tickTimer.cancel() oldRunningPausedState.tickTimer.cancel()
nextButton.isDisable = false view.nextButton.isDisable = false
resetButton.isDisable = false view.resetButton.isDisable = false
RunningPausedState.Paused() NetworkMapVisualiser.RunningPausedState.Paused()
} }
is RunningPausedState.Paused -> { is NetworkMapVisualiser.RunningPausedState.Paused -> {
val tickTimer = timer.scheduleAtFixedRate(stepDuration.toMillis().toLong(), stepDuration.toMillis().toLong()) { val tickTimer = timer.scheduleAtFixedRate(model.stepDuration.toMillis().toLong(), model.stepDuration.toMillis().toLong()) {
Platform.runLater { Platform.runLater {
onNextInvoked() onNextInvoked()
} }
} }
nextButton.isDisable = true view.nextButton.isDisable = true
resetButton.isDisable = true view.resetButton.isDisable = true
if (!simulateInitialisationCheckbox.isSelected && !simulation.networkInitialisationFinished.isDone) { if (!view.simulateInitialisationCheckbox.isSelected && !model.simulation.networkInitialisationFinished.isDone) {
skipNetworkInitialisation() skipNetworkInitialisation()
} }
RunningPausedState.Running(tickTimer) NetworkMapVisualiser.RunningPausedState.Running(tickTimer)
} }
} }
runPauseButton.text = newRunningPausedState.buttonLabel.toString() view.runPauseButton.text = newRunningPausedState.buttonLabel.toString()
runningPausedState = newRunningPausedState model.runningPausedState = newRunningPausedState
}
view.styleChoice.selectionModel.selectedItemProperty()
.addListener { ov, value, newValue -> model.displayStyle = newValue }
model.simulation.dateChanges.observeOn(uiThread).subscribe { view.dateLabel.text = it.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) }
}
private fun reloadStylesheet(stage: Stage) {
stage.scene.stylesheets.clear()
// Enable hot reload without needing to rebuild.
val mikesCSS = "/Users/mike/Source/R3/r3dlg-prototyping/network-explorer/src/main/resources/com/r3cev/corda/netmap/styles.css"
if (Files.exists(Paths.get(mikesCSS)))
stage.scene.stylesheets.add("file://$mikesCSS")
else
stage.scene.stylesheets.add(NetworkMapVisualiser::class.java.getResource("styles.css").toString())
}
//========================
// Temp
//========================
private fun bindSidebar() {
model.simulation.allProtocolSteps.observeOn(uiThread).subscribe { step: Pair<Simulation.SimulatedNode, ProgressTracker.Change> ->
val (node, change) = step
if (change is ProgressTracker.Change.Position) {
val tracker = change.tracker.topLevelTracker
if (change.newStep == ProgressTracker.DONE) {
if (change.tracker == tracker) {
// Protocol done; schedule it for removal in a few seconds. We batch them up to make nicer
// animations.
println("Protocol done for ${node.info.identity.name}")
model.doneTrackers += tracker
} else {
// Subprotocol is done; ignore it.
}
} else if (!model.trackerBoxes.containsKey(tracker)) {
// New protocol started up; add.
val extraLabel = model.simulation.extraNodeLabels[node]
val label = if (extraLabel != null) "${node.storage.myLegalIdentity.name}: $extraLabel" else node.storage.myLegalIdentity.name
val widget = view.buildProgressTrackerWidget(label, tracker.topLevelTracker)
bindProgressTracketWidget(tracker.topLevelTracker, widget)
println("Added: ${tracker}, ${widget}")
model.trackerBoxes[tracker] = widget.vbox
view.sidebar.children += widget.vbox
}
}
} }
Timer().scheduleAtFixedRate(0, 500) {
val displayStyles = FXCollections.observableArrayList<Style>() Platform.runLater {
Style.values().forEach { displayStyles.add(it) } for (tracker in model.doneTrackers) {
val pane = model.trackerBoxes[tracker]!!
val styleChoice = ChoiceBox(displayStyles).apply { // Slide the other tracker widgets up and over this one.
styleClass += "choice" val slideProp = SimpleDoubleProperty(0.0)
styleClass += "style-choice" slideProp.addListener { obv -> pane.padding = Insets(0.0, 0.0, slideProp.value, 0.0) }
val timeline = Timeline(
KeyFrame(Duration(250.0),
KeyValue(pane.opacityProperty(), 0.0),
KeyValue(slideProp, -pane.height - 50.0) // Subtract the bottom padding gap.
)
)
timeline.setOnFinished {
println("Removed: ${tracker}")
val vbox = model.trackerBoxes.remove(tracker)
view.sidebar.children.remove(vbox)
}
timeline.play()
}
model.doneTrackers.clear()
}
} }
styleChoice.value = displayStyle }
styleChoice.selectionModel.selectedItemProperty()
.addListener { ov, value, newValue -> updateDisplayStyle(newValue) }
val dropShadow = Pane().apply { styleClass += "drop-shadow-pane-horizontal"; minHeight = 8.0 } private fun bindProgressTracketWidget(tracker: ProgressTracker, widget: TrackerWidget) {
val logoImage = ImageView(javaClass.getResource("R3 logo.png").toExternalForm()) val allSteps: List<Pair<Int, ProgressTracker.Step>> = tracker.allSteps
logoImage.fitHeight = 65.0 tracker.changes.observeOn(uiThread).subscribe { step: ProgressTracker.Change ->
logoImage.isPreserveRatio = true val stepHeight = widget.cursorBox.height / allSteps.size
val logoLabel = HBox(logoImage, VBox( if (step is ProgressTracker.Change.Position) {
Label("D I S T R I B U T E D L E D G E R G R O U P").apply { styleClass += "dlg-label" }, // Figure out the index of the new step.
Label("Network Simulator").apply { styleClass += "logo-label" } val curStep = allSteps.indexOfFirst { it.second == step.newStep }
)) // Animate the cursor to the right place.
logoLabel.spacing = 10.0 with(TranslateTransition(Duration(350.0), widget.cursor)) {
HBox.setHgrow(logoLabel, Priority.ALWAYS) fromY = widget.cursor.translateY
logoLabel.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_PREF_SIZE) toY = (curStep * stepHeight) + 22.5
val dateLabel = Label("").apply { styleClass += "date-label" } play()
simulation.dateChanges.observeOn(uiThread).subscribe { dateLabel.text = it.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) } }
} else if (step is ProgressTracker.Change.Structural) {
// Buttons area. In presentation mode there are no controls visible and you must use the keyboard. val new = view.buildProgressTrackerWidget(widget.label.text, tracker)
val hbox = if ("--presentation-mode" in parameters.raw) { val prevWidget = model.trackerBoxes[step.tracker] ?: throw AssertionError("No previous widget for tracker: ${step.tracker}")
HBox(logoLabel, dateLabel).apply { styleClass += "controls-hbox" } val i = (prevWidget.parent as VBox).children.indexOf(model.trackerBoxes[step.tracker])
} else { (prevWidget.parent as VBox).children[i] = new.vbox
HBox(logoLabel, dateLabel, simulateInitialisationCheckbox, runPauseButton, nextButton, resetButton, styleChoice).apply { styleClass += "controls-hbox" } model.trackerBoxes[step.tracker] = new.vbox
}
}
}
//========================
// Controller functions
//========================
var started = false
private fun startSimulation() {
if (!started) {
model.simulation.start()
started = true
} }
hbox.styleClass += "fat-buttons"
hbox.spacing = 20.0
hbox.alignment = Pos.CENTER_RIGHT
hbox.padding = Insets(10.0, 20.0, 10.0, 20.0)
val vbox = VBox(hbox, dropShadow)
vbox.styleClass += "controls-vbox"
vbox.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE)
vbox.setMaxSize(Region.USE_COMPUTED_SIZE, Region.USE_PREF_SIZE)
return vbox
} }
private fun reset() { private fun reset() {
simulation.stop() model.simulation.stop()
simulation = IRSSimulation(true, false, null) model.simulation = IRSSimulation(true, false, null)
started = false started = false
start(this.stage) start(view.stage)
} }
private fun skipNetworkInitialisation() { private fun skipNetworkInitialisation() {
startSimulation() startSimulation()
while (!simulation.networkInitialisationFinished.isDone) { while (!model.simulation.networkInitialisationFinished.isDone) {
iterateSimulation() iterateSimulation()
} }
} }
@ -435,7 +366,7 @@ class NetworkMapVisualiser : Application() {
private fun iterateSimulation() { private fun iterateSimulation() {
// Loop until either we ran out of things to do, or we sent an interesting message. // Loop until either we ran out of things to do, or we sent an interesting message.
while (true) { while (true) {
val transfer: InMemoryMessagingNetwork.MessageTransfer = simulation.iterate() ?: break val transfer: InMemoryMessagingNetwork.MessageTransfer = model.simulation.iterate() ?: break
if (transferIsInteresting(transfer)) if (transferIsInteresting(transfer))
break break
else else
@ -451,384 +382,6 @@ class NetworkMapVisualiser : Application() {
return true return true
} }
private fun buildScrollPane(backgroundColor: Color): ScrollPane {
when (displayStyle) {
Style.MAP -> {
mapImage.fitWidth = bitmapWidth * defaultZoom
mapImage.fitHeight = bitmapHeight * defaultZoom
mapImage.onZoom = EventHandler<javafx.scene.input.ZoomEvent> { event ->
event.consume()
mapImage.fitWidth = mapImage.fitWidth * event.zoomFactor
mapImage.fitHeight = mapImage.fitHeight * event.zoomFactor
repositionNodes()
}
}
Style.CIRCLE -> {
val scaleRatio = Math.min(stageWidth / bitmapWidth, stageHeight / bitmapHeight)
mapImage.fitWidth = bitmapWidth * scaleRatio
mapImage.fitHeight = bitmapHeight * scaleRatio
}
}
return ScrollPane(Group(root)).apply {
when (displayStyle) {
Style.MAP -> {
hvalue = 0.4
vvalue = 0.7
}
Style.CIRCLE -> {
hvalue = 0.0
vvalue = 0.0
}
}
hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
isPannable = true
isFocusTraversable = false
style = "-fx-background-color: " + colorToRgb(backgroundColor)
styleClass += "edge-to-edge"
}
}
private fun colorToRgb(color: Color): String {
val builder = StringBuilder()
builder.append("rgb(")
builder.append(Math.round(color.red * 256))
builder.append(",")
builder.append(Math.round(color.green * 256))
builder.append(",")
builder.append(Math.round(color.blue * 256))
builder.append(")")
return builder.toString()
}
private fun repositionNodes() {
for ((index, bank) in simulation.banks.withIndex()) {
nodesToWidgets[bank]!!.position(index, when (displayStyle) {
Style.MAP -> { node, index -> nodeMapCoords(node) }
Style.CIRCLE -> { node, index -> nodeCircleCoords(NodeType.BANK, index) }
})
}
for ((index, serviceProvider) in (simulation.serviceProviders + simulation.regulators).withIndex()) {
nodesToWidgets[serviceProvider]!!.position(index, when (displayStyle) {
Style.MAP -> { node, index -> nodeMapCoords(node) }
Style.CIRCLE -> { node, index -> nodeCircleCoords(NodeType.SERVICE, index) }
})
}
}
private val trackerBoxes = HashMap<ProgressTracker, Pane>()
private val doneTrackers = ArrayList<ProgressTracker>()
private fun buildSidebar(): Node {
val sidebar = VBox()
sidebar.styleClass += "sidebar"
sidebar.isFillWidth = true
simulation.allProtocolSteps.observeOn(uiThread).subscribe { step: Pair<Simulation.SimulatedNode, ProgressTracker.Change> ->
val (node, change) = step
if (change is ProgressTracker.Change.Position) {
val tracker = change.tracker.topLevelTracker
if (change.newStep == ProgressTracker.DONE) {
if (change.tracker == tracker) {
// Protocol done; schedule it for removal in a few seconds. We batch them up to make nicer
// animations.
println("Protocol done for ${node.info.identity.name}")
doneTrackers += tracker
} else {
// Subprotocol is done; ignore it.
}
} else if (!trackerBoxes.containsKey(tracker)) {
// New protocol started up; add.
val extraLabel = simulation.extraNodeLabels[node]
val label = if (extraLabel != null) "${node.storage.myLegalIdentity.name}: $extraLabel" else node.storage.myLegalIdentity.name
val widget = buildProgressTrackerWidget(label, tracker.topLevelTracker)
trackerBoxes[tracker] = widget
sidebar.children += widget
}
}
}
Timer().scheduleAtFixedRate(0, 500) {
Platform.runLater {
for (tracker in doneTrackers) {
val pane = trackerBoxes[tracker]!!
// Slide the other tracker widgets up and over this one.
val slideProp = SimpleDoubleProperty(0.0)
slideProp.addListener { obv -> pane.padding = Insets(0.0, 0.0, slideProp.value, 0.0) }
val timeline = Timeline(
KeyFrame(Duration(250.0),
KeyValue(pane.opacityProperty(), 0.0),
KeyValue(slideProp, -pane.height - 50.0) // Subtract the bottom padding gap.
)
)
timeline.setOnFinished {
val vbox = trackerBoxes.remove(tracker)
sidebar.children.remove(vbox)
}
timeline.play()
}
doneTrackers.clear()
}
}
val sp = ScrollPane(sidebar)
sp.isFitToWidth = true
sp.isFitToHeight = true
sp.styleClass += "sidebar"
sp.hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
sp.vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
sp.minWidth = 0.0
return sp
}
// TODO: Extract this to a real widget.
private fun buildProgressTrackerWidget(label: String, tracker: ProgressTracker): Pane {
val allSteps: List<Pair<Int, ProgressTracker.Step>> = tracker.allSteps
val stepsBox = VBox().apply {
styleClass += "progress-tracker-widget-steps"
}
for ((indent, step) in allSteps) {
val stepLabel = Label(step.label).apply { padding = Insets(0.0, 0.0, 0.0, indent * 15.0) }
stepsBox.children += StackPane(stepLabel)
}
val arrowSize = 7.0
val cursor = Polygon(-arrowSize, -arrowSize, arrowSize, 0.0, -arrowSize, arrowSize).apply {
styleClass += "progress-tracker-cursor"
}
val cursorBox = Pane(cursor).apply {
styleClass += "progress-tracker-cursor-box"
minWidth = 25.0
}
var curStep = allSteps.indexOfFirst { it.second == tracker.currentStep }
Platform.runLater {
val stepHeight = cursorBox.height / allSteps.size
cursor.translateY = (curStep * stepHeight) + 20.0
}
val vbox: VBox?
tracker.changes.observeOn(uiThread).subscribe { step: ProgressTracker.Change ->
val stepHeight = cursorBox.height / allSteps.size
if (step is ProgressTracker.Change.Position) {
// Figure out the index of the new step.
curStep = allSteps.indexOfFirst { it.second == step.newStep }
// Animate the cursor to the right place.
with(TranslateTransition(Duration(350.0), cursor)) {
fromY = cursor.translateY
toY = (curStep * stepHeight) + 22.5
play()
}
} else if (step is ProgressTracker.Change.Structural) {
val new = buildProgressTrackerWidget(label, tracker)
val prevWidget = trackerBoxes[step.tracker] ?: throw AssertionError("No previous widget for tracker")
val i = (prevWidget.parent as VBox).children.indexOf(trackerBoxes[step.tracker])
(prevWidget.parent as VBox).children[i] = new
trackerBoxes[step.tracker] = new
}
}
HBox.setHgrow(stepsBox, Priority.ALWAYS)
val content = HBox(cursorBox, stepsBox)
// Make the title bar
val title = Label(label).apply { styleClass += "sidebar-title-label" }
StackPane.setAlignment(title, Pos.CENTER_LEFT)
vbox = VBox(StackPane(title), content)
vbox.padding = Insets(0.0, 0.0, 25.0, 0.0)
return vbox
}
fun nodeMapCoords(node: MockNetwork.MockNode): Pair<Double, Double> {
// For an image of the whole world, we use:
// return node.place.coordinate.project(mapImage.fitWidth, mapImage.fitHeight, 85.0511, -85.0511, -180.0, 180.0)
// For Europe, our bounds are: (lng,lat)
// bottom left: -23.2031,29.8406
// top right: 33.0469,64.3209
try {
return node.place.coordinate.project(mapImage.fitWidth, mapImage.fitHeight, 64.3209, 29.8406, -23.2031, 33.0469)
} catch(e: Exception) {
throw Exception("Cannot project ${node.info.identity}", e)
}
}
fun nodeCircleCoords(type: NodeType, index: Int): Pair<Double, Double> {
val stepRad: Double = when(type) {
NodeType.BANK -> 2 * Math.PI / bankCount
NodeType.SERVICE -> (2 * Math.PI / serviceCount)
}
val tangentRad: Double = stepRad * index + when(type) {
NodeType.BANK -> 0.0
NodeType.SERVICE -> Math.PI / 2
}
val radius = when (type) {
NodeType.BANK -> Math.min(stageWidth, stageHeight) / 3.5
NodeType.SERVICE -> Math.min(stageWidth, stageHeight) / 8
}
val xOffset = -220
val yOffset = -80
val circleX = stageWidth / 2 + xOffset
val circleY = stageHeight / 2 + yOffset
val x: Double = radius * Math.cos(tangentRad) + circleX;
val y: Double = radius * Math.sin(tangentRad) + circleY;
return Pair(x, y)
}
inner class NodeWidget(val node: MockNetwork.MockNode, val innerDot: Circle, val outerDot: Circle, val longPulseDot: Circle,
val pulseAnim: Animation, val longPulseAnim: Animation,
val nameLabel: Label, val statusLabel: Label) {
fun position(index: Int, nodeCoords: (node: MockNetwork.MockNode, index: Int) -> Pair<Double, Double>) {
val (x, y) = nodeCoords(node, index)
innerDot.centerX = x
innerDot.centerY = y
outerDot.centerX = x
outerDot.centerY = y
longPulseDot.centerX = x
longPulseDot.centerY = y
(nameLabel.parent as StackPane).relocate(x - 270.0, y - 10.0)
(statusLabel.parent as StackPane).relocate(x + 20.0, y - 10.0)
}
}
private val nodesToWidgets = HashMap<MockNetwork.MockNode, NodeWidget>()
private fun createNodes() {
bankCount = simulation.banks.size
serviceCount = simulation.serviceProviders.size + simulation.regulators.size
for ((index, bank) in simulation.banks.withIndex()) {
nodesToWidgets[bank] = makeNodeWidget(bank, "bank", bank.configuration.myLegalName, NodeType.BANK, index)
}
for ((index, service) in simulation.serviceProviders.withIndex()) {
nodesToWidgets[service] = makeNodeWidget(service, "network-service", service.configuration.myLegalName, NodeType.SERVICE, index)
}
for ((index, service) in simulation.regulators.withIndex()) {
nodesToWidgets[service] = makeNodeWidget(service, "regulator", service.configuration.myLegalName, NodeType.SERVICE, index + simulation.serviceProviders.size)
}
}
fun makeNodeWidget(forNode: MockNetwork.MockNode, type: String, label: String = "Bank of Bologna",
nodeType: NodeType, index: Int): NodeWidget {
fun emitRadarPulse(initialRadius: Double, targetRadius: Double, duration: Double): Pair<Circle, Animation> {
val pulse = Circle(initialRadius).apply {
styleClass += "node-$type"
styleClass += "node-circle-pulse"
}
val animation = Timeline(
KeyFrame(Duration.seconds(0.0),
pulse.radiusProperty().keyValue(initialRadius),
pulse.opacityProperty().keyValue(1.0)
),
KeyFrame(Duration.seconds(duration),
pulse.radiusProperty().keyValue(targetRadius),
pulse.opacityProperty().keyValue(0.0)
)
)
return Pair(pulse, animation)
}
val innerDot = Circle(10.0).apply {
styleClass += "node-$type"
styleClass += "node-circle-inner"
}
val (outerDot, pulseAnim) = emitRadarPulse(10.0, 50.0, 0.45)
val (longPulseOuterDot, longPulseAnim) = emitRadarPulse(10.0, 100.0, 1.45)
root.children += outerDot
root.children += longPulseOuterDot
root.children += innerDot
val nameLabel = Label(label)
val nameLabelRect = StackPane(nameLabel).apply {
styleClass += "node-label"
alignment = Pos.CENTER_RIGHT
// This magic min width depends on the longest label of all nodes we may have, which we aren't calculating.
// TODO: Dynamically adjust it depending on the longest label to display.
minWidth = 250.0
}
root.children += nameLabelRect
val statusLabel = Label("")
val statusLabelRect = StackPane(statusLabel).apply { styleClass += "node-status-label" }
root.children += statusLabelRect
val widget = NodeWidget(forNode, innerDot, outerDot, longPulseOuterDot, pulseAnim, longPulseAnim, nameLabel, statusLabel)
when (displayStyle) {
Style.CIRCLE -> widget.position(index, { node, index -> nodeCircleCoords(nodeType, index) } )
Style.MAP -> widget.position(index, { node, index -> nodeMapCoords(node) })
}
return widget
}
private fun fireBulletBetweenNodes(senderNode: MockNetwork.MockNode, destNode: MockNetwork.MockNode, startType: String, endType: String) {
val sx = nodesToWidgets[senderNode]!!.innerDot.centerX
val sy = nodesToWidgets[senderNode]!!.innerDot.centerY
val dx = nodesToWidgets[destNode]!!.innerDot.centerX
val dy = nodesToWidgets[destNode]!!.innerDot.centerY
val bullet = Circle(3.0)
bullet.styleClass += "bullet"
bullet.styleClass += "connection-$startType-to-$endType"
with(TranslateTransition(stepDuration, bullet)) {
fromX = sx
fromY = sy
toX = dx
toY = dy
setOnFinished {
// For some reason removing/adding the bullet nodes causes an annoying 1px shift in the map view, so
// to avoid visual distraction we just deliberately leak the bullet node here. Obviously this is a
// memory leak that would break long term usage.
//
// TODO: Find root cause and fix.
//
// root.children.remove(bullet)
bullet.isVisible = false
}
play()
}
val line = Line(sx, sy, dx, dy).apply { styleClass += "message-line" }
// Fade in quick, then fade out slow.
with(FadeTransition(stepDuration.divide(5.0), line)) {
fromValue = 0.0
toValue = 1.0
play()
setOnFinished {
with(FadeTransition(stepDuration.multiply(6.0), line)) { fromValue = 1.0; toValue = 0.0; play() }
}
}
root.children.add(1, line)
root.children.add(bullet)
}
private fun reloadStylesheet(stage: Stage) {
stage.scene.stylesheets.clear()
// Enable hot reload without needing to rebuild.
val mikesCSS = "/Users/mike/Source/R3/r3dlg-prototyping/network-explorer/src/main/resources/com/r3cev/corda/netmap/styles.css"
if (Files.exists(Paths.get(mikesCSS)))
stage.scene.stylesheets.add("file://$mikesCSS")
else
stage.scene.stylesheets.add(NetworkMapVisualiser::class.java.getResource("styles.css").toString())
}
/**
* Update the current display style. MUST only be called on the UI
* thread.
*/
fun updateDisplayStyle(style: Style) {
displayStyle = style
requireNotNull(splitter)
splitter.items.remove(scrollPane!!)
val backgroundColor: Color = mapImage.image.pixelReader.getColor(0, 0)
scrollPane = buildScrollPane(backgroundColor)
splitter.items.add(scrollPane!!)
splitter.dividers[0].position = 0.3
repositionNodes()
bindHideButtonPosition()
mapImage.isVisible = when (style) {
Style.MAP -> true
Style.CIRCLE -> false
}
stage.scene.accelerators[KeyCodeCombination(KeyCode.SPACE)] = Runnable { onNextInvoked() }
// TODO: Can any current bullets be re-routed in flight?
}
} }
fun main(args: Array<String>) { fun main(args: Array<String>) {

View File

@ -0,0 +1,222 @@
package com.r3cev.corda.netmap
import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.node.internal.testing.IRSSimulation
import com.r3corda.node.internal.testing.MockNetwork
import javafx.animation.*
import javafx.geometry.Pos
import javafx.scene.control.Label
import javafx.scene.layout.Pane
import javafx.scene.layout.StackPane
import javafx.scene.shape.Circle
import javafx.scene.shape.Line
import javafx.util.Duration
import java.util.*
class VisualiserModel() {
enum class Style {
MAP, CIRCLE;
override fun toString(): String {
return name.toLowerCase().capitalize()
}
}
inner class NodeWidget(val node: MockNetwork.MockNode, val innerDot: Circle, val outerDot: Circle, val longPulseDot: Circle,
val pulseAnim: Animation, val longPulseAnim: Animation,
val nameLabel: Label, val statusLabel: Label) {
fun position(index: Int, nodeCoords: (node: MockNetwork.MockNode, index: Int) -> Pair<Double, Double>) {
val (x, y) = nodeCoords(node, index)
innerDot.centerX = x
innerDot.centerY = y
outerDot.centerX = x
outerDot.centerY = y
longPulseDot.centerX = x
longPulseDot.centerY = y
(nameLabel.parent as StackPane).relocate(x - 270.0, y - 10.0)
(statusLabel.parent as StackPane).relocate(x + 20.0, y - 10.0)
}
}
internal lateinit var view: VisualiserView
var presentationMode: Boolean = false
var simulation = IRSSimulation(true, false, null) // Manually pumped.
val trackerBoxes = HashMap<ProgressTracker, Pane>()
val doneTrackers = ArrayList<ProgressTracker>()
val nodesToWidgets = HashMap<MockNetwork.MockNode, NodeWidget>()
var bankCount: Int = 0
var serviceCount: Int = 0
var stepDuration = Duration.millis(500.0)
var runningPausedState: NetworkMapVisualiser.RunningPausedState = NetworkMapVisualiser.RunningPausedState.Paused()
var displayStyle: Style = Style.MAP
set(value) {
field = value
view.updateDisplayStyle(value)
repositionNodes()
view.bindHideButtonPosition()
}
fun repositionNodes() {
for ((index, bank) in simulation.banks.withIndex()) {
nodesToWidgets[bank]!!.position(index, when (displayStyle) {
Style.MAP -> { node, index -> nodeMapCoords(node) }
Style.CIRCLE -> { node, index -> nodeCircleCoords(NetworkMapVisualiser.NodeType.BANK, index) }
})
}
for ((index, serviceProvider) in (simulation.serviceProviders + simulation.regulators).withIndex()) {
nodesToWidgets[serviceProvider]!!.position(index, when (displayStyle) {
Style.MAP -> { node, index -> nodeMapCoords(node) }
Style.CIRCLE -> { node, index -> nodeCircleCoords(NetworkMapVisualiser.NodeType.SERVICE, index) }
})
}
}
fun nodeMapCoords(node: MockNetwork.MockNode): Pair<Double, Double> {
// For an image of the whole world, we use:
// return node.place.coordinate.project(mapImage.fitWidth, mapImage.fitHeight, 85.0511, -85.0511, -180.0, 180.0)
// For Europe, our bounds are: (lng,lat)
// bottom left: -23.2031,29.8406
// top right: 33.0469,64.3209
try {
return node.place.coordinate.project(view.mapImage.fitWidth, view.mapImage.fitHeight, 64.3209, 29.8406, -23.2031, 33.0469)
} catch(e: Exception) {
throw Exception("Cannot project ${node.info.identity}", e)
}
}
fun nodeCircleCoords(type: NetworkMapVisualiser.NodeType, index: Int): Pair<Double, Double> {
val stepRad: Double = when(type) {
NetworkMapVisualiser.NodeType.BANK -> 2 * Math.PI / bankCount
NetworkMapVisualiser.NodeType.SERVICE -> (2 * Math.PI / serviceCount)
}
val tangentRad: Double = stepRad * index + when(type) {
NetworkMapVisualiser.NodeType.BANK -> 0.0
NetworkMapVisualiser.NodeType.SERVICE -> Math.PI / 2
}
val radius = when (type) {
NetworkMapVisualiser.NodeType.BANK -> Math.min(view.stageWidth, view.stageHeight) / 3.5
NetworkMapVisualiser.NodeType.SERVICE -> Math.min(view.stageWidth, view.stageHeight) / 8
}
val xOffset = -220
val yOffset = -80
val circleX = view.stageWidth / 2 + xOffset
val circleY = view.stageHeight / 2 + yOffset
val x: Double = radius * Math.cos(tangentRad) + circleX;
val y: Double = radius * Math.sin(tangentRad) + circleY;
return Pair(x, y)
}
fun createNodes() {
bankCount = simulation.banks.size
serviceCount = simulation.serviceProviders.size + simulation.regulators.size
for ((index, bank) in simulation.banks.withIndex()) {
nodesToWidgets[bank] = makeNodeWidget(bank, "bank", bank.configuration.myLegalName, NetworkMapVisualiser.NodeType.BANK, index)
}
for ((index, service) in simulation.serviceProviders.withIndex()) {
nodesToWidgets[service] = makeNodeWidget(service, "network-service", service.configuration.myLegalName, NetworkMapVisualiser.NodeType.SERVICE, index)
}
for ((index, service) in simulation.regulators.withIndex()) {
nodesToWidgets[service] = makeNodeWidget(service, "regulator", service.configuration.myLegalName, NetworkMapVisualiser.NodeType.SERVICE, index + simulation.serviceProviders.size)
}
}
fun makeNodeWidget(forNode: MockNetwork.MockNode, type: String, label: String = "Bank of Bologna",
nodeType: NetworkMapVisualiser.NodeType, index: Int): NodeWidget {
fun emitRadarPulse(initialRadius: Double, targetRadius: Double, duration: Double): Pair<Circle, Animation> {
val pulse = Circle(initialRadius).apply {
styleClass += "node-$type"
styleClass += "node-circle-pulse"
}
val animation = Timeline(
KeyFrame(Duration.seconds(0.0),
pulse.radiusProperty().keyValue(initialRadius),
pulse.opacityProperty().keyValue(1.0)
),
KeyFrame(Duration.seconds(duration),
pulse.radiusProperty().keyValue(targetRadius),
pulse.opacityProperty().keyValue(0.0)
)
)
return Pair(pulse, animation)
}
val innerDot = Circle(10.0).apply {
styleClass += "node-$type"
styleClass += "node-circle-inner"
}
val (outerDot, pulseAnim) = emitRadarPulse(10.0, 50.0, 0.45)
val (longPulseOuterDot, longPulseAnim) = emitRadarPulse(10.0, 100.0, 1.45)
view.root.children += outerDot
view.root.children += longPulseOuterDot
view.root.children += innerDot
val nameLabel = Label(label)
val nameLabelRect = StackPane(nameLabel).apply {
styleClass += "node-label"
alignment = Pos.CENTER_RIGHT
// This magic min width depends on the longest label of all nodes we may have, which we aren't calculating.
// TODO: Dynamically adjust it depending on the longest label to display.
minWidth = 250.0
}
view.root.children += nameLabelRect
val statusLabel = Label("")
val statusLabelRect = StackPane(statusLabel).apply { styleClass += "node-status-label" }
view.root.children += statusLabelRect
val widget = NodeWidget(forNode, innerDot, outerDot, longPulseOuterDot, pulseAnim, longPulseAnim, nameLabel, statusLabel)
when (displayStyle) {
Style.CIRCLE -> widget.position(index, { node, index -> nodeCircleCoords(nodeType, index) } )
Style.MAP -> widget.position(index, { node, index -> nodeMapCoords(node) })
}
return widget
}
fun fireBulletBetweenNodes(senderNode: MockNetwork.MockNode, destNode: MockNetwork.MockNode, startType: String, endType: String) {
val sx = nodesToWidgets[senderNode]!!.innerDot.centerX
val sy = nodesToWidgets[senderNode]!!.innerDot.centerY
val dx = nodesToWidgets[destNode]!!.innerDot.centerX
val dy = nodesToWidgets[destNode]!!.innerDot.centerY
val bullet = Circle(3.0)
bullet.styleClass += "bullet"
bullet.styleClass += "connection-$startType-to-$endType"
with(TranslateTransition(stepDuration, bullet)) {
fromX = sx
fromY = sy
toX = dx
toY = dy
setOnFinished {
// For some reason removing/adding the bullet nodes causes an annoying 1px shift in the map view, so
// to avoid visual distraction we just deliberately leak the bullet node here. Obviously this is a
// memory leak that would break long term usage.
//
// TODO: Find root cause and fix.
//
// root.children.remove(bullet)
bullet.isVisible = false
}
play()
}
val line = Line(sx, sy, dx, dy).apply { styleClass += "message-line" }
// Fade in quick, then fade out slow.
with(FadeTransition(stepDuration.divide(5.0), line)) {
fromValue = 0.0
toValue = 1.0
play()
setOnFinished {
with(FadeTransition(stepDuration.multiply(6.0), line)) { fromValue = 1.0; toValue = 0.0; play() }
}
}
view.root.children.add(1, line)
view.root.children.add(bullet)
}
}

View File

@ -0,0 +1,18 @@
package com.r3cev.corda.netmap
import javafx.scene.paint.Color
internal
fun colorToRgb(color: Color): String {
val builder = StringBuilder()
builder.append("rgb(")
builder.append(Math.round(color.red * 256))
builder.append(",")
builder.append(Math.round(color.green * 256))
builder.append(",")
builder.append(Math.round(color.blue * 256))
builder.append(")")
return builder.toString()
}

View File

@ -0,0 +1,309 @@
package com.r3cev.corda.netmap
import com.r3corda.core.utilities.ProgressTracker
import javafx.animation.KeyFrame
import javafx.animation.Timeline
import javafx.application.Platform
import javafx.collections.FXCollections
import javafx.event.EventHandler
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Group
import javafx.scene.Node
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.image.Image
import javafx.scene.image.ImageView
import javafx.scene.input.ZoomEvent
import javafx.scene.layout.*
import javafx.scene.paint.Color
import javafx.scene.shape.Polygon
import javafx.scene.text.Font
import javafx.stage.Stage
import javafx.util.Duration
import com.r3cev.corda.netmap.VisualiserModel.Style
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyCodeCombination
data class TrackerWidget(val vbox: VBox, val cursorBox: Pane, val label: Label, val cursor: Polygon)
internal class VisualiserView() {
// Structural elements
lateinit var root: Pane
lateinit var stage: Stage
lateinit var splitter: SplitPane
lateinit var sidebar: VBox
lateinit var resetButton: Button
lateinit var nextButton: Button
lateinit var runPauseButton: Button
lateinit var simulateInitialisationCheckbox: CheckBox
lateinit var styleChoice: ChoiceBox<Style>
var dateLabel = Label("")
var scrollPane: ScrollPane? = null
var hideButton = Button("«").apply { styleClass += "hide-sidebar-button" }
// Content
// -23.2031,29.8406,33.0469,64.3209
val mapImage = ImageView(Image(NetworkMapVisualiser::class.java.getResourceAsStream("Europe.jpg")))
// Display properties
val backgroundColor: Color = mapImage.image.pixelReader.getColor(0, 0)
val stageWidth = 1024.0
val stageHeight = 768.0
var defaultZoom = 0.7
val bitmapWidth = 1900.0
val bitmapHeight = 1900.0
fun setup(runningPausedState: NetworkMapVisualiser.RunningPausedState,
displayStyle: Style,
presentationMode: Boolean) {
NetworkMapVisualiser::class.java.getResourceAsStream("SourceSansPro-Regular.otf").use {
Font.loadFont(it, 120.0)
}
if(displayStyle == Style.MAP) {
mapImage.onZoom = EventHandler<javafx.scene.input.ZoomEvent> { event ->
event.consume()
mapImage.fitWidth = mapImage.fitWidth * event.zoomFactor
mapImage.fitHeight = mapImage.fitHeight * event.zoomFactor
//repositionNodes()
}
}
scaleMap(displayStyle);
root = Pane(mapImage)
root.background = Background(BackgroundFill(backgroundColor, CornerRadii.EMPTY, Insets.EMPTY))
scrollPane = buildScrollPane(backgroundColor, displayStyle)
val vbox = makeTopBar(runningPausedState, displayStyle, presentationMode)
StackPane.setAlignment(vbox, Pos.TOP_CENTER)
// Now build the sidebar
val defaultSplitterPosition = 0.3
splitter = SplitPane(makeSidebar(), scrollPane)
splitter.styleClass += "splitter"
Platform.runLater {
splitter.dividers[0].position = defaultSplitterPosition
}
VBox.setVgrow(splitter, Priority.ALWAYS)
// And the left hide button.
hideButton = makeHideButton(defaultSplitterPosition)
val screenStack = VBox(vbox, StackPane(splitter, hideButton))
screenStack.styleClass += "root-pane"
stage.scene = Scene(screenStack, backgroundColor)
stage.width = 1024.0
stage.height = 768.0
}
fun buildScrollPane(backgroundColor: Color, displayStyle: Style): ScrollPane {
when (displayStyle) {
Style.MAP -> {
mapImage.fitWidth = bitmapWidth * defaultZoom
mapImage.fitHeight = bitmapHeight * defaultZoom
mapImage.onZoom = EventHandler<ZoomEvent> { event ->
event.consume()
mapImage.fitWidth = mapImage.fitWidth * event.zoomFactor
mapImage.fitHeight = mapImage.fitHeight * event.zoomFactor
}
}
Style.CIRCLE -> {
val scaleRatio = Math.min(stageWidth / bitmapWidth, stageHeight / bitmapHeight)
mapImage.fitWidth = bitmapWidth * scaleRatio
mapImage.fitHeight = bitmapHeight * scaleRatio
}
}
return ScrollPane(Group(root)).apply {
when (displayStyle) {
Style.MAP -> {
hvalue = 0.4
vvalue = 0.7
}
Style.CIRCLE -> {
hvalue = 0.0
vvalue = 0.0
}
}
hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
isPannable = true
isFocusTraversable = false
style = "-fx-background-color: " + colorToRgb(backgroundColor)
styleClass += "edge-to-edge"
}
}
fun makeHideButton(defaultSplitterPosition: Double): Button {
var hideButtonToggled = false
hideButton.isFocusTraversable = false
hideButton.setOnAction {
if (!hideButtonToggled) {
hideButton.translateXProperty().unbind()
Timeline(
KeyFrame(Duration.millis(500.0),
splitter.dividers[0].positionProperty().keyValue(0.0),
hideButton.translateXProperty().keyValue(0.0),
hideButton.rotateProperty().keyValue(180.0)
)
).play()
} else {
bindHideButtonPosition()
Timeline(
KeyFrame(Duration.millis(500.0),
splitter.dividers[0].positionProperty().keyValue(defaultSplitterPosition),
hideButton.rotateProperty().keyValue(0.0)
)
).play()
}
hideButtonToggled = !hideButtonToggled
}
bindHideButtonPosition()
StackPane.setAlignment(hideButton, Pos.TOP_LEFT)
return hideButton
}
fun bindHideButtonPosition() {
hideButton.translateXProperty().unbind()
hideButton.translateXProperty().bind(splitter.dividers[0].positionProperty().multiply(splitter.widthProperty()).subtract(hideButton.widthProperty()))
}
fun scaleMap(displayStyle: Style) {
when (displayStyle) {
Style.MAP -> {
mapImage.fitWidth = bitmapWidth * defaultZoom
mapImage.fitHeight = bitmapHeight * defaultZoom
}
Style.CIRCLE -> {
val scaleRatio = Math.min(stageWidth / bitmapWidth, stageHeight / bitmapHeight)
mapImage.fitWidth = bitmapWidth * scaleRatio
mapImage.fitHeight = bitmapHeight * scaleRatio
}
}
}
fun makeSidebar(): Node {
sidebar = VBox()
sidebar.styleClass += "sidebar"
sidebar.isFillWidth = true
val sp = ScrollPane(sidebar)
sp.isFitToWidth = true
sp.isFitToHeight = true
sp.styleClass += "sidebar"
sp.hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
sp.vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
sp.minWidth = 0.0
return sp
}
fun makeTopBar(runningPausedState: NetworkMapVisualiser.RunningPausedState,
displayStyle: Style,
presentationMode: Boolean): VBox {
nextButton = Button("Next").apply {
styleClass += "button"
styleClass += "next-button"
}
runPauseButton = Button(runningPausedState.buttonLabel.toString()).apply {
styleClass += "button"
styleClass += "run-button"
}
simulateInitialisationCheckbox = CheckBox("Simulate initialisation")
resetButton = Button("Reset").apply {
styleClass += "button"
styleClass += "reset-button"
}
val displayStyles = FXCollections.observableArrayList<Style>()
Style.values().forEach { displayStyles.add(it) }
styleChoice = ChoiceBox(displayStyles).apply {
styleClass += "choice"
styleClass += "style-choice"
}
styleChoice.value = displayStyle
val dropShadow = Pane().apply { styleClass += "drop-shadow-pane-horizontal"; minHeight = 8.0 }
val logoImage = ImageView(javaClass.getResource("R3 logo.png").toExternalForm())
logoImage.fitHeight = 65.0
logoImage.isPreserveRatio = true
val logoLabel = HBox(logoImage, VBox(
Label("D I S T R I B U T E D L E D G E R G R O U P").apply { styleClass += "dlg-label" },
Label("Network Simulator").apply { styleClass += "logo-label" }
))
logoLabel.spacing = 10.0
HBox.setHgrow(logoLabel, Priority.ALWAYS)
logoLabel.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_PREF_SIZE)
dateLabel = Label("").apply { styleClass += "date-label" }
// Buttons area. In presentation mode there are no controls visible and you must use the keyboard.
val hbox = if (presentationMode) {
HBox(logoLabel, dateLabel).apply { styleClass += "controls-hbox" }
} else {
HBox(logoLabel, dateLabel, simulateInitialisationCheckbox, runPauseButton, nextButton, resetButton, styleChoice).apply { styleClass += "controls-hbox" }
}
hbox.styleClass += "fat-buttons"
hbox.spacing = 20.0
hbox.alignment = Pos.CENTER_RIGHT
hbox.padding = Insets(10.0, 20.0, 10.0, 20.0)
val vbox = VBox(hbox, dropShadow)
vbox.styleClass += "controls-vbox"
vbox.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE)
vbox.setMaxSize(Region.USE_COMPUTED_SIZE, Region.USE_PREF_SIZE)
return vbox
}
// TODO: Extract this to a real widget.
fun buildProgressTrackerWidget(label: String, tracker: ProgressTracker): TrackerWidget {
val allSteps: List<Pair<Int, ProgressTracker.Step>> = tracker.allSteps
val stepsBox = VBox().apply {
styleClass += "progress-tracker-widget-steps"
}
for ((indent, step) in allSteps) {
val stepLabel = Label(step.label).apply { padding = Insets(0.0, 0.0, 0.0, indent * 15.0) }
stepsBox.children += StackPane(stepLabel)
}
val arrowSize = 7.0
val cursor = Polygon(-arrowSize, -arrowSize, arrowSize, 0.0, -arrowSize, arrowSize).apply {
styleClass += "progress-tracker-cursor"
}
val cursorBox = Pane(cursor).apply {
styleClass += "progress-tracker-cursor-box"
minWidth = 25.0
}
val curStep = allSteps.indexOfFirst { it.second == tracker.currentStep }
Platform.runLater {
val stepHeight = cursorBox.height / allSteps.size
cursor.translateY = (curStep * stepHeight) + 20.0
}
val vbox: VBox?
HBox.setHgrow(stepsBox, Priority.ALWAYS)
val content = HBox(cursorBox, stepsBox)
// Make the title bar
val title = Label(label).apply { styleClass += "sidebar-title-label" }
StackPane.setAlignment(title, Pos.CENTER_LEFT)
vbox = VBox(StackPane(title), content)
vbox.padding = Insets(0.0, 0.0, 25.0, 0.0)
return TrackerWidget(vbox, cursorBox, title, cursor)
}
/**
* Update the current display style. MUST only be called on the UI
* thread.
*/
fun updateDisplayStyle(displayStyle: Style) {
requireNotNull(splitter)
splitter.items.remove(scrollPane!!)
scrollPane = buildScrollPane(backgroundColor, displayStyle)
splitter.items.add(scrollPane!!)
splitter.dividers[0].position = 0.3
mapImage.isVisible = when (displayStyle) {
Style.MAP -> true
Style.CIRCLE -> false
}
// TODO: Can any current bullets be re-routed in flight?
}
}

View File

@ -5,4 +5,5 @@ include 'core'
include 'node' include 'node'
include 'client' include 'client'
include 'experimental' include 'experimental'
include 'test-utils' include 'test-utils'
include 'network-map-visualiser'