From f76c7c9cf9c4cc205a6b258bb9324fe66fa4b8a3 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Thu, 14 Jul 2016 13:47:31 +0100 Subject: [PATCH] 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. --- .../corda/netmap/NetworkMapVisualiser.kt | 771 ++++-------------- .../com/r3cev/corda/netmap/VisualiserModel.kt | 222 +++++ .../com/r3cev/corda/netmap/VisualiserUtils.kt | 18 + .../com/r3cev/corda/netmap/VisualiserView.kt | 309 +++++++ settings.gradle | 3 +- 5 files changed, 713 insertions(+), 610 deletions(-) create mode 100644 network-map-visualiser/src/main/kotlin/com/r3cev/corda/netmap/VisualiserModel.kt create mode 100644 network-map-visualiser/src/main/kotlin/com/r3cev/corda/netmap/VisualiserUtils.kt create mode 100644 network-map-visualiser/src/main/kotlin/com/r3cev/corda/netmap/VisualiserView.kt diff --git a/network-map-visualiser/src/main/kotlin/com/r3cev/corda/netmap/NetworkMapVisualiser.kt b/network-map-visualiser/src/main/kotlin/com/r3cev/corda/netmap/NetworkMapVisualiser.kt index b34b69ad6e..db0452ad10 100644 --- a/network-map-visualiser/src/main/kotlin/com/r3cev/corda/netmap/NetworkMapVisualiser.kt +++ b/network-map-visualiser/src/main/kotlin/com/r3cev/corda/netmap/NetworkMapVisualiser.kt @@ -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 WritableValue.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 -> + model.simulation.allProtocolSteps.observeOn(uiThread).subscribe { step: Pair -> 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 -> - nodes.forEach { nodesToWidgets[it]!!.longPulseAnim.play() } + model.simulation.doneSteps.observeOn(uiThread).subscribe { nodes: Collection -> + 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 { 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 -> + 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