SVG + Java servlets for Web Maps

Server side SVG DOM parsing

Randy George
Geotechnologies, Inc
17715 Canterbury Dr
Monument, CO 80132

e-mail: rkgeorge@cadmaps.com
phone: 719-487-1031
webpage: http://www.web-maps.com
webpage: http://www.academy-computing.com/svgweb

Keywords: SVG Web Maps; Java servlets; DOM Parsing

Abstract

SVG provides a foundation for publishing vector maps over the Internet. SVG web maps can have very flexible database linkage and a full range of customization, while still remaining accessible to generic browsers over the Internet. The purpose of this case study is to look at the details of a typical web mapping application using SVG as the presentation of both map data and database query results. Along the way we will have a chance to look at several approaches to manipulating SVG templates on the server.

Introduction

A number technologies will be used in this example including:

This example will first look at building a typical SVG map with a two level hierarchy utilizing the open source data resources of the U.S. Census Bureau. Next we will add some JavaScript affects for rollover labeling and then move to the server side. Using Java servlets we will first connect to a database and build a simple HTML table query response. Next we will extend our database query with an svg bar chart response. The bar chart will be built from a template in four different ways, comparing strengths and weaknesses of each approach.

Some map basics

Computer mapping has evolved around the convergence of three technologies: vector drawing, raster images, and databases. SVG directly addresses vector and raster capabilities, but until future standards like XML Query are widely implemented, additional tools are needed to provide database access. There are several approaches to database access all requiring some type of server technology for database linkage. CGI, ASP, PHP can all be used in this capacity. However, this case study will focus on Java solutions, which are portable to a wide spectrum of servers, as well as being highly extensible with an apparently unending stream of APIs.

There are some issues to keep in mind when dealing with maps. First the amount of map data can be overwhelming. Even though vectors describe lines much more efficiently than raster images, terrestrial features will easily outpace existing bandwidth. Cartographers have developed several approaches to this problem generally implementing some form of "divide and conquer" to split a map project into many smaller files. A regular tile grid or a hierarchical approach, are two common methods of reducing the world to manageable chunks. This case study uses the hierarchical approach presenting map data in successive levels of detail, as illustrated in Figure 1.

Figure 1 - Map Hierarchy

A second issue that will necessarily come up in almost any web map application is the coordinate system. Cartographers have developed innumerable ways of projecting the surface of the earth's oblate spheroid onto a 2D plane. Choosing a coordinate system that best represents the required extent of a map application can be complex. With map hierarchies that are potentially world wide in extent the easiest coordinate system is simply longitude and latitude, which is used in this example. By convention western hemisphere longitudes are negative as are southern hemisphere latitudes. Figure 2 shows a simple null projected, Longitude, Latitude, world map. The mathematics of projections is a fascinating field with a long history.

Additional information about coordinate system projections can be found at this link: http://mathworld.wolfram.com/topics/MapProjections.html

Figure 2 - Longitude, Latitude projection

Obtaining map data

Map data is available by the terabyte. Much of the existing map data is copyrighted material, which requires some form of release or compensation for use. Fortunately the U.S. government has taken a liberal perspective on data compiled with tax dollars, releasing huge collections of map data to the public without copyright restriction, and at minimal cost. We will take advantage of this for our simple case study utilizing map data as well as demographic data from the U.S. Census Bureau. We will be looking at a county map of Massachusetts and then drilling down to a more detailed census tract map of a single county, Plymouth, MA. Census tracts are just one of several sets of boundary polygons for which the U.S. Census aggregates demographic data. This data, as well as data for any state in the U.S., is freely available from the Census Bureau website, http://www.census.gov.

Although this data is freely available it will still need to be converted into the SVG format. At present map data can be found in dozens of formats with generally widely published specifications. The standard Census Bureau format is TIGER, which stands for Topologically Integrated Geographic Encoded Reference. However, there are sources on their website for other well known data formats. The data for this example has been converted to SVG format and made available with this paper.

This project used .SHP format data files from the Census website and a SHPtoSVG translator. See the official w3c SVG site,
http://www.w3.org/Graphics/SVG/Overview.htm8, or the Adobe site, http://www.adobe.com/svg/tools/3party.html, for links to numerous conversion tools for legacy data formats.

Web map project overview

For many web mapping applications that use SVG, there is a three step progression. In the first phase the required map data is converted to SVG. This will require an examination of the map structure, how features will be grouped and how events will be tied to particular features. Recall that SVG specifies a "Painter's model" of rendering, with features rendered in the order they occur. This first phase of a web map project may require consideration of feature order in the final SVG file.

As the project progresses, the natural second phase is the development of custom ECMAScript functions, which are connected to event listeners in the SVG file. ECMAScript affords a great deal of flexibility when developing the user interaction with the map. Some common functions we will include in our web map will be rollover affects, color change and cursor following labels, as well as database linkage.

The next distinct phase of development is on the server side. In this phase of our project, Java servlets are developed to access a behind the scenes database and produce different views of the Census population data associated with the web map. The JDBC query results will be displayed with a variety of technologies starting with the simplest HTML table output and then moving to an SVG bar chart output. The bar chart will utilize a simple SVG template processed with several alternative XML technologies including:

Our example project will deal with a single smaller state and a single county. This is a representative subset of a larger project, which illustrates some common SVG mapping approaches without getting overwhelmed by the data. A complete project would likely have a three level hierarchy starting at the U.S., then linking to individual states, and finally to individual counties within a state. The resulting project would have a single US key map, with 50 state SVG files on level two, and several thousand county SVG files on level three. Also keep in mind that saving our files as compressed .svgz will save space on the server and more importantly bandwidth for slower Internet connections.

Massachusetts Key Map - level one

For our simplified project the first map to consider is the root map or key map for the project, shown in Figure 3. This will be the top level in our simple two step hierarchy. In this project we have converted the Census TIGER county map for Massachusetts into a set of SVG closed paths as shown in Figure 3.

Figure 3 - Massachusetts county key map

The corresponding SVG for the Massachusetts key map is shown in Listing 1

Listing 1 - Massachusetts SVG

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" 
              "http://www.w3.org/TR/SVG/DTD/svg10.dtd" [
<!ENTITY MA  "stroke:blue;stroke-width:0.0001;fill:blue;fill-opacity:0.15">
<!ENTITY Hilite  "stroke:red;stroke-width:0.0001;fill:red;fill-opacity:0.15">
<!ENTITY BOUNDARY   "stroke:magenta;stroke-width:0.0001;fill:none;">
<!ENTITY TEXT   "stroke:black;stroke-width:0.0001;fill:blue;font-size:0.15;">
]>
<svg width="600" height="276.3"  preserveAspectRatio="xMinYMin" 
  viewBox="-73.508142 -42.886589 3.579881 1.648625" onload="on_load(evt)" 
  xmlns="http://www.w3.org/2000/svg" 
  xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Massachusetts</title> 
<text x="-73.0" y="-41.25" style="&TEXT;">Massachusetts, U.S.A.</text>

<g id="canvas" onmousemove="DoOnMouseMove(evt)">
<polyline style="&BOUNDARY;"
  points="-73.508142,-42.886589 -69.928261,-42.886589 -69.928261,-41.237964
          -73.508142,-41.237964 -73.508142,-42.886589 "/>

  <g id="Massachusetts">
<path id="Essex" style="&MA;" onmouseover="over(evt)" onmouseout="out(evt)"
 d="M-70.914899,-42.886589 L-70.914886,-42.886564 L-70.902768,-42.88653  
L-70.886136,-42.88261   L-70.8837340828846,-42.88122235515428 L-70.848625,
-42.8609370.82420578217194,-42.87108033804639 L-70.821769,-42.87188 
L-70.817296,-42.87229 L-70.81729906081927,-42.87213747269107 
               .
               .

Here we have included the style properties in the DOCTYPE as an internal DTD subset rather than referencing an external CSS file:

<!ENTITY MA  "stroke:blue;stroke-width:0.0001;fill:blue;fill-opacity:0.15">

This makes the file easier to maintain in this small project. In a large project with repetitive map sets or tiles it would be beneficial to keep the stylesheet separate and reference it with each similar SVG map.

The svg element attributes include a width and height, which are scaled to match the ratio of map extents. This will allow us to later calculate screen position for ECMAScript labels. Our Massachusetts key map has been left in the native TIGER coordinate system using decimal longitude for x values and decimal latitude for y values. The horizontal extent of 3.579881 degrees longitude corresponds to the pixel width of 600 while the vertical dimension 1.648625 degrees latitude determines the ratio for the height of 276.3 pixels. The preserveAspectRatio is explicitly set to the upper left corner, xMinYMin , again to allow coordinate calculations for ECMAScript labels.

<svg width="600" height="276.3"  preserveAspectRatio="xMinYMin" 
  viewBox=" -73.508142 -42.886589 3.579881 1.648625" onload="on_load(evt)"
  xmlns=http://www.w3.org/2000/svg
  xmlns:xlink="http://www.w3.org/1999/xlink">

One important detail that affects map conversion is the origin of SVG coordinates. The SVG specification follows programmer conventions, using the upper left corner as the origin. This means that positive x values will act as expected, increasing to the right. However, positive y values will increase toward the bottom of the screen. The effect is to invert map coordinates around the x axis. At first it might seem appropriate to handle correction of this map inversion inside the svg element by applying a simple transform= "scale(1,-1)" This type of transform will correct the map orientation, but unfortunately it will also invert any text in the map. In order to avoid lengthy transform corrections on every text feature it is easiest to take the external approach and invert all the y values on translation. In our Massachusetts example the longitude values are negative because Massachusetts is in the western hemisphere, but the latitude values are negative because we wish to correct the map orientation in SVG coordinates.

The purpose of the viewBox attribute is to map our world coordinates onto screen coordinates.

viewBox="-73.508142 -42.886589 3.579881 1.648625"

Finally we add an event listener to the svg element, onload="on_load(evt)". This is standard practice for calling an ECMAScript setup function at the initial svg load. We will look at this in more detail when examining the ECMAScript code. The title and text elements are standard svg format. These are followed by a g grouping with an associated event listener for mouse movements:

<g id="canvas" onmousemove="DoOnMouseMove(evt)">

Here we are adding a g element to hold the entire set of map features and collect mouse movement events that will be used in the ECMAScript. In our Massachusetts map we are not interested in collecting mouse move events anywhere outside the defined county paths. However, in some cases it is necessary to collect these events even outside map features, an example might be a floating toolbar. It would then be necessary to use the boundary polyline as the event boundary. However, by default, events will only be triggered on filled surfaces which means the polyline style would need to be changed from fill:none to some color such as fill:white. If the boundary is offensive its visibility can be controlled by setting its style attribute property to opacity:0 or visibility:hidden.

Within the canvas we add another g element for all of Massachusetts and then start adding county path elements. Each county has its own unique id as well as mouse event listeners. The absolute Moveto, M, and absolute Lineto, L, prefixes to the path element's d attribute have been explicitly included by the translation. The TIGER translator also furnished a closing coordinate on each path so the Z closure prefix is not needed. Note that events will only be associated with the stroke or bounding line of our closed paths unless we have set a fill to something other than none. In this case we set the style fill with a color and a fill-opacity that would allow any underlying features to remain visible. If a color is distracting, the fill-opacity may be set to 0, but we still need some fill to grab events as our cursor moves across the inside of our path.

There is one added twist with Suffolk, Norfolk, and Duke counties. Each of these counties is disjoint, i.e. made up of more than one polygon. These could have been handled within a single path element's d attribute using Moveto (M) prefixes for jumping to additional external boundaries, but in this case the translator has grouped the separate polygonal features into a <g> feature and attached the id attribute value and event listeners to this enclosing group.


  <path d="M-71.2611,-42.326796 L-71.259348,-42 . . . L-71.123087,-42.35155"/>
  <path d="M-70.824660,-42.265934 . . . L-70.82466085670549,-42.265934676602"/>
</g>
]]>

We can now use this group for events associated with both of the distinct polygons making up Norfolk. Referring to figure 3, you can see that Plymouth has the additional distinction of being the only active link in our key map.


  <path id="Plymouth" style="&Hilite;" onmouseover="over(evt)" 
   onmouseout="out(evt)" d="M-70.90630201246992,-42.27163620517878
                   .
                   .      
  L-70.90630201246992,-42.27163620517878"/>
</a>
]]>

Here we have made a direct hyperlink to the next level of the map hierarchy. By using a target="_blank", a new window creation is forced. At the next level of the map hierarchy we will see another approach to linking, but first let's look at the ECMAScript used to make this key map interact with the user.

Following the example of the stylesheet we will include the ECMAScript in our Massachusetts SVG to make it more compact. Again in more complex map projects it would be better to reference the ECMAScript externally from each of the presumably numerous map tiles. We will see this second approach in our next map of Plymouth county. However for Massachusetts the ECMAScript is included as CDATA shown in Listing 2.

Listing 2 - Massachusetts ECMAScript
<script language="JavaScript"><![CDATA[
  var label = "";
  var style;
  var offset = 0.05;

  // must be calculated from viewbox to w x h
  var x0 = 0;
  var y0 = 0;
  var vboxW = 0;
  var vboxH = 0;
  var svgW = 0;
  var svgH = 0;

  function on_load(e){
    svgdoc = evt.target.getOwnerDocument();
    svgroot = svgdoc.getDocumentElement();

    //initialize cursor parameters
    var vbox = (svgroot.getAttribute("viewBox")).split(' ');
    x0 = parseFloat(vbox[0]);
    y0 = parseFloat(vbox[1]);
    vboxW = parseFloat(vbox[2]);
    vboxH = parseFloat(vbox[3]);
    svgW = parseFloat(svgroot.getAttribute("width"));
    svgH = parseFloat(svgroot.getAttribute("height"));

    // initialize label text
    var data = svgdoc.createTextNode("");
    var text = svgdoc.createElement("text");
    text.setAttribute("transform","translate("+ x0 + "," + y0 + ")");
    text.setAttribute("style", 
"stroke:black;stroke-width:0.001;fill:black;font-size:0.1;font-family:arial;");
    text.setAttribute("id", "label");
    text.appendChild(data);
    svgroot.appendChild(text);
  }

  function DoOnMouseMove(e) {
    var X = x0 + (e.clientX - svgroot.currentTranslate.x)*
                                           (vboxW/(svgW*svgroot.currentScale));
    var Y = y0 - offset + (e.clientY - svgroot.currentTranslate.y)*
                                           (vboxH/(svgH*svgroot.currentScale));
    label =  svgdoc.getElementById("label");
    label.setAttribute("transform","translate("+ X + "," + Y + ")");
  }

  // mouse_over
  function over(e) {
    target = e.currentTarget;
    var id = target.id;
    if (id!="") {
      //rollover label on
      label =  svgdoc.getElementById("label");
      label.firstChild.setData(id);
      var svgstyle = e.currentTarget.style;
      style = svgstyle.getPropertyValue('fill');
      svgstyle.setProperty ('fill','yellow');
    }
  }

  //mouse_out
  function out(e) {
    //rollover label off
    var label =  svgdoc.getElementById("label");
    label.firstChild.setData(" ");
    var svgstyle = e.currentTarget.style;
    svgstyle.setProperty('fill',style);      
  }
]]></script>

The on_load function is called when the document is loaded. It is used to initialize cursor parameters and create an additional label text element:

  function on_load(e){
    svgdoc = evt.target.getOwnerDocument();
    svgroot = svgdoc.getDocumentElement();

After getting the doc and the root we can start initializing the origin and other parameters, which are used to track the cursor. These parameters are obtained by splitting the viewBox attribute. Note that we are assuming a space separator, which may not always be the case:

    //initialize cursor parameters
    var vbox = (svgroot.getAttribute("viewBox")).split(' ');
    x0 = parseFloat(vbox[0]);
    y0 = parseFloat(vbox[1]);
    vboxW = parseFloat(vbox[2]);
    vboxH = parseFloat(vbox[3]);
    svgW = parseFloat(svgroot.getAttribute("width"));
    svgH = parseFloat(svgroot.getAttribute("height"));

A blank text node is initialized at the origin. We will position the text with a transform attribute instead of x and y, making position changes a bit easier later:

    // initialize label text
    var data = svgdoc.createTextNode("");
    var text = svgdoc.createElement("text");
    text.setAttribute("transform","translate("+ x0 + "," + y0 + ")");
    text.setAttribute("style", 
"stroke:black;stroke-width:0.001;fill:black;font-size:0.1;font-family:arial;");
    text.setAttribute("id", "label");
    text.appendChild(data);
    svgroot.appendChild(text);

The mouse movement event was associated with the canvas group in our SVG map, which includes all of the features in the map. Since SVG allows zooming and panning it is necessary to keep track of the currentScale and currentTranslate transforms to properly calculate positions in the map. This calculation will only work as long as the svg element is explicitly positioned at the origin. Future SVG Viewing tools may provide better methods for obtaining X,Y positions even when there is no prior knowledge of svg width and height, as in the case of percentage dimensions, width="100%" height="100%". The offset value will position the text slightly away from the cursor. If this offset is not provided, whenever the cursor is over the text feature, it will be blocked from the mouseover event resulting in a continuously blinking text label. Alternatively this could be handled by adding a style property pointer-event:none to the label text element:

  function DoOnMouseMove(e) {
    var X = x0 + (e.clientX - svgroot.currentTranslate.x)*
                                           (vboxW/(svgW*svgroot.currentScale));
    var Y = y0 - offset + (e.clientY - svgroot.currentTranslate.y)*
                                           (vboxH/(svgH*svgroot.currentScale));

Finally we can look up our label by its id and change its position by setting a new translation transform. This moves our label text to follow the cursor position:

    label =  svgdoc.getElementById("label");
    label.setAttribute("transform","translate("+ X + "," + Y + ")");

The mouse over event is triggered the first time our cursor enters an associated path feature. We use this event to add text to our label.

  // mouse_over
  function over(e) {

First we find the id attribute of the feature triggering the event.

  // mouse_over
  function over(e) {
    target = e.currentTarget;
    var id = target.id;

If there is an id we use it as the new label text.

    if (id!="") {
      //rollover label on
      label =  svgdoc.getElementById("label");
      label.firstChild.setData(id);

In addition to setting the label text we also change the fill color as a rollover hint. We first save the current fill color in a variable to use later when we restore the original color:

      var svgstyle = e.currentTarget.style;
      style = svgstyle.getPropertyValue('fill');
      svgstyle.setProperty ('fill','yellow');

The mouseout event occurs whenever our cursor leaves the associated path feature.

  //mouse_out
  function out(e) {
    //rollover label off

First we turn off the label by setting the text to blank.

    var label =  svgdoc.getElementById("label");
    label.firstChild.setData(" ");

Next we change the fill color back to its original color.

    var svgstyle = e.currentTarget.style;
    svgstyle.setProperty('fill',style);

This completes the top level of our web map. We have a county map of Massachusetts setup with cursor following county labels and a single href link to the more detailed second level, Plymouth County.

Plymouth County Map - level two

The next level of the hierarchy moves to Plymouth County and the Census tract polygons shown in Figure 4.

Figure 4 - Plymouth County Census tracts

The associated SVG for Plymouth County Census tracts is shown in Listing 3.

Listing 3 - Plymouth Census tracts SVG

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" 
              "http://www.w3.org/TR/SVG/DTD/svg10.dtd" [
<!ENTITY Plymouth  
             "stroke:purple;stroke-width:0.0001;fill:purple;fill-opacity:0.25">
<!ENTITY BOUNDARY  "stroke:magenta;stroke-width:0.0001;fill:none;">
<!ENTITY TEXT  "stroke:black;stroke-width:0.0001;fill:blue;font-size:0.035;">
]>
<svg width="700" height="700"  preserveAspectRatio="xMinYMin" 
  viewBox="-71.25 -42.35 1 1" onload="on_load(evt)"
  xmlns="http://www.w3.org/2000/svg" 
  xmlns:xlink="http://www.w3.org/1999/xlink">
<script type="text/javascript" xlink:href="../js/map.js"/>
<title>Plymouth Tracts</title> 
<text transform="translate(-71.0,-41.58)" style="&TEXT;">
  Plymouth County, Massachusetts
</text>
<g id="canvas" onmousemove="DoOnMouseMove(evt)">
<g id="Plymouth">
<path id="0235001.01" style="&Plymouth;" 
onmouseover="over(evt)" onmouseout="out(evt)" onclick="click(evt)" 
d="M-70.89612186423385,-42.28728004812354 
L-70.89603267844453,-42.28861783885205 L-70.89577810704088,-42.29243642100662 
L-70.89621068473353,-42.29353753284876 L-70.89712319627394,-42.29586030001939 
L-70.91558769004943,. . . .

L-70.89612186423385,-42.28728004812354"/>
<path id="0235001.02" style="&Plymouth;" 
onmouseover="over(evt)" onmouseout="out(evt)" onclick="click(evt)" 
d="M-70.876004,-42.29277 . . .
.
.
<path id="0235601" style="&Plymouth;" 
onmouseover="over(evt)" onmouseout="out(evt)" onclick="click(evt)" 
d="M-70.803848,-41.705399 L-70.808424,-41.69148 . . . L-70.803848,-41.705399"/>
</g><!-- Plymouth -->
</g><!-- canvas -->
</svg>

The structure of Plymouth.svg is virtually identical to the top level Massachusetts map. Adjustments have been made to the svg width, height, and viewBox attributes in order to accommodate the difference in size and location. There is also an additional event listener attached to each polygon, onclick="click(evt)". The only other difference is moving the ECMAScript to an external file with this line:

<script type="text/ecmascript" xlink:href="../js/map.js"/>

The map.js ECMAScript is also very similar to the internal Massachusetts ECMAScript with the addition of one more function that is checking for events from the left mouse button. This additional click function is shown in Listing 4:

Listing 4 - click function in map.js

  //mouse click
  function click(e) {
    if (e.getButton()==0) {
      if (dataWin!=null) dataWin.close();
      if (e.getShiftKey()==1) {
      dataWin=window.open("/servlet/dataQuery1?table="+ 
        e.currentTarget.parentNode.id + "&id=" + e.currentTarget.id + 
        "&chart=false", "dataWindow", 
         "width=400, height=300, left=500, top=0, scrollbars=yes, toolbar=yes,location=yes, menubar=yes, resizable=yes, status=yes");
      }
      else {
      dataWin=window.open("/servlet/dataQuery1?table="+ 
        e.currentTarget.parentNode.id + "&id=" + e.currentTarget.id + 
        "&chart=true", "dataWindow", 
        "width=400, height=300, left=500, top=0,scrollbars=yes, toolbar=yes,location=yes, menubar=yes, resizable=yes, status=yes");
      }
      dataWin.focus();
    }
  }

This new function will check mouse click events for the left mouse button, e.getButton()==0. If the query window is already opened it is first closed in preparation for a new window and then a call is made to a servlet. The servlet in this case has three URL encoded parameters i.e. parameters added to the URL address of the servlet.

The e.getShiftKey()==1 indicates that a 'Shift' key is being pressed. Holding the 'Shift' key while clicking the left mouse button triggers a window.open to a servlet with a chart parameter set to false. A single left click will call the same servlet with chart set to true.

We should note here that due to internationalization issues, the key events currently implemented in the Adobe Viewer will be changed in DOM Level 3 and SVG 1.1 to text events. Consequently the use of key events in our ECMAScript will need to be revised at some point.

This completes the SVG and ECMAScript portion of the web map application. It's now time to switch over to the server side. We will first look at a servlet that returns a simple HTML query result and then move to four different approaches to producing a bar chart from our database.

Server side processing

There are many options for server side processing, but we will explore several approaches using basic Java servlets. We will first illustrate a simple HTML query result using JDBC in our servlet to connect to a MySQL database. Then we will enhance this with an alternative SVG bar chart display. Since there are several methods of handling XML we will illustrate four common ways of customizing the SVG chart template with the results of our database query.

Servlet 1 - Database access - Query database and return HTML table

First we need to set up a database on the server. Any SQL database will do, but our project uses MySQL as a freely available example (http://www.mysql.org/). Again this population data was taken from the U.S. Census website. Census population figures are available for several levels of detail: city, county, tract, block group, and block in progressively finer grain. This case study has chosen to use the tract level polygons. There are numerous additional population figures available including age, income, race, gender etc. In this example we pulled a limited subset of age and gender. There are more than a hundred population categories to choose from, but we will arbitrarily limit the example to just twelve categories. First we create a simple flat table in our database on the server with 2 Integer fields per record plus a primary key id field. Listing 5 shows the SQL CREATE TABLE command to set up this table.

Listing 5 - SQL CREATE TABLE statement

CREATE TABLE Plymouth (
total int(8),
totalMale int(8),
totalFemale int(8),
under18 int(8),
under18Male int(8),
under18Female int(8),
adult int(8),
adultMale int(8),
adultFemale int(8),
over65 int(8),
over65Male int(8),
over65Female int(8),
ID varchar(12) DEFAULT '' NOT NULL,
PRIMARY KEY (ID) );
We then populate the records one per tract polygon and use a combination of county FIPS code, "023", and census tract number as our unique record id. FIPS stands for Federal Index Processing System and identifies unique numeric codes for common US governmental entities. If this application is expanded to the entire U.S. we would have to add the State FIPS code, "25" for Massachusetts, in order to guarantee a unique id. For example: '250235001.01'
INSERT INTO Plymouth VALUES 
('4259','2080','2179','1022','537','485','2762','1324','1438','475','219',
'256','0235001.01');
.
.
.
INSERT INTO Plymouth VALUES 
('5123','2461','2662','1285','667','618','2931','1402','1529','907','392',
'515','0235611');

Once a database table is setup we are ready to start looking at Java servlet options. The first servlet connects to the new database table, and using the SVG path id, looks up the corresponding table record. The dataQuery1 servlet, shown in Listing 6, is normal servlet code setting up the JDBC connection and using the doGet method for catching parameters and responding to the client. Here we are returning ordinary HTML to the client browser, so we need to set the CONTENT TYPE=text/html.

Listing 6

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.sql.*;

public class dataQuery1 extends HttpServlet implements SingleThreadModel {
  private static final String CONTENT_TYPE = "text/html";
  private Connection con = null;

  /**Initialize global variables*/
  public void init(ServletConfig config) throws ServletException {
    super.init(config);
    try {
      // Load the MYSQL MM driver
      Class.forName("org.gjt.mm.mysql.Driver").newInstance();

      //establish connection to database
      con = DriverManager.getConnection("jdbc:mysql://localhost/sams","user", "password");
    }
    catch (ClassNotFoundException e) {
      throw new UnavailableException("Couldn't load database driver");
    }
    catch (SQLException e) {
      throw new UnavailableException("Couldn't get db connection");
    }
    catch (IllegalAccessException e) {
      throw new UnavailableException("Illegal access");
    }
    catch (Exception e) {
      throw new UnavailableException("General Acess error");
    }
  }

  /**Process the HTTP Get request*/
  public void doGet(HttpServletRequest request, HttpServletResponse response)
                                         throws ServletException, IOException {
    response.setContentType(CONTENT_TYPE);
    PrintWriter out = response.getWriter();


    //initialize variables
    String table = "";
    String id = "";
    String sql = "";
    boolean chart = false;

    String [] values = new String[10];

    //collect parameters
    Enumeration enum  = request.getParameterNames();
    while (enum.hasMoreElements()) {
      String name = (String) enum.nextElement();
      if (name.equals("table")) {
        values = request.getParameterValues(name);
        table = values[0];
      }
      else if (name.equals("id")){
        values = request.getParameterValues(name);
        id =  values[0];
      }
      else if (name.equals("chart")){
        values = request.getParameterValues(name);
        chart =  values[0].equals("true");
      }
    }
    // setup the HtmlTable
    HtmlTable1 outTable = new HtmlTable1(con,table,id);
    //return HtmlTable to the client
    outTable.write(response.getWriter());
 }

  /**Process the HTTP Post request*/
  public void doPost(HttpServletRequest request, HttpServletResponse response) 
                                         throws ServletException, IOException {
    doGet(request,response);
  }

  /**Clean up resources*/
  public void destroy() {
    // Clean up.
    try {
      if (con != null) con.close();
    }
    catch (SQLException ignored) { };
  }

The more interesting work goes on in the HtmlTable class. Here the JDBC connection, created in the servlet initialization, is used with the table name and id string, to query our Plymouth table. Remember the table name and id are passed from our SVG map as parameters in the ECMAScript click function:

  import java.io.*;
  import java.util.*;
  import java.sql.*;

public class HtmlTable {
  private Connection con = null;
  private String table;


  private String id;


  public HtmlTable(Connection con, String table, String id) {
    this.con = con;
    this.table = table;
    this.id = id;
  }

The HtmlTable.write method uses the servlet response PrintWriter to send our response back to the client as text/html:
  public void write(PrintWriter out) {
    String sql = "";

First we send the HTML header information, which will allow us to write any errors that might occur as legible HTML code:
    out.println("<html>");
    out.println("<head>");
    out.println("<title>Data Query</title>");
    out.println("</head>");
    out.println(" <body>");
    if ((table!=null) && (id!=null)) {
      out.println("<h3>Query - " + table + "</h3>");
      try {

Next we attempt to setup and execute the query using the table name and id:
        Statement stmt = con.createStatement();
        sql = "SELECT * FROM " + table + " WHERE id=\"" + id + "\";";
        if (stmt.execute(sql)) {

If the sql statement executes and a result set is returned, we can use the first, and only, record to populate our HTML Table:
          // There is a ResultSet
          ResultSet rs = stmt.getResultSet();
          ResultSetMetaData rsmd = rs.getMetaData();

The Java JDBC ResultSetMetaData is handy for looping through all the fields and then providing a field label and field type. In our database we have only used VarChar and Int fields but the JDBC API has a full complement of type accessors.
          out.println("<table>");
          while (rs.next()) {
            for (int i = 1; i <= rsmd.getColumnCount(); i++) {
     out.println("<tr><td width=\"26%\">" + rsmd.getColumnName(i) + "</td>");
     out.print("<td width=\"74%\"> <input type=\"text\" size=\"25\" value=\"");
              switch (rsmd.getColumnType(i)) {
                case Types.INTEGER: out.print(rs.getInt(i)); break;
                case Types.VARCHAR: out.print(rs.getString(i)); break;
                case Types.TIMESTAMP: out.print(rs.getTimestamp(i)); break;
                case Types.CHAR: out.print(rs.getString(i)); break;
                case Types.DATE: out.print(rs.getDate(i)); break;
                case Types.FLOAT: out.print(rs.getFloat(i)); break;
              }
              out.println("\"></td></tr>");
            }
          }
          rs.close();
          out.println("</table>");
        } else {

If errors occur we can still print a meaningful message in html:
        out.println("<p>" + table + " has no record for Id =" + id + "</p>");
        }
        stmt.close();
      } catch (SQLException e) {
        out.println("<h1>ERROR:MySQL</h1>" + e.getMessage());
      }
    } else {
      out.println("<p>Error: the Table or Id Parameter is null</p>");
    }
    out.println(" </body>");
    out.println(" </html>");
  }
}

However if everything works correctly the result of a query will appear in its own HTML window as shown in Figure 5.

Figure 5 - HTML Table result of query

Servlet 2 - Database query2 - return SVG Bar Chart or HTML table

Now we want to extend our database query servlet to return either an HTML table or a bar chart. First we modify the dataQuery servlet slightly to branch on the results of the "chart" parameter sent from the ECMAScript click function. Here we set the ContentType according to the type of response. An "image/svg+xml" MIME type is required for svg output, while a "text/html" type is required for the HtmlTable.

           .
           .
    if (chart) {
      response.setContentType("image/svg+xml");
      SvgChart2 outChart = new SvgChart2(con,table,id);
      outChart.write(response.getWriter());
    } else {
      response.setContentType("text/html");
      HtmlTable2 outTable = new HtmlTable2(con,table,id);
      outTable.write(response.getWriter());
    }
          .
          .

Important:

It will also be necessary to add this image/svg+xml MIME type to your web server configuration. If your server is the Apache Jakarta Tomcat this is accomplished by creating an additional MIME type record in the \tomcat\conf\web.xml configuration file. There should also be a record for the svgz extension, which is the gzipped version of svg used to compress file sizes and reduce bandwidth load. Listing 7 shows an example <mime-mapping> record.

Listing 7 - sample <mime-mapping> records

<mime-mapping>
  <extension>
    svg
  </extension>
  <mime-type>
    image/svg+xml
  </mime-type>
</mime-mapping>
<mime-mapping>
  <extension>
    svgz
  </extension>
  <mime-type>
    image/svg+xml
  </mime-type>
</mime-mapping>

If the chart parameter is true, which will be the case when we simply click a census tract polygon, then we will create a bar chart. If we hold down the shift key while clicking with our mouse, our chart parameter will be false and the previous HTML table will be returned.

Let's look at the SvgChart2 class in this iteration. This class will be similar to the HtmlTable class except for the SvgChart.write method. In this version, the bar chart svg is created by reading a previously created chart.svg file, stored on the server. Text content and rect attributes are modified to reflect the database fields from our query. This "template" approach allows us to create the chart visually with any svg editing tool and verify its appearance manually before feeding it through the servlet. Listing 8 is the chart template:

Listing 8 the Chart template

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" 
                   "http://www.w3.org/TR/SVG/DTD/svg10.dtd" [
<!ENTITY AXIS  "stroke:black;stroke-width:1;stroke-linejoin:round;fill:none;">
<!ENTITY LABEL "stroke:black;stroke-width:0.01;fill:black;font-size:5;">
<!ENTITY TITLE "stroke:black;stroke-width:0.01;fill:blue;font-size:6;">
<!ENTITY BAR 
  "stroke:black;stroke-width:0.001;fill:black;fill-opacity:0.5;font-size:3.5;">
<!ENTITY TIC "stroke:black;stroke-width:0.1;fill:black;font-size:3.0;">
]>
<svg width="100%" height="100%"  preserveAspectRatio="xMidYMid" 
     viewBox=" 0 0 120 130">
<text id="title" style="&TITLE;" x="25" y="10">Plymouth</text>
<g id="axis" style="&AXIS;">
<path d="M14.5,10 14.5,110.5 125,110.5"/>
<g id="test" style="&TIC;">
<line x1="12" y1="10" x2="15" y2="10"/>
<text x="3" y="10">100%</text>
<line x1="12" y1="35" x2="15" y2="35"/>
<text x="3" y="35">75%</text>
<line x1="12" y1="60" x2="15" y2="60"/>
<text x="3" y="60">50%</text>
<line x1="12" y1="85" x2="15" y2="85"/>
<text x="3" y="85">25%</text>
</g>
<text style="&LABEL;" x="-5" y="90" transform="rotate(-90,-5,90)">
  Population %
</text>
<text style="&LABEL;" x="25" y="117">Total</text>
<text style="&LABEL;" x="65" y="117">Male</text>
<text style="&LABEL;" x="100" y="117">Female</text>
</g>

<g id="totals">
<text id="totalTxt" style="&LABEL;" x="25" y="125">total</text>
<text id="totalMaleTxt" style="&LABEL;" x="65" y="125">totalMale</text>
<text id="totalFemaleTxt" style="&LABEL;" x="100" y="125">totalFemale</text>
</g>
<g id="total" style="&BAR;">
<rect id="under18" x="25" y="110" width="10" height="30" style="fill:green" transform="rotate(180,25,110)"/>
<text x="20" y="108" transform="rotate(-90,20,108)">under 18</text>
<rect id="adult" x="35" y="110" width="10" height="50" style="fill:red" transform="rotate(180,35,110)"/>
<text x="30" y="108" transform="rotate(-90,30,108)">18 to 65</text>
<rect id="over65" x="45" y="110" width="10" height="20" style="fill:blue" transform="rotate(180,45,110)"/>
<text x="40" y="108" transform="rotate(-90,40,108)">over 65</text>
</g>

<g id="male" style="&BAR;">
<rect id="under18Male" x="65" y="110" width="10" height="30" 
  style="fill:green" transform= "rotate(180,65,110)"/>
<text x="60" y="108" transform="rotate(-90,60,108)">under 18</text>
<rect id="adultMale" x="75" y="110" width="10" height="50" 
  style="fill:red" transform="rotate(180,75,110)"/>
<text x="70" y="108" transform="rotate(-90,70,108)">18 to 65</text>
<rect id="over65Male" x="85" y="110" width="10" height="20" 
  style="fill:blue" transform="rotate(180,85,110)"/>
<text x="80" y="108" transform="rotate(-90,80,108)">over 65</text>
</g>

<g id="female" style="&BAR;">
<rect id="under18Female" x="105" y="110" width="10" height="30" 
  style="fill:green" transform="rotate(180,105,110)"/>
<text x="100" y="108" transform="rotate(-90,100,108)">under 18</text>
<rect id="adultFemale" x="115" y="110" width="10" height="50" 
  style="fill:red" transform="rotate(180,115,110)"/>
<text x="110" y="108" transform="rotate(-90,110,108)">18 to 65</text>
<rect id="over65Female" x="125" y="110" width="10" height="20" 
  style="fill:blue" transform="rotate(180,125,110)"/>
<text x="120" y="108" transform="rotate(-90,120,108)">over 65</text>
</g>

</svg>

The only thing to note on this chart template is the liberal use of id attributes, which will make editing our template easier. The <rect> shapes have been positioned with a transform in order to minimize the number of edits required in the servlet processing. By rotating the rectangle 180 degrees we can position all of the rectangles on the bottom axis of our chart. Only a modification of the height attribute will be required with this approach. Again we are taking into account the inverted Cartesian plane of the SVG specification. Listing 9 shows the SvgChart2 class.

Listing 9 SvgChart2 processing XML as Strings

  import java.io.*;
  import java.util.*;
  import java.sql.*;

public class SvgChart2 {
  private Connection con = null;
  private String table;
  private String id;


  public SvgChart2(Connection con, String table, String id) {
    this.con = con;
    this.table = table;
    this.id = id;
  }

  public void write(PrintWriter out) {
    String sql = "";
    int total;
    String line;
    StringBuffer buf;

    if ((table!=null) && (id!=null)) {
      try {
        Statement stmt = con.createStatement();
        sql = "SELECT * FROM " + table + " WHERE id=\"" + id + "\";";

        if (stmt.execute(sql)) {
          // There is a ResultSet
           ResultSet rs = stmt.getResultSet();
          // There is only one record
          if (rs.next()) {
            total = rs.getInt("total");
            BufferedReader infile = new BufferedReader(
                         new FileReader( "/servlet/DataQuery2/svg/chart.svg"));

            while (((line = infile.readLine()) != null)) {
              if (line.startsWith("<text id=\"title\"")) {
                buf = new StringBuffer(line.substring(0,line.indexOf(">")+1));
                buf = buf.append(table + " tract: " + id);
                line = buf.append(
                                line.substring(line.indexOf("</"))).toString();
              }
              else if (line.startsWith("<text id=\"totalTxt\"")) {
                buf = new StringBuffer(line.substring(0,line.indexOf(">")+1));
                buf = buf.append("Total: " + total);
                line = buf.append(
                                line.substring(line.indexOf("</"))).toString();
              }
              else if (line.startsWith("<text id=\"totalMaleTxt\"")) {
                buf = new StringBuffer(line.substring(0,line.indexOf(">")+1));
                buf = buf.append(Integer.toString(rs.getInt("totalMale")));
                line = buf.append(
                                line.substring(line.indexOf("</"))).toString();
              }
              else if (line.startsWith("<text id=\"totalFemaleTxt\"")) {
                buf = new StringBuffer(line.substring(0,line.indexOf(">")+1));
                buf = buf.append(Integer.toString(rs.getInt("totalFemale")));
              line = buf.append(
                                line.substring(line.indexOf("</"))).toString();
              }

              if (line.startsWith("<rect id=\"under18\"")) {
                buf = new StringBuffer(
                                  line.substring(0,line.indexOf("height=")+8));
                buf = buf.append(
                            Double.toString(rs.getInt("under18")*100.0/total));
                line = buf.append(
                         line.substring(line.indexOf("\" style="))).toString();
              }
              else if (line.startsWith("<rect id=\"adult\"")) {
                buf = new StringBuffer(
                                  line.substring(0,line.indexOf("height=")+8));
                buf = buf.append(
                              Double.toString(rs.getInt("adult")*100.0/total));
                line = buf.append(
                         line.substring(line.indexOf("\" style="))).toString();
              }
             else if (line.startsWith("<rect id=\"over65\"")) {
               buf = new StringBuffer(
                                  line.substring(0,line.indexOf("height=")+8));
               buf = buf.append(
                             Double.toString(rs.getInt("over65")*100.0/total));
               line = buf.append(
                         line.substring(line.indexOf("\" style="))).toString();
             }
             if (line.startsWith("<rect id=\"under18Male\"")) {
               buf = new StringBuffer(
                                  line.substring(0,line.indexOf("height=")+8));
               buf = buf.append(
                        Double.toString(rs.getInt("under18Male")*100.0/total));
               line = buf.append(
                         line.substring(line.indexOf("\" style="))).toString();
             }
             else if (line.startsWith("<rect id=\"adultMale\"")) {
               buf = new StringBuffer(
                                  line.substring(0,line.indexOf("height=")+8));
               buf = buf.append(
                          Double.toString(rs.getInt("adultMale")*100.0/total));
               line = buf.append(
                         line.substring(line.indexOf("\" style="))).toString();
             }
             else if (line.startsWith("<rect id=\"over65Male\"")) {
               buf = new StringBuffer(
                                  line.substring(0,line.indexOf("height=")+8));
               buf = buf.append(
                         Double.toString(rs.getInt("over65Male")*100.0/total));
               line = buf.append(
                         line.substring(line.indexOf("\" style="))).toString();
             }

              if (line.startsWith("<rect id=\"under18Female\"")) {
                buf = new StringBuffer(
                                  line.substring(0,line.indexOf("height=")+8));
                buf = buf.append(
                      Double.toString(rs.getInt("under18Female")*100.0/total));
                line = buf.append(
                         line.substring(line.indexOf("\" style="))).toString();
              }
              else if (line.startsWith("<rect id=\"adultFemale\"")) {
                buf = new StringBuffer(
                                  line.substring(0,line.indexOf("height=")+8));
                buf = buf.append(
                        Double.toString(rs.getInt("adultFemale")*100.0/total));
                line = buf.append(
                         line.substring(line.indexOf("\" style="))).toString();
              }
              else if (line.startsWith("<rect id=\"over65Female\"")) {
                buf = new StringBuffer(
                                  line.substring(0,line.indexOf("height=")+8));
                buf = buf.append(
                       Double.toString(rs.getInt("over65Female")*100.0/total));
                line = buf.append(
                         line.substring(line.indexOf("\" style="))).toString();
             }
              out.println(line);

            } //while
            infile.close();
          }
          rs.close();
        } else {
          out.println("<text style=\"&LABEL;\" x=\"25\" y=\"125\">" + table
                             + " has no record for Id =" + id+"</text></svg>");
        }
        stmt.close();
      } catch (SQLException e) {
        out.println("<text style=\"&LABEL;\" x=\"25\" y=\"125\">ERROR:Mysql  "
                                            + e.getMessage()+"</text></svg>");
      } catch (IOException e) {
       out.println(
                "<text style=\"&LABEL;\" x=\"25\" y=\"125\">IO ERROR:Infile  "
                                             + e.getMessage()+"</text></svg>");
      }
    } else {
       out.println("<text style=\"&LABEL;\" x=\"25\" y=\"125\">Error: table or id parameter is null</text></svg>");
    }
  }

Again we are using our JDBC connection and obtaining a result set from the same sql statement, "SELECT * FROM " + table + " WHERE id=" + id;" However, instead of populating a Table of HTML tags we are creating svg output. Creating all the features from scratch could be quite tedious, especially if our template svg is quite large. In this example we read a pre-made template, chart.svg, and just modifying the parts that change from one record to the next. Even with this very small chart.svg template, this approach leads to a significant amount of repetitious else if clauses. Figure 6 shows the output of the SvgChart.

Figure 6 SVG Bar Chart of query result

Servlet 3 - Database query3 - return SVG Bar Chart using a SAX parser ContentHandler

As we saw in servlet 2, modifying a template with text filters can be tedious even with a simple template. Let's explore some other possibilities. First we want to look at using a SAX parser to read our template and modify the output with a ContentHandler. Recall that SAX stands for Simple API for XML. SAX is an event based approach to XML processing, which reads the XML serially without building a DOM in memory. There are several popular Parsers available, but we have chosen to use the Xerces parser available from Apache (http://xml.apache.org/). Listing 10 shows the main class, DataQuery3, which sets up the SAX parsing if chart is true:

Listing 10 - DataQuery3 servlet

           .
           .
      if (chart) {
        response.setContentType("image/svg+xml");
        PrintWriter out = response.getWriter();

          // setup SAX Parser with result set
          SVGContentHandler svgHandler = 
                                    new SVGContentHandler(out, con, table, id);
          try {
            String template = "/servlet/DataQuery3/svg/chart.svg";

            XMLReader reader = 
       XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
            ContentHandler svg = new SVGContentHandler(out, con, table, id);
            ErrorHandler svgError = new SVGErrorHandler(out);
            //Register the SVG contentHandler
            reader.setContentHandler(svg);

            //Register the SVG ErrorHandler
            reader.setErrorHandler(svgError);

            //Set validation only if interested 
            //reader.setFeature("http://xml.org/sax/features/validation",true);

            //Parse SVG to client browser
            InputSource inputSource = new InputSource(template);
            reader.parse(inputSource);

            out.flush();
          } catch (IOException e) {
            out.println("IO Error: " + e.getMessage());
          } catch (SAXException e) {
            out.println("SAX Excepion: " + e.getMessage());
          }
      } else {
        response.setContentType("text/html");
        HtmlTable3 outTable = new HtmlTable3(con,table,id);
        outTable.write(response.getWriter());
      }
           .
           .

Here we are using the XMLReaderFactory to set up our SAXParser from the Xerces.jar for this project. Once we have a parser and we begin to parse our template, the action moves to the SVGContentHandler, shown in Listing 11.

Listing 11 - SVGContentHandler for DataQuery3

  import java.io.*;
  import org.xml.sax.*;
  import org.xml.sax.helpers.DefaultHandler;
  import java.sql.*;

public class SVGContentHandler extends DefaultHandler {
  private PrintWriter out;
  private Connection con = null;
  private String table;
  private String id;

  private ResultSet rs;
  private boolean results;
  private boolean skipTxt = false;
  private int total;

  public SVGContentHandler(PrintWriter out, Connection con, 
                                                     String table, String id) {
    this.out = out;
    this.con = con;
    this.table = table;
    this.id = id;

    String sql = "";

      // Collect query results
      try {
        Statement stmt = con.createStatement();
        sql = "SELECT * FROM " + table + " WHERE id=\"" + id + "\";";
        //out.println(sql);
        if (stmt.execute(sql)) {
          // There is a ResultSet
          rs = stmt.getResultSet();

          if (rs.next()) {
            results = true;
            total = rs.getInt("total");
          }
          stmt.close();
        }
      } catch (SQLException e) {
        out.println("MySQL Error: " + e.getMessage());
      }
  }

  public void processingInstruction(String target, String data)
                                                          throws SAXException {
    out.println(target+data);
  }

  public void startElement(String namespaceURI, String localName, 
                         String rawName, Attributes atts) throws SAXException {
      try {
        out.print("<"+localName);
        for (int i=0; i<atts.getLength(); i++) {
          if (results && (localName.equals("rect") && 
              atts.getLocalName(i).equals("height"))) {
            out.print(" "+atts.getLocalName(i)+"=\""+(rs.getInt(atts.getValue("id"))*100.0/total)+"\"");
          }
          else out.print(" "+atts.getLocalName(i)+"=\""+atts.getValue(i)+"\"");
        }
        out.print(">");
        if (results) {
          if (localName.equals("text") &&
                  atts.getValue("id")!=null &&
                    atts.getValue("id").equals("title")){
            out.print(table + " - Tract: " + id);
            skipTxt = true;
          }
          else if (localName.equals("text") &&
                  atts.getValue("id")!=null &&
                    atts.getValue("id").equals("totalTxt")){
            out.print("Total: " + rs.getInt("total"));
            skipTxt = true;
          }
          else if (localName.equals("text") &&
                  atts.getValue("id")!=null &&
                    atts.getValue("id").equals("totalMaleTxt")){
            out.print(rs.getInt("totalMale"));
            skipTxt = true;
          }
          else if (localName.equals("text") &&
                  atts.getValue("id")!=null &&
                   atts.getValue("id").equals("totalFemaleTxt")){
            out.print(rs.getInt("totalFemale"));
            skipTxt = true;
          }
      }
    } catch (SQLException e) {
      out.print("Sql error");
    }
  }

  public void endElement(String namespaceURI, String localName, String rawName)
                                                          throws SAXException {
    out.print("</"+localName+">");
    skipTxt = false;
  }

  public void characters(char[] characters, int start, int end)
                                                          throws SAXException {
    if (!skipTxt) {
      String line = new String(characters,start,end);
      out.print(line);
    }
  }

  public void ignorableWhitespace(char[] characters, int start, int end)
                                                          throws SAXException {
    String line = new String(characters,start,end);
    out.print(line);
  }

  public void endDocument() throws SAXException {
    try {
      rs.close();
    } catch (SQLException e) {
        out.println("MySQL Error: " + e.getMessage());
    }
  }
}

The SVGContentHandler extends the DefaultHandler and furnishes callback methods for the important events as they occur in the parsing. As soon as we receive a notice of the document start, we collect the usual result set from our database query. This is saved in a class variable for use when needed. We also set a results flag to let us know that a result is available.

The main parsing event of interest is the startElement. Here we check elements and id attributes looking for the elements that need to be changed. As we loop through the attributes we check for rect elements and replace the height attribute with a new calculated height. Otherwise we simply print the elements and their attributes to the servlet response. The new height calculation takes advantage of information in the chart id attributes:

(rs.getInt(atts.getValue("id"))*100.0/total

We have used the table field names as id values in our pre-made chart.svg file. This helps us grab the correct field value from our result set, because atts.getValue("id") will return the Plymouth table field name needed for the calculation. A few of the text elements of our chart.svg template need to have their character data modified. We must check for the appropriate text elements and print out the new text content:

if (localName.equals("text") &&
    atts.getValue("id")!=null &&
    atts.getValue("id").equals("title")){
  out.print(table + " - Tract: " + id);
  skipTxt = true;
}

However we do not want the old text content to print so we must also set a skipTxt flag and check this flag in the character callback:

  public void characters(char[] characters, int start, int end)
                                                          throws SAXException {
    if (!skipTxt) {   
      String line = new String(characters,start,end);
      out.print(line);
    }
  }

If the skipTxt flag has been set true we know that the character content should not be printed. At the end of our document we simply close up our result set.

The SAXParser approach to template processing adds some advantages. First we are able to do some checking, if desired. We can set the validation to true and verify that our SVG template is "valid" in addition to "well formed". However, the SAX parser approach is somewhat cumbersome to implement. We are breaking up our processing code across several of the callback methods, which can get complicated. There are additional flags to set and check whenever a replacement is needed. However, the SAXParser has one big advantage. It does not create and store the entire SVG template in memory as a DOM tree. This means very large templates could be processed through a SAXParser without running into memory constraints. Our sample uses a very small template, but in the case where the template size is large, a SAXParser might be the better choice. Since bandwidth constraints are still a real issue it, is unlikely that SVG projects will use very large templates. As we will see in our next servlet iteration, the DOMParser is much cleaner to code, but the SAXParser will always be faster. In fact the DOMParser actually uses the SAXParser to create its in memory DOM tree.

Servlet 4 - Database query4 - return SVG Bar Chart using DOM Parser

Changing our template process from a SAXParser to a DOMParser is not very complex. Instead of using a ContentHandler that triggers responses to individual elements of our template file, a DOMParser reads the entire file into memory. Once we have a DOM tree in memory we can use org.w3c.dom methods to make the necessary changes to reflect our database record. This will be familiar since it is similar to DOM processing used in ECMAScript. Listing 12 shows the necessary changes to our main class, DataQuery4.

Listing 12 - DataQuery4 servlet

           .
           .
      if (chart) {
        response.setContentType("image/svg+xml");
        SvgChart outChart = new SvgChart(con,table,id);
        outChart.write(response.getWriter());
      } else {
        response.setContentType("text/html");
        HtmlTable outTable = new HtmlTable(con,table,id);
        outTable.write(response.getWriter());
      }
           .
           .

This is identical to the dataQuery2 class in servlet 2, allowing the chart parameter from the SVG ECMAScript to choose between the HTML output and the SVG bar chart output. Again the real work goes on in the SvgChart4 class, shown in Listing 13

Listing 13 - SvgChart4

  import java.io.*;
  import java.util.*;
  import java.sql.*;
  import javax.xml.parsers.DocumentBuilder;
  import javax.xml.parsers.DocumentBuilderFactory;
  import javax.xml.parsers.ParserConfigurationException;
  import org.xml.sax.SAXException;
  import org.apache.xml.serialize.XMLSerializer;
  import org.apache.xml.serialize.OutputFormat;
  import org.w3c.dom.*;

public class SvgChart4 {
  private Connection con = null;
  private String table;
  private String id;


  public SvgChart4(Connection con, String table, String id) {
    this.con = con;
    this.table = table;
    this.id = id;
  }

  public void write(PrintWriter out) {
    String sql = "";
    int total;

    if ((table!=null) && (id!=null)) {
      String template = "C:/Program Files/Apache Group/jakarta-tomcat/webapps/DataQuery4/svg/chart.svg";
      // setup DOM Parser with result set
      try {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder svgParser = factory.newDocumentBuilder();
        // read in our template and parse into a DOM tree
        Document doc = svgParser.parse( new File(template));
        try {
          //collect a result set from our sql query
          Statement stmt = con.createStatement();
          sql = "SELECT * FROM " + table + " WHERE id=\"" + id + "\"";
          if (stmt.execute(sql)) {
            // There is a ResultSet
            ResultSet rs = stmt.getResultSet();
            // There is only one record
            while (rs.next()) {
              total = rs.getInt("total");
   // use org.w3c.dom methods to change dom tree according to the result record
              Element obj;
              obj = doc.getElementById("title");
              obj.getFirstChild().setNodeValue(table + " - tract: " + id);

              obj = doc.getElementById("totalTxt");
              obj.getFirstChild().setNodeValue("Total: " + total);
              obj = doc.getElementById("totalMaleTxt");
              obj.getFirstChild().setNodeValue(
                                           "Total: " + rs.getInt("totalMale"));
              obj = doc.getElementById("totalFemaleTxt");
              obj.getFirstChild().setNodeValue(
                                         "Total: " + rs.getInt("totalFemale"));

              obj = doc.getElementById("under18");
              obj.setAttribute("height", 
                            Double.toString(rs.getInt("under18")*100.0/total));
              obj = doc.getElementById("adult");
              obj.setAttribute("height",
                              Double.toString(rs.getInt("adult")*100.0/total));
              obj = doc.getElementById("over65");
              obj.setAttribute("height",
                             Double.toString(rs.getInt("over65")*100.0/total));

              obj = doc.getElementById("under18Male");
              obj.setAttribute("height",
                        Double.toString(rs.getInt("under18Male")*100.0/total));
              obj = doc.getElementById("adultMale");
              obj.setAttribute("height",
                          Double.toString(rs.getInt("adultMale")*100.0/total));
              obj = doc.getElementById("over65Male");
              obj.setAttribute("height",
                         Double.toString(rs.getInt("over65Male")*100.0/total));

              obj = doc.getElementById("under18Female");
              obj.setAttribute("height",
                      Double.toString(rs.getInt("under18Female")*100.0/total));
              obj = doc.getElementById("adultFemale");
              obj.setAttribute("height",
                        Double.toString(rs.getInt("adultFemale")*100.0/total));
              obj = doc.getElementById("over65Female");
              obj.setAttribute("height",
                       Double.toString(rs.getInt("over65Female")*100.0/total));
            }
            rs.close();
          } else {
            out.println("<text style=\"&LABEL;\" x=\"25\" y=\"125\">"+table + " has no record for Id =" + id+"</text>");
          }
          stmt.close();
        } catch (SQLException e) {
          out.println("<text style=\"&LABEL;\" x=\"25\" y=\"125\">ERROR:Mysql  " + e.getMessage()+"</text>");
        }

        // now that the changes are done output the doc to our servlet response
        XMLSerializer serializer = 
            new XMLSerializer((Writer)out, new OutputFormat(doc,"UTF-8",true));
        serializer.serialize(doc);
      } catch (IOException e) {
        out.println("IO Error: " + e.getMessage());
        e.printStackTrace(out);
      } catch (ParserConfigurationException e) {
        out.println("Parser Configuration Error: " + e.getMessage());
      } catch (SAXException e) {
        out.println("SAX Excepion: " + e.getMessage());
      }
    }
  }
}

First a parser is obtained using the Jaxp DocumentBuilderFactory.

 
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder svgParser = factory.newDocumentBuilder();

In our case we used the Xerces2.0 jar file for the actual DOM parser. Once we have a parser we use it to read and parse the chart.svg template.

        Document doc = svgParser.parse( new File(template));

We can now use org.w3c.dom methods on the svg doc in memory. Since we have carefully provided id attributes for all our elements, the DOM Level 2 getElementById() method is very useful for minimizing DOM manipulation.

              obj = doc.getElementById("under18");
              obj.setAttribute("height", Double.toString(rs.getInt("under18")*100.0/total));

The actual modification of our template is much cleaner and straightforward using the DOM methods. We are not tangling with convoluted String calculations as in servlet 2 nor do we have code spread all over a contentHandler as in servlet 3. This demonstrates some of the advantage of choosing a DOM parser for our template processing. The only difficulty occurs when the template file is large. If our chart.svg template were moderately large the SAX parser would provide an advantage in processing time even though it is more complex to get setup. Let's take this through one more iteration and look at template processing using JDOM.

Servlet 5 - Database query - return svg Bar Chart using JDOM

Our final iteration (listing 14) will look at the JDOM approach to our template processing. JDOM is a Java centric open source DOM API. It is currently in the Java Community Process as a Java Specification request, JSR-102. Eventually it will work its way into a consolidated Java XML offering along with several other rapidly evolving Java/XML APIs: JAXP, JAXB, JAX-RPC, JAXM, and JAXR. JDom beta 0.7 was used for this iteration and more information is available at (http://www.jdom.org/). JDOM offers a couple of advantages for the Java programmer. JDOM provides a light-weight API for XML processing that takes advantage of Java collections. Consequently JDOM provides access to a Java optimized DOM that consumes less memory and is more familiar to the Java community than some of the other parsers.

Listing 14 - DataQuery5 using JDOM

  import java.io.*;
  import java.util.*;
  import java.sql.*;
  import java.net.*;

  import org.jdom.*;
  import org.jdom.JDOMException;
  import org.jdom.input.SAXBuilder;
  import org.jdom.output.XMLOutputter;

public class SvgChart5 {
  private Connection con = null;
  private String table;
  private String id;

  public SvgChart5(Connection con, String table, String id) {
    this.con = con;
    this.table = table;
    this.id = id;
  }

  public void write(PrintWriter out) {
    String sql = "";
    int total;

    if ((table!=null) && (id!=null)) {
      String template = "/servlet/DataQuery5/svg/chart.svg";
      // setup DOM Parser with result set
      try {
      // create a SAX document builder using default Xerces without vailidation
        SAXBuilder builder = new SAXBuilder();
        // create the svg JDOM document from our chart.svg template
        Document svg = builder.build( new File(template));

        try {
          //collect a result set from our sql query
          Statement stmt = con.createStatement();
          sql = "SELECT * FROM " + table + " WHERE id=\"" + id + "\"";
          if (stmt.execute(sql)) {
            // There is a ResultSet
            ResultSet rs = stmt.getResultSet();
            // There is only one record
            while (rs.next()) {
              total = rs.getInt("total");
              Element root = svg.getRootElement();

       // change all the text elements
       Iterator it = root.getChildren("text").iterator();
       while (it.hasNext()) {
         Element obj = (Element)it.next();
         if (obj.getAttribute("id").getValue().equals("title")) {
           obj.setText(table + " - tract: " + id);
         }
         else if (obj.getAttribute("id").getValue().equals("totalTxt")) {
           obj.setText("Total: " + total);
         }
         else if (obj.getAttribute("id").getValue().equals("totalMaleTxt")) {
           obj.setText("Total: " + rs.getInt("totalMale"));
         }
         else if (obj.getAttribute("id").getValue().equals("totalFemaleTxt")) {
            obj.setText("Total: " + rs.getInt("totalFemale"));
         }
       }
       // change all the rect elements
       it = root.getChildren("rect").iterator();
       while (it.hasNext()) {
         Element obj = (Element)it.next();
         if (obj.getAttribute("id").getValue().equals("under18")) {
           obj.setAttribute("height",
                            Double.toString(rs.getInt("under18")*100.0/total));
         }
         else if (obj.getAttribute("id").getValue().equals("adult")) {
           obj.setAttribute("height",
                              Double.toString(rs.getInt("adult")*100.0/total));
         }
         else if (obj.getAttribute("id").getValue().equals("over65")) {
           obj.setAttribute("height",
                             Double.toString(rs.getInt("over65")*100.0/total));
         }
         if (obj.getAttribute("id").getValue().equals("under18Male")) {
           obj.setAttribute("height",
                        Double.toString(rs.getInt("under18Male")*100.0/total));
         }
         else if (obj.getAttribute("id").getValue().equals("adultMale")) {
           obj.setAttribute("height",
                          Double.toString(rs.getInt("adultMale")*100.0/total));
         }
         else if (obj.getAttribute("id").getValue().equals("over65Male")) {
           obj.setAttribute("height",
                         Double.toString(rs.getInt("over65Male")*100.0/total));
         }
         if (obj.getAttribute("id").getValue().equals("under18Female")) {
           obj.setAttribute("height",
                      Double.toString(rs.getInt("under18Female")*100.0/total));
         }
         else if (obj.getAttribute("id").getValue().equals("adultFemale")) {
           obj.setAttribute("height",
                        Double.toString(rs.getInt("adultFemale")*100.0/total));
         }
         else if (obj.getAttribute("id").getValue().equals("over65Female")) {
           obj.setAttribute("height",
                       Double.toString(rs.getInt("over65Female")*100.0/total));
         }
       } //while

            }
            rs.close();
          } else {
            out.println("<text style=\"&LABEL;\" x=\"25\" y=\"125\">"+table +
                                     " has no record for Id =" + id+"</text>");
          }
          stmt.close();
        } catch (SQLException e) {
         out.println("<text style=\"&LABEL;\" x=\"25\" y=\"125\">ERROR:Mysql  "
                                                   + e.getMessage()+"</text>");
        }

        // now that the changes are done output the doc to our servlet response
        XMLOutputter outputter = new XMLOutputter();
        outputter.output(svg, out);
      } catch (JDOMException e) {
        e.getMessage();
      } catch (Exception e) {
        e.getMessage();
      }
    }
  }
}

JDOM is still not in a final release but at this stage there are fewer DOM methods to choose from, when moving around the DOM tree. Instead of directly accessing an element with the convenient getElementById("name") as in the DOMParser we need to iterate through different levels and check for a desired id. We could have altered the chart.svg template to better suite JDOM parsing tools by eliminating nesting. However, that is an unlikely possibility in a real world template. Perhaps a better approach would be creating a custom namespace with custom elements specifically for the JDOM parsing. In that case the Element.GetChild(tagName, tagNameSpace) could conveniently locate individual elements which require modification.

Summary

In this case study we have shown how to develop a small but full-scale web map application with SVG. Server side XML tools can be used for more than just template processing. For example server side layer selection, data merging, data attribute filtering, and geo-spatial queries are just a few areas of GIS that would lend themselves to server side solutions. Combining open source SVG with server side Java APIs provides an extremely powerful and flexible approach to Internet GIS. The technologies illustrated have equal application across a wide spectrum of engineering design fields requiring the flexibility of vectors capable of linking to server side functions and database interaction.

Here are the specific technologies we can now apply to new projects:


Valid XHTML 1.0!