4. View

The view in JGraph is somewhat different from other classes in Swing that carry the term view in their names. The difference is that JGraph's view is stateful, which means it contains information that is solely stored in the view. The GraphLayoutCache object and the CellView instances make up the view of a graph, which has an internal representation of the graph's model.

4.1. Graph Layout Cache

Figure 27. GraphLayoutCache and GraphModel

GraphLayoutCache and GraphModel

The GraphLayoutCache object holds the cell views, namely one for each cell in the model. These views are kept in two structures, an array that holds the port views, and a list that holds the views for the root cells. Thus, ports and other cells are kept in different data structures in the graph view, whereas in the model, they are kept in the same structure. The graph view also has a reference to a hash table, which is used to provide a mapping from cells to cell views. The inverse mapping is not needed, because the cell views point to their corresponding cells.

Figure 28. GraphLayoutCache class and static relations

GraphLayoutCache class and static relations

GraphLayoutCache extends CellMapper, has a reference to a CellMapper (usually this), and a CellViewFactory, and instantiates an array of PortViews, a List that holds the root views, and a map that is used to implement the CellMapper interface.

The children may be accessed through the CellView interface, and are not accessible through the GraphLayoutCache object. The creation of new views is deferred to the CellViewFactory. By default, JGraph is used to provide this factory, which means that the JGraph object is ultimately responsible to create the views.

The graph view's representation of the graph is different from the model's internal structure, in that it was designed with the view's operations in mind. For example, the view, in contrast to the model, makes a difference between children that implement the Port interface, and other children. The children that implement the Port interface are treated separately, both, as children, and with respect to storage, and are kept in a separate array.

This is because the set of available ports is often accessed independently of the rest, for example when the graph is drawn with port visibility set to true, or when the source or target of an edge is changed interactively, in which case the new ports must be found and highlighted. To find the port at a specific location, the graph view loops through the array of ports in the view, and checks if a port intersects the specified point, and if so, returns this port.

Even if the ports are always visible and painted on top of all cells, the order in which ports are stored reflects the layering of the corresponding cells in the view. This means, if two ports are on top of each other, the port that belongs to the topmost cell will be selected first.

4.2. Cell Mapper

Figure 29. The CellMapper maps from GraphCells to CellViews

The CellMapper maps from GraphCells to CellViews

The GraphLayoutCache class implements the CellMapper interface, which defines the mapping from cells to views. The interface provides two methods, one to retrieve the cell view for a given cell ñ and optionally create the view if it does not exist, using the CellViewFactory ñ and one to associate a given cell with a newly created cell view.

The graph view provides additional useful methods that are not required by this interface, namely a method to map an array of cells to an array of views, one to get all descendants for a specific view (not including ports), and one to return the visual bounds for an array of views.

The CellMapper interface is used in JGraph to encapsulate the most important part of a GraphLayoutCache object, namely the mapping from GraphCells to CellViews. (It also allows the graph view's mapping to be exchanged at run-time, which is needed for live-preview that is explained later.) The reverse mapping is not needed, because each CellView instance has a reference to its corresponding GraphCell instance.

Figure 30. CellViews may be created automatically if they do not exist

CellViews may be created automatically if they do not exist

When a cell is looked-up in the graph view in order to find its corresponding cell view, a hash table access is performed using the cell's hash code, which is provided for every Java object by the Object class, as a key. If the cell is not associated, that is, if the hash table returns null, the view is created based on the create argument, using the CellViewFactory interface.

4.3. CellView Factory

The association between a cell and its view is established during the creation of the view, from within the CellViewFactory. This is because the creating of a cell view entails the creation of its dependent views, such as children, or the source and target of an edge. Therefore, the association between the initial cell and its view must be available by the time the views of the children are created (because child views have a reference to their parent views, and edge views have a reference to their source and target port views, which are looked-up using the CellMapper object that is passed to the createView method upon creation of the view.)

4.4. Cell Views

Cell views are used to store the geometric pattern of a graph, and also to associate a renderer, editor and cell handle with a certain cell view. The renderer is responsible to paint the cell, the editor is used for in-place editing, and the cell handle is used for more sophisticated editing. For efficiency, the Flyweight pattern is used to share the renderer among all instances of a given specific CellView class. This is achieved by making the reference to the renderer static. The editor and cell handle, instead, are created on the fly.

Figure 31. CellView interface, default implementations and static relations

CellView interface, default implementations and static relations

VertexView, EdgeView and PortView extend AbstractCellView, which in turn extends CellView. AbstractCellView has a static reference to the GraphCellEditor and to the CellHandle. Each concrete view has a static reference its corresponding subclass of CellViewRenderer, which is used to paint the cell.

4.4.1. CellView Interface

The CellView interface is used to access the view's graph and group structure, and to hold attributes. The CellView interface provides access to the parent and child views, and to a CellViewRenderer, a GraphCellEditorand a CellHandle. The getBounds method returns the bounds of the view, and the intersects method is used to perform hit-detection.

The refresh method is messaged when the corresponding cell has changed in the model, and so is the update method, but the latter is also invoked when a dependent cell has changed, or when the view needs to be updated (for example during a live-preview, explained in Section 5.4.1, “Live-Preview”).

Figure 32. CellView update and refresh

CellView update and refresh

The update method is a good place to implement automatic attributes, such as edge routing, that is, attributes that are computed based on other attributes or the graph's geometry. The attributes of a cell view are accessed using the getAttributes method, with the exception of the bounds, which are cached, and accessed using the getBounds method.

4.4.2. CellView Default Implementations

By default, the JGraph object, which acts as a CellViewFactory is able to return cell views for each of the default implementations of the GraphCell interface. The cell view is created based on the type of the passed-in cell. For objects that implement the Edge interface, EdgeViews are created, and for objects that implement the Port interface, PortViews are created. For other objects, VertexViews are created.

Figure 33. CellView default implementations and static relations

CellView default implementations and static relations

AbstractCellView has a reference to the parent CellView, and an array of CellViews that represent the views of the children. EdgeView has a reference to the CellView of the source and target port, and PortView has a reference to the PortView of the anchor. In contrast to the DefaultPort's edge set, the PortView does not provide access to the set of attached edges.

4.4.2.1. AbstractCellView

The AbstractCellView class provides the basic functionality for its subclasses. For example, it implements the getRendererComponent method to create a renderer, configure and return it. The concrete renderer is retrieved using an inner call to the getRenderer abstract method, which must be implemented by subclassers to return a specific renderer.

Figure 34. AbstractCellView

AbstractCellView

The intersects method, which is used to implement hit-detection, uses the getBounds method to check if a given point intersects a cell view. The EdgeView class overrides this default implementation, because in the context of an edge, a hit should only be detected if the mouse is over the shape of the edge (or its label), but not if it is inside the bounds and outside the shape.

4.5. Changing the View

Figure 35. Nested map for the graph view's edit method

Nested map for the graph view's edit method

Using the view's edit method, the attributes of the cell views may be changed. The argument is a map, from views to attributes. The toBackand toFront methods are used to change the layering of the cell views.

4.6. Graph Context

The GraphContext class is named after the fact that each selection in a graph may possibly have a number of dependent cells, which are not selected. These cells must be treated in a special way in order to display a live-preview, that is, an exact view of how the graph will look after the current transaction. This is because for example an edge may change position when its source and target port are moved, even if the edge itself is not selected.

Figure 36. GraphContext for the moving of vertex B

GraphContext for the moving of vertex B

For example, when moving the vertex B in the above graph, its port, and the attached edge must be duplicated and repainted to reflect the new position of the vertex, even if the edge and port themselves are not selected. Thus, the selection consists of the vertex B only, and the port and edge are duplicated because they are part of the context of this selection. (In the case where the user holds down the control-key while moving cells, which is interpreted as cloning, the unselected cells are not visible, because they will not be cloned when the mouse button is released.)

The term context is used to describe the set of cells that are indirectly modified by a change, but are not themselves part of the selection. The GraphContext object is used to compute and manage this set. Additionally, the object may be used to disconnect a set of edges from the original graph, that is, from all the ports that are not part of the GraphContext. (Being part of the graph context either means that a cell or one of its ancestors is selected, or the cell is part of the context of this selection.)

4.6.1. Construction

To create a GraphContext object, the current selection is passed to the constructor. In the constructor, the set of descendants of the selected cells is computed using the model's getDescendants method, and stored within the GraphContext object. Then, the set of edges that is attached to these descendants is computed using the models getEdges method, and the set of descendants is subtracted from it, resulting in the context, that is, the set of cells that are attached, but neither being selected, nor having selected ancestors.

Having these two sets at hand, the graph context may create the temporary views for the selected cells and their context. The temporary views are typically used to provide a live-preview for the change that is currently in progress, such as a move or a resize.

For such a change, the temporary views are created using the above mechanism, and then changed in-place, until the user releases the mouse, in which case the changes are applied to the original attributes using the applyMap method. If the user presses escape during the change, then the change is aborted, and therefore the initial attributes are not changed in this case.

4.6.2. Temporary Views

The temporary views are created in two steps: First, the temporary views of the selected cells are created, and associated with their corresponding cells using a local hash table. Then, the temporary views for the context are created, using the GraphContext instance as the CellMapper, and also stored in this local hash table.

The mapping in the graph context uses the local hash table to look-up the references for the temporary views. That is, the views created by the graph context reference the temporary views as their parents, children or source and target ports, if such temporary views exist. (The temporary views are created lazily, when needed, based on the cell being contained in either the set of descendants, or the context, and stored in the local hash table upon creation.)

Thus, in contrast to the graph view, this cell mapper does only create a new (temporary) cell view if the cell is part of the context, and therefore, the resulting views reference the original graph's cell views if the corresponding cells are not part of the context.

These temporary views are used for live-preview, which is explained in the next chapter, because it is related with the CellHandles, which are used in the UI-delegate (the "control").