mirror of
https://github.com/corda/corda.git
synced 2024-12-20 21:43:14 +00:00
Delete the network visualiser sample. It's very old and the build is strange and breaks in odd ways when Gradle runs in parallel. We haven't used it for a long time and have better demos these days anyway
This commit is contained in:
parent
230d5e294e
commit
3e98efe04b
@ -1,13 +0,0 @@
|
|||||||
# Network Visualiser
|
|
||||||
|
|
||||||
This package contains a network visualiser that uses a simulation to visualise the interaction and messages between nodes on the Corda network.
|
|
||||||
|
|
||||||
Please see the either the [online documentation](https://docs.corda.net/network-simulator.html) for more info on the network visualiser, or the [included offline version](../../docs/build/html/network-simulator.html).
|
|
||||||
|
|
||||||
From the root directory of the repository, run it like this (Windows):
|
|
||||||
|
|
||||||
gradle samples:network-visualiser:run
|
|
||||||
|
|
||||||
or (Mac / Unix)
|
|
||||||
|
|
||||||
./gradlew samples:network-visualiser:run
|
|
@ -1,78 +0,0 @@
|
|||||||
buildscript {
|
|
||||||
ext {
|
|
||||||
springBootVersion = '1.5.7.RELEASE'
|
|
||||||
}
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
|
|
||||||
classpath "io.spring.gradle:dependency-management-plugin:1.0.4.RELEASE"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spring Boot plugin adds a numerous hardcoded dependencies in the version much lower then Corda expects
|
|
||||||
// causing the problems in runtime. Those can be changed by manipulating above properties
|
|
||||||
// See https://github.com/spring-gradle-plugins/dependency-management-plugin/blob/master/README.md#changing-the-value-of-a-version-property
|
|
||||||
// This has to be repeated here as otherwise the order of files does matter
|
|
||||||
ext['artemis.version'] = "$artemis_version"
|
|
||||||
ext['hibernate.version'] = "$hibernate_version"
|
|
||||||
ext['jackson.version'] = "$jackson_version"
|
|
||||||
ext['dropwizard-metrics.version'] = "$metrics_version"
|
|
||||||
ext['mockito.version'] = "$mockito_version"
|
|
||||||
|
|
||||||
apply plugin: 'java'
|
|
||||||
apply plugin: 'kotlin'
|
|
||||||
apply plugin: 'idea'
|
|
||||||
apply plugin: 'application'
|
|
||||||
apply plugin: 'org.springframework.boot'
|
|
||||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
|
||||||
apply plugin: 'us.kirchmeier.capsule'
|
|
||||||
|
|
||||||
// Spring Boot plugin have to be reimported, however it picks up the settings from irs-demo, so there is no need to
|
|
||||||
// reconfigure
|
|
||||||
|
|
||||||
// Warning: The network visualiser is not a Cordapp so please do not use it as an example of how
|
|
||||||
// to build a cordapp
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testCompile "junit:junit:$junit_version"
|
|
||||||
|
|
||||||
// Corda integration dependencies
|
|
||||||
compile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
|
||||||
compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
|
|
||||||
compile project(':core')
|
|
||||||
compile project(':finance')
|
|
||||||
compile project(':node-driver')
|
|
||||||
compile project(':finance')
|
|
||||||
compile project(':samples:irs-demo')
|
|
||||||
|
|
||||||
// GraphStream: For visualisation
|
|
||||||
compileOnly "co.paralleluniverse:capsule:$capsule_version"
|
|
||||||
}
|
|
||||||
|
|
||||||
idea {
|
|
||||||
module {
|
|
||||||
downloadJavadoc = true // defaults to false
|
|
||||||
downloadSources = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mainClassName = 'net.corda.netmap.NetworkMapVisualiser'
|
|
||||||
|
|
||||||
task deployVisualiser(type: FatCapsule) {
|
|
||||||
applicationClass 'net.corda.netmap.NetworkMapVisualiser'
|
|
||||||
reallyExecutable
|
|
||||||
capsuleManifest {
|
|
||||||
minJavaVersion = '1.8.0'
|
|
||||||
javaAgents = [configurations.quasar.singleFile.name]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jar {
|
|
||||||
manifest {
|
|
||||||
attributes(
|
|
||||||
'Automatic-Module-Name': 'net.corda.samples.network.visualiser'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,359 +0,0 @@
|
|||||||
package net.corda.netmap
|
|
||||||
|
|
||||||
import javafx.animation.*
|
|
||||||
import javafx.application.Application
|
|
||||||
import javafx.application.Platform
|
|
||||||
import javafx.beans.property.SimpleDoubleProperty
|
|
||||||
import javafx.beans.value.WritableValue
|
|
||||||
import javafx.geometry.Insets
|
|
||||||
import javafx.scene.input.KeyCode
|
|
||||||
import javafx.scene.input.KeyCodeCombination
|
|
||||||
import javafx.scene.layout.VBox
|
|
||||||
import javafx.stage.Stage
|
|
||||||
import javafx.util.Duration
|
|
||||||
import net.corda.core.serialization.deserialize
|
|
||||||
import net.corda.core.utilities.ProgressTracker
|
|
||||||
import net.corda.netmap.VisualiserViewModel.Style
|
|
||||||
import net.corda.netmap.simulation.IRSSimulation
|
|
||||||
import net.corda.node.services.statemachine.*
|
|
||||||
import net.corda.testing.core.singleIdentity
|
|
||||||
import net.corda.testing.node.InMemoryMessagingNetwork
|
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
|
||||||
import rx.Scheduler
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
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 <T : Any> WritableValue<T>.keyValue(endValue: T, interpolator: Interpolator = Interpolator.EASE_OUT) = KeyValue(this, endValue, interpolator)
|
|
||||||
|
|
||||||
// TODO: This code is all horribly ugly. Refactor to use TornadoFX to clean it up.
|
|
||||||
|
|
||||||
class NetworkMapVisualiser : Application() {
|
|
||||||
enum class NodeType {
|
|
||||||
BANK, SERVICE
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val view = VisualiserView()
|
|
||||||
private val viewModel = VisualiserViewModel()
|
|
||||||
|
|
||||||
val timer = Timer()
|
|
||||||
val uiThread: Scheduler = Schedulers.from { Platform.runLater(it) }
|
|
||||||
|
|
||||||
override fun start(stage: Stage) {
|
|
||||||
viewModel.view = view
|
|
||||||
viewModel.presentationMode = "--presentation-mode" in parameters.raw
|
|
||||||
buildScene(stage)
|
|
||||||
viewModel.displayStyle = if ("--circle" in parameters.raw) {
|
|
||||||
Style.CIRCLE
|
|
||||||
} else {
|
|
||||||
viewModel.displayStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
val simulation = viewModel.simulation
|
|
||||||
// Update the white-backgrounded label indicating what flow step it's up to.
|
|
||||||
simulation.allFlowSteps.observeOn(uiThread).subscribe { (node, change) ->
|
|
||||||
val label = viewModel.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.mockNet.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer ->
|
|
||||||
val senderNode: InternalMockNetwork.MockNode = simulation.mockNet.addressToNode(msg.sender)
|
|
||||||
val destNode: InternalMockNetwork.MockNode = simulation.mockNet.addressToNode(msg.recipients)
|
|
||||||
|
|
||||||
if (transferIsInteresting(msg)) {
|
|
||||||
viewModel.nodesToWidgets[senderNode]!!.pulseAnim.play()
|
|
||||||
viewModel.fireBulletBetweenNodes(senderNode, destNode, "bank", "bank")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Pulse all parties in a trade when the trade completes
|
|
||||||
simulation.doneSteps.observeOn(uiThread).subscribe { nodes: Collection<InternalMockNetwork.MockNode> ->
|
|
||||||
nodes.forEach { viewModel.nodesToWidgets[it]!!.longPulseAnim.play() }
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildScene(stage: Stage) {
|
|
||||||
view.stage = stage
|
|
||||||
view.setup(viewModel.runningPausedState, viewModel.displayStyle, viewModel.presentationMode)
|
|
||||||
bindSidebar()
|
|
||||||
bindTopbar()
|
|
||||||
viewModel.createNodes()
|
|
||||||
|
|
||||||
// Spacebar advances simulation by one step.
|
|
||||||
stage.scene.accelerators[KeyCodeCombination(KeyCode.SPACE)] = Runnable { onNextInvoked() }
|
|
||||||
|
|
||||||
reloadStylesheet(stage)
|
|
||||||
|
|
||||||
stage.focusedProperty().addListener { _, _, new ->
|
|
||||||
if (new) {
|
|
||||||
reloadStylesheet(stage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindTopbar() {
|
|
||||||
view.resetButton.setOnAction({ reset() })
|
|
||||||
view.nextButton.setOnAction {
|
|
||||||
if (!view.simulateInitialisationCheckbox.isSelected && !viewModel.simulation.networkInitialisationFinished.isDone) {
|
|
||||||
skipNetworkInitialisation()
|
|
||||||
} else {
|
|
||||||
onNextInvoked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.simulation.networkInitialisationFinished.thenAccept {
|
|
||||||
view.simulateInitialisationCheckbox.isVisible = false
|
|
||||||
}
|
|
||||||
view.runPauseButton.setOnAction {
|
|
||||||
val oldRunningPausedState = viewModel.runningPausedState
|
|
||||||
val newRunningPausedState = when (oldRunningPausedState) {
|
|
||||||
is NetworkMapVisualiser.RunningPausedState.Running -> {
|
|
||||||
oldRunningPausedState.tickTimer.cancel()
|
|
||||||
|
|
||||||
view.nextButton.isDisable = false
|
|
||||||
view.resetButton.isDisable = false
|
|
||||||
|
|
||||||
NetworkMapVisualiser.RunningPausedState.Paused()
|
|
||||||
}
|
|
||||||
is NetworkMapVisualiser.RunningPausedState.Paused -> {
|
|
||||||
val tickTimer = timer.scheduleAtFixedRate(viewModel.stepDuration.toMillis().toLong(), viewModel.stepDuration.toMillis().toLong()) {
|
|
||||||
Platform.runLater {
|
|
||||||
onNextInvoked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.nextButton.isDisable = true
|
|
||||||
view.resetButton.isDisable = true
|
|
||||||
|
|
||||||
if (!view.simulateInitialisationCheckbox.isSelected && !viewModel.simulation.networkInitialisationFinished.isDone) {
|
|
||||||
skipNetworkInitialisation()
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkMapVisualiser.RunningPausedState.Running(tickTimer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.runPauseButton.text = newRunningPausedState.buttonLabel.toString()
|
|
||||||
viewModel.runningPausedState = newRunningPausedState
|
|
||||||
}
|
|
||||||
view.styleChoice.selectionModel.selectedItemProperty()
|
|
||||||
.addListener { _, _, newValue -> viewModel.displayStyle = newValue }
|
|
||||||
viewModel.simulation.dateChanges.observeOn(uiThread).subscribe { view.dateLabel.text = it.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reloadStylesheet(stage: Stage) {
|
|
||||||
stage.scene.stylesheets.clear()
|
|
||||||
stage.scene.stylesheets.add(NetworkMapVisualiser::class.java.getResource("styles.css").toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindSidebar() {
|
|
||||||
viewModel.simulation.allFlowSteps.observeOn(uiThread).subscribe { (node, change) ->
|
|
||||||
if (change is ProgressTracker.Change.Position) {
|
|
||||||
val tracker = change.tracker.topLevelTracker
|
|
||||||
if (change.newStep == ProgressTracker.DONE) {
|
|
||||||
if (change.tracker == tracker) {
|
|
||||||
// Flow done; schedule it for removal in a few seconds. We batch them up to make nicer
|
|
||||||
// animations.
|
|
||||||
updateProgressTrackerWidget(change)
|
|
||||||
println("Flow done for ${node.started!!.info.singleIdentity().name}")
|
|
||||||
viewModel.doneTrackers += tracker
|
|
||||||
} else {
|
|
||||||
// Subflow is done; ignore it.
|
|
||||||
}
|
|
||||||
} else if (!viewModel.trackerBoxes.containsKey(tracker)) {
|
|
||||||
// New flow started up; add.
|
|
||||||
val extraLabel = viewModel.simulation.extraNodeLabels[node]
|
|
||||||
val label = node.started!!.info.singleIdentity().name.organisation.let { if (extraLabel != null) "$it: $extraLabel" else it }
|
|
||||||
val widget = view.buildProgressTrackerWidget(label, tracker.topLevelTracker)
|
|
||||||
println("Added: $tracker, $widget")
|
|
||||||
viewModel.trackerBoxes[tracker] = widget
|
|
||||||
view.sidebar.children += widget.vbox
|
|
||||||
} else {
|
|
||||||
updateProgressTrackerWidget(change)
|
|
||||||
}
|
|
||||||
} else if (change is ProgressTracker.Change.Structural) {
|
|
||||||
updateProgressTrackerWidget(change)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer().scheduleAtFixedRate(0, 500) {
|
|
||||||
Platform.runLater {
|
|
||||||
for (tracker in viewModel.doneTrackers) {
|
|
||||||
val pane = viewModel.trackerBoxes[tracker]!!.vbox
|
|
||||||
// Slide the other tracker widgets up and over this one.
|
|
||||||
val slideProp = SimpleDoubleProperty(0.0)
|
|
||||||
slideProp.addListener { _ -> 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 = viewModel.trackerBoxes.remove(tracker)?.vbox
|
|
||||||
view.sidebar.children.remove(vbox)
|
|
||||||
}
|
|
||||||
timeline.play()
|
|
||||||
}
|
|
||||||
viewModel.doneTrackers.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateProgressTrackerWidget(step: ProgressTracker.Change) {
|
|
||||||
if (step is ProgressTracker.Change.Position) {
|
|
||||||
// Animate the cursor to the right place.
|
|
||||||
Platform.runLater {
|
|
||||||
val tracker: ProgressTracker = step.tracker.topLevelTracker
|
|
||||||
val widget = viewModel.trackerBoxes[tracker] ?: return@runLater
|
|
||||||
val allSteps: List<Pair<Int, ProgressTracker.Step>> = tracker.allSteps
|
|
||||||
|
|
||||||
// Figure out the index of the new step.
|
|
||||||
val curStep = allSteps.indexOfFirst { it.second == step.newStep }
|
|
||||||
with(TranslateTransition(Duration(350.0), widget.cursor)) {
|
|
||||||
fromY = widget.cursor.translateY
|
|
||||||
toY = (curStep * view.sideBarStepHeight) + (view.sideBarStepHeight / 2.0)
|
|
||||||
play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (step is ProgressTracker.Change.Structural) {
|
|
||||||
Platform.runLater {
|
|
||||||
val tracker: ProgressTracker = step.tracker.topLevelTracker
|
|
||||||
val widget = viewModel.trackerBoxes[tracker] ?: return@runLater
|
|
||||||
val new = view.buildProgressTrackerWidget(widget.label.text, tracker)
|
|
||||||
val prevWidget = viewModel.trackerBoxes[tracker]?.vbox ?: throw AssertionError("No previous widget for tracker: $tracker")
|
|
||||||
val i = (prevWidget.parent as VBox).children.indexOf(viewModel.trackerBoxes[tracker]?.vbox)
|
|
||||||
(prevWidget.parent as VBox).children[i] = new.vbox
|
|
||||||
viewModel.trackerBoxes[tracker] = new
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var started = false
|
|
||||||
private fun startSimulation() {
|
|
||||||
if (!started) {
|
|
||||||
viewModel.simulation.start()
|
|
||||||
started = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reset() {
|
|
||||||
viewModel.simulation.stop()
|
|
||||||
viewModel.simulation = IRSSimulation(true, false, null)
|
|
||||||
started = false
|
|
||||||
start(view.stage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun skipNetworkInitialisation() {
|
|
||||||
startSimulation()
|
|
||||||
while (!viewModel.simulation.networkInitialisationFinished.isDone) {
|
|
||||||
iterateSimulation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onNextInvoked() {
|
|
||||||
if (started) {
|
|
||||||
iterateSimulation()
|
|
||||||
} else {
|
|
||||||
startSimulation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun iterateSimulation() {
|
|
||||||
// Loop until either we ran out of things to do, or we sent an interesting message.
|
|
||||||
while (true) {
|
|
||||||
val transfer: InMemoryMessagingNetwork.MessageTransfer = viewModel.simulation.iterate() ?: break
|
|
||||||
if (transferIsInteresting(transfer))
|
|
||||||
break
|
|
||||||
else
|
|
||||||
System.err.println("skipping boring $transfer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun transferIsInteresting(transfer: InMemoryMessagingNetwork.MessageTransfer): Boolean {
|
|
||||||
// Loopback messages are boring.
|
|
||||||
if (transfer.sender == transfer.recipients) return false
|
|
||||||
val message = transfer.messageData.deserialize<SessionMessage>()
|
|
||||||
return when (message) {
|
|
||||||
is InitialSessionMessage -> message.firstPayload != null
|
|
||||||
is ExistingSessionMessage -> when (message.payload) {
|
|
||||||
is ConfirmSessionMessage -> false
|
|
||||||
is DataSessionMessage -> true
|
|
||||||
is ErrorSessionMessage -> true
|
|
||||||
is RejectSessionMessage -> true
|
|
||||||
is EndSessionMessage -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
Application.launch(NetworkMapVisualiser::class.java, *args)
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package net.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()
|
|
||||||
}
|
|
@ -1,305 +0,0 @@
|
|||||||
package net.corda.netmap
|
|
||||||
|
|
||||||
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 net.corda.core.utilities.ProgressTracker
|
|
||||||
import net.corda.netmap.VisualiserViewModel.Style
|
|
||||||
|
|
||||||
data class TrackerWidget(val vbox: VBox, val cursorBox: Pane, val label: Label, val cursor: Polygon)
|
|
||||||
|
|
||||||
internal class VisualiserView {
|
|
||||||
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" }
|
|
||||||
|
|
||||||
|
|
||||||
// -23.2031,29.8406,33.0469,64.3209
|
|
||||||
val mapImage = ImageView(Image(NetworkMapVisualiser::class.java.getResourceAsStream("Europe.jpg")))
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
// This row height is controlled in the CSS and needs to match.
|
|
||||||
val sideBarStepHeight = 40.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("Corda logo.png").toExternalForm())
|
|
||||||
logoImage.fitHeight = 65.0
|
|
||||||
logoImage.isPreserveRatio = true
|
|
||||||
val logoLabel = HBox(logoImage,
|
|
||||||
Label("Network Simulator").apply { styleClass += "logo-label" }
|
|
||||||
)
|
|
||||||
logoLabel.spacing = 10.0
|
|
||||||
logoLabel.alignment = Pos.CENTER_LEFT
|
|
||||||
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 trackerCurrentStep = tracker.currentStepRecursive
|
|
||||||
val curStep = allSteps.indexOfFirst { it.second == trackerCurrentStep }
|
|
||||||
val arrowSize = 7.0
|
|
||||||
val cursor = Polygon(-arrowSize, -arrowSize, arrowSize, 0.0, -arrowSize, arrowSize).apply {
|
|
||||||
styleClass += "progress-tracker-cursor"
|
|
||||||
translateY = (Math.max(0, curStep - 1) * sideBarStepHeight) + (sideBarStepHeight / 2.0)
|
|
||||||
}
|
|
||||||
val cursorBox = Pane(cursor).apply {
|
|
||||||
styleClass += "progress-tracker-cursor-box"
|
|
||||||
minWidth = 25.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?
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,225 +0,0 @@
|
|||||||
package net.corda.netmap
|
|
||||||
|
|
||||||
import javafx.animation.*
|
|
||||||
import javafx.geometry.Pos
|
|
||||||
import javafx.scene.control.Label
|
|
||||||
import javafx.scene.layout.StackPane
|
|
||||||
import javafx.scene.shape.Circle
|
|
||||||
import javafx.scene.shape.Line
|
|
||||||
import javafx.util.Duration
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
|
||||||
import net.corda.core.utilities.ProgressTracker
|
|
||||||
import net.corda.finance.utils.ScreenCoordinate
|
|
||||||
import net.corda.netmap.simulation.IRSSimulation
|
|
||||||
import net.corda.netmap.simulation.place
|
|
||||||
import net.corda.testing.core.singleIdentity
|
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class VisualiserViewModel {
|
|
||||||
enum class Style {
|
|
||||||
MAP, CIRCLE;
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return name.toLowerCase().capitalize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class NodeWidget(val node: InternalMockNetwork.MockNode, val innerDot: Circle, val outerDot: Circle, val longPulseDot: Circle,
|
|
||||||
val pulseAnim: Animation, val longPulseAnim: Animation,
|
|
||||||
val nameLabel: Label, val statusLabel: Label) {
|
|
||||||
fun position(nodeCoords: (node: InternalMockNetwork.MockNode) -> ScreenCoordinate) {
|
|
||||||
val (x, y) = nodeCoords(node)
|
|
||||||
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, TrackerWidget>()
|
|
||||||
val doneTrackers = ArrayList<ProgressTracker>()
|
|
||||||
val nodesToWidgets = HashMap<InternalMockNetwork.MockNode, NodeWidget>()
|
|
||||||
|
|
||||||
var bankCount: Int = 0
|
|
||||||
var serviceCount: Int = 0
|
|
||||||
|
|
||||||
var stepDuration: Duration = 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(when (displayStyle) {
|
|
||||||
Style.MAP -> { node -> nodeMapCoords(node) }
|
|
||||||
Style.CIRCLE -> { _ -> nodeCircleCoords(NetworkMapVisualiser.NodeType.BANK, index) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for ((index, serviceProvider) in (simulation.serviceProviders + simulation.regulators).withIndex()) {
|
|
||||||
nodesToWidgets[serviceProvider]!!.position(when (displayStyle) {
|
|
||||||
Style.MAP -> { node -> nodeMapCoords(node) }
|
|
||||||
Style.CIRCLE -> { _ -> nodeCircleCoords(NetworkMapVisualiser.NodeType.SERVICE, index) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun nodeMapCoords(node: InternalMockNetwork.MockNode): ScreenCoordinate {
|
|
||||||
// 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.started!!.info.singleIdentity()}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun nodeCircleCoords(type: NetworkMapVisualiser.NodeType, index: Int): ScreenCoordinate {
|
|
||||||
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 ScreenCoordinate(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: InternalMockNetwork.MockNode, type: String, label: CordaX500Name = CordaX500Name(organisation = "Bank of Bologna", locality = "Bologna", country = "IT"),
|
|
||||||
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.organisation)
|
|
||||||
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 { _ -> nodeCircleCoords(nodeType, index) }
|
|
||||||
Style.MAP -> widget.position { node -> nodeMapCoords(node) }
|
|
||||||
}
|
|
||||||
return widget
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fireBulletBetweenNodes(senderNode: InternalMockNetwork.MockNode, destNode: InternalMockNetwork.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,172 +0,0 @@
|
|||||||
package net.corda.netmap.simulation
|
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import net.corda.client.jackson.JacksonSupport
|
|
||||||
import net.corda.core.contracts.StateAndRef
|
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.FlowSession
|
|
||||||
import net.corda.core.flows.InitiatedBy
|
|
||||||
import net.corda.core.flows.InitiatingFlow
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.internal.FlowStateMachine
|
|
||||||
import net.corda.core.internal.uncheckedCast
|
|
||||||
import net.corda.core.node.services.queryBy
|
|
||||||
import net.corda.core.toFuture
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.finance.flows.TwoPartyDealFlow.Acceptor
|
|
||||||
import net.corda.finance.flows.TwoPartyDealFlow.AutoOffer
|
|
||||||
import net.corda.finance.flows.TwoPartyDealFlow.Instigator
|
|
||||||
import net.corda.finance.plugin.registerFinanceJSONMappers
|
|
||||||
import net.corda.irs.contract.InterestRateSwap
|
|
||||||
import net.corda.irs.flows.FixingFlow
|
|
||||||
import net.corda.testing.core.singleIdentity
|
|
||||||
import net.corda.testing.node.InMemoryMessagingNetwork
|
|
||||||
import net.corda.testing.node.internal.startFlow
|
|
||||||
import net.corda.testing.node.makeTestIdentityService
|
|
||||||
import rx.Observable
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import java.util.concurrent.CompletableFuture.allOf
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simulation in which banks execute interest rate swaps with each other, including the fixing events.
|
|
||||||
*/
|
|
||||||
class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) : Simulation(networkSendManuallyPumped, runAsync, latencyInjector) {
|
|
||||||
lateinit var om: ObjectMapper
|
|
||||||
|
|
||||||
init {
|
|
||||||
currentDateAndTime = LocalDate.of(2016, 3, 8).atStartOfDay()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>())
|
|
||||||
|
|
||||||
override fun startMainSimulation(): CompletableFuture<Unit> {
|
|
||||||
om = JacksonSupport.createInMemoryMapper(makeTestIdentityService(*(banks + regulators + ratesOracle).flatMap { it.started!!.info.legalIdentitiesAndCerts }.toTypedArray()))
|
|
||||||
registerFinanceJSONMappers(om)
|
|
||||||
|
|
||||||
return startIRSDealBetween(0, 1).thenCompose {
|
|
||||||
val future = CompletableFuture<Unit>()
|
|
||||||
// Next iteration is a pause.
|
|
||||||
executeOnNextIteration.add {}
|
|
||||||
executeOnNextIteration.add {
|
|
||||||
// Keep fixing until there's no more left to do.
|
|
||||||
val initialFixFuture = doNextFixing(0, 1)
|
|
||||||
fun onFailure(t: Throwable) {
|
|
||||||
future.completeExceptionally(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSuccess() {
|
|
||||||
// Pause for an iteration.
|
|
||||||
executeOnNextIteration.add {}
|
|
||||||
executeOnNextIteration.add {
|
|
||||||
val f = doNextFixing(0, 1)
|
|
||||||
if (f != null) {
|
|
||||||
f.handle { _, throwable ->
|
|
||||||
if (throwable == null) {
|
|
||||||
onSuccess()
|
|
||||||
} else {
|
|
||||||
onFailure(throwable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// All done!
|
|
||||||
future.complete(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
initialFixFuture!!.handle { _, throwable ->
|
|
||||||
if (throwable == null) {
|
|
||||||
onSuccess()
|
|
||||||
} else {
|
|
||||||
onFailure(throwable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
future
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doNextFixing(i: Int, j: Int): CompletableFuture<Void>? {
|
|
||||||
println("Doing a fixing between $i and $j")
|
|
||||||
val node1 = banks[i].started!!
|
|
||||||
val node2 = banks[j].started!!
|
|
||||||
|
|
||||||
val swaps = node1.services.vaultService.queryBy<InterestRateSwap.State>().states
|
|
||||||
val theDealRef: StateAndRef<InterestRateSwap.State> = swaps.single()
|
|
||||||
|
|
||||||
// Do we have any more days left in this deal's lifetime? If not, return.
|
|
||||||
val nextFixingDate = theDealRef.state.data.calculation.nextFixingDate() ?: return null
|
|
||||||
extraNodeLabels[node1.internals] = "Fixing event on $nextFixingDate"
|
|
||||||
extraNodeLabels[node2.internals] = "Fixing event on $nextFixingDate"
|
|
||||||
|
|
||||||
// Complete the future when the state has been consumed on both nodes
|
|
||||||
val futA = node1.services.vaultService.whenConsumed(theDealRef.ref)
|
|
||||||
val futB = node2.services.vaultService.whenConsumed(theDealRef.ref)
|
|
||||||
|
|
||||||
showConsensusFor(listOf(node1.internals, node2.internals, regulators[0]))
|
|
||||||
|
|
||||||
// For some reason the first fix is always before the effective date.
|
|
||||||
if (nextFixingDate > currentDateAndTime.toLocalDate())
|
|
||||||
currentDateAndTime = nextFixingDate.atTime(15, 0)
|
|
||||||
|
|
||||||
return allOf(futA.toCompletableFuture(), futB.toCompletableFuture())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startIRSDealBetween(i: Int, j: Int): CompletableFuture<SignedTransaction> {
|
|
||||||
val node1 = banks[i].started!!
|
|
||||||
val node2 = banks[j].started!!
|
|
||||||
|
|
||||||
extraNodeLabels[node1.internals] = "Setting up deal"
|
|
||||||
extraNodeLabels[node2.internals] = "Setting up deal"
|
|
||||||
|
|
||||||
// We load the IRS afresh each time because the leg parts of the structure aren't data classes so they don't
|
|
||||||
// have the convenient copy() method that'd let us make small adjustments. Instead they're partly mutable.
|
|
||||||
// TODO: We should revisit this in post-Excalibur cleanup and fix, e.g. by introducing an interface.
|
|
||||||
|
|
||||||
val resourceAsStream = javaClass.classLoader.getResourceAsStream("net/corda/irs/web/simulation/trade.json")
|
|
||||||
?: error("Trade representation cannot be loaded.")
|
|
||||||
val irs = om.readValue<InterestRateSwap.State>(resourceAsStream
|
|
||||||
.reader()
|
|
||||||
.readText()
|
|
||||||
.replace("oracleXXX", RatesOracleNode.RATES_SERVICE_NAME.toString()))
|
|
||||||
irs.fixedLeg.fixedRatePayer = node1.info.singleIdentity()
|
|
||||||
irs.floatingLeg.floatingRatePayer = node2.info.singleIdentity()
|
|
||||||
node1.registerInitiatedFlow(FixingFlow.Fixer::class.java)
|
|
||||||
node2.registerInitiatedFlow(FixingFlow.Fixer::class.java)
|
|
||||||
@InitiatingFlow
|
|
||||||
class StartDealFlow(val otherParty: Party,
|
|
||||||
val payload: AutoOffer) : FlowLogic<SignedTransaction>() {
|
|
||||||
@Suspendable
|
|
||||||
override fun call(): SignedTransaction {
|
|
||||||
val session = initiateFlow(otherParty)
|
|
||||||
return subFlow(Instigator(session, payload))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@InitiatedBy(StartDealFlow::class)
|
|
||||||
class AcceptDealFlow(otherSession: FlowSession) : Acceptor(otherSession)
|
|
||||||
val acceptDealFlows: Observable<AcceptDealFlow> = node2.registerInitiatedFlow(AcceptDealFlow::class.java)
|
|
||||||
val acceptorTxFuture = acceptDealFlows.toFuture().toCompletableFuture().thenCompose {
|
|
||||||
uncheckedCast<FlowStateMachine<*>, FlowStateMachine<SignedTransaction>>(it.stateMachine).resultFuture.toCompletableFuture()
|
|
||||||
}
|
|
||||||
|
|
||||||
showProgressFor(listOf(node1, node2))
|
|
||||||
showConsensusFor(listOf(node1.internals, node2.internals, regulators[0]))
|
|
||||||
|
|
||||||
val instigator = StartDealFlow(
|
|
||||||
node2.info.singleIdentity(),
|
|
||||||
AutoOffer(mockNet.defaultNotaryIdentity, irs)) // TODO Pass notary as parameter to Simulation.
|
|
||||||
val instigatorTxFuture = node1.services.startFlow(instigator).resultFuture
|
|
||||||
|
|
||||||
return allOf(instigatorTxFuture.toCompletableFuture(), acceptorTxFuture).thenCompose { instigatorTxFuture.toCompletableFuture() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun iterate(): InMemoryMessagingNetwork.MessageTransfer? {
|
|
||||||
if (executeOnNextIteration.isNotEmpty())
|
|
||||||
executeOnNextIteration.removeAt(0)()
|
|
||||||
return super.iterate()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,205 +0,0 @@
|
|||||||
package net.corda.netmap.simulation
|
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
|
||||||
import net.corda.core.utilities.ProgressTracker
|
|
||||||
import net.corda.finance.utils.CityDatabase
|
|
||||||
import net.corda.irs.api.NodeInterestRates
|
|
||||||
import net.corda.node.internal.StartedNode
|
|
||||||
import net.corda.node.services.statemachine.StateMachineManager
|
|
||||||
import net.corda.testing.core.TestIdentity
|
|
||||||
import net.corda.testing.node.InMemoryMessagingNetwork
|
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
|
||||||
import net.corda.testing.node.TestClock
|
|
||||||
import net.corda.testing.node.internal.InternalMockNetwork
|
|
||||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
|
||||||
import net.corda.testing.node.internal.MockNodeArgs
|
|
||||||
import rx.Observable
|
|
||||||
import rx.subjects.PublishSubject
|
|
||||||
import java.math.BigInteger
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import java.util.concurrent.CompletableFuture.allOf
|
|
||||||
import java.util.concurrent.Future
|
|
||||||
|
|
||||||
internal val InternalMockNetwork.MockNode.place get() = configuration.myLegalName.locality.let { CityDatabase[it] }!!
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for network simulations that are based on the unit test / mock environment.
|
|
||||||
*
|
|
||||||
* Sets up some nodes that can run flows between each other, and exposes their progress trackers. Provides banks
|
|
||||||
* in a few cities around the world.
|
|
||||||
*/
|
|
||||||
abstract class Simulation(val networkSendManuallyPumped: Boolean,
|
|
||||||
runAsync: Boolean,
|
|
||||||
latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) {
|
|
||||||
private companion object {
|
|
||||||
val defaultParams // The get() is necessary so that entropyRoot isn't shared.
|
|
||||||
get() = InternalMockNodeParameters(configOverrides = {
|
|
||||||
doReturn(makeTestDataSourceProperties(it.myLegalName.organisation)).whenever(it).dataSourceProperties
|
|
||||||
})
|
|
||||||
val DUMMY_REGULATOR = TestIdentity(CordaX500Name("Regulator A", "Paris", "FR"), 100).party
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (!runAsync && latencyInjector != null)
|
|
||||||
throw IllegalArgumentException("The latency injector is only useful when using manual pumping.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val bankLocations = listOf(Pair("London", "GB"), Pair("Frankfurt", "DE"), Pair("Rome", "IT"))
|
|
||||||
|
|
||||||
class RatesOracleNode(args: MockNodeArgs) : InternalMockNetwork.MockNode(args) {
|
|
||||||
companion object {
|
|
||||||
// TODO: Make a more realistic legal name
|
|
||||||
val RATES_SERVICE_NAME = CordaX500Name(organisation = "Rates Service Provider", locality = "Madrid", country = "ES")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun start() = super.start().apply {
|
|
||||||
registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java)
|
|
||||||
registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java)
|
|
||||||
javaClass.classLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt").use {
|
|
||||||
services.cordaService(NodeInterestRates.Oracle::class.java).uploadFixes(it.reader().readText())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mockNet = InternalMockNetwork(
|
|
||||||
cordappPackages = listOf("net.corda.finance.contract", "net.corda.irs"),
|
|
||||||
networkSendManuallyPumped = networkSendManuallyPumped,
|
|
||||||
threadPerNode = runAsync)
|
|
||||||
// TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request.
|
|
||||||
// So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it.
|
|
||||||
// But that's fine for visualisation purposes.
|
|
||||||
val regulators = listOf(mockNet.createUnstartedNode(defaultParams.copy(legalName = DUMMY_REGULATOR.name)))
|
|
||||||
val ratesOracle = mockNet.createUnstartedNode(defaultParams.copy(legalName = RatesOracleNode.RATES_SERVICE_NAME), ::RatesOracleNode)
|
|
||||||
// All nodes must be in one of these two lists for the purposes of the visualiser tool.
|
|
||||||
val serviceProviders: List<InternalMockNetwork.MockNode> = listOf(mockNet.defaultNotaryNode.internals, ratesOracle)
|
|
||||||
val banks: List<InternalMockNetwork.MockNode> = bankLocations.mapIndexed { i, (city, country) ->
|
|
||||||
val legalName = CordaX500Name(organisation = "Bank ${'A' + i}", locality = city, country = country)
|
|
||||||
// Use deterministic seeds so the simulation is stable. Needed so that party owning keys are stable.
|
|
||||||
mockNet.createUnstartedNode(defaultParams.copy(legalName = legalName, entropyRoot = BigInteger.valueOf(i.toLong())))
|
|
||||||
}
|
|
||||||
val clocks = (serviceProviders + regulators + banks).map { it.platformClock as TestClock }
|
|
||||||
|
|
||||||
// These are used from the network visualiser tool.
|
|
||||||
private val _allFlowSteps = PublishSubject.create<Pair<InternalMockNetwork.MockNode, ProgressTracker.Change>>()
|
|
||||||
private val _doneSteps = PublishSubject.create<Collection<InternalMockNetwork.MockNode>>()
|
|
||||||
@Suppress("unused")
|
|
||||||
val allFlowSteps: Observable<Pair<InternalMockNetwork.MockNode, ProgressTracker.Change>> = _allFlowSteps
|
|
||||||
@Suppress("unused")
|
|
||||||
val doneSteps: Observable<Collection<InternalMockNetwork.MockNode>> = _doneSteps
|
|
||||||
|
|
||||||
private var pumpCursor = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current simulated date. By default this never changes. If you want it to change, you should do so from
|
|
||||||
* within your overridden [iterate] call. Changes in the current day surface in the [dateChanges] observable.
|
|
||||||
*/
|
|
||||||
var currentDateAndTime: LocalDateTime = LocalDate.now().atStartOfDay()
|
|
||||||
protected set(value) {
|
|
||||||
field = value
|
|
||||||
_dateChanges.onNext(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _dateChanges = PublishSubject.create<LocalDateTime>()
|
|
||||||
val dateChanges: Observable<LocalDateTime> get() = _dateChanges
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Advance node clocks when current time is changed
|
|
||||||
dateChanges.subscribe {
|
|
||||||
val instant = currentDateAndTime.toInstant(ZoneOffset.UTC)
|
|
||||||
clocks.forEach { it.setTo(instant) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A place for simulations to stash human meaningful text about what the node is "thinking", which might appear
|
|
||||||
* in the UI somewhere.
|
|
||||||
*/
|
|
||||||
val extraNodeLabels: MutableMap<InternalMockNetwork.MockNode, String> = Collections.synchronizedMap(HashMap())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterates the simulation by one step.
|
|
||||||
*
|
|
||||||
* The default implementation circles around the nodes, pumping until one of them handles a message. The next call
|
|
||||||
* will carry on from where this one stopped. In an environment where you want to take actions between anything
|
|
||||||
* interesting happening, or control the precise speed at which things operate (beyond the latency injector), this
|
|
||||||
* is a useful way to do things.
|
|
||||||
*
|
|
||||||
* @return the message that was processed, or null if no node accepted a message in this round.
|
|
||||||
*/
|
|
||||||
open fun iterate(): InMemoryMessagingNetwork.MessageTransfer? {
|
|
||||||
if (networkSendManuallyPumped) {
|
|
||||||
mockNet.messagingNetwork.pumpSend(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep going until one of the nodes has something to do, or we have checked every node.
|
|
||||||
val endpoints = mockNet.messagingNetwork.endpointsExternal
|
|
||||||
var countDown = endpoints.size
|
|
||||||
while (countDown > 0) {
|
|
||||||
val handledMessage = endpoints[pumpCursor].pumpReceive(false)
|
|
||||||
if (handledMessage != null)
|
|
||||||
return handledMessage
|
|
||||||
// If this node had nothing to do, advance the cursor with wraparound and try again.
|
|
||||||
pumpCursor = (pumpCursor + 1) % endpoints.size
|
|
||||||
countDown--
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun showProgressFor(nodes: List<StartedNode<InternalMockNetwork.MockNode>>) {
|
|
||||||
nodes.forEach { node ->
|
|
||||||
node.smm.changes.filter { it is StateMachineManager.Change.Add }.subscribe {
|
|
||||||
linkFlowProgress(node.internals, it.logic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun linkFlowProgress(node: InternalMockNetwork.MockNode, flow: FlowLogic<*>) {
|
|
||||||
val pt = flow.progressTracker ?: return
|
|
||||||
pt.changes.subscribe { change: ProgressTracker.Change ->
|
|
||||||
// Runs on node thread.
|
|
||||||
_allFlowSteps.onNext(Pair(node, change))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected fun showConsensusFor(nodes: List<InternalMockNetwork.MockNode>) {
|
|
||||||
val node = nodes.first()
|
|
||||||
node.started!!.smm.changes.filter { it is StateMachineManager.Change.Add }.first().subscribe {
|
|
||||||
linkConsensus(nodes, it.logic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun linkConsensus(nodes: Collection<InternalMockNetwork.MockNode>, flow: FlowLogic<*>) {
|
|
||||||
flow.progressTracker?.changes?.subscribe { _: ProgressTracker.Change ->
|
|
||||||
// Runs on node thread.
|
|
||||||
if (flow.progressTracker!!.currentStep == ProgressTracker.DONE) {
|
|
||||||
_doneSteps.onNext(nodes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val networkInitialisationFinished: CompletableFuture<Void> = allOf(*mockNet.nodes.map { it.nodeReadyFuture.toCompletableFuture() }.toTypedArray())
|
|
||||||
|
|
||||||
fun start(): Future<Unit> {
|
|
||||||
mockNet.startNodes()
|
|
||||||
// Wait for all the nodes to have finished registering with the network map service.
|
|
||||||
return networkInitialisationFinished.thenCompose { startMainSimulation() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sub-classes should override this to trigger whatever they want to simulate. This method will be invoked once the
|
|
||||||
* network bringup has been simulated.
|
|
||||||
*/
|
|
||||||
protected abstract fun startMainSimulation(): CompletableFuture<Unit>
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
mockNet.stopNodes()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
{
|
|
||||||
"fixedLeg": {
|
|
||||||
"fixedRatePayer": "GfHq2tTVk9z4eXgyUEefbHpUFfpnDvsFoZVZe3ikrLbwdRA4jebSJPykJwgw",
|
|
||||||
"notional": "$25000000",
|
|
||||||
"paymentFrequency": "SemiAnnual",
|
|
||||||
"effectiveDate": "2016-03-11",
|
|
||||||
"effectiveDateAdjustment": null,
|
|
||||||
"terminationDate": "2026-03-11",
|
|
||||||
"terminationDateAdjustment": null,
|
|
||||||
"fixedRate": {
|
|
||||||
"ratioUnit": {
|
|
||||||
"value": "0.01676"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dayCountBasisDay": "D30",
|
|
||||||
"dayCountBasisYear": "Y360",
|
|
||||||
"rollConvention": "ModifiedFollowing",
|
|
||||||
"dayInMonth": 10,
|
|
||||||
"paymentRule": "InArrears",
|
|
||||||
"paymentDelay": 0,
|
|
||||||
"paymentCalendar": "London",
|
|
||||||
"interestPeriodAdjustment": "Adjusted"
|
|
||||||
},
|
|
||||||
"floatingLeg": {
|
|
||||||
"floatingRatePayer": "GfHq2tTVk9z4eXgyMYwWRYKSgGpARSquPTt8V4Z54RmNe2SJ7BUq2jSUUfvT",
|
|
||||||
"notional": {
|
|
||||||
"quantity": 2500000000,
|
|
||||||
"token": "USD"
|
|
||||||
},
|
|
||||||
"paymentFrequency": "Quarterly",
|
|
||||||
"effectiveDate": "2016-03-11",
|
|
||||||
"effectiveDateAdjustment": null,
|
|
||||||
"terminationDate": "2026-03-11",
|
|
||||||
"terminationDateAdjustment": null,
|
|
||||||
"dayCountBasisDay": "D30",
|
|
||||||
"dayCountBasisYear": "Y360",
|
|
||||||
"rollConvention": "ModifiedFollowing",
|
|
||||||
"fixingRollConvention": "ModifiedFollowing",
|
|
||||||
"dayInMonth": 10,
|
|
||||||
"resetDayInMonth": 10,
|
|
||||||
"paymentRule": "InArrears",
|
|
||||||
"paymentDelay": 0,
|
|
||||||
"paymentCalendar": [ "London" ],
|
|
||||||
"interestPeriodAdjustment": "Adjusted",
|
|
||||||
"fixingPeriodOffset": 2,
|
|
||||||
"resetRule": "InAdvance",
|
|
||||||
"fixingsPerPayment": "Quarterly",
|
|
||||||
"fixingCalendar": [ "NewYork" ],
|
|
||||||
"index": "ICE LIBOR",
|
|
||||||
"indexSource": "Rates Service Provider",
|
|
||||||
"indexTenor": {
|
|
||||||
"name": "3M"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"calculation": {
|
|
||||||
"expression": "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
|
||||||
"floatingLegPaymentSchedule": {
|
|
||||||
},
|
|
||||||
"fixedLegPaymentSchedule": {
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"common": {
|
|
||||||
"baseCurrency": "EUR",
|
|
||||||
"eligibleCurrency": "EUR",
|
|
||||||
"eligibleCreditSupport": "Cash in an Eligible Currency",
|
|
||||||
"independentAmounts": "€0",
|
|
||||||
"threshold": "€0",
|
|
||||||
"minimumTransferAmount": "250000 EUR",
|
|
||||||
"rounding": "10000 EUR",
|
|
||||||
"valuationDateDescription": "Every Local Business Day",
|
|
||||||
"notificationTime": "2:00pm London",
|
|
||||||
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
|
||||||
"interestRate": {
|
|
||||||
"oracle": "C=ES,L=Madrid,O=Rates Service Provider",
|
|
||||||
"tenor": {
|
|
||||||
"name": "6M"
|
|
||||||
},
|
|
||||||
"ratioUnit": null,
|
|
||||||
"name": "EONIA"
|
|
||||||
},
|
|
||||||
"addressForTransfers": "",
|
|
||||||
"exposure": {},
|
|
||||||
"localBusinessDay": [ "London" , "NewYork" ],
|
|
||||||
"dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360",
|
|
||||||
"tradeID": "tradeXXX",
|
|
||||||
"hashLegalDocs": "put hash here"
|
|
||||||
},
|
|
||||||
"oracle": "oracleXXX"
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
Before Width: | Height: | Size: 302 KiB |
@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
|
|
||||||
<?import javafx.geometry.*?>
|
|
||||||
<?import javafx.scene.control.*?>
|
|
||||||
<?import javafx.scene.layout.*?>
|
|
||||||
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
|
|
||||||
prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
|
|
||||||
<children>
|
|
||||||
<HBox alignment="CENTER_RIGHT" prefHeight="0.0" prefWidth="600.0">
|
|
||||||
<children>
|
|
||||||
<Button fx:id="nextButton" mnemonicParsing="false" onAction="#onNextClicked" text="Next"/>
|
|
||||||
</children>
|
|
||||||
<padding>
|
|
||||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
|
|
||||||
</padding>
|
|
||||||
</HBox>
|
|
||||||
<SplitPane dividerPositions="0.2408026755852843" prefHeight="336.0" prefWidth="600.0" VBox.vgrow="ALWAYS">
|
|
||||||
<items>
|
|
||||||
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0"/>
|
|
||||||
<ScrollPane fx:id="mapView" hbarPolicy="NEVER" pannable="true" prefHeight="200.0" prefWidth="200.0"
|
|
||||||
vbarPolicy="NEVER"/>
|
|
||||||
</items>
|
|
||||||
</SplitPane>
|
|
||||||
</children>
|
|
||||||
</VBox>
|
|
@ -1,43 +0,0 @@
|
|||||||
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
|
||||||
|
|
||||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
||||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
|
||||||
|
|
||||||
-----------------------------------------------------------
|
|
||||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
||||||
-----------------------------------------------------------
|
|
||||||
|
|
||||||
PREAMBLE
|
|
||||||
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
|
|
||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
|
|
||||||
|
|
||||||
DEFINITIONS
|
|
||||||
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
|
|
||||||
|
|
||||||
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
|
|
||||||
|
|
||||||
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
|
|
||||||
|
|
||||||
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
|
|
||||||
|
|
||||||
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
|
|
||||||
|
|
||||||
PERMISSION & CONDITIONS
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
|
|
||||||
|
|
||||||
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
|
|
||||||
|
|
||||||
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
|
|
||||||
|
|
||||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
|
|
||||||
|
|
||||||
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
|
|
||||||
|
|
||||||
TERMINATION
|
|
||||||
This license becomes null and void if any of the above conditions are not met.
|
|
||||||
|
|
||||||
DISCLAIMER
|
|
||||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 342 KiB |
Binary file not shown.
Before Width: | Height: | Size: 469 KiB |
@ -1,228 +0,0 @@
|
|||||||
.root-pane {
|
|
||||||
-fx-font-family: "Source Sans Pro", sans-serif;
|
|
||||||
-fx-font-size: 12pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-bank {
|
|
||||||
-base-fill: #9e7bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-regulator {
|
|
||||||
-base-fill: #fff2d1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-network-service {
|
|
||||||
-base-fill: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-circle-inner {
|
|
||||||
-fx-fill: -base-fill;
|
|
||||||
-fx-stroke: derive(-base-fill, -40%);
|
|
||||||
-fx-stroke-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-circle-pulse {
|
|
||||||
-fx-fill: radial-gradient(center 50% 50%, radius 50%, #ffffff00, derive(-base-fill, 50%));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-sidebar-button {
|
|
||||||
-fx-background-color: linear-gradient(to left, #464646, derive(#1c1c1c, 10%));
|
|
||||||
-fx-min-width: 0;
|
|
||||||
-fx-text-fill: #ffffffaa;
|
|
||||||
-fx-alignment: top-right;
|
|
||||||
-fx-label-padding: 0;
|
|
||||||
-fx-padding: 0 10 0 10;
|
|
||||||
-fx-border-color: #00000066;
|
|
||||||
-fx-border-width: 1 0 1 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bullet {
|
|
||||||
-fx-fill: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-bank-to-bank {
|
|
||||||
-fx-fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-line {
|
|
||||||
-fx-stroke: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-bank-to-regulator {
|
|
||||||
-fx-stroke: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-label > Label, .node-status-label > Label {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
-fx-effect: dropshadow(gaussian, black, 10, 0.1, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hack around the Modena theme that makes all scroll panes grey by default */
|
|
||||||
.scroll-pane > .viewport {
|
|
||||||
-fx-background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-pane .scroll-bar {
|
|
||||||
-fx-background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flat-button {
|
|
||||||
-fx-background-color: white;
|
|
||||||
-fx-padding: 0 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flat-button:hover {
|
|
||||||
-fx-underline: true;
|
|
||||||
-fx-cursor: hand;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flat-button:focused {
|
|
||||||
-fx-font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fat-buttons Button {
|
|
||||||
-fx-padding: 10 15 10 15;
|
|
||||||
-fx-min-width: 100;
|
|
||||||
-fx-font-weight: bold;
|
|
||||||
-fx-base: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fat-buttons ChoiceBox {
|
|
||||||
-fx-padding: 4 8 4 8;
|
|
||||||
-fx-min-width: 100;
|
|
||||||
-fx-font-weight: bold;
|
|
||||||
-fx-base: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fat-buttons Button:default {
|
|
||||||
-fx-base: orange;
|
|
||||||
-fx-text-fill: white;
|
|
||||||
-fx-font-family: 'Source Sans Pro', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fat-buttons Button:cancel {
|
|
||||||
-fx-background-color: white;
|
|
||||||
-fx-background-insets: 1;
|
|
||||||
-fx-border-color: lightgray;
|
|
||||||
-fx-border-radius: 3;
|
|
||||||
-fx-text-fill: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fat-buttons Button:cancel:hover {
|
|
||||||
-fx-base: white;
|
|
||||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
|
||||||
-fx-text-fill: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** take out the focus ring */
|
|
||||||
.no-focus-button:focused {
|
|
||||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
|
||||||
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
|
|
||||||
-fx-background-radius: 3px, 3px, 2px, 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blue-button {
|
|
||||||
-fx-base: lightblue;
|
|
||||||
-fx-text-fill: darkslategrey;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blue-button:disabled {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.green-button {
|
|
||||||
-fx-base: #62c462;
|
|
||||||
-fx-text-fill: darkslategrey;
|
|
||||||
}
|
|
||||||
|
|
||||||
.green-button:disabled {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-button {
|
|
||||||
-fx-base: #66b2ff;
|
|
||||||
-fx-text-fill: white;
|
|
||||||
|
|
||||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
|
||||||
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
|
|
||||||
-fx-background-radius: 3px, 3px, 2px, 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.style-choice:default {
|
|
||||||
-fx-base: #66b2ff;
|
|
||||||
-fx-text-fill: white;
|
|
||||||
|
|
||||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
|
||||||
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
|
|
||||||
-fx-background-radius: 3px, 3px, 2px, 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-hbox {
|
|
||||||
-fx-background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-shadow-pane-horizontal {
|
|
||||||
/*-fx-background-color: linear-gradient(to top, #888, #fff);*/
|
|
||||||
-fx-background-color: white;
|
|
||||||
-fx-border-color: #555;
|
|
||||||
-fx-border-width: 0 0 1 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-label {
|
|
||||||
-fx-font-size: 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-label {
|
|
||||||
-fx-font-size: 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.splitter {
|
|
||||||
-fx-padding: 0;
|
|
||||||
-fx-background-color: #464646;
|
|
||||||
}
|
|
||||||
|
|
||||||
.splitter > .split-pane-divider {
|
|
||||||
-fx-background-color: linear-gradient(to left, #1c1c1c, transparent);
|
|
||||||
-fx-border-color: black;
|
|
||||||
-fx-border-width: 0 1 0 0;
|
|
||||||
-fx-padding: 0 2 0 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-tracker-cursor-box {
|
|
||||||
-fx-padding: 0 15 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-tracker-cursor {
|
|
||||||
-fx-translate-x: 15.0;
|
|
||||||
-fx-fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
-fx-background-color: #464646;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar > VBox > StackPane {
|
|
||||||
-fx-background-color: #666666;
|
|
||||||
-fx-padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar > VBox > StackPane > Label {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-tracker-widget-steps {
|
|
||||||
-fx-spacing: 5;
|
|
||||||
-fx-fill-width: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-tracker-widget-steps > StackPane {
|
|
||||||
-fx-background-color: #5a5a5a;
|
|
||||||
-fx-padding: 7px;
|
|
||||||
-fx-alignment: center-left;
|
|
||||||
-fx-max-height: 35px;
|
|
||||||
-fx-min-height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-tracker-widget-steps > StackPane > Label {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package net.corda.netmap.simulation
|
|
||||||
|
|
||||||
import net.corda.core.utilities.getOrThrow
|
|
||||||
import net.corda.testing.internal.LogHelper
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class IRSSimulationTest {
|
|
||||||
// TODO: These tests should be a lot more complete.
|
|
||||||
@Test
|
|
||||||
fun `runs to completion`() {
|
|
||||||
LogHelper.setLevel("+messages") // FIXME: Don't manipulate static state in tests.
|
|
||||||
val sim = IRSSimulation(false, false, null)
|
|
||||||
val future = sim.start()
|
|
||||||
while (!future.isDone) {
|
|
||||||
sim.iterate()
|
|
||||||
}
|
|
||||||
future.getOrThrow()
|
|
||||||
}
|
|
||||||
}
|
|
@ -51,7 +51,6 @@ include 'samples:trader-demo'
|
|||||||
include 'samples:irs-demo'
|
include 'samples:irs-demo'
|
||||||
include 'samples:irs-demo:cordapp'
|
include 'samples:irs-demo:cordapp'
|
||||||
include 'samples:irs-demo:web'
|
include 'samples:irs-demo:web'
|
||||||
include 'samples:network-visualiser'
|
|
||||||
include 'samples:simm-valuation-demo'
|
include 'samples:simm-valuation-demo'
|
||||||
include 'samples:simm-valuation-demo:flows'
|
include 'samples:simm-valuation-demo:flows'
|
||||||
include 'samples:simm-valuation-demo:contracts-states'
|
include 'samples:simm-valuation-demo:contracts-states'
|
||||||
|
Loading…
Reference in New Issue
Block a user