mirror of
https://github.com/corda/corda.git
synced 2025-06-01 15:10:54 +00:00
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:
parent
ba22990938
commit
f76c7c9cf9
@ -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>) {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
@ -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?
|
||||||
|
}
|
||||||
|
}
|
@ -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'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user