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:
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.
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:
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:
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:
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).
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:
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:
@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.@Hotkey(KeyEvent.VK_P)
annotation is used to assign the key P
as a hotkey to activate the place generator tool. @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).addPropertyDeclaration
method below.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. draw
is also overridden to display the tokens in the place (as well as to represent both the place and token as circles). 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:
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); } }
Up-to-date source code of the example classes used throughout this exercise are available in the workcraft-plugin-example-petri repo.