This is part 6 of a series of blog posts on the design and implementation of the location-aware Windows Store app "Location Data Logger". Download the source code to Location Data Logger here.
The Export ClassThe early development versions of Location Data Logger only logged data points to CSV files. This was fairly easy to implement and the code responsible for the file I/O was mixed in with the MainPage class. When the logger was started the log file was created directly inside of what would later become the logger_start() method, and log points were written out inside of update_position(). While this was not great modular design, at the time it was fast and simple, more than adequate for the task, and certainly appropriate for an early phase of development. Long term, however, I needed something more scalable, and more robust.
The goal for Location Data Logger was always to be able to write to multiple file formats. This is something that is supported by nearly every other GPS data logger app that you can find for mobile devices, as well as by dedicated consumer data loggers, and I did not want to release a less capable app for this project. And, on top of that, I didn't want Location Data Logger to be a merely academic exercise: as an instructional tool, I felt it important to have just enough complexity to require some thoughtful design.
At minimum, the code for handling the file operations would have to move out of the MainPage class and into its own module. The real question, though, was how to handle the mutliple file formats.
One solution would have been to have one large exporter that was simply responsible for everything, but that felt unwieldly. Though it would allow for sufficient code reuse, the fact that each file format had its own dependancy modules and its own quirks (I'm looking at you, KML) meant that it would be a monolithic class with everything but the kitchen sink. On top of that, I'd need to either pass flags to tell the module which file formats were actively being logged, or created individual methods for logging each independantly as needed. While a valid approach, it flies in the face of object-oriented design principles.
The approach I chose was to make use of inheritence, creating a master Exporter class with child classes for managing each file format.
The base class
There are three basic logging operations in Location Data Logger, and operations that are common to all file formats are implemented in the base class which is called Export:
- Start the logging. This entails opening the log file on the filesystem and getting a handle to the file.
- Stop the logging. Close the file.
- Write to the log file. Write out one or more data points.
Note that I said "common to all file formats". The implication here is that some file formats require special handling for one or more of these operations, but at minimum they all have some basic stuff that they need to do. Specifically, open the file, close the file, and write text to it. Any customization to these three operations is handled within the child classes.
Note that, while the Export class defines a Write() method for writing to the file, this is a low-level function. Trackpoints are logged by calling the LogPoint() method defined in the child classes, which in turn call Write() as needed.
The Export class also defines two class members:
protected StorageFile file; protected enum ExportStatus { Inactive, Initializing, Ready }; protected ExportStatus status;
The file variable is of type StorageFile and is the handle that each module uses to write to the underlying, physical file. The Start() method in the base class is responsible for opening this handle and setting file.
The status variable provides some basic bookkeeping. It's an enumeration with three possible values:
- Initializing. The logger is in the process of opening the file, which is an asynchronous operation.
- Ready. The log file was successfully opened and the Export object can write to the file.
- Inactive. The log file is not open for writing. This means it has either not yet been opened, that it has been explicitly closed because logging was stopped, or a failure occured during an I/O operation.
A child object can use this status variable to make intelligent decisions about what file operations should or should not be attempted. (Ideally, the base Export object would also have this logic as a precaution. This is something I should add in a future version.)
The ExportCSV class
The ExportCSV class is the simplest of the export modules because the file format does not have a complicated schema. Data is organized in rows, with commas separating each field or column, and the first row contains the field names. Thus, the overload for the Start() method is very short, and is used to print the header row:
public async void Start(StorageFolder folder, String basename)
{
pid = 1;
await base.Start(folder, basename, extension);
if (status != Export.ExportStatus.Ready) return;
try
{
await this.Write("PID,Latitude,Longitude,Accuracy,Altitude,AltitudeAccuracy,Speed,Heading,Orientation,HighPrecision,Timestamprn");
}
catch
{
status = Export.ExportStatus.Inactive;
}
}
Note that I use exception handling to set the status property to Inactive in the event of a failure. This will prevent the logger from attempting to write to a file that is not open.
No overloading is needed for the Stop() method. The LogPoint() method merely prints a line to the CSV file every time a track point comes in.
The ExportGPX class
This module is more complicated than the CSV module because a GPX file is an XML data file with a set schema. I had two choices here: either create the XML by hand, or use one of the XML libraries. I opted for the former because the GPX format is not very complicated, particularly for recording a single track log. It also offers slightly better crash protection, since writing the XML out as track points come in means that the file will be mostly complete (missing just the closing XML tags) in the event the app quits unexpectedly. Using an XML builder would require writing the whole file out perioidically, and when the logger is stopped. That can cause data loss in the event of a crash.
Like the ExportCSV module, then, the Starrt() method overload is used to print the preamble for the file, which in this case is a large chunk of XML. The LogPoint() method is similarly used to print the XML for each track point as they come in. Unlike the CSV module, however, this one needs an override for Stop() so that the closing XML tags can be printed:
const String footer= "</trkseg>rn</trk>rn</gpx>rn"; public override async void Stop() { if (status == Export.ExportStatus.Ready) { try { await this.Write(footer); } catch { } } base.Stop(); }
The ExportKML class
This is the most complicated of the exporters because a KML file has an elaborate schema, and there is no practical way to build the XML as you go. For this reason, I opted to use the XML classes in the Windows* Runtime to build the data file, and only write it out when the logger is stopped. One implication of doing this is that there is no crash protection: if the app quits unexpectedly, the KML file will not be generated. It would be good to add support for periodic writes (perhaps once or twice a minute) in future versions.
The Start() method sets up the base XML document structure and defines the parent elements that must be referenced when new log points are added. The LogPoint() method creates the XML for each log point, and adds it to the appropriate parent element. The Stop() method finishes up some final XML structures and then writes the whole thing out.
Calling the Export modules
The export modules are members of the DataLogger object. Logging a data point is quite simple, and done in the log_position() method which is called from the geo_PositionChanged event handler.
if (logCSV) eCSV.LogPoint(trkpt); if (logGPX) eGPX.LogPoint(trkpt); if (logKML) eKML.LogPoint(trkpt);
Opening and closing the export modules is similarly easy, and handled within the Start() and Stop() methods in DataLogger:
public void Start()
{
basename = DateTime.Now.ToString("yyyyMMdd_HHmmss");
Resume();
running = true;
if (logCSV) eCSV.Start(folder, basename);
if (logGPX) eGPX.Start(folder, basename);
if (logKML) eKML.Start(folder, basename);
}
public void Stop()
{
running = false;
if (logCSV) eCSV.Stop();
if (logGPX) eGPX.Stop();
if (logKML) eKML.Stop();
Pause();
}
← Part 5: The Data Grid View |