GraphStream UI Tutorial 4 : Layout and node positions, responding to mouse actions on the graph elements

SourceForge.net Logo

In this tutorial we will learn how to copy back in the graph attributes the coordinates computed by the automatic layout. We will also see how a program can respond to mouse events on graphic elements displayed in the viewer.

A graph image

The layout computes automatically node coordinates, how to get them ?

You have two ways to position the node representations in the graph viewer. Either you specify them yourself using x, y and eventually z attributes, or you let GraphStream compute the best (yes, this may be debatable) node positions.

In the second case, these positions are computed for you, but you may be interested to retrieve them. For example, the distance between the nodes may be of interest to your algorithm.

It is important to understand that this automatic layout process runs in its own thread. Due to this fact, it is difficult to automatically copy back the positions computed in the layout thread back to the thread where you manipulate your graph.

For this copy to occur, you must explicitly request it (since there may be quite a quantity of data to pass between threads). Furthermore, you must fetch back this data explicitly, since a thread cannot automatically call methods on objects in another thread without introducing a lot of contention and locking.

Therefore to equip your graph with the coordinates computed by the automatic layout process, you must first ask it using the GraphViewerRemote.copyBackLayoutCoordinates(Graph). This method registers your graph in the remotes so that each time a node graphic representation moves, the corresponding node in the graph is modified (its x, y, and z attributes are updated).

But if you do this only nothing will happen. You must also regularly fetch the node movement information by calling ˛ GraphViewerRemote.pumpEvents(). This method handles automatically all inter-thread communications and avoid as far as possible any locking between these threads.

Lets see this mechanism in an example where we request the node coordinates and modify the node labels to print these coordinates :

import java.util.Iterator;

import org.miv.graphstream.graph.Graph;
import org.miv.graphstream.graph.Node;
import org.miv.graphstream.graph.implementations.DefaultGraph;
import org.miv.graphstream.ui.GraphViewerRemote;

public class TutorialUI004a
{
	public static void main( String args[] )
	{
		new TutorialUI004a();
	}
	
	public TutorialUI004a()
	{
		Graph graph = new DefaultGraph( "Coos", false, true );
		
		// The "true" argument ask automatic layout.

		GraphViewerRemote remote = graph.display( true );

		graph.addAttribute( "ui.stylesheet", "graph { text-align:aside; }" );
		
		// Ask for node coordinates (only possible if automatic layout was requested).

		remote.copyBackLayoutCoordinates( graph );
		
		graph.addEdge( "AB", "A", "B" );
		graph.addEdge( "BC", "B", "C" );
		graph.addEdge( "CD", "C", "D" );
		graph.addEdge( "DA", "D", "A" );
		graph.addEdge( "CA", "C", "A" );
		
		while( true )
		{
			// Regularly fetch the computed coordinates from the viewer and layout.

			remote.pumpEvents();

			// Update the node labels to reflect the coordinates.
			
			Iterator<? extends Node> i = graph.getNodeIterator();

			while( i.hasNext() )
			{
				Node node = i.next();
				float x = (float) node.getNumber( "x" );
				float y = (float) node.getNumber( "y" );
				node.addAttribute( "label", String.format( "(%.4f, %.4f)", x, y ) );
			}
			
			try{ Thread.sleep( 100 ); } catch( Exception e ) { }
		}
	}
}

And here is the result :

A graph image

Mouse actions on graphical elements

You may have noticed that it is possible to click on a node representation in the graph viewer window and to drag it to another position. This works with and without automatic layout, although dragging nodes when using the automatic layout can only serve to unlock a bad layout computation.

The graph viewer tracks the position and size of each sprite and node and therefore is able to send you events when an action is made on these elements with the mouse. In order to receive such events, you have to register a special listener in the graph viewer remote : GraphViewerListener.

This listener is an interface that defines methods for five events :

  • Node selection : The user clicked on a node. The event tells you when the mouse button 1 is pressed on the node and is generated anew when the user releases the mouse button. You thus receive it twice.
  • Sprite selection : Like for node, sprites can be clicked. You also receive this events twice, once for the button click, another for the button release.
  • Node drag : Once the mouse button has been clicked on a node, but not yet released, you receive this event for every mouse move, and therefore every node move.
  • Background click : The user clicked in the graph viewer window but neither on a node nor a sprite.

When you receive these events, the arguments give you the positions of clicks and moves. These coordinates are always expressed in graph units. The graph units are the units you define when specifying the position of nodes. They are also the coordinates used by the automatic graph layout algorithms. Graph units are distinct from pixels.

As explained in the preceding section on automatic layout coordinates, the graph viewer runs in a distinct thread, and therefore it cannot directly call the graph viewer listener you registered in the viewer remote. These events are sent to a special buffer that must be checked to send you these events back using the GraphViewerRemote.pumpEvents() method.

Lets see an example where you will be able to change the appearance of a node when it is clicked and released, as well as reporting its position during the move. In addition this example shows you how to add and remove a sprite on the graph background using the mouse.

import org.miv.graphstream.graph.Graph;
import org.miv.graphstream.graph.Node;
import org.miv.graphstream.graph.implementations.DefaultGraph;
import org.miv.graphstream.ui.GraphViewerListener;
import org.miv.graphstream.ui.GraphViewerRemote;
import org.miv.graphstream.ui.Sprite;

public class TutorialUI004b implements GraphViewerListener
{
	public static void main( String args[] )
	{
		new TutorialUI004b();
	}
	
	protected Graph graph;
	
	protected GraphViewerRemote remote;
	
	protected int sprites = 0;
	
	public TutorialUI004b()
	{
		graph = new DefaultGraph( "Click!", false, true );
		
		remote = graph.display( false );

		graph.addAttribute( "ui.stylesheet", styleSheet );
		
		Node A = graph.addNode( "A" );
		Node B = graph.addNode( "B" );
		Node C = graph.addNode( "C" );
		
		graph.addEdge( "AB", "A", "B" );
		graph.addEdge( "BC", "B", "C" );
		graph.addEdge( "CA", "C", "A" );
		
		A.addAttribute( "xy",  0, 1 );
		B.addAttribute( "xy", -1, 0 );
		C.addAttribute( "xy",  1, 0 );
		
		remote.addViewerListener( this );
		
		while( true )
		{
			remote.pumpEvents();
			try { Thread.sleep( 100 ); } catch( Exception e ) {}
		}
	}
	
	protected static String styleSheet =
		"node.active { color: yellow; border-width: 1px; border-color: black; }" +
		"node { width: 16px; color: black; text-size:9; text-align: aside; }" +
		"sprite { width: 8px; color: red; border-width:2; border-color: black; }";

	public void backgroundClicked( float x, float y, int button, boolean clicked )
    {
		if( ! clicked )
		{
			Sprite s = remote.addSprite( String.format( "%d", sprites++ ) );
			s.position( x, y, 0 );
		}
    }

	public void nodeMoved( String id, float x, float y, float z )
    {
		graph.getNode( id ).addAttribute( "label", String.format( "(%.2f, %.2f)", x, y ) );
    }

	public void nodeSelected( String id, boolean selected )
    {
		if( selected )
		{
			graph.getNode(id).addAttribute( "ui.class", "active" );
		}
		else
		{
			graph.getNode(id).removeAttribute( "ui.class" );
			graph.getNode(id).removeAttribute( "label" );
		}
    }

	public void spriteMoved( String id, float x, float y, float z )
    {
    }

	public void spriteSelected( String id, boolean selected )
    {
		if( ! selected )
			remote.removeSprite( id );
    }
}

The class we created implements GraphViewerListener. The graph we create is assigned a special style sheet that will allow us to change easily the look of nodes when they are clicked thanks to a special style class named node.active. All we will have to do in order to change the node appearance when a node is clicked, is to add it the attribute ui.class with value active. Then to come back to the old appearance we will only need to remove this ui.class attribute.

We do not use the automatic layout and position the nodes by ourself. At the end of the constructor we do a temporised loop that regularly pump events coming from the graph viewer. In a normal program you will have some sort of algorithm or simulation running on the graph, probably in a large loop. You would then put the pumpEvents() method in this loop.

Then we implement some of the methods of the GraphViewerListener interface. When a node is selected (clicked) we change its ui.class attribute. Depending on the fact the mouse button is clicked on the node or released, we add or remove this attribute.

When a node is dragged, we add it a label that gives the its two coordinates in the 2D place.

Finally, when the background is clicked, we add a new sprite at this position. When a sprite is clicked, we remove it. We take care of the fact the "selected" events is sent twice : once when mouse button 1 is clicked on the sprite, another time when the mouse button is released.

A graph image

Using meta classes in style sheets to visually indicate interactions.

We used a class to specify the style to use when a sprite or node is clicked. This works but may become cumbersome. You can instead use meta classes in style sheets. A meta class is a class that is assigned to an element automatically when certain conditions are met.

The meta classes understood by GraphStream are :clicked and :selected. These two meta classes allow to specify the style of the element when the mouse is clicked on the element and when the element has an attribute ui.selected on it. Therefore, you do not have anything to do when the element is clicked, and you only have to set the attribute ui.selected for selection to work.

Let see another example of the use of meta classes, where we can click on the nodes and on edges to select or deselect them. Clicking on the background will deselect all. We use sprites put on the middle of edges to implement a way to select edges, since there is no way to click on edges in GraphStream (yet?) :

import java.util.Iterator;

import org.miv.graphstream.graph.Edge;
import org.miv.graphstream.graph.Graph;
import org.miv.graphstream.graph.Node;
import org.miv.graphstream.graph.implementations.MultiGraph;
import org.miv.graphstream.ui.GraphViewerListener;
import org.miv.graphstream.ui.GraphViewerRemote;
import org.miv.graphstream.ui.Sprite;

public class TestStyleEvents implements GraphViewerListener
{
	public static void main( String args[] )
	{
		new TestStyleEvents();
	}
	
	protected Graph graph;
	
	public TestStyleEvents()
	{
		graph = new MultiGraph( "test", false, true );
		
		graph.addAttribute( "stylesheet", styleSheet );
		graph.addEdge( "AB", "A", "B" );
		graph.addEdge( "BC", "B", "C" );
		graph.addEdge( "CA", "C", "A" );

		GraphViewerRemote remote = graph.display( false );

		remote.addViewerListener( this );
		
		Node A = graph.getNode( "A" );
		Node B = graph.getNode( "B" );
		Node C = graph.getNode( "C" );
		
		A.addAttribute( "xy",  0, 1 );
		B.addAttribute( "xy", -1, 0 );
		C.addAttribute( "xy",  1, 0 );
		
		Sprite s1 = remote.addSprite( "AB" );
		Sprite s2 = remote.addSprite( "BC" );
		Sprite s3 = remote.addSprite( "CA" );
		s1.attachToEdge( "AB" );
		s2.attachToEdge( "BC" );
		s3.attachToEdge( "CA" );
		s1.position( 0.5f );
		s2.position( 0.5f );
		s3.position( 0.5f );
		
		while( true )
		{
			remote.pumpEvents();
			
			try
            {
	            Thread.sleep( 40 );
            }
            catch( InterruptedException e )
            {
	            e.printStackTrace();
            }
		}
	}
	
	protected String styleSheet =
		"node           { color: darkgrey; }" +
		"edge           { color: #404040; }" +
		"sprite         { width: 8px; color: lightgrey; }" +
		"node  :clicked { border-width: 2px; border-color: yellow; }" +
		"sprite:clicked { border-width: 2px; border-color: green; }" +
		"node:selected  { color: red; }" +
		"edge:selected  { color: red; }";

	public void backgroundClicked( float x, float y, int button, boolean clicked )
    {
		Iterator i = graph.getNodeIterator();
		
		while( i.hasNext() )
		{
			Node n = i.next();
			n.removeAttribute( "ui.selected" );
		}
		
		Iterator j = graph.getEdgeIterator();
		
		while( j.hasNext() )
		{
			Edge e = j.next();
			e.removeAttribute( "ui.selected" );
		}
    }

	public void nodeMoved( String id, float x, float y, float z )
    {
    }

	public void nodeSelected( String id, boolean selected )
    {
		if( selected )
		{
			Node n = graph.getNode( id );
	
			if( n.hasAttribute( "ui.selected" ) )
			     n.removeAttribute( "ui.selected" );
			else n.addAttribute( "ui.selected" );
		}
    }

	public void spriteMoved( String id, float x, float y, float z )
    {
    }

	public void spriteSelected( String id, boolean selected )
    {
		if( selected )
		{
			Edge e = graph.getEdge( id );
			
			if( e != null )
			{
				if( e.hasAttribute( "ui.selected" ) )
				     e.removeAttribute( "ui.selected" );
				else e.addAttribute( "ui.selected" );
			}
		}
    }
}

A graph image

Some remarks on what have been done

For the examples above, we are forced to use the GraphViewerRemote.pumpEvents() method in a loop. As explained above, this method is a necessity to avoid at most locks and contentions between threads. This may seem a problem. However, most of the time, your program will work on the graph as a loop and you can easily insert a call to pumpEvents() in it.

css xhtml