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


var req_cache = new ReqCache(function() { return GXmlHttp.create(); });

function assert(x) {
    if(!x) {
        if(Error)
            alert("assertion failed: " + Error().stack);
        else
            alert("assertion failed");
    }
}

// Work-around for IE's lack of proper namespace support in their
// DOM interface.
var gpx_ns_prefix = "";
var gpx_ns = "http://www.topografix.com/GPX/1/1";
if(document.getElementsByTagNameNS) {
    var gpxGetElements = function(node, name) {
                             try {
                                 return node.getElementsByTagNameNS(gpx_ns, name);
                             } catch(e) {
                                 alert(e.toString() + "\n" + e.stack);
                                 return null;
                             }
                         };
} else {
    var gpxGetElements = function(node, name) {
                             return node.getElementsByTagName(gpx_ns_prefix + name);
                         };
}

/**
 * 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) {
    // Use a processing function to insure that an XML DOM is
    // passed to the callback even if the content-type was
    // text/plain.
    var parser = function(s) {
                     return GXml.parse(s);
                 };
    req_cache.fetch(url,
                    function(xml) {
                        if(!xml)
                            alert("Document contains no data: " + url);
                        else
                            cback(xml);
                    },
                    null,
                    function(resp) {
                        var doc;
                        if(resp.documentElement)
                            doc = resp;
                        else
                            doc = parser(resp);
                        // Try to find the GPX namespace prefix (IE sucks)
                        var attrs = doc.documentElement.attributes;
                        for(var i = 0;i < attrs.length;i++) {
                            if(attrs[i].nodeValue == gpx_ns) {
                                var name = attrs[i].nodeName;
                                if(name == "xmlns")
                                    gpx_ns_prefix = "";
                                else
                                    gpx_ns_prefix = name.substr(6) + ":";
                            }
                        }

                        return doc;
                    });
}

/**
 * Return the bounds of a GPX file.
 *
 * @param  gpx  top-level GPX DOM node
 * @return      a GBounds object
 */
function get_bounds(gpx) {
    bounds = gpxGetElements(gpx, "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 = gpxGetElements(gpx, "rte");
    for(var j = 0;j < routes.length;j++) {
        var pts = gpxGetElements(routes[j], "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 = gpxGetElements(gpx, "trkseg");
    for(var j = 0;j < segs.length;j++) {
        var pts = gpxGetElements(segs[j], "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 bounds = new GBounds(Number.MAX_VALUE, Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE); 
    var poly = [];
    var j = 0;
    inc = 1;
    if(pts.length>500) inc = 2;
    if(pts.length>1500) inc = 3;
    if(pts.length>2000) inc = 4;
    inc = 1;
    minDist = .01;
    oldTmpPoint = "";
    totalXDist = 0;
		dbg("pts.length="+pts.length+" inc="+inc+" minDist="+minDist);

    for(var i = 0; i<pts.length; i=i+inc) {
    	dbg("i="+i);
        j++;
        tmpPoint = new GPoint(parseFloat(pts[i].getAttribute("lon")),
                              parseFloat(pts[i].getAttribute("lat")));
		if(i == 0) {
			dbg("Starting point x="+tmpPoint.x+" y="+tmpPoint.y);
        	poly.push(tmpPoint);
			bounds.extend(tmpPoint); 	
		}
		else if(i == pts.length-1){
			debug("Last point");
			poly.push(tmpPoint);
			bounds.extend(tmpPoint);
		}
        else {
        	xDist = test_distance(oldTmpPoint, tmpPoint);
        	totalXDist = totalXDist + xDist;
        	debug("totalXDist="+totalXDist);
        	if( totalXDist >= minDist){
	        	poly.push(tmpPoint);
				bounds.extend(tmpPoint); 
				debug("Push now poly.length="+poly.length);
				totalXDist = 0;
			}			
		}
		oldTmpPoint = tmpPoint;
  }
	i=-1;
	dbg("Building route from "+poly.length+" points");
	do {
		i++;
		j=0;
		if(i>0)j=1;
		beg = i*300;
		end = beg + 300 + 1;
		if(end>=poly.length) end = poly.length+1;
		debug("i="+i+" Beg="+beg+" End="+end);
	    map.addOverlay(new GPolyline(poly.slice(beg,end), color, lw, 0.9));
	}while (end < poly.length);
	
    map.centerAndZoomOnBounds(bounds); 	
    
}

function test_distance(point1, point2) {
	var lat1 = point1.x;
	var lon1 = point1.y;
	var lat2 = point2.x;
	var lon2 = point2.y;
	var radlat1 = Math.PI * lat1/180;
	var radlat2 = Math.PI * lat2/180;
	var radlon1 = Math.PI * lon1/180;
	var radlon2 = Math.PI * lon2/180;
	var theta = lon1-lon2;
	var radtheta = Math.PI * theta/180;
	var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
	debug("dist="+dist);
	if(dist>1) dist=1.0;
	dist = Math.acos(dist);
	debug("dist="+dist);
	dist = dist * 180/Math.PI;
	debug("dist="+dist);
	dist = dist * 60 * 1.1515;
	debug("lat1="+lat1+" lon1="+lon1+" lat2="+lat2+" lon2="+lon2+" dist="+dist);
	return dist;
}

/**
 * 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 = gpxGetElements(seg, "trkpt");
        var ptnum = skip;
        var lastpt = null;
        var nextT = null;
        var redraw = function () {
            try {
                pt = new GPoint(parseFloat(pts[ptnum].getAttribute("lon")),
                                parseFloat(pts[ptnum].getAttribute("lat")));
                if(zcolor) {
		    var node = gpxGetElements(pts[ptnum], "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(gpxGetElements(pts[ptnum], "time")[0].firstChild.nodeValue);

	    if((ptnum%npan) == 0)
                map.recenterOrPanToLatLng(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(gpxGetElements(pts[ptnum], "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(gpxGetElements(gpx, "wpt"), map, xsl, icon);
    else
        plot_points(gpxGetElements(gpx, "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(gpxGetElements(rte, "rtept"), map, xsl, icon);
    else
        plot_points(gpxGetElements(rte, "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 GPoint(parseFloat(pts[i].getAttribute("lon")),
                                            parseFloat(pts[i].getAttribute("lat"))), icon);
	var links = gpxGetElements(pts[i], "link");
	var desc = gpxGetElements(pts[i], "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 = gpxGetElements(links[j], "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) {
    function make_handler(marker, node) {
        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 GPoint(parseFloat(pts[i].getAttribute("lon")),
                                            parseFloat(pts[i].getAttribute("lat"))), icon);
        GEvent.addListener(marker, "click", make_handler(marker, pts[i]));
        map.addOverlay(marker);
    }
}

/**
 * Return the bounding box which encloses a set of GPoints.
 *
 * @param  points  array of GPoint objects
 * @return         a GBounds instance
 */
function bounding_box(points) {
    var maxlat = -90;
    var minlat = 90;
    var maxlon = -180;
    var minlon = 180;

    for(var i = 0;i < points.length;i++) {
	if(points[i].x > maxlon)
	    maxlon = points[i].x;
        else if(points[i].x < minlon)
            minlon = points[i].x;

        if(points[i].y > maxlat)
	    maxlat = points[i].y;
        else if(points[i].y < minlat)
	    minlat = points[i].y;
    }

    return new GBounds(minlon, maxlat, maxlon, minlat);
}

/**
 * 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(routeName, points, route) {
dbg("Enter make_gpx");
    var type;
    var strings = [];
    var routeName;

    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;");
    }

    var bounds = bounding_box(points);
    dbg("bounds:"+bounds);

    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));

/* add the creation link to the file load
    try {
        strings.push(encodeURI(window.location.href));
    } catch(e) {
        strings.push(escape(window.location.href));
    }
*/
    strings.push(routeName);
    strings.push(end_tag("desc"));

/*
    strings.push(start_tag("bounds", {"maxlat" : bounds.maxY.toFixed(6),
                                      "maxlon" : bounds.maxX.toFixed(6),
                                      "minlat" : bounds.minY.toFixed(6),
                                      "minlon" : bounds.minX.toFixed(6)}, true));
*/
    strings.push(start_tag("bounds", {"maxlat" : bounds.maxY,
                                      "maxlon" : bounds.maxX,
                                      "minlat" : bounds.minY,
                                      "minlon" : bounds.minX}, true));

    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++) {
    	dbg("lat / lon:"+points[i].y+" / "+points[i].x);
			strings.push(start_tag(type, {'lat' : points[i].y,
                                    'lon' : points[i].x}, 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"));
    dbg("gpx:<br>"+strings);
    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 bounds = bounding_box(points);
    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);
    sub = doc.createElement("bounds");
    sub.setAttribute("maxlat", bounds.maxY.toFixed(6));
    sub.setAttribute("maxlon", bounds.maxX.toFixed(6));
    sub.setAttribute("minlat", bounds.minY.toFixed(6));
    sub.setAttribute("minlon", bounds.minX.toFixed(6));
    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;
}

