/* bike-gpx.js: JavaScript glue for Google Maps/Bike Maps app (with GPX)
 * David Maze <dmaze@mit.edu>
 * 21 Jan 2006
 */

// GLOBALS.
var map;
var rep;
var markers;
var yearSel;
var tripSel;
var colorSel;
var unitSel;
var tripIndex;
var photoXML;

// Let's get set up:
function bootstrap()
{
  // Very first step: figure out how big the map div actually should be.
  var div = document.getElementById("map");

  // NB: lots of ways to find the window height; this seems like an okay compromise
  var ww = document.body.scrollWidth;
  var wh = document.body.scrollHeight;
  // Finding the current position is at least algorithmic
  var px = 0;
  var py = 0;
  var obj = div;
  while (obj.offsetParent)
  {
    px += obj.offsetLeft;
    py += obj.offsetTop;
    obj = obj.offsetParent;
  }
  div.style.width = (ww - px) + "px";
  div.style.height = (wh - py) + "px";

  map = new google.maps.Map2(div);
  map.addControl(new google.maps.LargeMapControl());
  map.addControl(new google.maps.ScaleControl());
  map.addControl(new google.maps.MapTypeControl());
  map.addControl(new google.maps.OverviewMapControl());
  google.maps.Event.addListener(map, "zoomend",
                                function (from, to) { redrawOverlays(); });
  // TODO: write something more intelligent
  // (If pointsVisible() only actually returns visible points, then
  // initial draws are much faster, but this needs to be turned on and
  // dragging is stupidly slow.  Right answer: remember which
  // points/overlays are visible, and add/remove them rather than
  // outright recalculating.)
  // GEvent.addListener(map, "move", redrawOverlays);
  markers = new Array(0);
  yearSel = document.getElementById("year-select");
  tripSel = document.getElementById("trip-select");
  colorSel = document.getElementById("color-select");
  unitSel = document.getElementById("unit-select");

  // Part 2: build the trip index.
  tripIndex = null;
  photoXML = null;
  google.maps.DownloadUrl("trips.xml", haveTrips);
  google.maps.DownloadUrl("photos.xml", havePhotos);
}

function haveTrips(text, code)
{
    tripIndex = new TripIndex();
    tripIndex.grokDOM(google.maps.Xml.parse(text));
    if (photoXML != null)
    	haveTripsAndPhotos();
}

function havePhotos(text, code)
{
	photoXML = google.maps.Xml.parse(text);
	if (tripIndex != null)
		haveTripsAndPhotos();
}

function haveTripsAndPhotos()
{
	tripIndex.grabPhotos(photoXML);
    yearSel.options.length = 0;
    var years = tripIndex.getYears();
    for (var i = 0; i < years.length; i++)
    {
        yearSel.options[i] = new Option(years[i], years[i]);
    }

    // Were we handed a trip in the URL?
    var trip = findTripInURL();
    if (trip != null) trip = tripIndex.findTripByName(trip);
    if (trip != null)
    {
        // Select its year, naming the target trip.
        for (var i = 0; i < years.length; i++)
            if (years[i] == trip.year)
                yearSel.selectedIndex = i;
        selectYear(trip);
    }
    else
    {
        // No trip in URL.
        yearSel.selectedIndex = years.length - 1;
        selectYear(null);
    }
}

// Triggered when a new year is selected.
// If trip is non-null, then that trip is shown and selected if present.
function selectYear(trip)
{
    var year = yearSel.options[yearSel.selectedIndex].value;
    var trips = tripIndex.tripsInYear(year);
    tripSel.options.length = 0;
    var count = 0;
    var last = 0;
    for (var i = 0; i < trips.length; i++)
    {
        if (trips[i] != trip && trips[i].visible == "no")
            continue;
        tripSel.options[count] = new Option(trips[i].getLabel(), i);
        if (trips[i] == trip ||
            (trip == null && trips[i].visible == "yes"))
            last = count;
        count++;
    }
    tripSel.selectedIndex = last;
    selectTrip();
}

function findTripInURL()
{
  var q = window.location.search;
  if (q.charAt(0) == '?') q = q.substr(1);
  if (q.length == 0) return null;
  parts = q.split('&');
  for (var i = 0; i < parts.length; i++)
  {
    term = parts[i].split('=', 2);
    if (term[0] == 'trip')
      return term[1];
  }
  return null;
}

// How to pick a trip:
function selectTrip()
{
    changeMap(tripSel.options[tripSel.selectedIndex].value);
}

function changeMap(idx)
{
    var year = yearSel.options[yearSel.selectedIndex].value;
    var trips = tripIndex.tripsInYear(year);
    var trip = trips[idx];
    var mapurl = trip.getFilename();
    google.maps.DownloadUrl
      (mapurl, function(text, code) { buildRep(trip, google.maps.Xml.parse(text)); });
}

// Infrastructure: find element children of a DOM node with a particular qname.
function childrenNamed(dom, ns, local)
{
    var result = new Array();
    for (var i = 0; i < dom.childNodes.length; i++)
    {
        var node = dom.childNodes[i];
        if (node.nodeType == 1 &&
            node.namespaceURI == ns &&
            node.nodeName == local)
            result.push(node);
    }
    return result;
}

// Infrastructure: get the value of the first text child of dom, if any.
function elementText(dom)
{
    for (var i = 0; i < dom.childNodes.length; i++)
    {
        var node = dom.childNodes[i];
        if (node.nodeType == 3)
            return node.nodeValue;
    }
    return "";
}

// Trip index: this reads in trips.xml and indexes trips in various ways.
function Trip()
{
}

Trip.prototype.grokDOM = function(dom)
{
    var bikens = "http://www.mit.edu/~dmaze/bike/";
    var date = elementText(childrenNamed(dom, bikens, "date")[0]);
    var dates = date.split("-");
    this.year = dates[0];
    this.month = dates[1];
    this.day = dates[2];
    this.name = elementText(childrenNamed(dom, bikens, "name")[0]);
    var formats = childrenNamed(dom, bikens, "format");
    this.format = "gpx";
    if (formats.length > 0) this.format = elementText(formats[0]);
    this.title = elementText(childrenNamed(dom, bikens, "title")[0]);
    this.visible = "yes";
    var visibles = childrenNamed(dom, bikens, "visible");
    if (visibles.length > 0) this.visible = elementText(visibles[0]);
    this.photos = new Array();
}

Trip.prototype.getExtension = function()
{
    if (this.format == "bike")
        return "xml";
    return this.format;
}

Trip.prototype.getFilename = function()
{
    return this.year + this.month + this.day + "-" + this.name + "." + this.getExtension();
}

Trip.prototype.getURLName = function()
{
    return this.year + this.month + this.day + "-" + this.name;
}

Trip.prototype.getLabel = function()
{
    var result = '';
    if (this.month == 1) result = 'Jan';
    if (this.month == 2) result = 'Feb';
    if (this.month == 3) result = 'Mar';
    if (this.month == 4) result = 'Apr';
    if (this.month == 5) result = 'May';
    if (this.month == 6) result = 'Jun';
    if (this.month == 7) result = 'Jul';
    if (this.month == 8) result = 'Aug';
    if (this.month == 9) result = 'Sep';
    if (this.month == 10) result = 'Oct';
    if (this.month == 11) result = 'Nov';
    if (this.month == 12) result = 'Dec';
    result += ' ' + this.day + ', ' + this.title;
    return result;
}

function TripIndex()
{
    this.trips = new Array();
}

TripIndex.prototype.grokDOM = function(dom)
{
    var trips = childrenNamed(dom.documentElement, "http://www.mit.edu/~dmaze/bike/", "trip");
    for (var i = 0; i < trips.length; i++)
    {
        var trip = new Trip();
        trip.grokDOM(trips[i]);
        this.trips.push(trip);
    }
}

TripIndex.prototype.getYears = function()
{
    var result = new Array();
    for (var i = 0; i < this.trips.length; i++)
    {
        var trip = this.trips[i];
        var year = trip.year;
        for (var j = 0; j < result.length; j++)
        {
            if (result[j] == year)
            {
                year = null;
                break;
            }
        }
        if (year != null)
            result.push(year);
    }
    return result;
}

TripIndex.prototype.tripsInYear = function(year)
{
    var result = new Array();
    for (var i = 0; i < this.trips.length; i++)
    {
        if (this.trips[i].year == year)
            result.push(this.trips[i]);
    }
    return result;
}

TripIndex.prototype.findTripByName = function(name)
{
    for (var i = 0; i < this.trips.length; i++)
        if (this.trips[i].getURLName() == name)
            return this.trips[i];
    return null;
}

TripIndex.prototype.grabPhotos = function(dom)
{
	var trips = childrenNamed(dom.documentElement, "http://www.mit.edu/~dmaze/bike/", "photos");
	for (var i = 0; i < trips.length; i++)
	{
		var name = trips[i].getAttribute("trip");
		var trip = this.findTripByName(name);
		if (trip == null)
		{
			alert("photos.xml referenced non-existent trip " + name);
			continue;
		}
		var photos = childrenNamed(trips[i], "http://www.mit.edu/~dmaze/bike/", "photo");
		for (var j = 0; j < photos.length; j++)
		{
			var node = photos[j];
			var p = new Object();
		    for (var k = 0; k < node.childNodes.length; k++)
		    {
        		var child = node.childNodes[k];
		        if (child.nodeType == 1)
		        {
		        	// I am an element.
		        	p[child.nodeName] = elementText(child);
		        }
		    }
		    trip.photos.push(p);
		}
	}
}

function MapRep()
{
    this.points = new Array();
}

MapRep.prototype.getPoint = function(i, j)
{
    return this.points[i][j].point;
}

MapRep.prototype.getTime = function(i, j)
{
    return this.points[i][j].time;
}

MapRep.prototype.getElevation = function(i, j)
{
    return this.points[i][j].elevation;
}

MapRep.prototype.getSegmentCount = function()
{
    return this.points.length;
}

MapRep.prototype.getPointCount = function(i)
{
    return this.points[i].length;
}


MapRep.prototype.getBounds = function()
{
    var bounds = new google.maps.LatLngBounds();
    for (var i = 0; i < this.points.length; i++)
    {
        for (var j = 0; j < this.points[i].length; j++)
        {
            bounds.extend(this.points[i][j].point);
        }
    }
    return bounds;
}

function GPXRep(gpxns)
{
    // Watch this subtlety: if we don't explicitly create
    // our own points array, then this.points will always
    // refer to the prototype object's array.  That's bad.
    this.points = new Array();
    this.gpxns = gpxns;
}

GPXRep.prototype = new MapRep();

GPXRep.prototype.grokDOM = function(dom)
{
    var gpx = dom.documentElement;
    // Get data off the tracks
    var tracks = childrenNamed(gpx, this.gpxns, "trk");
    var segnum = 0;
    for (var i = 0; i < tracks.length; i++)
    {
        // For each segment...
        var segs = childrenNamed(tracks[i], this.gpxns, "trkseg");
        for (var j = 0; j < segs.length; j++)
        {
            var pts = childrenNamed(segs[j], this.gpxns, "trkpt");
            this.points[segnum] = new Array();
            for (var k = 0; k < pts.length; k++)
            {
                var pt = pts[k];
                // TODO: precalculate other visible fields here too
                // (time to next point, distance to next point,
                // height to next point, dx/dt, dy/dt, dy/dx, ...)
                // (and maybe even precalculate their colors)
                this.points[segnum][k] = 
                    {
                        point: this.eltToGLatLng(pt),
                        time: this.eltToDate(pt),
                        elevation: this.eltToElevation(pt)
                    }
                ;
            }
            segnum++;
        }
    }
}

GPXRep.prototype.eltToGLatLng = function(dom)
{
    return new google.maps.LatLng(parseFloat(dom.getAttribute("lat")),
                                  parseFloat(dom.getAttribute("lon")));
}

GPXRep.prototype.eltToElevation = function(dom)
{
    var eledom = childrenNamed(dom, this.gpxns, "ele");
    if (eledom == null || eledom.length == 0) return 0;
    var eletext = elementText(eledom[0]);
    var eleft = parseFloat(eletext);
    return eleft * 0.3048; // in meters
}

GPXRep.prototype.eltToDate = function(dom)
{
    var timedom = childrenNamed(dom, this.gpxns, "time");
    if (timedom == null || timedom.length == 0) return null;
    var timestr = elementText(timedom[0]);
    // The GPX spec says the time should be UTC, though this isn't
    // enforced in the schema.
    var dtstrs = new Array();
    dtstrs = timestr.split("T");
    var datestr = dtstrs[0];
    var tstrs = dtstrs[1].split("Z");
    var timestr = tstrs[0];
    var dateparts = datestr.split("-");
    var timeparts = timestr.split(":");
    return new Date(dateparts[0], dateparts[1]-1, dateparts[2],
                    timeparts[0], timeparts[1], timeparts[2]);
}

function BikeRep()
{
    this.points = new Array();
}

BikeRep.prototype = new MapRep();

BikeRep.prototype.grokDOM = function(dom)
{
    var trip = dom.documentElement;
    var points = childrenNamed(trip, null, "points");
    this.points = new Array();
    for (var i = 0; i < points.length; i++)
    {
        this.points[i] = new Array();
        var pts = childrenNamed(points[i], null, "point");
        for (var j = 0; j < pts.length; j++)
        {
            var pt = pts[j];
            this.points[i][j] =
                {
                    point: this.eltToGLatLng(pt)
                }
            ;
        }
    }
}

BikeRep.prototype.eltToGLatLng = function(dom)
{
    return new google.maps.LatLng(parseFloat(dom.getAttribute("lat")),
                                  parseFloat(dom.getAttribute("lng")));
}

function buildRep(trip, dom)
{
    // Rebuild the representation object.
    var root = dom.documentElement;
    if (root.nodeName == "gpx")
        rep = new GPXRep(root.namespaceURI);
    else if (root.nodeName == "trip" &&
             (root.namespaceURI == "" ||
              root.namespaceURI == null))
        rep = new BikeRep();
    else
        rep = null;
    rep.grokDOM(dom);
    rep.trip = trip;
    var bounds = rep.getBounds();
    var center = new google.maps.LatLng((bounds.getSouthWest().lat() +
                                         bounds.getNorthEast().lat()) / 2,
                                        (bounds.getSouthWest().lng() +
                                         bounds.getNorthEast().lng()) / 2);
    map.clearOverlays();
    var zoom = map.getBoundsZoomLevel(bounds);
    if (map.isLoaded() && map.getZoom() == zoom)
        map.panTo(center);
    else
        map.setCenter(center, zoom);
    redrawOverlays();
}

function redrawOverlays()
{
    map.clearOverlays();
    newpts = pointsVisible(map, rep);
    for (var i = 0; i < newpts.length; i++)
        drawSegment(map, rep, newpts[i]);
    // Place some distance markers based on checkbox settings
    updateUnit();
    // Place photo markers as appropriate
    updatePhotos();
}

// What points are visible in the current segment?
// Trim down to visually distinct points and things that are visible.
// Returns an array of array of [segment,point].
function pointsVisible(map, rep)
{
    var result = new Array();
    var bounds = map.getBounds();
    for (var segnum = 0; segnum < rep.getSegmentCount(); segnum++)
    {
        var here = null;
        var lastpx = null;
        for (var ptnum = 0; ptnum < rep.getPointCount(segnum); ptnum++)
        {
            var pt = rep.getPoint(segnum, ptnum);
            if (true || bounds.contains(pt))
            {
                // This is on the map.  Is there a current array?
                if (ptnum == 0)
                {
                    // It's the first point, so no.
                    here = new Array();
                    result.push(here);
                    here.push([segnum,ptnum]);
                    lastpx = map.fromLatLngToDivPixel(pt);
                }
                if (here == null)
                {
                    // Not the first point.  Put the *previous*
                    // point into the array, then fall through.
                    here = new Array();
                    result.push(here);
                    here.push([segnum,ptnum-1]);
                    var lastpt = rep.getPoint(segnum, ptnum-1);
                    lastpx = map.fromLatLngToDivPixel(lastpt);
                }
                if (ptnum > 0 && ptnum < rep.getPointCount(segnum) - 1)
                {
                    // Is it distinct from the previous point?
                    var thispx = map.fromLatLngToDivPixel(pt);
                    var dx = thispx.x - lastpx.x;
                    var dy = thispx.y - lastpx.y;
                    var limit = 2;
                    if (dx <= -limit || dx >= limit ||
                        dy <= -limit || dy >= limit)
                    {
                        here.push([segnum,ptnum]);
                        lastpx = thispx;
                    }
                }
                else if (ptnum > 0)
                {
                    // Last point, always add if visible.
                    here.push([segnum,ptnum]);
                    lastpx = null;
                }
            }
            else
            {
                // Not on the map.  If there's a current array,
                // it's over...but put this point into it.
                if (here != null)
                    here.push([segnum,ptnum]);
                here = null;
            }
        }
    }
    return result;
}

// This is the encoding for the Google Maps encoded polyline stuff.
function encodeFloat(f)
{
    var i = Math.floor(f * 100000);
    i = i << 1;
    if (f < 0.0) i = ~i;
    var s = "";
    while ((i & ~0x1F) != 0)
    {
        // not last digit yet
        var b = (i & 0x1F) | 0x20;
        s = s + String.fromCharCode(b + 63);
        i = i >> 5;
    }
    // last digit
    s = s + String.fromCharCode(i + 63);
    return s;
}

function drawSegment(map, rep, ptind)
{
    // What kind of color are we doing?
    var color = colorSel.options[colorSel.selectedIndex].value;
    // If the color isn't "none", we'll need to draw the line
    // one segment at a time, since each can be a different color.
    if (color == "elevation" || color == "climb" || color == "slope" ||
        color == "speed")
    {
        for (var h = 1; h < ptind.length; h++)
        {
            var segnum = ptind[h][0];
            var i = ptind[h][1];
            var p0 = rep.getPoint(segnum, i-1);
            var p1 = rep.getPoint(segnum, i);
            var s = encodeFloat(p0.lat()) + encodeFloat(p0.lng()) +
                    encodeFloat(p1.lat() - p0.lat()) +
                    encodeFloat(p1.lng() - p0.lng());
            var elevation = 0.0; // meters
            var climb = 0.0; // meters
            var el0 = rep.getElevation(segnum, i-1);
            var el1 = rep.getElevation(segnum, i);
            if (el0 != null && el1 != null)
            {
                elevation = (el0 + el1) / 2;
                climb = el1 - el0;
            }
            var distance = p0.distanceFrom(p1);
            var dt = 0.0; // milliseconds
            var t0 = rep.getTime(segnum, i-1);
            var t1 = rep.getTime(segnum, i);
            if (t0 != null && t1 != null) dt = t1 - t0;
            // We just want to pick a color at this point.  It's the hue
            // part of an HSV triple; 0.0 = 1.0 is red, 1/3 is green,
            // 2/3 is blue.
            
            var hue = 0.6667;
            if (color == "elevation")
            {
                // Arbitrarily let 0m == blue, 200m == green, etc.;
                // this should cycle some but generally work for ground
                // trips.
                hue = elevation / 60.0;
            }
            else if (color == "climb")
            {
                // meters per minute
                var rate = climb * 60000.0 / dt;
                // -10 m/min is green; 0 is blue; +10 is red
                // see algebra under "slope"
                // hue = rate / -15.0 + 1.0 / 3.0;
                // whiteboard says:
                hue = (rate + 20.0) / 30.0;
            }
            else if (color == "slope")
            {
                // meters per, erm, meter
                // Remember that elevation is in m, distance in m;
                // so we want (climb/distance).  Multiply by
                // 100 to get percent.
                var grade = 100.0 * climb / distance;
                // (Mile Hill Road on Climb to the Clouds is +9%;
                // a little wrapping is okay.)
                // -6% is green; 0% is blue; +6% is red.
                // -6 => 1/3; 0 => 2/3; 6 => 1
                // hue = a(grade)+b
                // 3/3 = 6a + b
                // 2/3 = 0a + b = b
                // 1/3 = 6a
                // 1/18 = a
                // hue = grade / 18.0 + 2.0 / 3.0;
                // hue = (12.0 + grade) / 18.0;
                // HACK-O-RAMA: This doesn't actually come out right.
                // Why?  Experimentally, this looks more like what I want:
                hue = (6.0 + grade) / 9.0;
            }
            else if (color == "speed")
            {
                // kilometers per hour
                // distance = m; dt = ms
                // distance/dt = m/ms
                // distance/dt * 1000 ms/s = m/s
                // distance/dt * 1000 ms/s / 1000 m/km = km/s
                // distance/dt = km/s
                // distance/dt * 3600 s/hr = km/hr
                var kmph = distance * 3600.0 / dt;
                // 0 = red; 20 = blue; 40 = green; 60 = red
                hue = (60.0 - kmph) / 60.0;
            }

            var polyline = new google.maps.Polyline.fromEncoded({
                color: hueToRgbString(hue),
                weight: 4,
                points: s,
                levels: "BB",
                zoomFactor: 32,
                numLevels: 4
            });
            map.addOverlay(polyline);
        }
    }
    else
    {
        // Build a list of short polylines; Mozilla croaks on really
        // long ones.
        var maxlen = 65;
        // Remember kids, need one point of overlap.
        for (var base = 0;
             base < ptind.length;
             base += maxlen - 1)
        {
            var plen = ptind.length - base;
            if (plen > maxlen) plen = maxlen;
            var s = "";
            var l = "";
            var p0 = new google.maps.LatLng(0.0, 0.0);
            for (var i = 0; i < plen; i++)
            {
                var segnum = ptind[base+i][0];
                var ptnum = ptind[base+i][1];
                var p1 = rep.getPoint(segnum, ptnum);
                var dlat = p1.lat() - p0.lat();
                var dlng = p1.lng() - p0.lng();
                p0 = p1;
                s += encodeFloat(dlat) + encodeFloat(dlng);
                l += "B";
            }
            var polyline = new google.maps.Polyline.fromEncoded({
                color: "#0000FF",
                weight: 4,
                points: s,
                levels: l,
                zoomFactor: 32,
                numLevels: 4
            });
            map.addOverlay(polyline);
        }
    }
}

function hueToRgbString(hue)
{
    var r, g, b;
    while (hue < 0.0) hue += 1.0;
    while (hue >= 1.0) hue -= 1.0;
    hue = hue * 6.0;
    if (hue < 1.0)
    {
        r = 1.0;
        g = hue;
        b = 0.0;
    }
    else if (hue < 2.0)
    {
        r = 2.0 - hue;
        g = 1.0;
        b = 0.0;
    }
    else if (hue < 3.0)
    {
        r = 0.0;
        g = 1.0;
        b = hue - 2.0;
    }
    else if (hue < 4.0)
    {
        r = 0.0;
        g = 4.0 - hue;
        b = 1.0;
    }
    else if (hue < 5.0)
    {
        r = hue - 4.0;
        g = 0.0;
        b = 1.0;
    }
    else
    {
        r = 1.0;
        g = 0.0;
        b = 6.0 - hue;
    }
    r = Math.floor(r * 255);
    g = Math.floor(g * 255);
    b = Math.floor(b * 255);
    var hex = ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'];
    var s = "#";
    s += hex[Math.floor(r / 16)];
    s += hex[r % 16];
    s += hex[Math.floor(g / 16)];
    s += hex[g % 16];
    s += hex[Math.floor(b / 16)];
    s += hex[b % 16];
    // GLog.write("hue=" + hue + " " + r + " " + g + " " + b + " " + s);
    return s;
}

function updateUnit()
{
  clearDistanceMarkers();
  if (unitSel.options[unitSel.selectedIndex].value != "none" && rep != null)
    placeDistanceMarkers(unitSel.options[unitSel.selectedIndex].value == "kilometers");
}

function clearDistanceMarkers()
{
  for (var i = 0; i < markers.length; i++)
    map.removeOverlay(markers[i]);
  markers = new Array(0);
}

function placeDistanceMarkers(km)
{
    // Stop now if our representation doesn't have any points.
    if (rep.getSegmentCount() == 0) return;
    
  var r_earth = 6371.01; // km
  var y_deg_per_km = 360 / (2 * Math.PI * r_earth);
  var incr = 1;
  // How often to place markers?  Let's
  // aim for about one every 100px; then we
  // need to know pixels/mile.
  var pxheight = map.getSize().height;
  var bounds = map.getBounds();
  var degheight = bounds.getNorthEast().lat() - bounds.getSouthWest().lat();
  var unitheight = degheight / y_deg_per_km;
  if (!km) unitheight = unitheight * 0.62137;
  var unit_per_px = unitheight / pxheight;
  var rawincr = 200.0 * unit_per_px;
  if (rawincr < 1.5) incr = 1;
  else if (rawincr < 3) incr = 2;
  else if (rawincr < 7.5) incr = 5;
  else if (rawincr < 15) incr = 10;
  else if (rawincr < 30) incr = 20;
  else incr = 50;
  var totaldist = 0.0;
  var nextdist = 0;
  var count = 0;
  var lastp = rep.getPoint(0, 0);
  for (var i = 0; i < rep.getSegmentCount(); i++)
  {
      for (var j = 0; j < rep.getPointCount(i); j++)
      {
          var nextp = rep.getPoint(i, j);
          var dist = lastp.distanceFrom(nextp) / 1000.0;
          if (!km) dist = dist * 0.62137;
          var newdist = dist + totaldist;
          if (newdist > nextdist)
          {
              // Add a marker.  How far down?
              var delta = newdist - totaldist;
              var frac = delta / dist;
              var xp = lastp.x + (nextp.x - lastp.x) * frac;
              var yp = lastp.y + (nextp.y - lastp.y) * frac;
              
              var icon = new google.maps.Icon();
              icon.image = "marker/marker-" + nextdist + ".png";
              icon.shadow = "marker/shadow-" + nextdist + ".png";
              icon.iconSize = new google.maps.Size(40,27);
              icon.shadowSize = new google.maps.Size(67,27);
              icon.iconAnchor = new google.maps.Point(20,13);
              icon.infoWindowAnchor = new google.maps.Point(20,0);
              
              var marker = new google.maps.Marker(new google.maps.Point(xp,yp), icon);
              map.addOverlay(marker);
              markers.push(marker);
              
              nextdist = nextdist + incr;
          }
          totaldist = newdist;
          lastp = nextp;
      }
  }
}

function updatePhotos()
{
	var i = 0;
	while (i < rep.trip.photos.length)
	{
		// Find a set of photos that are all within, let's say, two pixels
		// of each other.
		var start = i;
		var here = map.fromLatLngToDivPixel(new google.maps.LatLng(rep.trip.photos[i].lat,
		                                                           rep.trip.photos[i].lon));
		i++;
		while (i < rep.trip.photos.length)
		{
			var there = map.fromLatLngToDivPixel(new google.maps.LatLng(rep.trip.photos[i].lat,
			                                                            rep.trip.photos[i].lon));
			if (here.x - there.x > 2 || there.x - here.x > 2 ||
			    here.y - there.y > 2 || there.y - here.y > 2)
			    break;
			i++;
		}
		// Now either end is the length of the photo list or it's something
		// that's sufficiently far away to merit its own marker.
		createMarkerForPhotos(rep.trip.photos, start, i);
	}
}

function createMarkerForPhotos(photos,first,last)
{
	var icon = new google.maps.Icon();
	icon.image = "http://maps.google.com/mapfiles/kml/pal4/icon46.png";
	icon.shadow = "http://maps.google.com/mapfiles/kml/pal4/icon46s.png";
	icon.iconSize = new google.maps.Size(32,32);
	icon.shadowSize = new google.maps.Size(59,32);
	icon.iconAnchor = new google.maps.Point(16,16);
	icon.infoWindowAnchor = new google.maps.Point(16,0);
    var contents = "<table>";
	var point = new google.maps.LatLng(photos[first].lat, photos[first].lon);
	var marker = new google.maps.Marker(point, icon);
	// While there are some safety issues with this, creating HTML text is
	// probably easier and more portable than creating a DOM tree.
	for (var i = first; i < last; i++)
	{
		var photo = photos[i];
		contents += "<tr><td align=\"center\">"+
					"<a href=\"" + photo.target + "\">" +
	                "<img src=\"" + photo.thumbnail + "\" alt=\"\"" +
                    " width=\"" + photo.thumbwidth + "\"" +
                    " height=\"" + photo.thumbheight + "\">" +
	                "</a></td><td valign=\"center\">" +
        	        "<a href=\"" + photo.target + "\">" +
            	    photo.title + "</a></td></tr>";
    }
    contents += "</table>";
	google.maps.Event.addListener(marker, "click", function() { marker.openInfoWindowHtml(contents); });
	map.addOverlay(marker);
}
