// arch-tag: e49e6bfd-ad54-4ef5-b434-f157b42a2c3c
//
// GPX utility functions for use with the GMaps API
//
// Copyright (c) 2005 Mike Kenney
// released under GPL


/**
 * Asynchronously fetch the GPX file from the specified URL,
 * The GPX DOM is passed to the callback when the operation
 * is complete.
 *
 * @param  url    URL for the GPX file
 * @param  cback  callback function
 */
function fetch_gpx(url, cback) {
    var req = GXmlHttp.create();
    req.open("GET", url, true);
    req.onreadystatechange = function() {
        if(req.readyState == 4) {
            if(req.responseText)
                cback(GXml.parse(req.responseText));
            else
                alert("No response: GET " + url);
        }
    }
    req.send(null);
}

/**
 * Return the bounds of a GPX file.
 *
 * @param  gpx  top-level GPX DOM node
 * @return      a GBounds object
 */
function get_bounds(gpx) {
    bounds = gpx.getElementsByTagName("bounds")[0];
    if(!bounds)
        return null;
    return new GBounds(	parseFloat(bounds.getAttribute("minlon")),
	               				parseFloat(bounds.getAttribute("maxlat")),
                       	parseFloat(bounds.getAttribute("maxlon")),
                       	parseFloat(bounds.getAttribute("minlat")));
}

/**
 * Convert a GPX datetime string into a JS Date object
 * 
 * @param  input  GPX timestamp string (YYYY-MM-DDTHH:MM:SSZ)
 * @return        a Date object
 */
function gpx_datetime(input) {
    var dts = new String(input).slice(0,-1);  // chop the trailing 'Z'
    var dt 	= dts.split("T");
    var ymd = dt[0].split("-");
    var hms = dt[1].split(":");

    return new Date(ymd[0], ymd[1], ymd[2],
                    hms[0], hms[1], hms[2]);
}

/**
 * Plot a series of routes as a polyline with a marker for
 * each route point
 *
 * @param     gpx     top-level GPX DOM node
 * @param     map     GMap object
 * @param     lw      line width (optional: default 2)
 * @param     xsl     URL of XSL stylesheet to convert the route
 *                    point info into HTML
 */
function plot_routes(gpx, map, lw, xsl) {
    if(!lw)
        lw = 2;
    var colors = ["#ff0000", "#00ff00", "#0000ff"];

    var n = colors.length;
    var routes = gpx.getElementsByTagName("rte");
    for(var j = 0;j < routes.length;j++) {
        var pts = routes[j].getElementsByTagName("rtept");
        plot_pointlist(pts, map, lw, colors[j%n]);
        if(xsl)
            plot_points_xsl(pts, map, xsl);
        else
            plot_points(pts, map);
    }
}

/**
 * Plot a series of track segments as a polyline
 *
 * @param     gpx     top-level GPX DOM node
 * @param     map     GMap object
 * @param     lw      line width (optional: default 2)
 * @param     colors  array of color strings, one for each
 *                     segment (optional: default ['#0000aa']).
 * @param     limit   minimum segment length (optional: default 2)
 */
function plot_segments(gpx, map, lw, colors, limit) {
    if(!lw)
        lw = 2;
    if(!colors)
        colors = ["#0000aa"];
    if(!limit)
        limit = 2;

    var n = colors.length;
    var segs = gpx.getElementsByTagName("trkseg");
    for(var j = 0;j < segs.length;j++) {
        var pts = segs[j].getElementsByTagName("trkpt");
        if(pts.length > limit) {
            plot_pointlist(pts, map, lw, colors[j%n]);
        }
    }
}

/**
 * Plot a polyline connecting a list of GPX "points". These can be track-points,
 * waypoints, or route-points -- from the DOM standpoint, they are all the
 * same. Techically, this will handle a list of any DOM nodes that have the
 * attributes "lat" and "lon"
 *
 * @param     pts     list (Array) of GPX points
 * @param     map     GMap object
 * @param     lw      line width (optional: default 2)
 * @param     color   line color (optional: default #0000aa)
 */
function plot_pointlist(pts, map, lw, color) {
    var lw = 2;
    var colors = ['#0000aa'];
    var limit = 2;

    if(!lw)
        lw = 2;
    if(!color)
        color = "#0000aa";

    var poly = [];
    for(var i = 0;i < pts.length;i++) {
        poly.push(new GLatLng(parseFloat(pts[i].getAttribute("lat")),
                             parseFloat(pts[i].getAttribute("lon"))))
    }

    map.addOverlay(new GPolyline(poly, color, lw, 0.8));
}

/**
 * Animate a GPX track segment in real-time (uses the elapsed time
 * between each track point to set animation interval). The scale
 * argument can be used to speed-up the animation (i.e. setting
 * scale to N will run the animation at N times the real-time rate).
 *
 * @param     seg     GPX trkseg DOM node
 * @param     map     GMap object
 * @param     desc    animation descriptor Object with the following
 *                    attributes:
 *                      scale  =  time interval scale factor
 *                      skip   =  number of initial trackpoints to skip
 *                      color  =  line color
 *                      zcolor =  if != 0, color the line based on Z (elevation). The
 *                                value of zcolor is the maximum elevation. In this
 *                                case the 'color' attribute is ignored.
 *                      fstep  =  callback function for each time step
 *                      fdone  =  callbcak function when animation is complete
 *                      npan   =  "pan" the map every npan points
 * @return            a function taking no arguments which can be called to
 *                    cancel the animation.
 */
function animate_segment(seg, map, desc) {
    var scale = desc.scale || 1;
    var skip = desc.skip || 0;
    var color = desc.color || "#ff0000";
    var fstep = desc.fstep;
    var fdone = desc.fdone;
    var npan = desc.npan || 4;
    var zcolor = desc.zcolor || 0;

    // Package-up a function and some state variables. The function
    // updates the map to show the current trackpoint and returns
    // the number of milliseconds until the next trackpoint is reached.
    function make_handler() {
        var pts = seg.getElementsByTagName("trkpt");
        var ptnum = skip;
        var lastpt = null;
        var nextT = null;
        var redraw = function () {
            try {
                pt = new GLatLng(parseFloat(pts[ptnum].getAttribute("lat")),
                                parseFloat(pts[ptnum].getAttribute("lon")));
                if(zcolor) {
		    var node = pts[ptnum].getElementsByTagName("ele")[0];
                    var elev = parseFloat(node.firstChild.nodeValue);
                    var z = elev/zcolor;
                    if(z < 0)
                        z = 0.;
                    else if(z > 1.)
                        z = 1.;
                    var rgb = hsv2rgb(z*359, 0.9, 0.9);
                    color = rgb.toString();
                }
            } catch (e) {
                return 0;
            }

	    var T;
	    if(nextT)
	        T = nextT;
            else
                T = gpx_datetime(pts[ptnum].getElementsByTagName("time")[0].firstChild.nodeValue);

	    if((ptnum%npan) == 0)
                map.panTo(pt);
            if(lastpt) {
	        map.addOverlay(new GPolyline([lastpt, pt], color, 3, 0.9));
                if(fstep)
		    fstep(T, lastpt, pt);
            }

            ptnum++;
            lastpt = pt;

	    if(ptnum >= pts.length) {
	        return 0;
            } else {
                nextT = gpx_datetime(pts[ptnum].getElementsByTagName("time")[0].firstChild.nodeValue);
	        return nextT.valueOf() - T.valueOf();
            }
        }
            
        return redraw;
    }


    var handler = make_handler();
    var timeout_id = null;
    
    // Schedule the redraws with a scaled time interval.
    function go() {
        var dt = handler();
        if(dt > 0)
           timeout_id = setTimeout(go, dt/scale);
        else if(fdone)
           setTimeout(fdone, 500);
    }

    go();

    // Return a function which can be used to stop the animation
    return function() {
               clearTimeout(timeout_id);
               alert("Animation canceled");
           };
}

/**
 * Plot a series of GPX waypoints as markers.If the point has a link associated 
 * with it, use this to construct the HTML for an Info Window.
 *
 * @param     gpx     top-level GPX DOM node
 * @param     map     GMap object
 * @param     xsl     optional XSL stylesheet URL, if present, use
 *                    it to convert the waypoint info to HTML.
 * @param     icon    optional GIcon for each marker
 *
 */
function plot_waypoints(gpx, map, xsl, icon) {
    if(xsl)
        plot_points_xsl(gpx.getElementsByTagName("wpt"), map, xsl, icon);
    else
        plot_points(gpx.getElementsByTagName("wpt"), map, icon);
}

/**
 * Plot a series of GPX waypoints as markers.If the point has a link associated 
 * with it, use this to construct the HTML for an Info Window.
 *
 * @param     rte     GPX rte (route) DOM node
 * @param     map     GMap object
 * @param     xsl     optional XSL stylesheet URL, if present, use
 *                    it to convert the waypoint info to HTML.
 * @param     icon    optional GIcon for each marker
 */
function plot_routepoints(rte, map, xsl, icon) {
    if(xsl)
        plot_points_xsl(rte.getElementsByTagName("rtept"), map, xsl, icon);
    else
        plot_points(rte.getElementsByTagName("rtept"), map, icon);
}

/**
 * Plot a series of GPX points as markers. These points may be waypoints,
 * route-points, or track-points (not recommended because there tend to
 * be large numbers of track points). If the point has a link associated 
 * with it, use this to construct the HTML for an Info Window.
 *
 * @param     pts     list (Array) of GPX points
 * @param     map     GMap object
 * @param     icon    GIcon for each marker (optional)
 */
function plot_points(pts, map, icon) {

    function make_handler(marker, html) {
        return function() {
            marker.openInfoWindowHtml(html);
        }
    }

    if(!icon) {
        icon = new GIcon();
        icon.image = "http://labs.google.com/ridefinder/images/mm_20_red.png";
				icon.shadow = "http://labs.google.com/ridefinder/images/mm_20_shadow.png";
				icon.iconSize = new GSize(12, 20);
				icon.shadowSize = new GSize(22, 20);
				icon.iconAnchor = new GPoint(6, 20);
				icon.infoWindowAnchor = new GPoint(5, 1);
    }

    for(var i = 0;i < pts.length;i++) {
        var marker = new GMarker(new GLatLng(parseFloat(pts[i].getAttribute("lat")),
                                            parseFloat(pts[i].getAttribute("lon"))), icon);
				var links = pts[i].getElementsByTagName("link");
				var desc = pts[i].getElementsByTagName("desc")[0];
				var html = [];

				if(desc) {
				    html.push("<p class='desc'>" + GXml.value(desc) + "</p>");
				}

        for(var j = 0;j < links.length;j++) {
            var url = links[j].getAttribute("href");
            var tnode = links[j].getElementsByTagName("text")[0];
            var text;
            if(tnode) {
                text = GXml.value(tnode);
            } else {
                text = url;
            }
            html.push("<a href='" + url + "' target='_new'>" + text + "</a>");
        }

	
	if(html.length > 0) {
            GEvent.addListener(marker, "click", make_handler(marker, html.join("")));
	}
        map.addOverlay(marker);
    }
}


/**
 * Plot a series of GPX points as markers. These points may be waypoints,
 * route-points, or track-points (not recommended because there tend to
 * be large numbers of track points). The point node is processed with the 
 * supplied stylesheet to create the HTML for the associated Info Window.
 *
 * @param     pts     list (Array) of GPX points
 * @param     map     GMap object
 * @param     xslurl  URL of XSL stylesheet
 * @param     icon    GIcon for each marker (optional)
 */
function plot_points_xsl(pts, map, xslurl, icon) {
    var _xsl = null;
    var IS_MOZ = document.implementation.createDocument;

    // Work around the lack of "disable-output-escape" support
    // in Mozilla's XSL processing. I use CDATA sections to
    // include arbitrary XHTML within a GPX wpt/link/text node
    // so d-o-e support is essential.
    function fetch_xsl(url) {
        if(!_xsl) {
            var req = GXmlHttp.create();
            req.open("GET", url, false);
            req.send(null);
            var doc = GXml.parse(req.responseText);

            _xsl = new XSLTProcessor();
            _xsl.importStylesheet(doc);
        }
        return _xsl;
    }

    function xsl_transform(xmldom, url) {
        var xsl = fetch_xsl(url);
        var html = xsl.transformToFragment(xmldom, 
                          document.implementation.createDocument("", "", null));
        var htmlstr = (new XMLSerializer()).serializeToString(html);

        // Now we need to clean-up the HTML. First we expand the
        // escaped < and > from any CDATA sections and then we
        // need to remove the xhtml namespace modifiers added by
        // createDocument...
        return htmlstr.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/<[a-z0-9]+:/g, "<");
    }

    function make_handler(marker, node) {
        if(IS_MOZ) {
            return function() {
                var s = xsl_transform(node, xslurl);
                marker.openInfoWindowHtml(s);
            };
        } else {
            return function() {
                marker.openInfoWindowXslt(node, xslurl);
            };
        }
    }

    if(!icon) {
        icon = new GIcon();
        icon.image = "http://labs.google.com/ridefinder/images/mm_20_red.png";
				icon.shadow = "http://labs.google.com/ridefinder/images/mm_20_shadow.png";
				icon.iconSize = new GSize(12, 20);
				icon.shadowSize = new GSize(22, 20);
				icon.iconAnchor = new GPoint(6, 20);
				icon.infoWindowAnchor = new GPoint(5, 1);
    }

    for(var i = 0;i < pts.length;i++) {

        var marker = new GMarker(new GLatLng(parseFloat(pts[i].getAttribute("lat")),
                                            parseFloat(pts[i].getAttribute("lon"))), icon);
        var desc = pts[i].getElementsByTagName("desc")[0];
	if(desc) 	{
            GEvent.addListener(marker, "click", make_handler(marker, pts[i]));
						}
        map.addOverlay(marker);
    }
}

/**
 * Create a GPX string from a list of GPoints. The points will be
 * treated as "route points" if 'route' is 1, otherwise they will be
 * treated as "waypoints".
 *
 * @param  points  array of GPoint objects
 * @param  route   if 1, create a route, otherwise waypoints
 * @return         a string containing the GPX file contents
 */
function make_gpx(points, route) {
    var type;
    var strings = [];

    function start_tag(name, attrs, empty) {
        var strings = [];

        strings.push("<" + name );
        for(var name in attrs) {
            strings.push(" " + name + "=\"" + attrs[name] + "\" ");
        }

        if(empty) {
	    strings.push("/>");
        } else {
    	    strings.push(">");
        }

        return strings.join("");
    }

    function end_tag(name) {
        return "</" + name + ">";
    }

    function xml_escape(s) {
        return s.replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;");
    }

    strings.push("<?xml version=\"1.0\" ?>");
    strings.push(start_tag("gpx", {"version" : "1.1",
                                   "creator" : "gmapgpx.js",
                                   "xmlns:xsi" : "http://www.w3.org/2001/XMLSchema-instance",
                                   "xsi:schemaLocation" : "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd",
                                   "xmlns" : "http://www.topografix.com/GPX/1/1"}, 0));
    strings.push(start_tag("metadata", null, 0));
    strings.push(start_tag("desc", null, 0));
    try {
        strings.push(encodeURI(window.location.href));
    } catch(e) {
        strings.push(escape(window.location.href));
    }
    strings.push(end_tag("desc"));   
    strings.push(end_tag("metadata"));

    if(route) {
	strings.push(start_tag("rte", null, 0));
        type = "rtept";
    } else {
        type = "wpt";
    }

    for(var i = 0; i < points.length;i++) {
	strings.push(start_tag(type, {'lat' : points[i].y.toFixed(6),
                                      'lon' : points[i].x.toFixed(6)}, 0));
        try {
            strings.push(start_tag("ele"));
            strings.push(points[i].parasite["ele"].toFixed(2));
            strings.push(end_tag("ele"));
				    strings.push(start_tag("name"));
				    strings.push(xml_escape(points[i].parasite["name"]));
				    strings.push(end_tag("name"));
				    strings.push(start_tag("desc"));
				    strings.push(xml_escape(points[i].parasite["desc"]));
				    strings.push(end_tag("desc"));
			        } catch(e) {
				    strings.push(start_tag("name"));
				    strings.push("GMAP" + i);
				    strings.push(end_tag("name"));
        		}
        strings.push(end_tag(type));
    }
    
    if(route)
        strings.push(end_tag("rte"));
    strings.push(end_tag("gpx"));
    return strings.join("\n");
}

/**
 * Create a GPX XML document from a list of GPoints. The points will be
 * treated as "route points" if 'route' is true, otherwise they will be
 * treated as "waypoints". The function requires the Sarissa Javascript
 * package for cross-browser XML document creation. This function is
 * for the future when browsers properly support loading XML into a
 * popup window.
 *
 * @param  points  array of GPoint objects
 * @param  route   if true, create a route, otherwise waypoints
 * @return         an XML document containing the GPX file contents
 */
function make_gpx_doc(points, route) {
    if(!Sarissa.getDomDocument)
	return null;

    var doc = Sarissa.getDomDocument("http://www.topografix.com/GPX/1/1", "gpx");
    var gpx = doc.documentElement;
    gpx.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
    gpx.setAttribute("xsi:schemaLocation", 
      "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd");
    var node = doc.createElement("metadata");
    var sub = doc.createElement("desc");
    sub.appendChild(doc.createTextNode(window.location.href));
    node.appendChild(sub);
    gpx.appendChild(node);

    var type;
    if(route) {
        node = gpx.appendChild(doc.createElement("rte"));
        type = "rtept";
    } else {
        node = gpx;
        type = "wpt";
    }

    var subelems = ["ele", "name", "desc"];
    for(var i = 0; i < points.length;i++) {
	var pnode = doc.createElement(type);
        pnode.setAttribute("lat", points[i].y.toFixed(6));
        pnode.setAttribute("lon", points[i].x.toFixed(6));
        try {
            for(var j = 0;j < subelems.length;j++) {
                sub = doc.createElement(subelems[j]);
                sub.appendChild(doc.createTextNode(points[i].parasite[subelems[j]]));
                pnode.appendChild(sub);
            }
        } catch(e) {
            sub = doc.createElement("name");
	    sub.appendChild(doc.createTextNode("GMAP" + i));
            pnode.appendChild(sub);
        }
	node.appendChild(pnode);
    }

    return doc;
}

