Mon 26 Jan 2009
3:24PM
compton

XML Parsing and Display of GPX data in Java

With all these GPX recordings of routes I'm collecting, I wanted to view and compare details of past routes in a more user-friendly manner than poring through reams of XML output. The Google Maps API would be great to incorporate somehow, and it is freely available for JavaScript use. However I decided to be awkward and write a desktop application in Java. I'm figuring that I'll find some way around the problem of getting graphical map imagery later.

So far I have a small Java Swing/AWT application which reads an XML file of GPS points from a recording of a cycle ride and displays the route in a JPanel within a JFrame:

There are three primary classes - quickChart which extends JFrame, chartPanel which extends JPanel, and gpxParser which extends org.xml.sax.helpers.DefaultHandler.

When the JFrame fires up, it sets the JPanel to visible. The JFrame also has a menu bar, with only two options at present: Open and Exit. Exit is obvious, and Open calls the gpxParser class to read a XML document via SAX. There are 600 to 4000 points in a file, and as it walks through the XML tree gpxParser reads each one into an ArrayList. This array list is then passed over to the JPanel, which iterates through them when rendering the route, and also when checking for 'mouseovers' (the point under the mouse is selected, as shown in orange on the route graphic).

Guest

6:17 pm, Sunday, 15 February 09

this sounds great, can you put an example code for me? thx!
 

compton

10:28 pm, Tuesday, 17 February 09

Here is the gpxParser class which parses the source XML using an event-driven SAX model.

To implement a SAX parser like this, you create a class which extends org.xml.sax.helpers.DefaultHandler, and in this class you override functions of DefaultHandler in order to handle various events which are fired as the XML document is read element-by-element:
import java.io.*; import java.text.ParseException; import java.util.ArrayList;   import org.xml.sax.*;   public class gpxParser extends org.xml.sax.helpers.DefaultHandler { private stringStack elementNames; private float minLat, maxLat, minLon, maxLon; private int totalPoints; private long totalSeconds; private ArrayList<journeyPoint> plots = new ArrayList<journeyPoint>(50); private StringBuilder contentBuffer; private double currentDistance;   public gpxParser() { clear(); }   public void clear() { totalPoints = 0; currentDistance = 0; totalSeconds = 0; elementNames = new stringStack(); plots.clear(); contentBuffer = new StringBuilder(); }   /* * READ GPX DATA FILE */ public int read(String filename) { clear();   try { FileInputStream in = new FileInputStream(new File(filename)); InputSource source = new InputSource(in);   XMLReader parser = org.xml.sax.helpers.XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser"); parser.setContentHandler(this); parser.parse(source); in.close(); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SAXException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return 0; }   public void load(ArrayList<journeyPoint> arrayList, journeyPoint topLeft, journeyPoint bottomRight) { clear(); plots.addAll(arrayList); totalPoints = plots.size(); }   /* * DefaultHandler::startElement() fires whenever an XML start tag is encountered * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) */ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {   // the <bounds> element has attributes which specify min & max latitude and longitude if (localName.compareToIgnoreCase("bounds") == 0) {   minLat = new Float(attributes.getValue("minlat")).floatValue(); maxLat = new Float(attributes.getValue("maxlat")).floatValue(); minLon = new Float(attributes.getValue("minlon")).floatValue(); maxLon = new Float(attributes.getValue("maxlon")).floatValue();   } else {   // the <trkpt> element has attributes which specify latitude and longitude (it has child elements that specify the time and elevation) if (localName.compareToIgnoreCase("trkpt") == 0) { totalPoints++; plots.add(new journeyPoint(Double.parseDouble(attributes.getValue("lon")), Double.parseDouble(attributes.getValue("lat")))); } }   // Clear content buffer contentBuffer.delete(0, contentBuffer.length());   // Store name of current element in stack elementNames.push(qName); }   /* * the DefaultHandler::characters() function fires 1 or more times for each text node encountered * */ public void characters(char[] ch, int start, int length) throws SAXException { contentBuffer.append(String.copyValueOf(ch, start, length)); }   /* * the DefaultHandler::endElement() function fires for each end tag * */ public void endElement(String uri, String localName, String qName) throws SAXException { String currentElement = elementNames.pop();   if (totalPoints > 0 && currentElement != null) { if (currentElement.compareToIgnoreCase("ele") == 0) { plots.get(totalPoints-1).setElevation(Float.parseFloat(contentBuffer.toString())); } else { if (currentElement.compareToIgnoreCase("time") == 0) { try { plots.get(totalPoints-1).setTime(contentBuffer.toString()); } catch (ParseException e) { System.out.println("Bad date: " + contentBuffer.toString()); } } else if (currentElement.compareToIgnoreCase("trkpt") == 0) { if (totalPoints > 1) { currentDistance += plots.get(totalPoints-1).metresTo(plots.get(totalPoints-2)); plots.get(totalPoints-1).setDistance(currentDistance); totalSeconds += plots.get(totalPoints-1).secondsTo(plots.get(totalPoints-2)); plots.get(totalPoints-1).setDuration(totalSeconds); } } } } }   public int getTotalPoints() { return totalPoints; }   public ArrayList<journeyPoint> getPlots() { return plots; }   public journeyPoint getTopLeft() { return new journeyPoint(minLon, minLat); }   public journeyPoint getBottomRight() { return new journeyPoint(maxLon, maxLat); } }
The SAX model is effectively a stateless XML parser. The XML document is read one time, and is not stored in memory. If we want to know where we are in the document tree, we need to record our position ourselves. This is what makes it so lightweight and fast, however it also means it isn't always intuitive to work with. Take a look at the GPS files which we're parsing here. They record track data as a series of <trkpt> elements:
<trkpt lat="52.446738677" lon="-1.880564885"> <ele>155.390259</ele> <time>2008-12-15T15:56:02Z</time> </trkpt>
Each of these elements records a GPS position using attributes specifying the latitude and longitude, and two child elements, <ele> and <time>, which each contain a string (in XML parlance a "child text node") representing the data value recorded at that point. The <ele> element records the elevation in metres, and the other is obvious.

So in order for us to record all the data for a point, we first need to read the attributes of the <trkpt> element, and then the text data of its two child elements. Each of these requires a different approach in our Java code. Reading attributes is straightforward - we can do it in the startElement() function which SAX fires each time the start of an XML element is encountered.

Reading the two text nodes is not so simple. Text nodes can be handled by overriding the characters() method, however a single text node may call this multiple times, once for each chunk of text. This means that we won't know that we have read all the text until we hit the end of the XML element - which we deal with in the endElement() method. So our characters() method just has to concatenate all the chunks of text, and the endElement() method will then process that text.

I use a simple stack to record the element we're currently reading, and the points of the GPX route are saved as an ArrayList of journeyPoint instances.

This ArrayList forms the internal model that our Java application uses to represent a recorded route. Once the gpsParser class has built it, it is passed to the chartPanel class for display.
 

Juan

8:02 pm, Wednesday, 23 December 09

Hi, I wonder if you could show me the code of the journeyPoint class...that would be very useful...Thanxs a lot!
 

compton

2:35 pm, Monday, 28 December 09

Hi Juan, I've uploaded all the code here. Give me a shout if you have any problems getting it to run OK.
 

Quique

6:19 pm, Tuesday, 6 April 10

Hi, I want to use your gpx parser (probably modified) in a personal project. Can I do that? What license or conditions will apply?
Thanks.
 

compton

1:01 am, Thursday, 8 April 10

Hi Quique, please feel free to make use of the code here. I haven't done any work on it for a while, so I'm glad to hear it could be useful to someone.

No restrictions, but I'd be interested to hear how it goes!
 

Guest

4:58 pm, Wednesday, 21 September 11

To read GPX files easily in Java see: http://sourceforge.net/p/gpsanalysis/home/Home
 

/xkcd/ Scary Triangles