====== 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: - They use two different types of nodes: //**places**// and //**transitions**//. - 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. - 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 [[:devel:plugin:graph|Directed Graph model]] and their description is skipped. ===== Mathematical layer classes ===== Like the ''Graph'' class that was implemented in the [[:devel:plugin:graph|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). ===== 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: 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 [[:devel:plugin:graph|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). 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); } } ===== Solution ===== Up-to-date source code of the example classes used throughout this exercise are available in the [[https://github.com/workcraft/workcraft-plugin-example-petri|workcraft-plugin-example-petri]] repo.