From e27a61ffe637055a67a1931eae78f2b3dc69ea79 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 13 Jul 2016 13:23:58 +0200 Subject: [PATCH 01/10] Make the name of the Network Map Visualiser tool consistent in the code and module naming. --- .idea/modules.xml | 5 +- network-map-visualiser/build.gradle | 58 ++ .../corda/netmap/NetworkMapVisualiser.kt | 837 ++++++++++++++++++ .../com/r3cev/corda/netmap/Europe.jpg | Bin 0 -> 309247 bytes .../com/r3cev/corda/netmap/MainWindow.fxml | 29 + .../com/r3cev/corda/netmap/R3 logo.png | Bin 0 -> 23602 bytes .../corda/netmap/SIL Open Font License.txt | 43 + .../corda/netmap/SourceSansPro-Black.otf | Bin 0 -> 234176 bytes .../corda/netmap/SourceSansPro-BlackIt.otf | Bin 0 -> 81120 bytes .../r3cev/corda/netmap/SourceSansPro-Bold.otf | Bin 0 -> 235128 bytes .../corda/netmap/SourceSansPro-BoldIt.otf | Bin 0 -> 80392 bytes .../corda/netmap/SourceSansPro-ExtraLight.otf | Bin 0 -> 221580 bytes .../netmap/SourceSansPro-ExtraLightIt.otf | Bin 0 -> 76400 bytes .../r3cev/corda/netmap/SourceSansPro-It.otf | Bin 0 -> 79724 bytes .../corda/netmap/SourceSansPro-Light.otf | Bin 0 -> 226032 bytes .../corda/netmap/SourceSansPro-LightIt.otf | Bin 0 -> 77816 bytes .../corda/netmap/SourceSansPro-Regular.otf | Bin 0 -> 229588 bytes .../corda/netmap/SourceSansPro-Semibold.otf | Bin 0 -> 232680 bytes .../corda/netmap/SourceSansPro-SemiboldIt.otf | Bin 0 -> 80316 bytes .../com/r3cev/corda/netmap/WorldMap.png | Bin 0 -> 349746 bytes .../com/r3cev/corda/netmap/WorldMapSquare.png | Bin 0 -> 480246 bytes .../com/r3cev/corda/netmap/styles.css | 227 +++++ 22 files changed, 1198 insertions(+), 1 deletion(-) create mode 100644 network-map-visualiser/build.gradle create mode 100644 network-map-visualiser/src/main/kotlin/com/r3cev/corda/netmap/NetworkMapVisualiser.kt create mode 100644 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/Europe.jpg create mode 100644 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/MainWindow.fxml create mode 100644 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/R3 logo.png create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SIL Open Font License.txt create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-Black.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-BlackIt.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-Bold.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-BoldIt.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-ExtraLight.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-ExtraLightIt.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-It.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-Light.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-LightIt.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-Regular.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-Semibold.otf create mode 100755 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/SourceSansPro-SemiboldIt.otf create mode 100644 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/WorldMap.png create mode 100644 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/WorldMapSquare.png create mode 100644 network-map-visualiser/src/main/resources/com/r3cev/corda/netmap/styles.css diff --git a/.idea/modules.xml b/.idea/modules.xml index 5517a9dc4a..63596a0a6f 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -21,6 +21,9 @@ + + + @@ -34,4 +37,4 @@ - \ No newline at end of file + diff --git a/network-map-visualiser/build.gradle b/network-map-visualiser/build.gradle new file mode 100644 index 0000000000..dfbc958bc8 --- /dev/null +++ b/network-map-visualiser/build.gradle @@ -0,0 +1,58 @@ +buildscript { + repositories { + mavenCentral() + maven { + url 'http://oss.sonatype.org/content/repositories/snapshots' + } + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +plugins { + id "us.kirchmeier.capsule" version "1.0.2" +} +apply plugin: 'java' +apply plugin: 'kotlin' +apply plugin: 'application' + +group 'com.r3cev.prototyping' +version '1.0-SNAPSHOT' + +sourceCompatibility = 1.5 + +repositories { + mavenLocal() + mavenCentral() + maven { + url 'http://oss.sonatype.org/content/repositories/snapshots' + } + jcenter() +} + +applicationDefaultJvmArgs = ["-javaagent:${rootProject.configurations.quasar.singleFile}"] +mainClassName = 'com.r3cev.corda.netmap.NetworkExplorerKt' + +dependencies { + compile project(":core") + compile project(":node") + compile project(":contracts") + compile rootProject + + // GraphStream: For visualisation + compile "org.graphstream:gs-core:1.3" + compile "org.graphstream:gs-ui:1.3" + + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + testCompile group: 'junit', name: 'junit', version: '4.11' +} + +task capsule(type: FatCapsule) { + applicationClass 'com.r3cev.corda.netmap.NetworkExplorerKt' + reallyExecutable + capsuleManifest { + minJavaVersion = '1.8.0' + javaAgents = [rootProject.configurations.quasar.singleFile.name] + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..b34b69ad6e --- /dev/null +++ b/network-map-visualiser/src/main/kotlin/com/r3cev/corda/netmap/NetworkMapVisualiser.kt @@ -0,0 +1,837 @@ +/* + * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members + * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms + * set forth therein. + * + * All other rights reserved. + */ + +package com.r3cev.corda.netmap + +import com.r3corda.core.messaging.SingleMessageRecipient +import com.r3corda.core.then +import com.r3corda.core.utilities.BriefLogFormatter +import com.r3corda.core.utilities.ProgressTracker +import com.r3corda.node.internal.testing.IRSSimulation +import com.r3corda.node.internal.testing.MockNetwork +import com.r3corda.node.internal.testing.Simulation +import com.r3corda.node.services.network.InMemoryMessagingNetwork +import com.r3corda.node.services.network.NetworkMapService +import javafx.animation.* +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.* +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 +import rx.schedulers.Schedulers +import java.nio.file.Files +import java.nio.file.Paths +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.* +import kotlin.concurrent.schedule +import kotlin.concurrent.scheduleAtFixedRate +import kotlin.system.exitProcess + +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() { + enum class NodeType { + BANK, SERVICE + } + + enum class Style { + MAP, CIRCLE; + + override fun toString(): String { + return name.toLowerCase().capitalize() + } + } + + enum class RunPauseButtonLabel { + RUN, PAUSE; + + override fun toString(): String { + return name.toLowerCase().capitalize() + } + } + + sealed class RunningPausedState { + class Running(val tickTimer: TimerTask): RunningPausedState() + class Paused(): RunningPausedState() + + val buttonLabel: RunPauseButtonLabel + get() { + return when (this) { + is RunningPausedState.Running -> RunPauseButtonLabel.PAUSE + is RunningPausedState.Paused -> RunPauseButtonLabel.RUN + } + } + } + + val stageWidth = 1024.0 + val stageHeight = 768.0 + var defaultZoom = 0.7 + + private lateinit var stage: Stage + private lateinit var root: Pane + + val bitmapWidth = 1900.0 + val bitmapHeight = 1900.0 + var stepDuration = Duration.millis(500.0) + + 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 + buildScene(stage) + + // Update the white-backgrounded label indicating what protocol step it's up to. + simulation.allProtocolSteps.observeOn(uiThread).subscribe { step: Pair -> + val (node, change) = step + val label = nodesToWidgets[node]!!.statusLabel + if (change is ProgressTracker.Change.Position) { + // Fade in the status label if it's our first step. + if (label.text == "") { + with(FadeTransition(Duration(150.0), label)) { + fromValue = 0.0 + toValue = 1.0 + play() + } + } + label.text = change.newStep.label + if (change.newStep == ProgressTracker.DONE && change.tracker == change.tracker.topLevelTracker) { + runLater(500, -1) { + // Fade out the status label. + with(FadeTransition(Duration(750.0), label)) { + fromValue = 1.0 + toValue = 0.0 + setOnFinished { label.text = "" } + play() + } + } + } + } else if (change is ProgressTracker.Change.Rendering) { + label.text = change.ofStep.label + } + } + // 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) + + if (transferIsInteresting(msg)) { + nodesToWidgets[senderNode]!!.pulseAnim.play() + 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() } + } + + if ("--circle" in parameters.raw) + updateDisplayStyle(Style.CIRCLE) + + stage.setOnCloseRequest { exitProcess(0) } + //stage.isMaximized = true + stage.show() + } + + fun runLater(startAfter: Int, delayBetween: Int, body: () -> Unit) { + if (delayBetween != -1) { + timer.scheduleAtFixedRate(startAfter.toLong(), delayBetween.toLong()) { + Platform.runLater { + body() + } + } + } else { + timer.schedule(startAfter.toLong()) { + Platform.runLater { + body() + } + } + } + } + + // -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 + + 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) + + // Spacebar advances simulation by one step. + stage.scene.accelerators[KeyCodeCombination(KeyCode.SPACE)] = Runnable { onNextInvoked() } + + reloadStylesheet(stage) + + stage.focusedProperty().addListener { value, old, 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) { + skipNetworkInitialisation() + } else { + onNextInvoked() + } + } + + simulation.networkInitialisationFinished.then { + simulateInitialisationCheckbox.isVisible = false + } + + runPauseButton.setOnAction { + val oldRunningPausedState = runningPausedState + val newRunningPausedState = when (oldRunningPausedState) { + is RunningPausedState.Running -> { + oldRunningPausedState.tickTimer.cancel() + + nextButton.isDisable = false + resetButton.isDisable = false + + RunningPausedState.Paused() + } + is RunningPausedState.Paused -> { + val tickTimer = timer.scheduleAtFixedRate(stepDuration.toMillis().toLong(), stepDuration.toMillis().toLong()) { + Platform.runLater { + onNextInvoked() + } + } + + nextButton.isDisable = true + resetButton.isDisable = true + + if (!simulateInitialisationCheckbox.isSelected && !simulation.networkInitialisationFinished.isDone) { + skipNetworkInitialisation() + } + + RunningPausedState.Running(tickTimer) + } + } + + runPauseButton.text = newRunningPausedState.buttonLabel.toString() + runningPausedState = newRunningPausedState + + } + + + val displayStyles = FXCollections.observableArrayList