if (Garmin == undefined) var Garmin = {};
/**
 * Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.Broadcaster is for registering listeners and dispatching call-back metheds.
 * 
 * @author Jason Holmes jason.holmes.at.garmin.com
 * @version 1.0
 */
/** 
 * @class Broadcaster
 * Acts as an event broadcaster.  Code is pretty similar to Ajax.Responders, 
 * but doesn't need to extend Enumerable.  
 * <br><br>
 * To use, register an object, then dispatch methods.<br>
 * var toBeAlerted = new AlertedDude();<br>
 * var broadcaster = new Broadcaster();<br>
 * <br>
 * broadcaster.register(toBeAlerterd);<br>
 * broadcaster.dispatch("alerting");<br>
 * 
 * that will call toBeAlerted.alerting();<br>
 * if you pass an object w/ the dispatch call the object will be passed as well.<br>
 * Most calls are implemented using JSON, with controller as the owning broadcaster
 * object.<br>
 * so ... <br>
 * broadcaster.dispatch("alerting", {message: "howdy", controller: this});<br>
 * toBeAlerted.alerting({message: 'howdy', controller: broadcaster})
 * @constructor 
 */
Garmin.Broadcaster = function(){}; //just here for jsdoc
Garmin.Broadcaster = Class.create();
Garmin.Broadcaster.prototype = {
	initialize: function() {
	    this.responders = new Array();
	},

	/**
     * Register an object to listen for events
     * 
     * @param {Object} responder
     * @member Garmin.Broadcaster
     */
	register: function(responderToAdd) {
	  if (!this.responders.include(responderToAdd))
	    this.responders.push(responderToAdd);
	},

	/**
     * Unregister an object that is listening
     * 
     * @param {Object} responder
     * @member Garmin.Broadcaster
     */
	unregister: function(responderToRemove) {
	  this.responders = this.responders.without(responderToRemove);
	},

	/**
     * Dispatch an event to all listeners
     * 
     * @param {String} callback
     * @param {Object} json
     * @member Garmin.Broadcaster
     */
	dispatch: function(callback, json) {
	  this.responders.each(function(responder) {
	    if (responder[callback] && typeof responder[callback] == 'function') {
	      try {
	        responder[callback].apply(responder, [json]);
	      } catch (e) { alert(e) }
	    }
	  });
	}
};
if (Garmin == undefined) var Garmin = {};
/**
 * @fileoverview BrowserDetect from http://www.quirksmode.org/js/detect.html. Not API.
 */
/**
 * @class BrowserDetect
 * A library for detecting the user's browser and OS
 * Found at http://www.quirksmode.org/js/detect.html
 */
var BrowserDetect = {
	init: function () {
		this.browser = this.searchString(this.dataBrowser) || "An unknown browser";
		this.version = this.searchVersion(navigator.userAgent)
			|| this.searchVersion(navigator.appVersion)
			|| "an unknown version";
		this.OS = this.searchString(this.dataOS) || "an unknown OS";
	},
	searchString: function (data) {
		for (var i=0;i<data.length;i++)	{
			var dataString = data[i].string;
			var dataProp = data[i].prop;
			this.versionSearchString = data[i].versionSearch || data[i].identity;
			if (dataString) {
				if (dataString.indexOf(data[i].subString) != -1)
					return data[i].identity;
			}
			else if (dataProp)
				return data[i].identity;
		}
	},
	searchVersion: function (dataString) {
		var index = dataString.indexOf(this.versionSearchString);
		if (index == -1) return;
		return parseFloat(dataString.substring(index+this.versionSearchString.length+1));
	},
	dataBrowser: [
		{ 	string: navigator.userAgent,
			subString: "OmniWeb",
			versionSearch: "OmniWeb/",
			identity: "OmniWeb"
		},
		{
			string: navigator.vendor,
			subString: "Apple",
			identity: "Safari"
		},
		{
			prop: window.opera,
			identity: "Opera"
		},
		{
			string: navigator.vendor,
			subString: "iCab",
			identity: "iCab"
		},
		{
			string: navigator.vendor,
			subString: "KDE",
			identity: "Konqueror"
		},
		{
			string: navigator.userAgent,
			subString: "Firefox",
			identity: "Firefox"
		},
		{
			string: navigator.vendor,
			subString: "Camino",
			identity: "Camino"
		},
		{		// for newer Netscapes (6+)
			string: navigator.userAgent,
			subString: "Netscape",
			identity: "Netscape"
		},
		{
			string: navigator.userAgent,
			subString: "MSIE",
			identity: "Explorer",
			versionSearch: "MSIE"
		},
		{
			string: navigator.userAgent,
			subString: "Gecko",
			identity: "Mozilla",
			versionSearch: "rv"
		},
		{ 		// for older Netscapes (4-)
			string: navigator.userAgent,
			subString: "Mozilla",
			identity: "Netscape",
			versionSearch: "Mozilla"
		}
	],
	dataOS : [
		{
			string: navigator.platform,
			subString: "Win",
			identity: "Windows"
		},
		{
			string: navigator.platform,
			subString: "Mac",
			identity: "Mac"
		},
		{
			string: navigator.platform,
			subString: "Linux",
			identity: "Linux"
		}
	]

};
BrowserDetect.init();
if (Garmin == undefined) var Garmin = {};
/**
 * Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.DateTimeFormat Utility class for parsing GPX dates and other date support functions.
 * 
 * @author Michael Bina michael.bina.at.garmin.com
 * @version 1.0
 */
/**
 * @class Garmin.DateTimeFormat
 * @constructor 
 */
Garmin.DateTimeFormat = function(){}; //just here for jsdoc
Garmin.DateTimeFormat = Class.create();
Garmin.DateTimeFormat.prototype = {

	initialize: function() {
		this.hoursInADay = 24;
	    this.minutesInAnHour = 60;
	    this.secondsInAMinute = 60;
	    this.millisecondsInASecond = 1000;
	    this.millisecondsInADay = this.hoursInADay * this.minutesInAnHour * this.secondsInAMinute * this.millisecondsInASecond;
	    this.millisecondsInAnHour = this.minutesInAnHour * this.secondsInAMinute * this.millisecondsInASecond;
	    this.millisecondsInAMinute = this.secondsInAMinute * this.millisecondsInASecond;

		this.xsdString = "";
		this.date = new Date();
	},

	/**
     * Get the date object associated with this object
     * 
     * @type Date
     * @return The Date object that 
     */
	getDate: function() {
		return this.date;
	},

	/**
     * Based on 2003-01-31T17:42:14.160Z (YYYY-MM-DDTHH:MM:SS.sssZ) date this will set this 
     * objects date after parsing the standard time string
     * 
     * @param {String} xsdDateTime
     * @type Garmin.DateTimeFormat
     * @return This object with the parsed date
     */
	parseXsdDateTime: function(xsdDateTime) {
		this.xsdString = xsdDateTime;
	    var pieces = xsdDateTime.split('T');
	    var datePiece = pieces[0];
	    var timePiece = pieces[1];
	    var offset = 0;
	
		// xsd:dateTime -> [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]
	
	    // tear apart the date
	    var datePieces = datePiece.split('-');
	    
	    // parse the year
	    var year = parseInt(datePieces[0],10);
	    
	    // parsae the month, javascript month is 0-based
	    var month = parseInt(datePieces[1],10) -1;
	    
	    // parse the day
	    var dayOfMonth = parseInt(datePieces[2],10);
	    
	    // find the index in the timepiece where the offset piece begins
	    var offsetIndex;
	    if (timePiece.indexOf('Z') != -1) {
	    	offsetIndex = timePiece.indexOf('Z');
	    } else if (timePiece.indexOf('+') != -1) {
			offsetIndex = timePiece.indexOf('+');
	    } else if (timePiece.indexOf('-') != -1) {
			offsetIndex = timePiece.indexOf('-');
	    } else {
	    	offsetIndex = timePiece.length;
	    }
	    
	    // tear apart the offset piece
		var offsetPieces = timePiece.substring(offsetIndex).split(':');
		var offsetHour = 0;
		var offsetMinute = 0;
		
		// parse the offset hour and offset minute if they exist
		if (offsetPieces.length > 1) {
			offsetHour = parseInt(offsetPieces[0], 10);
			offsetMinute = parseInt(offsetPieces[1], 10); 
		}
		
		// figure out the time zone offset in milliseconds
		var offsetMilliseconds = ((((offsetHour * 60) + offsetMinute) * 60) * 1000);
		
	    // tear apart the time, exclude offset string
	    var timePieces = timePiece.substring(0, offsetIndex).split(':');
	    
	    // parse the hour
	    var hourBase24 = parseInt(timePieces[0],10);
	    
	    // parse the minute
	    var minute = parseInt(timePieces[1],10);
	    
	    // split the second up for milliseconds
	    var secondPieces = timePieces[2].split('.');
	    
	    // parse the second
	    var second = parseInt(secondPieces[0],10);
	    
	    // parse the millisecond
	    var millisecond = 0;
	    if(secondPieces.length > 1) {
	        millisecond = parseInt(secondPieces[1],10);
	    }
	    
	    // create the date object	    
	    var date = new Date();
	    date.setUTCFullYear(year);
	    date.setUTCMonth(month);
	    date.setUTCDate(dayOfMonth);
	    date.setUTCHours(hourBase24);
	    date.setUTCMinutes(minute);
	    date.setUTCSeconds(second);
	    date.setUTCMilliseconds(millisecond);
	    
	    // apply the time zone offset to the date object so its in utc time
		date.setTime(date.getTime() - offsetMilliseconds);
	    
	    this.date = date;
	    return this;
	},

	/**
     * Generate a duration string of the format hh:mm:ss
     * 
     * @param {Number} Number of milliseconds to convert
     * @type String
     * @return String of the format hh:mm:ss built from the number of milliseconds
     */
	formatDuration: function(milliseconds) {
	    var remaining = milliseconds;
	    var result = "";
	    var separator = ':';
	    var units = new Array(this.millisecondsInADay, this.millisecondsInAnHour, this.millisecondsInAMinute, this.millisecondsInASecond);
	    for(var whichUnit = 0; whichUnit < units.length; whichUnit++) {
	        var millisecondsInUnit = units[whichUnit];
	        var totalOfUnit = parseInt(remaining / millisecondsInUnit);
	        if(whichUnit != 0 || totalOfUnit != 0) {
	            if(totalOfUnit < 10)  result += "0";
	            result = result + totalOfUnit.toString();
	            if(whichUnit < units.length-1) result += separator;
	        }
	        remaining = remaining - (totalOfUnit * millisecondsInUnit);
	    }
	    return result;
	},

	/**
     * Get the duration from this date to another date in the future.
     * 
     * @param {Garmin.DateTimeFormat} The end date to get the duration to.
     * @type String
     * @return Duration string (hh:mm:ss)
     */
	getDurationTo: function(endDateTime) {
	    return this.formatDuration(endDateTime.getDate().getTime() - this.getDate().getTime());
	},

	/**
     * getDayOfYear
     * http://www.merlyn.demon.co.uk/js-date0.htm
     */
	getDayOfYear: function() {
	    with (this.getDate()) {
	        var Y = getFullYear(), M = getMonth(), D = getDate();
	    }
	    var K, N;
	    N = (Date.UTC(Y, M, D) - Date.UTC(Y, 0, 0)) / 86400000;
	
	    M++;
	    K = 2 - (Y % 4 == 0);
	    N = Math.floor(275 * M / 9) - K * (M > 2) + D - 30;
	
	    with (this.getDate()) {
	        K = valueOf();
	        setMonth(0);
	        setDate(0);
	        N = Math.round((K - valueOf()) / 86400000);
	    }
		return N;
	},

	/** Formats date.
	 * Uses Garmin.DateTimeFormat.FORMAT.date format string.
	 * @type String
	 * @return Date string of the format mm/dd/yyyy
     */
	getDateString: function() {
		return this.formatDate(true);
	},

	/** Formats timestamp using 12 hour clock.
	 * @type String
	 * @return formatted timestamp
     */
	getTimeString: function() {
		return this.format(Garmin.DateTimeFormat.FORMAT.timestamp12hour, true, true);
	},

	/**
	 * @type String
	 * @return Xsd date string of the format mm/dd/yyyy
     * @member Garmin.DateTimeFormat
     */
	getXsdString: function() {
		return this.xsdString;
	},

	/**
	 * @type String
	 * @return this.date.toString()
     */
	toString: function() {
		return this.date.toString();
	},
	
	/** Format date using Garmin.DateTimeFormat.FORMAT.date template. 
	 * @param {Boolean} if true single digits have a zero inserted on the left
	 * @type String
	 * @return formatted date
	 */
	formatDate: function(leftPad) {
		return this.format(Garmin.DateTimeFormat.FORMAT.date, leftPad);
	},
	
	/** Format time using Garmin.DateTimeFormat.FORMAT.time template. 
	 * @param {Boolean} if true single digits have a zero inserted on the left
	 * @type String
	 * @return formatted time
	 */
	formatTime: function(leftPad) {
		return this.format(Garmin.DateTimeFormat.FORMAT.time, leftPad);
	},
	
	/** Format timestamp using Garmin.DateTimeFormat.FORMAT.timestamp template. 
	 * @param {Boolean} if true single digits have a zero inserted on the left
	 * @type String
	 * @return formatted timestamp
	 */
	formatTimestamp: function(leftPad) {
		return this.format(Garmin.DateTimeFormat.FORMAT.timestamp, leftPad);
	},
	
	/** Applies template to date fields.  
	 * Valid fields are: month, day, year, hour, minute, second, millisecond and meridian (AM or PM)
	 * timezone is also available but is known to be unreliable.
	 * Uses prototype Template object.
	 * @param {String} template which specifies formatting
	 * @param {Boolean} leftPad if true single digits have a zero inserted on the left. Defaults to true.
	 * @param {Boolean} twelveHourClock if true set clock to 12 hour verses 24
	 * @type String
	 * @return formatted date
     */
	format: function(template, leftPad, twelveHourClock) {
		if (leftPad!=false)
			leftPad = true;
		var hours = this.date.getHours();
		var values = {
			meridian: this.date.getHours() >= 12 ? "PM" : "AM",
			month: this.leftPad(this.date.getMonth()+1, leftPad), 
			day: this.leftPad(this.date.getDate(), leftPad), 
			year: this.date.getFullYear(), 
			hour: this.leftPad( (twelveHourClock && hours > 12) ? hours - 12 : hours, leftPad), 
			minute: this.leftPad(this.date.getMinutes(), leftPad), 
			second: this.leftPad(this.date.getSeconds(), leftPad),
			millisecond: this.date.getMilliseconds(),
			timezone: this.date.getTimezoneOffset() / 60
		};
		return new Template(template).evaluate(values);
	},
	
	/** left pad integer if activate is true
	 * @param {Number} integer to left pad
	 * @param {Boolean} activate must be true or no padding is done
	 * @type String
	 */
	leftPad: function(integer, activate) {
		return (activate && integer < 10) ? "0" + integer : "" + integer;
	}
}

/** Internationalization constants for date/time formatting.
 */
Garmin.DateTimeFormat.FORMAT = {
	date: "#{month}/#{day}/#{year}",
	time: "#{hour}:#{minute}:#{second}",
	timestamp: "#{month}/#{day}/#{year} #{hour}:#{minute}:#{second}",
	timestamp12hour: "#{month}/#{day}/#{year} #{hour}:#{minute}:#{second} #{meridian}"
};


if (Garmin == undefined) var Garmin = {};
/** Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.

 * @fileoverview PluginDetect from http://developer.apple.com/internet/webcontent/detectplugins.html. Not API.
 *
 * @author Bobby Yang bobby.yang.at.garmin.com
 * @version 1.0
 
 */
/** A library for detecting the browser's plugins, by Apple
 * Found at http://developer.apple.com/internet/webcontent/detectplugins.html
 *
 * Modification has been made to the original source.
 * @class PluginDetect
 */
var detectableWithVB = false;
var PluginDetect = {
	
	init: function() {
		// Here we write out the VBScript block for MSIE Windows
		if ((navigator.userAgent.indexOf('MSIE') != -1) 
			&& (navigator.userAgent.indexOf('Win') != -1)) {
		    document.writeln('<script language="VBscript">');
		
		    document.writeln('\'do a one-time test for a version of VBScript that can handle this code');
		    document.writeln('detectableWithVB = False');
		    document.writeln('If ScriptEngineMajorVersion >= 2 then');
		    document.writeln('  detectableWithVB = True');
		    document.writeln('End If');
		
		    document.writeln('\'this next function will detect most plugins');
		    document.writeln('Function detectActiveXControl(activeXControlName)');
		    document.writeln('  on error resume next');
		    document.writeln('  detectActiveXControl = False');
		    document.writeln('  If detectableWithVB Then');
		    document.writeln('     detectActiveXControl = IsObject(CreateObject(activeXControlName))');
		    document.writeln('  End If');
		    document.writeln('End Function');
		
		    document.writeln('</script>');
		}
	},
	
	canDetectPlugins: function() {
	    if( detectableWithVB || (navigator.plugins && navigator.plugins.length > 0) ) {
			return true;
	    } else {
			return false;
	    }			
	},
	
	detectFlash: function() {
	    var pluginFound = PluginDetect.detectPlugin('Shockwave','Flash'); 
	    // if not found, try to detect with VisualBasic
	    if(!pluginFound && detectableWithVB) {
			pluginFound = detectActiveXControl('ShockwaveFlash.ShockwaveFlash.1');
	    }
	    // check for redirection
	    return pluginFound;
	},
	
	detectGarminCommunicatorPlugin: function() {
	    var pluginFound = PluginDetect.detectPlugin('Garmin Communicator');
	    // if not found, try to detect with VisualBasic
	    if(!pluginFound && detectableWithVB) {
			pluginFound = detectActiveXControl('GARMINAXCONTROL.GarminAxControl_t.1');
	    }
	    return pluginFound;		
	},
	
	detectPlugin: function() {
	    // allow for multiple checks in a single pass
	    var daPlugins = PluginDetect.detectPlugin.arguments;
	    // consider pluginFound to be false until proven true
	    var pluginFound = false;
	    // if plugins array is there and not fake
	    if (navigator.plugins && navigator.plugins.length > 0) {
			var pluginsArrayLength = navigator.plugins.length;
			// for each plugin...
			for (pluginsArrayCounter=0; pluginsArrayCounter < pluginsArrayLength; pluginsArrayCounter++ ) {
			    // loop through all desired names and check each against the current plugin name
			    var numFound = 0;
			    for(namesCounter=0; namesCounter < daPlugins.length; namesCounter++) {
				// if desired plugin name is found in either plugin name or description
					if( (navigator.plugins[pluginsArrayCounter].name.indexOf(daPlugins[namesCounter]) >= 0) || 
					    (navigator.plugins[pluginsArrayCounter].description.indexOf(daPlugins[namesCounter]) >= 0) ) {
					    // this name was found
					    numFound++;
					}   
			    }
			    // now that we have checked all the required names against this one plugin,
			    // if the number we found matches the total number provided then we were successful
			    if(numFound == daPlugins.length) {
					pluginFound = true;
					// if we've found the plugin, we can stop looking through at the rest of the plugins
					break;
			    }
			}
	    }
	    return pluginFound;		
	}
}

PluginDetect.init();
if (Garmin == undefined) var Garmin = {};
/**
 * Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.XmlConverter A class for converting between xml strings and DOM objects.
 * 
 * @author Jason Holmes jason.holmes.at.garmin.com
 * @version 1.0
 */
/**
 * @class Garmin.XmlConverter
 * Convert XML text to a DOM and back.
 * @constructor 
 */
Garmin.XmlConverter = function(){}; //just here for jsdoc
Garmin.XmlConverter = {
    /**
     * Returns an xml document based on the string passed in
     * @param {String} fromString is the xml string to convert
     * @return {Document}
     * @member Garmin.XmlConverter
     */
    toDocument: function(fromString) {
        return Try.these(
            function() {
    		    var theDocument = new ActiveXObject("Microsoft.XMLDOM");
    		    theDocument.async = "false";
    		    theDocument.loadXML( fromString );
    		    return theDocument;
            },
            function() {
    		    return new DOMParser().parseFromString(fromString, "text/xml");
            }
        );        
    },
    
    /**
     * Converts a document to a string, and then returns the string
     * @param {Document} fromDocument is the DOM Object to convert
     * @return {String}
     * @member Garmin.XmlConverter
     */  
    toString: function(fromDocument) {
		if( window.ActiveXObject ) {
			return fromDocument.xml
		}
		else {
			var theXmlSerializer = new XMLSerializer();
			return theXmlSerializer.serializeToString( fromDocument );
		}
    }
};
if (Garmin == undefined) var Garmin = {};
/**
 * Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.Measurement - A datastructure designed to contain a single data measurement.
 * 
 * @author Bobby Yang bobby.yang.at.garmin.com
 * @version 1.0
 */
/**Represent a real measurement.
 * @class Garmin.Measurement
 * @constructor 
 * @param value - value of the measurement
 * @param context - the context of the measurement (feet, seconds, etc...)
 */
Garmin.Measurement = function(value, context){};
Garmin.Measurement = Class.create();
Garmin.Measurement.prototype = {

	initialize: function(value, context) {
		this.value = value;
		this.context = context;
	},
	
	getContext: function() {
		return this.context;
	},
	
	setContext: function(context) {
		this.context = context;
	},
	
	getValue: function() {
		return this.value;
	},
	
	setValue: function(value) {
		this.value = value;
	},
	
	printMe: function(tabs) {
		var output = "";
		output += tabs + "  [Measurement]\n";
		output += tabs + "    value: " + this.value + '\n';
		//output += tabs + "    context: " + this.context + '\n';
		return output;
	},
	
	toString: function() {
		return this.value + " " + this.context;
	}
};

if (Garmin == undefined) var Garmin = {};
/**
 * Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.Sample - A datastructure designed to contain a number of measurements
 * 									recorded at a single point.
 * 
 * @author Bobby Yang bobby.yang.at.garmin.com
 * @version 1.0
 */
/**A collection of measurements recorded at a single point.
 * @class Garmin.Sample
 * @constructor 
 */
Garmin.Sample = function(){};
Garmin.Sample = Class.create();
Garmin.Sample.prototype = {

	initialize: function() {
		// lazy loading related values
		this.isLazyLoaded = false;
		this.factory = null;
		this.dom = null;
		
		// measurements of this sample
		this.measurements = new Hash();
	},

	setLazyLoading: function(isLazyLoaded, factory, dom) {
		this.isLazyLoaded = isLazyLoaded;
		this.factory = factory;
		this.dom = dom;
	},

	getMeasurements: function() {
		this.finishLoading();
		return this.measurements;
	},
	
	getMeasurement: function(mKey) {
		this.finishLoading();
		return this.measurements[mKey];
	},
	
	getMeasurementValue: function(mKey) {
		this.finishLoading();
		return (this.measurements[mKey] ? this.measurements[mKey].getValue() : null);
	},

	getMeasurementContext: function(mKey) {
		this.finishLoading();
		return (this.measurements[mKey] ? this.measurements[mKey].getContext() : null);
	},
	
	getLatitude: function() {
		return this.getMeasurementValue(Garmin.Sample.MEASUREMENT_KEYS.latitude);
	},
	
	getLongitude: function() {
		return this.getMeasurementValue(Garmin.Sample.MEASUREMENT_KEYS.longitude);
	},	
	
	getTime: function() {
		return this.getMeasurementValue(Garmin.Sample.MEASUREMENT_KEYS.time);
	},
	
	setMeasurement: function(mKey, mValue, mContext) {
		// if the key does not exist or is not of type Garmin.Measurement, create a new measurement object
		// else overwrite existing value and context
		if (!this.measurements[mKey] || !(this.measurements[mKey] instanceof Garmin.Measurement)) {
			this.measurements[mKey]= new Garmin.Measurement(mValue, mContext);
		} else {
			this.measurements[mKey].setValue(mValue);
			this.measurements[mKey].setContext(mContext);
		}
	},
	
	/** Determines if this Sample is valid for determing location
	 * @type Boolean
	 * @return True if latitude and longitude exist, false otherwise
	 */
    isValidLocation: function() {
    	var latitude = this.getMeasurement(Garmin.Sample.MEASUREMENT_KEYS.latitude);
    	var longitude = this.getMeasurement(Garmin.Sample.MEASUREMENT_KEYS.latitude);    	
        return ((latitude != null && latitude.getValue() != null) && 
        		(longitude != null && longitude.getValue() != null));
    },	
	
	/** Finish loading the measurements for this sample if previously lazy-loaded.
	 */
	finishLoading: function() {
		if (this.isLazyLoaded) {
			this.factory.finishLoadingSample(this.dom, this);
		}	
	},	
	
	printMe: function(tabs) {
		var output = ""
		output += tabs + "  [Sample]\n";	
		
		var measKeys = this.measurements.keys();
		for (var i = 0; i < measKeys.length; i++) {
			output += tabs + "    " + measKeys[i] + ":\n";	
			output += this.measurements[measKeys[i]].printMe(tabs + "    "); 
		}
		
		return output;
	},
	
	toString: function() {
		return "[Garmin.Sample]"
	}
};

Garmin.Sample.MEASUREMENT_KEYS = {
	cadence:			"cadence",
	distance:			"distance",
	elevation:			"elevation",
	heartRate:			"heartRate",
	latitude:			"latitude",
	longitude:			"longitude",
	sensorState:		"sensorState",
	time:				"time"
};
/*
// Dynamic include of required libraries and check for Prototype
// Code taken from scriptaculous
// TODO: put this code in a library and reuse is instead of copying it to new files
var GarminSample = {
	require: function(libraryName) {
	  // inserting via DOM fails in Safari 2.0, so brute force approach
	  document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
	},

	load: function() {
		if((typeof Prototype=='undefined') || 
			(typeof Element == 'undefined') || 
			(typeof Element.Methods=='undefined') ||
			parseFloat(Prototype.Version.split(".")[0] + "." +
			Prototype.Version.split(".")[1]) < 1.5) {
			throw("GarminSample requires the Prototype JavaScript framework >= 1.5.0");
		}

		$A(document.getElementsByTagName("script"))
		.findAll(
			function(s) {
				return (s.src && s.src.match(/GarminSample\.js(\?.*)?$/))
			}
		)
		.each(
			function(s) {
				var path = s.src.replace(/GarminSample\.js(\?.*)?$/,'../../');
				var includes = s.src.match(/\?.*load=([a-z,]*)/);
				(includes ? includes[1] : 'garmin/activity/GarminMeasurement').split(',').each(
					function(include) {
						GarminSample.require(path+include+'.js') 
					}
				);
			}
		);
	}
}

GarminSample.load();*/
if (Garmin == undefined) var Garmin = {};
/**
 * Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.Series - A datastructure designed to contain a series of Garmin.Sample.
 * 
 * @author Bobby Yang bobby.yang.at.garmin.com
 * @version 1.0
 */
/**Contains a series of samples.  Could represent tracks, routes, waypoints, and
 * many other types of data.
 * @class Garmin.Series
 * @constructor 
 * @param (String) type - the type of data this series contain.  Should be determined by what
 * 							information is recorded by the samples this series contain.
 */
Garmin.Series = function(type){};
Garmin.Series = Class.create();
Garmin.Series.prototype = {

	initialize: function(seriesType) {
		this.seriesType = seriesType;
		this.samples = new Array();
	},

	getSeriesType: function() {
		return this.seriesType;
	},

	setSeriesType: function(seriesType) {
		this.seriesType = seriesType;
	},

	getSamples: function() {
		return this.samples;		
	},
	
	getSample: function(index) {
		var targetSample = null;
		if (index >= 0 && index < this.samples.length) {
			targetSample = this.samples[index];
		}
		return targetSample;
	},
	
	addSample: function(sample) {
		if (sample != null) {
			this.samples.push(sample);			
		}
	},
	
	getSamplesLength: function() {
		return this.samples.length;
	},

	getFirstValidLocationSample: function() {
		return this.findNearestValidLocationSample(0, 1);
	},
	
	getLastValidLocationSample: function() {
		return this.findNearestValidLocationSample(this.getSamplesLength()-1, -1);
	},
    
    /** Find the nearest valid location point to the index given
     * 
     * @param index is the index
     * @param incDirection is an int in the direction we'd like to look positive 
     * 	nums are forward, negative nums are backwards
     * 
     * @type Garmin.Sample 
     * @return The nearest point (possibly the index) that has a valid latitude and longitude
     */ 
    findNearestValidLocationSample: function(index, incDirection) {
		return this._findNearestValidLocationSampleInternal(index, incDirection, 0);
    },
	
	_findNearestValidLocationSampleInternal: function(index, incDirection, count) {
		// make sure we haven't looped through every element already
		if (this.getSamplesLength() > 0 && count < this.getSamplesLength()) {
			// make sure index requested is within bounds
			if (index >= 0 && index < this.getSamplesLength()) {
				var sample = this.getSample(index);
				if (sample.isValidLocation()) {
					return sample;	
				} else {
					return this._findNearestValidLocationSampleInternal(index + incDirection, incDirection, ++count);
				}
			} else if (index > this.getSamplesLength()) {
				return this._findNearestValidLocationSampleInternal(this.getSamplesLength()-1, -1, count);
			} else {
				return this._findNearestValidLocationSampleInternal(0, 1, count);
			}	
		} else {
			return null;
		}		
	},
	
	printMe: function(tabs) {
		var output = tabs + "  [Series]\n";
		output += tabs + "    seriesType: " + this.seriesType + "\n";	
		output += tabs + "    samples:\n";
		for (var i = 0; i < this.samples.length; i++) {
			output += this.samples[i].printMe(tabs + "    ");
		}
		return output;
	},
	
	toString: function() {
		return "[Series]";
	}
};

Garmin.Series.TYPES = {
	history:		"history",
	route:			"route",
	waypoint:		"waypoint",
	course:			"course"
};
/*
// Dynamic include of required libraries and check for Prototype
// Code taken from scriptaculous
// TODO: put this code in a library and reuse is instead of copying it to new files
var GarminSeries = {
	require: function(libraryName) {
	  // inserting via DOM fails in Safari 2.0, so brute force approach
	  document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
	},

	load: function() {
		if((typeof Prototype=='undefined') || 
			(typeof Element == 'undefined') || 
			(typeof Element.Methods=='undefined') ||
			parseFloat(Prototype.Version.split(".")[0] + "." +
			Prototype.Version.split(".")[1]) < 1.5) {
			throw("GarminSeries requires the Prototype JavaScript framework >= 1.5.0");
		}

		$A(document.getElementsByTagName("script"))
		.findAll(
			function(s) {
				return (s.src && s.src.match(/GarminSeries\.js(\?.*)?$/))
			}
		)
		.each(
			function(s) {
				var path = s.src.replace(/GarminSeries\.js(\?.*)?$/,'../../');
				var includes = s.src.match(/\?.*load=([a-z,]*)/);
				var dependencies = 'garmin/activity/GarminMeasurement' +
									',garmin/activity/GarminSample';
			    (includes ? includes[1] : dependencies).split(',').each(
					function(include) {
						GarminSeries.require(path+include+'.js') 
					}
				);
			}
		);
	}
}

GarminSeries.load();*/
if (Garmin == undefined) var Garmin = {};
/**
 * Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.Activity A data structure representing an activity
 * 
 * @author Bobby Yang bobby.yang.at.garmin.com
 * @version 1.0
 */
/**A data structure for storing data commonly found in various
 * formats supported by various gps devices.  Some examples are
 * gpx track, gpx route, gpx wayoint, and tcx activity.
 * @class Garmin.Activity
 * @constructor 
 */
Garmin.Activity = function(){};
Garmin.Activity = Class.create();
Garmin.Activity.prototype = {
	
	initialize: function() {
		this.attributes = new Hash();
		this.summary = new Garmin.Sample();
		this.series = new Array();
	},
	
	getAttributes: function() {
		return this.attributes;
	},
	
	getAttribute: function(aKey) {
		return this.attributes[aKey];
	},
	
	setAttribute: function(aKey, aValue) {
		this.attributes[aKey] = aValue;
	},
	
	getSeries: function() {
		return this.series;
	},
	
	getHistorySeries: function() {
		for (var i = 0; i < this.series.length; i++) {
			if (this.series[i].getSeriesType() == Garmin.Series.TYPES.history) {
				return this.series[i];
			}
		}
		return null;
	},
	
	addSeries: function(series) {
		this.series.push(series);
	},
	
	getSingleSeries: function(index) {
		var targetSeries = null;
		if (index >= 0 && index < this.series.length) {
			targetSeries = this.series[index];
		}
		return targetSeries;
	},
	
	getSummary: function() {
		return this.summary;
	},
	
	getSummaryValue: function(sKey) {
		return this.summary.getMeasurement(sKey);
	},
	
	setSummaryValue: function(sKey, sValue, sContext) {
		this.summary.setMeasurement(sKey, sValue, sContext);
	},
	
	getEndTime: function() {
		return this.getSummaryValue(Garmin.Activity.SUMMARY_KEYS.endTime).getValue();
	},	
	
	getStartTime: function() {
		return this.getSummaryValue(Garmin.Activity.SUMMARY_KEYS.startTime).getValue();
	},
	
	printMe: function(tabs) {
		var output = "";
		output += tabs + "\n\n[Activity]\n";
		
		output += tabs + "  attributes:\n";
		var attKeys = this.attributes.keys();
		for (var i = 0; i < attKeys.length; i++) {
			output += tabs + "    " + attKeys[i] + ": " + this.attributes[attKeys[i]] + "\n"; 
		}
		
		output += tabs + "  summary:\n";
		output += this.summary.printMe(tabs + "  ");

		output += tabs + "  series:\n";		
		for (var i = 0; i < this.series.length; i++) {
			output += this.series[i].printMe(tabs + "  ");
		}
		
		return output;
	},
	
	toString: function() {
		return "[Garmin.Activity]"
	}
};

Garmin.Activity.ATTRIBUTE_KEYS = {
	activityName:		"activityName",
	activitySport:		"activitySport",
	creatorName:		"creatorName",
	creatorUnitId:		"creatorUnitId",
	creatorProdId:		"creatorProductId",
	creatorVersion:		"creatorVersion",
	dom:				"documentObjectModel"
};

Garmin.Activity.SECTION_KEYS = {
	gpsSignals:			"gpsSignal",
	heartRateSignals:	"heartRateSignal",	
	laps:				"laps",
	tracks:				"tracks"
};

Garmin.Activity.SUMMARY_KEYS = {
	avgHeartRate:		"averageHeartRate",
	calories:			"calories",	
	endTime:			"endTime",
	intensity:			"intensity",	
	maxHeartRate:		"maximumHeartRate",
	maxSpeed:			"maximumSpeed",		
	startTime:			"startTime",
	totalDistance:		"totalDistance",	
	totalTime:			"totalTime"
};
/*
// Dynamic include of required libraries and check for Prototype
// Code taken from scriptaculous
// TODO: put this code in a library and reuse is instead of copying it to new files
var GarminActivity = {
	require: function(libraryName) {
	  // inserting via DOM fails in Safari 2.0, so brute force approach
	  document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
	},

	load: function() {
		if((typeof Prototype=='undefined') || 
			(typeof Element == 'undefined') || 
			(typeof Element.Methods=='undefined') ||
			parseFloat(Prototype.Version.split(".")[0] + "." +
			Prototype.Version.split(".")[1]) < 1.5) {
			throw("GarminActivity requires the Prototype JavaScript framework >= 1.5.0");
		}

		$A(document.getElementsByTagName("script"))
		.findAll(
			function(s) {
				return (s.src && s.src.match(/GarminActivity\.js(\?.*)?$/))
			}
		)
		.each(
			function(s) {
				var path = s.src.replace(/GarminActivity\.js(\?.*)?$/,'../../');
				var includes = s.src.match(/\?.*load=([a-z,]*)/);
				var dependencies = 'garmin/activity/GarminMeasurement' +
									',garmin/activity/GarminSample' +
									',garmin/activity/GarminSeries';
			    (includes ? includes[1] : dependencies).split(',').each(
					function(include) {
						GarminActivity.require(path+include+'.js') 
					}
				);
			}
		);
	}
}

GarminActivity.load();*/
if (Garmin == undefined) var Garmin = {};
/**
 * Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.GpxActivityFactory - A factory for producing gpx activity and data.
 * 
 * @author Bobby Yang bobby.yang.at.garmin.com
 * @version 1.0
 */
/**A factory that can produce an array activity given gpx xml and produce gps xml given an
 * array of activity.
 * many other types of data.
 * @class Garmin.GpxActivityFactory
 * @constructor 
 */
Garmin.GpxActivityFactory = function(){};
Garmin.GpxActivityFactory = {
	
	parseString: function(gpxString) {
		var gpxDocument = Garmin.XmlConverter.toDocument(gpxString);		
		return Garmin.GpxActivityFactory.parseDocument(gpxDocument);	
	},
	
	parseDocument: function(gpxDocument) {
        return this.parseDocumentByType(gpxDocument, Garmin.GpxActivityFactory.GPX_TYPE.all);
	},
	
	parseDocumentByType: function(gpxDocument, type) {
		var activities = new Array();
        var routes = new Array();
        var tracks = new Array();
        var waypoints = new Array();        
        
        switch(type) {
          case Garmin.GpxActivityFactory.GPX_TYPE.routes:
              activities = Garmin.GpxActivityFactory._parseGpxRoutes(gpxDocument);
              break;
          case Garmin.GpxActivityFactory.GPX_TYPE.waypoints:
              activities = Garmin.GpxActivityFactory._parseGpxWaypoints(gpxDocument);
              break;
          case Garmin.GpxActivityFactory.GPX_TYPE.tracks:
              activities = Garmin.GpxActivityFactory._parseGpxTracks(gpxDocument);
              break;
          case Garmin.GpxActivityFactory.GPX_TYPE.all:
              routes = Garmin.GpxActivityFactory._parseGpxRoutes(gpxDocument);
              tracks = Garmin.GpxActivityFactory._parseGpxTracks(gpxDocument);
              waypoints = Garmin.GpxActivityFactory._parseGpxWaypoints(gpxDocument);
              activities = waypoints.concat(routes).concat(tracks);
              break;    
        }
         
        return activities;
	},
	
	produceString: function(activities) {
		var gpxString = "";
		
		// default creator information incase we can't find the creator info in the dom
		var creator = Garmin.GpxActivityFactory.DETAIL.creator;
		
		// default metadata information incase we can't find the metadata node in the dom
		var metadata = "\n  <metadata>";
		metadata += "\n    <link href=\"http://www.garmin.com\">";
		metadata += "\n      <text>Garmin International</text>";
		metadata += "\n    </link>";						
		metadata += "\n  </metadata>";
		
		// try to find creator and metadata info in the dom
		if (activities != null && activities.length > 0) {
			var activityDom = activities[0].getAttribute(Garmin.Activity.ATTRIBUTE_KEYS.dom);
			var gpxNode = activityDom.ownerDocument.getElementsByTagName(Garmin.GpxActivityFactory.SCHEMA_TAGS.gpx);
			if (gpxNode.length > 0) {
				// grab creator information from the dom if possible
				var creatorStr = gpxNode[0].getAttribute(Garmin.GpxActivityFactory.SCHEMA_TAGS.creator);
				if (creatorStr != null && creatorStr != "") {
					creator = creatorStr;
				}
				// grab metadata info
				var metadataNode = gpxNode[0].getElementsByTagName(Garmin.GpxActivityFactory.SCHEMA_TAGS.metadata);
				if (metadataNode.length > 0) {
					metadata = Garmin.XmlConverter.toString(metadataNode[0]);
				}
			}
		}

		// header tags
		gpxString += "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>";	
		gpxString += "\n<gpx xmlns=\"http://www.topografix.com/GPX/1/1\" creator=\"" + creator + "\" version=\"1.1\" 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 http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensions/v3/GpxExtensionsv3.xsd\">";
	
		//metadata tag
		gpxString += "\n  " + metadata;
		
		if (activities != null) {
			// waypoint and track tags
			for(var i = 0; i < activities.length; i++) {		
				gpxString += "\n  " + Garmin.GpxActivityFactory._produceActivityString(activities[i]);
			}
		}
		
		// footer tags
		gpxString += "\n</gpx>";
		
		return gpxString;
	},
	
	/** Fully load the sample, assume sample was previously lazy-loaded
	 */	
	finishLoadingSample: function(domNode, sample) {
		if (domNode.nodeName == Garmin.GpxActivityFactory.SCHEMA_TAGS.routePoint) {
			Garmin.GpxActivityFactory._parseGpxRoutePoint(domNode, sample);
			sample.isLazyLoaded = false;
		} else if (domNode.nodeName == Garmin.GpxActivityFactory.SCHEMA_TAGS.trackPoint) {
			Garmin.GpxActivityFactory._parseGpxTrackPoint(domNode, sample);
			sample.isLazyLoaded = false;
		}
	},		
	
	_produceActivityString: function(activity) {
		var activityString = "";
		if (activity != null) {
			var series = activity.getSeries();
			for (var i = 0; i < series.length; i++) {
				var currentSeries = series[i];
				if (currentSeries.getSeriesType() == Garmin.Series.TYPES.history) {
					// converting the dom back into string
					// this is the lazy way, this will not work if 
					// converting between file types or activity data
					// has been modified.
					var activityDom = activity.getAttribute(Garmin.Activity.ATTRIBUTE_KEYS.dom);			
					if (activityDom != null) {
						activityString = Garmin.XmlConverter.toString(activityDom);
					}
				} else if (currentSeries.getSeriesType() == Garmin.Series.TYPES.waypoint) {
					// converting the dom back into string
					// this is the lazy way, this will not work if 
					// converting between file types or activity data
					// has been modified.
					var activityDom = activity.getAttribute(Garmin.Activity.ATTRIBUTE_KEYS.dom);			
					if (activityDom != null) {
						activityString = Garmin.XmlConverter.toString(activityDom);
					}						
				}
			}
		}		
		return activityString;
	},
	
	_parseGpxRoutes: function(gpxDocument) {
		var routes = new Array();
    	var routeNodes = gpxDocument.getElementsByTagName(Garmin.GpxActivityFactory.SCHEMA_TAGS.route);

		for( var i=0; i < routeNodes.length; i++ ) {
			var route = new Garmin.Activity();
			
			var routeName = Garmin.GpxActivityFactory._tagValue(routeNodes[i], Garmin.GpxActivityFactory.SCHEMA_TAGS.routeName);
			if (routeName == null) {
				routeName = "";
			}
			
			route.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.dom, routeNodes[i]);
			route.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.activityName, routeName);

			var series = new Garmin.Series(Garmin.Series.TYPES.route);
			route.addSeries(series);

			var routePoints = routeNodes[i].getElementsByTagName(Garmin.GpxActivityFactory.SCHEMA_TAGS.routePoint);					
			if (routePoints.length > 0) {					
				for( var j=0; j < routePoints.length; j++ ) {
					var routePoint = new Garmin.Sample();
					routePoint.setLazyLoading(true, Garmin.GpxActivityFactory, routePoints[j]);
					series.addSample(routePoint);
				}
			}
			
			if (series.getSamplesLength() > 0) {
				routes.push(route);
			}
		}
		
    	return routes;			
	},
	
	_parseGpxRoutePoint: function(routePointNode, routePointSample) {
		if (routePointSample == null) {
			routePointSample = new Garmin.Sample();
		}

		routePointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.latitude, routePointNode.getAttribute(Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointLatitude));
		routePointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.longitude, routePointNode.getAttribute(Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointLongitude));
		
		var elevation =  Garmin.GpxActivityFactory._tagValue(routePointNode,Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointElevation);
		if (elevation != null) {
			routePointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.elevation, elevation);
		}		
		
		return routePointSample;		
	},
	
	_parseGpxTracks: function(gpxDocument) {
		var tracks = new Array();
		
    	var trackNodes = gpxDocument.getElementsByTagName(Garmin.GpxActivityFactory.SCHEMA_TAGS.track);
		for( var i=0; i < trackNodes.length; i++ ) {
			var track = new Garmin.Activity();
			
			var trackName = Garmin.GpxActivityFactory._tagValue(trackNodes[i], Garmin.GpxActivityFactory.SCHEMA_TAGS.trackName);
			if (trackName == null) {
				trackName = "";
			}
			
			track.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.dom, trackNodes[i]);
			track.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.activityName, trackName);

			var series = new Garmin.Series(Garmin.Series.TYPES.history);
			track.addSeries(series);

			var trackSegments = trackNodes[i].getElementsByTagName(Garmin.GpxActivityFactory.SCHEMA_TAGS.trackSegment);	
			for( var j=0; j < trackSegments.length; j++ ) {
				
				// grab all the trackpoints
				var trackPoints = trackSegments[j].getElementsByTagName(Garmin.GpxActivityFactory.SCHEMA_TAGS.trackPoint);											
				if (trackPoints.length > 0) {
					
					// set the start and end time summary values		
					var startTime = Garmin.GpxActivityFactory._tagValue(trackPoints[0], Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointTime);
					var endTime = Garmin.GpxActivityFactory._tagValue(trackPoints[trackPoints.length - 1], Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointTime);					
					if (startTime != null && endTime != null) {
						track.setSummaryValue(Garmin.Activity.SUMMARY_KEYS.startTime, (new Garmin.DateTimeFormat()).parseXsdDateTime(startTime));
						track.setSummaryValue(Garmin.Activity.SUMMARY_KEYS.endTime, (new Garmin.DateTimeFormat()).parseXsdDateTime(endTime));
					} else {
						// can't find timestamps, must be a route reported as a track (GPSMap does this)
						series.setSeriesType(Garmin.Series.TYPES.route);
					}
				
					// loop through all the trackpoints in this segment				
					for( var k=0; k < trackPoints.length; k++ ) {
						var trackPoint = new Garmin.Sample();
						trackPoint.setLazyLoading(true, Garmin.GpxActivityFactory, trackPoints[k]);
						series.addSample(trackPoint);						
					}
					
					// add the track to the list of tracks
					tracks.push(track);
				}
			}
		}

    	return tracks;	
	},
	
	_parseGpxTrackPoint: function(trackPointNode, trackPointSample) {
		if (trackPointSample == null) {
			trackPointSample = new Garmin.Sample();	
		}
		
		trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.latitude, trackPointNode.getAttribute(Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointLatitude));
		trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.longitude, trackPointNode.getAttribute(Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointLongitude));
		
		var elevation =  Garmin.GpxActivityFactory._tagValue(trackPointNode,Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointElevation);
		if (elevation != null) {
			trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.elevation, elevation);
		}

		var time = Garmin.GpxActivityFactory._tagValue(trackPointNode, Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointTime);
		if (time != null) {
			trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.time, (new Garmin.DateTimeFormat()).parseXsdDateTime(time));
		}			
		
		return trackPointSample;
	},
	
	_parseGpxWaypoints: function(gpxDocument) {
		var waypoints = new Array();
    	var waypointNodes = gpxDocument.getElementsByTagName(Garmin.GpxActivityFactory.SCHEMA_TAGS.waypoint);
    	
		for( var i=0; i < waypointNodes.length; i++ ) {
			waypoints.push(Garmin.GpxActivityFactory._parseGpxWaypoint(waypointNodes[i]));
		}
    	
    	return waypoints;
	},
	
	_parseGpxWaypoint: function(waypointNode) {
		var waypoint = new Garmin.Activity();
		var waypointSeries = new Garmin.Series(Garmin.Series.TYPES.waypoint);
		var waypointSample = new Garmin.Sample();
		
		waypoint.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.dom, waypointNode);			

		waypointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.latitude, waypointNode.getAttribute(Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointLatitude)); 
		waypointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.longitude, waypointNode.getAttribute(Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointLongitude));
				
		var elevation =  Garmin.GpxActivityFactory._tagValue(waypointNode,Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointElevation);
		if (elevation != null) {
			waypointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.elevation, elevation);
		}
		
		var wptName =  Garmin.GpxActivityFactory._tagValue(waypointNode,Garmin.GpxActivityFactory.SCHEMA_TAGS.waypointName);
		if (wptName != null) {
			waypoint.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.activityName, wptName);
		}
		
		waypointSeries.addSample(waypointSample);
		waypoint.addSeries(waypointSeries);		
   		return waypoint;
	},
	
	_tagValue: function(parentNode, tagName) {
		var subNode = parentNode.getElementsByTagName(tagName);
		return subNode.length > 0 ? subNode[0].childNodes[0].nodeValue : null;
	},	
	
    toString: function() {
        return "[GpxActivityFactory]";
    }	
};

/** Constants defining GPX type
 */
Garmin.GpxActivityFactory.GPX_TYPE = {
	routes:    "routes",
	waypoints: "waypoints",
	tracks:    "tracks",
	all:       "all"
}

/** Constants defining details about the factory
 */
Garmin.GpxActivityFactory.DETAIL = {
	creator:			"Garmin Communicator Plug-In API"
};

/** Constants defining tags used by the gpx schema. This is used
 *  by the factory when converting between the xml and datastructure.
 */
Garmin.GpxActivityFactory.SCHEMA_TAGS = {
	creator:					"creator",
	gpx:						"gpx",
	metadata:					"metadata",
	route:						"rte",
	routeName:					"name",
	routePoint:					"rtept",
	track:						"trk",
	trackName:					"name",
	trackPoint:					"trkpt",
	trackSegment:				"trkseg",
	waypoint:					"wpt",
	waypointComment:			"cmt",
	waypointDGPSAge:			"ageofdgpsdata",
	waypointDGPSID:				"dgpsid",
	waypointDescription:		"desc",
	waypointGeoIdHeight:		"geoidheight",
	waypointHDOP:				"hdop",
	waypointMagVar:				"magvar",
	waypointName:				"name",
	waypointLatitude:			"lat",
	waypointLink:				"link",
	waypointLongitude:			"lon",
	waypointElevation:			"ele",
	waypointPDOP:				"pdop",
	waypointSatellites:			"sat",
	waypointSource:				"src",
	waypointSymbol:				"sym",
	waypointTime:				"time",
	waypointType:				"type",
	waypointVDOP:				"vdop"
};
/*
// Dynamic include of required libraries and check for Prototype
// Code taken from scriptaculous
// TODO: put this code in a library and reuse is instead of copying it to new files
var GpxActivityFactory = {
	require: function(libraryName) {
		// inserting via DOM fails in Safari 2.0, so brute force approach
		document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
	},

	load: function() {
		if((typeof Prototype=='undefined') || 
			(typeof Element == 'undefined') || 
			(typeof Element.Methods=='undefined') ||
			parseFloat(Prototype.Version.split(".")[0] + "." +
			Prototype.Version.split(".")[1]) < 1.5) {
			throw("GpxActivityFactory requires the Prototype JavaScript framework >= 1.5.0");
		}

		$A(document.getElementsByTagName("script"))
		.findAll(
			function(s) {
				return (s.src && s.src.match(/GpxActivityFactory\.js(\?.*)?$/))
			}
		)
		.each(
			function(s) {
				var path = s.src.replace(/GpxActivityFactory\.js(\?.*)?$/,'../../');
				var includes = s.src.match(/\?.*load=([a-z,]*)/);
				var dependencies = 'garmin/util/Util-XmlConverter' +
									',garmin/util/Util-DateTimeFormat' +
									',garmin/activity/GarminMeasurement' +
									',garmin/activity/GarminSample' +
									',garmin/activity/GarminSeries' +
									',garmin/activity/GarminActivity';
			    (includes ? includes[1] : dependencies).split(',').each(
					function(include) {
						GpxActivityFactory.require(path+include+'.js') 
					}
				);
			}
		);
	}	
}

GpxActivityFactory.load();*/

if (Garmin == undefined) var Garmin = {};
/**
 * Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.TcxActivityFactory - A factory for producing tcx activity and data.
 * 
 * @author Bobby Yang bobby.yang.at.garmin.com
 * @version 1.0
 */
/**A factory that can produce an array activity given tcx xml and produce tcx xml given an
 * array of activity.
 * many other types of data.
 * @class Garmin.TcxActivityFactory
 * @constructor 
 */
Garmin.TcxActivityFactory = function(){};
Garmin.TcxActivityFactory = {
	
	parseString: function(tcxString) {
		var tcxDocument = Garmin.XmlConverter.toDocument(tcxString);		
		return Garmin.TcxActivityFactory.parseDocument(tcxDocument);		
	},
	
	/* Creates and returns an array of activities from the document. */
	parseDocument: function(tcxDocument) {
		
		// Not TCX parseable
		if( tcxDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.activities).length == 0
			&& tcxDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.courses).length == 0) {
			throw new Error("ERROR: Unable to parse TCX document.");
		}
		
		var parsedDocument;
		
		// Activities		
		if( tcxDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.activity).length >= 0) {
			
			if( tcxDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.track).length >= 0) { 
				// Complete activity
				parsedDocument = Garmin.TcxActivityFactory._parseTcxActivities(tcxDocument);
			}
			else {
				// Directory listing
				parsedDocument = Garmin.TcxActivityFactory._parseTcxHistoryDirectory(tcxDocument);
			}
		} 
		// Courses
		else if(tcxDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.course).length >= 0) {
		
			if( tcxDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.lap).length >= 0) {
				// Complete course
				parsedDocument = Garmin.TcxActivityFactory._parseTcxCourses(tcxDocument);
			}
			else {
				// Directory listing
				parsedDocument = Garmin.TcxActivityFactory._parseTcxCourseDirectory(tcxDocument);
			}
		}
		
		return parsedDocument;
	},
	
	produceString: function(activities) {
		var tcxString = "";
		
		// header tags
		tcxString += '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>';
		tcxString += '\n<TrainingCenterDatabase xmlns="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd http://www.garmin.com/xmlschemas/FatCalories/v1 http://www.garmin.com/xmlschemas/fatcalorieextensionv1.xsd">';
		tcxString += '\n  <Activities>';		
		
		if (activities != null && activities.length > 0) {			
			// activity tags
			for (var i = 0; i < activities.length; i++) {
				tcxString += "\n    " + Garmin.TcxActivityFactory._produceActivityString(activities[i]);
			}
			tcxString += '\n  </Activities>';
			
			// author tag
			var activityDom = activities[0].getAttribute(Garmin.Activity.ATTRIBUTE_KEYS.dom);
			if (activityDom != null) {
				var authorDom = activityDom.ownerDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.author);
				if (authorDom.length > 0) {
					tcxString += "\n  " + Garmin.XmlConverter.toString(authorDom[0]);
				}
			}
		}

		// footer tags
		tcxString += '\n</TrainingCenterDatabase>';
				
		return tcxString;
	},
	
	/** Fully load the sample, assume sample was previously lazy-loaded
	 */	
	finishLoadingSample: function(domNode, sample) {
		Garmin.TcxActivityFactory._parseTcxTrackPoint(domNode, sample);
		sample.isLazyLoaded = false;
	},	
	
	_produceActivityString: function(activity) {
		var activityString = "";
		
		if (activity != null) {
			// converting the dom back into string
			// this is the lazy way, this will not work if 
			// converting between file types or activity data
			// has been modified.
			var activityDom = activity.getAttribute(Garmin.Activity.ATTRIBUTE_KEYS.dom);			
			if (activityDom != null) {
				activityString = Garmin.XmlConverter.toString(activityDom);
			}
		}
		
		return activityString;
	},
	
	_parseTcxHistoryDirectory: function(tcxDocument) {
		var activities = new Array();
		var activityNodes;

		// Grab the activity/course nodes, depending on document		
		activityNodes = tcxDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.activity);
		
		// loop through all activities in the document
		for (var i = 0; i < activityNodes.length; i++) {
			
			if( activityNodes[i].parentNode.tagName != Garmin.TcxActivityFactory.SCHEMA_TAGS.nextSport ){
				// create new activity object
				var activity = Garmin.TcxActivityFactory._parseTcxActivity(activityNodes[i], Garmin.TcxActivityFactory.SCHEMA_TAGS.activity);
				
				// grab all the lap nodes in the dom			
				var lapNodes = activityNodes[i].getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.lap);
				
				// grab start time from the first lap and set duration to 0
				if (lapNodes.length > 0) {
					var activityStartTimeMS = lapNodes[0].getAttribute(Garmin.TcxActivityFactory.SCHEMA_TAGS.lapStartTime);
					var activityDurationMS = 0;	// in ms
				}			
				
				// loop through all laps in this activity
				for (var j = 0; j < lapNodes.length; j++) {
					
					// update the duration of this activity
					var lapTotalTime = Garmin.TcxActivityFactory._tagValue(lapNodes[j], Garmin.TcxActivityFactory.SCHEMA_TAGS.lapTotalTime);
					activityDurationMS += parseFloat(lapTotalTime + "e+3");
				}
				
				if ( lapNodes.length > 0) {
					// set the start and end time summary data for the activity if possible
					activityStartTimeObj = (new Garmin.DateTimeFormat()).parseXsdDateTime(activityStartTimeMS);
					activityEndTimeObj	=  new Garmin.DateTimeFormat();
					// NOTE: switch to using setDate() once it is implemented in Garmin.DateTimeFormat
					activityEndTimeObj.date = new Date(activityStartTimeObj.getDate().getTime() + activityDurationMS);
					activity.setSummaryValue(Garmin.Activity.SUMMARY_KEYS.startTime, activityStartTimeObj);
					activity.setSummaryValue(Garmin.Activity.SUMMARY_KEYS.endTime, activityEndTimeObj);
				}
				
				// Add the populated activity to the list of activities.  This activity may not have laps (if it's a directory listing entry).
				activities.push(activity);
			}
		}
		
		return activities;
	},
	
	_parseTcxCourseDirectory: function(tcxDocument) {
		var activities = new Array();
		var activityNodes;

		// Grab the activity/course nodes, depending on document		
		activityNodes = tcxDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.course);
		
		// loop through all activities in the document
		for (var i = 0; i < activityNodes.length; i++) {
			
			// create new activity object
			var activity = Garmin.TcxActivityFactory._parseTcxActivity(activityNodes[i], Garmin.TcxActivityFactory.SCHEMA_TAGS.course);
			
			// Add the populated activity to the list of activities.  This activity will not have laps.
			activities.push(activity);
		}
		
		return activities;
	},
	
	_parseTcxActivities: function(tcxDocument) {
		var activities = new Array();
		var activityNodes;

		// Grab the activity/course nodes, depending on document		
		activityNodes = tcxDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.activity);
		
		// loop through all activities in the document
		for (var i = 0; i < activityNodes.length; i++) {
			
			if( activityNodes[i].parentNode.tagName == Garmin.TcxActivityFactory.SCHEMA_TAGS.nextSport ){
				continue;
				}
				
			// create new activity object
			var activity = Garmin.TcxActivityFactory._parseTcxActivity(activityNodes[i], Garmin.TcxActivityFactory.SCHEMA_TAGS.activity);
			
			// create a history series for all the trackpoints in this activity
			var historySeries = new Garmin.Series(Garmin.Series.TYPES.history);
			
			// grab all the lap nodes in the dom			
			var lapNodes = activityNodes[i].getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.lap);
			
			// grab start time from the first lap and set duration to 0
			if (lapNodes.length > 0) {
				var activityStartTimeMS = lapNodes[0].getAttribute(Garmin.TcxActivityFactory.SCHEMA_TAGS.lapStartTime);
				var activityDurationMS = 0;	// in ms
			}			
			
			// loop through all laps in this activity
			for (var j = 0; j < lapNodes.length; j++) {
				
				// update the duration of this activity
				var lapTotalTime = Garmin.TcxActivityFactory._tagValue(lapNodes[j], Garmin.TcxActivityFactory.SCHEMA_TAGS.lapTotalTime);
				activityDurationMS += parseFloat(lapTotalTime + "e+3");
				
				/* not implemented until sections are in place
				// create lap section
				// set start time				
				// set total time				
				// set distance				
				// set max speed				
				// set calories				
				// set intensity				
				// set trigger method
				*/
				
				// loop through all the tracks in this lap
				var trackNodes = lapNodes[j].getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.track);			
				for (var k = 0; k < trackNodes.length; k++) {
					
					/* not implemented until sections are in place
					// create track section
					*/					
					
					// loop through all the trackpoints in this track
					var trackPointNodes = trackNodes[k].getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPoint);
					for (var l = 0; l < trackPointNodes.length; l++) {
						//historySeries.addSample(Garmin.TcxActivityFactory._parseTcxTrackPoint(trackPointNodes[l]));
						var trackPoint = new Garmin.Sample();
						trackPoint.setLazyLoading(true, Garmin.TcxActivityFactory, trackPointNodes[l]);
						historySeries.addSample(trackPoint);
						//historySeries.addSample(new Garmin.Sample());
					}					
				}
			}
			
			if ( lapNodes.length > 0) {
				// set the start and end time summary data for the activity if possible
				activityStartTimeObj = (new Garmin.DateTimeFormat()).parseXsdDateTime(activityStartTimeMS);
				activityEndTimeObj	=  new Garmin.DateTimeFormat();
				// NOTE: switch to using setDate() once it is implemented in Garmin.DateTimeFormat
				activityEndTimeObj.date = new Date(activityStartTimeObj.getDate().getTime() + activityDurationMS);
				activity.setSummaryValue(Garmin.Activity.SUMMARY_KEYS.startTime, activityStartTimeObj);
				activity.setSummaryValue(Garmin.Activity.SUMMARY_KEYS.endTime, activityEndTimeObj);
			}
			
			if (historySeries.getSamplesLength() > 0) {				
				// add the populated series to the activity
				activity.addSeries(historySeries);
			}
			
			// Add the populated activity to the list of activities.  This activity may not have laps (if it's a directory listing entry).
			activities.push(activity);
		}
		
		return activities;
	},
	
	_parseTcxCourses: function(tcxDocument) {
		var activities = new Array();
		var activityNodes;

		// Grab the course nodes, depending on document		
		activityNodes = tcxDocument.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.course);
		
		// loop through all activities in the document
		for (var i = 0; i < activityNodes.length; i++) {
			
			// create new activity object
			var activity = Garmin.TcxActivityFactory._parseTcxActivity(activityNodes[i], Garmin.TcxActivityFactory.SCHEMA_TAGS.course);
			
			// create a history series for all the trackpoints in this activity
			var historySeries = new Garmin.Series(Garmin.Series.TYPES.course);
			
			// grab all the lap nodes in the dom			
			var lapNodes = activityNodes[i].getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.lap);
			
			// grab start time from the first lap and set duration to 0
			if (lapNodes.length > 0) {
				var activityDurationMS = 0;	// in ms
			}		
			
			// loop through all laps in this activity
			for (var j = 0; j < lapNodes.length; j++) {
				
				// update the duration of this activity
				var lapTotalTime = Garmin.TcxActivityFactory._tagValue(lapNodes[j], Garmin.TcxActivityFactory.SCHEMA_TAGS.lapTotalTime);
				activityDurationMS += parseFloat(lapTotalTime + "e+3");
				
				/* not implemented until sections are in place
				// create lap section
				// set start time				
				// set total time				
				// set distance				
				// set max speed				
				// set calories				
				// set intensity				
				// set trigger method
				*/
			}
			
			// loop through all the tracks in this lap
			var trackNodes = activityNodes[i].getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.track);			
			for (var k = 0; k < trackNodes.length; k++) {
				
				/* not implemented until sections are in place
				// create track section
				*/					
				
				// loop through all the trackpoints in this track
				var trackPointNodes = trackNodes[k].getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPoint);
				for (var l = 0; l < trackPointNodes.length; l++) {
					//historySeries.addSample(Garmin.TcxActivityFactory._parseTcxTrackPoint(trackPointNodes[l]));
					var trackPoint = new Garmin.Sample();
					trackPoint.setLazyLoading(true, Garmin.TcxActivityFactory, trackPointNodes[l]);
					historySeries.addSample(trackPoint);
					//historySeries.addSample(new Garmin.Sample());
				}					
			}
			
			if (historySeries.getSamplesLength() > 0) {				
				// add the populated series to the activity
				activity.addSeries(historySeries);
			}
			
			// Add the populated activity to the list of activities.  This activity may not have laps (if it's a directory listing entry).
			activities.push(activity);
		}
		
		return activities;
	},
	
	_parseTcxActivity: function(activityNode, activityType) {
		// create new activity object
		var activity = new Garmin.Activity();
		
		// set lazy loaded
		activity.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.isLazyLoaded, true);
		
		// set factory
		activity.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.factory, Garmin.TcxActivityFactory);
		
		// set dom
		activity.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.dom, activityNode);
		
		// set id
		var id;
		if(activityType == Garmin.TcxActivityFactory.SCHEMA_TAGS.activity) {
			id = Garmin.TcxActivityFactory._tagValue(activityNode, Garmin.TcxActivityFactory.SCHEMA_TAGS.activityId);
		} else {
			id = Garmin.TcxActivityFactory._tagValue(activityNode, Garmin.TcxActivityFactory.SCHEMA_TAGS.courseName);
		}
		activity.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.activityName, id)		
		
		// set sport
		var sport = activityNode.getAttribute(Garmin.TcxActivityFactory.SCHEMA_TAGS.activitySport);
		activity.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.activitySport, sport);	
		
		// set creator information, optional in schema
		var creator = activityNode.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.creator);
		if (creator != null && creator.length > 0) {
			// set creator name
			var creatorName = Garmin.TcxActivityFactory._tagValue(creator[0], Garmin.TcxActivityFactory.SCHEMA_TAGS.creatorName);
			activity.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.creatorName, creatorName);
			
			// set creator unit id
			var unitId = Garmin.TcxActivityFactory._tagValue(creator[0], Garmin.TcxActivityFactory.SCHEMA_TAGS.creatorUnitID);
			activity.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.creatorUnitId, unitId);
							
			// set creator product id
			var prodId = Garmin.TcxActivityFactory._tagValue(creator[0], Garmin.TcxActivityFactory.SCHEMA_TAGS.creatorProductID);
			activity.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.creatorProdId, prodId);
							
			// set creator version
			var version = Garmin.TcxActivityFactory._parseTcxVersion(creator[0]);
			if (version != null) {
				activity.setAttribute(Garmin.Activity.ATTRIBUTE_KEYS.creatorVersion, version);
			}
		}
		
		return activity;
	},
	
	
	
	_parseTcxTrackPoint: function(trackPointNode, trackPointSample) {
		// create a sample for this trackpoint if needed
		if (trackPointSample == null) {
			trackPointSample = new Garmin.Sample();
		}
		/*
		var trackPointValueNodes = trackPointNode.childNodes;
		for (var i = 1; i < trackPointValueNodes.length; i += 2) {
			if (trackPointValueNodes[i].nodeType == 1 && trackPointValueNodes[i].hasChildNodes()) {
				var nodeValue = trackPointValueNodes[i].childNodes[0].nodeValue;
				if (nodeValue != null) {
					switch(trackPointValueNodes[i].nodeName) {
						case Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointTime:
							trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.time, (new Garmin.DateTimeFormat()).parseXsdDateTime(nodeValue));						
							break;						
						case Garmin.TcxActivityFactory.SCHEMA_TAGS.position:
							//var latitude = Garmin.TcxActivityFactory._tagValue(trackPointValueNodes[i], Garmin.TcxActivityFactory.SCHEMA_TAGS.positionLatitude);		
							//var longitude = Garmin.TcxActivityFactory._tagValue(trackPointValueNodes[i], Garmin.TcxActivityFactory.SCHEMA_TAGS.positionLongitude);
							var latitude = trackPointValueNodes[i].childNodes[1].childNodes[0].nodeValue;
							var longitude = trackPointValueNodes[i].childNodes[3].childNodes[0].nodeValue;
							trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.latitude, latitude);
							trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.longitude, longitude);						
							break;						
						case Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointElevation:
							trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.elevation, nodeValue);
							break;
						case Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointDistance:
							trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.distance, nodeValue);
							break;
						case Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointHeartRate:
							//var heartRate = Garmin.TcxActivityFactory._tagValue(trackPointValueNodes[i], Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointHeartRateValue);
							var heartRate = trackPointValueNodes[i].childNodes[1].childNodes[0].nodeValue;
							trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.heartRate, heartRate);
							break;
						case Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointCadence:
							trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.cadence, nodeValue);
							break;
						case Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointSensorState:
							trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.sensorState, nodeValue);
							break;																																				
						default:
					}
				}
			}
		}
		*/
		
		// set time
		var time = Garmin.TcxActivityFactory._tagValue(trackPointNode, Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointTime);
		trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.time, (new Garmin.DateTimeFormat()).parseXsdDateTime(time));	

		// set latitude and longitude, optional in schema (signal loss, create signal section);					
		var position = trackPointNode.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.position);
		if (position.length > 0) {
			var latitude = Garmin.TcxActivityFactory._tagValue(position[0], Garmin.TcxActivityFactory.SCHEMA_TAGS.positionLatitude);		
			var longitude = Garmin.TcxActivityFactory._tagValue(position[0], Garmin.TcxActivityFactory.SCHEMA_TAGS.positionLongitude);
			trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.latitude, latitude);
			trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.longitude, longitude);						
		}
					
		// set elevation, optional in schema
		var elevation = Garmin.TcxActivityFactory._tagValue(trackPointNode, Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointElevation);
		if (elevation != null) {
			trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.elevation, elevation);
		}
		
		// set distance, optional in schema
		var distance = Garmin.TcxActivityFactory._tagValue(trackPointNode, Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointDistance);
		if (distance != null) {
			trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.distance, distance);
		}

		// set heart rate, optional in schema
		var heartRateNode = trackPointNode.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointHeartRate);
		if (heartRateNode.length > 0) {
			var heartRate = Garmin.TcxActivityFactory._tagValue(heartRateNode[0], Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointHeartRateValue);
			trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.heartRate, heartRate);
		}

		// set cadence, optional in schema
		var cadence = Garmin.TcxActivityFactory._tagValue(trackPointNode, Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointCadence);
		if (cadence != null) {
			trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.cadence, cadence);
		}

		// set sensor state, optional in schema
		var sensorState = Garmin.TcxActivityFactory._tagValue(trackPointNode, Garmin.TcxActivityFactory.SCHEMA_TAGS.trackPointSensorState);
		if (sensorState != null) {
			trackPointSample.setMeasurement(Garmin.Sample.MEASUREMENT_KEYS.sensorState, sensorState);
		}
		
		return trackPointSample;
	},
	
	_parseTcxVersion: function(parentNode) {
		// find the version node
		var versionNodes = parentNode.getElementsByTagName(Garmin.TcxActivityFactory.SCHEMA_TAGS.version);
		
		// if there is a version node
		if (versionNodes.length > 0) {					
			// get version major and minor
			var vMajor = Garmin.TcxActivityFactory._tagValue(versionNodes[0], Garmin.TcxActivityFactory.SCHEMA_TAGS.versionMajor);
			var vMinor = Garmin.TcxActivityFactory._tagValue(versionNodes[0], Garmin.TcxActivityFactory.SCHEMA_TAGS.versionMinor);
			
			// get buid major and minor, optional in schema
			var bMajor = Garmin.TcxActivityFactory._tagValue(versionNodes[0], Garmin.TcxActivityFactory.SCHEMA_TAGS.versionBuildMajor);
			var bMinor = Garmin.TcxActivityFactory._tagValue(versionNodes[0], Garmin.TcxActivityFactory.SCHEMA_TAGS.versionBuildMinor);
			
			// return version
			if ((bMajor != null) && (bMinor != null)) {
				return { versionMajor: vMajor, versionMinor: vMinor, buildMajor: bMajor, buildMinor: bMinor };
			} else {
				return { versionMajor: vMajor, versionMinor: vMinor };
			}
		} else {
			return null;
		}
	},	
	
	_tagValue: function(parentNode, tagName) {
		var subNode = parentNode.getElementsByTagName(tagName);
		return subNode.length > 0 ? subNode[0].childNodes[0].nodeValue : null;
	},	
	
    toString: function() {
        return "[TcxActivityFactory]";
    }	
};

Garmin.TcxActivityFactory.DETAIL = {
	creator:			"Garmin Communicator Plugin API - http://www.garmin.com/"
};

Garmin.TcxActivityFactory.SCHEMA_TAGS = {
	activities:					"Activities",
	activity:					"Activity",
	activityId:					"Id",
	activitySport:				"Sport",
	author:						"Author",
	course:						"Course",
	courses:					"Courses",
	courseName:					"Name",
	creator:					"Creator",
	creatorName:				"Name",
	creatorUnitID:				"UnitId",
	creatorProductID:			"ProductID",
	lap:						"Lap",
	lapAverageHeartRate:		"AverageHeartRateBpm",
	lapCadence:					"Cadence",
	lapCalories:				"Calories",
	lapDistance:				"DistanceMeters",
	lapIntensity:				"Intensity",
	lapMaxHeartRate:			"MaximumHeartRateBpm",
	lapMaxSpeed:				"MaximumSpeed",
	lapNotes:					"Notes",
	lapStartTime:				"StartTime",
	lapTotalTime:				"TotalTimeSeconds",
	lapTriggerMethod:			"TriggerMethod",
	multiSportSession:			"MultiSportSession",
	nextSport:					"NextSport",
	position:					"Position",
	positionLatitude:			"LatitudeDegrees",
	positionLongitude:			"LongitudeDegrees",
	track:						"Track",
	trackPoint:					"Trackpoint",
	trackPointCadence:			"Cadence",
	trackPointDistance:			"DistanceMeters",
	trackPointElevation:		"AltitudeMeters",	
	trackPointHeartRate:		"HeartRateBpm",
	trackPointHeartRateValue:	"Value",
	trackPointSensorState:		"SensorState",
	trackPointTime:				"Time",
	version:					"Version",
	versionBuildMajor:			"BuildMajor",
	versionBuildMinor:			"BuildMinor",	
	versionMajor:				"VersionMajor",
	versionMinor:				"VersionMinor"
};
/*
// Dynamic include of required libraries and check for Prototype
// Code taken from scriptaculous
// TODO: put this code in a library and reuse is instead of copying it to new files
var TcxActivityFactory = {
	require: function(libraryName) {
		// inserting via DOM fails in Safari 2.0, so brute force approach
		document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
	},

	load: function() {
		if((typeof Prototype=='undefined') || 
			(typeof Element == 'undefined') || 
			(typeof Element.Methods=='undefined') ||
			parseFloat(Prototype.Version.split(".")[0] + "." +
			Prototype.Version.split(".")[1]) < 1.5) {
			throw("TcxActivityFactory requires the Prototype JavaScript framework >= 1.5.0");
		}

		$A(document.getElementsByTagName("script"))
		.findAll(
			function(s) {
				return (s.src && s.src.match(/TcxActivityFactory\.js(\?.*)?$/))
			}
		)
		.each(
			function(s) {
				var path = s.src.replace(/TcxActivityFactory\.js(\?.*)?$/,'../../');
				var includes = s.src.match(/\?.*load=([a-z,]*)/);
				var dependencies = 'garmin/util/Util-XmlConverter' +
									',garmin/util/Util-DateTimeFormat' +
									',garmin/activity/GarminMeasurement' +
									',garmin/activity/GarminSample' +
									',garmin/activity/GarminSerie' +
									',garmin/activity/GarminActivity';
			    (includes ? includes[1] : dependencies).split(',').each(
					function(include) {
						TcxActivityFactory.require(path+include+'.js') 
					}
				);
			}
		);
	}	
}

TcxActivityFactory.load();*/
/**
 * This snippet of code is required to generate the object tag
 * only when the browser reports that the plugin is installed.
 * 
 * @requires PluginDetect, BrowserDetect
 */
if (PluginDetect.detectGarminCommunicatorPlugin()) {
	
	// Insert object tag based on browser
	switch(BrowserDetect.browser) {
	    // TODO pull these out into constants later, in BrowserDetect
	    case "Explorer":
            document.write('<object id="GarminActiveXControl" style="WIDTH: 0px; HEIGHT: 0px; visible: hidden" height="0" width="0" classid="CLSID:099B5A62-DE20-48C6-BF9E-290A9D1D8CB5">&#160;</object>');
            break;
	    case "Firefox":
	    case "Mozilla":
	    case "Safari":
        	// Outer div necessary for Safari and Chrome
        	// TODO try removing the divs to see if Safari 3+ has fixed their bug  
        	document.write('<div style="height:0px; width:0px;">');
        	document.write('<object id="GarminNetscapePlugin" type="application/vnd-garmin.mygarmin" width="0" height="0">&#160;</object>');
        	document.write('</div>');
            break;
	    default:
        	document.write('<div style="height:0px; width:0px;"><object id="GarminActiveXControl" style="WIDTH: 0px; HEIGHT: 0px; visible: hidden" height="0" width="0" classid="CLSID:099B5A62-DE20-48C6-BF9E-290A9D1D8CB5">');
        	document.write('	<object id="GarminNetscapePlugin" type="application/vnd-garmin.mygarmin" width="0" height="0">&#160;</object>');
        	document.write('</object></div>');
        	break;
	}
	
}

if (Garmin == undefined) var Garmin = {};
/** Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.DeviceControl A mostly deprecated library of GPS track and waypoint data structures along with parsing tools.
 * @deprecated use Garmin.GpxActivityFactory instead
 * 
 * @author Developer developer.connect.at.garmin.com
 * @version 1.0
 */

/** A waypoint represents a stored location.
 * Equivalent to a <wpt> in GPX format
 * Note: this class is only used by Garmin.Geocode but otherwise has been replaced by Garmin.Activity.
 * 
 * @class Garmin.WayPoint 
 * @constructor 
 * @param {Number} lat
 * @param {Number} lng
 * @param {Number} elev
 * @param {String} name
 */
Garmin.WayPoint = function(lat, lng, elev, name, addrdetails, desc, sym, type, cmt){};
Garmin.WayPoint = Class.create();
Garmin.WayPoint.prototype = {

	/** Prototype constructor
	 */
	initialize: function(lat, lng, elev, name, addrdetails, desc, sym, type, cmt) {
		this.lat = lat;
		this.lng = lng;
		this.name = name;
		this.addrdetails = addrdetails;
		
		// Get city, streetaddr, and zip data.
		if( this.addrdetails ) {
			this._initSubArea();
		}
		this.elev = elev;		
		this.desc = desc;
		this.sym = sym;
		this.type = type;
		this.cmt = cmt;
		this.date = null;
	},
	
	/** Initializes the subadministrative area, as designated by Google Maps API (see http://code.google.com/apis/maps/documentation/services.html#Geocoding_Structured).
	 *  Apparently some locations have Locality while others don't, so this function takes care of both.
	 */
	_initSubArea: function() {
		
		if( this.addrdetails.Country ) {
            this.country = this.addrdetails.Country.CountryNameCode;
            if (this.addrdetails.Country.AdministrativeArea) {
                this.state = this.addrdetails.Country.AdministrativeArea.AdministrativeAreaName;
				
				if( this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea ) {
	                if( this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.Locality) {
	                    this.city = this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.LocalityName;
	                    if( this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.Thoroughfare ) {
	                        this.streetaddr = this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.Thoroughfare.ThoroughfareName;
	                    }
	                    if( this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.PostalCode ) {
	                        this.zip = this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.PostalCode.PostalCodeNumber;
	                    }
	                } else {
	                   this.city = this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.SubAdministrativeAreaName; 
	
	                   if( this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.Thoroughfare ) {
	                       this.streetaddr = this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.Thoroughfare.ThoroughfareName;
	                   } 
	
	                   if( this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.PostalCode ) {
	                       this.zip = this.addrdetails.Country.AdministrativeArea.SubAdministrativeArea.PostalCode.PostalCodeNumber;
	                   } 
	                }
				} else {
					if( this.addrdetails.Country.AdministrativeArea.Locality) {
	                    this.city = this.addrdetails.Country.AdministrativeArea.Locality.LocalityName;
	                    if( this.addrdetails.Country.AdministrativeArea.Locality.Thoroughfare ) {
	                        this.streetaddr = this.addrdetails.Country.AdministrativeArea.Locality.Thoroughfare.ThoroughfareName;
	                    }
	                    if( this.addrdetails.Country.AdministrativeArea.Locality.PostalCode ) {
	                        this.zip = this.addrdetails.Country.AdministrativeArea.Locality.PostalCode.PostalCodeNumber;
	                    }
	                } else {
	                   this.city = this.addrdetails.Country.AdministrativeArea.SubAdministrativeAreaName; 
	
	                   if( this.addrdetails.Country.AdministrativeArea.Thoroughfare ) {
	                       this.streetaddr = this.addrdetails.Country.AdministrativeArea.Thoroughfare.ThoroughfareName;
	                   } 
	
	                   if( this.addrdetails.Country.AdministrativeArea.PostalCode ) {
	                       this.zip = this.addrdetails.Country.AdministrativeArea.PostalCode.PostalCodeNumber;
	                   } 
	                }
				}
            }
        }
	},
	
	/** Get waypoint symbol usually associated with a display icon.
	 * @type String 
	 * @return The symbol of this waypoint
	 */
	getSymbol: function() {
		return this.sym;
	},

	/** Get waypoint type.
	 * @type String 
	 * @return The type of this waypoint
	 */
	getType: function() {
		return this.type;
	},

	/** Get waypoint name.
	 * @type String 
	 * @return The name of this waypoint
	 */
	getName: function() {
		return this.name;
	},

	/** Get waypoint address XML string.  Uses the extension element to generate XML address format listed in the
	 * GPX Extensions v3 schema: http://www8.garmin.com/xmlschemas/GpxExtensions/v3/GpxExtensionsv3.xsd
	 *
	 * @type String 
	 * @return The address of this waypoint, as an XML string with the outermost element being gpxx::Address
	 */
	getAddress: function() {
		return (this.addrdetails != null) ? "<gpxx:Address><gpxx:StreetAddress>" 
				+ this.getStreetAddr() + "</gpxx:StreetAddress><gpxx:City>" 
				+ this.getCity() + "</gpxx:City><gpxx:State>" 
				+ this.getState() + "</gpxx:State><gpxx:PostalCode>"
				+ this.getZip() + "</gpxx:PostalCode></gpxx:Address>" : null;
	},
	
	/** Get country that the waypoint is located in, according to Google Maps API.
	 *  See http://www.google.com/apis/maps/ for details.
	 * @type String 
	 * @return The country the waypoint is located in.
	 */
	getCountry: function() {
		return this.country;
	},

	/** Get state that the waypoint is located in, according to Google Maps API.
	 * 	See http://www.google.com/apis/maps/ for details.
	 * @type String 
	 * @return The state the waypoint is located in.
	 */
	getState: function() {
		return this.state;
	},

	/** Get city that the waypoint is located in, according to Google Maps API.
	 * See http://www.google.com/apis/maps/ for details.
	 * @type String 
	 * @return The city the waypoint is located in.
	 */
	getCity: function() {
		return this.city;
	},
	
	/** Get street address that the waypoint is located in, according to Google Maps API.
	 * See http://www.google.com/apis/maps/ for details.
	 * @type String 
	 * @return The street address the waypoint is located in.
	 */
	getStreetAddr: function() {
		return this.streetaddr;
	},
	
	/** Get zip code that the waypoint is located in, according to Google Maps API.
	 * See http://www.google.com/apis/maps/ for details.
	 * @type String 
	 * @return The zip code the waypoint is located in.
	 */
	getZip: function() {
		return this.zip;
	},
	
	/** Get waypoint description.
	 * @type String 
	 * @return A description of this waypoint
	 */
	getDescription: function() {
		return this.desc;
	},

	/** Shortcut for directly getting the lat value
	 * 
	 * @type Number
	 * @return The value of the latitude for this point
	 */
	getLat: function() {
		return this.lat;
	},
	
	/** Shortcut for directly getting the longitude value
	 * 
	 * @type Number
	 * @return The value of the longitude for this point
	 */
	getLng: function() {
		return this.lng;
	},
	
	/** Shortcut for directly getting the elevation value
	 * 
	 * @type Number
	 * @return The value of the elevation for this point
	 */
	getElev: function() {
		return this.elev;
	},
	
	/** Get comment.
	 * 
	 * @type String
	 * @return The value of the comment for this point
	 */
	getComment: function() {
		return this.cmt;
	},
	
	
	/** Shortcut for directly getting the date/time
	 * 
	 * @type Garmin.DateTimeFormat
	 * @return The DateTimeFormat object for this point
	 */
	getDate: function() {
		return this.date;
	},
	
	toString: function() {
		return "Waypoint: (" + this.getLat() + ", " +  this.getLng() + ")";
	}
};


/** TrackPoint class reprsents a point from a track.<br>
 * A TrackPoint contains an associative array of measurements, which can be retrieved with a 
 * #getMeasurement call passing in a string index.<br>
 * Equivalent to a <trkpt> in GPX format  
 * @deprecated use Garmin.Activity instead
 * @class Garmin.TrackPoint
 * @constructor 
 */
Garmin.TrackPoint = function(){};
Garmin.TrackPoint = Class.create();
Garmin.TrackPoint.prototype = {
    /** prototype constructor
     */
	initialize: function() {
		this.measurements = null;
		this.date = null;
	},

	/** Get a Measurement from this TrackPoint
	 * If the measurement does not exist - return null
	 * 
	 * @param {String} context of the measurement we would like to get
	 * @type Object
	 * @return A measurement object (important to remember it's value is in measurementObject.value!)
	 * 	or null if the measurement doesn't exist
	 */
	getMeasurement: function(context) {
		var meas = this.measurements[context];
		if(meas == undefined) {
		  meas = null;
		}
		return meas;
	},

	/** Determines if this TrackPoint point is valid for determing location
	 * @type Boolean
	 * @return True if lat/lon exist, false otherwise
	 */
    isValidLocation: function() {
        return ( (this.getLat() != "null") && (this.getLat() != null) && (this.getLng() != "null") && (this.getLng() != null));
    },

	/** Shortcut for directly getting the lat value
	 * 
	 * @type Number
	 * @return The value of the latitude for this point
	 */
	getLat: function() {
	    var meas = this.getMeasurement( "latitude" );
	    if(meas == null) {
	    	return null;
	    } else {
	    	return meas.value;
	    }
	},
	
	/** Shortcut for directly getting the longitude value
	 * 
	 * @type Number
	 * @return The value of the longitude for this point
	 */
	getLng: function() {
		var meas = this.getMeasurement( "longitude" );
	    if(meas == null) {
	    	return null;
	    } else {
	    	return meas.value;
	    }	
	},
	
	/** Shortcut for directly getting the elevation value
	 * 
	 * @type Number
	 * @return The value of the elevation for this point
	 */
	getElev: function() {
		var meas = this.getMeasurement( "elevation" );
	    if(meas == null) {
	    	return null;
	    } else {
	    	return meas.value;
	    }	
	},
	
	/** Shortcut for directly getting the date/time
	 * 
	 * @type  Garmin.DateTimeFormat
	 * @return The time for this point
	 */
	getDate: function() {
		return this.date;
	},
	
	toString: function() {
		return "TrackPoint Point: (" + this.getLat() + ", " +  this.getLng() + ")";
	}
};



/** Equivalent to a <trkseg> in GPX format
 * 
 * @deprecated use Garmin.Activity instead
 * @class Garmin.TrackSegment
 * @constructor
 */
Garmin.TrackSegment = function(){};
Garmin.TrackSegment = Class.create();
Garmin.TrackSegment.prototype = {

    initialize: function() {
        this.points = new Array();
    },
    
    addTrackPoint: function(trackPointObject) {
    	this.points.push(trackPointObject);
    },
    
    /** Find the nearest valid point to the index given
     * 
     * @param index is the index
     * @param incDirection is an int in the direction we'd like to look positive 
     * 	nums are forward, negative nums are backwards
     * 
     * @type Garmin.TrackPoint 
     * @return The nearest point (possibly the index) that has a validLocation
     */ 
    findNearestValidLocationPoint: function(index, incDirection) {
        if( this.getPoint( index ).isValidLocation() ) {
            return this.getPoint( index );
        } else if( index >= this.getLength() ) {
        	return this.findNearestValidLocationPoint(this.getLength()-1, -1);
        } else {
            return this.findNearestValidLocationPoint(index+incDirection, incDirection);
        }
    },

	/** Get the point specified on the track
	 * If the number is negative, get's the first
	 * If it's larger than possible, get's the last
	 * Otherwise it gets the number requested
	 *
	 * @param {Number} index is the point we want
     * @type Garmin.TrackPoint 
	 * @return A TrackPoint that fits the pattern described above 
	 */
    getPoint: function(index) {
        index = Math.floor(index);
    
        if(index >= this.getLength()) {
            return this.getEnd();
        }
        if(index <= 0) {
            return this.getStart();
        }
            
        return this.points[index];
    },

    /** Quick method to get the first point
     * @type Garmin.TrackPoint 
     * @return The first point of this track
     */
    getStart: function() {
        return this.points[0];
    },

    /** Quick method to get the last point
     * @type Garmin.TrackPoint 
     * @return The last point of this track
     */
    getEnd: function() {
        return this.points[this.getLength()-1];
    },

    /** Get the latitude for the start point of this segment
     * @type Number
	 * @return Latitude of the first trackpoint
     */
    getStartLat: function() {
    	return this.getStart().getLat();
    },

    /** Get the longitude for the start point of this segment
     * @type Number
	 * @return Longitude of the first trackpoint
     */
    getStartLng: function() {
    	return this.getStart().getLng();
    },

    /** Get the data/time for the start point of this segment
     * @return date/time of the first trackpoint
     * @type Garmin.DateTimeFormat
     */
    getStartDate: function() {
    	return this.getStart().getDate();
    },

    /** Get the data/time for the end point of this segment
     * @type Garmin.DateTimeFormat
     * @return Date/time of the last trackpoint
     */
    getEndDate: function() {
    	return this.getEnd().getDate();
    },
    
    /** Get the total duration for this track segment
	 * @type String
	 * @return String of Duration (hh:mm:ss)
     */
    getDuration: function() {
    	return this.getStartDate().getDurationTo(this.getEndDate());
    },

    /** Get the total number of trackpoints in this segment
	 * @type Number
	 * @return Total number of trackpoints in this segment
     */
    getLength: function() {
        return this.points.length;
    },

    /** String representation
	 * @type String
     */
    toString: function() {
        return "Track Segment w/ " + this.getLength() + " points.";
    }
};


/** A track is an ordered list of track segments.<br>
 * Equivalent to a <trk> in GPX format.
 * 
 * @deprecated use Garmin.Activity instead
 * @class Garmin.Track
 * @constructor
 */
Garmin.Track = function(){}; 
Garmin.Track = Class.create();
Garmin.Track.prototype = {
    initialize: function() {
        this.segments = new Array();
    },
    
    /** Add a segment to this track
	 * @type Garmin.TrackPoint
     */
    addSegment: function(trackSegment) {
    	this.segments.push(trackSegment);
    },

	/** Get the segment specified on the track
	 * If the number is negative, get's the first
	 * If it's larger than possible, get's the last
	 * Otherwise it gets the number requested
	 *
	 * @param {Number} index is the segment we want
	 * @type Garmin.TrackSegment
	 * @return A segment that fits the pattern described above 
	 */
    getSegment: function(index) {
        index = Math.floor(index);
    
        if(index >= this.getLastSegment()) {
            return this.getEnd();
        }
        if(index <= 0) {
            return this.getFirstSegment();
        }
            
        return this.segments[index];
    },

    /** Get the first segment
	 * @type Garmin.TrackSegment
     * @return The first segment of this track
     */
    getFirstSegment: function() {
        return this.segments[0];
    },

    /** Get the last segment
	 * @type Garmin.TrackSegment
     * @return The last segment of this track
     */
    getLastSegment: function() {
        return this.segments[this.getNumSegments()-1];
    },

    /** Get the total length of the track
	 * @type Number
     */
    getNumSegments: function() {
        return this.segments.length;
    },

    /** Get the start point for the track
	 * @type Garmin.TrackPoint
     */
    getStart: function() {
    	return this.getFirstSegment().getStart();
    },

    /** Get the latitude of the start point for the track
	 * @type Number
     */
    getStartLat: function() {
    	return this.getFirstSegment().getStartLat();
    },

    /** Get the lpngitude of the start point for the track
	 * @type Number
     */
    getStartLng: function() {
    	return this.getFirstSegment().getStartLng();
    },
    
    /** Get the DateTimeFormat object for the start of this track
	 * @type Garmin.DateTimeFormat
     */
    getStartDate: function() {
    	return this.getFirstSegment().getStartDate();
    },

    /** Get the end point for the track
	 * @type Garmin.TrackPoint
     */
    getEnd: function() {
    	return this.getLastSegment().getEnd();
    },

    /** Get the DateTimeFormat object for the end of this track
	 * @type Garmin.DateTimeFormat
     */
    getEndDate: function() {
    	return this.getLastSegment().getEndDate();
    },
    
    /** Get the total duration for this track
	 * @type String
     */
    getDuration: function() {
    	return this.getStartDate().getDurationTo(this.getEndDate());
    },

    /** Get the total number of trackpoints in this track
	 * @type Number
     */
    getLength: function() {
		var length = 0;
		for( var i=0; i < this.segments.length; i++ ) {
			length += this.segments[i].getLength();
		}
        return length;
    },

    /** Checks to see if the startDate is defined.  If not then this is
     * technically a route and presents issues for some
     * track log databases where timestamps are used to merge old
     * and new entries.
	 * @type Boolean
	 * @return True if this track has a timestamp
     */
    isDrawable: function() {
    	return (this.getStartDate() != null);
    },

    toString: function() {
        return "Track w/ " + this.getNumSegments() + " segments.";
    }
};

/* THIS CLASS NOT NEEDED YET
 * @class Garmin.Address
 * Address class reprsents a US postal address.<br>
 * @constructor 

Garmin.Address = function(){};
Garmin.Address = Class.create();
Garmin.Address.prototype = {
	initialize: function() {
		this.streetAddress = null;
		this.streetAddress2 = null;
		this.city = null;
		this.state = null;
		this.postalCode = null;
	},

	toString: function() {
		return "Address: (" + this.streetAddress + ", " + 
			  (this.streetAddress2 ? this.streetAddress2+", " : "") + 
			  (this.city ? this.city+", " : "") + 
			  (this.state ? this.state+" " : "") + 
			  (this.postalCode ? this.postalCode : "") + 
			  ")";
	}
};
 */


/** Used to parse track and/or waypoint data from a number of Xml formats.<br>
 * Currently only supports tracks from a GPX file.
 * 
 * @deprecated use Garmin.GpxActivityFactory instead
 * @class Garmin.GpsDataFactory
 * @constructor
 */
Garmin.GpsDataFactory = function(){}; 
Garmin.GpsDataFactory = Class.create();
Garmin.GpsDataFactory.prototype = {

    initialize: function() {
    	this.tracks = new Array();
    	this.waypoints = new Array();
    },

    /** Get the tracks parsed by this factory
	 * @type Array<Garmin.Tracks>
     */
	getTracks: function() {
		return this.tracks;
	},

    /** Get the waypoints parsed by this factory
	 * @type Array<Garmin.WayPoint>
     */
	getWaypoints: function() {
		return this.waypoints;
	},

    /** Parse a gpx string and save the tracks found as objects
	 * @param {String} xml string in GPX format
     */
    parseGpxString: function(gpxString) {
		var gpxDocument = Garmin.XmlConverter.toDocument(gpxString);
		
		this.parseGpxDocument(gpxDocument);
	},

    /** Parse a gpx document and save the tracks and waypoints found
	 * @param {Document} xml document in GPX format
     */
    parseGpxDocument: function(gpxDocument) {
		this.parseGpxTracks(gpxDocument);
		this.parseGpxWaypoints(gpxDocument);
    },

    /** Parse a GPX xml document for tracks
	 * @param {Document} xml document in GPX format
     * @type Array<Garmin.Track>
     */
	parseGpxTracks: function(gpxDocument) {
		var tracks = new Array();

    	var trackNodes = gpxDocument.getElementsByTagName("trk");

		// triple for-loop fun
		for( var i=0; i < trackNodes.length; i++ ) {
			var trk = new Garmin.Track();

			var trackSegments = trackNodes[i].getElementsByTagName("trkseg");
	
			for( var j=0; j < trackSegments.length; j++ ) {
				var trkseg = new Garmin.TrackSegment();
				
				var trackPoints = trackSegments[j].getElementsByTagName("trkpt");
		
				for( var k=0; k < trackPoints.length; k++ ) {
					var trkpt = new Garmin.TrackPoint();

					var lat = trackPoints[k].getAttribute("lat");
					var lng = trackPoints[k].getAttribute("lon");
					var eleElement = trackPoints[k].getElementsByTagName("ele");
					var ele = (eleElement.length > 0) ? eleElement[0].childNodes[0].nodeValue : null;
					
					var timeNodes = trackPoints[k].getElementsByTagName("time");
					if(timeNodes.length > 0) {
						var time = timeNodes[0].childNodes[0].nodeValue;
						trkpt.date = (new Garmin.DateTimeFormat()).parseXsdDateTime(time);
					}
					
					trkpt.measurements = {
						latitude: {
							value: lat,
							context: "latitude"
						},
						longitude: {
							value: lng,
							context: "longitude"
						},
						elevation: {
							value: ele,
							context: "feet"
						}
					};
					
					trkseg.addTrackPoint(trkpt);
				}
				
				trk.addSegment(trkseg);
			}

			tracks.push(trk);
		}

    	this.tracks = tracks;
    	return tracks;
	},

    /** Parse a GPX xml waypoint into a Waypoint object.
	 * @param {Element} xml waypoint in GPX format
     * @type Garmin.WayPoint
     */
	parseGpxWaypoint: function(waypointNode) {
		var lat  = waypointNode.getAttribute("lat");
		var lng  = waypointNode.getAttribute("lon");
		var name = this._tagValue(waypointNode,"name");
		var desc = this._tagValue(waypointNode,"desc");
		var ele  = this._tagValue(waypointNode,"ele");
		var sym  = this._tagValue(waypointNode,"sym");
		var type  = this._tagValue(waypointNode,"type");
		var cmt  = this._tagValue(waypointNode,"cmt");
		
		var wpt  = new Garmin.WayPoint(lat, lng, ele, name, null, desc, sym, type, cmt);		
   		return wpt;
	},

    /** Parse a GPX xml document for waypoints
	 * @param {Document} xml document in GPX format
     * @type Array<Garmin.WayPoint>
     */
	parseGpxWaypoints: function(gpxDocument) {
		var waypoints = new Array();
    	var waypointNodes = gpxDocument.getElementsByTagName("wpt");
		for( var i=0; i < waypointNodes.length; i++ ) {
			var waypointNode = waypointNodes[i];
			var wpt = this.parseGpxWaypoint(waypointNode);
			waypoints.push(wpt);
		}
    	this.waypoints = waypoints;
    	return waypoints;
	},

    /** Take a list of tracks and waypoints and generate a GPX xml string
	 * @param {Array<Garmin.Track>} Tracks
	 * @param {Array<Garmin.WayPoint>} Waypoints
     * @type String
     */
	produceGpxString: function(tracks, waypoints) {
		gpxString = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>";
	
		gpxString += "<gpx xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:gpxx=\"http://www.garmin.com/xmlschemas/GpxExtensions/v3\" creator=\"Garmin Communicator Plug-In API\" version=\"1.1\" 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 http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensions/v3/GpxExtensionsv3.xsd\">";
		
		if(tracks != null) {
			for( var i=0; i < tracks.length; i++ ) {
				gpxString += this.produceTrackGpxString(tracks[i]);
			}
		}

		if(waypoints != null) {		
			for( var i=0; i < waypoints.length; i++ ) {
				gpxString += this.produceWaypointGpxString(waypoints[i]);
			}
		}
		gpxString += "</gpx>";
	
		return gpxString;
	},

    /** Take a track object and generate a GPX xml string (without gpx or xml headers)
	 * @param {Garmin.Track} Track
     * @type String
     */
	produceTrackGpxString: function(track) {
		gpxString = "<trk>";

		for( var i=0; i < track.getNumSegments(); i++ ) {
			var segment = track.getSegment(i);
			
			gpxString += "<trkseg>";
			for(var j=0; j < segment.getLength(); j++) {
				var point = segment.getPoint(j);
				
				gpxString += "<trkpt lat=\"" + point.getLat() + "\" lon=\"" + point.getLng() + "\">";
				if(point.getElev()) {
					gpxString += "<ele>" + point.getElev() + "</ele>";
				}
				if(point.getDate()) {
					gpxString += "<time>" + point.getDate().getXsdString() + "</time>";
				}
				gpxString += "</trkpt>";
			}
			gpxString += "</trkseg>";

		}
		
		gpxString += "</trk>";
	
		return gpxString;
	},

    /** Take a waypoint object and generate a GPX xml string (without gpx or xml headers)
	 * @param {Garmin.WayPoint} WayPoint
     * @type String
     */
	produceWaypointGpxString: function(waypoint) {
		gpxString = "<wpt lat=\"" + waypoint.getLat() + "\" lon=\"" + waypoint.getLng() + "\">";
	
		if(waypoint.getElev()) {
			gpxString += "<ele>" + waypoint.getElev() + "</ele>";
		}
		if(waypoint.getName()) {
			gpxString += "<name>" + waypoint.getName() + "</name>";
		}
		if(waypoint.getComment()) {
			gpxString += "<cmt>" + waypoint.getComment() + "</cmt>";
		}
		if(waypoint.getDescription()) {
			gpxString += "<desc>" + waypoint.getDescription() + "</desc>";
		}
		if(waypoint.getSymbol()) {
			gpxString += "<sym>" + waypoint.getSymbol() + "</sym>";
		}
		if(waypoint.getAddress()) {
			gpxString += "<extensions><gpxx:WaypointExtension>" + waypoint.getAddress() + "</gpxx:WaypointExtension></extensions>";
		}
		if(waypoint.getType()) {
			gpxString += "<type>" + waypoint.getType() + "</type>";
		}
		gpxString += "</wpt>";
	
		return gpxString;
	},

    /** Utility method to get the value of a child element.
     * @param {Node} parent DOM node
     * @param {String} name of child element
     * @type String value of child element
     */
	_tagValue: function(parentNode, tagName) {
		var subNode = parentNode.getElementsByTagName(tagName);
		return subNode.length > 0 ? subNode[0].childNodes[0].nodeValue : null;
	},
	
    /** String representation of instance.
     * @type String
     */
    toString: function() {
        return "GpsDataFactory.";
    }
};

if (Garmin == undefined) var Garmin = {};
/**
 * Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.Device A place-holder for Garmin device information. <br/>
 * Source: 
 * <a href="http://developer.garmin.com/web/communicator-api/garmin/device/GarminDevice.js">Hosted Distribution</a> &nbsp;
 * <a href="https://svn.garmindeveloper.com/web/trunk/communicator/communicator-api/src/main/webapp/garmin/device/GarminDevice.js">Source Control</a><br/>
 * 
 * @author Richard Easterling developer.connect.at.garmin.com
 * @version 1.0
 */
 
/** Plugin-specific utility functions.
 *
 * @class Garmin.PluginUtils
 * @constructor 
 */
Garmin.PluginUtils = function(){}; //just here for jsdoc
Garmin.PluginUtils = {
//Garmin.PluginUtils.prototype = {

	initialize: function() {
	},
	
	/** Parse device xml string into device objects.  
	 * 
	 * Each device object contains the following:
	 * 1) device display name
	 * 2) device number
	 * 3) device XML as an XML document
	 * 
	 * This function returns an array of the described Device objects.
	 * 
	 * @param garminPlugin - the GarminDevicePlugin object having access to the device XML data.
	 * @param getDetailedDeviceData - boolean indicating if you want to get the entire device XML 
	 *  as an XML document (rather than the few essentials)
	 */ 
	parseDeviceXml: function(garminPlugin, getDetailedDeviceData) {
        var xmlDevicesString = garminPlugin.getDevicesXml();
        var xmlDevicesDoc = Garmin.XmlConverter.toDocument(xmlDevicesString); 
        
        var deviceList = xmlDevicesDoc.getElementsByTagName("Device");
        var devices = new Array();
        var numDevices = deviceList.length;
        
    	for( var i=0; i < numDevices; i++ ) {
			var displayName = deviceList[i].getAttribute("DisplayName");        		
    		var deviceNumber = parseInt( deviceList[i].getAttribute("Number") );
    		var deviceDescriptionDoc = null;
    		if (getDetailedDeviceData) {
				var deviceDescriptionXml = garminPlugin.getDeviceDescriptionXml(deviceNumber);
				deviceDescriptionDoc = Garmin.XmlConverter.toDocument(deviceDescriptionXml);    
    		}
    		devices.push(Garmin.PluginUtils._createDeviceFromXml(displayName, deviceNumber, deviceDescriptionDoc));              		  		
    	}
    	return devices;
	},
	
	/** Create a Garmin.Device instance for each connected device found.
	 * @private
	 */
	_createDeviceFromXml: function(displayName, deviceNumber, deviceDescriptionDoc) {
   		var device = new Garmin.Device(displayName, deviceNumber);

   		if(deviceDescriptionDoc) {						
			var partNumber = deviceDescriptionDoc.getElementsByTagName("PartNumber")[0].childNodes[0].nodeValue;
			var softwareVersion = deviceDescriptionDoc.getElementsByTagName("SoftwareVersion")[0].childNodes[0].nodeValue;
			var description = deviceDescriptionDoc.getElementsByTagName("Description")[0].childNodes[0].nodeValue;
			var id = deviceDescriptionDoc.getElementsByTagName("Id")[0].childNodes[0].nodeValue;
			
			device.setPartNumber(partNumber);
			device.setSoftwareVersion(softwareVersion);
			device.setDescription(description);
			device.setId(id);
			
//			var dataTypeList = deviceDescriptionDoc.getElementsByTagName("DataType");
			var dataTypeList = deviceDescriptionDoc.getElementsByTagName("MassStorageMode")[0].getElementsByTagName("DataType");
			var numOfDataTypes = dataTypeList.length;
	
			for ( var j = 0; j < numOfDataTypes; j++ ) {
				var dataName = dataTypeList[j].getElementsByTagName("Name")[0].childNodes[0].nodeValue;					
				var dataExt = dataTypeList[j].getElementsByTagName("FileExtension")[0].childNodes[0].nodeValue;
				
				var dataType = new Garmin.DeviceDataType(dataName, dataExt);
				var fileList = dataTypeList[j].getElementsByTagName("File");
				
				var numOfFiles = fileList.length;
				
				for ( var k = 0; k < numOfFiles; k++ ) {
					// Path is an optional element in the schema
					var pathList = fileList[k].getElementsByTagName("Path");
					var transferDir = fileList[k].getElementsByTagName("TransferDirection")[0].childNodes[0].nodeValue;											
					
					if ((transferDir == Garmin.DeviceControl.TRANSFER_DIRECTIONS.read)) {
						dataType.setReadAccess(true);
						
						if (pathList.length > 0) {
						    var filePath = pathList[0].childNodes[0].nodeValue;
						    dataType.setReadFilePath(filePath);							
						}
					} else if ((transferDir == Garmin.DeviceControl.TRANSFER_DIRECTIONS.write)) {			
						dataType.setWriteAccess(true);
						
						if (pathList.length > 0) {
                            var filePath = pathList[0].childNodes[0].nodeValue;
                            dataType.setWriteFilePath(filePath);                         
                        }
					} else if ((transferDir == Garmin.DeviceControl.TRANSFER_DIRECTIONS.both)) {		
						dataType.setReadAccess(true);
						dataType.setWriteAccess(true);
						
						if (pathList.length > 0) {
                            var filePath = pathList[0].childNodes[0].nodeValue;
                            dataType.setReadFilePath(filePath);
                            dataType.setWriteFilePath(filePath);                         
                        }
					}

                    // Deprecated! Need to be removed at some point.
					if( pathList.length > 0) {
						var filePath = pathList[0].childNodes[0].nodeValue;
						dataType.setFilePath(filePath);
					}
					
					// Identifier is optional
					var identifierList = fileList[k].getElementsByTagName("Identifier");
					if( identifierList.length > 0) {
						var identifier = identifierList[0].childNodes[0].nodeValue;
						dataType.setIdentifier(identifier);
					}
				}			
				device.addDeviceDataType(dataType);
			}   			
   		}
		return device;
	},
	
	/** Is this a device XML error message.
	 * @param {String} xml string or Error instance with embedded xml
	 * @type Boolean
	 * @return true if error is device-generared error
	 */
	isDeviceErrorXml: function(error) {
		var msg = (typeof(error)=="string") ? error : error.name + ": " + error.message;
		return ( (msg.indexOf("<ErrorReport") > 0) );
	},
	
	/** Best effort to convert XML error message to a String.
	 * @param {String} xml string or Error instance with embedded xml
	 * @type String
	 * @return Human readable interpretation of XML message
	 */
	getDeviceErrorMessage: function(error) {
		var msg = (typeof(error)=="string") ? error : error.name + ": " + error.message;
		var startPos = msg.indexOf("<ErrorReport");
		if (startPos>0) { //strip off any text surrounding the xml
			var endPos = msg.indexOf("</ErrorReport>") + "</ErrorReport>".length;
			msg = msg.substring(startPos, endPos);
		}
        var xmlDoc = Garmin.XmlConverter.toDocument(msg); 
        var errorMessage = Garmin.PluginUtils._getElementValue(xmlDoc, "Extra");
        var sourceFileName = Garmin.PluginUtils._getElementValue(xmlDoc, "SourceFileName");
        var sourceFileLine = Garmin.PluginUtils._getElementValue(xmlDoc, "SourceFileLine");
        var msg = "";
        if (errorMessage) {
        	msg = errorMessage;
        } else { // gota show something :-(
        	msg = "Plugin error: ";
	        if (sourceFileName)
	        	msg += "source: "+sourceFileName;
	        if (sourceFileLine)
	        	msg += ", line: "+sourceFileLine;
        }
		return msg;
	},

	/** Get the value of a document element
	 * @param doc - the document that the element is contained in
	 * @param elementName - the name of the element to get the value from
	 * @return the value of the element identified by elementName 
	 */	
	_getElementValue: function(doc, elementName) {
        var elementNameNodes = doc.getElementsByTagName(elementName);
        var value = (elementNameNodes && elementNameNodes.length>0) ? elementNameNodes[0].childNodes[0].nodeValue : null;
 		return value;		
	}
};


/** GPI XML generation utility.
 *
 * @class Garmin.PluginUtils
 * @constructor 
 **/
Garmin.GpiUtil = function(){};
Garmin.GpiUtil = {
	
	/** Build a single DeviceDownload XML for multiple file downloads.  
	 * 
	 * @param descriptionArray - Even sized array with matching source and destination pairs.
	 * @param regionId - Optional parameter designating RegionId attribute of File.  For now, this single
	 * regionId will be applied to all files in the descriptionArray if provided at all.    
	 * 
	 */
	buildMultipleDeviceDownloadsXML: function(descriptionArray) {
		if(descriptionArray.length % 2 != 0) {
			throw new Error("buildMultipleDeviceDownloadsXML expects even sized array with matching source and destination pairs");
		}
		var xml =
		'<?xml version="1.0" encoding="UTF-8"?>\n' +
		'<DeviceDownload xmlns="http://www.garmin.com/xmlschemas/PluginAPI/v1"\n' +
		' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' +
		' xsi:schemaLocation="http://www.garmin.com/xmlschemas/PluginAPI/v1 http://www.garmin.com/xmlschemas/GarminPluginAPIV1.xsd">\n';

		for(var i=0;i<descriptionArray.length;i+=2) {
			var source = descriptionArray[i];
			var destination = descriptionArray[i+1];
		
//			if(!Garmin.GpiUtil.isDestinationValid(destinationArray[i])) {
//				throw new Error("Destination filename contains invalid characters: [" + destinationArray[i] + "]");
//			}
			xml += ' <File Source="'+source+'" Destination="'+destination+'" RegionId="46" />\n';
		}
		xml += '</DeviceDownload>';
		return xml;
	},
	
	buildDeviceDownloadXML: function(source, destination) {
		return Garmin.GpiUtil.buildMultipleDeviceDownloadsXML([source, destination]);
	},
	
	isDestinationValid: function(destination) {
		var splitPath = destination.split("/");
		var filename = splitPath[splitPath.length-1];

		var lengthBefore = filename.length;
		
		var stringAfter = Garmin.GpiUtil.cleanUpFilename(filename);
		
		return(lengthBefore == stringAfter.length);
	},
	
	cleanUpFilename: function(filename) {
		var result = filename;

		var replacement = "";						// see http://www.asciitable.com/
		result = result.stripTags();
		result = result.replace(/&amp;/, replacement);
		result = result.replace(/[\x21-\x2F]/g, replacement); 	// using range "!" through "/"
		result = result.replace(/[\x5B-\x60]/g, replacement);	// using range "[" through "`"
		result = result.replace(/[\x3A-\x40]/g, replacement);	// using range ":" through "@"
		result = result.strip();
		
		return result;
	}
};
if (Garmin == undefined) var Garmin = {};
/** Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.Device A place-holder for Garmin device information. <br/>
 * Source: 
 * <a href="http://developer.garmin.com/web/communicator-api/garmin/device/GarminDevice.js">Hosted Distribution</a> &nbsp;
 * <a href="https://svn.garmindeveloper.com/web/trunk/communicator/communicator-api/src/main/webapp/garmin/device/GarminDevice.js">Source Control</a><br/>
 * 
 * @author Michael Bina michael.bina.at.garmin.com
 * @version 1.0
 */
/** A place-holder for Garmin device information.
 * @class Garmin.Device
 *
 * @constructor 
 * @param {String} displayName for the device
 * @param {Number} number of the model
 */
Garmin.Device = function(displayName, number){}; //just here for jsdoc
Garmin.Device = Class.create();
Garmin.Device.prototype = {

	initialize: function(displayName, number) {
	    this.displayName = displayName;
	    this.number = number;
	    
	    this.partNumber = null;
	    this.softwareVersion = null;
	    this.description = null;
	    this.id = null;
	    
	    this.dataTypes = new Hash({});
	},
	
	/** The display name of this device.
	 * @type String
	 * @return display name
	 * @member Garmin.Device
	 */
	getDisplayName: function() {
		return this.displayName;
	},
	
	/** The device number that the plug-in uses to identify this device
	 * @type Number
	 * @return device number
	 */
	getNumber: function() {
		return this.number;
	},
	
	/** Set part number of device
	 * @param {String} part number
	 */
	setPartNumber: function(partNumber) {
		this.partNumber = partNumber;
	},

	/** The part number of the device
	 * @type String
	 * @return The part number of the device
	 */
	getPartNumber: function() {
		return this.partNumber;
	},

	/** Software version installed on device
	 * @param {String} Garmin.Device
	 */
	setSoftwareVersion: function(softwareVersion) {
		this.softwareVersion = softwareVersion;
	},

	/** The software version currently on the device
	 * @type String
	 * @return software version
	 */
	getSoftwareVersion: function() {
		return this.softwareVersion;
	},

	/** Set description of the device
	 * @param {String} device description
	 */
	setDescription: function(description) {
		this.description = description;
	},

	/** A description of the device.  This usually represents the model name of the device
	 * and includes the software version, i.e. "Forerunner 405  Jun 27 2008 2.17"
	 * In the GarminDevice XML, this is Model > Description.
	 * @type String
	 * @return device description
	 */
	getDescription: function() {
		return this.description;
	},

	/** set device id
	 * @param {String} device id
	 */
	setId: function(id) {
		this.id = id;
	},

	/** The device id
	 * @type String
	 * @return The device id
	 */
	getId: function() {
		return this.id;
	},
	
	/** Adds a data type to the list of data types supported by this device
	 * @param dataType A DeviceDataType object containing information about the data type being added
	 */
	addDeviceDataType: function(dataType) {
		var newDataType = new Hash();
		newDataType.set(dataType.getTypeName(), dataType);
		this.dataTypes.update(newDataType);	
	},

	/** Returns a specific DeviceDataType object
	 * @type Garmin.DeviceDataType
	 * @return The DeviceDataType object containing the specified extension
	 * @param extension The file extension of the data type you are trying to get
	 */	
	getDeviceDataType: function(extension) {
	    return this.dataTypes.get(extension);
	},

	/** Returns a hash containing all DeviceDataType objects
	 * @type Hash
	 * @return all DeviceDataType objects
	 */	
	getDeviceDataTypes: function() {
		return this.dataTypes;
	},

	/**	Returns true if the device has read support for the file type
	 * @param {String} The extension of the file type you are checking for support
	 * @type Boolean
	 * @return read support for the file type
	 */	
	supportDeviceDataTypeRead: function(extension) {
		var dataType = this.getDeviceDataType(extension);
		if (dataType != null && dataType.hasReadAccess()) {
			return true;
		} else {
			return false;
		}	
	},
	
	/** Check if device has write support for the file type.
	 * @param extension The extension of the file type you are checking for support
	 * @type Boolean
	 * @return True if write support 
	 */		
	supportDeviceDataTypeWrite: function(extension) {
		var dataType = this.getDeviceDataType(extension);
		if (dataType != null && dataType.hasWriteAccess()) {
			return true;
		} else {
			return false;
		}			
	},
	
	toString: function() {
		return "Device["+this.getDisplayName()+", "+this.getDescription()+", "+this.getNumber()+"]";
	}
	
};

/** A place-holder for Garmin Device Data Type information
 * @class Garmin.DeviceDataType
 *
 * @constructor 
 * @param {String} typeName for the data type
 * @param {String} file extension for the data type
 */
Garmin.DeviceDataType = function(typeName, fileExtension){}; //just here for jsdoc
Garmin.DeviceDataType = Class.create();
Garmin.DeviceDataType.prototype = {
	
	initialize: function(typeName, fileExtension) {
		this.typeName = typeName;
		this.fileExtension = fileExtension;
		this.readAccess = false;
		this.writeAccess = false;
		this.filePath = null;
		this.readFilePath = null;
		this.writeFilePath = null;
		this.identifier = null;
	},
	
	/**
	 * @type String
	 * @return The type name of this data type
	 */
	getTypeName: function() {
		return this.typeName;
	},
	
	/** 
	 * @deprecated
	 * @type String
	 * @return The type/display name of this data type
	 */
	getDisplayName: function() {
		return this.getTypeName();
	},
	
	/**
	 * @type String
	 * @return The file extension of this data type
	 */
	getFileExtension: function() {
		return this.fileExtension;
	},
	
	/**
	 * @type String
	 * @return The file path for this data type
	 */
	getFilePath: function() {
		return this.filePath;
	},
	
	/**
	 * @param filePath - the filepath for this datatype
	 */
	setFilePath: function(filePath) {
		this.filePath = filePath;
	},
	
	/**
     * @param readFilePath - the readFilePath for this datatype
     */
	getReadFilePath: function() {
	   return this.readFilePath;	
	},
	
	/**
	 * @type String
     * @return The read file path for this data type
     */
	setReadFilePath: function(readFilePath) {
		this.readFilePath = readFilePath;
	},
	
	/**
     * @param writeFilePath - the readFilePath for this datatype
     */
    getWriteFilePath: function() {
       return this.writeFilePath;    
    },
    
    /**
     * @type String
     * @return The write file path for this data type
     */
    setWriteFilePath: function(writeFilePath) {
        this.writeFilePath = writeFilePath;
    },
	
	/**
	 * @type String
	 * @return The identifier for this data type
	 */
	getIdentifier: function() {
		return this.identifier;
	},
	
	/**
	 * @param identifier- the identifier for this datatype
	 */
	setIdentifier: function(identifier) {
		this.identifier = identifier;
	},
	
	/**
	 * @param readAccess True == has read access
	 */
	setReadAccess: function(readAccess) {
		this.readAccess = readAccess;
	},
	
	/** Returns value indicating if the device supports read access for this file type
	 * @type Boolean
	 * @return supports read access for this file type
	 */
	hasReadAccess: function() {
		return this.readAccess;
	},
	
	/**
	 * @param {Boolean} has write access
	 */	
	setWriteAccess: function(writeAccess) {
		this.writeAccess = writeAccess;
	},
	
	/** return the value indicating if the device supports write access for this file type
	 * @type Boolean
	 * @return supports write access for this file type
	 */
	hasWriteAccess: function() {
		return this.writeAccess;
	}	
};

if (Garmin == undefined) var Garmin = {};
/** Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.MapController Overlays tracks and waypoint data on Google maps.
 * 
 * @author Jason Holmes jason.holmes.at.garmin.com
 * @author Bobby Yang bobby.yang.at.garmin.com
 * @version 1.0
 */
/**
 * Accepts Garmin.Series objects and draws them on a Google Map.
 * 
 * @class Garmin.MapController
 * @constructor 
 * @param (String) mapString id of element to place map in
 */
Garmin.MapController = function(mapString){}; //just here for jsdoc
Garmin.MapController = Class.create();
Garmin.MapController.prototype = {

    initialize: function(mapString) {
        this.mapElement = $(mapString);
        this.usePositionMarker = true;
        
        this.polylines = new Array();
        this.markers = new Array();
        this.tracks = new Array();
        this.markerIndex = 0;

        this.timeToCheck = false;
        try {
	        this.map = new GMap2( $(mapString) );
	        this.map.addControl(new GSmallMapControl());
			this.map.addControl(new GMapTypeControl());
        	new GKeyboardHandler(this.map);
        } catch (e) {
        	alert("WARNING: application will not function properly with missing Google script element or invalid Google map key.  Error: "+e);
        }
        window.onUnload = "GUnload()";
    },

    /** Set the center point and zoom level of the map.
     * @param (Number) Latitude of the center point
     * @param (Number) Longitude of the center point
     * @param (Number) Zoom level
     */
    centerAndScale: function(lat, lon, scale) {
    	scale = (scale == null ? 13 : scale);
        this.map.setCenter(new GLatLng(lat, lon), scale);
    },
    
    /** Draw track on map.
     * @param (Garmin.Track) The track to draw
     * @param (String) Color in RGB Hex format, default: "#ff0000"
     */    
    drawTrack: function(series, color) {
        color = (color == null ? "#ff0000" : color);
        
        // create a smaller version of the whole track
        // create 300 points or so ...
	    // Problem is that Google Maps dies when you hit near 500 points, so we have to
	    // ensure that we create fewer than that for the track.
        var drawAt = Math.ceil(series.getSamplesLength()/300);
        var drawnPoints = new Array();

        try {
        	// create up to 300 points
			for(var h = 0; h < series.getSamplesLength(); h += drawAt) {
				drawnPoints.push(this.createNearestValidLocationPoint(series, h, -1));
		    }
		    // create the end point
           	drawnPoints.push(this.createNearestValidLocationPoint(series, series.getSamplesLength()-1, -1));
        } catch( e ) {
            alert("GoogleMapControl.drawTrack: " + e.message);
        }	    
        
        if (drawnPoints.length > 0) {
	        //draw the new smaller version
	        var polyline = new GPolyline(drawnPoints, color, 2, .8)
			try {
				this.centerAndScale(drawnPoints[0].lat(), drawnPoints[0].lng());			
	        	this.map.addOverlay( polyline );
		        this.addStartFinishMarkers(series);
		        this.bounds = this.findAZoomLevel(drawnPoints);
		        this.setOnBounds( this.bounds );
			} catch(e){ alert("GoogleMapControl.drawTrack, IE error on map.addOverlay("+polyline+") err: "+e); }
        }
    },

	/**Creates a GLatLng for the sample in the series closest to the index with a valid location (lat and lon).
	 * @param series - The series to search through.
	 * @param index - The index to start searching from.
	 * @param incDirection - The direction to travel for the search.
	 * @return A GLatLng of the nearest valid location sample found.
	 */
	createNearestValidLocationPoint: function(series, index, incDirection) {
    	var sample = series.findNearestValidLocationSample(index, -1);
    	if (sample != null) {
    		var sampleLat = sample.getMeasurement(Garmin.Sample.MEASUREMENT_KEYS.latitude).getValue();
    		var sampleLon = sample.getMeasurement(Garmin.Sample.MEASUREMENT_KEYS.longitude).getValue();
    		return new GLatLng(sampleLat, sampleLon);    		
    	} else {
			throw new Error("No valid location point in series.");
    	}
	},

    /** Draw waypoint on map.
     * @param (Garmin.Series) series containing a waypoint to add to the map
     */
    drawWaypoint: function(series) {
    	var sample = series.getSample(0);
        this.centerAndScale(sample.getLatitude(), sample.getLongitude(), 15);    	
        this.addMarker(sample.getLatitude(), sample.getLongitude());
    },

    /** Calculates minimum bounding box for an set of points.
     * @param (Array) The array of points to find a zoom level for
     */
    findAZoomLevel: function(points) {
        var bounds = new GLatLngBounds(points[0], points[0]);
        
        for(var i=1; i<points.length-1; i+=3) {
            bounds.extend(points[i]);
        }
        
        return bounds;
    },
    
	/** Check the new dimensions of the map, and determine the bounds of the tracks
	 * Then set the map to zoom to that bound level
   	 * @private
	 */
    sizeAndSetOnBounds: function() {
        this.map.checkResize();
        this.setOnBounds( this.bounds );
    },

    /** Set the bounding box on the map.
     * @param (GLatLngBounds) bounding box for the
     */
    setOnBounds: function(bounds) {
        this.map.setCenter( this.bounds.getCenter(), this.map.getBoundsZoomLevel(this.bounds) );
    },
    
    /** Add an icon to a point.
     * @param {Number} latitude of marker
     * @param {Number} longitude of marker
     */
    addMarker: function(latitude, longitude) {
    	this.addMarkerWithIcon(latitude, longitude, Garmin.MapIcons.getRedIcon());
    },

    /** Adds a marker to the point with the icon specified
     * @param {Number} latitude of marker
     * @param {Number} longitude of marker
     * @param (GIcon) icon to add at the point
     */
    addMarkerWithIcon: function(latitude, longitude, icon) {
        var gMarker = new GMarker(new GLatLng(latitude, longitude), icon);
        this.markers.push(gMarker);
        this.map.addOverlay(gMarker);
    },

    /** Add start and finish markers to a track
     * @param (Garmin.Series) The series to add markers to
     */
    addStartFinishMarkers: function(series) {
    	var firstSample = series.getFirstValidLocationSample();
    	var lastSample = series.getLastValidLocationSample();
        this.addMarkerWithIcon(firstSample.getLatitude(), firstSample.getLongitude(), Garmin.MapIcons.getGreenIcon());
        this.addMarkerWithIcon(lastSample.getLatitude(), lastSample.getLongitude(), Garmin.MapIcons.getRedIcon());
    },

    /** String representation of map.
     * @return (String)
     */
    toString: function() {
        return "Google Based Map Controller, managing " + this.tracks.length + " track(s)";
    }
};

/** Icons used to mark waypoints and POIs on Google maps.  
 * 
 * @class Garmin.MapIcons
 * @constructor 
 */
Garmin.MapIcons = function(){}; //just here for jsdoc
Garmin.MapIcons = {
    getRedIcon: function() {
        var icon = new GIcon();
        icon.image = "http://trail.motionbased.com/trail/site/images/marker_red.png";
        return Garmin.MapIcons._applyShadowAndStuff(icon);
    },
    
    getGreenIcon: function() {
        var icon = new GIcon();
        icon.image = "http://trail.motionbased.com/trail/site/images/marker_green.png";
        return Garmin.MapIcons._applyShadowAndStuff(icon);
    },
    
    getBaseIcon: function() {
    	var baseIcon = new GIcon();
		baseIcon.shadow = "http://www.google.com/mapfiles/shadow50.png";
		baseIcon.iconSize = new GSize(20, 34);
		baseIcon.shadowSize = new GSize(37, 34);
		baseIcon.iconAnchor = new GPoint(9, 34);
		baseIcon.infoWindowAnchor = new GPoint(9, 2);
		baseIcon.infoShadowAnchor = new GPoint(18, 25);
        return baseIcon;
    },
    
    _applyShadowAndStuff: function(icon) {
        icon.iconSize = new GSize(12, 20);
        icon.shadow = "http://trail.motionbased.com/trail/site/images/marker_shadow.png";
        icon.shadowSize = new GSize(22, 20);
        icon.iconAnchor = new GPoint(6, 20);
        icon.infoWindowAnchor = new GPoint(5, 1);
        return icon;
    }
}
if (Garmin == undefined) var Garmin = {};
/** Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview GarminDevicePlugin wraps the Garmin ActiveX/Netscape plugin that should be installed on your machine inorder to talk to a Garmin Gps Device.
 * The plugin is available for download from http://www8.garmin.com/support/download_details.jsp?id=3608
 * More information is available about this plugin from http://www8.garmin.com/products/communicator/
 * 
 * @author Diana Chow diana.chow[at]garmin.com, Carlo Latasa carlo.latasa@garmin.com
 * @version 1.0
 */

/** This api provides a set of functions to accomplish the following tasks with a Gps Device:
 * <br/>
 * <br/>  1) Unlocking devices allowing them to be found and accessed.
 * <br/>  2) Finding avaliable devices plugged into this machine.
 * <br/>  3) Reading from the device.
 * <br/>  4) Writing gpx files to the device.
 * <br/>  5) Downloading data to the device.
 * <br/>	 6) Geting messages, getting transfer status/progress and version information from the device.
 * <br/><br/>
 * Note that the GarminPluginAPIV1.xsd is referenced throughout this API. Please find more information about the GarminPluginAPIV1.xsd from http://
 *  
 * @class
 * requires Prototype
 * @param pluginElement element that references the Garmin GPS Control Web Plugin that should be installed.
 * 
 * constructor 
 * @return a new GarminDevicePlugin
 **/
Garmin.DevicePlugin = function(pluginElement){};  //just here for jsdoc
Garmin.DevicePlugin = Class.create();
Garmin.DevicePlugin.prototype = {

    /** Constructor.
     * @private
     */
	initialize: function(pluginElement) {        
	    this.plugin = pluginElement;
	    this.unlocked = false;
	    //console.debug("DevicePlugin constructor supportsFitnessWrite="+this.supportsFitnessWrite)
	},
	
	/** Unlocks the GpsControl object to be used at the given web address.  
     * More than one set of path-key pairs my be passed in, for example:
     * ['http://myDomain.com/', 'xxx','http://www.myDomain.com/', 'yyy']
     * See documentation site for more info on getting a key. <br/>
     * <br/>
     * Minimum plugin version 2.0.0.4
     * 
     * @param pathKeyPairsArray {Array}- baseURL and key pairs.  
     * @type Boolean
     * @return true if successfully unlocked or undefined otherwise
     */
	unlock: function(pathKeyPairsArray) {
	    var len = pathKeyPairsArray ? pathKeyPairsArray.length / 2 : 0;
	    for(var i=0;i<len;i++) {
	    	if (this.plugin.Unlock(pathKeyPairsArray[i*2], pathKeyPairsArray[i*2+1])){
	    		this.unlocked = true;
	    		return this.unlocked;
	    	}
	    }
	    
	    // Unlock codes for local development
	    this.tryUnlock = this.plugin.Unlock("file:///","cb1492ae040612408d87cc53e3f7ff3c")
        	|| this.plugin.Unlock("http://localhost","45517b532362fc3149e4211ade14c9b2")
        	|| this.plugin.Unlock("http://127.0.0.1","40cd4860f7988c53b15b8491693de133");
        
        this.unlocked = !this.plugin.Locked;
        	
	    return this.unlocked;
	},
	
	/** Returns true if the plug-in is unlocked.
	 */
	isUnlocked: function() {
		return this.unlocked;
	},
	
	/**
	* Check to see if plugin supports this function.  We are having to pass in the string due
	* to IE evaluating the function when passed in as a parameter.
	*
	* @param pluginFunctionName {String} - name of the plugin function.
	* @return true - if function is available in the plugin.  False otherwise.
	*/
	_getPluginFunctionExists: function(pluginFunctionName) {
		var pluginFunction = "this.plugin." + pluginFunctionName;
		
	    try {
		    if( typeof eval(pluginFunction) == "function" ) {
		        return true;
		    }
		    else if(eval(pluginFunction)) {
		        return true;
		    }
		    else {
		        return false;
		    }
		}
		catch( e ) {
		    // For a supported function Internet Explorer says type is undefined but
		    // throws when the call is made.
		    return true;
    	}
	},

	/**
	* Check to see if plugin supports this field.
	*
	* @param pluginField {String} - name of the plugin field.
	* @return true - if the field is available in the plugin.  False otherwise.
	*/
	_getPluginFieldExists: function(pluginField) {
	    try {
		    if( typeof pluginField == "string" ) {
		        return true;
		    }
		    else if( pluginField ) {
		        return true;
		    }
		    else {
		        return false;
		    }
		}
		catch( e ) {
		    // For a supported function Internet Explorer says type is undefined but
		    // throws when the call is made.
		    return true;
    	}
	},
	
	/** Lazy-logic accessor to fitness write support var.
	 * This is used to detect whether the user's installed plugin supports fitness writing.
	 * Fitness writing capability has a minimum requirement of plugin version 2.2.0.1.
	 * This should NOT be called until the plug-in has been unlocked.
	 */
	getSupportsFitnessWrite: function() {
	    return this._getPluginFunctionExists("StartWriteFitnessData"); 
	},
	
	/** Lazy-logic accessor to fitness write support var.
	 * This is used to detect whether the user's installed plugin supports fitness directory reading,
	 * which has a minimum requirement of plugin version 2.2.0.2.
	 * This should NOT be called until the plug-in has been unlocked.
	 */
	getSupportsFitnessDirectoryRead: function() {	
		return this._getPluginFunctionExists("StartReadFitnessDirectory");
	},

	/** Lazy-logic accessor to FIT read support var.
	 * This is used to detect whether the user's installed plugin supports FIT directory reading,
	 * which has a minimum requirement of plugin version 2.8.x.x (TBD)
	 * This should NOT be called until the plug-in has been unlocked.
	 */
	getSupportsFitDirectoryRead: function() {		
			return this._getPluginFunctionExists("StartReadFITDirectory");
	},
	
	/** Lazy-logic accessor to fitness read compressed support var.
	 * This is used to detect whether the user's installed plugin supports fitness reading in compressed format,
	 * which has a minimum requirement of plugin version 2.2.0.2.
	 * This should NOT be called until the plug-in has been unlocked.
	 */
	getSupportsFitnessReadCompressed: function() {
	    return this._getPluginFieldExists(this.plugin.TcdXmlz);
	},
	
	/** Initiates a find Gps devices action on the plugin. 
	 * Poll with finishFindDevices to determine when the plugin has completed this action.
	 * Use getDeviceXmlString to inspect xml contents for and array of Device nodes.<br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
	 * 
	 * @see #finishFindDevices
	 * @see #cancelFindDevices
	 */
	startFindDevices: function() {
		this.plugin.StartFindDevices();
	},

	/** Cancels the current find devices interaction. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
	 * 
	 * @see #startFindDevices
	 * @see #finishFindDevices
	 */
	cancelFindDevices: function() {
        this.plugin.CancelFindDevices();
	},

	/** Poll - with this function to determine completion of startFindDevices. Used after 
	 * the call to startFindDevices(). <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
	 * 
	 * @type Boolean
	 * @return Returns true if completed finding devices otherwise false.
	 * @see #startFindDevices
	 * @see #cancelFindDevices
	 */
	finishFindDevices: function() {
    	return this.plugin.FinishFindDevices();
	},
	
	/** Returns information about the number of devices connected to this machine as 
	 * well as the names of those devices.  Refer to the 
	 * <a href="http://developer.garmin.com/schemas/device/v2/xmlspy/index.html#Link04DDFE88">Devices_t</a>
	 * element in the Device XML schema for what is included.
	 * The xml returned should contain a 'Device' element with 'DisplayName' and 'Number'
	 * if there is a device actually connected. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
	 * 
	 * @type String
	 * @return Xml string with detailed device info
	 * @see #getDeviceDescriptionXml
	 */
	getDevicesXml: function(){
		return this.plugin.DevicesXmlString();
	},

	/** Returns information about the specified Device indicated by the device Number. 
	 * See the getDevicesXml function to get the actual deviceNumber assigned.
	 * Refer to the 
	 * <a href="http://developer.garmin.com/schemas/device/v2/xmlspy/index.html#Link04DDFE88">Devices_t</a>
	 * element in the Device XML schema for what is included in the XML. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
	 * 
	 * @param deviceNumber {Number} Assigned by the plugin, see getDevicesXml for 
	 * assignment of that number.
	 * @type String
	 * @return Xml string with detailed device info
	 * @see #getDevicesXml
	 */
	getDeviceDescriptionXml: function(deviceNumber){
		return this.plugin.DeviceDescription(deviceNumber);
	},
	
	// Read Methods
	
	/** Initiates the read from the gps device conneted. Use finishReadFromGps and getGpsProgressXml to 
	 * determine when the plugin is done with this operation. Also, use getGpsXml to extract the
	 * actual data from the device. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
	 * 
	 * @param deviceNumber {Number} assigned by the plugin, see getDevicesXml for 
	 * assignment of that number.
	 * @see #finishReadFromGps
	 * @see #cancelReadFromGps
	 * @see #getDevicesXml
	 */
	startReadFromGps: function(deviceNumber) {
		 this.plugin.StartReadFromGps( deviceNumber );
	},

	/** Indicates the status of the read process. It will return an integer
	 * know as the completion state.  The purpose is to show the 
 	 * user information about what is happening to the plugin while it 
 	 * is servicing your request. Used after startReadFromGps(). <br/>
 	 * <br/>
 	 * Minimum plugin version 2.0.0.4
 	 * 
	 * @type Number
	 * @return Completion state - The completion state can be one of the following: <br/>
	 *  <br/>
	 *	0 = idle <br/>
 	 * 	1 = working <br/>
 	 * 	2 = waiting <br/>
 	 * 	3 = finished <br/>
 	 * @see #startReadFromGps
	 * @see #cancelReadFromGps
	 */
	finishReadFromGps: function() {
		return this.plugin.FinishReadFromGps();
	},
	
	/** Cancels the current read from the device. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
	 * @see #startReadFromGps
	 * @see #finishReadFromGps
     */	
	cancelReadFromGps: function() {
		this.plugin.CancelReadFromGps();
	},
	
	/** Start the asynchronous ReadFitnessData operation. <br/>
	 * <br/>
	 * Minimum plugin version 2.1.0.3 for FitnessHistory type<br/>
     * Minimum plugin version 2.2.0.1 for FitnessWorkouts, FitnessUserProfile, FitnessCourses
	 * 
	 * @param deviceNumber {Number} assigned by the plugin, see getDevicesXmlString for 
	 * assignment of that number.
	 * @param dataTypeName {String} a fitness datatype from the 
	 * <a href="http://developer.garmin.com/schemas/device/v2">Garmin Device XML</a> 
	 * retrieved with getDeviceDescriptionXml
	 * @see #finishReadFitnessData  
	 * @see #cancelReadFitnessData
	 * @see #getDeviceDescriptionXml
	 * @see Garmin.DeviceControl#FILE_TYPES
	 */
	startReadFitnessData: function(deviceNumber, dataTypeName) {
		if( !this.checkPluginVersionSupport([2,1,0,3]) ) {
			throw new Error("Your Communicator Plug-in version (" + this.getPluginVersionString() + ") does not support reading this type of fitness data.");
		}

		 this.plugin.StartReadFitnessData( deviceNumber, dataTypeName );
	},

	/** Poll for completion of the asynchronous ReadFitnessData operation. <br/>
     * <br/>
     * If the CompletionState is eMessageWaiting, call MessageBoxXml
     * to get a description of the message box to be displayed to
     * the user, and then call RespondToMessageBox with the value of the
     * selected button to resume operation.<br/>
     * <br/>
     * Minimum plugin version 2.1.0.3 for FitnessHistory type <br/>
     * Minimum plugin version 2.2.0.1 for FitnessWorkouts, FitnessUserProfile, FitnessCourses
	 * 
	 * @type Number
	 * @return Completion state - The completion state can be one of the following: <br/>
	 *  <br/>
	 *	0 = idle <br/>
 	 * 	1 = working <br/>
 	 * 	2 = waiting <br/>
 	 * 	3 = finished <br/>
 	 * @see #startReadFitnessData  
	 * @see #cancelReadFitnessData
	 */
	finishReadFitnessData: function() {
	 	 return  this.plugin.FinishReadFitnessData();
	},
	
	/** Cancel the asynchronous ReadFitnessData operation. <br/>
	 * <br/>
	 * Minimum plugin version 2.1.0.3 for FitnessHistory type <br/>
     * Minimum plugin version 2.2.0.1 for FitnessWorkouts, FitnessUserProfile, FitnessCourses
     * 
     * @see #startReadFitnessData  
	 * @see #finishReadFitnessData
     */	
	cancelReadFitnessData: function() {
		this.plugin.CancelReadFitnessData();
	},
	
	/**
	 * List all of the FIT files on the device. Starts an asynchronous directory listing operation for the device.
	 * Poll for finished with FinishReadFitDirectory. The result is stored in ______.
	 * 
	 * Minimum plugin version 2.7.2.0
	 * @see #finishReadFitDirectory
	 */
	startReadFitDirectory: function(deviceNumber) {
	    if( !this.getSupportsFitDirectoryRead() ) {
			throw new Error("Your Communicator Plug-in version (" + this.getPluginVersionString() + ") does not support reading directory listing data.");
		}
	    this.plugin.StartReadFITDirectory(deviceNumber);
	},
	
	/** Poll for completion of the asynchronous startReadFitDirectory operation. <br/>
     * <br/>
	 * Minimum plugin version 2.7.2.0
	 * 
	 * @type Number
	 * @return Completion state - The completion state can be one of the following: <br/>
	 *  <br/>
	 *	0 = idle <br/>
 	 * 	1 = working <br/>
 	 * 	2 = waiting <br/>
 	 * 	3 = finished <br/>
	 * 
	 * @see #startReadFitDirectory
	 * @see #cancelReadFitDirectory
	 * @see #getMessageBoxXml
	 * @see #respondToMessageBox
	 */
	finishReadFitDirectory: function() {
		return this.plugin.FinishReadFITDirectory();
	},
	
	/** Start the asynchronous ReadFitnessDirectory operation. <br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.2
	 * 
	 * @param deviceNumber {Number} assigned by the plugin, see getDevicesXmlString for 
	 * assignment of that number.
	 * @param dataTypeName a Fitness DataType from the GarminDevice.xml retrieved with DeviceDescription
	 * @see #finishReadFitnessDirectory
	 * @see #cancelReadFitnessDirectory
	 * @see Garmin.DeviceControl#FILE_TYPES
	 */
	startReadFitnessDirectory: function(deviceNumber, dataTypeName) {
		if( !this.getSupportsFitnessDirectoryRead() ) {
			throw new Error("Your Communicator Plug-in version (" + this.getPluginVersionString() + ") does not support reading fitness directory data.");
		}
		this.plugin.StartReadFitnessDirectory( deviceNumber, dataTypeName);
	},
	
	/** Poll for completion of the asynchronous ReadFitnessDirectory operation. <br/>
     * <br/>
     * If the CompletionState is eMessageWaiting, call getMessageBoxXml
     * to get a description of the message box to be displayed to
     * the user, and then call respondToMessageBox with the value of the
     * selected button to resume operation.<br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.2
	 * 
	 * @type Number
	 * @return Completion state - The completion state can be one of the following: <br/>
	 *  <br/>
	 *	0 = idle <br/>
 	 * 	1 = working <br/>
 	 * 	2 = waiting <br/>
 	 * 	3 = finished <br/>
	 * 
	 * @see #startReadFitnessDirectory
	 * @see #cancelReadFitnessDirectory
	 * @see #getMessageBoxXml
	 * @see #respondToMessageBox
	 */
	finishReadFitnessDirectory: function() {
		return this.plugin.FinishReadFitnessDirectory();
	},
	
	/** Cancel the asynchronous ReadFitnessDirectory operation. <br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.2
	 * 
	 * @see #startReadFitnessDirectory
	 * @see #finishReadFitnessDirectory
     */	
	cancelReadFitnessDirectory: function() {
		this.plugin.CancelReadFitnessDirectory();
	},

	/** Cancel the asynchronous ReadFitDirectory operation. <br/>
	 * <br/>
	 * Minimum plugin version 2.7.2.0
	 * 
	 * @see #startReadFitDirectory
	 * @see #finishReadFitDirectory
     */	
	cancelReadFitDirectory: function() {
		this.plugin.CancelReadFitDirectory();
	},
	
	/** Start the asynchronous ReadFitnessDetail operation. <br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.2
	 * 
	 * @param deviceNumber assigned by the plugin, see getDevicesXmlString for 
	 * assignment of that number.
	 * @param dataTypeName a Fitness DataType from the GarminDevice.xml retrieved with DeviceDescription
	 * @see #finishReadFitnessDetail
	 * @see #cancelReadFitnessDetail
	 * @see Garmin.DeviceControl#FILE_TYPES
	 */
	startReadFitnessDetail: function(deviceNumber, dataTypeName, dataId) {
		if( !this.checkPluginVersionSupport([2,2,0,2]) ) {
			throw new Error("Your Communicator Plug-in version (" + this.getPluginVersionString() + ") does not support reading fitness detail.");
		}
		
		this.plugin.StartReadFitnessDetail(deviceNumber, dataTypeName, dataId);
	},
	
	/** Poll for completion of the asynchronous ReadFitnessDetail operation. <br/>
     * <br/>
     * If the CompletionState is eMessageWaiting, call MessageBoxXml
     * to get a description of the message box to be displayed to
     * the user, and then call RespondToMessageBox with the value of the
     * selected button to resume operation.<br/>
     * <br/>
     * Minimum plugin version 2.2.0.2
	 * 
	 * @type Number
	 * @return Completion state - The completion state can be one of the following: <br/>
	 *  <br/>
	 *	0 = idle <br/>
 	 * 	1 = working <br/>
 	 * 	2 = waiting <br/>
 	 * 	3 = finished <br/>
	 * 
	 */
	finishReadFitnessDetail: function() {
		return this.plugin.FinishReadFitnessDetail();
	},
	
	/** Cancel the asynchronous ReadFitnessDirectory operation. <br/>
	 * <br/>
	 * Minimum version 2.2.0.2
	 * 
	 * @see #startReadFitnessDetail
	 * @see #finishReadFitnessDetail
     */	
	cancelReadFitnessDetail: function() {
		this.plugin.CancelReadFitnessDetail();
	},
	
	// Write Methods
	
	/** Initates writing the gpsXml to the device specified by deviceNumber with a filename set by filename.
	 * The gpsXml is typically in GPX fomat and the filename is only the name without the extension. The 
	 * plugin will append the .gpx extension automatically.<br/>
	 * <br/>
	 * Use finishWriteToGps to poll when the write operation/plugin is complete.<br/>
	 * <br/>
	 * Uses the helper functions to set the xml info and the filename.  <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4<br/>
     * Minimum plugin version 2.2.0.1 for writes of GPX to SD Card
	 * 
	 * @param gpsXml {String} the gps/gpx information that should be transferred to the device.
	 * @param filename {String} the desired filename for the gpsXml that shall end up on the device.
	 * @param deviceNumber {Number} the device number assigned by the plugin.
	 * @see #finishWriteToGps
	 * @see #cancelWriteToGps  
	 */
	startWriteToGps: function(gpsXml, filename, deviceNumber) {
		this._setWriteGpsXml(gpsXml);
		this._setWriteFilename(filename);
	    this.plugin.StartWriteToGps(deviceNumber);
	},

	/** Sets the gps xml content that will end up on the device once the transfer is complete.
	 * Use in conjunction with startWriteToGps to initiate the actual write.
	 *
	 * @private 
	 * @param gpsXml {String} xml data that is to be written to the device. Must be in GPX format.
	 */
	_setWriteGpsXml: function(gpsXml) {
    	this.plugin.GpsXml = gpsXml;
	},

	/** This the filename that wil contain the gps xml once the transfer is complete. Use with 
	 * setWriteGpsXml to set what the file contents will be. Also, use startWriteToGps to 
	 * actually make the write happen.
	 * 
	 * @private
	 * @param filename {String} the actual filename that will end up on the device. Should only be the
	 * name and not the extension. The plugin will append the extension portion to the file name--typically .gpx.
	 * @see #setWriteGpsXml, #startWriteToGps, #startWriteFitnessData
	 */
	_setWriteFilename: function(filename) {
    	this.plugin.FileName = filename;
	},

	/** This is used to indicate the status of the write process. It will return an integer
	 * know as the completion state.  The purpose is to show the 
 	 * user information about what is happening to the plugin while it 
 	 * is servicing your request. <br/>
 	 * <br/>
 	 * Minimum plugin version 2.0.0.4<br/>
     * Minimum plugin version 2.2.0.1 for writes of GPX to SD Card 
 	 * 
	 * @type Number
	 * @return Completion state - The completion state can be one of the following: <br/>
	 *  <br/>
	 *	0 = idle <br/>
 	 * 	1 = working <br/>
 	 * 	2 = waiting <br/>
 	 * 	3 = finished <br/>
 	 * @see #startWriteToGps
	 * @see #cancelWriteToGps  
 	 */
	finishWriteToGps: function() {
		//console.debug("Plugin.finishWriteToGps");
	   	return  this.plugin.FinishWriteToGps();
	},
    
	/** Cancels the current write operation to the gps device. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4<br/>
     * Minimum plugin version 2.2.0.1 for writes of GPX to SD Card
     * 
     * @see #startWriteToGps
	 * @see #finishWriteToGps  
     */	
	cancelWriteToGps: function() {
		this.plugin.CancelWriteToGps();
	},

	/** Start the asynchronous StartWriteFitnessData operation. <br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.1
	 * 
	 * @param tcdXml {String} XML of TCD data
	 * @param deviceNumber {Number} the device number, assigned by the plugin. See getDevicesXmlString for 
	 * assignment of that number.
	 * @param filename {String} the filename to write to on the device.
	 * @param dataTypeName {String} a Fitness DataType from the GarminDevice.xml retrieved with DeviceDescription
	 * @see #finishWriteFitnessData  
	 * @see #cancelWriteFitnessData
	 * @see Garmin.DeviceControl#FILE_TYPES
	 */
	startWriteFitnessData: function(tcdXml, deviceNumber, filename, dataTypeName) {	
		if( !this.checkPluginVersionSupport([2,2,0,1]) ) {
			throw new Error("Your Communicator Plug-in version (" + this.getPluginVersionString() + ") does not support writing fitness data.");
		}
		
		this._setWriteTcdXml(tcdXml);
		this._setWriteFilename(filename);
		this.plugin.StartWriteFitnessData(deviceNumber, dataTypeName);
	},
	
	/** This is used to indicate the status of the write process for fitness data. It will return an integer
	 * know as the completion state.  The purpose is to show the 
 	 * user information about what is happening to the plugin while it 
 	 * is servicing your request. <br/>
 	 * <br/>
 	 * Minimum plugin version 2.2.0.1
 	 * 
	 * @type Number
	 * @return Completion state - The completion state can be one of the following: <br/>
	 *  <br/>
	 *	0 = idle <br/>
 	 * 	1 = working <br/>
 	 * 	2 = waiting <br/>
 	 * 	3 = finished <br/>
 	 * @see #startWriteFitnessData  
	 * @see #cancelWriteFitnessData
	 */
	finishWriteFitnessData: function() {
	 	return  this.plugin.FinishWriteFitnessData();
	},
	
	/** Cancel the asynchronous ReadFitnessData operation. <br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.1
	 * 
	 * @see #startWriteFitnessData  
	 * @see #finishWriteFitnessData
     */	
	cancelWriteFitnessData: function() {
		this.plugin.CancelWriteFitnessData();
	},
	
	/** Sets the tcd xml content that will end up on the device once the transfer is complete.
	 * Use in conjunction with startWriteFitnessData to initiate the actual write.
	 *
	 * @private 
	 * @param tcdXml {String} xml data that is to be written to the device. Must be in TCX format.
	 */
	_setWriteTcdXml: function(tcdXml) {
    	this.plugin.TcdXml = tcdXml;
	},
	
	/**
	 * Determine the amount of space available on a Mass Storage Mode Device Volume.
	 * 
	 * @param {Number} deviceNumber - the device number assigned by the plugin. See {@link getDevicesXmlString} for 
	 * assignment of that number.
	 * @param {String} relativeFilePath - if a file is being replaced, set to relative path on device, otherwise set to empty string.
	 * @return -1 for non-mass storage mode devices.  
	 */
	bytesAvailable: function(deviceNumber, relativeFilePath) {
	    return this.plugin.BytesAvailable(deviceNumber, relativeFilePath);
	},

    /** Responds to a message box on the device. <br/>
     * <br/>
     * Minimum plugin version 2.0.0.4
     *   
     * @param response should be an int which corresponds to a button value from this.plugin.MessageBoxXml
     */
    respondToMessageBox: function(response) {
        this.plugin.RespondToMessageBox(response);
    },

	/** Initates downloading the gpsDataString to the device specified by deviceNumber.
	 * The gpsDataString is typically in GPI fomat and the filename is only the name without the extension. The 
	 * plugin will append the .gpx extension automatically.<br/>
	 * <br/>
	 * Use finishWriteToGps to poll when the write operation/plugin is complete.<br/>
	 * <br/>
	 * Uses the helper functions to set the xml info and the filename.  <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
	 *  
	 * @param gpsDataString {String} the gpi information that should be transferred to the device.
	 * @param filename {String} the filename to write to on the device.
	 * @param deviceNumber {Number} the device number assigned by the plugin. 
	 * @see #finishDownloadData  
	 * @see #cancelDownloadData
	 */
	startDownloadData: function(gpsDataString, deviceNumber) {
		//console.debug("Plugin.startDownloadData gpsDataString="+gpsDataString);
		this.plugin.StartDownloadData(gpsDataString, deviceNumber);
	},

	/** This is used to indicate the status of the download process. It will return an integer
	 * know as the completion state.  The purpose is to show the 
 	 * user information about what is happening to the plugin while it 
 	 * is servicing your request.<br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
	 * 
	 * @type Number
	 * @return Completion state - The completion state can be one of the following: <br/>
	 *  <br/>
	 *	0 = idle <br/>
 	 * 	1 = working <br/>
 	 * 	2 = waiting <br/>
 	 * 	3 = finished <br/>
 	 * @see #startDownloadData  
	 * @see #cancelDownloadData
	 */
	finishDownloadData: function() {
		//console.debug("Plugin.finishDownloadData");
		return this.plugin.FinishDownloadData();
	},

	/** Cancel the asynchronous Download Data operation. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
	 * 
	 * @see #startDownloadData  
	 * @see #finishDownloadData
	 */
	cancelDownloadData: function() {
		this.plugin.CancelDownloadData();
	},

    /** Indicates success of StartDownloadData operation. <br/>
     * <br/>
     * Minimum plugin version 2.0.0.4
     * 
     * @type Boolean
     * @return True if the last StartDownloadData operation was successful
     */
    downloadDataSucceeded: function() {
		return this.plugin.DownloadDataSucceeded;
    },

    /** Download and install a list of unit software updates.  Start the asynchronous 
     * StartUnitSoftwareUpdate operation.
     * 
     * Check for completion with the FinishUnitSoftwareUpdate() method.  After
     * completion check the DownloadDataSucceeded property to make sure that all of the downloads 
     * were successfully placed on the device. 
     * 
     * See the Schema UnitSoftwareUpdatev3.xsd for the format of the UpdateResponsesXml description
     *
     * @see Garmin.DevicePlugin.finishUnitSoftwareUpdate
     * @see Garmin.DevicePlugin.cancelUnitSoftwareUpdate
     * @see Garmin.DevicePlugin.downloadDataSucceeded
     * @version plugin v2.6.2.0
     */
    startUnitSoftwareUpdate: function(updateResponsesXml, deviceNumber) {
        this.plugin.StartUnitSoftwareUpdate(updateResponsesXml, deviceNumber);
    },
    
    /** Poll for completion of the asynchronous Unit Software Update operation. It will return an integer
	 * know as the completion state.  The purpose is to show the 
 	 * user information about what is happening to the plugin while it 
 	 * is servicing your request.<br/>
 	 * @type Number 
     * @version plugin v2.6.2.0
     * @return Completion state - The completion state can be one of the following: <br/>
	 *  <br/>
	 *	0 = idle <br/>
 	 * 	1 = working <br/>
 	 * 	2 = waiting <br/>
 	 * 	3 = finished <br/>
 	 * @see Garmin.DevicePlugin.startUnitSoftwareUpdate
 	 * @see Garmin.DevicePlugin.cancelUnitSoftwareUpdate
     */
    finishUnitSoftwareUpdate: function() {
        return this.plugin.FinishUnitSoftwareUpdate();  
    },
    
    /** Cancel the asynchrous Download Data operation
     * @version plugin v2.6.2.0
     */
    cancelUnitSoftwareUpdate: function() {
        this.plugin.CancelUnitSoftwareUpdate();
    },
    
    /** Get the UnitSoftwareUpdateRequests for a given device.
     * This request retrieves the main system software (system region only.)
     * @param deviceNumber {Number} the device number to retrieve unit software information for. 
     * @return {String} XML string of the document format in the namespace below, or
     * the most current version of that xms namespace
     * http://www.garmin.com/xmlschemas/UnitSoftwareUpdate/v3
     * @version plugin v2.6.2.0
     * @see Garmin.DevicePlugin.getAdditionalSoftwareUpdateRequests
     */
//    getUnitSoftwareUpdateRequests: function(deviceNumber) {
//        return this.plugin.UnitSoftwareUpdateRequests(deviceNumber);
//    },
    
    /** Get the AdditionalSoftwareUpdateRequests for a given device.
     * This request retrieves the additional system software (all software except for system region.)
     * @param deviceNumber {Number} the device number to retrieve unit software information for.
     * @return {String} XML string of the document format in the namespace below, or
     * the most current version of that xms namespace
     * http://www.garmin.com/xmlschemas/UnitSoftwareUpdate/v3
     * @version plugin v2.6.2.0
     * @see Garmin.DevicePlugin.getUnitSoftwareUpdateRequests
     */
//    getAdditionalSoftwareUpdateRequests: function(deviceNumber) {
//        return this.plugin.AdditionalSoftwareUpdateRequests(deviceNumber);
//    },
    
    /** Indicates success of WriteToGps operation. <br/>
     * <br/>
     * Minimum plugin version 2.0.0.4
     * 
     * @type Boolean
     * @return True if the last ReadFromGps or WriteToGps operation was successful
     */
    gpsTransferSucceeded: function() {
		return this.plugin.GpsTransferSucceeded;
    },

    /** Indicates success of ReadFitnessData or WriteFitnessData operation. <br/>
     * <br/>
     * Minimum plugin version 2.1.0.3
     * 
     * @type Boolean
     * @return True if the last ReadFitnessData or WriteFitnessData operation succeeded
     */
    fitnessTransferSucceeded: function() {
		return this.plugin.FitnessTransferSucceeded;
    },
    
    /** Return the specified file as a UU-Encoded string
     * <br/>
     * Minimum version 2.6.3.1
     * 
     * If the file is known to be compressed, compressed should be
     * set to false. Otherwise, set compressed to true to retrieve a
     * gzipped and uuencoded file.
     * 
     * @param relativeFilePath {String} path relative to the Garmin folder on the device
     */
    getBinaryFile: function(deviceNumber, relativeFilePath, compressed) {
        return this.plugin.GetBinaryFile(deviceNumber, relativeFilePath, compressed);
    },
    
    /** This is the GpsXml information from the device. Typically called after a read operation.
     * 
     * @see #finishReadFromGps
     */
	getGpsXml: function(){
		return this.plugin.GpsXml;
	},

    /** This is the fitness data Xml information from the device. Typically called after a ReadFitnessData operation. <br/>
	 * <br/>
     * Schemas for the TrainingCenterDatabase format are available at
     * <a href="http://developer.garmin.com/schemas/tcx/v2/">http://developer.garmin.com/schemas/tcx/v2/</a><br/>
     * <br/>
     * Minimum plugin version 2.1.0.3
     * 
     * @see #finishReadFitnessData
     * @see #finishReadFitnessDirectory
     * @see #finishReadFitnessDetail
     */
	getTcdXml: function(){
		return this.plugin.TcdXml;
	},
	
	 /** Returns last read fitness xml data in compressed format.  The xml is compressed as gzp and base64 expanded. <br/>
	  * <br/>
	  * Minimum plugin version 2.2.0.2
	  * 
	  * @return The read xml data in compressed gzp and base64 expanded format.
	  * @see #finishReadFitnessData
      * @see #finishReadFitnessDirectory
      * @see #finishReadFitnessDetail
	  */
	getTcdXmlz: function() {
		return this.plugin.TcdXmlz;
	},

	 /** Returns last read directory xml data.<br/>
	  * <br/>
	  * 
	  * @return The directory xml data
	  * @see #finishReadFitDirectory
	  */
	getDirectoryXml: function() {
		return this.plugin.DirectoryListingXml;
	},

    /** Returns the xml describing the message when the plug-in is waiting for input from the user.
     * @type String
     * @return The xml describing the message when the plug-in is waiting for input from the user.
     */
	getMessageBoxXml: function(){
		return this.plugin.MessageBoxXml;
	},
    
	/** Get the status/progress of the current state or transfer.
     * @type String
     * @return The xml describing the current progress state of the plug-in.
     */	
	getProgressXml: function() {
		return this.plugin.ProgressXml;
	},

	/** Returns metadata information about the plugin version. 
     * @type String
     * @return The xml describing the user's version of the plug-in.
	 */
	getVersionXml: function() {
		return this.plugin.VersionXml;
	},
	
	/** Gets a string of the version number for the plugin the user has currently installed.
     * @type String 
     * @return A string of the format "versionMajor.versionMinor.buildMajor.buildMinor", ex: "2.0.0.4"
     */	
	getPluginVersionString: function() {
		var versionArray = this.getPluginVersion();
	
		var versionString = versionArray[0] + "." + versionArray[1] + "." + versionArray[2] + "." + versionArray[3];
	    return versionString;
	},
	
	/** Gets the version number for the plugin the user has currently installed.
     * @type Array 
     * @return An array of the format: [versionMajor, versionMinor, buildMajor, buildMinor].
     */	
	getPluginVersion: function() {
    	var versionMajor = parseInt(this._getElementValue(this.getVersionXml(), "VersionMajor"));
    	var versionMinor = parseInt(this._getElementValue(this.getVersionXml(), "VersionMinor"));
    	var buildMajor = parseInt(this._getElementValue(this.getVersionXml(), "BuildMajor"));
    	var buildMinor = parseInt(this._getElementValue(this.getVersionXml(), "BuildMinor"));

	    var versionArray = [versionMajor, versionMinor, buildMajor, buildMinor];
	    return versionArray;
	},
	
	/** Sets the required plugin version number for the application.
	 * @param reqVersionArray {Array} The required version to set to.  In the format [versionMajor, versionMinor, buildMajor, buildMinor]
	 * 			i.e. [2,2,0,1]
	 */
	setPluginRequiredVersion: function(reqVersionArray) {
		Garmin.DevicePlugin.REQUIRED_VERSION.versionMajor = reqVersionArray[0];
		Garmin.DevicePlugin.REQUIRED_VERSION.versionMinor = reqVersionArray[1];
		Garmin.DevicePlugin.REQUIRED_VERSION.buildMajor = reqVersionArray[2];
		Garmin.DevicePlugin.REQUIRED_VERSION.buildMinor = reqVersionArray[3];
	},
	
	/** Sets the latest plugin version number.  This represents the latest version available for download at Garmin.
	 * We will attempt to keep the default value of this up to date with each API release, but this is not guaranteed,
	 * so set this to be safe or if you don't want to upgrade to the latest API.
	 * 
	 * @param reqVersionArray {Array} The latest version to set to.  In the format [versionMajor, versionMinor, buildMajor, buildMinor]
	 * 			i.e. [2,2,0,1]
	 */
	setPluginLatestVersion: function(reqVersionArray) {
		Garmin.DevicePlugin.LATEST_VERSION.versionMajor = reqVersionArray[0];
		Garmin.DevicePlugin.LATEST_VERSION.versionMinor = reqVersionArray[1];
		Garmin.DevicePlugin.LATEST_VERSION.buildMajor = reqVersionArray[2];
		Garmin.DevicePlugin.LATEST_VERSION.buildMinor = reqVersionArray[3];
	},
	
	/** Used to check if the user's installed plugin version meets the required version for feature support purposes.
	 *  
	 * @param {Array} reqVersionArray An array representing the required version, in the format: [versionMajor, versionMinor, buildMajor, buildMinor]. 
	 * @return {boolean} true if the passed in required version is met by the user's plugin version (user's version is equal to or greater), false otherwise.
	 * @see setPluginRequiredVersion
	 */
	checkPluginVersionSupport: function(reqVersionArray) {
		
		var pVersion = this._versionToNumber(this.getPluginVersion());
   		var rVersion = this._versionToNumber(reqVersionArray);
        return (pVersion >= rVersion);
	},
	
	/**
	 * @private
	 */
	_versionToNumber: function(versionArray) {
		if (versionArray[1] > 99 || versionArray[2] > 99 || versionArray[3] > 99)
			throw new Error("version segment is greater than 99: "+versionArray);
		return 1000000*versionArray[0] + 10000*versionArray[1] + 100*versionArray[2] + versionArray[3];
	},
	
	/** Determines if the Garmin plugin is at least the required version for the application.
     * @type Boolean
     * @see setPluginRequiredVersion
	 */
	isPluginOutOfDate: function() {
    	var pVersion = this._versionToNumber(this.getPluginVersion());
   		var rVersion = this._versionToNumber(Garmin.DevicePlugin.REQUIRED_VERSION.toArray());
        return (pVersion < rVersion);
	},
	
	/** Checks if plugin is the most recent version released, for those that want the latest and greatest.
     */
    isUpdateAvailable: function() {
    	var pVersion = this._versionToNumber(this.getPluginVersion());
   		var cVersion = this._versionToNumber(Garmin.DevicePlugin.LATEST_VERSION.toArray());
        return (pVersion < cVersion);
    },
	
	/** Pulls value from xml given an element name or null if no tag exists with that name.
	 * @private
	 */
	_getElementValue: function(xml, tagName) {
		var start = xml.indexOf("<"+tagName+">");
		if (start == -1)
			return null;
		start += tagName.length+2;
		var end = xml.indexOf("</"+tagName+">");
		var result = xml.substring(start, end);
		return result;
	}
	
};

/** Latest version (not required) of the Garmin Communicator Plugin, and a complementary toString function to print it out with
 */
Garmin.DevicePlugin.LATEST_VERSION = {
    versionMajor: 2,
    versionMinor: 7,
    buildMajor: 3,
    buildMinor: 0,
    
    toString: function() {
        return this.versionMajor + "." + this.versionMinor + "." + this.buildMajor + "." + this.buildMinor;
    },
    
    toArray: function() {
        return [this.versionMajor, this.versionMinor, this.buildMajor, this.buildMinor];
    }	
}; 
 
 
/** Latest required version of the Garmin Communicator Plugin, and a complementary toString function to print it out with. 
 */
Garmin.DevicePlugin.REQUIRED_VERSION = {
    versionMajor: 2,
    versionMinor: 1,
    buildMajor: 0,
    buildMinor: 1,
    
    toString: function() {
        return this.versionMajor + "." + this.versionMinor + "." + this.buildMajor + "." + this.buildMinor;
    },
    
    toArray: function() {
        return [this.versionMajor, this.versionMinor, this.buildMajor, this.buildMinor];
    }	
};
if (Garmin == undefined) var Garmin = {};
/** Copyright 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.DeviceControl A high-level JavaScript API which supports listener and callback functionality.
 * 
 * @author Diana Chow diana.chow[at]garmin.com, Carlo Latasa carlo.latasa@garmin.com
 * @version 1.0
 */
/** A controller object that can retrieve and send data to a Garmin 
 * device.<br><br>
 * @class Garmin.DeviceControl
 * 
 * The controller must be unlocked before anything can be done with it.  
 * Then you'll have to find a device before you can start to read data from
 * and write data to the device.<br><br>
 * 
 * We use the <a href="http://en.wikipedia.org/wiki/Observer_pattern">observer pattern</a> 
 * to handle the asynchronous nature of device communication.  You must register
 * your class as a listener to this Object and then implement methods that will 
 * get called on certain events.<br><br>
 * 
 * Events:<br><br>
 *     onStartFindDevices called when starting to search for devices.
 *       the object returned is {controller: this}<br><br>
 *
 *     onCancelFindDevices is called when the controller is told to cancel finding
 *         devices {controller: this}<br><br>
 *
 *     onFinishFindDevices called when the devices are found.
 *       the object returned is {controller: this}<br><br>
 *
 *     onException is called when an exception occurs in a method
 *         object passed back is {msg: exception}<br><br>
 *
 *	   onInteractionWithNoDevice is called when the device is lazy loaded, but finds no devices,
 * 			yet still attempts a read/write action {controller: this}<br><br>
 * 
 *     onStartReadFromDevice is called when the controller is about to start
 *         reading from the device {controller: this}<br><br>
 * 
 *     onFinishReadFromDevice is called when the controller is done reading 
 *         the device.  the read is either a success or failure, which is 
 *         communicated via json.  object passed back contains 
 *         {success:this.garminPlugin.GpsTransferSucceeded, controller: this} <br><br>
 *
 *     onWaitingReadFromDevice is called when the controller is waiting for input
 *         from the user about the device.  object passed back contains: 
 *         {message: this.garminPlugin.MessageBoxXml, controller: this}<br><br>
 *
 *     onProgressReadFromDevice is called when the controller is still reading information
 *         from the device.  in this case the message is a percent complete/ 
 *         {progress: this.getDeviceStatus(), controller: this}<br><br>
 *
 *     onCancelReadFromDevice is called when the controller is told to cancel reading
 *         from the device {controller: this}<br><br>
 *
 *     onFinishWriteToDevice is called when the controller is done writing to 
 *         the device.  the write is either a success or failure, which is 
 *         communicated via json.  object passed back contains 
 *         {success:this.garminPlugin.GpsTransferSucceeded, controller: this}<br><br>
 *
 *     onWaitingWriteToDevice is called when the controller is waiting for input
 *         from the user about the device.  object passed back contains: 
 *         {message: this.garminPlugin.MessageBoxXml, controller: this}<br><br>
 *
 *     onProgressWriteToDevice is called when the controller is still writing information
 *         to the device.  in this case the message is a percent complete/ 
 *         {progress: this.getDeviceStatus(), controller: this}<br><br>
 *
 *     onCancelWriteToDevice is called when the controller is told to cancel writing
 *         to the device {controller: this}<br><br>
 *
 * @constructor 
 *
 * requires Prototype
 * @requires BrowserDetect
 * @requires Garmin.DevicePlugin
 * @requires Garmin.Broadcaster
 * @requires Garmin.XmlConverter
 */
Garmin.DeviceControl = function(){}; //just here for jsdoc
Garmin.DeviceControl = Class.create();
Garmin.DeviceControl.prototype = {


	/////////////////////// Initialization Code ///////////////////////	

    /** Instantiates a Garmin.DeviceControl object, but does not unlock/activate plugin.
     */
	initialize: function() {
		
		this.pluginUnlocked = false;
		
		try {
			if (typeof(Garmin.DevicePlugin) == 'undefined') throw '';
		} catch(e) {
			throw new Error(Garmin.DeviceControl.MESSAGES.deviceControlMissing);
		};

    	// check that the browser is supported
     	if(!BrowserSupport.isBrowserSupported()) {
    	    var notSupported = new Error(Garmin.DeviceControl.MESSAGES.browserNotSupported);
    	    notSupported.name = "BrowserNotSupportedException";
    	    //console.debug("Control.validatePlugin throw BrowserNotSupportedException")
    	    throw notSupported;
        }
		
		// make sure the browser has the plugin installed
		if (!PluginDetect.detectGarminCommunicatorPlugin()) {
     	    var notInstalled = new Error(Garmin.DeviceControl.MESSAGES.pluginNotInstalled);
    	    notInstalled.name = "PluginNotInstalledException";
    	    throw notInstalled;			
		}
				
		// grab the plugin object on the page
		var pluginElement;
		if( window.ActiveXObject ) { // IE
			pluginElement = $("GarminActiveXControl");
		} else { // FireFox
			pluginElement = $("GarminNetscapePlugin");
		}
		
		// make sure the plugin object exists on the page
		if (pluginElement == null) {
			var error = new Error(Garmin.DeviceControl.MESSAGES.missingPluginTag);
			error.name = "HtmlTagNotFoundException";
			throw error;			
		}
		
		// instantiate a garmin plugin
		this.garminPlugin = new Garmin.DevicePlugin(pluginElement);
		 
		// validate the garmin plugin
		this.validatePlugin();
		
		// instantiate a broacaster
		this._broadcaster = new Garmin.Broadcaster();

		this.getDetailedDeviceData = true;
		this.devices = new Array();
		this.deviceNumber = null;
		this.numDevices = 0;

		this.gpsData = null;
		this.gpsDataType = null; //used by both read and write methods to track data context
		this.gpsDataString = "";
		this.gpsDataStringCompressed = "";  // Compresed version of gpsDataString.  gzip compressed and base 64 expanded.
		
		//this.wasMessageHack = false; //needed because garminPlugin.finishDownloadData returns true after out-of-memory error message is returned
	},

	/** Checks plugin validity: browser support, installation and required version.
	 * @private
     * @throws BrowserNotSupportedException
     * @throws PluginNotInstalledException
     * @throws OutOfDatePluginException
     */
    validatePlugin: function() {
		if (!this.isPluginInstalled()) {
     	    var notInstalled = new Error(Garmin.DeviceControl.MESSAGES.pluginNotInstalled);
    	    notInstalled.name = "PluginNotInstalledException";
    	    throw notInstalled;
        }
		if(this.garminPlugin.isPluginOutOfDate()) {
    	    var outOfDate = new Error(Garmin.DeviceControl.MESSAGES.outOfDatePlugin1+Garmin.DevicePlugin.REQUIRED_VERSION.toString()+Garmin.DeviceControl.MESSAGES.outOfDatePlugin2+this.getPluginVersionString());
    	    outOfDate.name = "OutOfDatePluginException";
    	    outOfDate.version = this.getPluginVersionString();
    	    throw outOfDate;
        }
    },
    
    /** Checks plugin for updates.  Throws an exception if the user's plugin version is
     * older than the one set by the API.
     * 
     * Plugin updates are not required so use this function with caution.
     * @see #setPluginLatestVersion
     */
    checkForUpdates: function() {
    	if(this.garminPlugin.isUpdateAvailable()) {
    		var notLatest = new Error(Garmin.DeviceControl.MESSAGES.updatePlugin1+Garmin.DevicePlugin.LATEST_VERSION.toString()+Garmin.DeviceControl.MESSAGES.updatePlugin2+this.getPluginVersionString());
    	    notLatest.name = "UpdatePluginException";
    	    notLatest.version = this.getPluginVersionString();
    	    throw notLatest;
    	}
    },
    
	/////////////////////// Device Handling Methods ///////////////////////	

	/** Finds any connected Garmin Devices.  
     * When it's done finding the devices, onFinishFindDevices is dispatched <br/>
     * <br/>
     * this.numDevices = the number of devices found<br/>
     * this.deviceNumber is the device that we'll use to communicate with<br/>
     * <br/>
     * Use this.getDevices() to get an array of the found devices and 
     * this.setDeviceNumber({Number}) to change the device. <br/>
     * <br/>
     * Minimum Plugin version 2.0.0.4
     * 
     * @see #getDevices
     * @see #setDeviceNumber
     */	
	findDevices: function() {
		if (!this.isUnlocked())
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
        this.garminPlugin.startFindDevices();
	    this._broadcaster.dispatch("onStartFindDevices", {controller: this});
        setTimeout(function() { this._finishFindDevices() }.bind(this), 1000);
	},

	/** Cancels the current find devices interaction. <br/>
	 * <br/>
	 * Minimum Plugin version 2.0.0.4
     */	
	cancelFindDevices: function() {
		this.garminPlugin.cancelFindDevices();
    	this._broadcaster.dispatch("onCancelFindDevices", {controller: this});
	},

	/** Loads device data into devices array.
	 * 
	 * Minimum Plugin version 2.0.0.4
	 * 
	 * @private
     */	
	_finishFindDevices: function() {
    	if(this.garminPlugin.finishFindDevices()) {
            //console.debug("_finishFindDevices devXml="+this.garminPlugin.getDevicesXml())
            this.devices = Garmin.PluginUtils.parseDeviceXml(this.garminPlugin, this.getDetailedDeviceData);
            //console.debug("_finishFindDevices devXml="+this.garminPlugin.getDevicesXml())
            
            this.numDevices = this.devices.length;
       		this.deviceNumber = 0;
	        this._broadcaster.dispatch("onFinishFindDevices", {controller: this});
    	} else {
    		setTimeout(function() { this._finishFindDevices() }.bind(this), 500);
    	}
	},

	/** Sets the deviceNumber variable which determines which connected device to talk to.
     * @param {Number} deviceNumber The device number
     */	
	setDeviceNumber: function(deviceNumber) {
		this.deviceNumber = deviceNumber;
	},
	
	/**
	 * Get the device number of the connected device to communicate with (multiple devices may
	 * be connected simultaneously, but the plugin only transfers data with one at a time).
	 * @return the device number (assigned by the plugin) determining which connected device
	 * to talk to.
	 */
	getDeviceNumber: function() {
        return this.deviceNumber;
	},

	/** Get a list of the devices found
     * @type Array<Garmin.Device>
     */	
	getDevices: function() {
		return this.devices;
	},
	
	/** Returns the DeviceXML of the current device, as a string.
	 */
	getCurrentDeviceXml: function() {
		return this.garminPlugin.getDeviceDescriptionXml(this.deviceNumber);		
	},

	/////////////////////// Read Methods ///////////////////////
	
	/** Generic read method, supporting GPX and TCX Fitness types: Courses, Workouts, User Profiles, Activity Goals, 
	 * TCX activity directory, and TCX course directory reads. <br/>
	 * <br/>
	 * Fitness detail reading (one specific activity) is not supported by this read method, refer to 
	 * readDetailFromDevice for that. <br/>  
	 * 
	 * @param {String} fileType the filetype to read from device.  Possible values for fileType are located in Garmin.DeviceControl.FILE_TYPES--detail 
	 * types are not supported.
	 * @see #readDetailFromDevice, Garmin.DeviceControl#FILE_TYPES
	 * @throws InvalidTypeException, UnsupportedTransferTypeException
     */	
	readDataFromDevice: function(fileType) {
		if (!this.isUnlocked())
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
		if (this.numDevices == 0)
			throw new Error(Garmin.DeviceControl.MESSAGES.noDevicesConnected);
		// Make sure filetype passed in is a valid type
		if ( ! this._isAMember(fileType, [Garmin.DeviceControl.FILE_TYPES.gpx,
		                                  Garmin.DeviceControl.FILE_TYPES.gpxDetail, 
										  Garmin.DeviceControl.FILE_TYPES.gpxDir,
		                                  Garmin.DeviceControl.FILE_TYPES.tcx, 
		                                  Garmin.DeviceControl.FILE_TYPES.crs, 
										  Garmin.DeviceControl.FILE_TYPES.tcxDir, 
										  Garmin.DeviceControl.FILE_TYPES.crsDir, 
										  Garmin.DeviceControl.FILE_TYPES.wkt, 
										  Garmin.DeviceControl.FILE_TYPES.tcxProfile,
										  Garmin.DeviceControl.FILE_TYPES.goals,
										  Garmin.DeviceControl.FILE_TYPES.fit,
										  Garmin.DeviceControl.FILE_TYPES.fitDir
										  ])) {
			var error = new Error(Garmin.DeviceControl.MESSAGES.invalidFileType + fileType);
			error.name = "InvalidTypeException";
			throw error;
		} 
		// Make sure the device supports this type of data transfer for this type
		if( !this.checkDeviceReadSupport(fileType) ) {
		    var error = new Error(Garmin.DeviceControl.MESSAGES.unsupportedReadDataType + fileType);
    	    error.name = "UnsupportedDataTypeException";
			throw error;
		}
		this.gpsDataType = fileType;
		this.gpsData = null;		
		this.gpsDataString = null;
		this.idle = false;
		
		try {
        	this._broadcaster.dispatch("onStartReadFromDevice", {controller: this});
        	
        	switch(this.gpsDataType) {        		
				case Garmin.DeviceControl.FILE_TYPES.gpxDir:
        		case Garmin.DeviceControl.FILE_TYPES.gpx:
        			this.garminPlugin.startReadFromGps( this.deviceNumber );
        			break;
        		case Garmin.DeviceControl.FILE_TYPES.tcx:
        		case Garmin.DeviceControl.FILE_TYPES.crs:
        		case Garmin.DeviceControl.FILE_TYPES.wkt:
        		case Garmin.DeviceControl.FILE_TYPES.goals:
        		case Garmin.DeviceControl.FILE_TYPES.tcxProfile:        		
        			this.garminPlugin.startReadFitnessData( this.deviceNumber, this.gpsDataType );
        			break;
        		case Garmin.DeviceControl.FILE_TYPES.tcxDir:
        			this.garminPlugin.startReadFitnessDirectory(this.deviceNumber, Garmin.DeviceControl.FILE_TYPES.tcx);
        			break;
        		case Garmin.DeviceControl.FILE_TYPES.crsDir:
        			this.garminPlugin.startReadFitnessDirectory(this.deviceNumber, Garmin.DeviceControl.FILE_TYPES.crs);
        			break;
        		case Garmin.DeviceControl.FILE_TYPES.fitDir:
                    this.garminPlugin.startReadFitDirectory(this.deviceNumber);
        			break;
        		case Garmin.DeviceControl.FILE_TYPES.deviceXml:
        			this.gpsDataString = this.getCurrentDeviceXml();
        			break;
        	} 
		    this._progressRead();
		} catch(e) {
		    this._reportException(e);
		}
	},
	
	/** Generic detail read method, which reads a specific fitness activity from the device given an activity ID.  
	 * Supported detail types are history activities and course activities.  The resulting data read is available 
	 * in via gpsData as an XML DOM and gpsDataString as an XML string once the read successfully finishes. 
	 * Typically used after calling readDataFromDevice to read a fitness directory.<br/> 
	 * <br/>
	 * Minimum plugin version 2.2.0.2
	 * 
	 * @param {String} fileType Filetype to be read from the device.  Supported values are 
	 * Garmin.DeviceControl.FILE_TYPES.tcxDetail and Garmin.DeviceControl.FILE_TYPES.crsDetail
	 * @param {String} dataId The ID of the data to be read from the device.  The format of these values depends 
	 * on the type of data being read (i.e. course data or history data). The CourseName element in the course schema 
	 * is used to identify courses, and the Id element is used to identify history activities.
	 * @see #readDataFromDevice, #readHistoryDetailFromFitnessDevice, #readCourseDetailFromFitnessDevice
	 * @throws InvalidTypeException, UnsupportedTransferTypeException
	 */
	readDetailFromDevice: function(fileType, dataId) {
		if (!this.isUnlocked())
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
		if (this.numDevices == 0)
			throw new Error(Garmin.DeviceControl.MESSAGES.noDevicesConnected);
		if ( ! this._isAMember(fileType, [Garmin.DeviceControl.FILE_TYPES.tcxDetail, Garmin.DeviceControl.FILE_TYPES.crsDetail])) {
			var error = new Error(Garmin.DeviceControl.MESSAGES.invalidFileType + fileType);
			error.name = "InvalidTypeException";
			throw error;
		}
		if( !this.checkDeviceReadSupport(fileType) ) {
			throw new Error(Garmin.DeviceControl.MESSAGES.unsupportedReadDataType + fileType);
		}
		
		this.gpsDataType = fileType;
		this.gpsData = null;		
		this.gpsDataString = null;
		this.idle = false;
		
		try {
        	this._broadcaster.dispatch("onStartReadFromDevice", {controller: this});
        	
        	switch(this.gpsDataType) {
        		case Garmin.DeviceControl.FILE_TYPES.tcxDetail:
        			this.garminPlugin.startReadFitnessDetail(this.deviceNumber, Garmin.DeviceControl.FILE_TYPES.tcx, dataId);
        			break;
        		case Garmin.DeviceControl.FILE_TYPES.crsDetail:
        			this.garminPlugin.startReadFitnessDetail(this.deviceNumber, Garmin.DeviceControl.FILE_TYPES.crs, dataId);
        			break;
        	} 
		    this._progressRead();
		} catch(e) {
		    this._reportException(e);
		}
	},
	
	/** Asynchronously reads GPX data from the connected device.  Only handles reading
     * from the device in this.deviceNumber. <br/>
     * <br/>
     * When the data has been gathered, the onFinishedReadFromDevice is fired, and the
     * data is stored in this.gpsDataString and this.gpsData
     * 
     * @see #readDataFromDevice
     */
	readFromDevice: function() {
		this.readDataFromDevice(Garmin.DeviceControl.FILE_TYPES.gpx);
	},
	
	/** Asynchronously reads a single fitness history record from the connected device as TCX format.
	 * Only handles reading from the device in this.deviceNumber.<br/>
	 * <br/>
     * When the data has been gathered, the onFinishedReadFromDevice is fired, and the
     * data is stored in this.gpsDataString
     * 
     * Minimum plugin version 2.2.0.2
     * 
     * @param {String} historyId The ID of the history record on the device.
     * 
     * @see #readDetailFromDevice
     */	
	readHistoryDetailFromFitnessDevice: function(historyId) {
		this.readDetailFromDevice(Garmin.DeviceControl.FILE_TYPES.tcx, historyId)
	},
	
	/** Asynchronously reads a single fitness course from the connected device as TCX format.
	 * Only handles reading from the device in this.deviceNumber. <br/>
     * <br/>
     * When the data has been gathered, the onFinishedReadFromDevice is fired, and the
     * data is stored in this.gpsDataString<br/>
     * <br/>
     * Minimum plugin version 2.2.0.2
     * 
     * @param {String} courseId The name of the course on the device.
     * 
     * @see #readDetailFromDevice
     */			
	readCourseDetailFromFitnessDevice: function(courseId){
		this.readDetailFromDevice(Garmin.DeviceControl.FILE_TYPES.crs, courseId)
	},
	
	/** Asynchronously reads entire fitness history data (TCX) from the connected device.  
	 * Only handles reading from the device in this.deviceNumber. <br/>
     * <br/>
     * When the data has been gathered, the onFinishedReadFromDevice is fired, and the
     * data is stored in this.gpsDataString<br/>
     * <br/>
     * Minimum plugin version 2.1.0.3
     * 
     * @see #readDataFromDevice
     */	
	readHistoryFromFitnessDevice: function() {	
		this.readDataFromDevice(Garmin.DeviceControl.FILE_TYPES.tcx);
	},
	
	/** Asynchronously reads entire fitness course data (CRS) from the connected device.  
	 * Only handles reading from the device in this.deviceNumber<br/>
     * <br/>
     * When the data has been gathered, the onFinishedReadFromDevice is fired, and the
     * data is stored in this.gpsDataString<br/>
     * <br/>
     * Minimum plugin version 2.2.0.1
     * 
     * @see #readDataFromDevice
     */	
	readCoursesFromFitnessDevice: function() {
		this.readDataFromDevice(Garmin.DeviceControl.FILE_TYPES.crs);
	},
	
	/** Asynchronously reads fitness workout data (WKT) from the connected device.  
	 * Only handles reading from the device in this.deviceNumber<br/>
     * <br/>
     * When the data has been gathered, the onFinishedReadFromDevice is fired, and the
     * data is stored in this.gpsDataString<br/>
     * <br/>
     * Minimum plugin version 2.2.0.1
     * 
     * @see #readDataFromDevice
     */	
	readWorkoutsFromFitnessDevice: function() {
		this.readDataFromDevice(Garmin.DeviceControl.FILE_TYPES.wkt);
	},
	
	/** Asynchronously reads fitness profile data (TCX) from the connected device.
	 * Only handles reading from the device in this.deviceNumber<br/>
     * <br/>
     * When the data has been gathered, the onFinishedReadFromDevice is fired, and the
     * data is stored in this.gpsDataString<br/>
     * <br/>
     * Minimum plugin version 2.2.0.1
     * 
     * @see #readDataFromDevice
     */	
	readUserProfileFromFitnessDevice: function() {
		this.readDataFromDevice(Garmin.DeviceControl.FILE_TYPES.tcxProfile);
	},

	/** Asynchronously reads fitness goals data (TCX) from the connected device.
	 * Only handles reading from the device in this.deviceNumber<br/>
     * <br/>
     * When the data has been gathered, the onFinishedReadFromDevice is fired, and the
     * data is stored in this.gpsDataString<br/>
     * <br/>
     * Minimum plugin version 2.2.0.1
     * 
     * @see #readDataFromDevice
     */	
	readGoalsFromFitnessDevice: function() {
		this.readDataFromDevice(Garmin.DeviceControl.FILE_TYPES.goals);
	},
	
	
	
	/** Returns the GPS data that was last read as an XML DOM. <br/> 
	 * Pre-requisite - Read function was called successfully.  <br/> 
	 * <br/>
	 * Minimum plugin version 2.1.0.3
	 * 
	 * @return XML DOM of read GPS data
	 * @see #readDataFromDevice
	 * @see #readHistoryFromFitnessDevice
	 * @see #readHistoryDetailFromFitnessDevice
	 * @see #readCourseDetailFromFitnessDevice
	 */
	getGpsData: function() {
		
		if (!this.isUnlocked())
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
		if (this.numDevices == 0)
			throw new Error(Garmin.DeviceControl.MESSAGES.noDevicesConnected);
		if( this.getReadCompletionState != Garmin.DeviceControl.FINISH_STATES.finished ) {
			throw new Error(Garmin.DeviceControl.MESSAGES.incompleteRead);
		}
		
		return this.gpsData;
	},
	
	/** Returns the GPS data that was last read as an XML string. <br/>  
	 * Pre-requisite - Read function was called successfully. <br/>
	 * <br/>
	 * Minimum plugin version 2.1.0.3
	 * 
	 * @return XML string of read GPS data
	 * @see #readDataFromDevice
	 * @see #readHistoryFromFitnessDevice
	 * @see #readHistoryDetailFromFitnessDevice
	 * @see #readCourseDetailFromFitnessDevice
	 */
	getGpsDataString: function() {
		if (!this.isUnlocked())
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
		if (this.numDevices == 0)
			throw new Error(Garmin.DeviceControl.MESSAGES.noDevicesConnected);
		if( this.getReadCompletionState != Garmin.DeviceControl.FINISH_STATES.finished ) {
			throw new Error(Garmin.DeviceControl.MESSAGES.incompleteRead);
		}
		
		return this.gpsDataString;
	},
	
	/** Returns the last read fitness data in compressed format.  A fitness read method must be called and the read must
	 * finish successfully before this function returns good data. <br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.2
	 * 
	 * @return Compressed fitness XML data from the last successful read.  The data is gzp compressed and base64 expanded.
	 * @see #readDataFromDevice
	 * @see #readHistoryFromFitnessDevice
	 * @see #readHistoryDetailFromFitnessDevice
	 * @see #readCourseDetailFromFitnessDevice
	 */
	getCompressedFitnessData: function() {
		
		if (!this.isUnlocked())
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
		if (this.numDevices == 0)
			throw new Error(Garmin.DeviceControl.MESSAGES.noDevicesConnected);
		if( this.getReadCompletionState != Garmin.DeviceControl.FINISH_STATES.finished ) {
			throw new Error(Garmin.DeviceControl.MESSAGES.incompleteRead);
		}

		try{
			this.garminPlugin.getTcdXmlz();
		}
		catch( aException ) {
 			this._reportException( aException );
		}
	},

	/** Returns the completion state of the current read.  This function can be used with
	 * GPX and TCX (fitness) reads.
	 * 
	 * @type Number
	 * @return {Number} The completion state of the current read.  The completion state can be one of the following: <br/>
	 *  <br/>
	 *	0 = idle <br/>
 	 * 	1 = working <br/>
 	 * 	2 = waiting <br/>
 	 * 	3 = finished <br/>
	 */	
	getReadCompletionState: function() {
		switch(this.gpsDataType) {
			case Garmin.DeviceControl.FILE_TYPES.gpxDir:
			case Garmin.DeviceControl.FILE_TYPES.gpxDetail:
			case Garmin.DeviceControl.FILE_TYPES.gpx:
				return this.garminPlugin.finishReadFromGps();
			case Garmin.DeviceControl.FILE_TYPES.tcx:
			case Garmin.DeviceControl.FILE_TYPES.crs:
			case Garmin.DeviceControl.FILE_TYPES.wkt:
			case Garmin.DeviceControl.FILE_TYPES.tcxProfile:
			case Garmin.DeviceControl.FILE_TYPES.goals:
				return this.garminPlugin.finishReadFitnessData();
			case Garmin.DeviceControl.FILE_TYPES.tcxDir:
			case Garmin.DeviceControl.FILE_TYPES.crsDir:
				return this.garminPlugin.finishReadFitnessDirectory();
			case Garmin.DeviceControl.FILE_TYPES.tcxDetail:
			case Garmin.DeviceControl.FILE_TYPES.crsDetail:
				return this.garminPlugin.finishReadFitnessDetail();
			case Garmin.DeviceControl.FILE_TYPES.fitDir:
                return this.garminPlugin.finishReadFitDirectory();
		}
	},
	
	/** Internal read dispatching and polling delay.
	 * @private
     */	
	_progressRead: function() {
		this._broadcaster.dispatch("onProgressReadFromDevice", {progress: this.getDeviceStatus(), controller: this});
        setTimeout(function() { this._finishReadFromDevice() }.bind(this), 200); //200		 
	},
	
	/** Internal read state logic. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4 for GPX and TCX history read.<br/>
	 * Minimum plugin version 2.2.0.2 for directory and detail read.<br/>
	 * Minimum plugin version 2.2.0.2 for compressed file get.
	 * 
	 * @private
     */	
	_finishReadFromDevice: function() {
		var completionState = this.getReadCompletionState();
		
		//console.debug("control._finishReadFromDevice this.gpsDataType="+this.gpsDataType+" completionState="+completionState)
        try {
        	
			if( completionState == Garmin.DeviceControl.FINISH_STATES.finished ) {
	        	switch( this.gpsDataType ) {
					case Garmin.DeviceControl.FILE_TYPES.gpxDir:
	        		case Garmin.DeviceControl.FILE_TYPES.gpxDetail:
	        		case Garmin.DeviceControl.FILE_TYPES.gpx:
	        			if (this.garminPlugin.gpsTransferSucceeded()) {
		        			this.gpsDataString = this.garminPlugin.getGpsXml();
							this.gpsData = Garmin.XmlConverter.toDocument(this.gpsDataString);
							this._broadcaster.dispatch("onFinishReadFromDevice", {success: this.garminPlugin.gpsTransferSucceeded(), controller: this});	
	        			}
						break;
					case Garmin.DeviceControl.FILE_TYPES.tcx:
					case Garmin.DeviceControl.FILE_TYPES.crs:
					case Garmin.DeviceControl.FILE_TYPES.tcxDir:
					case Garmin.DeviceControl.FILE_TYPES.crsDir:
					case Garmin.DeviceControl.FILE_TYPES.tcxDetail:
					case Garmin.DeviceControl.FILE_TYPES.crsDetail:
					case Garmin.DeviceControl.FILE_TYPES.wkt:
					case Garmin.DeviceControl.FILE_TYPES.tcxProfile:
					case Garmin.DeviceControl.FILE_TYPES.goals:
						if (this.garminPlugin.fitnessTransferSucceeded()) {
							this.gpsDataString = this.garminPlugin.getTcdXml();
							this.gpsDataStringCompressed = this.garminPlugin.getTcdXmlz();
							
							this.gpsData = Garmin.XmlConverter.toDocument(this.gpsDataString);
							this._broadcaster.dispatch("onFinishReadFromDevice", {success: this.garminPlugin.fitnessTransferSucceeded(), controller: this});										
						}
						break;
					case Garmin.DeviceControl.FILE_TYPES.fitDir:
                        // TODO is there a fitness transfer succeeded with fit?
//					    if(this.garminPlugin.fitnessTransferSucceeded()) {
					        this.gpsDataString = this.garminPlugin.getDirectoryXml();
//							this.gpsDataStringCompressed = this.garminPlugin.getTcdXmlz();
							this.gpsData = Garmin.XmlConverter.toDocument(this.gpsDataString);
							this._broadcaster.dispatch("onFinishReadFromDevice", {success: this.garminPlugin.fitnessTransferSucceeded(), controller: this});
//					    }
                        break;
	        	}
			} else if( completionState == Garmin.DeviceControl.FINISH_STATES.messageWaiting ) {
				var msg = this._messageWaiting();
				this._broadcaster.dispatch("onWaitingReadFromDevice", {message: msg, controller: this});
			} else {
	    	    this._progressRead();
			}
		} catch( aException ) {
 			this._reportException( aException );
		}
    },

	/** User canceled the read. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
     */	
	cancelReadFromDevice: function() {
		if (this.gpsDataType == Garmin.DeviceControl.FILE_TYPES.gpx) {
			this.garminPlugin.cancelReadFromGps();
		} else {
			this.garminPlugin.cancelReadFitnessData();
		}
    	this._broadcaster.dispatch("onCancelReadFromDevice", {controller: this});
	},
	
	
	/** Return the specified file as a UU-Encoded string
     * <br/>
     * Minimum version 2.6.3.1
     * 
     * If the file is known to be compressed, compressed should be
     * set to false. Otherwise, set compressed to true to retrieve a
     * gzipped and uuencoded file.
     * 
     * @param relativeFilePath {String} path relative to the Garmin folder on the device
     */
     getBinaryFile: function(deviceNumber, relativeFilePath) {
        if (!this.isUnlocked())
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
		if(this.numDevices == 0)
			throw new Error(Garmin.DeviceControl.MESSAGES.noDevicesConnected);
        // Attempt to detect Fit file
        if(relativeFilePath.capitalize().endsWith(".fit")) {
            // Capitalize makes all but first letters lowercase. I can't believe prototype doesn't have a lowercase method. :(
            this.gpsDataType = Garmin.DeviceControl.FILE_TYPES.fit;
        } else {
    		this.gpsDataType = Garmin.DeviceControl.FILE_TYPES.binary;
        }
		var success;
		try {
		    this.gpsDataString = this.garminPlugin.getBinaryFile(deviceNumber, relativeFilePath, false);
		    this.gpsDataStringCompressed = this.garminPlugin.getBinaryFile(deviceNumber, relativeFilePath, true);
//		    this.gpsData = this.garminPlugin.getBinaryFile(deviceNumber, relativeFilePath, compressed);
		    success = true;
	    } catch(e) {
	        success = false;
			this._reportException(e);
	    }
	    
	    this._broadcaster.dispatch("onFinishReadFromDevice", {success: success, controller: this});
        return this.gpsData;
     },

	/////////////////////// Web Drop Methods (Write) ///////////////////////
	
    /** Writes an address to the currently selected device.
     * 
     * @param {String} address The address to be written to the device. This doesn't check validity
     */	
	writeAddressToDevice: function(address) {
		if (!this.isUnlocked())
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
		if (!this.geocoder) {
			this.geocoder = new Garmin.Geocode();
			this.geocoder.register(this);
		}
		this.geocoder.findLatLng(address);
	},

	/** Handles call-back from geocoder and forwards call to onException on registered listeners.
	 * @private
     * @param {Error} json error wrapped in JSON 'msg' object.
     */
	onException: function(json) {
		this._reportException(json.msg);
	},
	
	/** Handles call-back from geocoder and forwards call to writeToDevice.
	 * Registered listeners will recieve an onFinishedFindLatLon call before writeToDevice is invoked.
	 * Listeners can change the 'fileName' if they choose avoiding overwritting old waypoints on
	 * some devices.
	 * @private
     * @param {Object} json waypoint, fileName and controller in JSON wrapper.
     */
	onFinishedFindLatLon: function(json) {
		json.fileName = "address.gpx";
		json.controller = this;
		this._broadcaster.dispatch("onFinishedFindLatLon", json);
   		var factory = new Garmin.GpsDataFactory();
		var gpxStr = factory.produceGpxString(null, [json.waypoint]);
		this.writeToDevice(gpxStr, json.fileName);
	},

	/////////////////////// More Write Methods ///////////////////////	
    /**
     * Generic write method for GPX and TCX file formats.  For binary write, use {@link downloadToDevice}.
     * 
     * @param {String} dataType - the datatype to write to device.  Possible values are located in {@link #Garmin.DeviceControl.FILE_TYPES}
     * @param {String} dataString - the datastring to write to device.  Should be in the format of the dataType value.
     * @param {String} fileName - the filename to write the data to on the device. File extension is not necessary, 
     * but is suggested for device compatibility.  This parameter is ignored when the dataType value is FitnessActivityGoals (see {@link #writeGoalsToFitnessDevice}).
     * @see #writeToDevice, #writeFitnessToDevice
     * @throws InvalidTypeException, UnsupportedTransferTypeException
     */ 
    writeDataToDevice: function(dataType, dataString, fileName) {
        if (!this.isUnlocked()) {
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
        }
        
		if (this.numDevices == 0) {
			throw new Error(Garmin.DeviceControl.MESSAGES.noDevicesConnected);
        }

		this.gpsDataType = dataType;
        
		if (!this.checkDeviceWriteSupport(this.gpsDataType)) {
			throw new Error(Garmin.DeviceControl.MESSAGES.unsupportedWriteDataType + this.gpsDataType);
		}
		
		try {
        	this._broadcaster.dispatch("onStartWriteToDevice", {controller: this});
            
        	switch(this.gpsDataType) {
            	case Garmin.DeviceControl.FILE_TYPES.gpx:
        			this.garminPlugin.startWriteToGps(dataString, fileName, this.deviceNumber);
        			break;
        		case Garmin.DeviceControl.FILE_TYPES.crs:
        		case Garmin.DeviceControl.FILE_TYPES.wkt:
        		case Garmin.DeviceControl.FILE_TYPES.goals:
        		case Garmin.DeviceControl.FILE_TYPES.tcxProfile:
        		case Garmin.DeviceControl.FILE_TYPES.nlf:                
        			this.garminPlugin.startWriteFitnessData(dataString, this.deviceNumber, fileName, this.gpsDataType);
        			break;
        		default:
					throw new Error(Garmin.DeviceControl.MESSAGES.unsupportedWriteDataType + this.gpsDataType);
        	}
		    this._progressWrite();
	    } catch(e) {
			this._reportException(e);
	   	}
    },
    
    /** Writes the given GPX XML string to the device selected in this.deviceNumber. <br/>
     * <br/>
     * Minimum plugin version 2.0.0.4
     * 
     * @param gpxString XML to be written to the device. This doesn't check validity.
     * @param fileName The filename to write data to.  Validity is not checked here.
     */	
	writeToDevice: function(gpxString, fileName) {
        this.writeDataToDevice(Garmin.DeviceControl.FILE_TYPES.gpx, gpxString, fileName);	    
	},

	/** DEPRECATED - See {@link #writeCoursesToFitnessDevice}<br/> 
	 * <br/>
	 * Writes fitness course data (TCX) to the device selected in this.deviceNumber. <br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.1
	 * 
     * @param tcxString {String} TCX Course XML string to be written to the device. This doesn't check validity.
     * @param fileName {String} filename to write data to on the device.  Validity is not checked here.
     */	
	writeFitnessToDevice: function(tcxString, fileName) {
		this.writeDataToDevice(Garmin.DeviceControl.FILE_TYPES.crs, tcxString, fileName);
	},

	/** Writes fitness course data (TCX) to the device selected in this.deviceNumber. <br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.1
	 * 
     * @param tcxString {String} TCX Course XML string to be written to the device. This doesn't check validity.
     * @param fileName {String} filename to write data to on the device.  Validity is not checked here.
     */	
	writeCoursesToFitnessDevice: function(tcxString, fileName) {
		this.writeDataToDevice(Garmin.DeviceControl.FILE_TYPES.crs, tcxString, fileName);
	},

	/** Writes fitness goals data (TCX) string to the device selected in this.deviceNumber. All fitness goals
	 * are written to the filename 'ActivityGoals.TCX' in the device's goals directory, in order for the device
	 * to recognize the file.<br/> 
	 * <br/>
	 * Minimum plugin version 2.2.0.1
	 * 
     * @param tcxString {String} ActivityGoals TCX string to be written to the device. This doesn't check validity.
     */	
	writeGoalsToFitnessDevice: function(tcxString) {
		this.writeDataToDevice(Garmin.DeviceControl.FILE_TYPES.goals, tcxString, '');
	},
	
	/** Writes fitness workouts data (XML) string to the device selected in this.deviceNumber. <br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.1
	 * 
     * @param tcxString XML (workouts) string to be written to the device. This doesn't check validity.
     * @param fileName String of filename to write it to on the device.  Validity is not checked here.
     */	
	writeWorkoutsToFitnessDevice: function(tcxString, fileName) {
		this.writeDataToDevice(Garmin.DeviceControl.FILE_TYPES.wkt, tcxString, fileName);
	},
	
	/** Writes fitness user profile data (TCX) string to the device selected in this.deviceNumber. <br/>
	 * <br/>
	 * Minimum plugin version 2.2.0.1
	 * 
     * @param tcxString XML (user profile) string to be written to the device. This doesn't check validity.
     * @param fileName String of filename to write it to on the device.  Validity is not checked here.
     */	
	writeUserProfileToFitnessDevice: function(tcxString, fileName) {
		this.writeDataToDevice(Garmin.DeviceControl.FILE_TYPES.tcxProfile, tcxString, fileName);
	},
	
	/** Downloads and writes binary data asynchronously to device. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4
     *
     * @param xmlDownloadDescription {String} xml string containing information about the files to be downloaded onto the device.
     * @param fileName {String} this parameter is ignored!  We will remove this param from the API in a future compatibility release.
     */	
	downloadToDevice: function(xmlDownloadDescription) {
		if (!this.isUnlocked())
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
		if(this.numDevices == 0)
			throw new Error(Garmin.DeviceControl.MESSAGES.noDevicesConnected);
		this.gpsDataType = Garmin.DeviceControl.FILE_TYPES.binary;
		try {
		    this.garminPlugin.startDownloadData(xmlDownloadDescription, this.deviceNumber );
		    this._progressWrite();
	    } catch(e) {
			this._reportException(e);
	    }
	},
	
	/** Internal dispatch and polling delay.
	 * @private
     */	
	_progressWrite: function() {
		//console.debug("control._progressWrite gpsDataType="+this.gpsDataType)		
    	this._broadcaster.dispatch("onProgressWriteToDevice", {progress: this.getDeviceStatus(), controller: this});
        setTimeout(function() { this._finishWriteToDevice() }.bind(this), 200);
	},
	
	/** Internal write lifecycle handling.
	 * @private
     */	
	_finishWriteToDevice: function() {
        try {
			var completionState;
			var success;
			
			switch( this.gpsDataType ) {
				
				case Garmin.DeviceControl.FILE_TYPES.gpx : 
					completionState = this.garminPlugin.finishWriteToGps();
					success = this.garminPlugin.gpsTransferSucceeded();
					break;
				case Garmin.DeviceControl.FILE_TYPES.crs :
				case Garmin.DeviceControl.FILE_TYPES.goals :
				case Garmin.DeviceControl.FILE_TYPES.wkt :
				case Garmin.DeviceControl.FILE_TYPES.tcxProfile :
				case Garmin.DeviceControl.FILE_TYPES.nlf :
					completionState = this.garminPlugin.finishWriteFitnessData();
					success = this.garminPlugin.fitnessTransferSucceeded();
					break;
				case Garmin.DeviceControl.FILE_TYPES.gpi :
				case Garmin.DeviceControl.FILE_TYPES.fitCourse :
				case Garmin.DeviceControl.FILE_TYPES.fitSettings :
				case Garmin.DeviceControl.FILE_TYPES.fitSport :
				case Garmin.DeviceControl.FILE_TYPES.binary :
					completionState = this.garminPlugin.finishDownloadData();
					success = this.garminPlugin.downloadDataSucceeded();
					break;				
				case Garmin.DeviceControl.FILE_TYPES.firmware :
					completionState = this.garminPlugin.finishUnitSoftwareUpdate();
					success = this.garminPlugin.downloadDataSucceeded();
					break;				
				default:
					throw new Error(Garmin.DeviceControl.MESSAGES.unsupportedWriteDataType + this.gpsDataType);
			}
			
			if( completionState == Garmin.DeviceControl.FINISH_STATES.finished ) {
				this._broadcaster.dispatch("onFinishWriteToDevice", {success: success, controller: this});											
			} else if( completionState == Garmin.DeviceControl.FINISH_STATES.messageWaiting ) {
				var msg = this._messageWaiting();
				this._broadcaster.dispatch("onWaitingWriteToDevice", {message: msg, controller: this});
			} else {
	    	     this._progressWrite();
			}
		} catch( aException ) {
 			this._reportException( aException );
		}
	},

	/** Cancels the current write transfer to the device. <br/>
	 * <br/>
	 * Minimum plugin version 2.0.0.4<br/>
     * Minimum plugin version 2.2.0.1 for writes of GPX to SD Card
     */	
	cancelWriteToDevice: function() {
		switch( this.gpsDataType) {
			case Garmin.DeviceControl.FILE_TYPES.gpx:
				this.garminPlugin.cancelWriteToGps();
				break;
			case Garmin.DeviceControl.FILE_TYPES.gpi:
			case Garmin.DeviceControl.FILE_TYPES.binary:
				this.garminPlugin.cancelDownloadData();
				break;
			case Garmin.DeviceControl.FILE_TYPES.firmware:
				this.garminPlugin.cancelUnitSoftwareUpdate();
				break;
			case Garmin.DeviceControl.FILE_TYPES.crs:
			case Garmin.DeviceControl.FILE_TYPES.goals:
			case Garmin.DeviceControl.FILE_TYPES.wkt:
			case Garmin.DeviceControl.FILE_TYPES.tcxProfile:
			case Garmin.DeviceControl.FILE_TYPES.nlf:
				this.garminPlugin.cancelWriteFitnessData();
				break;
		}
		this._broadcaster.dispatch("onCancelWriteToDevice", {controller: this});
	},

    /**
	 * Determine the amount of space available on a mass storage mode device (the
	 * currently selected device according to this.deviceNumber). 
	 * <br/> 
	 * Minimum Plugin version 2.5.1
	 * 
	 * @param {String} relativeFilePath - if a file is being replaced, set to relative path on device, otherwise set to empty string.
	 * @return -1 for non-mass storage mode devices.
	 * @see downloadToDevice 
	 */
	bytesAvailable: function(relativeFilePath) {
	    return this.garminPlugin.bytesAvailable(this.getDeviceNumber(), relativeFilePath);
	},
	
	/** Download and install a list of unit software updates.  Start the asynchronous 
     * StartUnitSoftwareUpdate operation.
     * 
     * Check for completion with the FinishUnitSoftwareUpdate() method.  After
     * completion check the DownloadDataSucceeded property to make sure that all of the downloads 
     * were successfully placed on the device. 
     * 
     * See the Schema UnitSoftwareUpdatev3.xsd for the format of the UpdateResponsesXml description
     *
     * @see Garmin.DeviceControl.cancelWriteToDevice
     * @see Garmin.DevicePlugin.downloadDataSucceeded
     * @see Garmin.DevicePlugin._finishWriteToDevice
     * @version plugin v2.6.2.0
     */
    downloadFirmwareToDevice: function(updateResponsesXml) {
        if (!this.isUnlocked())
			throw new Error(Garmin.DeviceControl.MESSAGES.pluginNotUnlocked);
		if(this.numDevices == 0)
			throw new Error(Garmin.DeviceControl.MESSAGES.noDevicesConnected);
		this.gpsDataType = Garmin.DeviceControl.FILE_TYPES.firmware;
		try {
            this.garminPlugin.startUnitSoftwareUpdate(updateResponsesXml, this.deviceNumber);
		    this._progressWrite();
	    } catch(e) {
			this._reportException(e);
	    }
    },

	/////////////////////// Support Methods ///////////////////////	


	/** Unlocks the GpsControl object to be used at the given web address. <br/>
     * <br/>
     * Minimum Plugin version 2.0.0.4
     * 
     * @param {Array} pathKeyPairsArray baseURL and key pairs.  
     * @type Boolean 
     * @return True if the plug-in was unlocked successfully
     */
	unlock: function(pathKeyPairsArray) {
		this.pluginUnlocked = this.garminPlugin.unlock(pathKeyPairsArray);
		return this.pluginUnlocked;
	},

	/** Register to be an event listener.  An object that is registered will be dispatched
     * a method if they have a function with the same dispatch name.  So if you register a
     * listener with an onFinishFindDevices, and the onFinishFindDevices message is called, you'll
     * get that message.  See class comments for event types
     *
     * @param {Object} listener Object that will listen for events coming from this object 
     * @see {Garmin.Broadcaster}
     */	
	register: function(listener) {
        this._broadcaster.register(listener);
	},

	/** True if plugin has been successfully created and unlocked.
	 * @type Boolean
	 */
	 isUnlocked: function() {
	 	return this.pluginUnlocked;
	 },
	 
    /** Responds to a message box on the device.
     * 
     * Minimum version 2.0.0.4
     * 
     * @param {Number} response should be an int which corresponds to a button value from this.garminPlugin.MessageBoxXml
     */
    // TODO: this method only works with writes - should it work with reads?
    respondToMessageBox: function(response) {
        this.garminPlugin.respondToMessageBox(response ? 1 : 2);
        this._progressWrite();
    },

	/** Called when device generates a message.
	 * This occurs when completionState == Garmin.DeviceControl.FINISH_STATES.messageWaiting.
	 * @private
     */	
	_messageWaiting: function() {
		var messageDoc = Garmin.XmlConverter.toDocument(this.garminPlugin.getMessageBoxXml());
		//var type = messageDoc.getElementsByTagName("Icon")[0].childNodes[0].nodeValue;
		var text = messageDoc.getElementsByTagName("Text")[0].childNodes[0].nodeValue;
		
		var message = new Garmin.MessageBox("Question",text);
		
		var buttonNodes = messageDoc.getElementsByTagName("Button");
		for(var i=0; i<buttonNodes.length; i++) {
			var caption = buttonNodes[i].getAttribute("Caption");
			var value = buttonNodes[i].getAttribute("Value");
			message.addButton(caption, value);
		}
		return message;
	},

	/** Get the status/progress of the current state or transfer
     * @type Garmin.TransferProgress
     */	
	getDeviceStatus: function() {
		var aProgressXml = this.garminPlugin.getProgressXml();
		var theProgressDoc = Garmin.XmlConverter.toDocument(aProgressXml);
		
		var title = "";
		if(theProgressDoc.getElementsByTagName("Title").length > 0) {
			title = theProgressDoc.getElementsByTagName("Title")[0].childNodes[0].nodeValue;
		}
		
		var progress = new Garmin.TransferProgress(title);

		var textNodes = theProgressDoc.getElementsByTagName("Text");
		for( var i=0; i < textNodes.length; i++ ) {
			if(textNodes[i].childNodes.length > 0) {
				var text = textNodes[i].childNodes[0].nodeValue;
				if(text != "") progress.addText(text);
			}
		}
		
		var percentageNode = theProgressDoc.getElementsByTagName("ProgressBar")[0];
		if(percentageNode != undefined) {
			if(percentageNode.getAttribute("Type") == "Percentage") {
				progress.setPercentage(percentageNode.getAttribute("Value"));
			} else if (percentageNode.getAttribute("Type") == "Indefinite") {
				progress.setPercentage(100);			
			}
		}

		return progress;
	},
		
	/**
	 * @private
	 */
	_isAMember: function(element, array) {
		return array.any( function(str){ return str==element; } );
	},
	
	/** Gets the version number for the plugin the user has currently installed.
     * @type Array 
     * @return An array of the format [versionMajor, versionMinor, buildMajor, buildMinor].
     * @see #getPluginVersionString
     */	
	getPluginVersion: function() {
		
    	return this.garminPlugin.getPluginVersion();
	},

	/** Gets a string of the version number for the plugin the user has currently installed.
     * @type String 
     * @return A string of the format "versionMajor.versionMinor.buildMajor.buildMinor", i.e. "2.0.0.4"
     * @see #getPluginVersion
     */	
	getPluginVersionString: function() {
		return this.garminPlugin.getPluginVersionString();
	},
	
	/** Sets the required version number for the plugin for the application.
	 * @param reqVersionArray {Array} The required version to set to.  In the format [versionMajor, versionMinor, buildMajor, buildMinor]
	 * 			i.e. [2,2,0,1]
	 */
	setPluginRequiredVersion: function(reqVersionArray) {
		if( reqVersionArray != null ) {
			this.garminPlugin.setPluginRequiredVersion(reqVersionArray);
		}
	},
	
	/** Sets the latest plugin version number.  This represents the latest version available for download at Garmin.
	 * We will attempt to keep the default value of this up to date with each API release, but this is not guaranteed,
	 * so set this to be safe or if you don't want to upgrade to the latest API.
	 * 
	 * @param reqVersionArray {Array} The latest version to set to.  In the format [versionMajor, versionMinor, buildMajor, buildMinor]
	 * 			i.e. [2,2,0,1]
	 */
	setPluginLatestVersion: function(reqVersionArray) {
		if( reqVersionArray != null ) {
			this.garminPlugin.setPluginLatestVersion(reqVersionArray);
		}
	},
	
	/** Determines if the plugin is initialized
     * @type Boolean
     */	
	isPluginInitialized: function() {
		return (this.garminPlugin != null);
	},

	/** Determines if the plugin is installed on the user's machine
     * @type Boolean
     */	
	isPluginInstalled: function() {
		return (this.garminPlugin.getVersionXml() != undefined);
	},

	/** Internal exception handling for asynchronous calls.
	 * @private
      */	
	_reportException: function(exception) {
		this._broadcaster.dispatch("onException", {msg: exception, controller: this});
	},
	
	/** Number of devices detected by plugin.
	 * @type Number
}    */	
	getDevicesCount: function() {
	    return this.numDevices;
	},
	
	/** Checks if the device lists the given datatype as a supported readable type.
	 * Plugin version affects the results of this function.  The latest plugin version is encouraged.
	 * 
	 * Internal file type support (such as directory types) is detected based on base
	 * type. i.e. tcxDir -> tcx, fitDir -> fit
	 */
	checkDeviceReadSupport: function( datatype ) {
		
        // Do the plugin version check early for fit directory reading
		if( datatype == Garmin.DeviceControl.FILE_TYPES.fitDir) {
		    if( this.garminPlugin.getSupportsFitDirectoryRead() == false) {
    	        // Yeah, breaking the 1 return rule... This is still cleaner than all the other options
    	        // and at least eliminates confusion between other types.
		        return false;
		    }
        } 
        
		var isDatatypeSupported;

		// The selected device
		var device = this._getDeviceByNumber(this.deviceNumber);
		var baseDatatype;

		// Internal types use base type for the support check.
		switch(datatype) {
			case Garmin.DeviceControl.FILE_TYPES.gpxDir:
			case Garmin.DeviceControl.FILE_TYPES.gpxDetail:
			     baseDatatype = Garmin.DeviceControl.FILE_TYPES.gpx;
			     break;			
            case Garmin.DeviceControl.FILE_TYPES.tcxDir:
            case Garmin.DeviceControl.FILE_TYPES.tcxDetail:
    		    baseDatatype = Garmin.DeviceControl.FILE_TYPES.tcx; 
    		    break;
            case Garmin.DeviceControl.FILE_TYPES.crsDir:
            case Garmin.DeviceControl.FILE_TYPES.crsDetail:
                baseDatatype = Garmin.DeviceControl.FILE_TYPES.crs;
                break;
            case Garmin.DeviceControl.FILE_TYPES.fitDir:
            case Garmin.DeviceControl.FILE_TYPES.fitFile:
                baseDatatype = Garmin.DeviceControl.FILE_TYPES.fit;
                break;
            default:     
                baseDatatype = datatype;
		}
		
		// Every device has a device xml and firmware
		if(baseDatatype == Garmin.DeviceControl.FILE_TYPES.deviceXml 
		|| baseDatatype == Garmin.DeviceControl.FILE_TYPES.firmware) {
		    isDatatypeSupported = true;
		} else {
            isDatatypeSupported = device.supportDeviceDataTypeRead(baseDatatype);
		}

		return isDatatypeSupported;
	},
	
	/** Checks if the device lists the given datatype as a supported writeable type.
	 * Plugin version affects the results of this function.  The latest plugin version is encouraged.
	 * 
	 * Internal file types (such as directory types) are NOT detected as supported write types.
	 */
	checkDeviceWriteSupport: function(datatype) {
	    var isDatatypeSupported = false;

		// The selected device
		var device = this._getDeviceByNumber(this.deviceNumber);
		
		// Don't include types that aren't in the Device XML
		if ( datatype == Garmin.DeviceControl.FILE_TYPES.binary 
		  || datatype == Garmin.DeviceControl.FILE_TYPES.gpi) {
		    isDatatypeSupported = true;
		} else {
            isDatatypeSupported = device.supportDeviceDataTypeWrite(datatype);
		}
		
		return isDatatypeSupported;
	},
	
	/** Retrieve a device from the list of found devices by device number. 
	 * @return Garmin.Device
	 */
	_getDeviceByNumber: function(deviceNum) {
		for( var index = 0; index < this.devices.length; index++) {
			if( this.devices[index].getNumber() == deviceNum){
				return this.devices[index];
			}
		}		
	},
	
	/** String representation of instance.
	 * @type String
     */	
	toString: function() {
	    return "Garmin Javascript GPS Controller managing " + this.numDevices + " device(s)";
	}
};

/** Dedicated browser support singleton.
 */
var BrowserSupport = {
    /** Determines if the users browser is currently supported by the plugin
     * @type Boolean
     */	 
	isBrowserSupported: function() {
		//console.debug("Display.isBrowserSupported BrowserDetect.OS="+BrowserDetect.OS+", BrowserDetect.browser="+BrowserDetect.browser)
		// TODO Extract strings to constants
		// TODO Move this out to plugin layer? 
		return ( (BrowserDetect.OS == "Windows" && 
					(BrowserDetect.browser == "Firefox" 
					|| BrowserDetect.browser == "Mozilla" 
					|| BrowserDetect.browser == "Explorer"
					|| BrowserDetect.browser == "Safari"))
				|| (BrowserDetect.OS == "Mac" && 
					(BrowserDetect.browser == "Firefox" 
					|| BrowserDetect.browser == "Safari")) );
	}
};

/** Constants defining possible errors messages for various errors on the page
 */
Garmin.DeviceControl.MESSAGES = {
	deviceControlMissing: "Garmin.DeviceControl depends on the Garmin.DevicePlugin framework.",
	missingPluginTag: "Plug-In HTML tag not found.",
	browserNotSupported: "Your browser is not supported to use the Garmin Communicator Plug-In.",
	pluginNotInstalled: "Garmin Communicator Plugin NOT detected.",
	outOfDatePlugin1: "Your version of the Garmin Communicator Plug-In is out of date.<br/>Required: ",
	outOfDatePlugin2: "Current: ",
	updatePlugin1: "Your version of the Garmin Communicator Plug-In is not the latest version. Latest version: ",
	updatePlugin2: ", current: ",
	pluginNotUnlocked: "Garmin Plugin has not been unlocked",
	noDevicesConnected: "No device connected, can't communicate with device.",
	invalidFileType: "Cannot process the device file type: ",
	incompleteRead: "Incomplete read, cannot get compressed format.",
	unsupportedReadDataType: "Your device does not support reading of the type: ",
	unsupportedWriteDataType: "Your device does not support writing of the type: "
};

/** Constants defining possible states when you poll the finishActions
 */
Garmin.DeviceControl.FINISH_STATES = {
	idle: 0,
	working: 1,
	messageWaiting: 2,
	finished: 3	
};

/** Constants defining possible file types associated with read and write methods.  File types can
 * be accessed in a static way, like so:<br/>
 * <br/>
 * Garmin.DeviceControl.FILE_TYPES.gpx<br/>
 * <br/>
 * NOTE: 'gpi' is being deprecated--please use 'binary' instead for gpi and other binary data. 
 */
Garmin.DeviceControl.FILE_TYPES = {
	gpx:               "GPSData",
	tcx:               "FitnessHistory",
	gpi:               "gpi", //deprecated, use binary instead
	crs:               "FitnessCourses",
	wkt:               "FitnessWorkouts",
	goals:             "FitnessActivityGoals",
	tcxProfile:        "FitnessUserProfile",
	binary:            "BinaryData", // Not in Device XML, so writing this type is "supported" for all devices. For FIT data, use fitFile.
	voices:            "Voices",
	nlf:               "FitnessNewLeaf",
	fit:               "FITBinary",
	fitCourse:         "FIT_TYPE_6",
	fitSettings:       "FIT_TYPE_2",
	fitSport:          "FIT_TYPE_3",
	
	// The following types are internal types used by the API only and cannot be found in the Device XML.
	// NOTE: When adding or removing types to this internal list, modify checkDeviceReadSupport() accordingly.
	tcxDir: 		   "FitnessHistoryDirectory",
	crsDir: 		   "FitnessCoursesDirectory",
	gpxDir: 		   "GPSDataDirectory",
	tcxDetail:         "FitnessHistoryDetail",
	crsDetail: 		   "FitnessCoursesDetail",
	gpxDetail:         "GPSDataDetail",
	deviceXml: 	       "DeviceXml",
	fitDir:     	   "FitDirectory",
	fitFile:    	   "FitFile",
	firmware:          "Firmware"
};

/** Constants defining the strings used by the Device.xml to indicate 
 * transfer direction of each file type
 */
Garmin.DeviceControl.TRANSFER_DIRECTIONS = {
	read:              "OutputFromUnit",
	write:             "InputToUnit",
	both:              "InputOutput"
};

/** Encapsulates the data provided by the device for the current process' progress.
 * Use this to relay progress information to the user.
 * @class Garmin.TransferProgress
 * @constructor 
 */
Garmin.TransferProgress = Class.create();
Garmin.TransferProgress.prototype = {
	initialize: function(title) {
		this.title = title;
		this.text = new Array();
		this.percentage = null;
	},
	
	addText: function(textString) {
		this.text.push(textString);
	},

    /** Get all the text entries for the transfer
     * @type Array
     */	 
	getText: function() {
		return this.text;
	},

    /** Get the title for the transfer
     * @type String
     */	 
	getTitle: function() {
		return this.title;
	},
	
	setPercentage: function(percentage) {
		this.percentage = percentage;
	},

    /** Get the completed percentage value for the transfer
     * @type Number
     */
	getPercentage: function() {
		return this.percentage;
	},

    /** String representation of instance.
     * @type String
     */	 	
	toString: function() {
		var progressString = "";
		if(this.getTitle() != null) {
			progressString += this.getTitle();
		}
		if(this.getPercentage() != null) {
			progressString += ": " + this.getPercentage() + "%";
		}
		return progressString;
	}
};


/** Encapsulates the data to display a message box to the user when the plug-in is waiting for feedback
 * @class Garmin.MessageBox
 * @constructor 
 */
Garmin.MessageBox = Class.create();
Garmin.MessageBox.prototype = {
	initialize: function(type, text) {
		this.type = type;
		this.text = text;
		this.buttons = new Array();
	},

    /** Get the type of the message box
     * @type String
     */	 
	getType: function() {
		return this.type;
	},

    /** Get the text entry for the message box
     * @type String
     */	 
	getText: function() {
		return this.text;
	},

    /** Get the text entry for the message box
     */	 
	addButton: function(caption, value) {
		this.buttons.push({caption: caption, value: value});
	},

    /** Get the buttons for the message box
     * @type Array
     */	 
	getButtons: function() {
		return this.buttons;
	},
	
	getButtonValue: function(caption) {
		for(var i=0; i< this.buttons.length; i++) {
			if(this.buttons[i].caption == caption) {
				return this.buttons[i].value;
			}
		}
		return null;
	},

    /**
	 * @type String
     */	 
	toString: function() {
		return this.getText();
	}
};

/*
 * Dynamic include of required libraries and check for Prototype
 * Code taken from scriptaculous (http://script.aculo.us/) - thanks guys!
var GarminDeviceControl = {
	require: function(libraryName) {
		// inserting via DOM fails in Safari 2.0, so brute force approach
		document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
	},

	load: function() {
		if((typeof Prototype=='undefined') || 
			(typeof Element == 'undefined') || 
			(typeof Element.Methods=='undefined') ||
			parseFloat(Prototype.Version.split(".")[0] + "." +
			Prototype.Version.split(".")[1]) < 1.5) {
			throw("GarminDeviceControl requires the Prototype JavaScript framework >= 1.5.0");
		}

		$A(document.getElementsByTagName("script"))
		.findAll(
			function(s) {
				return (s.src && s.src.match(/GarminDeviceControl\.js(\?.*)?$/))
			}
		)
		.each(
			function(s) {
				var path = s.src.replace(/GarminDeviceControl\.js(\?.*)?$/,'../../');
				var includes = s.src.match(/\?.*load=([a-z,]*)/);
				var dependencies = 'garmin/device/GarminDevicePlugin' +
									',garmin/device/GarminDevice' +
									',garmin/util/Util-XmlConverter' +
									',garmin/util/Util-Broadcaster' +
									',garmin/util/Util-DateTimeFormat' +
									',garmin/util/Util-BrowserDetect' +
									',garmin/util/Util-PluginDetect' +
									',garmin/device/GarminObjectGenerator';
			    (includes ? includes[1] : dependencies).split(',').each(
					function(include) {
						GarminDeviceControl.require(path+include+'.js') 
					}
				);
			}
		);
	}
}

GarminDeviceControl.load();
 */


﻿/* Stack-based Douglas Peucker line simplification routine 
   returned is a reduced GLatLng array 
   After code by  Dr. Gary J. Robinson,
   Environmental Systems Science Centre,
   University of Reading, Reading, UK
*/

function GDouglasPeucker (source, kink)
/* source[] Input coordinates in GLatLngs 	*/
/* kink	in metres, kinks above this depth kept  */
/* kink depth is the height of the triangle abc where a-b and b-c are two consecutive line segments */
{
    var	n_source, n_stack, n_dest, start, end, i, sig;    
    var dev_sqr, max_dev_sqr, band_sqr;
    var x12, y12, d12, x13, y13, d13, x23, y23, d23;
    var F = ((Math.PI / 180.0) * 0.5 );
    var index = new Array(); /* aray of indexes of source points to include in the reduced line */
	var sig_start = new Array(); /* indices of start & end of working section */
    var sig_end = new Array();	

    /* check for simple cases */

    if ( source.length < 3 ) 
        return(source);    /* one or two points */

    /* more complex case. initialize stack */
		
	n_source = source.length;
    band_sqr = kink * 360.0 / (2.0 * Math.PI * 6378137.0);	/* Now in degrees */
    band_sqr *= band_sqr;
    n_dest = 0;
    sig_start[0] = 0;
    sig_end[0] = n_source-1;
    n_stack = 1;

    /* while the stack is not empty  ... */
    while ( n_stack > 0 ){
    
        /* ... pop the top-most entries off the stacks */

        start = sig_start[n_stack-1];
        end = sig_end[n_stack-1];
        n_stack--;

        if ( (end - start) > 1 ){  /* any intermediate points ? */        
                    
                /* ... yes, so find most deviant intermediate point to
                       either side of line joining start & end points */                                   
            
            x12 = (source[end].lng() - source[start].lng());
            y12 = (source[end].lat() - source[start].lat());
            if (Math.abs(x12) > 180.0) 
                x12 = 360.0 - Math.abs(x12);
            x12 *= Math.cos(F * (source[end].lat() + source[start].lat()));/* use avg lat to reduce lng */
            d12 = (x12*x12) + (y12*y12);

            for ( i = start + 1, sig = start, max_dev_sqr = -1.0; i < end; i++ ){                                    

                x13 = (source[i].lng() - source[start].lng());
                y13 = (source[i].lat() - source[start].lat());
                if (Math.abs(x13) > 180.0) 
                    x13 = 360.0 - Math.abs(x13);
                x13 *= Math.cos (F * (source[i].lat() + source[start].lat()));
                d13 = (x13*x13) + (y13*y13);

                x23 = (source[i].lng() - source[end].lng());
                y23 = (source[i].lat() - source[end].lat());
                if (Math.abs(x23) > 180.0) 
                    x23 = 360.0 - Math.abs(x23);
                x23 *= Math.cos(F * (source[i].lat() + source[end].lat()));
                d23 = (x23*x23) + (y23*y23);
                                
                if ( d13 >= ( d12 + d23 ) )
                    dev_sqr = d23;
                else if ( d23 >= ( d12 + d13 ) )
                    dev_sqr = d13;
                else
                    dev_sqr = (x13 * y12 - y13 * x12) * (x13 * y12 - y13 * x12) / d12;// solve triangle

                if ( dev_sqr > max_dev_sqr  ){
                    sig = i;
                    max_dev_sqr = dev_sqr;
                }
            }

            if ( max_dev_sqr < band_sqr ){   /* is there a sig. intermediate point ? */
                /* ... no, so transfer current start point */
                index[n_dest] = start;
                n_dest++;
            }
            else{
                /* ... yes, so push two sub-sections on stack for further processing */
                n_stack++;
                sig_start[n_stack-1] = sig;
                sig_end[n_stack-1] = end;
                n_stack++;
                sig_start[n_stack-1] = start;
                sig_end[n_stack-1] = sig;
            }
        }
        else{
                /* ... no intermediate points, so transfer current start point */
                index[n_dest] = start;
                n_dest++;
        }
    }

    /* transfer last point */
    index[n_dest] = n_source-1;
    n_dest++;

    /* make return array */
    var r = new Array();
    for(var i=0; i < n_dest; i++)
        r.push(source[index[i]]);
    return r;
    
}

var GarminIntegration = function() {
   var that = this, device, controller, tracks = [], loading, progressInfo;
   
   // ------------------------------------------------------------------------
   // private methods
   // ------------------------------------------------------------------------

   var setInfo = function(msg) {
      $('garmin-tracks').hide();
      $('garmin-info').update(msg);
   };
      
   var startWaiting = function() {
      $('garmin-reload').src = 'images/protoload/waiting.gif';
   };
   
   var stopWaiting = function() {
      $('garmin-reload').src = 'images/reload.png';
   };

   // ------------------------------------------------------------------------
   // public methods
   // ------------------------------------------------------------------------
      
   that.loadTracks = function() {
      if (loading) return;
      
      try {
         setInfo('search for garmin devices');
         controller = new Garmin.DeviceControl;
         controller.register(this);
         controller.unlock([garmin_host, garmin_api_key]);
         startWaiting();
         progressInfo = "find device";
         loading = true;         
         controller.findDevices();
      } catch (e) {
         loading = false;
         stopWaiting();
         if (controller) setInfo('error initialize garmin plugin');
         else setInfo('garmin plugin not installed');
      }
   };
   
   // ------------------------------------------------------------------------
   // garmin controller callbacks
   // ------------------------------------------------------------------------

   that.onFinishFindDevices = function() {
      try {
         controller.setDeviceNumber(0);
         controller.readFromDevice();
         progressInfo = 'read data from device';
      } catch (e) {
         stopWaiting();
         loading = false;
         setInfo('no garmin device found');
      }
   };
   
   that.onProgressReadFromDevice = function(json) {
      var progress = json.progress.getPercentage() || 100;
      setInfo(progressInfo + ': ' + progress + '%');
   };
   
   that.onFinishReadFromDevice = function(json) {
      // reset  existing tracks
      tracks = [null];
      setInfo('Tracks:');
      $('garmin-tracks').innerHTML = '';
      $('garmin-tracks').options.add(new Option('Please Choose'));
      // get tracks
      var gpx = Garmin.XmlConverter.toDocument(json.controller.gpsDataString);
      var nodes = gpx.getElementsByTagName('trk');
      for (var i=0; i<nodes.length; i++) {
         try {
            var node = nodes.item(i);
            var name = node.getElementsByTagName('name').item(0).textContent;
            if (name) {
               tracks.push(node);
               $('garmin-tracks').options.add(new Option(name));
            }
         } catch(e) { }
      }
      // FIXME! what is with routes???
      $('garmin-tracks').show();
      loading = false;
      stopWaiting();
   };
   
   that.handleException = function(error) {
       var msg = error.name + ": " + error.message;
       if (Garmin.PluginUtils.isDeviceErrorXml(error)) {
           msg = Garmin.PluginUtils.getDeviceErrorMessage(error);
       }
       setInfo("Unknown error: " + msg);
   };
   
   // ------------------------------------------------------------------------
   // draw garmin track on map when the track-pulldown selection has changed
   // ------------------------------------------------------------------------

   $('garmin-tracks').onchange = function() {
      // get selected part
      var track = tracks[$('garmin-tracks').selectedIndex];
      if (!track) return;
      // extract points
      var points = [], trkpts = track.getElementsByTagName('trkpt');
      for (var i=0; i<trkpts.length; i++) {
         var trkpt = trkpts.item(i);
         var lat = trkpt.attributes.getNamedItem("lat").nodeValue;
         var lng = trkpt.attributes.getNamedItem("lon").nodeValue;
         points.push($P(lat, lng));
      }
      // reduce points
      points = GDouglasPeucker(points, garmin_tolerance);
      // may reverse points
      if ($('garmin-reverse').checked) points = points.reverse();
      // create parts (which can be imported on the mtk-map)
      var parts = [{ type: 'vertices', points: [] }];
      $EACH(points, function(p) {
         var _part = parts.last();
         if (_part.points.length > 15) {
            _part = { type: 'vertices', points: [] };
            parts.push(_part);
         }
         _part.points.push([ p.lat(), p.lng() ]);
      });
      // add parts to map
      importParts(parts);
   };
};

GEvent.addListener(window, "MapToolkit.Map.init", function() {
   try {
      window.garminIntegration = new GarminIntegration();
      garminIntegration.loadTracks();      
   } catch (e) { };
});


