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, as checked by the validateConnection method:

Petri.java
package org.workcraft.plugins.examples.petri;
 
import org.workcraft.dom.Container;
import org.workcraft.dom.math.AbstractMathModel;
import org.workcraft.dom.math.MathNode;
import org.workcraft.exceptions.InvalidConnectionException;
import org.workcraft.serialisation.References;
 
public class Petri extends AbstractMathModel {
 
    public Petri() {
        this(null, null);
    }
 
    public Petri(Container root, References refs) {
        super(root, refs);
    }
 
    @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:

VisualPetri.java
package org.workcraft.plugins.examples.petri;
 
import org.workcraft.annotations.DisplayName;
import org.workcraft.dom.generators.DefaultNodeGenerator;
import org.workcraft.dom.visual.AbstractVisualModel;
import org.workcraft.dom.visual.VisualGroup;
import org.workcraft.gui.tools.CommentGeneratorTool;
import org.workcraft.gui.tools.ConnectionTool;
import org.workcraft.gui.tools.NodeGeneratorTool;
import org.workcraft.gui.tools.SelectionTool;
 
@DisplayName("Petri Net")
public class VisualPetri extends AbstractVisualModel {
 
    public VisualPetri(Petri model) {
        this(model, null);
    }
 
    public VisualPetri(Petri model, VisualGroup root) {
        super(model, root);
    }
 
    @Override
    public void registerGraphEditorTools() {
        addGraphEditorTool(new SelectionTool());
        addGraphEditorTool(new CommentGeneratorTool());
        addGraphEditorTool(new ConnectionTool());
        addGraphEditorTool(new NodeGeneratorTool(new DefaultNodeGenerator(Place.class)));
        addGraphEditorTool(new NodeGeneratorTool(new DefaultNodeGenerator(Transition.class)));
    }
 
    @Override
    public Petri getMathModel() {
        return (Petri) super.getMathModel();
    }
}

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:

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.properties.PropertyDeclaration;
import org.workcraft.gui.tools.Decoration;
import org.workcraft.plugins.builtin.settings.VisualCommonSettings;
import org.workcraft.utils.ColorUtils;
 
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<>(Integer.class, Place.PROPERTY_TOKENS,
                value -> getReferencedComponent().setTokenCount(value),
                () -> getReferencedComponent().getTokenCount())
                .setCombinable());
    }
 
    @Override
    public Place getReferencedComponent() {
        return (Place) super.getReferencedComponent();
    }
 
    @Override
    public Shape getShape() {
        double size = VisualCommonSettings.getNodeSize() - VisualCommonSettings.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(ColorUtils.colorise(getForegroundColor(), d.getColorisation()));
        int tokenCount = getReferencedComponent().getTokenCount();
        double tokenSize = 0.5 * VisualCommonSettings.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());
        }
    }
 
    @Override
    public boolean hitTestInLocalSpace(Point2D pointInLocalSpace) {
        double size = VisualCommonSettings.getNodeSize() - VisualCommonSettings.getStrokeWidth();
        return pointInLocalSpace.distanceSq(0, 0) < size * size / 4;
    }
}

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:

VisualTransition.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.VisualComponent;
 
import java.awt.event.KeyEvent;
 
@DisplayName("Transition")
@Hotkey(KeyEvent.VK_T)
@SVGIcon("images/examples-petri-node-transition.svg")
public class VisualTransition extends VisualComponent {
 
    public VisualTransition(Transition transition) {
        super(transition);
    }
}

Solution

Up-to-date source code of the example classes used throughout this exercise are available in the workcraft-plugin-example-petri repo.