User Tools

Site Tools


devel:plugin:petri

Advanced plugin for Petri Net model

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

The key 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 i.e. a place can connect to a transition, but not to another place.
  3. Its state is expressed by the set of all places that hold a token. This is also known as a marking.

By taking the aspects of this model into consideration, it is possible to encode them as a Petri Net plugin. Note that the plugin and descriptor classes for the Petri Net model are similar to those of the Directed Graph model and their description is skipped.

Mathematical layer classes

Like the Graph class that was implemented in the Directed Graph plugin exercise, the Petri class here also extends the AbstractMathModel class meaning most of the necessary functionality is now implemented. The only difference here is that nodes of the same type are not able to connect 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");
        }
    }

Because there are now two different types of nodes to consider, both places and transitions should be assigned different prefixes such as p and t respectively. This can simply 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, because Petri Nets need to also consider the number of tokens that they hold, another property to represent this is also required. This can simply be achieved by implementing it as an integer field, along with its respective getter and setter methods:

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 the Place class calls the sendNotification method in its setter, which was actually implemented from the MathNode class. All this method does is just to notify Workcraft of any important background changes and update the model accordingly (e.g. when the number of tokens in a place changes).

Visual layer classes

As mentioned above, one of the key differences of Petri Nets from their Graph counterparts is that they now contain two types of nodes instead of one (i.e. places and transitions). Because of this, they must be added accordingly to the graph editor tools of the VisualPetri class:

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

Unlike the VisualVertex class from the Directed Graph exercise, there are several additions that needs to be made in the VisualPlace class. Note that most of the following have been implemented using annotations, which was used to help separate what needs to work in a class and what makes the class more appealing:

  • The @DisplayName(“Place”) annotation is used to give a user-friendly name of the Place node type. Note that the class name will be used by default instead, if no name 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 this specified SVG file should be placed in the res/images directory of the plugin project).
  • A new (integer) property called Tokens created with the Property editor to specify the number of tokens found in the selected place. For more about this property, see the use of the addPropertyDeclaration method below.
    • Note that the parameters (as ordered) in 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.utils.Coloriser;
import org.workcraft.gui.tools.Decoration;
import org.workcraft.gui.propertyeditor.PropertyDeclaration;
import org.workcraft.plugins.builtin.settings.CommonVisualSettings;
 
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
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) {
            @Override
            public void setter(VisualPlace object, Integer value) {
                object.getReferencedComponent().setTokenCount(value);
            }
            @Override
            public Integer getter(VisualPlace object) {
                return object.getReferencedComponent().getTokenCount();
            }
        });
    }
 
    @Override
    public Place getReferencedComponent() {
        return (Place) super.getReferencedComponent();
    }
 
    @Override
    public boolean hitTestInLocalSpace(Point2D pointInLocalSpace) {
        double size = CommonVisualSettings.getNodeSize() - CommonVisualSettings.getStrokeWidth();
        return pointInLocalSpace.distanceSq(0, 0) < size * size / 4;
    }
 
    @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, a display name and an icon to the VisualTransition class independently. Note that VisualTransition is very similar to VisualVertex of Graph example, but includes display name, hotkey and icon annotations:

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

Solution

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

petriplugin.zip (9 KiB)