Processing Library Tutorial: proXML

Elie Zananiri
last update :: 17/03/2008

introduction

proXML is a library used for reading and writing XML files. Although Processing comes with a built-in xml library, proXML is more robust and complete which makes it a better candidate for interfacing with XML files.

proXML introduces two new classes to our arsenal:

saving

We will modify our Pulse example to save the pulse data to disk in the XML format. If we look at how the Pulse object works, we notice that the important attributes to save are the x-position, the y-position, and the color. The size does not need to be saved as it is a value that fluctuates and is handled automatically for all the Pulses we create. Our XML tree will therefore look something like the following:

<pulses>
  <pulse>
    <position x="101" y="213"/>
    <color rgb="-8928148"/>
  </pulse>
  <pulse>
    <position x="102" y="213"/>
    <color rgb="-3899692"/>
  </pulse>
  <pulse>
    <position x="102" y="214"/>
    <color rgb="-15543329"/>
  </pulse>
  <pulse>
    <position x="103" y="214"/>
    <color rgb="-1437346"/>
  </pulse>
<pulses>          

Here is the main code for the sketch. You'll notice that the main change is in the addPulse() function, which now adds a node to the XML tree on top of adding a Pulse object to the array. We also added the savePulsesToDisk() function, which saves the XML tree to an xml file in the sketch folder. This function is called when the screen is cleared and when the mouse is released. We are not calling it on mouse press and drag because that would mean the file would have to be written over and over for every frame that the mouse is down, which is excessive and will affect performance. Finally, note that the Pulse class remains unchanged.

import proxml.*;

// ----------------------------------------------------------------------
// GLOBAL CONSTANTS
// ----------------------------------------------------------------------
int MAX_PULSES = 500;

// ----------------------------------------------------------------------
// GLOBAL VARIABLES
// ----------------------------------------------------------------------
int numPulses = 0;
Pulse pulses[] = new Pulse[MAX_PULSES];
XMLInOut xmlIO;
XMLElement xmlPulses;

// ----------------------------------------------------------------------
// BUILT-IN FUNCTIONS
// ----------------------------------------------------------------------
void setup() {
  size(400, 400);
  smooth();
  noStroke();

  // create a new XML tree
  xmlIO = new XMLInOut(this);
  xmlPulses = new XMLElement("pulses");
}

void draw() {
  background(0);

  // draw all the pulses
  for (int i=0; i < numPulses; i++) {
    pulses[i].draw(); 
  }
}

void mousePressed() {
  addPulse(mouseX, mouseY); 
}

void mouseDragged() {
  addPulse(mouseX, mouseY); 
}

void mouseReleased() {
  savePulsesToDisk(); 
}

void keyPressed() {
  if (key == ' ') {
    // clear all
    numPulses = 0;
  } 
}

// ----------------------------------------------------------------------
// USER FUNCTIONS
// ----------------------------------------------------------------------
/* adds a new pulse to the display and XML lists */
void addPulse(int newX, int newY) {
  if (numPulses < MAX_PULSES) {
    pulses[numPulses] = new Pulse(
      newX, 
      newY, 
      color(random(255), random(255), random(255))
    );

    // create a new pulse XML node
    XMLElement newPulse = new XMLElement("pulse");
    // add it to the XML root
    xmlPulses.addChild(newPulse);
    // add a position XML node to the new pulse
    XMLElement pos = new XMLElement("position");
    pos.addAttribute("x", newX);
    pos.addAttribute("y", newY);
    newPulse.addChild(pos);
    // add a color XML node to the new pulse
    XMLElement col = new XMLElement("color");
    col.addAttribute("rgb", pulses[numPulses].c);
    newPulse.addChild(col);

    numPulses++;
  }
}

/* saves the pulses XML list to disk */
void savePulsesToDisk() {
  xmlIO.saveElement(xmlPulses, "pulses.xml");
}                 

If we look at the generated XML file, we see that it is being written properly and that all the Pulse values are saved. However, when the spacebar is pressed to clear the screen, the XML tree is not affected, which means that it does not always reflect what is on screen. We will fix this problem by adding the clearPulses() function which overwrites the XML tree with a new, empty one.

import proxml.*;

// ...

// ----------------------------------------------------------------------
// BUILT-IN FUNCTIONS
// ----------------------------------------------------------------------

// ...

void keyPressed() {
  if (key == ' ') {
    // clear all
    clearPulses();
  } 
}

// ----------------------------------------------------------------------
// USER FUNCTIONS
// ----------------------------------------------------------------------

// ...

/* clears all pulses from the display and XML lists */
void clearPulses() {
  numPulses = 0;

  // create a new empty pulses XML list to overwrite the previous one
  xmlPulses = new XMLElement("pulses");

  // save the new empty list to disk
  savePulsesToDisk();
}

// ...                  

loading

Now that saving works properly, we will load the pulses from the XML file when the application is started. Any modifications we make to the sketch will be reflected in the XML file, so every time we run the application, it will start at the state where it left off the last time it ran.

import proxml.*;

// ----------------------------------------------------------------------
// GLOBAL CONSTANTS
// ----------------------------------------------------------------------
int MAX_PULSES = 500;

// ----------------------------------------------------------------------
// GLOBAL VARIABLES
// ----------------------------------------------------------------------
int numPulses = 0;
Pulse pulses[] = new Pulse[MAX_PULSES];
XMLInOut xmlIO;
XMLElement xmlPulses;

// ----------------------------------------------------------------------
// BUILT-IN FUNCTIONS
// ----------------------------------------------------------------------
void setup() {
  size(400, 400);
  smooth();
  noStroke();

  // load pulses from XML file, if it exists
  xmlIO = new XMLInOut(this);
  try {
    xmlIO.loadElement("pulses.xml"); 
  } catch (Exception e) {
    // the XML file could not be found, create a new XML root
    xmlEvent(new XMLElement("pulses"));
  }
}

/* called automatically whenever an XML file is loaded */
void xmlEvent(XMLElement element) {
  xmlPulses = element;
  initPulses();
}

void draw() {
  background(0);

  // draw all the pulses
  for (int i=0; i < numPulses; i++) {
    pulses[i].draw(); 
  }
}

void mousePressed() {
  addNewPulse(mouseX, mouseY);
}

void mouseDragged() {
  addNewPulse(mouseX, mouseY);
}

void mouseReleased() {
  savePulsesToDisk(); 
}

void keyPressed() {
  if (key == ' ') {
    // clear all
    clearPulses();
  } 
}

// ----------------------------------------------------------------------
// USER FUNCTIONS
// ----------------------------------------------------------------------
/* creates all pulses saved in the XML file */
void initPulses() {
  // create temporary XML nodes
  XMLElement pulse;
  XMLElement pos;
  XMLElement col;

  // parse through all pulse nodes to create Pulse objects
  for (int i=0; i < xmlPulses.countChildren(); i++) {
    pulse = xmlPulses.getChild(i);
    pos = pulse.getChild(0);
    col = pulse.getChild(1);

    addSavedPulse(
      pos.getIntAttribute("x"), 
      pos.getIntAttribute("y"), 
      col.getIntAttribute("rgb")
    );
  }
}

/* adds a new pulse to the display and XML lists */
void addNewPulse(int newX, int newY) {
  if (numPulses < MAX_PULSES) {
    pulses[numPulses] = new Pulse(
      newX, 
      newY, 
      color(random(255), random(255), random(255))
    );

    // create a new pulse XML node
    XMLElement newPulse = new XMLElement("pulse");
    // add it to the XML root
    xmlPulses.addChild(newPulse);
    // add a position XML node to the new pulse
    XMLElement pos = new XMLElement("position");
    pos.addAttribute("x", newX);
    pos.addAttribute("y", newY);
    newPulse.addChild(pos);
    // add a color XML node to the new pulse
    XMLElement col = new XMLElement("color");
    col.addAttribute("rgb", pulses[numPulses].c);
    newPulse.addChild(col);

    numPulses++;
  }
}

/* adds a saved pulse to the display list */
void addSavedPulse(int newX, int newY, color newCol) {
  if (numPulses < MAX_PULSES) {
    pulses[numPulses] = new Pulse(newX, newY, newCol);
    numPulses++;
  }
}

/* clears all pulses from the display and XML lists */
void clearPulses() {
  numPulses = 0;

  // create a new empty pulses XML list to overwrite the previous one
  xmlPulses = new XMLElement("pulses");

  // save the new empty list to disk
  savePulsesToDisk();
}

/* saves the pulses XML list to disk */
void savePulsesToDisk() {
  xmlIO.saveElement(xmlPulses, "pulses.xml");
}

The first thing to note is the use of the try / catch block in setup(). try / catch is special notation used to handle predictable errors. In this case, if the XML file is not found in the sketch folder, create a new XML tree.

try {
  // do something error-prone
} catch (Exception e) {
  // an error occured
  // do something to deal with it
}

Another thing to note is that the addPulse() function had to be split into two:

If you run the application many times, you'll see that the pulses from previous sessions are saved and loaded properly. However, the timing of the animation is not, and all pulses start at the same time when they are re-loaded in a new instance of the application. We could fix this by adding a new attribute to the pulse node in the XML for the time at which it was created or the size it was at when the application quit, but this is a little too complicated for now. We will keep it simple, and cheat by incrementing the size of each saved pulse upon creation.

// ...

// ----------------------------------------------------------------------
// USER FUNCTIONS
// ----------------------------------------------------------------------
/* creates all pulses saved in the XML file */
void initPulses() {
  // create temporary XML nodes
  XMLElement pulse;
  XMLElement pos;
  XMLElement col;

  // parse through all pulse nodes to create Pulse objects
  for (int i=0; i < xmlPulses.countChildren(); i++) {
    pulse = xmlPulses.getChild(i);
    pos = pulse.getChild(0);
    col = pulse.getChild(1);

    addSavedPulse(
      pos.getIntAttribute("x"), 
      pos.getIntAttribute("y"), 
      col.getIntAttribute("rgb"),
      i
    );
  }
}

// ...

void addSavedPulse(int newX, int newY, color newCol, int index) {
  if (numPulses < MAX_PULSES) {
    pulses[numPulses] = new Pulse(newX, newY, newCol);
    pulses[numPulses].s += 0.1*index;
    numPulses++;
  }
}

// ...

Although this is not completely accurate, it works visually so let's keep it at that.