Graphical Programming Architecture
Overview
The Program tab is indended to support multiple views of the functions in the system. To this end, the
View class contains the base functionality required to listen for events from all functions. Plugins extending
View can be placed in the
spiegel.viewcontrol.mapview.plugins.program.views package. However, plugin support is currently unimplemented, instead creating the
JGraphView class directly.
View class
The
View class handles placing listeners on all functions, groups, inputs, and outputs within its scope. The scope is defined by a
FunctionGroup passed to the
View constructor.
View defines a number of empty methods corresponding to events, which a class extending
View can override. These include
onNewFunction(),
onDelFunction(), and so on. This saves extending classes from having to track all creations and deletions manually in order to add listeners to all objects. Upon creation these methods are called for all objects within the scope, as if they had just been created. Following creation, these methods are called in response to events received from objects in the system. All of the events are also relayed from the
View, so listeners can be added directly to the
View to receive them.
JGraphView
JGraph
JGraphView uses as its core the
JGraph package. This package provides visualization of mathematical graphs, consisting of nodes connected by edges. In this case, the nodes are functions and the edges are the connections between inputs and outputs on the functions. JGraph also supports the concept of ports, which are a way of differentiating the different endpoints on a single node that an edge can connect to, in this case the inputs and outputs.
All nodes, ports, and edges (collectively known as cells) in JGraph inherit from
DefaultGraphNode, which provides a common interface for controlling them. All visible properties of cells are controlled by an attribute map, which is a
Map from predefined string constants in
GraphConstants to appropriately-typed values. The
GraphConstants class provides getters and setters which enforce the proper types.
Each object in the function framework has two counterparts in the GUI: a model object and a view object. The former contains the graph information, while the latter handles providing a renderer component to draw the object on-screen. Each
Function or
FunctionGroup has an associated
DefaultGraphCell and
VertexView. Each
DataInput or
DataOutput has a
DefaultPort and a
PortView. Links between inputs and outputs have only a
DefaultEdge and a
EdgeView, with no underlying object in the function framework.
JGraph consists of three main classes:
JGraph,
DefaultGraphModel, and
GraphLayoutCache.
JGraph serves as the Swing container for the graph.
DefaultGraphModel contains the cells of the graph structure. Finally,
GraphLayoutCache is a view of the model, following the model-view pattern, handling
CellViews which determine the appearance of the cells.
Plugins
JGraphView supports plugins, found in the
spiegel.viewcontrol.mapview.plugins.program.views.jgraph.plugins package or in locations specified by the system property
spiegel.program.jgraphview.plugins. Plugins must extend the
JGraphPlugin class. There are three types of plugins.
Option plugins, extending
JGraphOptionPlugin, provide display options that go in a specific category in the Options menu on the
JGraphView interface. When the user turns these options on or off, the plugin is notified and can take whatever actions are necessary.
Action plugins, extending
JGraphActionPlugin, specify operations that can be performed on the graph. They produce items on the Actions menu, which, when chosen, invoke the plugin.
Control panel plugins, extending
JGraphControlPanelPlugin, provide Swing panels that will be displayed as part of the frame around the graph area. They can be used to provide information or controls for the graph.
Attribute layers
Cells' attributes are changed via the
GraphLayoutCache.edit() method, which takes, among other arguments, a
Map between model cells and the corresponding attribute maps containing the changes to be made to the attributes.
JGraphView provides a higher-level interface to editing attributes, using
AttributeLayers.
All JGraph attributes used in
JGraphView are set by means of
AttributeLayers.
JGraphView contains a stack of these layers, each of which can supply any attribute for any object in the system. The function-level objects are used for this purpose, not the model or view objects. When two layers specify values for the same attribute on the same object, the one higher on the stack takes precedence (but see the exception below). This allows one layer to selectively override the value provided by a lower layer.
Any option plugin can provide attribute layers, by overriding the
getLayers() method from
JGraphOptionPlugin. Layers needn't be directly related to options; for example a plugin can have two layers which are both controlled by two separate options simultaneously. The layers from all plugins are stacked in an order specified by the
layerorder.lst file in the
spiegel.viewcontrol.mapview.plugins.program.views.jgraph package.
Attributes are stored in an
ObjectAttributeMap, which is a
Map<Object, Map<String, Object>> with extra methods to help manage the data structure. Each object-attribute pair is independent of all others, so each object can have its own set of attributes.
Layers are only allowed to set attributes which they have declared that they will affect. This is done by calling the
JGraphView.setAffectedAttributes() method. This takes two parameters: the layer making the call, and an array of strings indicating which attributes may be affected by the layer. This provides some protection from mistakes in writing layers, so that attributes cannot be changed accidentally. It also provides an easy way to remove all attributes from a layer when an option is deactivated. It may also in the future allow for more optimization in the updating of layers, since only the total set of attributes being affected in the stack must be processed, and completely separate layers can be skipped.
Each layer's current attribute values are stored by the
JGraphView, and can only be changed when
JGraphView updates the stack as a whole. Once a layer has declared its affected attributes, it can request that some of them be updated. This is done through the
JGraphView.requestUpdate() method, which takes three arguments in several different forms. The first argument is always the layer on which an update is being requested. The second argument specifies which objects' attributes need to be updated. This can be either a single object reference, an array of objects, or a
Class object, in which case all objects of that class will be updated. The third argument specifies which attributes on those objects need updating, as either a single
String from the
GraphConstants class or an array of
Strings.
By default, the layer stack is updated whenever a request is made for an update. However, sometimes multiple request calls are made together, and it would be inefficient to update after each one. Fo this reason
JGraphView provides the
beginUpdateRequest() and
endUpdateRequest() methods. All requests made between these calls will be queued until
endUpdateRequest() is called. These calls can be nested, so long as they come in pairs.
JGraphView.update() handles determining which requests must be fulfilled and combining the layers' data to determine the composite attribute map for each object. It does this by iterating over all layers from top to bottom. It keeps track of the current attribute states from the layers it has already processed as well as a map of all changes that have been made during this pass. For each layer, it first calls
AttributeLayer.attributesChanged(), passing it the change map. This allows one layer to change in response to changes in higher layers. If desired,
attributesChanged() can then call
requestUpdate(). If so, these requests are immediately processed. If any requests have been made for attributes which are shadowed by attributes higher in the stack, they are saved for later and not processed now. Then the layer's
update() method is called. The first argument is the complete attribute map from the layers above, in case it needs to read from them. The second argument is the map of requests in this layer that must be updated, with null values where data must be stored. Upon returning, the values for this layer are replaced by the values in the second argument's map. For each attribute, if a value is specified it replaces the old value. If it is null, the attribute is removed from the layer. If the mapping was removed during the
update(), the old value is retained. When all layers have been processed, the final map of changes made by the layers is applied to the graph.
Normally the topmost layer asserting an attribute gets its way. However, sometimes it is necessary for one layer to change the output of a previous layer. For this reason, layers can set overidden attributes. This is done using the
JGraphView.setOverrideAttributes() method, used in the same manner as
JGraphView.setAffectedAttributes(). The layer will still receive data from this attribute as input, but whatever it outputs will replace the input from previous layers. This used in the
UserFunctionhandler layer which determines the position of the
Container function based on the positions of all the other functions in the group.
Options
The
JGraphOptionPlugin class defines a method
getOptions(), which option-providing plugins override. The return value is a map between option types (found in
JGraphView) and lists of
JGraphView.Option objects. (A convenience method
createOptionMap() is provided to easily create this map.) For plugins with only one option, it is possible for the plugin class to also implement
Option. Each option will be displayed in the appropriate menu. When the user clicks on it, either
activate() or
deactivate() will be called on the Option object.
Options can add listeners to
JGraphView to learn of new objects for which updates should be requested.
JGraphOptionPlugin provides facilities for doing this for all of a given class of objects, through the nested
ObjectTracker class. This class can track functions, groups, inputs, outputs, and links, automatically requesting updates on different sets of attributes for each type. For example, to track functions and automatically request updates on certain attributes, call
trackFunctions(attributeArray). If the
JGraphOptionPlugin and
Option are one and the same for an object, the tracker's methods are available directly. If the options are separate objects, an
ObjectTracker must be explicitly created, and the option's
activate() and
deactivate() methods must be forwarded to it explicitly.
Actions
The
JGraphActionPlugin class defines a method
getActions(), which action-providing plugins override. The return value is a map between action types (found in
JGraphView, but unimplemented at present) and lists of
JGraphView.Action objects. (A convenience method
createActionMap() is provided to easily create this map.) When the user clicks on the menu item, the
Action='s =actionPerformed() method is called, which can do as it likes with the
JGraphView.
Control Panels
The
JGraphControlPanelPlugin class defines a method
getControlPanels(), which control panel-providing plugins override. The return value is a list of
Components, each of which is given a tab on one of the sides of the window.
Examples
Read with the source in hand to follow along.
group.GroupDefaultStyle
This is a very simple plugin. It provides no options (and thus is permanently active), and one attribute layer. Upon creation, it activates group tracking and adds a listener to
JGraphView for
setName events. The tracker handles requesting updates on newly-created groups. The only time the attributes for a group will change after creation is when the group's name changes, so the
setName event handler is the only place that calls
requestUpdate().
update() simply installs a batch of constant attributes and then sets the name and cell view.
filter.SizeOffsetToBounds
This is a utility layer, occurring at the very bottom of the stack to convert the separate size and offset attributes into the single bounds attribute that JGraph expects. Upon creation, it sets up one affected attribute, the bounds attribute. Whenever it hears that attributes in layers above it have changed, via
attributesChanged(), it checks to see if the size or offset attributes were changed on any functions or groups. If they have, it requests an update on the bounds for those objects. This causes
update() to be called. For each function or group that needs updating, it reads the size and offset from the input data (from previous layers). It computes the bounding rectangle and sets that on the output map.
layout.SugiyamaFunctionLayout
This plugin provides two options (
Horizontal and
Vertical) and one layer. The options are nested classes that simply call back to the main class, which also serves as the single
AttributeLayer. It doesn't use the tracker, but instead listens to all creation, deletion, connection, and disconnection of functions. Whenever any of these events occur, it requests an update of the positions of all functions. However, it uses a
DelayedExecutor so that the request only occurs one second after event activity stops, thus preventing unnecessary layouts during, for example, the loading of a script. Requests are also triggered by any functions or groups changing their size or visibility status. The
update() method collects all functions that are visible according to the input data, as well as groups that have no visible children (i.e. groups that have been collapsed). These cells as well as all ports and links are passed to the actual layout algorithm, which computes positions and sets that attribute on all of the visible objects.
Animation
JGraphView provides a hook for plugins to set an animation handler. After each
update(), the changes are passed
animateEdit(), which uses a
GraphAnimator object to process the attributes. The
LinearAnimation plugin uses
setGraphAnimator()- to install its own handler. =GraphAnimator specifies two methods:
beginAnimation() prepares to execute the given attribute edit, and
stepAnimation() is called periodically to step through thre animation. It returns true when the animation is complete. The animator should perform reasonably when
beginAnimation() is called during a previous animation request, since there is nothing to prevent multiple edits from happening in a short span of time.
Debugging
One of the
JGraphView menus is "Debug." This menu contains an "Attribute viewer" entry, which opens a window by the same name. This window displays each of the attributes for any object in each layer. The window opens to show the object that is currently selected, but drop-down lists are provided at the top of the window for browsing other objects. The box on the left selects which category of object to view, while the box on the right lists all objects of that type alphabetically. For each attribute on each layer, there are four possible states:
- Filled
- A value has been set.
- Blank
- No mapping; likely this attribute is not among the layer's affected attribute set.
- - (hyphen)
- Value is not specified (null), indicating that lower attributes will show through.
- ? (question mark)
- Value has been requested but is shadowed by a higher layer, so the value is unknown.
The bottom row of the window shows the composite attributes, i.e. the attributes that are actually applied to the object.
--
TimPeterson - 09 Aug 2006