Wednesday, May 2, 2012

A closer look at the Spot tracker API - Write your own tracker

[Added 12/27/2014:  Updated code using the new schema and JSON encoding, here]
[Added 1/22/2014:  Please note that SPOT has revised their API and this code has not been updated to reflect that.  See this post.]

Well, not very much closer, since the data and their representation are pretty simple, as described here.  So, I'll take a look at how to pull down the data, parse them out, and put them on a map.  This is where race tracking software starts, and the tracker put together by the folks who tracked the Percy Dewolfe didn't go a lot further.  A "real" tracker is a far more complicated thing and needs to be smart about speed to be credible, has to deal with storing and retrieving data for all participants over the length of the race, subsetting the data on demand, plus all that fancy stuff that Trackleaders and others do.

Snagging the data

An "API," or "application programming interface," is how services are exposed (or provided) to third parties programmatically.  That is to say, if I want to write my own programs that make use of Spot's tracking data, or Google Maps (or both!), I use their APIs from my programs.

The truth is that Spot's API can barely be considered an API, since all it really allows you to do is to download XML-formatted records.  Here's how it works:
  1. When you register your Spot tracker, or edit your settings, you have the opportunity (if and only if you've paid for the tracking service) to set up a "shared page" to display the data.  That shared page is automatically assigned a "glId" by Spot.  I believe that "glId" is a "guest link" identifier, but I wouldn't swear to it.  Anyway, you can find the glId for your shared pages by going to your list of shared pages at https://login.findmespot.com/spot-main-web/share/list.html and choosing the shared page that has the data you'd like to use.
  2. You'll find the glId embedded in the URI of the shared page.  So, if the URI of the shared page is "http://share.findmespot.com/shared/faces/viewspots.jsp?glId=0BW5A0B1QQFTHG4GivzbYsQFyIFo5VHTh" the glId is 0BW5A0B1QQFTHG4GivzbYsQFyIFo5VHTh.
  3. The data can be accessed through the URI http://share.findmespot.com/messageService/guestlinkservlet?glId=0BW5A0B1QQFTHG4GivzbYsQFyIFo5VHTh&completeXml=true with the glId from your shared page substituted in for the glId value.
When you do an HTTP GET on that URI, what you'll get is a chunk of XML, as described in this earlier post.

Displaying the data

Once you get the data out of your Spot shared page, you need to parse the XML, then turn it into data you can display using the mapping API of your choosing.  These days the most popular option is Google maps, which has a rich API and which gives the programmer a lot of control over what goes on the page and how the user interacts with it.  I've written a very simple example of using Spot data to create a track on a Google map, with clickable track points and pop-up info windows that show the timestamp and the distance from the previous track point.

I wrote it in Javascript because I wanted to be able to provide code you could play with without having to install additional infrastructure (languages, libraries, etc.).  If you're writing a real tracker you probably don't want to do this, since it puts some computational and storage load on the user's browser (seriously, doing trigonometric calculations in a browser window doesn't seem like a great idea).  It's cleaner and more efficient to do the computational work (including XML parsing) on the server side, in the language of your choice.  Note that in the code below I'm grabbing the data from localhost.  I made a local copy, since Spot only keeps your data for a week.  If you're writing a real tracker you'll have to add local data storage and data management (de-duplication, for example) to your code.  I've also left out all error-checking, data validation, etc., to keep the sample code compact and clean.  If you're writing code for production use you must check to make sure that operations are successful, that the data you're getting are clean, etc. - if you're going to fail, fail gracefully.  As an example of what I'm talking about, take a look at extract_gps_data, and notice that I'm making a lot of assumptions about what data are present and that the XML document hasn't been corrupted in some way - that's terrible programming practice.  Checking for run-time errors and validating your data gives you control over what your users experience if something goes wrong.  A program that "works" doesn't really work if it blows up on unexpected inputs.

So here's the code.  Drop me a line if anything isn't clear, or if you notice a problem.


<!DOCTYPE html>
<html>
<head>

 <title>My Wee Tracker</title>
 <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
 <style type="text/css">
  html { height: 100% }
  body { height: 100%; margin: 0; padding: 0 }
  #map_canvas { height: 100% }
 </style>
 <script type="text/javascript" 
  src="http://maps.googleapis.com/maps/api/js?key=AIzaSyBFoJjPtS9vWXIENOa-egd0XFFnnQbfTIk&sensor=false&libraries=geometry">
 </script>

<script type="text/javascript">
//<![CDATA[

// load_xml_doc takes a uri and retrieves the document at that location,
// and returns it.  

function load_xml_doc(uri)  {
 if (window.XMLHttpRequest)  {
  var request = new XMLHttpRequest();
 } 
 request.open("GET", uri, false);
 request.send();
 return request.responseXML;
}


// "point" is an object we use to hold the data we'll be putting on the map
//  I hate that javascript has us declare classes as functions

function point(timestamp, latitude, longitude)  {
 this.timestamp = timestamp;
 this.latitude = latitude;
 this.longitude = longitude;
}

// here's where we pull the tracker data out of the XML document
// and convert it into something easier to deal with when scribbling
// on the map.  It creates and returns an array of tracker points (messages) 

function extract_gps_data(trackerdata)  {
 var points = new Array();
 
 tracker_points = trackerdata.childNodes[0].getElementsByTagName("message");
 for ( i = 0 ; i < tracker_points.length ; i++ ) {
  tracker_point_node = tracker_points[i];
  timestamp = tracker_point_node.getElementsByTagName("timestamp")[0].textContent;
  latitude = tracker_point_node.getElementsByTagName("latitude")[0].textContent;
  longitude = tracker_point_node.getElementsByTagName("longitude")[0].textContent;
  
  var point_holder = new point(timestamp, latitude, longitude); 
  points.push(point_holder);
 }
 return points;
}
 
function makeinfobox(pointnum, thispoint, theotherpoint)  {
 var latlnga, latlngb; 
 var distance;
 var infoboxtext;
 var timestamp;
 
 timestamp = new Date(thispoint.timestamp); // we convert it from ISO format to something more readable
 infoboxtext = String(timestamp);
 if (pointnum > 0)  {  // no point calculating distance on the point
  latlnga = new google.maps.LatLng(thispoint.latitude, thispoint.longitude);
  latlngb = new google.maps.LatLng(theotherpoint.latitude, theotherpoint.longitude);
  distance = google.maps.geometry.spherical.computeDistanceBetween(latlnga, latlngb) / 1610; // convert to miles
  infoboxtext = infoboxtext + "<br />" + distance.toFixed(2) + " miles";
 } 
 return infoboxtext; 
}

// here's our pseudo-"main"

function initialize()  {
 var i = 0;
 var trackline = new Array();
 var windowtext;

 // First we pull down the tracker data and load it into an array of point objects

 trackerdata = load_xml_doc("http://localhost/~melinda/trackerdata.xml");
 points = extract_gps_data(trackerdata);
 
 // Next, we set up the map
 
 var spot = new google.maps.LatLng(points[0].latitude, points[0].longitude);
 var my_options = {
  center: spot,
  zoom: 12,
  mapTypeId: google.maps.MapTypeId.ROADMAP
 };
 var map = new google.maps.Map(document.getElementById("map_canvas"), my_options);


 for ( i = 0 ; i < points.length ; i++ )  {
  var contentstring = "Point " + i; 
  var spot = new google.maps.LatLng(points[i].latitude, points[i].longitude);
  // here we create the text that is displayed when we click on a marker
  var windowtext = makeinfobox(i, points[i], points[i-1]);  // if you tell anybody I did this I'll deny it vehemently
  var marker = new google.maps.Marker( {
   position: spot, 
   map: map,
   title: points[i].timestamp,
   html: windowtext
  } );
  
  // instantiate the infowindow
  
  var infowindow = new google.maps.InfoWindow( {
  } );

  // when you click on a marker, pop up an info window
  google.maps.event.addListener(marker, 'click', function() {
   infowindow.setContent(this.html);
   infowindow.open(map, this);
  });

  // set up the array from which we'll draw a line connecting the readings
  trackline.push(spot);
 }  
 
 // here's where we actually draw the path 
 var trackpath = new google.maps.Polyline( {
  path: trackline,
  strokeColor: "#FF00FF",
  strokeWeight: 3
 } );
 trackpath.setMap(map);
}
//]]>

</script>
</head>

<body onload="initialize()">

<div id="map_canvas" style="width:100%; height:100%"></div>

</body>
</html>