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.beans.property.SimpleDoubleProperty
import javafx.beans.value.WritableValue
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.KeyCode
import javafx.scene.input.KeyCodeCombination
import javafx.scene.layout.*
@ -39,7 +32,6 @@ import javafx.scene.paint.Color
import javafx.scene.shape.Circle
import javafx.scene.shape.Line
import javafx.scene.shape.Polygon
import javafx.scene.text.Font
import javafx.stage.Stage
import javafx.util.Duration
import rx.Scheduler
@ -52,24 +44,20 @@ import java.util.*
import kotlin.concurrent.schedule
import kotlin.concurrent.scheduleAtFixedRate
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)
// TODO: This code is all horribly ugly. Refactor to use TornadoFX to clean it up.
class NetworkMapVisualiser : Application() {
//========================
// Enums
//========================
enum class NodeType {
BANK, SERVICE
}
enum class Style {
MAP, CIRCLE;
override fun toString(): String {
return name.toLowerCase().capitalize()
}
}
enum class RunPauseButtonLabel {
RUN, PAUSE;
@ -78,6 +66,9 @@ class NetworkMapVisualiser : Application() {
}
}
//========================
// Classes
//========================
sealed class RunningPausedState {
class Running(val tickTimer: TimerTask): RunningPausedState()
class Paused(): RunningPausedState()
@ -91,39 +82,32 @@ class NetworkMapVisualiser : Application() {
}
}
val stageWidth = 1024.0
val stageHeight = 768.0
var defaultZoom = 0.7
//========================
// Object variables
//========================
private val view = VisualiserView()
private val model = VisualiserModel()
private lateinit var stage: Stage
private lateinit var root: Pane
val bitmapWidth = 1900.0
val bitmapHeight = 1900.0
var stepDuration = Duration.millis(500.0)
val timer = Timer()
val uiThread: Scheduler = Schedulers.from { Platform.runLater(it) }
//========================
// Init and shutdown
//========================
init {
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) {
this.stage = stage
model.view = view
model.presentationMode = "--presentation-mode" in parameters.raw
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.
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 label = nodesToWidgets[node]!!.statusLabel
val label = model.nodesToWidgets[node]!!.statusLabel
if (change is ProgressTracker.Change.Position) {
// Fade in the status label if it's our first step.
if (label.text == "") {
@ -150,23 +134,20 @@ class NetworkMapVisualiser : Application() {
}
}
// Fire the message bullets between nodes.
simulation.network.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer ->
val senderNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.sender.myAddress)
val destNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.recipients as SingleMessageRecipient)
model.simulation.network.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer ->
val senderNode: MockNetwork.MockNode = model.simulation.network.addressToNode(msg.sender.myAddress)
val destNode: MockNetwork.MockNode = model.simulation.network.addressToNode(msg.recipients as SingleMessageRecipient)
if (transferIsInteresting(msg)) {
nodesToWidgets[senderNode]!!.pulseAnim.play()
fireBulletBetweenNodes(senderNode, destNode, "bank", "bank")
model.nodesToWidgets[senderNode]!!.pulseAnim.play()
model.fireBulletBetweenNodes(senderNode, destNode, "bank", "bank")
}
}
// Pulse all parties in a trade when the trade completes
simulation.doneSteps.observeOn(uiThread).subscribe { nodes: Collection<Simulation.SimulatedNode> ->
nodes.forEach { nodesToWidgets[it]!!.longPulseAnim.play() }
model.simulation.doneSteps.observeOn(uiThread).subscribe { nodes: Collection<Simulation.SimulatedNode> ->
nodes.forEach { model.nodesToWidgets[it]!!.longPulseAnim.play() }
}
if ("--circle" in parameters.raw)
updateDisplayStyle(Style.CIRCLE)
stage.setOnCloseRequest { exitProcess(0) }
//stage.isMaximized = true
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")))
private var scrollPane: ScrollPane? = null
private lateinit var splitter: SplitPane
//========================
// View functions
//========================
private fun buildScene(stage: Stage) {
NetworkMapVisualiser::class.java.getResourceAsStream("SourceSansPro-Regular.otf").use {
Font.loadFont(it, 120.0)
}
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
}
}
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)
view.stage = stage
view.setup(model.runningPausedState, model.displayStyle, model.presentationMode)
bindSidebar()
bindTopbar()
model.createNodes()
// Spacebar advances simulation by one step.
stage.scene.accelerators[KeyCodeCombination(KeyCode.SPACE)] = Runnable { onNextInvoked() }
@ -248,178 +185,172 @@ class NetworkMapVisualiser : Application() {
reloadStylesheet(stage)
stage.focusedProperty().addListener { value, old, new ->
if (new)
if (new) {
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 {
val nextButton = Button("Next").apply {
styleClass += "button"
styleClass += "next-button"
}
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) {
private fun bindTopbar() {
view.resetButton.setOnAction({reset()})
view.nextButton.setOnAction {
if (!view.simulateInitialisationCheckbox.isSelected && !model.simulation.networkInitialisationFinished.isDone) {
skipNetworkInitialisation()
} else {
onNextInvoked()
}
}
simulation.networkInitialisationFinished.then {
simulateInitialisationCheckbox.isVisible = false
model.simulation.networkInitialisationFinished.then {
view.simulateInitialisationCheckbox.isVisible = false
}
runPauseButton.setOnAction {
val oldRunningPausedState = runningPausedState
view.runPauseButton.setOnAction {
val oldRunningPausedState = model.runningPausedState
val newRunningPausedState = when (oldRunningPausedState) {
is RunningPausedState.Running -> {
is NetworkMapVisualiser.RunningPausedState.Running -> {
oldRunningPausedState.tickTimer.cancel()
nextButton.isDisable = false
resetButton.isDisable = false
view.nextButton.isDisable = false
view.resetButton.isDisable = false
RunningPausedState.Paused()
NetworkMapVisualiser.RunningPausedState.Paused()
}
is RunningPausedState.Paused -> {
val tickTimer = timer.scheduleAtFixedRate(stepDuration.toMillis().toLong(), stepDuration.toMillis().toLong()) {
is NetworkMapVisualiser.RunningPausedState.Paused -> {
val tickTimer = timer.scheduleAtFixedRate(model.stepDuration.toMillis().toLong(), model.stepDuration.toMillis().toLong()) {
Platform.runLater {
onNextInvoked()
}
}
nextButton.isDisable = true
resetButton.isDisable = true
view.nextButton.isDisable = true
view.resetButton.isDisable = true
if (!simulateInitialisationCheckbox.isSelected && !simulation.networkInitialisationFinished.isDone) {
if (!view.simulateInitialisationCheckbox.isSelected && !model.simulation.networkInitialisationFinished.isDone) {
skipNetworkInitialisation()
}
RunningPausedState.Running(tickTimer)
NetworkMapVisualiser.RunningPausedState.Running(tickTimer)
}
}
runPauseButton.text = newRunningPausedState.buttonLabel.toString()
runningPausedState = newRunningPausedState
view.runPauseButton.text = newRunningPausedState.buttonLabel.toString()
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
}
}
}
val displayStyles = FXCollections.observableArrayList<Style>()
Style.values().forEach { displayStyles.add(it) }
val styleChoice = ChoiceBox(displayStyles).apply {
styleClass += "choice"
styleClass += "style-choice"
Timer().scheduleAtFixedRate(0, 500) {
Platform.runLater {
for (tracker in model.doneTrackers) {
val pane = model.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 {
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 }
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)
val dateLabel = Label("").apply { styleClass += "date-label" }
simulation.dateChanges.observeOn(uiThread).subscribe { dateLabel.text = it.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) }
// Buttons area. In presentation mode there are no controls visible and you must use the keyboard.
val hbox = if ("--presentation-mode" in parameters.raw) {
HBox(logoLabel, dateLabel).apply { styleClass += "controls-hbox" }
} else {
HBox(logoLabel, dateLabel, simulateInitialisationCheckbox, runPauseButton, nextButton, resetButton, styleChoice).apply { styleClass += "controls-hbox" }
private fun bindProgressTracketWidget(tracker: ProgressTracker, widget: TrackerWidget) {
val allSteps: List<Pair<Int, ProgressTracker.Step>> = tracker.allSteps
tracker.changes.observeOn(uiThread).subscribe { step: ProgressTracker.Change ->
val stepHeight = widget.cursorBox.height / allSteps.size
if (step is ProgressTracker.Change.Position) {
// Figure out the index of the new step.
val curStep = allSteps.indexOfFirst { it.second == step.newStep }
// Animate the cursor to the right place.
with(TranslateTransition(Duration(350.0), widget.cursor)) {
fromY = widget.cursor.translateY
toY = (curStep * stepHeight) + 22.5
play()
}
} else if (step is ProgressTracker.Change.Structural) {
val new = view.buildProgressTrackerWidget(widget.label.text, tracker)
val prevWidget = model.trackerBoxes[step.tracker] ?: throw AssertionError("No previous widget for tracker: ${step.tracker}")
val i = (prevWidget.parent as VBox).children.indexOf(model.trackerBoxes[step.tracker])
(prevWidget.parent as VBox).children[i] = new.vbox
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() {
simulation.stop()
simulation = IRSSimulation(true, false, null)
model.simulation.stop()
model.simulation = IRSSimulation(true, false, null)
started = false
start(this.stage)
start(view.stage)
}
private fun skipNetworkInitialisation() {
startSimulation()
while (!simulation.networkInitialisationFinished.isDone) {
while (!model.simulation.networkInitialisationFinished.isDone) {
iterateSimulation()
}
}
@ -435,7 +366,7 @@ class NetworkMapVisualiser : Application() {
private fun iterateSimulation() {
// Loop until either we ran out of things to do, or we sent an interesting message.
while (true) {
val transfer: InMemoryMessagingNetwork.MessageTransfer = simulation.iterate() ?: break
val transfer: InMemoryMessagingNetwork.MessageTransfer = model.simulation.iterate() ?: break
if (transferIsInteresting(transfer))
break
else
@ -451,384 +382,6 @@ class NetworkMapVisualiser : Application() {
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>) {

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 'client'
include 'experimental'
include 'test-utils'
include 'test-utils'
include 'network-map-visualiser'