User Tools

Site Tools


devel:plugin:petri

Advanced plugin for Petri Net model

The purpose of this exercise aims at demonstrating how a more sophisticated graph model can be implemented in Workcraft, by using Petri Nets as an example.

The main differences that distinguishes the Petri Net models from the basic graph ones are that:

  1. They use two different types of nodes: places and transitions.
  2. Nodes of the same type cannot be connected to each other (e.g. a place can connect to a transition, but not to another place).
  3. Its state is expressed by the number of tokens found in all the places, which is also known as a marking.

By considering these aspects of this type of model, it is now possible to encode them as a Petri Net (Workcraft) plugin. Before continuing with this exercise, it is worth noting that the module and model descriptor classes for the Petri Net model are very similar to those of the Directed Graph model. This means that these classes can be created independently.

Mathematical layer classes

Similar to how the Graph class was implemented in the Directed Graph plugin exercise, the Petri class also extends the AbstractMathModel class meaning most of the necessary functionality has already been implemented. The only difference for this class is that nodes of the same type cannot be connected to each other:

    @Override
    public void validateConnection(MathNode first, MathNode second) throws InvalidConnectionException {
        super.validateConnection(first, second);
 
        if ((first instanceof Place) && (second instanceof Place)) {
            throw new InvalidConnectionException("Connections between places are not allowed");
        }
 
        if ((first instanceof Transition) && (second instanceof Transition)) {
            throw new InvalidConnectionException("Connections between transitions are not allowed");
        }
    }

Now, because Petri Nets contains two different types of nodes, it is worth assigning different prefixes to both places and transitions such as p and t respectively. This simple assignment can be achieved by tagging both Place and Transition classes with the @IdentifierPrefix annotation:

Transition.java
package org.workcraft.plugins.examples.petri;
 
import org.workcraft.annotations.IdentifierPrefix;
import org.workcraft.annotations.VisualClass;
import org.workcraft.dom.math.MathNode;
 
@IdentifierPrefix("t")
@VisualClass(VisualTransition.class)
public class Transition extends MathNode {
}

However, for Petri Net Places, there is an additional property that must also be considered in its implementation. This property is the number of tokens that they hold and it can simply be implemented as an integer type, along with a pair of getter and setter methods to retrieve and change this number respectively:

Place.java
package org.workcraft.plugins.examples.petri;
 
import org.workcraft.annotations.IdentifierPrefix;
import org.workcraft.annotations.VisualClass;
import org.workcraft.dom.math.MathNode;
import org.workcraft.exceptions.ArgumentException;
import org.workcraft.observation.PropertyChangedEvent;
 
@IdentifierPrefix("p")
@VisualClass(VisualPlace.class)
public class Place extends MathNode {
    public static final String PROPERTY_TOKENS = "Tokens";
 
    protected int tokenCount = 0;
 
    public int getTokenCount() {
        return tokenCount;
    }
 
    public void setTokenCount(int value) {
        if (tokenCount != value) {
            if (value < 0) {
                throw new ArgumentException("The number of tokens cannot be negative.");
            }
            this.tokenCount = value;
            sendNotification(new PropertyChangedEvent(this, PROPERTY_TOKENS));
        }
    }
}

Note that our Place class calls the sendNotification method in setTokenCount. This method is actually implemented from the MathNode class and is simply used to notify Workcraft of any important background changes and makes it update the model accordingly (e.g. when a user changes the number of tokens held in a place).

Visual layer classes

As mentioned above, the main difference that distinguishes the VisualPetri class from VisualGraph is that there are now two types of nodes (i.e. places and transitions) to be added to the graph editor tools:

tools.add(new NodeGeneratorTool(new DefaultNodeGenerator(Place.class)));
tools.add(new NodeGeneratorTool(new DefaultNodeGenerator(Transition.class)));

It is worth noting that there are several additions to be made to the visual layer for a Petri Net Place class (VisualPlace):

  • The @DisplayName(“Place”) annotation is used to give a more user-friendly name of Place for this node type. Note that the class name will be used by default instead, if it is not specified.
  • The @Hotkey(KeyEvent.VK_P) annotation is used to assign the key P as a hotkey to activate the place generator tool.
  • The @SVGIcon(“images/examples-petri-node-place.svg”) annotation is used to specify an icon for the place generator tool (where the corresponding SVG file should be placed in the res/images directory under the plugin project directory).
  • A new property called Tokens is registered with the Property editor to specify the number of tokens in any selected place. For more about this property, see the use of the addPropertyDeclaration method below.
    • Note that the parameters (in order) for the PropertyDeclaration constructor references the specified object, name of its property, the property's type of class and if the property is writable, combinable and templatable.
  • The method draw is also overridden to display the tokens in the place (as well as to represent both the place and token as circles).
VisualPlace.java
package org.workcraft.plugins.examples.petri;
 
import org.workcraft.annotations.DisplayName;
import org.workcraft.annotations.Hotkey;
import org.workcraft.annotations.SVGIcon;
import org.workcraft.dom.visual.DrawRequest;
import org.workcraft.dom.visual.VisualComponent;
import org.workcraft.gui.Coloriser;
import org.workcraft.gui.graph.tools.Decoration;
import org.workcraft.gui.propertyeditor.PropertyDeclaration;
import org.workcraft.plugins.shared.CommonVisualSettings;
 
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
 
@DisplayName("Place")
@Hotkey(KeyEvent.VK_P)
@SVGIcon("images/examples-petri-node-place.svg")
public class VisualPlace extends VisualComponent {
 
    public VisualPlace(Place place) {
        super(place);
        addPropertyDeclarations();
    }
 
    private void addPropertyDeclarations() {
        addPropertyDeclaration(new PropertyDeclaration<VisualPlace, Integer>(
                this, Place.PROPERTY_TOKENS, Integer.class, true, true, false) {
            public void setter(VisualPlace object, Integer value) {
                object.getReferencedComponent().setTokenCount(value);
            }
            public Integer getter(VisualPlace object) {
                return object.getReferencedComponent().getTokenCount();
            }
        });
    }
 
    @Override
    public Place getReferencedComponent() {
        return (Place) super.getReferencedComponent();
    }
 
    @Override
    public Shape getShape() {
        double size = CommonVisualSettings.getNodeSize() - CommonVisualSettings.getStrokeWidth();
        double pos = -0.5 * size;
        return new Ellipse2D.Double(pos, pos, size, size);
    }
 
    @Override
    public void draw(DrawRequest r) {
        super.draw(r);
        Graphics2D g = r.getGraphics();
        Decoration d = r.getDecoration();
        g.setColor(Coloriser.colorise(getForegroundColor(), d.getColorisation()));
        int tokenCount = getReferencedComponent().getTokenCount();
        double tokenSize = 0.5 * CommonVisualSettings.getNodeSize();
        if (tokenCount == 1) {
            double tokenPos = -0.5 * tokenSize;
            g.fill(new Ellipse2D.Double(tokenPos, tokenPos, tokenSize, tokenSize));
        } else if (tokenCount > 1) {
            String tokenString = Integer.toString(tokenCount);
            Font font = g.getFont().deriveFont((float) tokenSize);
            Rectangle2D rect = font.getStringBounds(tokenString, g.getFontRenderContext());
            g.setFont(font);
            g.drawString(tokenString, (float) -rect.getCenterX(), (float) -rect.getCenterY());
        }
    }
}

To finish up, it is also worth assigning a hot-key, display name and an icon to the visual layer for a Petri Net Transition class (VisualTransition) independently:

@DisplayName("Transition")
@Hotkey(KeyEvent.VK_T)
@SVGIcon("images/examples-petri-node-transition.svg")

Note that VisualTransition basically uses the same constructor and overriddes the draw method like VisualVertex (with the exception of its naming and annotations).

Solution

Download all the source code of the example classes used throughout this exercise here:

petriplugin.zip (10 KiB)