Developing an SVG Logger Debugging Aid


Table of Contents

Foundations
Motivation
The event callback APIs
Event Processing
Document order and Z-ordering of Graphic Elements
The Big Hidden Rectangle
Development of a Simple Logger
Design Goals
Implementation Details
Using the Logger in an Interactive SVG Application
Drag-and-Drop
Graphic Elements
DND Methods
DND Lifecycle
Activating Two Loggers for DND
Future Work

Let's review the event processing method used for determining the SVG target element. The SVG engine selects the top-most relevant graphics element under the pointer when the user event occurs. The notion of "relevant" has to do with the circumstances that an SVG graphic element can be the target for a pointer event. We'll get to the meaning of "top-most", in a moment, when we discuss z-ordering.

To be relevant the SVG graphic element must be displayed, i.e., an attribute of

display="none"

would eliminate SVG graphic elements from consideration. By default, only the visible part of an SVG graphic element can receive events. But this can be further controlled by the attribute,

pointer-events

. The following example illustrates two SVG circles without fill. The unfilled, empty inside of one circle is dead while the other responds to UI events.


<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<!DOCTYPE svg>
<svg xmlns="http://www.w3.org/2000/svg">
  <!-- Define Background Fill Pattern -->
  <defs>
    <pattern id="polkadots" patternUnits="userSpaceOnUse"
	     width="20" height="20" viewBox="-10 -10 20 20">
      <circle r="5" fill="lightgray"/>
    </pattern>
  </defs>

  <!-- Background rectangle fills the canvas with polkadots -->
  <rect x="0" y="0" width="100%" height="100%" fill="url(#polkadots)"/>

  <!-- Two Circles with different pointer-events -->
  <circle cx="25%" cy="50%" r="20%" fill="none" stroke-width="5%" stroke="black"
	  pointer-events="visiblePainted"
  	  onclick="alert('I was Clicked');"/>

  <circle cx="75%" cy="50%" r="20%" fill="none" stroke-width="5%" stroke="black"
	  pointer-events="all"
	  onclick="alert('I was Clicked');"/>
     <!-- visibility="hidden" -->

  <text x="25%" y="75%">pointer-events="visiblePainted" (default)</text>
  <text x="75%" y="75%">pointer-events="all"</text>
</svg>



The next figure is the same as the previous except that we use

visibility="hidden"

attribute from the commented out section. Notice that the now hidden circle still responds to a mouse click.


Hidden elements are still present and can still respond to UI events.

According to the SVG 1.1 Spec,

Elements in an SVG document fragment have an implicit drawing order, with the first elements in the SVG document fragment getting "painted" first. Subsequent elements are painted on top of previously painted elements.

This is also known as "z-ordering": elements later in document order appear on top.

When SVG graphic elements overlap, the SVG rendering mechanism specifies not only the painting but also the ordering of event targets. It doesn't matter if the top-most is translucent or even hidden. If the top-most element is deemed "relevant" it alone receives the UI event while excluding elements lower in z-ordering (i.e., earlier in document order)from receiving events. The following two examples illustrate.



This final example on z-ordering shows what happens when adding to the blue rect the attribute,

pointer-events="none"

The blue rect is painted, but from an event processing point-of-view it's gone.


The logger is implemented in ECMAScript class whose source is in one file, logger.js that can be linked into an SVG application.

The logger's core data structure is implemented with parallel Javascript arrays. These act like a table and record for each event, a sequence number and logging message. The arrays store a fixed number of records and operate FIFO, discarding older log entries.

The logger's core data is painted onto a predetermined group of SVG text elements. Since the group is an SVG element with the specified id, this is easy to do by calling DOM mutation setters on each of the group's children text elements. By using id's independent groups represent parallel logger streams.


logger = function(logId) {

    this.initTxtElmts=function(logId) {
	var textGroup = svgDocument.getElementById(logId);
	for each (n in textGroup.childNodes) {
	    // checks that we are visiting svg:text node-- skip past whitespace nodes
	    if (n.nodeName == "text") {
		txtElmts.push(n.firstChild);
	    }
	}
    }

    this.paintStats = function() {
	var i=0;
	for each (line in lines) {
	    txtElmts[i].data = line;
	    i++;
	}
    }

    this.logSvgMouseEvent = function(msg, evt) {
	var x = evt.clientX;
	var y = evt.clientY;
	this.log(msg + "(" + x + "," + y + ")");
    }
    
    this.log = function(msg) {
	lines.push(sequenceNumber + ":" +  msg);
	if (lines.length > txtElmts.length) {
	    lines.shift();
	}
	this.paintStats();
	sequenceNumber++;
    }

    this.addMouseListener = function(loggerName, id, obj, eventCallback) {
	var value = obj.getAttributeNS(null, eventCallback);
	obj.setAttributeNS(null, eventCallback, loggerName+".logSvgMouseEvent('"+id+":"+eventCallback+"', evt);"+value);
	value = obj.getAttributeNS(null, eventCallback);
    }

    this.decorateMouseListeners = function(loggerName, id) {
        var obj = svgDocument.getElementById(id);
	this.addMouseListener(loggerName, id, obj, "onclick");
	this.addMouseListener(loggerName, id, obj, "onmousedown");
	this.addMouseListener(loggerName, id, obj, "onmouseup");
	this.addMouseListener(loggerName, id, obj, "onmouseover");
	this.addMouseListener(loggerName, id, obj, "onmouseout");
	this.addMouseListener(loggerName, id, obj, "onmousemove");
    }

    var sequenceNumber = 0;
    var txtElmts = [];
    var lines = [];
    this.initTxtElmts(logId);
}


Now let's turn our attention to a involved example that will benefit from logging.

The application oscillates between two states: dragging and not-dragging. It begins in the not-dragging state. The big hidden rect ignores events, while each of the three circles waits for a

mousedown()

event.

When a UI event hits a circle, that circle's event callback, the method,

capture()

, is called.

capture()

does two things: it memoizes the target SVG element and turns on pointer-events for the big hidden rect. The effects of

capture()

are to put the application into the dragging state.

Note, that because the big hidden rect is last in document order it's on top. Circles will no longer get any events, since only the rect receives events and the rect obscures everything. The big hidden rect receives all events in the dragging state.

While dragging, mouse movements are handled by mutating the x, y coordinates of the captured circle. Dragging continues until either the mouse is released or it's dragged off of the SVG canvas. Either of these events calls

release()

which resets the big hidden rect's pointer-events to "none" and reverts to the non-dragging state.

The next figure shows all the ECMAScript code.


init = function(evt) {
  if ( window.svgDocument == null )
    svgDocument = evt.target.ownerDocument;
  dndInstance = new dnd();
}

dnd = function() {
  var x;
  var y;
  var offsetX;
  var offsetY;
  var svgObject;

  var moveSvgObject = function() {
    svgObject.setAttributeNS(null, "cx", x+offsetX);
    svgObject.setAttributeNS(null, "cy", y+offsetY);
  }

  this.capture = function(evt) {
    svgObject = evt.target;
    var bigrect = svgDocument.getElementById("bigrect");
    x = evt.clientX;
    y = evt.clientY;
    offsetX = svgObject.getAttributeNS(null, "cx")-x;
    offsetY = svgObject.getAttributeNS(null, "cy")-y;
    bigrect.setAttributeNS(null, "pointer-events", "all");
    moveSvgObject();
  }

  this.release = function(evt) {
    svgObject=null;
    var bigrect = svgDocument.getElementById("bigrect");
    bigrect.setAttributeNS(null, "pointer-events", "none");
  }

  this.drag = function(evt) {
    x = evt.clientX;
    y = evt.clientY;
    moveSvgObject();
  }
}


The ECMAScript logger class presented here was derived from tutorial material aimed at teaching SVG to intermediate SVG and ECMAScript developers. There are several areas where the logger could be extended to yeild a developer framework for SVG and ECMAScript logging.

  • Integration with JavaScript cross-platform libraries.

  • Generating SVG text templates from the logger class would obviate the developer's cutting and pasting.

  • Introduce a subclass or callback function to abstract logging semantics. logger.js presented here, is hardwired to display x,y coordinate events.

  • Robust handling of errors/warnings.

  • More interactive UI controls for logger presentation UI, e.g., button to turn on and off display of logger, scrollbar, etc.

  • Add query and filtering capabitilies. Add event severities and types.