
//// GMAPGpx -- represents a GPX XML object.

// Detect if the browser is MSIE.
// Borrowed from http://snipplr.com/view/132/detect-ie/
function isIE()
{
  return /msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent);
}

// Constructor.
function GMAPGpx(xmlDoc, elevationAdjPoints)
{
  this.xmlDoc = xmlDoc;

  // Initialise the tracks as empty.
  this.totalAscent = 0.0;
  this.totalDescent = 0.0;
  this.minlat = null;
  this.minlon = null;
  this.maxlat = null;
  this.maxlon = null;
  this.tracks = this.ParseTracks(xmlDoc.documentElement, elevationAdjPoints);
  this.waypoints = this.ParseWaypoints(xmlDoc.documentElement);
}

// Parse track data from a GPX xml document.
GMAPGpx.prototype.ParseTracks = function(xDoc, elevationAdjPoints)
{
  var tracks = [];

  // Get a list of trk elements.
  var xTracks = xDoc.getElementsByTagName("trk");

  // Convert each element to a parsed track object.
  for(var n = 0; n < xTracks.length; n++)
  {
    var currentTrack = new GMAPGpxTrack(xTracks.item(n), elevationAdjPoints);
    tracks.push(currentTrack);
    this.totalAscent += currentTrack.GetTotalAscent();
    this.totalDescent += currentTrack.GetTotalDescent();
  }

  this.minlat = GetMinNotNull(this.minlat, GetMinValue(tracks, GetMinLat));
  this.minlon = GetMinNotNull(this.minlat, GetMinValue(tracks, GetMinLon));
  this.maxlat = GetMaxNotNull(this.minlat, GetMaxValue(tracks, GetMaxLat));
  this.maxlon = GetMaxNotNull(this.minlat, GetMaxValue(tracks, GetMaxLon));

  return tracks;
}

GMAPGpx.prototype.ParseWaypoints = function(xDoc)
{
  var waypoints = [];
  
  // Get a list of wpt elements.
  var xWaypoints = xDoc.getElementsByTagName("wpt");
  
  // Convert each element to a parsed waypoint object.
  for(var n=0; n < xWaypoints.length; n++)
  {
    var currentWaypoint = new GMAPGpxWaypoint(xWaypoints.item(n));
    waypoints.push(currentWaypoint);
  }

  this.minlat = GetMinNotNull(this.minlat, GetMinValue(waypoints, GetMinLat));
  this.minlon = GetMinNotNull(this.minlat, GetMinValue(waypoints, GetMinLon));
  this.maxlat = GetMaxNotNull(this.minlat, GetMaxValue(waypoints, GetMaxLat));
  this.maxlon = GetMaxNotNull(this.minlat, GetMaxValue(waypoints, GetMaxLon));
  return waypoints;
}

GMAPGpx.prototype.GetMinLat = function()
{
  return this.minlat;
}
GMAPGpx.prototype.GetMinLon = function()
{
  return this.minlon;
}
GMAPGpx.prototype.GetMaxLat = function()
{
  return this.maxlat;
}
GMAPGpx.prototype.GetMaxLon = function()
{
  return this.maxlon;
}

// Gets the added total distance of the gpx file.
GMAPGpx.prototype.GetDistance = function()
{
  var runningTotal = 0;
  for(var n = 0; n < this.trackData.length; n++)
  {
    runningTotal += this.trackData[n].GetDistance();
  }
  return runningTotal;
}

// Gets the total ascent, in metres.
GMAPGpx.prototype.GetTotalAscent = function()
{
  return this.totalAscent;
}

// Gets the total descent, in metres.
GMAPGpx.prototype.GetTotalDescent = function()
{
  return this.totalDescent;
}

// Gets the track objects.
GMAPGpx.prototype.GetTracks = function()
{
  return this.tracks;
}

// Gets the waypoint objects.
GMAPGpx.prototype.GetWaypoints = function()
{
  return this.waypoints;
}

///////////////////////////////////////////////////

//// GMAPGpxWaypoint -- represents a GPX waypoint object.
function GMAPGpxWaypoint(xWaypoint)
{
  var lat = parseFloat(xWaypoint.getAttribute("lat"));
  var lon = parseFloat(xWaypoint.getAttribute("lon"));

  // Store the position as a GLatLng object.
  this.pos = new GLatLng(lat, lon);
  this.ele = null;
  this.name = null;
  this.comment = null;
  this.desc = null;
  this.sym = null;

  // Get the elevation.
  var elVal = GetInnerTextFC(xWaypoint, "ele");
  if(null != elVal) {
    this.ele = parseFloat(elVal);
  }
  
  // Get the name.
  var nVal = GetInnerTextFC(xWaypoint, "name");
  if(null != nVal) {
    this.name = nVal;
  }

  // Get the comment.
  var cVal = GetInnerTextFC(xWaypoint, "cmt");
  if(null != cVal) {
    this.comment = cVal;
  }

  // Get the description.
  var dVal = GetInnerTextFC(xWaypoint, "desc");
  if(null != dVal) {
    this.desc = dVal;
  }

  // Get the symbol.
  var sVal = GetInnerTextFC(xWaypoint, "sym");
  if(null != sVal) {
    this.sym = sVal;
  }

}

// Get the name of this waypoint.
GMAPGpxWaypoint.prototype.GetName = function()
{
  return this.name;
}

// Get the description of this waypoint.
GMAPGpxWaypoint.prototype.GetDescription = function()
{
  return this.desc;
}

// Get the position of this waypoint.
GMAPGpxWaypoint.prototype.GetPosition = function()
{
  return this.pos;
}

// Get the elevation of this waypoint in metres.
GMAPGpxWaypoint.prototype.GetElevation = function()
{
  return this.ele;
}

GMAPGpxWaypoint.prototype.GetSym = function()
{
  return this.sym;
}

///////////////////////////////////////////////////

//// GMAPGpxTrack -- represents a GPX track object.
function GMAPGpxTrack(xTrack, elevationAdjPoints)
{
  this.totalAscent = 0.0;
  this.totalDescent = 0.0;
  this.name = null;
  this.minlat = null;
  this.minlon = null;
  this.maxlat = null;
  this.maxlon = null;

  // Get the track name, if any.
  var elName = GetInnerTextFC(xTrack, "name");
  if(null != elName) {
    this.name = elName;
  }

  this.segData = this.ParseSegments(xTrack, elevationAdjPoints);
}

GMAPGpxTrack.prototype.ParseSegments = function(xTrack, elevationAdjPoints)
{
  var segs = [];
  
  // Get a list of trkseg elements.
  var xSegs = xTrack.getElementsByTagName("trkseg");
  
  for(var n = 0; n < xSegs.length; n++)
  {
    var currentSeg = new GMAPGpxSeg(xSegs[n], elevationAdjPoints);
    segs.push(currentSeg);

    // Update total ascent and descent.
    this.totalAscent += currentSeg.GetTotalAscent();
    this.totalDescent += currentSeg.GetTotalDescent();

  }

  this.minlat = GetMinValue(segs, GetMinLat);
  this.minlon = GetMinValue(segs, GetMinLon);
  this.maxlat = GetMaxValue(segs, GetMaxLat);
  this.maxlon = GetMaxValue(segs, GetMaxLon);

  return segs;
}

GMAPGpxTrack.prototype.GetName = function()
{
  return this.name;
}
GMAPGpxTrack.prototype.GetMinLat = function()
{
  return this.minlat;
}
GMAPGpxTrack.prototype.GetMinLon = function()
{
  return this.minlon;
}
GMAPGpxTrack.prototype.GetMaxLat = function()
{
  return this.maxlat;
}
GMAPGpxTrack.prototype.GetMaxLon = function()
{
  return this.maxlon;
}


// Gets the added total distance of the track, in metres.
GMAPGpxTrack.prototype.GetDistance = function()
{
  var runningTotal = 0;
  for(var n = 0; n < this.segData.length; n++)
  {
    runningTotal += this.segData[n].GetDistance();
  }
  return runningTotal;
}

// Gets the total ascent, in metres.
GMAPGpxTrack.prototype.GetTotalAscent = function()
{
  return this.totalAscent;
}

// Gets the total descent, in metres.
GMAPGpxTrack.prototype.GetTotalDescent = function()
{
  return this.totalDescent;
}

// Gets the segments for this track.
GMAPGpxTrack.prototype.GetSegs = function()
{
  return this.segData;
}

///////////////////////////////////////////////////

//// GMAPGpxSeg -- represents a GPX segment object.
function GMAPGpxSeg(xSeg, elevationAdjPoints)
{
  this.totalAscent = 0.0;
  this.totalDescent = 0.0;
  this.minlat = null;
  this.minlon = null;
  this.maxlat = null;
  this.maxlon = null;
  this.tpsData = this.ParseTrackpoints(xSeg, elevationAdjPoints);
}

GMAPGpxSeg.prototype.ParseTrackpoints = function(xSeg, elevationAdjPoints)
{
  var tps = [];

  // Get a list of trkpt elements.
  var xTps = xSeg.getElementsByTagName("trkpt");

  var currentPoint = null;
  for(var n = 0; n < xTps.length; n++)
  {
    // Add this point, providing the reference to the previous point (if any).
    currentPoint = new GMAPGpxTrackpoint(xTps[n], currentPoint, elevationAdjPoints);
    tps.push(currentPoint);

  }

  // Now count total ascent and descent.
  // This is done after all the trackpoints have been read, so it's possible
  // to use the adjusted elevations which are averaged from both sides of the
  // current trackpoint.
  var oldEle = null;
  if(tps.length > 0) { oldEle = tps[0].GetAdjustedElevation(); }
  for(var n = 1; n < tps.length; n++)
  {
    var currentPoint = tps[n];

    // Count any ascent or descent.
    var newEle = currentPoint.GetAdjustedElevation();
/*
    // Get the elevation of the existing elevation, if any, before parsing the next one.
    var oldEle = null;
    if(null != currentPoint) {
      oldEle = currentPoint.GetElevation();
    }
*/
    if(null != oldEle && null != newEle) {
      if(newEle > oldEle) {
        // Going up.
        this.totalAscent += (newEle - oldEle);
      } else {
        // Going down (or 0 change).
        this.totalDescent += (oldEle - newEle);
      }
    }
    oldEle = newEle;

  }

  // Update min/max latitude and longitude.
  this.minlat = GetMinValue(tps, GetPointLat);
  this.minlon = GetMinValue(tps, GetPointLon);
  this.maxlat = GetMaxValue(tps, GetPointLat);
  this.maxlon = GetMaxValue(tps, GetPointLon);

  return tps;
}


GMAPGpxSeg.prototype.GetMinLat = function()
{
  return this.minlat;
}
GMAPGpxSeg.prototype.GetMinLon = function()
{
  return this.minlon;
}
GMAPGpxSeg.prototype.GetMaxLat = function()
{
  return this.maxlat;
}
GMAPGpxSeg.prototype.GetMaxLon = function()
{
  return this.maxlon;
}

// Gets the added total distance of the segment, in metres.
GMAPGpxSeg.prototype.GetDistance = function()
{
  var runningTotal = 0;
  for(var n = 0; n < this.tpsData.length; n++)
  {
    runningTotal += this.tpsData[n].GetDistance();
  }
  return runningTotal;
}

// Gets the total ascent, in metres.
GMAPGpxSeg.prototype.GetTotalAscent = function()
{
  return this.totalAscent;
}

// Gets the total descent, in metres.
GMAPGpxSeg.prototype.GetTotalDescent = function()
{
  return this.totalDescent;
}

GMAPGpxSeg.prototype.GetTrackpoints = function()
{
  return this.tpsData;
}
///////////////////////////////////////////////////

//// GMAPGpxTrackpoint -- represents a GPX trackpoint object.
function GMAPGpxTrackpoint(xTp, prevPoint, elevationAdjPoints)
{
  var lat = parseFloat(xTp.getAttribute("lat"));
  var lon = parseFloat(xTp.getAttribute("lon"));

  this.prevPoint = prevPoint;
  this.nextPoint = null;
  if(null != this.prevPoint) {
    this.prevPoint.nextPoint = this;
  }

  // Store the position as a GLatLng object.
  this.pos = new GLatLng(lat, lon);
  this.ele = null;
  this.time = null;

  // Get the elevation.
  var elVal = GetInnerTextFC(xTp, "ele");
  if(null != elVal) {
    this.ele = parseFloat(elVal);
  }
  
  // Get the time.
  var tVal = GetInnerTextFC(xTp, "time");
  if(null != tVal) {
    this.time = Date.parse(tVal);
  }
  
  // Set a dummy adjusted elevation. (To be set properly when it's first requested.)
  this.aEle = null;
  this.elevationAdjPoints = elevationAdjPoints;
}

// Hacky utility function to get the inner text of the
// first element of the specified name found under the
// specified parent.
function GetInnerTextFC(parentEl, elName)
{
  var xElC = parentEl.getElementsByTagName(elName);
  var xe;
  if(xElC.length >= 1)
  {
    // Get the first item, normalise it, get the first child, then take the value.
    xe = xElC.item(0);
    xe.normalize();
    xe = xe.firstChild;
    if(null != xe) {
      return xe.nodeValue;
    } else {
      return null;
    }
  }
}

function GetMinLat(o) { return o.minlat; }
function GetMinLon(o) { return o.minlon; }
function GetMaxLat(o) { return o.maxlat; }
function GetMaxLon(o) { return o.maxlon; }
function GetPointLat(point) { return point.GetPosition().lat(); }
function GetPointLon(point) { return point.GetPosition().lng(); }

function GetDelValue(objArr, extractorDeleg, deciderDeleg)
{
  var currentVal = null;
  for(var n=0; n<objArr.length; n++)
  {
    if(null == currentVal) { currentVal = extractorDeleg(objArr[n]); }
    else { currentVal = deciderDeleg(currentVal, extractorDeleg(objArr[n])); }
  }
  return currentVal;
}

function GetMinValue(objArr, deleg) { return GetDelValue(objArr, deleg, Math.min); }
function GetMaxValue(objArr, deleg) { return GetDelValue(objArr, deleg, Math.max); }
function GetMinNotNull(a, b) {
  if(null == a) { return b; }
  if(null == b) { return a; }
  return Math.min(a, b);
}
function GetMaxNotNull(a, b) {
  if(null == a) { return b; }
  if(null == b) { return a; }
  return Math.max(a, b);
}
function RoundNumber(num, places)
{
  return Math.round(num * Math.pow(10, places)) / Math.pow(10, places);
}

// Calculates the distance, in metres, between this point and the previous point (if any).
// Assumes a spherical Earth, which should be sufficient unless the points are
// a very long way apart.
GMAPGpxTrackpoint.prototype.GetDistance = function()
{
  if(null == this.prevPoint)
  {
    // Return 0 if we're at the start.
    return 0;
  }
  var thisPos = this.pos;
  var lastPos = this.prevPoint.pos;
  
  return thisPos.distanceFrom(lastPos);
}

// Gets the position of this point, as a GLatLong object.
GMAPGpxTrackpoint.prototype.GetPosition = function()
{
  return this.pos;
}

// Gets the elevation of this point, in metres.
// Returns null if no elevation has been specified.
GMAPGpxTrackpoint.prototype.GetElevation = function()
{
  return this.ele;
}

GMAPGpxTrackpoint.prototype.GetAdjustedElevation = function()
{
  if(null == this.aEle && null != this.elevationAdjPoints) {
    // Figure out the adjusted elevation.
    var eitherSide = this.elevationAdjPoints / 2;
    var elevations = [];
    if(this.ele != null)
    {
      elevations.push(this.GetElevation());
    }
    
    // Look on either side of this point to get elevations.
    var floater = this;
    for(var n = 0; n < eitherSide; n++)
    {
      floater = this.prevPoint;
      if(null == floater) { break; }
      if(null != floater.GetElevation()) { elevations.push(floater.GetElevation()); }
    }
    floater = this;
    for(var n = 0; n < eitherSide; n++)
    {
      floater = this.nextPoint;
      if(null == floater) { break; }
      if(null != floater.GetElevation()) { elevations.push(floater.GetElevation()); }
    }

    // Now take the average.
    if(elevations.length > 0)
    {
      var total =  0.0;
      for(var o = 0; o < elevations.length; o++)
      {
        total += elevations[o];
      }
      this.aEle = total / elevations.length;
    }
  }
  return this.aEle;
}

///////////////////////////////////////////////////
//// GMAPPainterWaypoint -- represents a waypoint that's been drawn on the map.
function GMAPPainterWaypoint(painter, waypoint, overlay, preferredSelectorColour, isVisible)
{
  this.painter = painter;
  this.waypoint = waypoint;
  this.overlay = overlay;
  this.preferredSelectorColour = preferredSelectorColour;
  this.isVisible = isVisible;
}

// Gets the preferred selector colour.
GMAPPainterTrack.prototype.GetPreferredSelectorColour = function()
{
  return this.preferredSelectorColour;
}

// Returns true if visible, otherwise false.
GMAPPainterWaypoint.prototype.IsVisible = function()
{
  return this.isVisible;
}

// Shows the waypoint.
GMAPPainterWaypoint.prototype.Show = function()
{
  if(!this.isVisible) {
    this.overlay.show();
    this.isVisible = true;
  }
}

// Hides the waypoint.
GMAPPainterWaypoint.prototype.Hide = function()
{
  if(this.isVisible) {
    this.overlay.hide();
    this.isVisible = false;
  }
}

///////////////////////////////////////////////////
//// GMAPPainterTrack -- represents a track that's been drawn on the map.
function GMAPPainterTrack(painter, track, segOverlays, preferredSelectorColour, isVisible)
{
  this.painter = painter;
  this.track = track;
  this.segOverlays = segOverlays;
  this.preferredSelectorColour = preferredSelectorColour;
  this.isVisible = isVisible;
}

// Gets the track object.
GMAPPainterTrack.prototype.GetTrack = function()
{
  return this.track;
}

// Gets the preferred selector colour.
GMAPPainterTrack.prototype.GetPreferredSelectorColour = function()
{
  return this.preferredSelectorColour;
}

// Returns true if visible, otherwise false.
GMAPPainterTrack.prototype.IsVisible = function()
{
  return this.isVisible;
}

// Shows the track.
GMAPPainterTrack.prototype.Show = function()
{
  if(!this.isVisible) {
    for(var n = 0; n < this.segOverlays.length; n++) {
      var cso = this.segOverlays[n].overlay;
      cso.show();
    }
    this.isVisible = true;
  }
}

// Hides the track.
GMAPPainterTrack.prototype.Hide = function()
{
  if(this.isVisible) {
    for(var n = 0; n < this.segOverlays.length; n++) {
      var cso = this.segOverlays[n].overlay;
      cso.hide();
    }
    this.isVisible = false;
  }
}


///////////////////////////////////////////////////
//// GMAPPainter -- paints a track onto a Google Map.
function GMAPPainter(map)
{
  this.map = map;
  this.minlat = null;
  this.minlon = null;
  this.maxlat = null;
  this.maxlon = null;
  this.waypointOverlays = [];
  this.trackOverlays = [];
  this.iconmap = new Object;
}

// Paints a segment on the map.
// Returns the GPolyline overlay object that was added.
GMAPPainter.prototype.PaintSegment = function(seg, colour, width)
{
  var tPoints = seg.GetTrackpoints();
  var points = [];
  for(var n = 0; n < tPoints.length; n++)
  {
    points.push(tPoints[n].GetPosition());
  }

  // Update min/max latitude and longitude.
  if(null == this.minlat) { this.minlat = seg.GetMinLat(); } else { this.minlat = Math.min(this.minlat, seg.GetMinLat()); }
  if(null == this.minlon) { this.minlon = seg.GetMinLon(); } else { this.minlon = Math.min(this.minlon, seg.GetMinLon()); }
  if(null == this.maxlat) { this.maxlat = seg.GetMaxLat(); } else { this.maxlat = Math.max(this.maxlat, seg.GetMaxLat()); }
  if(null == this.maxlon) { this.maxlon = seg.GetMaxLon(); } else { this.maxlon = Math.max(this.maxlon, seg.GetMaxLon()); }

  var pLine = new GPolyline(points, colour, width);
  this.map.addOverlay(pLine);

  return pLine;
}

GMAPPainter.prototype.SetSymbolIcon = function(symname, gicon)
{
  this.iconmap[symname] = gicon;
}

// Paints a track on the map.
GMAPPainter.prototype.PaintTrack = function(track, colour, width)
{
  var segOverlays = [];
  var segs = track.GetSegs();
  for(var n = 0; n < segs.length; n++)
  {
    var segOverlay = this.PaintSegment(segs[n], colour, width);

    var segMem = new Object();
    segMem.overlay = segOverlay;
    segMem.segment = segs[n];
    segMem.colour = colour;
    segMem.width = width;
    segOverlays.push(segMem);
  }

  var trackMem = new GMAPPainterTrack(this, track, segOverlays, colour, true);
  this.trackOverlays.push(trackMem);
}

// Paints a waypoint on the map.
GMAPPainter.prototype.PaintWaypoint = function(waypoint)
{

  var ic = null;
  if(null != waypoint.GetSym()) {
    if(waypoint.GetSym() in this.iconmap) {
      ic = this.iconmap[waypoint.GetSym()];
    }
  }

  var mkr = null;
  var moptions = {
    };

  if(null != ic) {
    moptions.icon = ic;
  }
  moptions.title = waypoint.GetName();

  mkr = new GMarker(waypoint.GetPosition(), moptions);

  var mainTabHtml = null;
  if(null != waypoint.GetName()) {
    mainTabHtml = "<div style='height:4em;overflow:auto;font-size:75%;font-family:arial,helvetica;'><strong>" + waypoint.GetName() + "</strong></div>";
  }

  var posTabHtml = null;
  if(null != waypoint.GetElevation() || null != waypoint.GetPosition()) {
    posTabHtml = "<div style='height:4em;overflow:auto;font-size:75%;font-family:arial,helvetica;'>";
    if(null != waypoint.GetElevation()) {
      posTabHtml += "<strong>Elevation:</strong> " + Math.round(waypoint.GetElevation()) + " metres<br />";
    }
    if(null != waypoint.GetPosition()) {
      // Get the grid reference.
      var nztmProj = get_NZTM_projection();
      var en = geod_tm(nztmProj, waypoint.GetPosition().lat() / rad2deg, waypoint.GetPosition().lng() / rad2deg);

      // Make chars 3 to 5 of the easting and northing boldened.
      var easting = new String(Math.round(en.easting));
      var northing = new String(Math.round(en.northing));
      easting = easting.slice(0,2) + "[" + easting.slice(2,5) + "]" + easting.slice(5, 100);
      northing = northing.slice(0,2) + "[" + northing.slice(2,5) + "]" + northing.slice(5, 100);

      posTabHtml += "<strong>NZTM2000:</strong> E" + easting + ", N" + northing + "<br />";

      posTabHtml += "<strong>NZGD2000 Lat/Long:</strong> " + waypoint.GetPosition().lat() + ", " + waypoint.GetPosition().lng() + "<br />";
//      posTabHtml += "<strong>Longitude:</strong> " + waypoint.GetPosition().lng() + "<br />";
    }
    posTabHtml += "</div>";
  }
  
  var notesTabHtml = null;
  if(null != waypoint.GetDescription()) {
    notesTabHtml = "<div style='height:4em;width:20em;overflow:auto;font-size:75%;font-family:arial,helvetica;'>";
    notesTabHtml += waypoint.GetDescription();
    notesTabHtml += "</div>";
  }

  var toptions = {
  };
  toptions.selectedTab = 0;
  
  var tabs = [];
  if(null != mainTabHtml) { tabs.push(new GInfoWindowTab("Main", mainTabHtml)); }
  if(null != posTabHtml) { tabs.push(new GInfoWindowTab("Position", posTabHtml)); }
  if(null != notesTabHtml) { tabs.push(new GInfoWindowTab("Notes", notesTabHtml)); }
  
  GEvent.addListener(mkr, "click", function() {
    mkr.openInfoWindowTabsHtml(
	tabs,
	toptions
	);
    }
  );

  // Record this waypoint as one that's been drawn.
  var wpMem = new GMAPPainterWaypoint(this, waypoint, mkr, '#0000cc', true);
  this.waypointOverlays.push(wpMem);

  // Add the overlay to the map.
  this.map.addOverlay(mkr);
}

// Gets a list of waypoint overlays.
GMAPPainter.prototype.GetPaintedWaypoints = function()
{
  return this.waypointOverlays;
}

// Gets a list of track overlays.
GMAPPainter.prototype.GetPaintedTracks = function()
{
  return this.trackOverlays;
}


// Centres and zooms around everything painted on the map.
GMAPPainter.prototype.CentreZoom = function(maptype)
{
  // Center around the middle of the points
  var clon = (this.maxlon + this.minlon) / 2;
  var clat = (this.maxlat + this.minlat) / 2;

  var bounds = new GLatLngBounds(new GLatLng(this.minlat, this.minlon), new GLatLng(this.maxlat, this.maxlon));

  this.map.setCenter(new GLatLng(clat, clon), this.map.getBoundsZoomLevel(bounds), maptype);
}


///////////////////////////////////////////////////
//// GMAPDisplayControl -- a control to display links that alter what's displayed.
function GMAPDisplayControl(painter, doWaypoints, doTracks, cssWidth)
{
  this.painter = painter;
  this.doWaypoints = doWaypoints;
  this.doTracks = doTracks;
  this.cssWidth = cssWidth;
}
GMAPDisplayControl.prototype = new GControl();

GMAPDisplayControl.prototype.initialize = function(map) {
  var container = document.createElement("div");
  container.style.width = this.cssWidth;

  var itemsDiv = document.createElement("div");
  this.setBoxStyle_(itemsDiv);

  var headerTextDiv = document.createElement("div");
  this.setHeaderStyle_(headerTextDiv);
  headerTextDiv.appendChild(document.createTextNode("Show/Hide:"));
  itemsDiv.appendChild(headerTextDiv);

  var painter = this.painter;

  if(this.doWaypoints) {
    var item = document.createElement("div");
    item.appendChild(document.createTextNode("Waypoints"));
    itemsDiv.appendChild(item);

    // Add an event handler to hide/display this track.
    GEvent.addDomListener(item, "click", function() {
      var paintedWaypoints = painter.GetPaintedWaypoints();
      for(var n = 0; n < paintedWaypoints.length; n++)
      {
        var wp = paintedWaypoints[n];
        if(wp.IsVisible()) {
          wp.Hide();
        } else {
          wp.Show();
        }
      }

    });
    this.setSelectorStyle_(item, null);

  }


  if(this.doTracks) {
    var paintedTracks = painter.GetPaintedTracks();

    for(var n = 0; n < paintedTracks.length; n++)
    {
      var item = document.createElement("div");
      var paintedTrack = paintedTracks[n];

      // Save the current instance of painted track for access by the event handler below.
      item.paintedTrack = paintedTrack;
      this.setSelectorStyle_(item, paintedTrack);
      var trackName = paintedTrack.GetTrack().GetName();
      if(null == trackName) { trackName = '[Unnamed track]'; }
      item.appendChild(document.createTextNode(trackName));
      
      var totalDistance = paintedTrack.GetTrack().GetDistance() / 1000;
      var totalAscent = paintedTrack.GetTrack().GetTotalAscent();
      var totalDescent = paintedTrack.GetTrack().GetTotalDescent();
      item.title =
          'Total distance: ' + RoundNumber(totalDistance, 2) + ' km, ascent: '
          + RoundNumber(totalAscent, 0) + ' m, descent: '
          + RoundNumber(totalDescent, 0) + ' m.';
      itemsDiv.appendChild(item);

      // Add an event handler to hide/display this track.
      GEvent.addDomListener(item, "click", function() {
        var pt = this.paintedTrack;
        if(pt.IsVisible()) {
          pt.Hide();
        } else {
          pt.Show();
        }
      });
    }

  }

  container.appendChild(itemsDiv);
  map.getContainer().appendChild(container);
  return container;
}

// By default, the control will appear in the top left corner of the
// map with 7 pixels of padding.
GMAPDisplayControl.prototype.getDefaultPosition = function() {
  return new GControlPosition(G_ANCHOR_BOTTOM_RIGHT, new GSize(7, 7));
}

// Sets the proper CSS for the main box element.
GMAPDisplayControl.prototype.setBoxStyle_ = function(box) {
//  box.style.textDecoration = "underline";
//  box.style.color = "#0000cc";


  if(isIE()) {
    // msie 7 (at least) doesn't like rgba.
    box.style.backgroundColor = "rgb(255,255,255)";
  } else {
    box.style.backgroundColor = "rgba(255,255,255,0.5)";
  }

/*
  try {
    box.style.backgroundColor = "rgba(255,255,255,0.5)";
  } catch(ex) {
    // msie 7 (at least) doesn't like rgba.
    box.style.backgroundColor = "rgb(255,255,255)";
  }
*/

  box.style.fontFamily = "arial";
  box.style.fontSize = "x-small";
  box.style.border = "1px solid black";
  box.style.borderTopWidth = "2px";
  box.style.padding = "2px";
  box.style.marginBottom = "3px";
  box.style.textAlign = "left";
//  box.style.width = "100%;";
}

// Sets the proper CSS for the main header element.
GMAPDisplayControl.prototype.setHeaderStyle_ = function(box) {
//  box.style.textDecoration = "underline";
  try {
    box.style.backgroundColor = "rgba(128,255,128,0.5)";
  } catch(ex) {
    // msie 7 (at least) doesn't like rgba.
    box.style.backgroundColor = "rgb(128,255,128)";
  }
  box.style.color = "#000000";
  box.style.fontWeight = "bold";
  box.style.borderBottom = "1px dotted black";
  box.style.padding = "2px";
  box.style.marginBottom = "3px";
}

// Sets the proper CSS for the main header element.
GMAPDisplayControl.prototype.setSelectorStyle_ = function(box, selector) {
  var selColour = "#0000cc";
  if(null != selector) {
    selColour = selector.GetPreferredSelectorColour();
  }

  box.style.color = selColour;
  box.style.fontWeight = "bold";
  box.style.padding = "2px";
  box.style.marginBottom = "1px";
  box.style.marginLeft = "6px";
  box.style.cursor = "pointer";
}


///////////////////////////////////////////////////
//// GMAPElevationChart -- creates an elevation chart using the Google Chart API.
function GMAPElevationChart(xDiv)
{
  this.xDiv = xDiv;
}

// Draws a chart inside the object's assigned div.
GMAPElevationChart.prototype.DrawChart = function(track, width, height)
{
  var url = this.BuildUrl(track, width, height);
  var img = document.createElement("img");
  img.src = url;
  this.xDiv.appendChild(img);
}

GMAPElevationChart.prototype.BuildUrl = function(track, width, height)
{
//  var chartTitle = track.GetName();
//  return 'http://chart.apis.google.com/chart?cht=lc&chtt='+chartTitle+'&chs='+width+'x'+height+'&chd=t:40,0,60,60,45,47,75,70,100,72';
  return 'http://chart.apis.google.com/chart?cht=lc&chs='+width+'x'+height+'&chd=t:40,0,60,60,45,47,75,70,100,72';
}



/*
 * Code to convert lat/long from NZGD2000 to grid reference for NZTM2000.
 * Adapted from C code published by Land Information New Zealand at
 * http://www.linz.govt.nz/geodetic/software-downloads/index.aspx
 */

var NZTM_A = 6378137 ;
var NZTM_RF = 298.257222101 ;

var NZTM_CM = 173.0 ;
var NZTM_OLAT = 0.0 ;
var NZTM_SF = 0.9996 ;
var NZTM_FE = 1600000.0 ;
var NZTM_FN = 10000000.0 ;

var PI = Math.PI ; // 3.1415926535898 ;
var TWOPI = (2.0*PI) ;
var rad2deg = (180/PI) ;


var g_NZTM_PROJECTION = null;
function get_NZTM_projection()
{
  if(null == g_NZTM_PROJECTION) {
    var f;
    var tm = new Object();

    tm.meridian = NZTM_CM / rad2deg;
    tm.scalef = NZTM_SF;
    tm.orglat = NZTM_OLAT / rad2deg;
    tm.falsee = NZTM_FE;
    tm.falsen = NZTM_FN;
    tm.utom = 1.0;

    if( NZTM_RF != 0.0 ) { f = 1.0 / NZTM_RF; } else { f = 0.0; }
    tm.a = NZTM_A;
    tm.rf = NZTM_RF;
    tm.f = f;
    tm.e2 = 2.0 * f - f * f;
    tm.ep2 = tm.e2 / ( 1.0 - tm.e2 );

    tm.om = meridian_arc( tm, tm.orglat );

    g_NZTM_PROJECTION = tm;
  }
  return g_NZTM_PROJECTION;
}

function meridian_arc( tm, lt ) {
  var e2 = tm.e2;
  var a = tm.a;
  var e4, e6, A0, A2, A4, A6;

  e4 = e2 * e2;
  e6 = e4 * e2;
 
  A0 = 1 - (e2/4.0) - (3.0*e4/64.0) - (5.0*e6/256.0);
  A2 = (3.0/8.0) * (e2+e4/4.0+15.0*e6/128.0);
  A4 = (15.0/256.0) * (e4 + 3.0*e6/4.0);
  A6 = 35.0*e6/3072.0;

  return a*(A0*lt-A2*Math.sin(2*lt)+A4*Math.sin(4*lt)-A6*Math.sin(6*lt));
}

function geod_tm( tm, lt, ln) {
  var ret = new Object();
  ret.northing = 0.0;
  ret.easting = 0.0;
  
  var fn = tm.falsen;
  var fe = tm.falsee;
  var sf = tm.scalef;
  var e2 = tm.e2;
  var a = tm.a;
  var cm = tm.meridian;
  var om = tm.om;
  var utom = tm.utom;
  var dlon, m, slt, eslt, eta, rho, psi, clt, w, wc, wc2, t, t2, t4, t6, trm1, trm2, trm3, gce, trm4, gcn;
 
  dlon  =  ln - cm;
  while ( dlon > PI ) { dlon -= TWOPI };
  while ( dlon < -PI ) { dlon += TWOPI };
 
  m = meridian_arc(tm, lt);
 
  slt = Math.sin(lt);
 
  eslt = (1.0-e2*slt*slt);
  eta = a/Math.sqrt(eslt);
  rho = eta * (1.0-e2) / eslt;
  psi = eta/rho;
 
  clt = Math.cos(lt);
  w = dlon;
 
  wc = clt*w;
  wc2 = wc*wc;
 
  t = slt/clt;
  t2 = t*t;
  t4 = t2*t2;
  t6 = t2*t4;
 
  trm1 = (psi-t2)/6.0;
 
  trm2 = (((4.0*(1.0-6.0*t2)*psi 
                + (1.0+8.0*t2))*psi 
                - 2.0*t2)*psi+t4)/120.0;
 
  trm3 = (61 - 479.0*t2 + 179.0*t4 - t6)/5040.0;
 
  gce = (sf*eta*dlon*clt)*(((trm3*wc2+trm2)*wc2+trm1)*wc2+1.0);

  ret.easting = gce/utom+fe;
//  *ce = gce/utom+fe;
 
  trm1 = 1.0/2.0;
 
  trm2 = ((4.0*psi+1)*psi-t2)/24.0;
 
  trm3 = ((((8.0*(11.0-24.0*t2)*psi
              -28.0*(1.0-6.0*t2))*psi
              +(1.0-32.0*t2))*psi 
              -2.0*t2)*psi
              +t4)/720.0;
 
  trm4 = (1385.0-3111.0*t2+543.0*t4-t6)/40320.0;
 
  gcn = (eta*t)*((((trm4*wc2+trm3)*wc2+trm2)*wc2+trm1)*wc2);

  ret.northing = (gcn+m-om)*sf/utom+fn;
//  *cn = (gcn+m-om)*sf/utom+fn;

  return ret;
}
