//
// Javascript to load a GPS gpx format track and using the Google
// maps API with AJAX draw this map onto a web page. All informaiton
// including times, description etc are taken from the .gpx file
// (which needs to be renamed .xml to be loaded here).
//
// The Title/Description seems to be an extension to the standard
// gpx XML so is dependent on the program creating the gpx file.
// This currently works with Memory-Map v5 and SportTracks gpx
// files.
//
// (C) John Bigg  john at bigg me uk
// 1.0 1st Feb 2008
// 1.1 9th Mar 2008 - minor update for SportTracks v2
// 1.2 29th Jun 2010 - add Terrain/Physical map type
// 2.0 13th Sep 2010 - convert to google api v3
//                see http://code.google.com/apis/maps/documentation/javascript/
// 2.1 26th Jan 2012 - fix to handle tracks without times
//
//<![CDATA[

//
// Globals
var map;			// The map object!
var base;			// URL base
var arrowfreq = -1;		// Arrow frequency/interval
var zoom = -1;			// To override default zoom
var month = new Array (
		"January",
		"February",
		"March",
		"April",
		"May",
		"June",
		"July",
		"August",
		"September",
		"October",
		"November",
		"December" );
var maptypes = new Array (
		google.maps.MapTypeId.ROADMAP,
		google.maps.MapTypeId.SATELLITE,
		google.maps.MapTypeId.HYBRID,
		google.maps.MapTypeId.TERRAIN );

//
// Format and ISO 8601 GPX time in a human readable format.
// The mask specifies if date/time or both are required.
function formatTime(datetime, fmtmask) {
	var dt = datetime.split('T');
	var t = dt[1].replace('Z', ' GMT');
	var d = dt[0].split('-');
	var fdate = d[2] + ' ' + month[d[1] - 1] + ' ' + d[0];
	var fmt = '';
	if (fmtmask & 2) {
		fmt += fdate;
	}
	if (fmtmask & 1) {
		if (fmt) { fmt += ' '; }
		fmt += t;
	}
	return fmt;
}

//
// Calculate elapsed time from 2 clock times assuming there is less than
// 24 hours between them.
function elapsedTime(begin, end) {
	var el = '';
	var bs = seconds(begin);
	var es = seconds(end);
	if (bs > es) { es += 86400 }
	var secs = es - bs;

	var hr = Math.floor(secs / 3600);
	var min = Math.floor((secs % 3600) / 60);
	var sec = secs % 60
	if (hr) { el += hr + ' hr ';}
	el += min + ' min ' + sec + ' sec';
	return el;
}

//
// Return a clock time as a number of seconds since
// midnight.
function seconds(isotime) {
	var t = isotime.split(/T/)[1].replace(/Z/, '').split(':');
	return t[0] * 3600 + t[1] * 60 + parseInt(t[2]);
}

//
// Returns an XMLHttp instance to use for asynchronous
// downloading. This method will never throw an exception, but will
// return NULL if the browser does not support XmlHttp for any reason.
//
function createXmlHttpRequest() {
	try {
		if (typeof ActiveXObject != 'undefined') {
			return new ActiveXObject('Microsoft.XMLHTTP');
		} else if (window["XMLHttpRequest"]) {
			return new XMLHttpRequest();
		}
	} catch (e) {
		changeStatus(e);
	}
	return null;
}

//
// This functions wraps XMLHttpRequest open/send function.
// It lets you specify a URL and will call the callback if
// it gets a status code of 200.
function downloadUrl(url, callback) {
	var status = -1;
	var request = createXmlHttpRequest();
	if (!request) {
		alert("Failed to create Http request");
		return false;
	}

	request.onreadystatechange = function() {
		if (request.readyState == 4) {
			try {
				status = request.status;
			} catch (e) {
			// Usually indicates request timed out in FF.
			}
			if (status == 200) {
				callback(request.responseXML, request.status);
				request.onreadystatechange = function() {};
			} else {
				alert("Could not load GPX file " + url);
			}
		}
	}
	request.open('GET', url, true);
	try {
		request.send(null);
	} catch (e) {
		changeStatus(e);
	}
	return true;
}


function Xmlvalue(node){
	if (!node) {
		return "";
	}
	var retStr = "";
	if (node.nodeType == 3 || node.nodeType == 4 || node.nodeType == 2) {
		retStr += node.nodeValue;
	} else if (node.nodeType == 1 || node.nodeType == 9 || node.nodeType == 11) {
		for(var i=0; i < node.childNodes.length; ++i) {
			retStr += arguments.callee(node.childNodes[i]);
		}
	}
	return retStr;
}

function rad(x) {
	return x * Math.PI/180;
}

//
// Not used now as distVincenty is more accurate
function distHaversine(from, to) {
	var R = 6371000; // earth's mean radius in m
	var dLat  = rad(to.lat() - from.lat());
	var dLong = rad(to.lng() - from.lng());

	var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(rad(from.lat())) * Math.cos(rad(to.lat())) * Math.sin(dLong/2) * Math.sin(dLong/2);
	var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
	var d = R * c;
	return d;
}

// distVincenty
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
/* Vincenty Inverse Solution of Geodesics on the Ellipsoid (c) Chris Veness 2002-2010             */
/*                                                                                                */
/* from: Vincenty inverse formula - T Vincenty, "Direct and Inverse Solutions of Geodesics on the */
/*       Ellipsoid with application of nested equations", Survey Review, vol XXII no 176, 1975    */
/*       http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf                                             */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
function distanceFrom(from, to) {
	var a = 6378137, b = 6356752.314245,  f = 1/298.257223563;  // WGS-84 ellipsoid params
	var L = rad(to.lng() - from.lng());
	var U1 = Math.atan((1-f) * Math.tan(rad(from.lat())));
	var U2 = Math.atan((1-f) * Math.tan(rad(to.lat())));
	var sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);
	var sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);
  
	var lambda = L, lambdaP, iterLimit = 100;
	do {
		var sinLambda = Math.sin(lambda), cosLambda = Math.cos(lambda);
		var sinSigma = Math.sqrt((cosU2*sinLambda) * (cosU2*sinLambda) + 
			(cosU1*sinU2-sinU1*cosU2*cosLambda) * (cosU1*sinU2-sinU1*cosU2*cosLambda));
		if (sinSigma==0) return 0;  // co-incident points
		var cosSigma = sinU1*sinU2 + cosU1*cosU2*cosLambda;
		var sigma = Math.atan2(sinSigma, cosSigma);
		var sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
		var cosSqAlpha = 1 - sinAlpha*sinAlpha;
		var cos2SigmaM = cosSigma - 2*sinU1*sinU2/cosSqAlpha;
		if (isNaN(cos2SigmaM)) cos2SigmaM = 0;  // equatorial line: cosSqAlpha=0
		var C = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha));
		lambdaP = lambda;
		lambda = L + (1-C) * f * sinAlpha *
			(sigma + C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)));
	} while (Math.abs(lambda-lambdaP) > 1e-12 && --iterLimit>0);

	if (iterLimit==0) return NaN  // formula failed to converge

	var uSq = cosSqAlpha * (a*a - b*b) / (b*b);
	var A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
	var B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
	var deltaSigma = B*sinSigma*(cos2SigmaM+B/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)-
		B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM)));
	return b*A*(sigma-deltaSigma);
}

//
// Calculate the bearing in degrees between two points
// with north == 0 increasing clockwise.
function bearing(from, to) {
	// Convert to radians.
	var lat1 = rad(from.lat());
	var lon1 = rad(from.lng());
	var lat2 = rad(to.lat());
	var lon2 = rad(to.lng());

	// Calculate the angle.
	var angle = - Math.atan2( Math.sin( lon1 - lon2 ) * Math.cos( lat2 ),
		Math.cos( lat1 ) * Math.sin( lat2 ) - Math.sin( lat1 ) * Math.cos( lat2 ) * Math.cos( lon1 - lon2 ) );
	if ( angle < 0.0 ) angle += Math.PI * 2.0;

	// Convert back to degrees.
	angle = angle * 180.0 / Math.PI;                // degrees per radian calculation
	angle = angle.toFixed(1);

	return angle;
}

//
// Add an arrow every X points based on the previous and next
// points.
function addArrows(points, bounds) {
	// If we were really fancy we would change the number of arrows
	// as the zoom level is changed with a bounds_changed listerner.
	// This way the distance between the arrows on the screen could
	// always be the same.
	if (arrowfreq == -1) {
		arrowfreq = Math.round(distanceFrom(bounds.getNorthEast(), bounds.getSouthWest())/160);
	}

	// Possibly adjust arrowfreq based on map size
	for (var i=1; i < points.length-1; i++) {
		if (i % arrowfreq == 0) {
			var dir = bearing(points[i-1], points[i+1]);
			// We have icons for every 3 degrees
			dir = Math.round(dir/3) * 3;
			var image = new google.maps.MarkerImage(base + "icons/narrow" + dir + ".png",
				new google.maps.Size(17,17),	// Size
				new google.maps.Point(0,0),	// Origin
				new google.maps.Point(8,8),	// Anchor
				new google.maps.Size(16,16));	// Scaled size
			new google.maps.Marker({ position: points[i], map: map, icon: image });
		}
	}
}

//
// Create a lettered marker.
function letterMarker(point, letter, text) {
	var shadow = new google.maps.MarkerImage(base + "icons/shadow50.png",
		new google.maps.Size(37,34),
		new google.maps.Point(0,0),
		new google.maps.Point(9,34));
	var image = new google.maps.MarkerImage(base + "icons/marker" + letter + ".png",
		new google.maps.Size(20,34),
		new google.maps.Point(0,0),
		new google.maps.Point(9,34));
	// May want to consider a shape so it matches the icon and not a square
	return new google.maps.Marker({ position: point, map:map, icon: image, shadow: shadow, title: text });
}

//
// Main routine to do the map!
function doMap() {
	// Set some defaults
	var trkwidth = 3;
	var trkopacity = 0.8;
	var trkcolour = "#7A96DF"; // #0000C0
	var trkfile = '';
	var miles = 0;	// Don't use miles by default


	// 0 - road, 1 - satellite, 2 - hybrid, 3 - terrain
	var maptype = google.maps.MapTypeId.HYBRID;

	//
	// Get any parameters from the URL
	var URL = document.URL;
	base =  URL.substr(0,URL.lastIndexOf('/')+1);
	var params = URL.replace(/.*\.(html|htm|php)\?/,"").split("&");
	for (var p = 0; p < params.length; p++) {
		var param = params[p].split("=",2);
		switch (param[0]) {
			case 'colour':
			case 'color':
			case 'c':
				trkcolour = '#' + String(param[1]);
				break;

			case 'freq':
			case 'f':
				arrowfreq = parseInt(param[1]);
				break;

			case 'miles':
			case 'm':
				miles = parseInt(param[1]);
				break;

			case 'track':
			case 'trk':
			case 't':
				trkfile = param[1];
				break;

			case 'width':
			case 'w':
				trkwidth = param[1];
				break;

			case 'y':
			case 'type':
				maptype = maptypes[parseInt(param[1])];
				break;

			case 'z':
			case 'zoom':
				zoom = parseInt(param[1]);
				break;
		}
	}

	//
	// Create map object
	map = new google.maps.Map(document.getElementById("map"), {scaleControl:1});

	//
	// Load and parse the GPS GPX (XML) file.
	var XML;

	if (trkfile) {
		XML = base + trkfile + '.xml';
	} else {
		XML = URL.replace(/\.(html|htm|php).*$/,".xml");
	}

	downloadUrl(XML, function(gpxDoc) {
		if( !gpxDoc.documentElement ) {
			alert("Document " + XML + "\nwas not recognized by the XML loader");
		} else {
			var bounds = new google.maps.LatLngBounds();
			//
			// Get track details from GPX file.

			// The description differs on the creator of the GPX file and how
			// we parse this is browser dependent!
			// IE seems to need st:activity whereas mozilla does not want the st:
			// Maybe if I knew XML better I'd uderstand this!
			var activity;
			var description = '';
			// First for SportTracks v1
			activity = gpxDoc.documentElement.getElementsByTagName("st:activity");
			if (!activity.length) { // Try mozilla flavour
				activity = gpxDoc.documentElement.getElementsByTagName("activity");
			}
			if (activity.length) {
				description = activity[0].getAttribute("location");
			// Otherwise SportTracks v2 & Memory-Map
			} else {
				var name = gpxDoc.documentElement.getElementsByTagName("name");
				if (name.length) {
					description = Xmlvalue(name[0]);
				}
			}

			if (!description) { description = 'GPS Track'; }
			document.title =
				document.getElementById("heading").innerHTML =
					description.replace(/\d{2,4}-\d{2}-\d{2,4}\s*/, '');
				// I date some things and I do not want this included.

			//
			// Parse tracks in <trk></trk>
			var trk = gpxDoc.documentElement.getElementsByTagName("trk");
			if (trk.length) {
				//
				// Process each point of track. We ignore all but first track.
				var point;
				var points = [];
				var lat, lon;
				var start, finish;
				var distance = 0;
				var trkpt = trk[0].getElementsByTagName("trkpt");
				for (var k = 0; k < trkpt.length; k++) {
					lat = parseFloat(trkpt[k].getAttribute("lat"));
					lon = parseFloat(trkpt[k].getAttribute("lon"));
					point = new google.maps.LatLng(lat, lon);
					points.push(point);
					bounds.extend(point);
					if (k == 0) {
						// time is in ISO 8601 format
						start = Xmlvalue(trkpt[k].getElementsByTagName("time")[0]);
					} else {
						distance += distanceFrom(point, points[k-1]);
					}
				}

				//
				// Set the centre and fit map to track bounds
				// based on the points of the GPS track (unless
				// and explict zoom is specified).
				if (zoom == -1) {
					map.fitBounds(bounds);
				} else {
					map.setZoom(zoom);
				}
				map.setCenter(bounds.getCenter());
				map.setMapTypeId(maptype);
				var track = new google.maps.Polyline({ path: points, strokeColor: trkcolour,
					strokeOpacity: trkopacity, strokeWeight: trkwidth });
				track.setMap(map);

				addArrows(points, bounds);
				// Convert to km to the nearest 100th.
				if (miles) {
					distance *= 0.621371192;
				}

				distance = Math.round(distance / 10) / 100;
				distance += miles ? ' miles' : ' km';
				finish = Xmlvalue(trkpt[k-1].getElementsByTagName("time")[0]);

				//
				// Add the markers
				// Not all tracks have times, for example those drawn from maps.
				if (start && finish) {
					letterMarker(points[0], 'S', 'Start time: ' + formatTime(start, 1));
					letterMarker(points[k-1], 'F', 'Finish time: ' + formatTime(finish, 1));
				} else {
					letterMarker(points[0], 'S', 'Start');
					letterMarker(points[k-1], 'F', 'Finish');
				}

				//
				// Set text from the XML now we have it
				document.getElementById("distance").innerHTML = distance;
				if (start && finish) {
					document.getElementById("date").innerHTML = formatTime(start, 2);
					document.getElementById("elapsed").innerHTML = elapsedTime(start,finish);
				}
			} // trk
		} // gpxDoc
	});
}

