Writing an Algorithm Plugin

Preliminaries

This document explains how a new graph algorithm can be realized using Gravisto's plug-in mechanism. Please make sure that Graffiti_Core is in your compile classpath.

Getting Started

First of all you need to create a new plug-in.
For Gravisto to recognize it, you need to provide a valid description in plugin.xml specifying the plug-in class in your package. See Writing a Plugin for a detailed explanation.

Every plug-in can include an arbitrary number of algorithms. After loading Gravisto will display a menu item for each of them in the Plugin menu. You may now want to group your algorithms and have them organized into submenus. This can be accomplished by implementing getPathInformation() and returning an instance of PluginPathNode. A PluginPathNode has a label that is used for the menu item it represents and an array of the algorithms that belong directly to it as well as an array of its child nodes. If any two siblings have the same label they are automatically merged into one node.

Below is a sample plug-in class extending GenericPluginAdapter which is the recommended way to write a plug-in:

package org.graffiti.plugins.algorithms.guideexample;
 
import org.graffiti.core.Bundle;
import org.graffiti.plugin.GenericPluginAdapter;

import org.graffiti.plugin.PluginPathNode;
import org.graffiti.plugin.algorithm.Algorithm;
 
public class GuideExampleAlgorithmPlugin extends GenericPluginAdapter

{
    /** Resource bundle for the plugin. */
    private Bundle bundle = Bundle.getBundle(getClass());

 
    /**
     * Creates a new algorithm plugin.
     */
    public GuideExampleAlgorithmPlugin() {
        this.algorithms = new Algorithm[] { new GuideExampleAlgorithm(), 
                                            new DemoAlgorithm() };

    }
 
    /*
     * @see org.graffiti.plugin.GenericPluginAdapter#getPathInformation()
    */
    @Override
    public PluginPathNode getPathInformation() {

        return new PluginPathNode(bundle.getString("path.guide"), null, 
            new PluginPathNode[] { 
                new PluginPathNode(bundle.getString("path.guide.examples"), new Algorithm[] { algorithms[0] }, null),
                new PluginPathNode(bundle.getString("path.guide.demos"), new Algorithm[] { algorithms[1] }, null) 
            });

    }
}

The example plug-in provides two algorithms. By extending GenericPluginAdapter you don't need to do anything more than assigning an array containing an instance of each of your algorithms to the inherited member algorithms.
In order to provide localized menu entries, we use string bundles here. See The Java Tutorials: Internationalization for an explanation of this mechanism.

The example code will create menu entries as shown below:

example plugin menu

Writing an algorithm

An algorithm must implement the interface Algorithm. It is recommended that you do this by extending AbstractAlgorithm. Afterwards there are only two methods left to be implemented, public void execute() and public String getName() which are pretty much self-explaining. The former is called when a user chose to run the algorithm, the latter gives your algorithm a name that is used e.g. in the menu. However there are a few things to consider regarding how Gravisto handles algorithms and the implementation of AbstractAlgorithm.

Algorithm handling

Every algorithm can define a list of parameters that can be set by the user to influence its behavior. Parameters are of type Parameter. There are implementations for many different kinds of parameters including BooleanParameter, IntegerParameter, DoubleParameter, StringParameter and SelectionParameter - just to mention a few. They are retrieved and set by public Parameter<?>[] getParameters() and public void setParameters(Parameter<?>[] params) respectively.

Before the algorithm is started Gravisto checks whether the algorithm needs any parameters and presents a settings dialog to the user. If your algorithm is derived from AbstractAlgorithm the dialog also features user preferences for the parameters. Afterwards the current graph is attached to the algorithm. Next it is given the chance to check its preconditions. Only if this succeeds the algorithm is executed.
Please note that if the user decides to run the algorithm a second time the same instance is used again. Therefore after each run the system calls public void reset(). You can override this method if your algorithm needs to clean up and prepare for another run.

Inheriting from AbstractAlgorithm

The abstract class AbstractAlgorithm provides some interesting amenities:

Localization

As mentioned above, Gravisto provides mechanisms to localize strings. Even if you don't plan to offer your algorithm in different languages it always is evidence of good style to separate source code and output strings. Read The Java Tutorials: Internationalization to learn how to use bundles.

Transactions

Gravisto uses a listener pattern in order to retrieve information on changes to the graph. The events fired can be grouped in transactions which reduce the number of events passed individually to the listeners. Instead they are collected until the transaction is finished and afterwards passed on as a list of changes. Listeners can decide whether they want to receive each event or the collection. Thus the whole system fastens up as it is sufficient for many listeners to update after the transaction is finished.

You are kindly asked to use transactions for your algorithms, too. Have a look at the example algorithm below to see how they are created.

There is another advantage of transactions: If the user adds the Undo plug-in an algorithm using transactions can be un- and redone in one step.

An example algorithm

Below is an example implementation of an algorithm. It generates nodes, arranges them on a line and links them with directed edges. The number of nodes can be specified by a parameter.
package org.graffiti.plugins.algorithms.guideexample;
 
import java.awt.geom.Point2D;
 
import org.graffiti.attributes.Attribute;

import org.graffiti.core.Bundle;
import org.graffiti.graph.Node;
import org.graffiti.graphics.CoordinateAttribute;
import org.graffiti.graphics.GraphicAttributeConstants;

import org.graffiti.plugin.algorithm.AbstractAlgorithm;
import org.graffiti.plugin.algorithm.PreconditionException;
import org.graffiti.plugin.parameter.IntegerParameter;
import org.graffiti.plugin.parameter.Parameter;

 
public class GuideExampleAlgorithm extends AbstractAlgorithm
{
    /** The number of nodes. */
    private IntegerParameter nodesParam;

 
    /** Resource bundle for the algorithm. */
    private Bundle bundle = Bundle.getBundle(getClass());

 
    public GuideExampleAlgorithm() {
        nodesParam = new IntegerParameter(5, bundle.getString("parameter.nodes_cnt.name"),
            bundle.getString("parameter.nodes_cnt.description"),
            0, 50, 0, Integer.MAX_VALUE);

    }
 
    @Override
    protected Parameter<?>[] getAlgorithmParameters() {

        return new Parameter[] { nodesParam };
    }

 
    /*
     * @see org.graffiti.plugin.algorithm.Algorithm#check()
     */
    @Override
    public void check() throws PreconditionException
    {

        PreconditionException errors = new PreconditionException();
 
        if (nodesParam.getInteger().compareTo(new Integer(0)) < 0)

        {
            errors.add(bundle.getString("precondition.nodes_ge_zero"));
        }

 
        // The graph is inherited from AbstractAlgorithm.
        if (graph == null)
        {

            errors.add(bundle.getString("precondition.graph_null"));
        }
 
        if (!errors.isEmpty())

        {
            throw errors;
        }
    }
 
    /*
     * @see org.graffiti.plugin.algorithm.Algorithm#execute()
     */

    @Override
    public void execute()
    {
        int n = nodesParam.getInteger().intValue();

        Node[] nodes = new Node[n];
 

        // start a transaction
        graph.getListenerManager().transactionStarted(this);
 
        // generate nodes and assign coordinates to them

        for (int i = 0; i < n; ++i)

        {
            nodes[i] = graph.addNode();

 
            CoordinateAttribute ca = (CoordinateAttribute)nodes[i]
                .getAttribute(GraphicAttributeConstants.GRAPHICS

                    + Attribute.SEPARATOR
                    + GraphicAttributeConstants.COORDINATE);
 
            double x = 100 + (i * 100);

            double y = 100;
 
            ca.setCoordinate(new Point2D.Double(x, y));

        }
 
        // add edges
        for (int i = 1; i < n; ++i)

        {
            graph.addEdge(nodes[i - 1], nodes[i], true);

        }
 
        // stop a transaction
        graph.getListenerManager().transactionFinished(this);

 
        // add arrows to edges
        graph.setDirected(true, true);
    }

 
    /*
     * @see org.graffiti.plugin.Parametrizable#getName()
     */
    @Override
    public String getName()
    {

        return bundle.getString("name");
    }
}
    

This is what the algorithm's parameter dialog will look like:

example algorithm parameter dialog