Putting GPS Tracks into Google Maps

After watching a recent episode of Hak5, which showcased Google Maps GPS Mashups, I was inspired to finally do something with some of my own GPS data.  I had GPS data from a canoe trip taken last year and a road trip from a few years ago stored on my handheld GPS with which I had always planned to do something, but just hadn’t had the motivation until now.  While a little simpler than the project described on the show, my initial goal was to draw the GPS track data in Google Maps annotated with waypoints drawn as map markers.  The markers would be used to mark specific locations with descriptive text and images. 

To achieve this, I threw together a little JavaScript to load GPS data from an XML based GPX file and add it to a map using the Google Maps API.  You can see the results here, and if you’re interested in how this was done, or would like to add your own GPS data to Google Maps, just keep reading.  I’ll provide a detailed description of the process I used to get my GPS data into Google Maps, along with the JavaScript source code for anyone to use with their GPS data and the Google Maps API. 

Preparing the Data

Before I could actually add any of the GPS data to a map, I had to transfer it from by old Garmin GPSMAP 76.  Since I didn’t have the software to perform the transfer, I decided to ask GOOG for free GPS software recommendations.  After trying GPSMan for Linux, which worked but shifted the times on my GPS points by twelve hours for some reason, and reviewing a procedure for installing Garmin MapSource without the original installation media, I decided on EasyGPS for Windows.  

EasyGPS allowed me to copy the data from my GPS a GPX file on my PC, and edit the files to add waypoints that would display as markers in Google Maps.  Then it was time to add the data to a map.  

Adding Data to the Map

I started by searching the web for existing GPX loaders for Google Maps and found something similar to what I wanted to achieve, but eventually decided on a custom solution that would give me full control of the appearance of the data.  After looking through the Google Maps API Docs and Examples, and taking some hints from the afore mentioned GPX loader, I came up a simple JavaScript GPX loader to place GPX track data in Google Maps:

Basic GPX Loader for Google Maps
<script type="text/javascript">
 
// Object constructor for GPXTrack object
function GPXTrack(filename, color, width, alpha) {
  this.filename = filename;
  this.color = color;
  this.width = width;
  if (alpha) {
    this.alpha = alpha;
  }
}
 
// Function to add track data from a GPX file to a map.
// map: pointer to a GMap2 object
// track: pointer to a GPXTrack object
// centerandzoom: optional boolean variable
// zoomlevel: optional number, 0..19 for supported levels, -1 to auto compute
//             best-fit zoom level
// startmsg: optional string to be placed in an info bubble at initial track
//           point
function addGPXTrack(map, track, centerandzoom, zoomlevel, startmsg) {
  if (GBrowserIsCompatible()) {
    GDownloadUrl(track.filename, function(data, responseCode) {
      if (responseCode == 200) {
        var xml = GXml.parse(data);
        if (xml) {
 
          if (centerandzoom) {
            var bounds = xml.getElementsByTagName("bounds");
            if (bounds && bounds.length) {
              var gllbounds = new GLatLngBounds(new GLatLng(parseFloat(bounds[0].getAttribute("minlat")),
                                                            parseFloat(bounds[0].getAttribute("minlon"))),
                                                new GLatLng(parseFloat(bounds[0].getAttribute("maxlat")),
                                                            parseFloat(bounds[0].getAttribute("maxlon"))));
              if (zoomlevel && zoomlevel != -1) {
                map.setCenter(gllbounds.getCenter(), zoomlevel);
              } else {
                map.setCenter(gllbounds.getCenter(), map.getBoundsZoomLevel(gllbounds));
              }
            }
          }
 
          var trkpts = xml.documentElement.getElementsByTagName("trkpt");
          if (trkpts && trkpts.length) {
            var glatlngs = new Array(trkpts.length);
            for (var i = 0; i < trkpts.length; i++) {
              glatlngs[i] = new GLatLng(parseFloat(trkpts[i].getAttribute("lat")),
                                        parseFloat(trkpts[i].getAttribute("lon")));
            }
 
            if (track.alpha) {
              var polyline = new GPolyline(glatlngs, track.color, track.width, track.alpha);
            } else {
              var polyline = new GPolyline(glatlngs, track.color, track.width);
            }
            map.addOverlay(polyline);
 
            if (startmsg && startmsg.length) {
              map.openInfoWindow(glatlngs[0], document.createTextNode(startmsg));
            }
          } else {
            alert("GPX file contains no track points.");
          }
        } else {
          alert("Could not load GPX document " + filename);
        }
      } else if (responseCode == -1) {
        alert("Data request timed out.");
      } else {
        alert("Error loading file \"" + filename + "\"");
      }
    });
  }
}
 
function loadMaps() {
  var map = new GMap2(document.getElementById("crosscountry_canvas"));
  map.setUIToDefault();
  var track = {filename: "/files/gps/crosscountry.gpx", color: "#0000ff", width: 4};
  addGPXTrack(map, track, 1, 4, "Start of GPS Recording");
 
  map = new GMap2(document.getElementById("canoe_canvas"));
  map.setUIToDefault();
  track = {filename: "/files/gps/oldforge-activelog.gpx", color: "#0000ff", width: 4};
  addGPXTrack(map, track, 1, 9, "Start of Canoe Trip");
}
 
</script>
 
<script type="text/javascript">
window.onload = function() {
  loadMaps();
}
window.onunload = function() {
  GUnload();
}
</script>

This initial GPX Loader loads the trkpt elements from the specified GPX file into a GPolyline overlay object for the specified GMap2 object.  The trkpt elements are connected to form a line with a specified width and color.  The map is centered around the track and applies the specified zoom level, or automatically determines an appropriate zoom level if a value of -1 is specified.  An info bubble is optionally placed at the position of the first track point if an info message is provided.  This was a good start, but there was still much work to be done.  

The next step for the project was the addition of support for GPX waypoint elements.  This was fairly straight forward, with a GMarker created at the position of each waypoint.  An info bubble is attached to each waypoint element containing name or description sub-elements.  The name element, if specified, is used to create a title for the info buble, rendered as an H1 HTML heading.  The description element is used to specify the body of the message for the info bubble.  The info bubbles are setup to be displayed when its associated map marker is clicked.  

Once waypoint support was complete, support for multiple track styles was added to allow the visual representation of the "connectedness" of a track.  Each GPX file contains one or more track elements.  These track elements are composed of one ore more track segment sub-elements.  The track segment elements contain track point elements.  It is the track points that provide the actual GPS coordinates.  Styles were added to draw a continuous line where all track points are connected, a split line where all points in a track are connected but the tracks themselves are not connected, and a segmented line where all points in a track segment are connected but the track segments are not connected.  

Because a new track is created whenever the GPS is turned on and off, and new track segments seem to be created whenever the GPS loses and acquires a satellite signal, the inclusion of the split and segmented line styles allows the map to show periods of satellite signal loss and times when the GPS was turned off, which could indicate a stopping point on an extended trip.   

At this point, all of the planned functionality for the project was complete.  All that was left was to add of some waypoints to my GPX files and to clean the code up for general purpose use.  

Creating Waypoints

For my trip maps, I had all of the GPS track data that I needed, but did not actually have any waypoints to display.  I had not actually created any waypoints on either of my trips and wanted to create a few custom waypoints to mark significant locations from the trips.  During the course of the road trip, we had stopped at the highest points in five of the states through which we had crossed and I wanted to create a waypoint to mark the location of each of these high points.  I used EasyGPS to create the waypoints and, with a little additional editing with Notepad++ to get past limit EasyGPS places on the length of a waypoint name, I had waypoints placed at the appropriate positions with names to match their associated high points and descriptions complete with location information, elevation, and photographs.  

With the data from the canoe trip I wanted to mark the first and last point from each day of a seven day trip.  While I could have searched through the GPS file manually looking for the start and end points of each day, I decided instead to write a little Python script to perform this task for me.  The script loads all of the tracks from a GPX file, extracts the first and last point of each track, compares their times to find the first and last points of each day, and then writes GPX waypoint data for each point to the console for cutting and pasting into the GPX file.  Each waypoint contains its associated day number within its name, and position and time as its description:

Day Bounds Script
# Usage: python daybounds.py <gpx-filename>
__author__="Dustin Graves"
__date__ ="$Sep 30, 2009 9:09:07 PM$"
__license__="Python License"
__copyright__="Copyright (c) 2009, Dustin Graves"
__version__="1.0"
 
import sys
import re
import time
import xml.dom.minidom as minidom
 
# Convert the GPX trkpt ISO 8061 time string to struct_time object with
# UTC time converted to local timezone.  A UTC offset (in seconds) can be
# specified for the timezone which contains the data. If no offset is
# specified, the local timezone offset will be used.
def getLocalTime(timestr, utcoffset = None):
  # Get time object from ISO time string
  utctime = time.strptime(timestr, '%Y-%m-%dT%H:%M:%SZ')
 
  # Convert to seconds since epoch
  secs = time.mktime(utctime)
 
  if not utcoffset:
    # Get local timezone offset
    if time.localtime(secs).tm_isdst:
      utcoffset = time.altzone
      pass
    else:
      utcoffset = time.timezone
      pass
    pass
 
  return time.localtime(secs - utcoffset)
 
 
# Extract string value from DOM element
def getElementValue(element, tag):
  values = element.getElementsByTagName(tag)
  if values:
    return values[0].firstChild.data
    pass
  return None
 
 
# Get the first and last points from the track segments contained by a GPX file
def getTrackSegmentBounds(filename):
  # Parse the GPX file
  doc = minidom.parse(filename)
  root = doc.documentElement
 
  bounds = []
 
  # Get the track segments
  trksegs = root.getElementsByTagName('trkseg')
  for trkseg in trksegs:
    trkpts = trkseg.getElementsByTagName('trkpt')
    bounds.append({'start':trkpts[0], 'end':trkpts[-1]})
    pass
 
  return bounds
 
 
# Find the first and last track segment bound for each day.
# A UTC offset (in seconds) can be specified for the timezone which contains
# the data. If no offset is specified, the local timezone offset will be used.
# Currently assuming all days fall within the same month and all trkpts have a
# time element.
def getDayBounds(bounds, utcoffset = None):
  daybounds = []
 
  # Get first point with time field
  first = bounds[0]
  last = bounds[0]
  ts = getLocalTime(getElementValue(first['start'], 'time'), utcoffset)
  day = ts.tm_mday
 
  for bound in bounds[1:]:
    ts = getLocalTime(getElementValue(bound['start'], 'time'), utcoffset)
    if ts.tm_mday > day:
      daybounds.append({'start':first['start'], 'end':last['end']})
      first = bound
      day = ts.tm_mday
      pass
 
    last = bound
    pass
 
  # Last day bound is not processed by loop
  daybounds.append({'start':first['start'], 'end':last['end']})
 
  return daybounds
 
 
# Make a HTML description consisting of time, lat, and lon values
# Time is formatted like separated ISO time without timezone value
def makeDescription(trkpt, utcoffset = None):
  ts = getLocalTime(getElementValue(trkpt, 'time'), utcoffset)
  return '<![CDATA[<b>Time:</b> %s<br><b>Lat:</b> %s<br><b>Lon:</b> %s]]>' % (time.strftime('%a, %d %b %Y %I:%M:%S %p', ts), trkpt.getAttribute('lat'), trkpt.getAttribute('lon'))
 
 
# Print the start and end points of each day as GPX wpt elements
def printWaypoints(daybounds, utcoffset = None):
  # Regular expression to split XML around trkpt tag
  splitRe = re.compile('<trkpt (.*)</trkpt>', re.DOTALL|re.MULTILINE)
 
  # Now print the start and stop points as waypoints
  day = 1
  for bound in daybounds:
    start = bound['start']
    g = splitRe.search(start.toxml())
    print '<wpt %s<name>Start of Day %d</name>\n<desc>%s</desc>\n</wpt>' % (g.group(1), day, makeDescription(start, utcoffset))
 
    end = bound['end']
    g = splitRe.search(end.toxml())
    print '<wpt %s<name>End of Day %d</name>\n<desc>%s</desc>\n</wpt>' % (g.group(1), day, makeDescription(end, utcoffset))
 
    day = day + 1
    pass
  pass
 
 
if __name__ == "__main__":
  bounds = getTrackSegmentBounds(sys.argv[1])
  daybounds = getDayBounds(bounds)
  printWaypoints(daybounds)
  pass

Once this was done, it was time to reorganize the code.  

Cleaning up the Code

Now that my GPX loader was functionally complete, I decided to reorganize it to better integrate with the Google Maps API.  This involved repackaging the code as a JavaScript object, which I named GpxTrack, that inherited from the GOverlay object from the Google Maps API.  This way, the GpxTrack object can be added to and removed from a map using the GMap2 object's addOverlay function just like the GPolyline and GMarker objects that it contains:

GpxTrack JavaScript object for loading GPX file data into Google Maps
// Object constructor for GpxTrack object
// Only filename is required
function GpxTrack(filename, type, style, color, weight, opacity) {
  // Public members
  this.filename_ = filename;
  this.color_ = color;
  this.weight_ = weight;
  this.opacity_ = opacity;
 
 
  // Private members
  var type_ = type || GpxTrack.TYPE_TRACK_AND_WAYPOINTS;
  var style_ = style || GpxTrack.STYLE_CONTINUOUS;
  var status_ = GpxTrack.STATUS_NOTLOADED;
  var bounds_ = new GLatLngBounds();  // Track boundary obtained from GPX file's bounds element
  var tracks_ = [], lines_ = [], markers_ = [], listeners_ = [];
 
 
  // Private methods
  function makeBounds(xml) {
    var bounds = xml.documentElement.getElementsByTagName("bounds");
    if (bounds && bounds.length) {
      bounds_ = new GLatLngBounds(new GLatLng(parseFloat(bounds[0].getAttribute("minlat")),
                                              parseFloat(bounds[0].getAttribute("minlon"))),
                                  new GLatLng(parseFloat(bounds[0].getAttribute("maxlat")),
                                              parseFloat(bounds[0].getAttribute("maxlon"))));
    } else {
      bounds_ = new GLatLngBounds();  // Empty bounds when bounds element absent from GPX file
    }
  }
 
  function makeTrkptArray(trkseg) {
    var latlngs = [];
    var trkpts = trkseg.getElementsByTagName("trkpt");
    if (trkpts && trkpts.length) {
      for (var i = 0; i < trkpts.length; i++) {
        latlngs[i] = new GLatLng(parseFloat(trkpts[i].getAttribute("lat")),
                                 parseFloat(trkpts[i].getAttribute("lon")));
      }
    }
    return latlngs;
  }
 
  function makeTrksegArray(trk) {
    var segments = [];
    var trksegs = trk.getElementsByTagName("trkseg");
    if (trksegs && trksegs.length) {
      for (var i = 0; i < trksegs.length; i++) {
        segments[i] = makeTrkptArray(trksegs[i]);
      }
    }
    return segments;
  }
 
  function makeTrkArray(xml) {
    var trks = xml.documentElement.getElementsByTagName("trk");
    if (trks && trks.length) {
      tracks_ = [];  // Clear any existing points
      for (var i = 0; i < trks.length; i++) {
        tracks_[i] = makeTrksegArray(trks[i]);
      }
    }
  }
 
  // Create GPolyline object(s) from track data
  function makePolylines() {
    var latlngs = [];
    lines_ = [];  // Clear any existing lines
 
    // Track loop
    for (var i = 0; i < tracks_.length; i++) {
      // Track segment loop
      for (var j = 0; j < tracks_[i].length; j++) {
        // Track point loop
        for (var k = 0; k < tracks_[i][j].length; k++) {
          latlngs.push(tracks_[i][j][k]);
        }
 
        // If style is segmented, create a new GPolyline
        if (style_ === GpxTrack.STYLE_SEGMENTED) {
          lines_.push(new GPolyline(latlngs, this.color_, this.weight_, this.opacity_));
          latlngs = [];
        }
      }
 
      // If style is split, create a new GPolyline
      if (style_ === GpxTrack.STYLE_SPLIT) {
        lines_.push(new GPolyline(latlngs, this.color_, this.weight_, this.opacity_));
        latlngs = [];
      }
    }
 
    // If style is segmented, create a new GPolyline
    if (style_ === GpxTrack.STYLE_CONTINUOUS) {
      lines_.push(new GPolyline(latlngs, this.color_, this.weight_, this.opacity_));
    }
  }
 
  // Create clickable marker with popup message
  function makeMessageMarker(glatlng, message) {
    var marker = new GMarker(glatlng);
    var listener = GEvent.addListener(marker, "click", function () {
      marker.openInfoWindowHtml(message);
    });
    return {marker: marker, listener: listener};
  }
 
  // Create clickable markers with popup messages from GPX waypoint elements
  function makeWptArrays(xml) {
    markers_ = [];  // Clear any existing markers
    for (var j = 0; j < listeners_.length; j++) { GEvent.removeListener(listeners_[j]); }  // Clear listeners
    listeners_ = [];
    var wpts = xml.documentElement.getElementsByTagName("wpt");
    if (wpts && wpts.length) {
      for (var i = 0; i < wpts.length; i++) {
        var name = GXml.value(wpts[i].getElementsByTagName("name")[0]);
        var desc = GXml.value(wpts[i].getElementsByTagName("desc")[0]);
        if ((name && name.length) || (desc && desc.length)) {
          var set = makeMessageMarker(new GLatLng(parseFloat(wpts[i].getAttribute("lat")), parseFloat(wpts[i].getAttribute("lon"))),
                                      "<h1>" + name + "</h1>" + desc + "<br>");
          markers_.push(set.marker);
          listeners_.push(set.listener);
        } else {
          markers_.push(new GMarker(new GLatLng(parseFloat(wpts[i].getAttribute("lat")), parseFloat(wpts[i].getAttribute("lon")))));
        }
      }
    }
  }
 
  // Make the encapsulated GOverlay objects from the specified GXml object
  function make(xml) {
    makeBounds(xml);
 
    if (type_ & GpxTrack.TYPE_TRACK) {
      makeTrkArray(xml);
      if (tracks_.length) {
        makePolylines();
      }
    }
 
    if (type_ & GpxTrack.TYPE_WAYPOINTS) {
      makeWptArrays(xml);
    }
  }
 
 
  // Privileged methods
  this.addLinesToMap = function () {
    for (var i = 0; i < lines_.length; i++) {
      this.map_.addOverlay(lines_[i]);
    }
  };
 
  this.removeLinesFromMap = function () {
    for (var i = 0; i < lines_.length; i++) {
      this.map_.removeOverlay(lines_[i]);
    }
  };
 
  this.addMarkersToMap = function () {
    for (var i = 0; i < markers_.length; i++) {
      this.map_.addOverlay(markers_[i]);
    }
  };
 
  this.removeMarkersFromMap = function () {
    for (var i = 0; i < markers_.length; i++) {
      this.map_.removeOverlay(markers_[i]);
    }
  };
 
  // Change the track line's draw style
  this.setStyle = function (style) {
    if (type_ & GpxTrack.TYPE_TRACK) {
      if (this.map_) {
        this.removeLinesFromMap();
      }
 
      style_ = style;
      makePolylines();
 
      if (this.map_) {
        this.addLinesToMap();
      }
    }
  };
 
  this.onLoad = function (data, responseCode) {
    if (responseCode == 200) {
      var xml = GXml.parse(data);
      if (xml && xml.documentElement) {
        make(xml);
        status_ = GpxTrack.STATUS_LOADED;
      } else {
        status_ = GpxTrack.STATUS_ERROR;
      }
    } else {
      status_ = GpxTrack.STATUS_ERROR;
    }
  };
 
  // Use GDownloadUrl to retrieve the GPX file and create the encapsulated
  // GOverlay objects
  this.startLoad = function(onload) {
    status_ = GpxTrack.STATUS_LOADING;
    var that = this;
    GDownloadUrl(this.filename_, function (data, responseCode) {
      that.onLoad(data, responseCode);
      onload(that, responseCode);
    });
  };
 
  this.getType = function () { return type_; };
  this.getStyle = function () { return style_; };
  this.getStatus = function () { return status_; };
  this.getBounds = function () { return bounds_; };
  this.getCenter = function () { return bounds_.getCenter(); };
  this.getPolylines = function () { return lines_; };
  this.getMarkers = function () { return markers_; };
  this.getListeners = function () { return listeners_; };
}  // GpxTrack
 
 
// Class constants
 
// Status codes
GpxTrack.STATUS_NOTLOADED = 0x00;
GpxTrack.STATUS_LOADING   = 0x01;
GpxTrack.STATUS_LOADED    = 0x02;
GpxTrack.STATUS_ERROR     = 0x03;
 
// Track data display types
GpxTrack.TYPE_TRACK               = 0x01;  // Draw the track as a polyline
GpxTrack.TYPE_WAYPOINTS           = 0x02;  // Draw the waypoints as markers
GpxTrack.TYPE_TRACK_AND_WAYPOINTS = (GpxTrack.TYPE_TRACK|GpxTrack.TYPE_WAYPOINTS);
 
// Track draw styles
GpxTrack.STYLE_CONTINUOUS = 0x01;  // Connect all points in file
GpxTrack.STYLE_SPLIT      = 0x02;  // Connect all points in a track, but do not connect separate tracks
GpxTrack.STYLE_SEGMENTED  = 0x03;  // Connect all points in a track segment, but do not connect separate segments
 
 
// Extend the GOverlay object
GpxTrack.prototype = new GOverlay();
 
// Add the encapsulate overlay objects to the map
GpxTrack.prototype.initialize = function(map) {
  // If data is loading, wait for it to finish
  while (this.getStatus() === GpxTrack.STATUS_LOADING) {}
 
  // Only process overlays if the data has been loaded
  if (this.getStatus() === GpxTrack.STATUS_LOADED) {
    this.map_ = map;
    this.addLinesToMap();
    this.addMarkersToMap();
  }
};
 
// Remove the encapsulate overlay objects from the map
GpxTrack.prototype.remove = function() {
  // Only process overlays if the data has been loaded
  if (this.getStatus() === GpxTrack.STATUS_LOADED) {
    this.removeLinesFromMap();
    this.removeMarkersFromMap();
    this.map_ = null;
  }
};
 
// Copy data to a new object; will be invalid until load() is invoked
GpxTrack.prototype.copy = function() {
  return new GpxTrack(this.filename_, this.getType(), this.getStyle(),
                      this.color_, this.weight_, this.opacity_);
};
 
// Object does not draw anything, only manages other overlays
GpxTrack.prototype.redraw = function(force) {
};
 
// New function to load asynchronously GPX file. When the load is comlete the
// onload function will be called with the GpxTrack object as the first
// argument, and the HTTP response status of the request for the object's GPX
// file as the second argument.  If the load times out, the first argument will
// be the GpxTrack object with the status set to GpxTrack.STATUS_ERROR and the
// second argument will be -1.  The object can only be loaded once.
// The onload function can be used to add the GpxTrack object to a map after
// load has completed.  Adding the GpxTrack object to a map before it has
// completed loading will cause the program to block until the load is complete.
GpxTrack.prototype.loadFile = function(onload) {
  if (this.getStatus() === GpxTrack.STATUS_NOTLOADED) {
    this.startLoad(onload);
  }
};
 
// Change the object, if the object has loaded succesfully
GpxTrack.prototype.changeStyle = function(style) {
  if (this.getStatus() === GpxTrack.STATUS_LOADED) {
    this.setStyle(style);
  }
};

Things were looking pretty good, but there was still one thing that I thought was missing.  

Creating a Custom Track Style Control

As soon as the GpxTrack object was done, at which point the project should have been complete, I started to feel the urge to create a custom Google Maps control capable of changing a GpxTrack object's track draw style.  This turned out to be fairly simple and really only involved the creation of a GpxTrackStyleControl object, which inherited from the GControl object of the Google Maps API, to draw a few buttons on a map which change an associated GpxTrack object's track draw style when clicked:

GpxTrackStyleControl JavaScript object for controlling GpxTrack object style
// Object constructor for GpxTrackStyleControl to change the style of
// a GpxTrack object
function GpxTrackStyleControl(track) {
  this.track_ = track;
}
 
// Extend the GControl object
GpxTrackStyleControl.prototype = new GControl();
 
// Create DIV elements for each button and places them in a container
// DIV which is returned as the control element.
GpxTrackStyleControl.prototype.initialize = function(map) {
  var that = this;  // For closures
  var container = document.createElement("div");
 
  // Create the buttons; current style has bold text
  var continuousStyleDiv = document.createElement("div");
  this.setButtonStyle_(continuousStyleDiv);
  continuousStyleDiv.appendChild(document.createTextNode("Continuous"));
  if (that.track_.getStyle() === GpxTrack.STYLE_CONTINUOUS) {
    continuousStyleDiv.style.fontWeight = "bold";
  }
  container.appendChild(continuousStyleDiv);
 
  var splitStyleDiv = document.createElement("div");
  this.setButtonStyle_(splitStyleDiv);
  splitStyleDiv.appendChild(document.createTextNode("Split"));
  if (that.track_.getStyle() === GpxTrack.STYLE_SPLIT) {
    splitStyleDiv.style.fontWeight = "bold";
  }
  container.appendChild(splitStyleDiv);
 
  var segmentedStyleDiv = document.createElement("div");
  this.setButtonStyle_(segmentedStyleDiv);
  segmentedStyleDiv.appendChild(document.createTextNode("Segmented"));
  if (that.track_.getStyle() === GpxTrack.STYLE_SEGMENTED) {
    segmentedStyleDiv.style.fontWeight = "bold";
  }
  container.appendChild(segmentedStyleDiv);
 
  // Create the listeners to perform button clicks
  GEvent.addDomListener(continuousStyleDiv, "click", function() {
    continuousStyleDiv.style.fontWeight = "bold";
    splitStyleDiv.style.fontWeight = "normal";
    segmentedStyleDiv.style.fontWeight = "normal";
    that.track_.changeStyle(GpxTrack.STYLE_CONTINUOUS);
  });
 
  GEvent.addDomListener(splitStyleDiv, "click", function() {
    continuousStyleDiv.style.fontWeight = "normal";
    splitStyleDiv.style.fontWeight = "bold";
    segmentedStyleDiv.style.fontWeight = "normal";
    that.track_.changeStyle(GpxTrack.STYLE_SPLIT);
  });
 
  GEvent.addDomListener(segmentedStyleDiv, "click", function() {
    continuousStyleDiv.style.fontWeight = "normal";
    splitStyleDiv.style.fontWeight = "normal";
    segmentedStyleDiv.style.fontWeight = "bold";
    that.track_.changeStyle(GpxTrack.STYLE_SEGMENTED);
  });
 
  map.getContainer().appendChild(container);
  return container;
};
 
// Default placement is bottom right corner of map with 7 pixels of padding
GpxTrackStyleControl.prototype.getDefaultPosition = function() {
  return new GControlPosition(G_ANCHOR_BOTTOM_RIGHT, new GSize(7, 7));
};
 
// Set the CSS for the given button element
GpxTrackStyleControl.prototype.setButtonStyle_ = function(button) {
  button.style.color = "#000000";
  button.style.backgroundColor = "white";
  button.style.font = "small Arial";
  button.style.border = "1px solid black";
  button.style.padding = "2px";
  button.style.marginBottom = "3px";
  button.style.textAlign = "center";
  button.style.width = "6em";
  button.style.cursor = "pointer";
//  button.style.position = "relative";
//  button.style.display = "inline";
//  button.style.margin = "1px";
}

My work with the Google Maps API was complete (for now), and my maps were ready for publication.  

Putting the Maps Online

Since I am currently using Drupal to manage my site, I needed to find a way to embed my Google Maps within Drupal.  Again I asked GOOG for advice and found the answer in a blog posting.  The answer, essentially, was to use the 'drupal_set_html_head' function.  Knowing this I created two separate files, one to create my maps, and the other to embed them within Drupal:

Loading the maps
<script src="/files/gps/gpxtools.js"
        type="text/javascript"></script>
<script type="text/javascript">
function loadMap(map, trip, zoom, message) {
  trip.loadFile(function (track, responseCode) {
    if (responseCode == 200) {
      if (track.getStatus() === GpxTrack.STATUS_LOADED) {
        map.setCenter(track.getCenter(),
                      (zoom == -1) ? map.getBoundsZoomLevel(track.getBounds()) : zoom);
        map.addOverlay(track);
        map.openInfoWindowHtml(track.getPolylines()[0].getVertex(0), message);
      } else {
        alert("Could not load GPX document \"" + track.filename_ + "\"");
        status_ = GpxTrack.STATUS_ERROR;
      }
    } else if (responseCode == -1) {
      alert("Data request timed out: GET \"" + track.filename_ + "\".");
      status_ = GpxTrack.STATUS_ERROR;
    } else {
      alert("Error loading file \"" + track.filename_ + "\"");
      status_ = GpxTrack.STATUS_ERROR;
    }
  });
}
 
function loadMaps() {
  var map = new GMap2(document.getElementById("crosscountry_canvas"));
  var trip = new GpxTrack("/files/gps/crosscountry.gpx", GpxTrack.TYPE_TRACK_AND_WAYPOINTS, GpxTrack.STYLE_CONTINUOUS, "#0000ff", 4);
  map.setUIToDefault();
  loadMap(map, trip, 4, "<h1>Start of GPS Recording</h1>");
 
  map = new GMap2(document.getElementById("canoe_canvas"));
  trip = new GpxTrack("/files/gps/oldforge.gpx", GpxTrack.TYPE_TRACK_AND_WAYPOINTS, GpxTrack.STYLE_CONTINUOUS, "#0000ff", 4);
  map.setUIToDefault();
  map.addControl(new GpxTrackStyleControl(trip),
                 new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(7, 33)));  // Control will be ineffective until track loads
  loadMap(map, trip, 9, "<h1>Start of Canoe Trip</h1>");
}
 
</script>
 
<script type="text/javascript">
window.onload = function() {
  loadMaps();
}
window.onunload = function() {
  GUnload();
}
</script>

Embedding the maps
<?php
drupal_set_html_head(file_get_contents('files/gps/loadgpx.inc'));
?>
 
<?php
print('
<h1>Cross Country Drive</h1><div align="center" id="crosscountry_canvas" style="width: 100%; height: 600px"></div><br>
');
 
print('
<h1>Adirondack Canoe Trip from Old Forge, NY to Saranac Lake</h1><div align="center" id="canoe_canvas" style="width: 100%; height: 600px"></div><br>
');
?>

Once the maps were published I was ready to move on to my next idea for creating KML/KMZ files containing data from GPX files bundled with photographs as placemarks located at the approximate position at which they were taken for display in Google Earth, to be described by a future post.  

Relevant Files

Here are links to the files containing all of the JavaScript and Python code presented by this post:

 

Project: