CPN Tools 4 Extensions: Part 3: Graphics and Callbacks

This post has 2127 words. Reading it will take approximately 11 minutes.

This part of the introduction to CPN Tools extensions is by far the more fun one.  It deals with graphics and callbacks.  It makes it possible to draw graphics directly in CPN Tools from an extension and to make hooks models can use to do that.  This in essence is a higher-level version of Mimic/CPN and a more integrated version of the BRITNeY Suite.

I shall assume readers are familiar with part 1 and part 2 of this introduction.  We’ll be setting up a visualization chain that works like below.  It makes use of several of the communication patterns discussed in part 1 (annotated), but all of this is not only transparent to users of the extension, but for a large part also to the developer of the extension.

Screen Shot 2013-09-30 at 16.50.53

In this section, we’ll go thru the generic visualization calls and how they are abstracted by the graphics extension. We’ll talk about the message sequence chart visualization which uses this library, and we’ll finish by showing how to make it possible to execute extension methods from models or using ML evaluate.

Visualization Callbacks

CPN Tools can do callbacks using communication patterns 3 or 8.  Originally, this only allowed updating the simulation status, but CPN Tools 4 has significantly cleaned up and improved this feature.  For a full overview of the available callbacks, refer to the documentation.  Note that callbacks are very much in flux, and reality may not correspond completely to the documentation (for example, at the time of writing, the documentation contains several planned but not yet implemented callbacks).

While it would be possible to construct the packages to do visualization, this would be like writing your program in machine code.  Instead, CPN Tools comes bundled with the following extension package:

org.cpntools.simulator.extensions.graphics
Implements a generic drawing library and the message sequence chart drawing functionality in CPN Tools.

The Library takes a simple scene-graph approach to graphics. We have a composite data-structure as shown here:

graphics

At the top, we have an Element which has a position and size, and can be moved around. It has four subclass Nodes, which adds colors and a couple other graphical attributes. Composites can contain other elements, and are in particular represented by a group, which is an invisible grouping of other elements and a Canvas, which contains a complete visualizations and corresponds to a canvas or page in CPN Tools.

A Canvas needs a Channel as input, but as soon as it has that, we can create graphics by simply adding Elements to it. The Canvas can operate in two modes: it can update immediately whenever anything is added, or it can do a bulk-update when a number of elements have been updated. Currently, the bulk-update also updates each element individually, so only little is gained, but if the extension needs to do a lot of processing between updates, this may be more smooth in the GUI, and also the Canvas combines updates, so if an element is moved twice in the extension, only the final position is sent to the GUI.

Here is a simple extension that creates a fresh Canvas and draws some graphics on it:

public class ColorfulWorld extends AbstractExtension {
    public ColorfulWorld() {
        addInstrument(new Instrument("Draw"));
        addObserver(new Observer() {
            public void update(final Observable source, final Object value) {
                if (value instanceof Invocation) {
                    try {
                        drawStuff(createCanvas("New Canvas"), 2);
                    } catch (final Exception e) { }
                }
            }
        });
    }

    public String getName() { return "Colorful World"; }
    public int getIdentifier() { return Extension.TESTING; }

    private int next = 0;
    private final Map canvases =
        Collections.synchronizedMap(new HashMap());

    String createCanvas(String name) throws Exception {
        final Canvas c = new Canvas(channel, name, true, true);
        final String key = "canvas" + ++next;
        canvases.put(key, c);
        return key;
    }

    void drawStuff(String canvas, int count) throws Exception {
        final Canvas c = canvases.get(canvas);
        synchronized (c) {
            boolean suspended = c.suspend(true);
            for (int i = 0; i < count; i++) {
                c.add(makeFigure().move(new Point(i * 150, 0)));
            }
            c.suspend(suspended);
        }
    }

    Element<?> makeFigure() throws Exception {
        final Group g = new Group();
        final Rectangle r = new Rectangle(-35, 0, 70, 100);
        r.setBackground(Color.RED);
        g.add(r);
        final Ellipse e = new Ellipse(-50, 100, 100, 100);
        e.setBackground(Color.ORANGE).setForeground(Color.ORANGE);
        g.add(e);
        final Line l = new Line(new Point(-20, 0), new Point(-70, -70),
                                new Point(70, -70), new Point(20, 0));
        l.setFilled(true).setBackground(Color.BLUE);
        g.add(l);
        return g;
    }
}

The first 16 lines should be pretty self-explanatory: we create an extension, add an instrument, and react to instrument invocation. When the instrument is applied, we call drawStuff on the output of createCanvas. This is the result of applying the new instrument:

Screen Shot 2013-10-01 at 17.10.34

createCanvas (ll. 22-27) instantiates a Canvas object (l. 23). This requires a Channel parameter, and the AbstractExtension exposes a field containing the current channel (after the call to start). The remaining parameters to the Canvas are a name (from the method parameter), whether the canvas should be moved to the front or just created in the background, and whether the canvas should be created as a canvas or regular page1). Other than that, we generate a string handle for the canvas and store it in the canvases map, and return the handle.

drawStuff (ll. 29-38) takes a canvas handle and a count and draws a corresponding number of figures. The implementation uses fine-grained synchronization, so the canvases map is synchronized (ll. 19-20) and we synchronize on individual canvases (ll. 31-37). Anything triggered from instruments, options or external events should be synchronizes to avoid race conditions.

We suspend (l. 32) the canvas before drawing starts and unsuspends (l. 36) afterwards. This is not necessary here, but a good idea so things that should be atomic updates can be handled as efficiently as possible by the framework. We get a return value from calling suspend with the parameter true (l. 32). This value indicates whether the canvas as already suspended. By using this value to unsuspend (l. 36) instead of the constant false, it is possible to call drawStuff from another method which suspends the canvas without the canvas being unsuspended unexpectedly after the call.

All mutator methods on Elements return the element itself. This makes it possible to chain mutators. This is seen in line 34 (and ll. 46 and 50), where we add the output of the mutator to the canvas. This line could also be written as:

                Element<?> f = makeFigure();
                f.move(new Point(i * 150, 0));
                c.add(f);

but chaining can often make code simpler, especially when we need to set multiple parameters as lines 46 and 50 show. Despite this looking like a traditional functional interface, the behavior is completely standard. It just allows writing code that looks functional and at the same time allowing writing code that looks imperative.

makeFigure (ll. 40-53) takes care of actually drawing a figure. We create a Group (l. 41), and add a Rectangle (ll. 42-44), a Ellipse (here really a circle; ll. 45-47) and a line (here really a filled polygon; ll. 48-52). The Line/polygon can take any number of intermediate points (not less than 2, though). By adding all the elements to a Group, we can treat them as one in the caller, as seen by drawStuff moving the entire figure in a single call (l. 34).

To sum up the important points of this example,

  • we should synchronize calls prompted by options, instruments, and external actions,
  • we should suspend a canvas prior to updates that should be considered atomic and unsuspend it afterwards,
  • we can chain calls to mutators of graphical elements, and
  • we should group elements that logically belong together to allow treating them as one.

Message Sequence Charts

Where the actual packages sent between extensions and the GUI can considered the machine language of the drawing framework, using the above drawing framework should be considered the assembler language of drawing in CPN Tools. It is definitely more abstract and high-level than fiddling with network packets, but it still requires a lot of book-keeping. The MSC library essentially build the structured or object-oriented language on top of this: instead of dealing with low-level primitives like rectangles and ellipses, we deal with graphical objects that make sense in the domain of message sequence charts: process lines, events, etc.

We encourage using a model-view-controller pattern when designing visualizations: make a model representing the domain objects of the visualization, create a view using the above primitives, and create the controller however you need (typically using the method described in the next section).

In the case of MSCs, the domain model comprises processes, events (internal, synchronous, asynchronous, dangling and dropped), and lines. Everything is tied together in a central MSC object. All model objects extend the Java Observable class to allow view(s) to attach themselves. The model is abstract and contains no unnecessary view information. The model does not even need to know anything about how it is being visualized. In fact, the entire model for MSCs was taken directly from the BRITNeY Suite with only minimal changes.

For the view, we identified building blocks. One building block is a process line, another is an event and the last is a milestone line. We created subclasses of Group for each of these. Each sub-class makes sure to add and maintain all elements belonging to itself. This means that when we change the model to add another process, we can update the view by (roughly) just adding another process line object. We made sure to draw each object at a well-defined origin, so it is easy to move them around thanks to grouping. Each view is added as an observer to the model object it represents, and makes sure to react to model changes appropriately (e.g., when an event is added to the model, all process lines need to extend themselves and move the small rectangle at the bottom).

A controller simply instantiated the model correctly. CPN Tools ships with a simple debugger (not installed by default and the topic of the next installment in this series) that makes it easy to make a quick-and-dirty controller for testing visualizations. Here is the MSC extension in action along with a simplistic controller running from the debugger:

Screen Shot 2013-10-01 at 18.36.56

This is exactly how all the figures for communication patterns were created for part 1.

Remote Procedure Calls

It is really easy to use the remote procedure call mechanism in CPN Tools. Developers do not have to concern themselves with the protocol, stub generation, communication or dispatching. All developers have to do is to provide a simple object which contains the desired calls.

In fact, without further ado, let’s dive directly into the implementation of a RPC implementation for our example above:

    public final class Handler implements NamedRPCHandler {
        public String structureName() { return "Draw"; }

        public String createCanvas(String name) throws Exception {
            return ColorfulWorld.this.createCanvas(name);
        }

        public void drawFigures(String canvas, Integer count) throws Exception {
            drawStuff(canvas, count);
        }

        public void gimmeFive(String name) throws Exception {
            drawFigures(createCanvas(name), 5);
        }
    }

    public Object getRPCHandler() {
        return new Handler();
    }

We just add this code to the ColorfulWorld class above. We implement the NamedRPCHandler (ll. 1-2) to give the generated code a recognizable namespace. By returning Draw from the structureName (l. 2), all code will be generated in a structure called Draw. If we do not do so, all generated code will be created in a generated namespace and be difficult to access.

We create 3 call-backs (ll. 4-14). The first passes on the call to a method with the same name (l. 5) and the second passes everything off to a method with another name (l. 9). We can also call multiple methods and even do computations in the controller (l. 13). In general, it is better to just do simple dispatching and translating between handles and values if necessary. For example, we could do the translation between Canvas handles and Canvases here instead of directly in the createCanvas and drawStuff methods.

The getRPCHandler method (ll. 17-19) just returns a new instance of our handler class. We note that the Handler class is an inner class of ColorfulWorld, so it has implicit access to the correct instance. It is also possible to create the handler as a separate class, but then it should probably have an explicit instance.

There are two important points about the handler we have not yet mentioned. First, it is important that the handler is a real class and not an anonymous class. This is due to how dispatching is done. Second, all primitive arguments should be boxed. This means, we should always use Integer in place of int and Boolean in place of boolean. We do that in drawFigures (l. 8). Since Java 5 and newer has automatic boxing and unboxing this makes little practical difference for programmers, but this works differently for the dispatch code, so it has to be like this.

If we start a new net with our extension running, we can get this:

Screen Shot 2013-10-01 at 19.00.31

We see that the Draw structure has a function for each of the methods in Handler above. Each is generated using the correct parameters and types. We see that when we execute code, parameters are passed, the code executed, and values returned. All in all, it works exactly as expected, and the user does not even see that the extension was involved. It is easy to make libraries like this, and this is exactly how the MSC library is implemented: it creates a MVC structure as above and exposes a controller using the RPC mechanism shown here.

We can consider the exported structure the end-user API or program building on top of machine code, assembler code, and structured code. Of course, the RPC machanism is completely independent of the visualization library above; we can use visualization without RPC and we can use RPC without visualization. They just work really well together.

Conclusion

Here, we have seen how it is possible to create visualizations and use a simple RPC mechanism to call back to extensions. We have seen how we in less than 75 lines can implement the rather daunting communication pattern for visualization at the top. We build upon a simple callback package format (the machine code), a simple but flexible scene-graph-like visualization library (the assembler code) to create a high-level model-view-controller architecture for out domain (the structured code), which is exposed using a super-simple but flexible RPC mechanism (the end-user API).

Time person of the year 2006, Nobel Peace Prize winner 2012.


  1. See more about canvas and regular pages in the introduction to message sequence charts. []

5 thoughts on “CPN Tools 4 Extensions: Part 3: Graphics and Callbacks

  1. Hi Michael,

    Does the current Simulator Extensions – CPN Tool v.4- have the library Draw ?

    Regards,

      1. There is no exported SML interface for this functionality – it is purely a Java interface available for extensions written in Java. If you need it, you can create your own interface using the RPC functionality – very similar to the MSC RPC wrapper – or you can write your code as a Java extension.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.