Skip to content
Snippets Groups Projects
Commit 42f5988a authored by lkeidel's avatar lkeidel
Browse files

Merge branch 'dev-group3-gephi' into 'master'

Overlay topology

This merge requests adds the presented overlay topology creation.
This way, one does not have to modify the Distributed Commit to obtain a overlay network that is used by the implemented algorithms, which should not work on a fully connected network.
The topologies are defined via Gephi.

See merge request !6
parents e9ea7f29 8f3e8879
No related branches found
No related tags found
No related merge requests found
Showing
with 822 additions and 33 deletions
......@@ -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
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);
}
}
......@@ -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()) {
......
......@@ -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);
}
}
......@@ -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;
......
......@@ -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);
......
......@@ -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));
}
}
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;
/**
......
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());
});
}
}
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
......
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;
}
}
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;
}
}
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());
}
}
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);
}
}
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;
}
}
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];
}
}
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;
}
}
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> {
}
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);
}
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);
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment