Serving Clickable Scalable Vector Graphics (SVG) Part 3
A Petri Net State Management Process (as part of the Express Server)
This tutorial assumes some knowledge of JavaScript, HTML, and node.js
SVG Scalable Vector Graphics is a part of HTML5. It is a markup language as well.
SVG allows for rendering graphics as line drawings. You make drawings that look like an HTML page. Or, you can create artistic master pieces.
Continuing from the previous post, SVG Part 2
We have already been through a few steps to get the file ready:
- Create an SVG graphic to serve on a page.
- Make a little JavaScript utility to generate some code for the animation tools.
- Set up a simple node.js server using express to serve the page
The last step is essentially done. But, there is the all important part of it that has to do with running a Petri Net state manager that has not been completely address. In the last part of this tutorial, SVG Part 2, the we launched a child process that could send state information to the web server, which in turn would send the information to all listening clients in web browsers. However, it did little more.
So, in this part of the tutorial Part 3, we can explore loading in a real Petri Net state keeper. Real is relative. But, we can use a simple node module that runs a Petri-net and could be used in a small embedded system to manage the relationship between hardware elements. It is a node.js module, too, called run-petri. And, I have it available on github: run-petri.
So, this part of the tutorial is about this substep:
-
- Include a working child process that manages Petri State.
There are a few steps left to go through before we can get a live page up. Here is what is left to do:
- Create JavaScript that will animate the SVG
- Run the page in a browser.
Staying with the lighter side of things, will have something to show off when we're done.
One More Support Script
DevOps is never done, that is, if you call some of our support scripts DevOps. In part one of this tutorial Part 1, we found a way to read in information from SVG and output JSON structures that describe a Petri Net seen in the SVG. Of coure, that had to do with being clever with naming SVG elements and making use of tha fact the SVG is a kind of XML format.
The output from the tools made there created JSON files that will prove to be useful in setting up operations on the web pages. But, the node module we will soon be using, run-petri, uses a slightly different JSON data structure for initialization. In fact, it has about the same information that we got from the graph. So, you might guess that it will be easy to make a tool to transform one JSON representation into another. And, that happens to be the case.
In fact, here is the code for it:
var fs = require('fs'); // we will be reading from files.
var scraperOutput = process.argv[2]; // named on the command line.
// Here is how to turn the text into a JSON data structure
// for use in JavaScritp
var petridDef = fs.readFileSync(scraperOutput,'ascii').toString();
petridDef = JSON.parse(petridDef);
// This is the high level structure of the JSON file
// that is required input to a run-petri application.
var runPetriConf = {
"nodes" : [],
"transitions" : []
};
// An easy -- cheesy -- way to ensure no duplicates in a node list.
function nodeDuplicates(nodeList) {
var asObject = {};
nodeList.forEach((name) => { asObject[name] = 1; });
nodeList = Object.keys(asObject);
return(nodeList);
}
//
// injectNodeList -- puts node objects into the config node list
function injectNodeList(nodeList,nodeType) {
nodeList = nodeDuplicates(nodeList)
nodeList.forEach((nodeId) => {
runPetriConf.nodes.push({ "id" : nodeId, "type" : nodeType })
});
}
// injectNodes -- puts all the types into our list.
// Here you can see the who the formats differ.
// One uses an array for each type of node.
// The other labels a node with its type.
// And, the way run-petri works, it just wants to run through a list and setup
// each node type.
function injectNodes(petriData) {
injectNodeList(petriData.inputs,"source");
injectNodeList(petriData.outputs,"exit");
injectNodeList(petriData.states,"state");
}
// Transitions are a separate thing in both formats.
// In fact, the are almost the same. But, this is a format switch
// from an object with transition name keys, to an array of transition objects
// each of which has a label to identify it.
function injectTransitions(petriData) {
var transObject = petriData.transitions;
for ( var transName in transObject ) {
runPetriConf.transitions.push({
"label" : transName,
"inputs" : transObject[transName].inputs,
"outputs" : transObject[transName].outputs
});
}
}
// Put the nodes into the configuration
injectNodes(petridDef);
// Put the transitions into the configurations.
injectTransitions(petridDef);
// Be lazy and dump this to the console.
console.log(JSON.stringify(runPetriConf,null,2))
NOTE: the stringify is important. The quotes have to appear in the JSON format.
So, you see that most of the difference in formats is that one format is using objects whose keys identify the Petri net element, while the other format is listing arrays of objects each containing Petri net elements label with names and types.
As this dumps the format to the console, you can dump its contents into the file with the right shell command as such:
node prepareRunPetri p1.json > petriConf.json
The output in the file should appear similar to this:
{ "nodes" :
[ { "id" : "circle-input-C", "type" : "source" },
{ "id" : "circle-input-A", "type" : "source" },
{ "id" : "circle-input-D", "type" : "source" },
{ "id" : "circle-input-E", "type" : "source" },
{ "id" : "circle-input-B", "type" : "source" },
{ "id" : "circle-output-Q", "type" : "exit" },
{ "id" : "circle-output-P", "type" : "exit" },
{ "id" : "circle-state-F", "type" : "state" },
{ "id" : "circle-state-G", "type" : "state" },
{ "id" : "circle-state-H", "type" : "state" } ],
"transitions" :
[ { "label" : "rect-transition-A:B-F",
"inputs" : [ "circle-input-A", "circle-input-B" ],
"outputs" : [ "circle-state-F" ] },
{ "label" : "rect-transition-C-G",
"inputs" : [ "circle-input-C" ],
"outputs" : [ "circle-state-G" ] },
{ "label" : "rect-transition-F:G-P",
"inputs" : [ "circle-state-F", "circle-state-G" ],
"outputs" : [ "circle-output-P" ] },
{ "label" : "rect-transition-H-Q",
"inputs" : [ "circle-state-H" ],
"outputs" : [ "circle-output-Q" ] } ] }
Petri Net State Keeper
Now, lets turn our attention to the node.js script that is spawned from the Express server. Recall that we set up communication between a child process in the file petriState.js in express server we developed in the second part of this tutorial SVG Part 2.
But, for the moment, the code looks like this:
process.on('message', (m) => {
console.log('CHILD got message:', m);
});
setInterval(() => {
process.send({ state: 'OK', data: "ready" });
},1000)
This is fairly pointless code.
We can start getting meaningful if we at least require the run-petri module. That's this for starters:
const PetriClasses = require('run-petri');
const pNode = PetriClasses.pNode;
const RunPetri = PetriClasses.RunPetri;
process.on('message', (m) => {
console.log('CHILD got message:', m);
});
setInterval(() => {
process.send({ state: 'OK', data: "ready" });
},1000)
Notice the small code change at the top of the file. PetriClasses is the container module that exposes to class definitions. The first is pNode, a Petri Net node. And, the second is the RunPetri class, which is the class that injests a run-petri configuration file (our p1.json) and has methods to take in resources, emit outputs, and step the Petri net through a sequence of state changes.
In order to make all this Petri net lingo a little more clear and in order to see how run-petri will help with the managing a restricted class of nets, we can quickly go over some Petri net theory. I will give links to more detailed discussions.
Run-Petri Module
Going a Little Deeper into Petri Nets and theSo, about Petri Nets. There is a picture of an unmarked one at the top of the page. You can see that they are directect acyclic graphs that have two types of nodes. There are circles, refered to as places and boxy lines refered to as transitions.
The state of the Petri net is a marking of the places with tokens. That is, each place that is marked will have a black circle in it. The black circle is referred to as token.
When the state changes, what changes is the marking. Here is a simple before and after state change in a picture:
Petri Nets have been around a long time. So, you can imagine these are well described. And, before writing more, I will just send you elsewhere to find out more about Petri Nets and how they work. Here is pretty good link to a brief description of Petri Nets Petri Nets Explained.
Once you have read that article, you may appreciate that run-petri attempts to avoid confusion and conflict. While it tries to make the following modes of operation possible:
- sequence
- concurrency
- synchronization
- merge
- priority/inhibit
So, please follow the links and get more information about these patterns. How run-petri makes these patterns possible is peculiar to its own implementation. Notice that I say, "makes these patterns possible". These patterns will be decided upon in when you make a configuration for run-petri. It is the job of run-petri to allow these patterns to run.
What I want to do now is tell you more about the module we will be using, run-petri.
Run-Petri
The node.js ModuleYesterday, I made a good exposition here. But, I came back this morning to find all my editing yesterday destroyed by the Codementor system. So, I will have to be brief.
npm installation
npm install run-petri
sources and exits
The node.js module, run-petri, identifies two types of places, nodes, which allow for data to enter and leave the network. Putting data into the network corresponds, roughly, to creating more tokens for markings.
The node.js module, run-petri, provides a means to specify the handlers and callbacks for these kinds of nodes.
Here is a code snipet that sets up a RunPetri instance.
var pNet = new RunPetri();
// initialize the network here. note: two parameters, no special cases.
pNet.setNetworkFromJson(net_def,callbackGenerator);
You can see the method, setNetworkFromJson being invoked. This has a paramter that is a callback, callbackGenerator. This is a function that generates callbacks by type. It itself has two parameters, id and cbType, the first being a node id, and th second being a type of node, "exit", etc.
See the example below for more information.
resources
run-petri treats the counts in the default case as resources. In the default case all tokens are just a counted place holder at a node. And, all arcs pass along just one token. You can do more with run-petri. I am not going into that here. For this tutorial I am using the default case.
some useful facts about its implementation
I will have to reconstruct what I said here on a previous date. In brief, there is a simple way to prevent confusion and conflict. That is to make sure that a node may be input for only one transition. Fan out from a node is best implemented by having a node be input to a transition that then fans out. So, fanout from a transition.
If the configuration file has confusions and conflicts, the RunPetri instance will throw an exception.
In run-petri one kind of node may act as an inhibitor to a transition, preventing it from firing if it is marked. These are the only nodes that may be used as an input more than once. They are specified as having the type "inihibit" and they also require a target transition. If you should use these, the format for their specification in the node list of the configuration JSON is as follows:
{ id: 'circle-input-C', type: 'inhibit', target : 'some transition label' }
A simple working example
Here is some, a change of the petriState.js file that was created in the last part of this tutorial, SVG Part 2
const fs = require('fs');
var PetriClasses = require('./index.js');
var pNode = PetriClasses.pNode;
var RunPetri = PetriClasses.RunPetri;
const nodeClasses = { pNode };
const gInterval = 2000;
const filename = "./pstate.json"; // load a particular file.
var jsonData = fs.readFileSync(filename,'ascii').toString();
var net_def = JSON.parse(jsonData); // into a data structure.
/* */
function callbackGenerator(id,cbType) {
if ( cbType === 'exit' ) {
// An exit callback that send data back to the parent Server.
var dataExitCb = (v) => { process.send({ state: 'data', id: nodeId, data: v }) }
return(dataExitCb)
} else if ( cbType === 'reduce' ) { // this is the default reducer...
var reducer = (accumulator, currentValue) => accumulator + currentValue;
return(reducer);
}
return((v) => { console.log(v); return(0); })
}
var pNet = new RunPetri();
// initialize the network here. note: two parameters, no special cases.
pNet.setNetworkFromJson(net_def,callbackGenerator);
// >>======== ======== ======== ======== ======== ======== ========
process.on('message', (m) => {
// A way to get data from the parent process.
// Allow the parent process a kind of
// message that feeds the network.
// This is useful for testing.
if ( m.mtype == "petri" ) {
pNet.emit(m.sourceNode,m.value);
}
});
setInterval(() => {
pNet.step(); // step the petriNet every gInterval miliseconds
process.send({ state: 'report', data: pNet.report() })
},gInterval)
Now, this is set up to take direction from the parent process and to send ouput to it for both states and data. In the code, 'pNet.report' is sending data back to the web server that the client will want to use for animating SVG. But, out data exit callback and the call pNet.emit(m.sourceNode,m.value)
also deal with data going to and from the server. These last two forms may be replaced in a application that is more for the real world, having communication with hardware modules - say. The fielding of input would not change, but process.on('message' would not be used to call the emit. And, the exit callback would not invoke, process.send(, but would call some other interface with the same parameter.
On To the Last Part of this Tutorial
Now we have a process that takes in input from someplace, one we can change out with real subsystems. It sends information to the web server which then sends messages to all clients in order to update the web pages. And, finally the Petri Net state keeper can emit outputs to listening processes.
So, there is one more step. We want to see the graphical representation of the state changes on our web pages. That is the topic the next and last part of this tutorial, Part 4. In Part 4, we can work soley on the client that loads our SVG page and animates when new messages come along. The client will receive a JSON description of the net, the same description that we made in Part 1. The client will use that description to identify the SVG elements that it is receiving messages about from the express.js server.