Serving Clickable Scalable Vector Graphics (SVG) Part 2
Serve SVG with Express on node.js
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 1, we are ready to serve up some SVG that we have already prepared.
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.
There are a few steps left to go through before we can get a live page up. Here is what is left to do:
- Set up a simple node.js server using express to serve the page
- 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.
For my server, I have picked node.js and Express. This selection is popular at the moment. And, in fact, it may be overkill for our little project. But, node.js is a good platform for working with networking. Express lets us just write paths with handlers. It is after all, a router. And, it's a winner because it allows the route->response pattern to be clearly explicated.
The program that I am going to produce is very short. In fact, I am going to drop the whole thing into the next code block. At that point, you might think that the tutorial is done. But, I will embellish the server just a little for making it special for our current application. And, there is a little something I want to do with the HTML to make loading it up work nicely.
Here is the server:
const express = require('express');
const app = express();
const fs = require('fs');
const appPort = 3000;
// Use on generic function to load file stuff and send it to the client
function loadPage(filename, res) {
fs.readFile(filename,'utf8', (err,data) => {
if ( err ) {
res.send(JSON.stringify(err));
} else {
var page = data.toString();
res.send(page);
}
})
}
// The paths (routes) to the files we want to send.
app.get('/', (req, res) => {
loadPage('example.html',res);
});
app.get('/:file', (req, res) => {
loadPage(req.params.file,res);
});
app.get('/svg/:svFile',(req,res) => { // look in a particular directory
loadPage('./svg/' + req.params.svFile,res);
});
// Run it. Really, you have to or else this app will terminate and the bowser
// will put up error messages.
app.listen(appPort, () => console.log(`Example app listening on port ${appPor}!`))
So, what is going on here? Well, this is very basic express app, a server listening on a port, 3000 in this case. This server response to three different URL patterns.
The first URL patterns is the root, '/', which responds with just one page, in this case the page is example.html, which is read in from a file. (No fancy rendering tricks here.)
The second URL patterns is for any file. What ever file the web page tells me to send. That allows for images, css, and JavaScript to be sent to the browser.
The third, and last, pattern is for any file that is in the svg directory.
Set up the HTML page and some JavaScript to Pull in the SVG
So, that looks pretty simple. And, it does not promise much activity. Just a server running. It puts up files if you have them. And, it has zero security. So, you will not want this to be your production server. But, it is nice to run on your own computer, desktop or laptop, and it can resonde to localhost requests.
These requests will be of this form: http://locahost:3000/svg/pic.svg
http://locahost:3000/
, etc.
So, wouldn't it be nice if all we had to do was type locahost:3000 into the browser and get up a working HTML page with SVG in it? Of course!
So, we have to fix up the HTML page to do something useful. So, now it's time to stop doing server side JavaScript and spend a little time doing some client side JavaScript.
In fact, we can have the HTML page be a fairly skeletal page and have it fetch SVG after it is loaded. Or, we can have a button on the page that grabs up an SVG file when we click it. In fact, we can do both. So, here is how:
Keep the HTML Short and Minimal
For our web page, we will still need some HTML, in particular HTML 5. HTLM 5 exposes SVG elements on the same level as well established HTML elements. But, the page only has to be a framework.
If we created a page without SVG, we could create a lot of our display with HTML and with a library of images, .png and .jpg files and others. The HTML would be long and complicated. There would be a lot of hand tweeking and special JS code to make it all show up.
But, for our purposes, all we really need to do is have a place put the SVG. It will have to be the right size box. And, then we need to get the file into that position. We might write an HTML page with the SVG pasted in. But, we don't have to. The SVG can be loaded a little after the HTML framework. (I have mostly forgotten one sanfu where the late loading of SVG solved an error in one browser.) We can later exptrapolate on our lazy loading of SVG, and load more than one image on the page when we so desire.
So, let's start with the simple HTML which can be used many times over to load any number of SVG pages.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>svg demo</title>
</head>
<body>
<h1>Simple example that loads an SVG application</h1>
<div id="svgGoesHere-petri" >
<!--- SVG GOES HERE -->
</div>
</body>
</html>
<script language="javascript">
window.appVars = {
petri : { }
};
window.fetchSections = "petri";
window.targetDiv = "svgGoesHere-petri";
window.fetchPage = "petri.svg"
window.useFetchSVG = true;
</script>
<script language="javascript" src="jquery-2.1.4.min.js"></script>
<script language="javascript" src='svg.min.js'></script>
<script language="javascript" src="petriClientApp.js"></script>
So, this is an HTML5 file. It uses jquery, which is fairly popular. And, for a little help with SVG, it uses svg.js. You can find out more about this SVG library at (svgjs.com)[http://svgjs.com/]. This example will only use svg.js to load in the SVG. However, you can do much more with it.
At the bottom of the HTML page, just up from the JavaScript references, is a small amount of JavaScript which sets up some window wide variables. These variables tell code in the file petriClientApp.js what to load and where to put it.
The last JavaScript reference loads the file, petriClientApp.js. The file, petriClientApp.js, provides code for loading the SVG once the page is done loading.
Supporting Client Side JavaScript
There are two kinds of Client Side JavaScript CSJS that we need to make for our web page app to work. One kind of CSJS addresses the loading of new resources and responding to changes sent by the server. The other kind of CSJS makes changes to the graphic interfaces.
In this part of the tutorial, part 2, we are focussing on the first kind. This kind has more of systems programming flavor and deals with the networking known to the HTML page. We want to ensure that data goes back and forth between the page and the server as planned.
The first data we want to get is the SVG page itself. We can load it as soon as the document is ready. When the document is ready, the <div> element designed to contain the SVG will exist. And, the whole SVG file will be attached as a child to that <div>.
$(document).ready(function() {
// Loading this part of the page on demand.
// Imagine a case where many versions of the SVG might be loaded
// into our same HTML context. Or, imagine parts of an SVG image
// being loaded on demand.
secondFetch();
// do other things here.
});
function secondFetch() {
var section = window.fetchSections;
// fetch the svg section from the server...
// Do that if that is what is require by the HTML context.
if ( window.useFetchSVG ) {
//
var page = window.fetchPage;
// it's in the svg directory on the server
var dataFetchPage = `/svg/${page}`;
// then put it here
var parentEl = "#" + window.targetDiv;
// let jquery do the work of getting ht page.
$.get( dataFetchPage, function( data ) {
$( parentEl ).html( data ); // put it where it belongs
});
}
}
So that is all that is needed to load SVG lazily, on demand, etc.
Run the Server
So, now we have client code and server code. In my directory, I have example.html as a sibling to petriexpress.js. And, I have the svg directory containing petri.svg.
I run the server this way:
node petriexpress.js
And, the expected output shows up: Example app listening on port 3000!
So, from there I go to the browser and type localhost:3000
into the navbar. And, then I see the picture.
Keep the State of the Machine on the Server
So, we can get SVG from the server. We can load it. We can get more files if we know their names. That is all good. But, so what!
Here are two things left to accomplish.
- Animate the page
- Make it meaningful
Let's do the second thing first. The reason to choose Petri Nets for an animation example was that the Petri Nets can be used to keep track of the state of a machine or some other process. Otherwise, we could just do anything and call it an animation exercise.
We can use the Petri Nets for real things. And, we don't have to do tons of work to make them useful. That's a big win.
But, what about just showing everyone how to do some animation? Good point! But, Petri Nets can be used to show off basic animation without getting into huge projects or even slightly tedious projects. Yet, Petri Nets force us to ask one fairly important question.
Where should we keep the state that our interface is displaying, in the client or on the server?
This is an important question. Why?
Well, if you look at a lot of CMS's available for use today, you will see that either the client is very busy, more than necessary, or that a single server is carrying the weight of the whole world on it's tiny processor shoulders (or little feet or pins or whatever...).
How much can be on the server for the work to be done? How much state information should be on the client given restrictions having to do with security and the responsiveness of the page?
In our case, the entire Petri Net processing could be on the client. There could be one net on the client for each process being examined. And, the state transitions could be sent from the client to the server to run the process.
That seems OK. But, suppose your process is moving things like acid from container to container and your cell phone, running the whole thing, dies a battery death moments before a safety valve has to be turned off? Oop! Don't put the process management on the client!
Well then, why bother with drawing a picture of the net and animating things on the client? Why not just do like certain popular CMS's written in PHP? Render everything on the server, and do that once for every client and send complete picture updates (SVG or otherwise) to every open page everytime something happens on the server? In fact, if you do it just like certain CMS's do, you can mix up the rendering and the process management to work at the same time in about 256Mbytes so that memory pages are consistantly swapped while tyring to time that acid shutoff valve. Well, this situation can end up about as bad as the cell phone dying a battery death.
So, you can send state information to the clients (in fact do something like a broadcast) and let them change their displays. You can do this as a response to the state change of your net, which reports after accomplishing its state changes before letting anything else get busy.
So, the server becomes the client of the network state. And, browser clients wait for events to come from the web server before telling us what has happened.
So, what we can do is have the server (a starting point for our admin setup) spawn a process manager (same or different machine). The child process can send events to the web server (our simple express app), which in turn can send Socket.io events to the web page.
So, both our HTML page and the Express server need to grow up just a little. We can make these changes in order to get information flowing. And, after that, we will be ready to finish off our animation showing state changes.
Let's get started on the changes.
A Process State Manager that Runs Our Petri Net
The first thing I want to do is make the web server capable of talking to a Petri net process manager. The talking part is all I am really concerned about at the moment. And, I could pick a number of ways to establish communication.
Here are some choices I can make for communication:
- Read and write to a shared file (very clunky)
- Set up IPC and manage it with my own code.
- Set up a TCP connection and exchange packets
- Read and write a UART between to machines with my own ascii protocol.
- Spawn a process in node.js with IPC enabled and use the associated send and event handlers.
Why does the last one sound so good?
So, I am going to alter the Express app. This is the first alteration of it:
const express = require('express');
const fs = require('fs');
// Here is where the node.js child process mechanics is introduced
const { spawn } = require('child_process');
// ---------
const appPort = 3000;
// ---------
const app = express();
const server = require('http').createServer(app);
// ---------
// Spawn a child process. Let node.js hadle the communication.
const HWProc = spawn('node', ['./petriState.js'], // a new file in our directory
{ // Tell nodeljs how to talk to the child process
stdio: [process.stdin, process.stdout, process.stderr, 'ipc']
});
// When the child process finishes or crashes
HWProc.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
// Receive message to be forwarded to socket.io clients.
HWProc.on('message', (message) => {
console.log(message);
});
// ---------
// Use on generic function to load file stuff and send it to the client
function loadPage(filename, res) {
fs.readFile(filename,'utf8', (err,data) => {
if ( err ) {
res.send(JSON.stringify(err));
} else {
var page = data.toString();
res.send(page);
}
})
}
// The paths (routes) to the files we want to send.
app.get('/', (req, res) => {
loadPage('example.html',res);
});
app.get('/:file', (req, res) => {
loadPage(req.params.file,res);
});
app.get('/svg/:svFile',(req,res) => { // look in a particular directory
loadPage('./svg/' + req.params.svFile,res);
});
// add a way to relay data from the browser client
app.get('/changes/:type/:parameter',(req, res) => {
// put stuff into the Petri Net application
// This might not have to change - ever.
HWProc.send({ "type" : req.params.type, "message" : req.params.parameter })
});
app.listen(appPort, () => console.log(`Example app listening on port ${appPort}!`))
So, we added about ten lines, some of which were code style choices. And, now we can relay from a machine to a browser. We can do that, of course, only if our child program talks to a machine. It does not have to. But, most importantly, the web server does not have to know what is going on. It just relays information.
To test our code, we can write a very simple child program,petriState.js. All it has to do is get a message and send the web server a message every now and then. Here are those few lines that can do the job:
process.on('message', (m) => {
console.log('CHILD got message:', m);
});
setInterval(() => {
process.send({ state: 'OK', data: "ready" });
},1000);
A Web Page that Fields State Changes
Now that we are surely able to send data between the web server and the critical process manager, we are ready to set up a relay to the browser.
Let's use socket.io to make the relay happen. At this point, we can just make sure the data goes where we want. First, we want data to end up in the context of our HTML page. So, we set up some client code, allowing for one page to join in on receiving events indicating the Petri net state.
For the following code to work, it is necessary to load this socket.io library into the HTML context. So, the following line has to be added to the HTML page, example.html.
<script language="javascript" src="socket.io.min.js"></script>
Then, we add a few lines to the CSJS file, petriClientApp.js.
// Here we are connecting to our known host.
// And, further, we are picking a particular name space that socket.io knows
// how to handle.
var petriIo = io.connect('http://localhost:3000/petri');
petriIo.on('connect', function (message) {
// tell the interface...
console.log(message);
});
// here comes the data
petriIo.on('datum', function (message) {
// put data in the right recepticle
console.log(message);
});
Now, when the server is ready to serve up the connection, the web page, example.html will start receiving events.
A Web Server that Relays State Information
It's a simple step to set up a socket.io connection. But, we also may want to send data to all listeners. To do that well, we only have to be careful about one thing.
We have to add yet a few more lines of code to our server to get socket.io working on the server side.
At the top of the file, we have to add the require statement that bring in socket.io.
const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server); // io socket.io
Here, the third line is the new line that brings in socket.io and binds it to our http server.
Now, we can make a relay from the child process to the client.
// This is a line you can find for reference
HWProc.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
// New lines here...
HWProc.io = io; // saving this here.
// make a petri namespace for the socket event lister, the web browser client
HWProc.petriEvents = io.of('/petri').on('connection', function (socket) {
socket.on('message', function (data) { // this is from the client
console.log(data);
});
});
// Here is the actual message relay.
// Receive message to be forwarded to socket.io clients.
HWProc.on('message', (message) => {
HWProc.petriEvents.emit('datum', message);
});
So, now what is that thing we have to be careful about to make sure that all clients receive state information? We have to be careful to do nothing more. The dafault socket.io behabvior is to send an unqualified emit to all the clients connected to the petri namespace. In fact, we could get a lot fancier with "rooms" and specific responses to client id's. But, there is no need to. We have enough going on to relay information.
Ready for the Next Step
We now have a system than can relay and respond to data. But, we are still trying to reach our goal of a meaningful application. So, maybe the next thing to do will be to make the file petriState.js do something useful. In fact, we can program a Petri net that is defined by data we defined in part one of this turtorial.
In the next part, part 3, we will expand on the use of the Petri net to govern physical processes. in particular, we will take the results of preprocessing and use those to define a process we wish to govern.
It seemed to be a good thing to give the Petri net processing its own part. The nets are simple. The explanation of them is a little trickier. Once we can run the network that takes in data from sensors, real or virtual, and can alter the states of machines, we will have something to display on our page.
For the final part, part 4, we will create some client side JavaScript and the system will seem to come alive. In that part, we will do our own animation tricks.