diff --git a/pom.xml b/pom.xml index 1a37142e64dcde75bec6b23cb852f4fcb0089deb..05d1ca3a064ed4aec8cf691471fb7f6185e84c04 100644 --- a/pom.xml +++ b/pom.xml @@ -56,5 +56,12 @@ <artifactId>reflections</artifactId> <version>0.9.10</version> </dependency> + <!-- https://mvnrepository.com/artifact/org.gephi/gephi-toolkit --> + <dependency> + <groupId>org.gephi</groupId> + <artifactId>gephi-toolkit</artifactId> + <version>0.9.1</version> + </dependency> + </dependencies> </project> \ No newline at end of file diff --git a/src/main/java/fucoin/actions/control/ActionAddOverlayNeighbours.java b/src/main/java/fucoin/actions/control/ActionAddOverlayNeighbours.java new file mode 100644 index 0000000000000000000000000000000000000000..890b6ec06ec9a938bc086a79047d4c8973df6b19 --- /dev/null +++ b/src/main/java/fucoin/actions/control/ActionAddOverlayNeighbours.java @@ -0,0 +1,26 @@ +package fucoin.actions.control; + +import akka.actor.ActorRef; +import akka.actor.UntypedActorContext; +import fucoin.actions.ClientAction; +import fucoin.wallet.AbstractWallet; + +import java.util.Collections; +import java.util.List; + +/** + * @author davidbohn + */ +public class ActionAddOverlayNeighbours extends ClientAction { + + protected List<ActorRef> neighbours; + + public ActionAddOverlayNeighbours(List<ActorRef> neighbours) { + this.neighbours = neighbours; + } + + @Override + protected void onAction(ActorRef sender, ActorRef self, UntypedActorContext context, AbstractWallet abstractNode) { + neighbours.stream().forEach(abstractNode::addOverlayNeighbour); + } +} diff --git a/src/main/java/fucoin/actions/join/ActionJoinAnswer.java b/src/main/java/fucoin/actions/join/ActionJoinAnswer.java index 89c4aadba026ce04ef9b2190b21566fdb7e68874..4b6cdd2f96bed048e45ba74ed5e81927f4e6f29b 100644 --- a/src/main/java/fucoin/actions/join/ActionJoinAnswer.java +++ b/src/main/java/fucoin/actions/join/ActionJoinAnswer.java @@ -28,7 +28,7 @@ public class ActionJoinAnswer extends ClientAction { protected void onAction(ActorRef sender, ActorRef self, UntypedActorContext context, AbstractWallet wallet) { - wallet.addLogMsg("Addressed to " + self.path().name() + " from " + sender.path().name() + ": someNeighbors:" + someNeighbors); + //wallet.addLogMsg("Addressed to " + self.path().name() + " from " + sender.path().name() + ": someNeighbors:" + someNeighbors); // your neighbours? my neighbours! for (Entry<String, ActorRef> neighbor : someNeighbors.entrySet()) { diff --git a/src/main/java/fucoin/actions/transaction/ActionCommitDistributedCommittedTransfer.java b/src/main/java/fucoin/actions/transaction/ActionCommitDistributedCommittedTransfer.java index 641fc121d4a38b970df45fb99e3d9c179bc56ad6..30c236cce5ebddbac4fbe059253555c503c36c0c 100644 --- a/src/main/java/fucoin/actions/transaction/ActionCommitDistributedCommittedTransfer.java +++ b/src/main/java/fucoin/actions/transaction/ActionCommitDistributedCommittedTransfer.java @@ -43,7 +43,7 @@ public class ActionCommitDistributedCommittedTransfer extends ClientAction { @Override protected void onAction(ActorRef sender, ActorRef self, UntypedActorContext context, AbstractWallet wallet) { - wallet.addLogMsg("ActionCommitDistributedCommittedTransfer is granted? " + granted); + //wallet.addLogMsg("ActionCommitDistributedCommittedTransfer is granted? " + granted); if (granted) { if (source.compareTo(self) == 0) { @@ -75,7 +75,6 @@ public class ActionCommitDistributedCommittedTransfer extends ClientAction { } } - //wallet.addLogMsg("wallet.amounts:" + wallet.amounts); } } diff --git a/src/main/java/fucoin/actions/transaction/ActionInvokeDistributedCommittedTransfer.java b/src/main/java/fucoin/actions/transaction/ActionInvokeDistributedCommittedTransfer.java index b277c64a9c613d842769089275996769947c0f60..82490ee04492fc29e4741b02128160d9b9f8fa83 100644 --- a/src/main/java/fucoin/actions/transaction/ActionInvokeDistributedCommittedTransfer.java +++ b/src/main/java/fucoin/actions/transaction/ActionInvokeDistributedCommittedTransfer.java @@ -28,9 +28,9 @@ public class ActionInvokeDistributedCommittedTransfer extends CoordinatorTransac @Override protected void onAction(ActorRef sender, ActorRef self, UntypedActorContext context, SuperVisorImpl superVisor) { - superVisor.addLogMsg("invoke transaction " + source.path().name() + + /*superVisor.addLogMsg("invoke transaction " + source.path().name() + " sends " + amount + - " to " + target.path().name()); + " to " + target.path().name());*/ long timeout = System.currentTimeMillis() + 500; diff --git a/src/main/java/fucoin/actions/transaction/ActionPrepareDistributedCommittedTransferAnswer.java b/src/main/java/fucoin/actions/transaction/ActionPrepareDistributedCommittedTransferAnswer.java index db21831de14d3e95c2d30f335586f35052dc307c..d7c9f45f6d58c11eaa6895e4ad5633f42c8ae83c 100644 --- a/src/main/java/fucoin/actions/transaction/ActionPrepareDistributedCommittedTransferAnswer.java +++ b/src/main/java/fucoin/actions/transaction/ActionPrepareDistributedCommittedTransferAnswer.java @@ -28,7 +28,7 @@ public class ActionPrepareDistributedCommittedTransferAnswer extends Coordinator protected void onAction(ActorRef sender, ActorRef self, UntypedActorContext context, SuperVisorImpl superVisor) { - superVisor.addLogMsg("granted?" + granted); + //superVisor.addLogMsg("granted?" + granted); DistributedCommittedTransferRequest request = superVisor.getRequest(id); diff --git a/src/main/java/fucoin/configurations/AbstractConfiguration.java b/src/main/java/fucoin/configurations/AbstractConfiguration.java index 8493704af98937d47688ad0b57323902394e0d8d..eba6a5e4a811f663cfa488cfa56d397738c93b2e 100644 --- a/src/main/java/fucoin/configurations/AbstractConfiguration.java +++ b/src/main/java/fucoin/configurations/AbstractConfiguration.java @@ -2,9 +2,11 @@ package fucoin.configurations; import akka.actor.ActorRef; import akka.actor.Props; +import akka.dispatch.Futures; import akka.pattern.Patterns; import akka.util.Timeout; import fucoin.AbstractNode; +import fucoin.actions.control.ActionAddOverlayNeighbours; import fucoin.actions.control.ActionAnnounceWalletCreation; import fucoin.actions.control.ActionWalletSendMoney; import fucoin.actions.join.ActionTellSupervisor; @@ -12,16 +14,20 @@ import fucoin.actions.transaction.ActionGetAmount; import fucoin.actions.transaction.ActionGetAmountAnswer; import fucoin.actions.transaction.ActionNotifyObserver; import fucoin.configurations.internal.ConfigurationCreator; +import fucoin.configurations.internal.NodeHelper; import fucoin.supervisor.SuperVisorImpl; import fucoin.wallet.WalletImpl; +import org.gephi.graph.api.Edge; +import org.gephi.graph.api.Graph; +import org.gephi.graph.api.Node; import scala.concurrent.Await; import scala.concurrent.Future; +import scala.concurrent.Promise; import scala.concurrent.duration.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; /** * @@ -30,11 +36,14 @@ public abstract class AbstractConfiguration extends AbstractNode { private ActorRef superVisor; - private final List<ActorRef> activeActors = new ArrayList<>(); + private final HashMap<String, ActorRef> activeActors = new HashMap<>(); private Timeout timeout = new Timeout(Duration.create(10, "seconds")); private int remainingTransactions; + private Promise<Void> transactionFinished; + + private ActorRef lastWallet = null; public static Props props(Class configurationClass) { @@ -58,21 +67,18 @@ public abstract class AbstractConfiguration extends AbstractNode { */ private ActorRef createWallet(String name, boolean createGUI) { Props props; - int numOfWallets = activeActors.size(); - if (numOfWallets == 0) { - props = WalletImpl.props(null, name, createGUI); - } else { - props = WalletImpl.props(activeActors.get(numOfWallets - 1), name, createGUI); - } - ActorRef actorRef = context().actorOf(props, name); + props = WalletImpl.props(lastWallet, name, createGUI); - activeActors.add(actorRef); + ActorRef actorRef = context().actorOf(props, name); + activeActors.put(name, actorRef); - if (numOfWallets == 0) { + if (lastWallet == null) { actorRef.tell(new ActionTellSupervisor(superVisor), superVisor); } + lastWallet = actorRef; + return actorRef; } @@ -90,25 +96,40 @@ public abstract class AbstractConfiguration extends AbstractNode { Await.result(future, timeout.duration()); } + public void spawnWalletsFromNodes(Collection<Node> nodes, boolean createGUI) throws Exception { + Future<Object> future = Patterns.ask(superVisor, new ActionAnnounceWalletCreation(nodes.size(), self()), timeout); + for (Node node : nodes) { + String nameOfTheWallet = NodeHelper.nameOfNode(node); + createWallet(nameOfTheWallet, createGUI); + } + Await.result(future, timeout.duration()); + } + /** * Fetch a random wallet */ public ActorRef getRandomWallet() { - return activeActors.get(ThreadLocalRandom.current().nextInt(activeActors.size())); + return wallets().get(ThreadLocalRandom.current().nextInt(activeActors.size())); } public List<ActorRef> wallets() { - return this.activeActors; + return new ArrayList<>(this.activeActors.values()); } - protected void randomTransactions(int number, int maxTransactionsAtTheSameTime) { + public ActorRef walletByName(String name) { + return activeActors.get(name); + } + protected Future<Void> randomTransactions(int number, int maxTransactionsAtTheSameTime) { + this.transactionFinished = Futures.promise(); remainingTransactions = number; for (int i = 0; i < Math.min(number, maxTransactionsAtTheSameTime); i++) { nextRandomTransaction(); } + return transactionFinished.future(); + } private void nextRandomTransaction() { @@ -116,8 +137,9 @@ public abstract class AbstractConfiguration extends AbstractNode { try { randomTransaction(); } catch (Exception e) { - System.err.println("Error while trying to perform a random transaction: "+e.getMessage()); + System.err.println("Error while trying to perform a random transaction: " + e.getMessage()); remainingTransactions = 0; + transactionFinished = null; } } @@ -169,6 +191,11 @@ public abstract class AbstractConfiguration extends AbstractNode { if (remainingTransactions > 0) { nextRandomTransaction(); + } else { + if (transactionFinished != null) { + transactionFinished.success(null); + transactionFinished = null; + } } } } @@ -181,4 +208,35 @@ public abstract class AbstractConfiguration extends AbstractNode { } public abstract void run(); + + protected void createOverlayNetwork(Graph g) { + Collection<Node> nodes = g.getNodes().toCollection(); + + try { + spawnWalletsFromNodes(nodes, false); + } catch (Exception e) { + e.printStackTrace(); + } + + nodes.stream().forEach(node -> { + ActorRef wallet = getWalletForNode(node); + Edge[] edges = g.getEdges(node).toArray(); + + // Search for all reachable neighbours of the node + // by filtering the list of incident edges of the node + // and retrieve the respective ActorRef instance + List<ActorRef> overlayNeighbours = Arrays.stream(edges).filter(edge -> !edge.isDirected() || edge.getSource() == node).map(edge -> { + if (edge.getSource() == node) { + return edge.getTarget(); + } + return edge.getSource(); + }).map(this::getWalletForNode).collect(Collectors.toList()); + + wallet.tell(new ActionAddOverlayNeighbours(overlayNeighbours), self()); + }); + } + + protected ActorRef getWalletForNode(Node node) { + return walletByName(NodeHelper.nameOfNode(node)); + } } diff --git a/src/main/java/fucoin/configurations/DefaultConfiguration.java b/src/main/java/fucoin/configurations/DefaultConfiguration.java index 403d071f351c51d413d9e38fd07331f52625a13f..6b5a3565cae995040e504ae510eef27c74a2b158 100644 --- a/src/main/java/fucoin/configurations/DefaultConfiguration.java +++ b/src/main/java/fucoin/configurations/DefaultConfiguration.java @@ -1,19 +1,11 @@ package fucoin.configurations; import akka.actor.ActorRef; -import akka.pattern.Patterns; import akka.util.Timeout; import fucoin.actions.control.ActionWalletSendMoney; -import fucoin.actions.transaction.ActionGetAmount; -import fucoin.actions.transaction.ActionGetAmountAnswer; -import fucoin.actions.transaction.ActionNotifyObserver; import fucoin.configurations.internal.ConfigurationName; -import scala.concurrent.Await; -import scala.concurrent.Future; import scala.concurrent.duration.Duration; -import java.util.Collections; -import java.util.List; import java.util.concurrent.ThreadLocalRandom; /** diff --git a/src/main/java/fucoin/configurations/GephiConfiguration.java b/src/main/java/fucoin/configurations/GephiConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..bdf2360515b9a9662276ee73bd09a6b6c04dd0bc --- /dev/null +++ b/src/main/java/fucoin/configurations/GephiConfiguration.java @@ -0,0 +1,69 @@ +package fucoin.configurations; + +import akka.actor.ActorRef; +import akka.dispatch.OnSuccess; +import akka.pattern.Patterns; +import akka.util.Timeout; +import fucoin.actions.transaction.ActionGetAmount; +import fucoin.actions.transaction.ActionGetAmountAnswer; +import fucoin.configurations.internal.ConfigurationName; +import fucoin.configurations.internal.GephiLoader; +import fucoin.configurations.internal.NodeHelper; +import fucoin.gui.gephi.GephiFileSelector; +import fucoin.gui.gephi.GraphWindow; +import org.gephi.graph.api.Graph; +import org.gephi.graph.api.Node; +import scala.concurrent.Future; +import scala.concurrent.duration.Duration; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; + +@ConfigurationName("Gephi Test Configuration") +public class GephiConfiguration extends AbstractConfiguration { + + private Timeout timeout = new Timeout(Duration.create(10, "seconds")); + + @Override + public void run() { + initSupervisor(); + + GephiLoader gephiLoader = new GephiLoader(); + GephiFileSelector fileSelector = new GephiFileSelector(); + + Graph g; + File selectedTopology; + try { + selectedTopology = fileSelector.selectTopology(); + g = gephiLoader.loadFile(selectedTopology); + } catch (URISyntaxException | IOException e) { + e.printStackTrace(); + return; + } + + createOverlayNetwork(g); + + GraphWindow graphWindow = new GraphWindow(); + graphWindow.setDisplayedFilename(selectedTopology.getName()); + graphWindow.setVisible(true); + + // add a click listener for displaying further information about a wallet when clicking on a node + graphWindow.addNodeClickHandler((node, event) -> { + + // get associated wallet and ask for its amount + ActorRef wallet = getWalletForNode(node); + + Future<Object> future = Patterns.ask(wallet, new ActionGetAmount(), timeout); + future.onSuccess(new OnSuccess<Object>() { + @Override + public void onSuccess(Object result) throws Throwable { + // display the amount when an answer is received + ActionGetAmountAnswer answer = (ActionGetAmountAnswer) result; + graphWindow.setInfobarText(NodeHelper.nameOfNode(node)+" has "+answer.amount+" FUCs"); + } + }, context().dispatcher()); + }); + + } +} diff --git a/src/main/java/fucoin/configurations/MassWalletConfiguration.java b/src/main/java/fucoin/configurations/MassWalletConfiguration.java index fcaaadf3cea173e7b0cf3fa6fc038fbee5dcecae..14b4979a14ef1cf6abf82476a6e3bf6b0906aa6c 100644 --- a/src/main/java/fucoin/configurations/MassWalletConfiguration.java +++ b/src/main/java/fucoin/configurations/MassWalletConfiguration.java @@ -1,5 +1,7 @@ package fucoin.configurations; +import akka.dispatch.OnSuccess; +import fucoin.actions.transaction.ActionGetAmountAnswer; import fucoin.configurations.internal.ConfigurationName; /** @@ -11,13 +13,20 @@ public class MassWalletConfiguration extends AbstractConfiguration { public void run() { initSupervisor(); try { - spawnWallets(200, false); + spawnWallets(2, false); System.out.println("Wallet spawning done!"); } catch (Exception e) { System.out.println("Wallet spawning timed out!"); } - randomTransactions(100, 10); + randomTransactions(5, 2).onSuccess(new OnSuccess<Void>() { + @Override + public void onSuccess(Void result) { + // You can start your algorithm here if you want to. + // Alternatively, you can also notify the user that all transactions are finished + System.out.println("All random transactions finished!"); + } + }, context().dispatcher()); } @Override diff --git a/src/main/java/fucoin/configurations/internal/GephiLoader.java b/src/main/java/fucoin/configurations/internal/GephiLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..b88f56f707473f2c03cca38f1f6ff0ed0c7e6b51 --- /dev/null +++ b/src/main/java/fucoin/configurations/internal/GephiLoader.java @@ -0,0 +1,54 @@ +package fucoin.configurations.internal; + +import org.gephi.graph.api.Graph; +import org.gephi.graph.api.GraphController; +import org.gephi.io.importer.api.Container; +import org.gephi.io.importer.api.ImportController; +import org.gephi.io.processor.plugin.DefaultProcessor; +import org.gephi.project.api.ProjectController; +import org.gephi.project.api.Workspace; +import org.openide.util.Lookup; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.URISyntaxException; + +public class GephiLoader { + + private Workspace workspace; + + /** + * Load a graph file that is in the resources directory of the application + * + * @throws URISyntaxException + * @throws FileNotFoundException + */ + public Graph loadFileFromResources(String path) throws URISyntaxException, FileNotFoundException { + return loadFile(new File(getClass().getResource(path).toURI())); + } + + /** + * Initialize the Gephi toolkit and load the provided file + * + * @throws FileNotFoundException + */ + public Graph loadFile(File file) throws FileNotFoundException { + ProjectController pc = Lookup.getDefault().lookup(ProjectController.class); + + pc.newProject(); + workspace = pc.getCurrentWorkspace(); + + Container container; + ImportController importController = Lookup.getDefault().lookup(ImportController.class); + container = importController.importFile(file); + + + importController.process(container, new DefaultProcessor(), workspace); + + return Lookup.getDefault().lookup(GraphController.class).getGraphModel(workspace).getGraph(); + } + + public Workspace getWorkspace() { + return workspace; + } +} diff --git a/src/main/java/fucoin/configurations/internal/NodeHelper.java b/src/main/java/fucoin/configurations/internal/NodeHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..359dcdeccf4246018b90aea548ed954fdd9d10f4 --- /dev/null +++ b/src/main/java/fucoin/configurations/internal/NodeHelper.java @@ -0,0 +1,23 @@ +package fucoin.configurations.internal; + +import org.gephi.graph.api.Node; + +public class NodeHelper { + + public static String nameOfNode(Node n) { + String label = n.getLabel(); + + if (label == null || label.isEmpty()) { + return "Wallet" + n.getId(); + } + + label = label.replace(" ", "_"); + + if (Character.isDigit(label.charAt(0))) { + return "Wallet" + label; + } + + return label; + } + +} diff --git a/src/main/java/fucoin/gui/gephi/GephiFileSelector.java b/src/main/java/fucoin/gui/gephi/GephiFileSelector.java new file mode 100644 index 0000000000000000000000000000000000000000..d4196a616dbd70baaa0eb30f73df6dadbd9720c9 --- /dev/null +++ b/src/main/java/fucoin/gui/gephi/GephiFileSelector.java @@ -0,0 +1,47 @@ +package fucoin.gui.gephi; + +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +public class GephiFileSelector { + + /** + * Display a file selector to select a gephi graph file + * @return the selected file or the first graph in the resources directory if none selected + * @throws IOException + * @throws URISyntaxException + */ + public File selectTopology() throws IOException, URISyntaxException { + JFileChooser fileChooser = new JFileChooser(new File(getClass().getResource("/").toURI())); + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + FileNameExtensionFilter fileNameExtensionFilter = new FileNameExtensionFilter("Graph Files", "gexf"); + fileChooser.setFileFilter(fileNameExtensionFilter); + int result = fileChooser.showOpenDialog(null); + + if (result == JFileChooser.APPROVE_OPTION) { + return fileChooser.getSelectedFile(); + } + + return getBundledTopologies().get(0); + } + + /** + * Get a list of all bundled topology files (i.e. that are stored in the resources) + * @throws URISyntaxException + * @throws IOException + */ + public List<File> getBundledTopologies() throws URISyntaxException, IOException { + return Files.list(Paths.get(getClass().getResource("/").toURI())) + .filter(Files::isRegularFile) + .filter(path -> path.toString().toLowerCase().endsWith(".gexf")) + .map(Path::toFile).collect(Collectors.toList()); + } +} diff --git a/src/main/java/fucoin/gui/gephi/GraphWindow.java b/src/main/java/fucoin/gui/gephi/GraphWindow.java new file mode 100644 index 0000000000000000000000000000000000000000..a0cfeed6b853293982c550b4af950f3cb4dee147 --- /dev/null +++ b/src/main/java/fucoin/gui/gephi/GraphWindow.java @@ -0,0 +1,146 @@ +package fucoin.gui.gephi; + +import org.gephi.graph.api.Node; +import org.gephi.preview.api.*; +import org.gephi.preview.types.DependantOriginalColor; +import org.gephi.project.api.Workspace; +import org.openide.util.Lookup; + +import javax.swing.*; +import java.awt.*; +import java.lang.reflect.Field; +import java.util.*; +import java.util.List; +import java.util.Timer; + +public class GraphWindow extends JFrame implements NodeMouseListener { + + protected List<NodeClickHandler> clickHandlers = new ArrayList<>(); + private final PreviewController previewController; + private final PreviewSketch previewSketch; + + private JLabel infobarText; + private Timer timer; + + private final String baseWindowTitle = "Network Overlay Graph"; + private final String defaultInfoBarText = "Click on a node to see further information."; + + public GraphWindow() { + super(); + + timer = new Timer(); + + setTitle(baseWindowTitle); + + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + + previewController = Lookup.getDefault().lookup(PreviewController.class); + PreviewModel previewModel = previewController.getModel(); + previewModel.getProperties().putValue(PreviewProperty.SHOW_NODE_LABELS, Boolean.TRUE); + previewModel.getProperties().putValue(PreviewProperty.NODE_LABEL_COLOR, new DependantOriginalColor(Color.BLACK)); + + Font labelFont = new Font("Verdana", Font.PLAIN, 5); + previewModel.getProperties().putValue(PreviewProperty.NODE_LABEL_FONT, labelFont); + previewModel.getProperties().putValue(PreviewProperty.EDGE_CURVED, Boolean.FALSE); + previewModel.getProperties().putValue(PreviewProperty.EDGE_OPACITY, 50); + previewModel.getProperties().putValue(PreviewProperty.EDGE_RADIUS, 10f); + previewModel.getProperties().putValue("graphWindow.mouse.handler", this); + + G2DTarget target = (G2DTarget) previewController.getRenderTarget(RenderTarget.G2D_TARGET); + previewSketch = new PreviewSketch(target, isRetina()); + + infobarText = new JLabel(defaultInfoBarText, SwingConstants.LEFT); + + this.add(previewSketch, BorderLayout.CENTER); + + JPanel infobar = new JPanel(new BorderLayout()); + infobar.add(infobarText, BorderLayout.WEST); + + JPanel zoomOptions = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 2)); + zoomOptions.add(new JLabel("Zoom: ")); + + Dimension zoomBtnSize = new Dimension(20, 20); + + JButton minusButton = new JButton("-"); + minusButton.setPreferredSize(zoomBtnSize); + minusButton.addActionListener(e -> previewSketch.zoomMinus()); + zoomOptions.add(minusButton); + + JButton resetButton = new JButton("0"); + resetButton.setPreferredSize(zoomBtnSize); + resetButton.addActionListener(e -> previewSketch.resetZoom()); + zoomOptions.add(resetButton); + + JButton plusButton = new JButton("+"); + plusButton.setPreferredSize(zoomBtnSize); + plusButton.addActionListener(e -> previewSketch.zoomPlus()); + zoomOptions.add(plusButton); + + infobar.add(zoomOptions, BorderLayout.EAST); + this.add(infobar, BorderLayout.SOUTH); + + previewController.refreshPreview(); + previewSketch.resetZoom(); + + + this.setSize(800, 600); + } + + public void setDisplayedFilename(String filename) { + setTitle(baseWindowTitle + " - " + filename); + } + + public static boolean isRetina() { + GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); + final GraphicsDevice device = env.getDefaultScreenDevice(); + + try { + Field field = device.getClass().getDeclaredField("scale"); + + if (field != null) { + field.setAccessible(true); + Object scale = field.get(device); + + if (scale instanceof Integer && (Integer) scale == 2) { + return true; + } + } + } catch (Exception ignore) { + } + return false; + } + + @Override + public void mouseClicked(Node node, PreviewMouseEvent event, PreviewProperties properties, Workspace workspace) { + clickHandlers.stream().forEach(nodeClickHandler -> nodeClickHandler.accept(node, event)); + } + + public void addNodeClickHandler(NodeClickHandler handler) { + clickHandlers.add(handler); + } + + @Override + public void setVisible(boolean b) { + super.setVisible(b); + + previewController.refreshPreview(); + previewSketch.refreshSketch(); + } + + /** + * Sets the displayed text of the infobar to text. + * After a certain time the text will be reset to the default text. + * + * @param text new infobar text + */ + public void setInfobarText(String text) { + SwingUtilities.invokeLater(() -> infobarText.setText(text)); + // set text back to default text after 2 seconds + timer.schedule(new TimerTask() { + @Override + public void run() { + SwingUtilities.invokeLater(() -> infobarText.setText(defaultInfoBarText)); + } + }, 2000); + } +} diff --git a/src/main/java/fucoin/gui/gephi/ItemBuilderTemplate.java b/src/main/java/fucoin/gui/gephi/ItemBuilderTemplate.java new file mode 100644 index 0000000000000000000000000000000000000000..d54941d98e7424b40b94ccf4c76cf52b8aed0016 --- /dev/null +++ b/src/main/java/fucoin/gui/gephi/ItemBuilderTemplate.java @@ -0,0 +1,20 @@ +package fucoin.gui.gephi; + +import org.gephi.graph.api.Graph; +import org.gephi.preview.api.Item; +import org.gephi.preview.spi.ItemBuilder; +import org.openide.util.lookup.ServiceProvider; + + +@ServiceProvider(service = ItemBuilder.class) +public class ItemBuilderTemplate implements ItemBuilder { + @Override + public Item[] getItems(Graph graph) { + return new Item[0]; + } + + @Override + public String getType() { + return null; + } +} diff --git a/src/main/java/fucoin/gui/gephi/LabelItem.java b/src/main/java/fucoin/gui/gephi/LabelItem.java new file mode 100644 index 0000000000000000000000000000000000000000..b4a123309ab4af6d32e452a2d255e18442ebf9e0 --- /dev/null +++ b/src/main/java/fucoin/gui/gephi/LabelItem.java @@ -0,0 +1,36 @@ +package fucoin.gui.gephi; + +import org.gephi.graph.api.Node; +import org.gephi.preview.api.Item; + +public class LabelItem implements Item { + Node node; + + public LabelItem(Node node) { + this.node = node; + } + + @Override + public Object getSource() { + return node; + } + + @Override + public String getType() { + return "label.sometype"; + } + + @Override + public <D> D getData(String key) { + return null; + } + + @Override + public void setData(String key, Object value) { + } + + @Override + public String[] getKeys() { + return new String[0]; + } +} diff --git a/src/main/java/fucoin/gui/gephi/MouseListenerTemplate.java b/src/main/java/fucoin/gui/gephi/MouseListenerTemplate.java new file mode 100644 index 0000000000000000000000000000000000000000..5e5d39ae4bca7ea7f7b7b90cb2a21d9d7001003a --- /dev/null +++ b/src/main/java/fucoin/gui/gephi/MouseListenerTemplate.java @@ -0,0 +1,48 @@ +package fucoin.gui.gephi; + +import org.gephi.graph.api.GraphController; +import org.gephi.graph.api.Node; +import org.gephi.preview.api.PreviewMouseEvent; +import org.gephi.preview.api.PreviewProperties; +import org.gephi.preview.spi.PreviewMouseListener; +import org.gephi.project.api.Workspace; +import org.openide.util.Lookup; +import org.openide.util.lookup.ServiceProvider; + +@ServiceProvider(service = PreviewMouseListener.class) +public class MouseListenerTemplate implements PreviewMouseListener { + @Override + public void mouseClicked(PreviewMouseEvent event, PreviewProperties properties, Workspace workspace) { + for (Node node : Lookup.getDefault().lookup(GraphController.class).getGraphModel(workspace).getGraph().getNodes()) { + if (clickingInNode(node, event)) { + if (properties.hasProperty("graphWindow.mouse.handler")) { + NodeMouseListener listener = properties.getValue("graphWindow.mouse.handler"); + listener.mouseClicked(node, event, properties, workspace); + } + event.setConsumed(true); + return; + } + } + event.setConsumed(true);//So the renderer is executed and the graph repainted + } + + @Override + public void mousePressed(PreviewMouseEvent event, PreviewProperties properties, Workspace workspace) { + } + + @Override + public void mouseDragged(PreviewMouseEvent event, PreviewProperties properties, Workspace workspace) { + } + + @Override + public void mouseReleased(PreviewMouseEvent event, PreviewProperties properties, Workspace workspace) { + } + + private boolean clickingInNode(Node node, PreviewMouseEvent event) { + float xdiff = node.x() - event.x; + float ydiff = -node.y() - event.y;//Note that y axis is inverse for node coordinates + float radius = node.size(); + + return xdiff * xdiff + ydiff * ydiff < radius * radius; + } +} diff --git a/src/main/java/fucoin/gui/gephi/NodeClickHandler.java b/src/main/java/fucoin/gui/gephi/NodeClickHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..9005017dddecabfb10636be551afaef1883b868c --- /dev/null +++ b/src/main/java/fucoin/gui/gephi/NodeClickHandler.java @@ -0,0 +1,10 @@ +package fucoin.gui.gephi; + +import org.gephi.graph.api.Node; +import org.gephi.preview.api.PreviewMouseEvent; + +import java.util.function.BiConsumer; + + +public interface NodeClickHandler extends BiConsumer<Node, PreviewMouseEvent> { +} diff --git a/src/main/java/fucoin/gui/gephi/NodeMouseListener.java b/src/main/java/fucoin/gui/gephi/NodeMouseListener.java new file mode 100644 index 0000000000000000000000000000000000000000..348028b82f5f55183da6cc66b3caae928b2286af --- /dev/null +++ b/src/main/java/fucoin/gui/gephi/NodeMouseListener.java @@ -0,0 +1,10 @@ +package fucoin.gui.gephi; + +import org.gephi.graph.api.Node; +import org.gephi.preview.api.PreviewMouseEvent; +import org.gephi.preview.api.PreviewProperties; +import org.gephi.project.api.Workspace; + +public interface NodeMouseListener { + public void mouseClicked(Node node, PreviewMouseEvent event, PreviewProperties properties, Workspace workspace); +} diff --git a/src/main/java/fucoin/gui/gephi/PreviewSketch.java b/src/main/java/fucoin/gui/gephi/PreviewSketch.java new file mode 100644 index 0000000000000000000000000000000000000000..4f324bc99fe7dd900b002db11d56247d34cfb8ba --- /dev/null +++ b/src/main/java/fucoin/gui/gephi/PreviewSketch.java @@ -0,0 +1,235 @@ +package fucoin.gui.gephi; + +import java.awt.Graphics; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +import org.gephi.preview.api.G2DTarget; +import org.gephi.preview.api.PreviewController; +import org.gephi.preview.api.PreviewMouseEvent; +import org.gephi.preview.api.Vector; +import org.openide.util.Lookup; + +public class PreviewSketch extends JPanel implements MouseListener, MouseWheelListener, MouseMotionListener { + private static final int WHEEL_TIMER = 500; + //Data + private final PreviewController previewController; + private final G2DTarget target; + //Geometry + private final Vector ref = new Vector(); + private final Vector lastMove = new Vector(); + //Utils + private final RefreshLoop refreshLoop = new RefreshLoop(); + private Timer wheelTimer; + private boolean inited; + private final boolean isRetina; + + public PreviewSketch(G2DTarget target, boolean retina) { + this.target = target; + previewController = Lookup.getDefault().lookup(PreviewController.class); + isRetina = retina; + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + if (!inited) { + //Listeners + addMouseListener(this); + addMouseMotionListener(this); + addMouseWheelListener(this); + inited = true; + } + + int width = (int) (getWidth() * (isRetina ? 2.0 : 1.0)); + int height = (int) (getHeight() * (isRetina ? 2.0 : 1.0)); + + if (target.getWidth() != width || target.getHeight() != height) { + target.resize(width, height); + } + + g.drawImage(target.getImage(), 0, 0, getWidth(), getHeight(), this); + } + + public void refreshSketch() { + refreshLoop.refreshSketch(); + } + + public void setMoving(boolean moving) { + target.setMoving(moving); + } + + @Override + public void mouseClicked(MouseEvent e) { + if (previewController.sendMouseEvent(buildPreviewMouseEvent(e, PreviewMouseEvent.Type.CLICKED))) { + refreshLoop.refreshSketch(); + } + } + + @Override + public void mousePressed(MouseEvent e) { + previewController.sendMouseEvent(buildPreviewMouseEvent(e, PreviewMouseEvent.Type.PRESSED)); + ref.set(e.getX(), e.getY()); + lastMove.set(target.getTranslate()); + + refreshLoop.refreshSketch(); + } + + @Override + public void mouseReleased(MouseEvent e) { + if (!previewController.sendMouseEvent(buildPreviewMouseEvent(e, PreviewMouseEvent.Type.RELEASED))) { + setMoving(false); + } + + refreshLoop.refreshSketch(); + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (e.getUnitsToScroll() == 0) { + return; + } + float way = -e.getUnitsToScroll() / Math.abs(e.getUnitsToScroll()); + target.setScaling(target.getScaling() * (way > 0 ? 2f : 0.5f)); + setMoving(true); + if (wheelTimer != null) { + wheelTimer.cancel(); + wheelTimer = null; + } + wheelTimer = new Timer(); + wheelTimer.schedule(new TimerTask() { + @Override + public void run() { + setMoving(false); + refreshLoop.refreshSketch(); + wheelTimer = null; + } + }, WHEEL_TIMER); + refreshLoop.refreshSketch(); + } + + @Override + public void mouseDragged(MouseEvent e) { + if (!previewController.sendMouseEvent(buildPreviewMouseEvent(e, PreviewMouseEvent.Type.DRAGGED))) { + setMoving(true); + Vector trans = target.getTranslate(); + trans.set(e.getX(), e.getY()); + trans.sub(ref); + trans.mult(isRetina ? 2f : 1f); + trans.div(target.getScaling()); // ensure const. moving speed whatever the zoom is + trans.add(lastMove); + + refreshLoop.refreshSketch(); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + } + + public void zoomPlus() { + target.setScaling(target.getScaling() * 2f); + refreshLoop.refreshSketch(); + } + + public void zoomMinus() { + target.setScaling(target.getScaling() / 2f); + refreshLoop.refreshSketch(); + } + + public void resetZoom() { + target.reset(); + refreshLoop.refreshSketch(); + } + + private Vector screenPositionToModelPosition(Vector screenPos) { + Vector center = new Vector(getWidth() / 2f, getHeight() / 2f); + Vector scaledCenter = Vector.mult(center, target.getScaling()); + Vector scaledTrans = Vector.sub(center, scaledCenter); + + Vector modelPos = new Vector(screenPos.x, screenPos.y); + modelPos.sub(scaledTrans); + modelPos.mult((isRetina) ? 2f : 1f); + modelPos.div(target.getScaling()); + modelPos.sub(target.getTranslate()); + return modelPos; + } + + private PreviewMouseEvent buildPreviewMouseEvent(MouseEvent evt, PreviewMouseEvent.Type type) { + int mouseX = evt.getX(); + int mouseY = evt.getY(); + PreviewMouseEvent.Button button = PreviewMouseEvent.Button.LEFT; + if (SwingUtilities.isMiddleMouseButton(evt)) { + button = PreviewMouseEvent.Button.MIDDLE; + } else if (SwingUtilities.isLeftMouseButton(evt)) { + button = PreviewMouseEvent.Button.LEFT; + } else if (SwingUtilities.isRightMouseButton(evt)) { + button = PreviewMouseEvent.Button.RIGHT; + } + + Vector pos = screenPositionToModelPosition(new Vector(mouseX, mouseY)); + + return new PreviewMouseEvent((int) pos.x, (int) pos.y, type, button, null); + } + + private class RefreshLoop { + + private final long DELAY = 100; + private final AtomicBoolean running = new AtomicBoolean(); + private final AtomicBoolean refresh = new AtomicBoolean(); + //Timer + private long timeout = DELAY * 10; + private Timer timer; + + public RefreshLoop() { + super(); + } + + public void refreshSketch() { + refresh.set(true); + if (!running.getAndSet(true)) { + startTimer(); + } + } + + private void startTimer() { + timer = new Timer("PreviewRefreshLoop", true); + timer.schedule(new TimerTask() { + @Override + public void run() { + if (refresh.getAndSet(false)) { + target.refresh(); + repaint(); + } else if (timeout == 0) { + timeout = DELAY * 10; + stopTimer(); + } else { + timeout -= DELAY; + } + } + }, 0, DELAY); + } + + private void stopTimer() { + timer.cancel(); + running.set(false); + } + } +} diff --git a/src/main/java/fucoin/gui/gephi/RendererTemplate.java b/src/main/java/fucoin/gui/gephi/RendererTemplate.java new file mode 100644 index 0000000000000000000000000000000000000000..7a69717f0869b037648568ee8bdd419031e85d59 --- /dev/null +++ b/src/main/java/fucoin/gui/gephi/RendererTemplate.java @@ -0,0 +1,64 @@ +package fucoin.gui.gephi; + +import org.gephi.graph.api.Node; +import org.gephi.preview.api.*; +import org.gephi.preview.spi.ItemBuilder; +import org.gephi.preview.spi.MouseResponsiveRenderer; +import org.gephi.preview.spi.PreviewMouseListener; +import org.gephi.preview.spi.Renderer; +import org.openide.util.lookup.ServiceProvider; + +import java.awt.*; + +@ServiceProvider(service = Renderer.class) +public class RendererTemplate implements Renderer, MouseResponsiveRenderer { + @Override + public String getDisplayName() { + return "Some name"; + } + + @Override + public void preProcess(PreviewModel previewModel) { + } + + @Override + public void render(Item item, RenderTarget target, PreviewProperties properties) { + //Retrieve clicked node for the label: + LabelItem label = (LabelItem) item; + Node node = label.node; + + //Finally draw your graphics for the node label in each target + if (target instanceof G2DTarget) { + Graphics2D g = ((G2DTarget) target).getGraphics(); + + g.setColor(Color.RED); + g.fillOval((int) node.x(), (int) -node.y(), 5, 5);//Note that y axis is inverse for node coordinates + } else if (target instanceof PDFTarget) { + } else if (target instanceof SVGTarget) { + } + } + + @Override + public PreviewProperty[] getProperties() { + return new PreviewProperty[0]; + } + + @Override + public boolean isRendererForitem(Item item, PreviewProperties properties) { + return item instanceof LabelItem; + } + + @Override + public boolean needsItemBuilder(ItemBuilder itemBuilder, PreviewProperties properties) { + return itemBuilder instanceof ItemBuilderTemplate; + } + + @Override + public boolean needsPreviewMouseListener(PreviewMouseListener pl) { + return pl instanceof MouseListenerTemplate; + } + + public CanvasSize getCanvasSize(Item item, PreviewProperties properties) { + return new CanvasSize(); + } +} diff --git a/src/main/java/fucoin/wallet/AbstractWallet.java b/src/main/java/fucoin/wallet/AbstractWallet.java index 882c2a547b9060a576589281eaaea8c9d3e078ba..210b67e80cc706159f9db444d1cddd75ba2ee0df 100644 --- a/src/main/java/fucoin/wallet/AbstractWallet.java +++ b/src/main/java/fucoin/wallet/AbstractWallet.java @@ -6,6 +6,8 @@ import fucoin.gui.TransactionLogger; import scala.concurrent.Future; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; /** * @@ -22,6 +24,8 @@ public abstract class AbstractWallet extends AbstractNode implements Serializabl */ protected final String name; + protected final List<ActorRef> overlayNeighbours = new ArrayList<>(); + /** * Init. a wallet with a name. * @@ -114,4 +118,8 @@ public abstract class AbstractWallet extends AbstractNode implements Serializabl * @param observer */ public abstract void send(String address, int amount, ActorRef observer); + + public void addOverlayNeighbour(ActorRef wallet) { + overlayNeighbours.add(wallet); + } } diff --git a/src/main/java/fucoin/wallet/WalletImpl.java b/src/main/java/fucoin/wallet/WalletImpl.java index 951a1519f91fa609e143fce37a554279eebcecbb..ec7899f800a855ef4f12af91c65d01639f7581ac 100644 --- a/src/main/java/fucoin/wallet/WalletImpl.java +++ b/src/main/java/fucoin/wallet/WalletImpl.java @@ -50,7 +50,7 @@ public class WalletImpl extends AbstractWallet { */ public void addAmount(int amount) { setAmount(this.getAmount() + amount); - addLogMsg(" My amount is now " + this.getAmount()); + //addLogMsg(" My amount is now " + this.getAmount()); } @Override @@ -61,7 +61,7 @@ public class WalletImpl extends AbstractWallet { @Override public void onReceive(Object message) { - addLogMsg(getSender().path().name() + " invokes " + getSelf().path().name() + " to do " + message.getClass().getSimpleName()); + //addLogMsg(getSender().path().name() + " invokes " + getSelf().path().name() + " to do " + message.getClass().getSimpleName()); if (message instanceof ActionInvokeRevive) { ((ActionInvokeRevive) message).doAction(this); } @@ -198,7 +198,7 @@ public class WalletImpl extends AbstractWallet { @Override public boolean addKnownNeighbor(String key, ActorRef value) { - addLogMsg(key + " is newNeighbor of " + name + "?" + !getKnownNeighbors().containsKey(key)); + //addLogMsg(key + " is newNeighbor of " + name + "?" + !getKnownNeighbors().containsKey(key)); if (getKnownNeighbors().containsKey(key) || key.equals(name)) { return false; } diff --git a/src/main/resources/topology.gexf b/src/main/resources/topology.gexf new file mode 100644 index 0000000000000000000000000000000000000000..45d86d94fb4f0fb2b7ffeccca6292f6313fe5414 --- /dev/null +++ b/src/main/resources/topology.gexf @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gexf xmlns="http://www.gexf.net/1.3" version="1.3" xmlns:viz="http://www.gexf.net/1.3/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gexf.net/1.3 http://www.gexf.net/1.3/gexf.xsd"> + <meta lastmodifieddate="2016-07-08"> + <creator>Gephi 0.9</creator> + <description></description> + </meta> + <graph defaultedgetype="directed" mode="static"> + <nodes> + <node id="0" label="Node0"> + <viz:size value="10.0"></viz:size> + <viz:position x="-294.57407" y="300.54153"></viz:position> + <viz:color r="153" g="153" b="153"></viz:color> + </node> + <node id="1" label="Node1"> + <viz:size value="10.0"></viz:size> + <viz:position x="342.42365" y="-58.31403"></viz:position> + <viz:color r="153" g="153" b="153"></viz:color> + </node> + <node id="2" label="Node2"> + <viz:size value="10.0"></viz:size> + <viz:position x="-482.98184" y="-252.69414"></viz:position> + <viz:color r="153" g="153" b="153"></viz:color> + </node> + <node id="3" label="Node3"> + <viz:size value="10.0"></viz:size> + <viz:position x="183.92188" y="291.57016"></viz:position> + <viz:color r="153" g="153" b="153"></viz:color> + </node> + </nodes> + <edges> + <edge id="0" source="2" target="1"></edge> + <edge id="1" source="1" target="3"></edge> + <edge id="2" source="3" target="0"></edge> + <edge id="3" source="0" target="2"></edge> + </edges> + </graph> +</gexf> diff --git a/src/main/resources/topology2.gexf b/src/main/resources/topology2.gexf new file mode 100644 index 0000000000000000000000000000000000000000..f7f453b0be6f37660060bb1bb27ab0241f7de0b6 --- /dev/null +++ b/src/main/resources/topology2.gexf @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gexf xmlns="http://www.gexf.net/1.3" version="1.3" xmlns:viz="http://www.gexf.net/1.3/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gexf.net/1.3 http://www.gexf.net/1.3/gexf.xsd"> + <meta lastmodifieddate="2016-07-08"> + <creator>Gephi 0.9</creator> + <description></description> + </meta> + <graph mode="static"> + <nodes> + <node id="0" label="Node0"> + <viz:size value="10.0"></viz:size> + <viz:position x="-294.57407" y="300.54153"></viz:position> + <viz:color r="153" g="153" b="153"></viz:color> + </node> + <node id="1" label="Node1"> + <viz:size value="10.0"></viz:size> + <viz:position x="342.42365" y="-58.31403"></viz:position> + <viz:color r="153" g="153" b="153"></viz:color> + </node> + <node id="2" label="Node2"> + <viz:size value="10.0"></viz:size> + <viz:position x="-482.98184" y="-252.69414"></viz:position> + <viz:color r="153" g="153" b="153"></viz:color> + </node> + <node id="3" label="Node3"> + <viz:size value="10.0"></viz:size> + <viz:position x="183.92188" y="291.57016"></viz:position> + <viz:color r="153" g="153" b="153"></viz:color> + </node> + </nodes> + <edges> + <edge id="0" source="2" target="1" type="directed"></edge> + <edge id="1" source="1" target="3" type="directed"></edge> + <edge id="2" source="3" target="0" type="directed"></edge> + <edge id="4" source="2" target="0" type="undirected"></edge> + </edges> + </graph> +</gexf>