I already posted my question in https://github.com/graphstream/gs-ui-swing/issues/19#issue-2109865450. But since the last answer on this repository has been posted on Jun 10, 2021, and other questions have been posted then without any reply, I'm not sure there is still someone looking at the issues there. That's why I'm re-asking my question here.
I created a Jframe composed of one View and two JTextField:
The View displays five nodes: four are just landmarks and are not expected to be moved by the user ("fixed_*"), and one is there to be moved by the user ("unfixed"). The two JTextField show the coordinates of the "unfixed" node. Both the View and the JTextField have to be synchronized with each other. Indeed, when the user moves the "unfixed" node in the View, then the two JTextField have to be updated accordingly :
Reciprocally, when the user modifies the coordinate in one of the JTextField, then the View has to be updated accordingly too:
Here are four test cases:
Test cases 1, 2 and 3 work fine, but test case 4 doesn't work. Actually, in test case 4, once the user moved the "unfixed" node in the View, then the modification of the coordinate in one of the JTextField doesn't update the View.
I tried to analyze what differs between the execution of test cases 3 and 4. To do so, I printed the name of the current thread at different places of the code. I saw that modifications through the JTextField are run on thread "AWT-EventQueue-0" (Event Dispatch Thread of Swing, isn't it?), and that modifications through the View are run on thread "Thread-0". In my implementation, "Thread-0" is the thread on which I run the GraphStream's pumping loop, to wait for events occurred in the GraphStream's Viewer thread and to copy back them inside "Thread-0". From what I understood of GraphStream's documentation:
Did I understand well the documentation?
In my implementation, I chose to access the GraphStream's Graph from another thread than the Swing one. Thus, I deduce from my previous runs of test cases 3 and 4 that:
I have the impression that I'm doing something wrong with all these threads. Could you help me please?
I tried to make a minimum working example (MWE) to reproduce my problem. Here is the content of the Java source file NodeSyncTest.java:
package mwe;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import org.apache.commons.lang3.math.NumberUtils;
import org.graphstream.graph.Graph;
import org.graphstream.graph.Node;
import org.graphstream.graph.implementations.MultiGraph;
import org.graphstream.ui.graphicGraph.GraphicGraph;
import org.graphstream.ui.graphicGraph.GraphicNode;
import org.graphstream.ui.swing_viewer.SwingViewer;
import org.graphstream.ui.view.View;
import org.graphstream.ui.view.Viewer;
import org.graphstream.ui.view.ViewerListener;
import org.graphstream.ui.view.ViewerPipe;
class NodeSyncTest {
public static void main(String[] args) {
System.out.println("NodeSyncTest.main : " + Thread.currentThread().getName());
javax.swing.SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
System.out.println("SwingUtilities.invokeLater.Runnable.run : " + Thread.currentThread().getName());
new NodeSyncTest();
}
});
}
NodeSyncTest() {
Map<String, MyNode> myNodes = Map.of(
"fixed_top_left", new MyNode(-2, 2),
"fixed_top_right", new MyNode(2, 2),
"fixed_bottom_left", new MyNode(-2, -2),
"fixed_bottom_right", new MyNode(2, -2),
"unfixed", new MyNode(0, 0)
);
GraphStreamControl graphStreamControl = new GraphStreamControl(myNodes);
JTextFieldsControl jTextFieldsControl = new JTextFieldsControl(myNodes);
graphStreamControl.jTextFieldsControl = jTextFieldsControl;
jTextFieldsControl.graphStreamControl = graphStreamControl;
graphStreamControl.fillGraphStreamGraph();
JFrame mainDialog = new JFrame();
mainDialog.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainDialog.setSize(300, 300);
mainDialog.getContentPane().add((Component) graphStreamControl.view, BorderLayout.CENTER);
mainDialog.getContentPane().add(jTextFieldsControl.panel, BorderLayout.SOUTH);
mainDialog.setLocationRelativeTo(null);
mainDialog.setVisible(true);
graphStreamControl.startPumpLoop();
}
class GraphStreamControl {
Map<String, MyNode> myNodes;
MyNode myUnfixedNode;
Graph graphStreamGraph;
Viewer viewer;
View view;
ViewerPipe viewerPipe;
JTextFieldsControl jTextFieldsControl;
GraphStreamControl(Map<String, MyNode> myNodes) {
this.myNodes = myNodes;
myUnfixedNode = myNodes.get("unfixed");
System.setProperty("org.graphstream.ui", "swing");
graphStreamGraph = new MultiGraph("");
viewer = new SwingViewer(graphStreamGraph, Viewer.ThreadingModel.GRAPH_IN_ANOTHER_THREAD);
viewer.disableAutoLayout();
view = viewer.addDefaultView(false);
viewerPipe = viewer.newViewerPipe();
viewerPipe.addSink(graphStreamGraph);
viewerPipe.addViewerListener(new ViewerListener() {
@Override
public void viewClosed(String viewName) {}
@Override
public void buttonPushed(String id) {}
@Override
public void buttonReleased(String id) {
System.out.println("ViewerListener.buttonReleased : " + Thread.currentThread().getName());
if ("unfixed".equals(id)) {
GraphicGraph graphicGraph = viewer.getGraphicGraph();
GraphicNode unfixedGraphStreamNode = (GraphicNode) graphicGraph.getNode("unfixed");
myUnfixedNode.x = unfixedGraphStreamNode.getX();
myUnfixedNode.y = unfixedGraphStreamNode.getY();
jTextFieldsControl.update();
}
}
@Override
public void mouseOver(String id) {}
@Override
public void mouseLeft(String id) {}
});
}
public void fillGraphStreamGraph() {
for (var entry : myNodes.entrySet()) {
String nodeId = entry.getKey();
MyNode myNode = entry.getValue();
Node graphStreamNode = graphStreamGraph.addNode(nodeId);
graphStreamNode.setAttribute("xy", myNode.x, myNode.y);
graphStreamNode.setAttribute("ui.label", nodeId);
graphStreamNode.setAttribute("ui.style", "text-alignment: under;");
}
}
void startPumpLoop() {
new Thread(() -> {
System.out.println("GraphStreamControl.startPumpLoop : " + Thread.currentThread().getName());
while (true) {
try {
viewerPipe.blockingPump();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
void update() {
GraphicGraph graphicGraph = viewer.getGraphicGraph();
GraphicNode unfixedGraphStreamNode = (GraphicNode) graphicGraph.getNode("unfixed");
unfixedGraphStreamNode.setAttribute("xy", myUnfixedNode.x, myUnfixedNode.y);
}
}
class JTextFieldsControl {
Map<String, MyNode> myNodes;
MyNode myUnfixedNode;
JPanel panel;
JTextField xTextField;
JTextField yTextField;
GraphStreamControl graphStreamControl;
JTextFieldsControl(Map<String, MyNode> myNodes) {
this.myNodes = myNodes;
myUnfixedNode = myNodes.get("unfixed");
panel = new JPanel(new GridLayout(1, 4));
JLabel xLabel = new JLabel("X:", SwingConstants.RIGHT);
xLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
panel.add(xLabel);
xTextField = new JTextField(3);
xTextField.setHorizontalAlignment(SwingConstants.RIGHT);
xTextField.setText(Double.toString(myUnfixedNode.x));
xTextField.getCaret().setDot(0);
xTextField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("JTextFieldsControl - actionPerformed on xTextField : " + Thread.currentThread().getName());
String xNodeString = xTextField.getText();
double xNodeDouble = NumberUtils.toDouble(xNodeString);
myUnfixedNode.x = xNodeDouble;
graphStreamControl.update();
}
});
panel.add(xTextField);
JLabel yLabel = new JLabel("Y:", SwingConstants.RIGHT);
yLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
panel.add(yLabel);
yTextField = new JTextField(3);
yTextField.setHorizontalAlignment(SwingConstants.RIGHT);
yTextField.setText(Double.toString(myUnfixedNode.y));
yTextField.getCaret().setDot(0);
yTextField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("JTextFieldsControl - actionPerformed on yTextField : " + Thread.currentThread().getName());
String yNodeString = yTextField.getText();
double yNodeDouble = NumberUtils.toDouble(yNodeString);
myUnfixedNode.y = yNodeDouble;
graphStreamControl.update();
}
});
panel.add(yTextField);
}
void update() {
String xNodeString = Double.toString(myUnfixedNode.x);
xTextField.setText(xNodeString);
xTextField.getCaret().setDot(0);
String yNodeString = Double.toString(myUnfixedNode.y);
yTextField.setText(yNodeString);
yTextField.getCaret().setDot(0);
}
}
class MyNode {
double x;
double y;
MyNode(double x, double y) {
this.x = x;
this.y = y;
}
}
}
and here is the one of the the Maven POM file pom.xml to build an executable JAR including all dependencies:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>mwe</groupId>
<artifactId>mwe</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>MWE</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>org.graphstream</groupId>
<artifactId>gs-core</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.graphstream</groupId>
<artifactId>gs-ui-swing</artifactId>
<version>2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<!-- Build an executable JAR -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<!-- here we specify that we want to use the main
method within the App class -->
<mainClass>mwe.NodeSyncTest</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>mwe.NodeSyncTest</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
</project>
To use these two files, just create a folder mwe/, put NodeSyncTest.java into mwe/src/main/java/mwe/ and pom.xml into mwe/, and, in mwe/, run
mvn compile assembly:single
and
java -jar target/mwe-0.0.1-SNAPSHOT-jar-with-dependencies.jar
Here is the full project folder: MWE.zip
After some debugging, I finally found from where the problem comes. I just needed to replace the line:
unfixedGraphStreamNode.setAttribute("xy", myUnfixedNode.x, myUnfixedNode.y);
by the two lines:
unfixedGraphStreamNode.setAttribute("x", myUnfixedNode.x);
unfixedGraphStreamNode.setAttribute("y", myUnfixedNode.y);
for everything to work fine.
Why
unfixedGraphStreamNode.setAttribute("xy", myUnfixedNode.x, myUnfixedNode.y);
doesn’t work remains a mystery for me. Indeed, the documentation on https://graphstream-project.org/doc/Tutorials/Graph-Visualisation/ and https://graphstream-project.org/doc/FAQ/Attributes/Is-there-a-list-of-attributes-with-a-predefined-meaning-for-the-graph-viewer/ says that we can use the xy
attribute to set the coordinates of a node. But it is also encouraged to use the attribute xyz
. Thus, I tried to change my code to:
unfixedGraphStreamNode.setAttribute("xyz", myUnfixedNode.x, myUnfixedNode.y, 0.0);
and it works! I will post an issue on the GitHub repository of the project.