/*global Snapper, document, window, $, console, opera, Snapper, PVector, Image, pageTracker, clearInterval, setInterval, clearTimeout, setTimeout,
Modernizr, navigator, google, FloatyApp */
/**
	@module Snapper
	@version 1.0
	@author Vince Allen
	@requires PVector, jQuery
*/

/**
	@namespace The root level object for all Snapper components.
*/
Snapper = {};

//

Snapper.viewStack = [];
Snapper.emptyViewStack = function () {
	"use strict";
	Snapper.viewStack = [];
};
Snapper.navigationBarStack = [];

//


/**
	@namespace Holds all Snapper constants.
*/
Snapper.CONSTANTS = {};
Snapper.CONSTANTS.ROOT = null;
Snapper.CONSTANTS.SYSTEM_ERROR = "An error has occurred. Please try again.";
Snapper.CONSTANTS.IS_TOUCH_DEVICE = null;
Snapper.CONSTANTS.SHOW_ME_OUTLINE_COLOR = "#ff0000";
Snapper.CONSTANTS.appWindow = null; // must be set to root level DOM element; can also be set to body via document.getElementsByTagName("body").item(0)
Snapper.CONSTANTS.FRAME_RATE = 24; // ms; lower = faster
Snapper.CONSTANTS.APP_HEIGHT = 480;

/**
	@namespace Called when device changes orientation.
	@requires Place onorientationchange="Snapper.updateOrientation();" in the <body> tag
*/
Snapper.updateOrientation = function () {
	"use strict";
	Snapper.reset();
};

Snapper.Loop = (function () {
	"use strict";
	return {
		"interval": null,
		"list": [],
		"start": function () {
			this.interval = setInterval(function () {

				var i, max;

				for (i = 0, max = Snapper.Loop.list.length; i < max; i += 1) {
					if (typeof Snapper.Loop.list[i].update === "function") {
						Snapper.Loop.list[i].update();
					}
					if (typeof Snapper.Loop.list[i].draw === "function") {
						if (Snapper.Loop.list[i].id === "coverView") { // !! need a better solution; specific to resume app; scroll bar controls position, not update
							if (Modernizr.touch) {
								Snapper.Loop.list[i].draw();
							}	 
						} else {
							Snapper.Loop.list[i].draw();
						}
					}
				}

			}, Snapper.CONSTANTS.FRAME_RATE);
		},
		"stop": function () {
			clearInterval(this.interval);
		},
		"clear": function () {
			this.list = [];
		}
	};
	
}());

/**
	@namespace Holds all Snapper Apps.
*/
Snapper.App = (function () {
	"use strict";
	/**
		@lends Snapper.App.prototype
	*/
	var alias = "Snapper.App",
		app;

	return {
		/**
			@description Initializes a Snapper app and creates the UI, UI.Collection, CONSTANTS, and Views namespaces.
			@param {String} params
			@param {Object} params.name The name of the App.
		 */
		init: function (params) {

			var i, params_required;

			params_required = {name: "string"};

			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface

				Snapper.App[params.name] = {};
				app = Snapper.App[params.name];

				/**
					Make a copy of the Snapper.Collection.
					All elements created for this App will live in Snapper.App[AppName].Collection.
					IMPORTANT: Include a protected variable in all views called "collection" and set it equal to Snapper.App[AppName].Collection.
				*/
				app.Collection = {};
				for (i in Snapper.Collection) { // copy the Snapper.UI.Collection
					if (Snapper.Collection.hasOwnProperty(i)) {
						app.Collection[i] = Snapper.Collection[i];
					}
				}
				app.Collection.Contents = {};
				app.Collection.id = params.name;

				// CONSTANTS; holds constant values; accessible by any View
				app.CONSTANTS = {};
				if (params.CONSTANTS) {
					for (i in params.CONSTANTS) {
						if (params.CONSTANTS.hasOwnProperty(i)) {
							app.CONSTANTS[i] = params.CONSTANTS[i];
						}
					}
				}

				// options; holds configuration options; accessible by any View; usually set in document.ready function
				app.Options = {};
				if (params.Options) {
					for (i in params.Options) {
						if (params.Options.hasOwnProperty(i)) {
							app.Options[i] = params.Options[i];
						}
					}
				}

				app.Views = {}; // create Views object; all views for the app should live in Views.

				app.Admin = { // create Admin object; holds methods to manage the App

					/**
						@description Sets up the view by copying the previous content, emptying the workspace and emptying the collection contents.
						@param {String} module_name
					 */
					initView: function (params) {
						
						var children;
						
						params_required = {app_name: "string", module_name: "string"};

						app = Snapper.App[params.app_name];

						if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
							app.Views[params.module_name].previous_content = $(app.CONSTANTS.workspace).contents(); // copy content to reload in case user cancels operation
							$(app.CONSTANTS.workspace).empty(); // empty the workspace container
							app.Collection.empty(); // reset the collection
							return true;
						} else {
							return false;
						}
					}

				};

				Snapper.CONSTANTS.BODY = document.getElementsByTagName("body").item(0); // create a reference to the document body
			}
		}
	};
}());


/**
	@namespace Utility functions used by Snapper.
*/
Snapper.Utils = (function () {
	"use strict";
	/**
		@lends Snapper.Utils.prototype
	*/
	return {
		/**
			@description Creates a new object, loops thru properties of passed object and assigns them to the new object's prototype.
			@returns {Object} A new object with passed object's properties defined in its prototype.
		*/
		clone : function(object) {
		    function F() {}
		    F.prototype = object;
		    return new F;
		},
		/**
			@description Removes whitespace from either side of a string.
			@returns {String} The trimmed string.
		*/
		trim: function (str, chars) {
			return this.ltrim(this.rtrim(str, chars), chars);
		},
		/**
			@description Removes whitespace from the left side of a string.
			@returns {String} The trimmed string.
		*/
		ltrim: function (str, chars) {
			chars = chars || "\\s";
			return str.replace(new RegExp("^[" + chars + "]+", "g"), "");
		},
		/**
			@description Removes whitespace from the right side of a string.
			@returns {String} The trimmed string.
		*/
		rtrim: function (str, chars) {
			chars = chars || "\\s";
			return str.replace(new RegExp("[" + chars + "]+$", "g"), "");
		},
		setParentHeight: function (params) {
			//console.log(params);
			var params_required = {
				"parent": "object",
				"children": "object",
				"obj": "object"
			},
			padTop,
			padBot,
			i, 
			max,
			el,
			parentHeight = 0,
			parent,
			children,
			obj,
			windowHeight;
			
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, "SetParentHeight")) { // interface	
				
				parent = params.parent;
				children = params.children;
				obj = params.obj;
				
				// find parent height
				// absolute positioning does not allow for variable element height
				// need to assign a specific height based on the height of the view controller's children
				// important: height() does not include padding
				for (i = 0, max = children.length; i < max; i += 1) {
					el = $(children[i]);
					padTop = el.css("padding-top").replace("px", "");
					padBot = el.css("padding-bottom").replace("px", "");
					parentHeight += parseInt(el.height(), 10) + parseInt(padTop.replace("px", ""), 10) + parseInt(padBot.replace("px", ""), 10);
				}
				
				obj.height = parentHeight;
				
				windowHeight = $(window).height();
				
				if (Snapper.CONSTANTS.APP_HEIGHT > windowHeight) {
					
					parent.css({
						//"height": windowHeight
						"height": windowHeight
					});
				} else {
					parent.css({
						"height": Snapper.CONSTANTS.APP_HEIGHT
						//"height": 356
					});
				}
				

			
			}
		},
		/**
			@description Reformats data passed from jQuery's $serialize() function as a property list.
			@param {Array} data The array of data to be converted.
			@returns {String} The trimmed string.
		*/
		serializedFormDataToPropList: function (data) {
			var i, max, obj = {};
			for (i = 0, max = data.length; i < max; i += 1) {
				obj[data[i].name] = data[i].value;
			}
			return obj;
		},
		PVectorSub: function (v1, v2) {
			return PVector.create(v1.x - v2.x, v1.y - v2.y);
		},
		PVectorAdd: function (v1, v2) {
			return PVector.create(v1.x + v2.x, v1.y + v2.y);
		},
		publisher: {
			subscribers : {
				any: [] // event type: subscribers
			},
			subscribe: function (type, fn, context) {
				type = type || "any";
				fn = typeof fn === "function" ? fn : context[fn];

				if (typeof this.subscribers[type] === "undefined") {
					this.subscribers[type] = [];
				}
				this.subscribers[type].push({fn: fn, context: context || this});
			},
			unsubscribe: function (type, fn, context) {
				this.visitSubscribers("unsubscribe", type, fn, context);
			},
			publish: function (type, publication) {
				this.visitSubscribers("publish", type, publication);
			},
			visitSubscribers: function (action, type, arg, context) {
				var pubtype = type || "any",
					subscribers = this.subscribers[pubtype],
					i,
					max = subscribers ? subscribers.length : 0; // do not loop if no objects have subscribed

				for (i = 0; i < max; i += 1) {
					if (action === "publish") {
						subscribers[i].fn.call(subscribers[i].context, arg);
					} else {
						// to unsubscribe an object, compare the function and the context; convert to a String before comparison
						if (String(subscribers[i].fn) === String(arg) && String(subscribers[i].context) === String(context)) {
							subscribers.splice(i, 1);
						}
					}
				}
			}
		},
		/**
			@description Adds a subscribers property and the following methods to an object: subscribe(), unsubscribe(), publish() and visitSubscribers().
			@returns {Object} The object to become a publisher.
		*/
		makePublisher: function (o) {
			var i;
			for (i in Snapper.Utils.publisher) {
				if (Snapper.Utils.publisher.hasOwnProperty(i) && typeof Snapper.Utils.publisher[i] === "function") {
					o[i] = Snapper.Utils.publisher[i];
				}
			}
			o.subscribers = {any: []};
		},
		/*
		Appends a CSS rule to a stylesheet.
		@param {Object} sheet
		@param {String} selector
		@param {String} rule
		*/
		appendCSSRule: function (sheet, selector, rule) {			
			if (sheet.insertRule) {
				this.appendCSSRule = function (sheet, selector, rule) {
					sheet.insertRule(selector + " {" + rule + "}", 0);					
				};
			} else if (sheet.addRule) {
				this.appendCSSRule = function (sheet, selector, rule) {
				sheet.addRule(selector, rule);
				//ex: sheet.addRule("p", "color:blue");
				};			
			}
			this.appendCSSRule(sheet, selector, rule);
		},
		randomRange: function (min, max) {
			return Math.random() * (max - min) + min;
		},
		map: function (value, min1, max1, min2, max2) { // returns a new value relative to a new range
			var unitratio = (value - min1) / (max1 - min1);
			return (unitratio * (max2 - min2)) + min2;
		},
		findObjById: function () {
			/*var i;
			for (i in Snapper.App) {
				
			}*/
		}
	};
}());

/**
	@namespace Holds all UI components.
*/
Snapper.UI = {};

/**
	@namespace Holds a collection of all objects created by the Snapper.Constructor that do not belong to an App. Snapper.App.init() will copy all properties of Snapper.UI.Collection to the App's UI.Collection.
*/
Snapper.Collection = (function () {
	"use strict";
	/**
		@lends Snapper.UI.Collection.prototype
	*/
	var alias = "UI.Collection";

	return {
		/**
			@description {Boolean} Toggle between using a documentFragment and rendering each object to the DOM; using documentFragment is faster.
		*/
		useDocFrag: true,
		/**
			@description {Object} Contents A collection of objects created by Snapper.Constructor.
		 */
		Contents: {},
		/**
			@description {Array} ref An array of ids that refer to objects in Collection.Contents. Use for faster looping over Snapper.Collection.Contents.
		 */
		ref: [],
		/**
			@description Appends all objects stored in Snapper.Collection.Contents to params.workspace.
			@param {Function} after A function to run after all objects have been appended.
		 */
		renderCollection : function (params) {

			var i, max, x, x_max, classNames, obj, parent, params_required = {workspace: "object"};

			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface

				Snapper.Log.time("renderCollection");

				Snapper.Constructor.create({
					collection: this,
					id: "docFrag",
					el: document.createDocumentFragment()
				});

				for (i = 0, max = this.ref.length; i < max; i += 1) {

					obj = this.Contents[this.ref[i]];

					if (obj.parent) { // if parent is defined, it is a child of an object
						if (typeof this.Contents[obj.parent] !== "undefined") { // check if parent is in the collection
							parent = this.Contents[obj.parent].el;
						}
					} else { // if no parent is defined, this is a root level object
						if (this.useDocFrag) { // if using docFrag
							parent = this.Contents.docFrag.el; // append object to documentFragment
						} else {
							parent = params.workspace; // append object directly to the root DOM object
						}
					}
					if (parent.appendChild && this.ref[i] !== "docFrag") {
						obj.el = parent.appendChild(obj.el); // update reference to the object
						parent.appendChild(document.createTextNode(" ")); // add a space to preserve normal spacing between html elements
					}
					// check if this object should carry event tracking
					if (obj.el.className && Snapper && Snapper.Tracker) { // 1. loop thru obj's classes
						classNames = obj.el.className.split(/\s+/);
						for (x = 0, x_max = classNames.length; x < x_max; x += 1) { // 2. is this class in the list of classes to track?
							if ($.inArray(classNames[x], Snapper.Tracker.className) !== -1) { // 3. if so, call Snapper.Tracker.trackThisObject()
								Snapper.Tracker.trackThisObject({
									obj: obj.el,
									className: classNames[x],
									action: "click"
								});
								Snapper.Tracker.trackThisObject({
									obj: obj.el,
									className: classNames[x],
									action: "touchend"
								});
							}
						}
					}
				}

				if (this.useDocFrag && typeof this.Contents.docFrag === "object") { // if using docFrag, append entire documentFragment to the root DOM object
					if (params.workspace.appendChild) { // workspace selected via document.getElementById()
						params.workspace.appendChild(this.Contents.docFrag.el);
					} else if (params.workspace[0].appendChild) { // workspace selected via jQuery
						params.workspace[0].appendChild(this.Contents.docFrag.el);
					} else {
						Snapper.Log.message("Error: workspace cannot call appendChild()");
					}
				}
				if (typeof params.after === "function") {
					params.after();
				}
				Snapper.Log.timeEnd("renderCollection");

			} else {
				return false;
			}
		},
		/**
			@description Removes all objects from this.Contents. Also resets this.ref to an empty array.
		 */
		empty: function () {
			this.Contents = {};
			this.ref = [];
		}
	};

}());

/**
	@namespace Renders an HTML structure to a workspace.
*/
Snapper.SnapKit = (function () {
	"use strict";
	/**
		@lends Snapper.SnapKit.prototype
	*/
	return {
		/**
			@description A recursive function that loops through a json description of an HTML stucture and sends objects to Snapper.Constructor with their parent/child relationships intact.
			@param {String} id The id of a dom object to construct.
			@param {Object} obj The object to construct.
			@param {Parent} parent The object's parent. If not defined or null, this is a root level object.
			@param {Object} collection The collection object that will store the objects.

			@example
construct_content = {

	container_generic: {
		render: {
			element_type: "div",
			className: "container-generic",
			children: [
				{
					id: "container_generic_inner",
					render: {
						element_type: "div",
						className: "container-generic-inner"
					}
				}
			]
		}
	}

};
for (i in construct_content) {
	if (construct_content.hasOwnProperty(i)) {
		Snapper.SnapKit.snap(i, construct_content[i], null, collection);
	}
}

		 */
		count: 0,
		date: new Date(),
		id_prefix: "snap",
		snap: function (type, obj, parent, collection) {

			var i, max, a, x, el, parent_id = null,
				render_props = {}, // build render properties to pass to render method of a preconfigured object
				constructor_props = {}; // build constructor properties to pass to the create method of the Constructor object

			if (typeof obj === "undefined") { // if obj is undefined, node is likely a blank text node; return true;
				return true;
			}

			this.count += 1; // increment object count

			if (!obj.id) {
				obj.id = this.id_prefix + this.date.getMilliseconds() + this.count; // if object does not have an id, create one using date and object count
			}

			if (type === "text") { // if type is text or html node, set the prop in the parent
				parent.text = obj;
				return true;
			} else if (type === "html") {
				parent.html = obj;
				return true;
			} else {
				for (a in obj) { // build any other render properties in render_props
					if (obj.hasOwnProperty(a)) {
						if (a === "children") {
							if (obj[a].constructor.toString().indexOf("Array") !== -1) { // check that children param is an array
								for (i = 0, max = obj[a].length; i < max; i += 1) { // loop thru child objects; pass this object as the parent
									for (x in obj[a][i]) { // iterate over child properties to get element type
										if (obj[a][i].hasOwnProperty(x)) {
											Snapper.SnapKit.snap(x, obj[a][i][x], obj, collection);
										}
									}
								}
							} else {
								Snapper.Log.message("Error from Snapper.SnapKit: Children must be passed as an array. Object: " + obj.id);
							}
						} else if (a === "events") { // do not append events as an attribute of the element; save to constructor_props
							constructor_props.events = obj[a];
						} else {
							render_props[a] = obj[a];
						}
					}
				}

				render_props.element_type = type; // set element type in render props
			}

			if (obj.text) { // if the object was assigned text from a child, assign it to the render_props.
				render_props.text = obj.text;
			}

			if (obj.html) { // if the object was assigned html from a child, assign it to the render_props.
				render_props.html = obj.html;
			}

			if (parent) { // only pass parent.id if parent exists
				parent_id = parent.id;
			}

			constructor_props.id = obj.id;
			constructor_props.clone = Snapper.UI.Element;
			constructor_props.el = Snapper.UI.Element.render(render_props); // pass render properties to render method of the preconfigured object
			constructor_props.parent = parent_id;
			constructor_props.collection = collection; // the object's collection should be passed to the Constructor.

			el = Snapper.Constructor.create(constructor_props);

			return el;

		},
		/**
		@description A recursive function that loops a collection of node objects and creates a json representation of the structure.
		@param {Array} children The collection of childNodes (usually created via elementNodeReference.childNodes).
		*/
		compact: function (children) {

			var i, max_i, a, max_a, construct = [], el, tagName, attr, textVal;

			for (i = 0, max_i = children.length; i < max_i; i += 1) {
				el = {};
				if (children[i].nodeType === 1) { // child is an element
					tagName = children[i].tagName;
					el[tagName] = {};
					attr = children[i].attributes; // loop thru element attributes
					for (a = 0, max_a = attr.length; a < max_a; a += 1) {
						el[tagName][attr[a].name] = attr[a].value;
					}
					if (children[i].childNodes.length > 0) {
						el[tagName].children = (function () {
							return Snapper.SnapKit.compact(children[i].childNodes);
						}()); // if this obj has children, call function recursively
					}
					construct.push(el);
				} else if (children[i].nodeType === 3) {  // child is a text node
					textVal = Snapper.Utils.trim(children[i].nodeValue); // get text
					if (textVal) { // do not add text if it is not a character
						el.text = textVal;
						construct.push(el);
					}
				}
			}

			return construct;
		},
		compactor: function (children) {
			//var win = window.open(); // open a new window with rendered json
			return this.compact(children);
		},
		/**
		@description Renders a plain text representation of a JSON object.
		@param {Object} json The JSON to render.
		@param {Object} json A DOM reference to a textarea that will display the JSON.
		@requires jquery, JSON
		*/
		render: function (json, textarea) {
			var str = JSON.stringify(json, null, 3);
			$(textarea).text(str);
		}
	};

}());

/**
	@namespace Creates new objects by Cloning parent object; Adds new object to Snapper.Collection.Contents and Snapper.Collection.ref
*/
Snapper.Constructor = (function () {
	"use strict";
	/**
		@lends Snapper.Constructor.prototype
	*/
	var alias = "Snapper.Constructor";

	return {
		/**
			@description Creates an empty object, assigns a passed object to its prototype, returns the new object. Use for prototypal inheritence.
			@param {Object} object
		 */
		clone : function(object) {
		    function F() {}
		    F.prototype = object;
		    return new F;
		},
		/**
			@description Adds an event listener to an object.
			@param {Object} obj The object to receive the event listener.
			@param {String} type The type of listener to add. Ex: mouseup, keydown, etc.
			@param {Object} data Data to attach to the event object.
			@param {Function} listener The function to run when the event is triggered.
			@param {Boolean} [use_capture=false] Set to true to use event capturing.
		 */
		addEvent: function (obj, type, data, listener, use_capture) {

			if (obj.el.addEventListener) { // W3C
				this.addEvent = function (obj, type, data, listener, use_capture) {
					var capture = use_capture || false,
						list_func = function (event) { event.params = data; listener(event); };

					obj.events[type] = list_func; // store the listener for removeEvent
					obj.el.addEventListener(type, list_func, capture);
				};
			} else if (obj.el.attachEvent) { // IE
				this.addEvent = function (obj, type, data, listener) {
					var list_func = function (event) { event.params = data; listener(event); };

					obj.events[type] = list_func; // store the listener for removeEvent
					obj.el.attachEvent("on" + type, list_func);
				};
			}

			this.addEvent(obj, type, data, listener, use_capture);

		},
		/**
			@description Removes an event listener from an object.
			@param {Object} obj The object that will have the event removed.
			@param {String} type The type of listener to remove. Ex: mouseup, keydown, etc.
			@param {Boolean} [use_capture=false] Set to true if the event uses event capturing.
		 */
		removeEvent: function (obj, type, use_capture) {

			if (obj.el.removeEventListener) { // W3C
				this.removeEvent = function (obj, type, use_capture) {
					var capture = use_capture || false;
					obj.el.removeEventListener(type, obj.events[type], capture);
				};
			} else if (obj.el.detachEvent) { // IE
				this.removeEvent = function (obj, type, use_capture) {
					var capture = use_capture || false;
					obj.el.detachEvent("on" + type, obj.events[type], capture);
				};
			}

			this.removeEvent(obj, type, use_capture);
		},
		/**
			@description Creates and adds an object to either a specified collection or the default Snapper.UI.Collection.
			@param {Object} params
			@param {Object} params.el A DOM object that either already exists in the document or is created via a render method from a preconfigured object like Snapper.UI.Element.
			@param {Object} [params.collection=Snapper.UI.Collection] A collection to store the object.
		*/
		create : function (params) {

			var i, obj, params_required, collection;

			params_required = {el: "object"};

			if (Snapper.Interface.checkRequiredParams(params, params_required, false, alias)) { // interface

				obj = this.clone(params.clone);
				for (i in params) {
					if (params.hasOwnProperty(i)) {
						obj[i] = params[i];
					}
				}
				if (obj.enable) {
					obj.enable(); // sets initial state; adds events
				}

				obj.showMe = function () {
					$(obj.el).css("border", "1px solid " + Snapper.CONSTANTS.SHOW_ME_OUTLINE_COLOR);
				};

				collection = params.collection || Snapper.Collection; // if a collection is passed, use it; otherwise use the Snapper collection

				if (!params.is_static) { // check if is_static; static objects are not added to the collection and cannot be directly controlled
					collection.ref.push(params.id); // add id to array of references
					collection.Contents[params.id] = obj; // add object to collection
				}

				Snapper.Constructor.subscribe("showMe", obj.showMe, obj);

				return obj.el; // return the element; can be used to nest elements

			} else {
				return false;
			}
		},
		/**
			@description Appends an object to its parent.
			@param {Object} obj The object to append.
		*/
		renderObject: function (obj) {
			if (typeof obj !== "undefined" && typeof obj.parent !== "undefined") {
				obj.parent.appendChild(obj.el);
			} else {
				return false;
			}
		}
	};
}());
if (Snapper.Utils) {
	Snapper.Utils.makePublisher(Snapper.Constructor);
}

/**
	@namespace Provides an interface to check required params passed between objects.
*/
Snapper.Interface = (function () {
	"use strict";
	/**
		@lends Snapper.Interface.prototype
	*/
	return {
		/**
		 *
		 * Checks the passed params match the required params.
		 * Returns true of all params match.
		 * Returns false if any params do not match.
		 *
		 * @param {JSON} params_passed
		 * @param {JSON} params_required
		 * @param {Boolean} debug
		 */
		checkRequiredParams : function (params_passed, params_required, debug, name) {
			var i, check, msg;
			check = true;
			for (i in params_required) { // loop thru required params
				if (params_required.hasOwnProperty(i)) {
					try {
						if (typeof params_passed[i] !== params_required[i] || params_passed[i] === "") { // if there is not a corresponding key in the passed params; or params passed value is blank
							check = false;

							if (params_passed[i] === "") {
								msg = "Interface.checkRequiredParams: required param '" + i + "' is empty.";
							} else if (typeof params_passed[i] === "undefined") {
								msg = "Interface.checkRequiredParams: required param '" + i + "' is missing from passed params.";
							} else {
								msg = "Interface.checkRequiredParams: passed param '" + i + "' must be type " + params_required[i] + ". Passed as " + typeof params_passed[i] + ".";
							}
							if (name) {
								msg = msg + " from: " + name;
							}
							throw new Error(msg);
						}
					} catch (e) {
						if (debug) {
							Snapper.Log.error(e);
						}
					}
				}
			}
			return check;
		}
	};

}());

/**
	@namespace Log messages or errors to the browser console.
	possibly add console.dir()... outputs DOM-level info, console.inspect()... highlights in the Elements tab, monitorEvents(obj, [mouse,key])...outputs event info in real-time
*/
Snapper.Log = (function () {
	"use strict";
	/**
		@lends Snapper.Log.prototype
	*/
	return {
		timer_enabled : true,
		count_enabled : false,
		/**
			@description Outputs a message to the browser console.
			@param {String} str The message to output.
			@requires The browser must have a console object.
		*/
		message : function (str) {
			try {
				if (typeof console !== "undefined") {
					console.log(str); // output error to console
				} else if (typeof opera !== "undefined") { // opera uses error console
					opera.postError(str);
				}
				return str;
			} catch (e) {
				// do nothing
				return false;
			}
		},
		/**
			@description Outputs an error to the browser console.
			@param {Object} e The Error object typically passed via a try/catch block.
			@requires The browser must have a console object.
		*/
		error: function (e) {
			return Snapper.Log.message(e.name.toUpperCase() + " line: " + e.lineNumber + " " + e.message);
		},
		/**
			@description Creates a new timer under the given name. Call Snapper.Log.timeEnd(name) with the same name to stop the timer and print the time elapsed.
			@param {String} name The timer name.
			@requires The browser must have a console object.
		*/
		time: function (name) {
			if (typeof console !== "undefined" && this.timer_enabled && console.time) {
				console.time(name); // start timer
			}
		},
		/**
			@description Stops a timer created by a call to Snapper.Log.time(name) and writes the time elapsed.
			@param {String} name The timer name.
			@requires The browser must have a console object.
		*/
		timeEnd: function (name) {
			if (typeof console !== "undefined" && this.timer_enabled && console.timeEnd) {
				console.timeEnd(name); // end timer and print time elapsed to output
			}
		},
		/**
			@description Writes the number of times that the line of code where count was called was executed. The optional argument title will print a message in addition to the number of the count.
			@param {String} title The count title.
			@requires The browser must have a console object.
		*/
		count: function (title) {
			if (typeof console !== "undefined" && this.count_enabled && console.count) {
				console.count(title);
			}
		},
		/**
			@description Allows to log provided data using tabular layout.
			@param {Object|Array[]} data Represents table like data (array of arrays or list of objects).
			@param {Object[]} [columns] Specify columns and/or properties to be logged. http://www.softwareishard.com/blog/firebug/tabular-logs-in-firebug/
			@requires The browser must have a console object.
		*/
		table: function (data, columns) {
			if (typeof console !== "undefined" && console.table) {
				console.table(data, columns);
			}
		}
	};
}());

/**
	@namespace Preloads images 
	The ImagePreloader tracks which images have completed their download. Snapper.UI objects can check the ImagePreloader object to determine if an image
	is available for use.
*/
Snapper.ImagePreloader = (function () {
	"use strict";
	/** @lends Snapper.ImagePreloader.prototype */
	var state = "static",
		images = {};
	
	return {
		state: state,
		images: images,
		afterLoad: null,
		timeout: null,
		timeoutDuration: 10000,
		timeoutFunction: null,
		/**
			@description Creates a new Image object, adds it to the 'images' property, sets the image's initial properties and adds event handlers to detect when load is complete or if an error occurs. Check the status of the entire collection of images at Snapper.ImagePreloader.state. Check the status of specific images at Snapper.ImagePreloader.images[name].state.
			@param {Array} params An array of images to preload formatted like: 
			[
				{
					path : "/images/", 
					name : "btn_sprite_default",
					extension: ".png"
				}
			]
		*/			
		fetch : function (params) {
			
			var i, max, img_name, params_required, onload, onerror, onabort, me;
								
			params_required = {path: "string", name: "string", extension: "string"};
			
			clearTimeout(this.timeout);
			me = this;
			this.timeout = setTimeout(function () {
				return (function () {
					if (typeof me.timeoutFunction === "function") {
						me.timeoutFunction();
					}
				}());
			}, this.timeoutDuration);
			
			onload = function (e) {
				Snapper.ImagePreloader.images[this.name].state = "loaded";
				Snapper.ImagePreloader.images[this.name].loaded = true;
				Snapper.ImagePreloader.checkPreloaderState();
			};
			
			onerror = function (e) {
				Snapper.ImagePreloader.images[this.name].state = "error";
				Snapper.ImagePreloader.checkPreloaderState();
			};
			
			onabort = function (e) {
				Snapper.ImagePreloader.images[this.name].state = "aborted";
				Snapper.ImagePreloader.checkPreloaderState();
			};
			
			for (i = 0, max = params.length; i < max; i += 1) {
				
				if (Snapper.Interface.checkRequiredParams(params[i], params_required, true, "Snapper.ImagePreloader")) { // interface
					
					img_name = params[i].name;
					this.images[img_name] = {};
					this.images[img_name].my_image = new Image();
					this.images[img_name].my_image.name = params[i].name;				
					this.images[img_name].state = "loading";
					this.images[img_name].loaded = false;
			
					this.images[img_name].my_image.onload = onload;
					this.images[img_name].my_image.onerror = onerror;
					this.images[img_name].my_image.onabort = onabort;
					this.images[img_name].my_image.src = params[i].path + params[i].name + params[i].extension; // IMPORTANT: Must set the image.src AFTER onload has been assigned to the new image object
				
				} else {
					this.state = "error";
				}
			
			}

		},
		/**
			@description Checks all image objects to see if they've loaded. If all have loaded, sets the state of Snapper.ImagePreloader to "loaded". Each event attached to an Image object runs checkPreloaderState().
		*/
		checkPreloaderState : function () {
			var i;
			for (i in this.images) {
				if (this.images.hasOwnProperty(i)) {
					if (this.images[i].state !== "loaded") {
						return false;
					}
				}
			}
			this.state = "loaded";
			if (this.afterLoad && typeof this.afterLoad === "function") {
				this.afterLoad();
			}
		}
	};

}());


/**
	@namespace Manages Geeolocation
*/
Snapper.Geo = (function () {
	/** @lends Snapper.Geo.prototype */
	"use strict";
	var supported,
		options = {
			timeout: 300000,
			enableHighAccuracy: true,
			maximumAge: 100000
		},
		api;
		//apiurl = 'http://maps.google.com/maps/geo?output=json&sensor=false&q=';

	if (navigator.geolocation) {
		supported = true;
		api = navigator.geolocation;
	}

	return {
		debug: false,
		api: api,
		supported: supported,
		position: null,
		latitude: null,
		longitude: null,
		latlng: null,
		city_short: null,
		city_long: null,		
		state_short: null,
		state_long: null,
		zip_short: null,
		zip_long: null,
		formatted_address: null,
		setPositionCallback: null,
		getCurrentPosition: function (callback) {
			this.setPositionCallback = callback;
			this.api.getCurrentPosition(Snapper.Geo.setCurrentPosition, Snapper.Geo.error, options);
		},
		setCurrentPosition: function (position) {
			if (position) {
				Snapper.Geo.position = position;
				Snapper.Geo.latitude = Snapper.Geo.position.coords.latitude;
				Snapper.Geo.longitude = Snapper.Geo.position.coords.longitude;				
				if (typeof Snapper.Geo.setPositionCallback === "function") {
					Snapper.Geo.setPositionCallback();
				}
			}					
		},
		error: function (error) {
			switch (error.code) {
				case error.TIMEOUT:
					Snapper.Log.message("Geoloaction Timeout");
					break;
				case error.POSITION_UNAVAILABLE:
					Snapper.Log.message("Geoloaction position unavailable");
					break;
				case error.PERMISSION_DENIED:
					Snapper.Log.message("Geoloaction permission denied");
				break;
				default:
					Snapper.Log.message("Geoloaction returned unknown error code: " + error.code);
				break;
			}
			if (typeof Snapper.Geo.errorCallback === "function") {
				Snapper.Geo.errorCallback();
			}
		},
		errorCallback: null,
		getStateViaGoogleAPI: function (callback) {
			
			var geocoder = new google.maps.Geocoder(), latlng = new google.maps.LatLng(Snapper.Geo.latitude, Snapper.Geo.longitude);

		    geocoder.geocode({'latLng': latlng}, function(results, status) {
				var i, max, x, max_x, detail_result, short_name, long_name, target_type_name = "administrative_area_level_1";

				if (results) {

					if (results[0].formatted_address) {
						Snapper.Geo.formatted_address = results[0].formatted_address;
					}
					
					if (results[0].address_components && $.isArray(results[0].address_components)) { // check for address_components key; check that address_components key is an array
						
						detail_result = results[0].address_components;
						
						if (Snapper.Geo.debug === true) {
							Snapper.Log.message(detail_result);
						}
						
						for (i = 0, max = detail_result.length; i < max; i += 1) { // loop thru components

							if (detail_result[i].types && $.isArray(detail_result[i].types)) { // check for types key; check that types key is an array

								for (x = 0, max_x = detail_result[i].types.length; x < max_x; x += 1) { // loop thru types

									if (detail_result[i].types[x] === target_type_name) { // the first time we find "administrative_area_level_1"				

										short_name = detail_result[i].short_name;
										long_name = detail_result[i].long_name;

										if (short_name.length === 2) {

											Snapper.Geo.state_short = short_name;
											Snapper.Geo.state_long = long_name;
											
											callback();
											break;
										}												
									}
								}
							}
						}
					}
				}
				
			});
		},
		getAddressComponentsViaGoogleAPI: function (callback) {
			
			var geocoder = new google.maps.Geocoder(), latlng = new google.maps.LatLng(Snapper.Geo.latitude, Snapper.Geo.longitude);

		    geocoder.geocode({'latLng': latlng}, function(results, status) {
				var i, max, x, max_x, detail_result, short_name, long_name, 
				target_type_city = "locality",
				target_type_state = "administrative_area_level_1",
				target_type_zip = "postal_code";

				if (results) {

					if (results[0].formatted_address) {
						Snapper.Geo.formatted_address = results[0].formatted_address;
					}
					
					if (results[0].address_components && $.isArray(results[0].address_components)) { // check for address_components key; check that address_components key is an array

						detail_result = results[0].address_components;
						
						if (Snapper.Geo.debug === true) {
							Snapper.Log.message(detail_result);
						}
						
						for (i = 0, max = detail_result.length; i < max; i += 1) { // loop thru components

							if (detail_result[i].types && $.isArray(detail_result[i].types)) { // check for types key; check that types key is an array

								for (x = 0, max_x = detail_result[i].types.length; x < max_x; x += 1) { // loop thru types

									if (detail_result[i].types[x] === target_type_city) { // city; the first time we find "locality"
									
										Snapper.Geo.city_short = detail_result[i].short_name;
										Snapper.Geo.city_long = detail_result[i].long_name;
									
									}
									
									if (detail_result[i].types[x] === target_type_state) { // state; the first time we find "administrative_area_level_1"				

										short_name = detail_result[i].short_name;
										long_name = detail_result[i].long_name;

										if (short_name.length === 2) {

											Snapper.Geo.state_short = short_name;
											Snapper.Geo.state_long = long_name;

											break;
										}												
									}
									
									if (detail_result[i].types[x] === target_type_zip) { // zip; the first time we find "postal_code"
									
										Snapper.Geo.zip_short = detail_result[i].short_name;
										Snapper.Geo.zip_long = detail_result[i].long_name;
									
									}
								}
							}
						}
						callback();
					}
				}
				
			});
		}
	};

}());

/**
	@namespace Manages Google Analytics event tracking.
*/
Snapper.Tracker = (function () {
	"use strict";
	
	var time_start = new Date().getTime();
	
	return {
		time_start: time_start,
		className: [], // add classes of objects to track; the object's id will appear in the event tracking report
		toTrack: {
			click: [], // add classes to these arrays that should NOT be tracked for these actions
			touchend: []
		},
		tracked_events: [],
		eventHandlerRef: function (data) {
			return function (e) {
				
				var time,
					category,
					action,
					label,
					value;					
									
				if (data.time_start) { // the time in ms from when script loaded and the user triggered the event
					time = new Date();
					value = time.getTime() - data.time_start;
				} else {
					value = null;
				}
				
				category = data.className.toLowerCase();
				action = data.action.toLowerCase() + " - " + data.className.toLowerCase();
				if (data.id) {
					label = data.id.toLowerCase();
				} else {
					label = null;
				}

				Snapper.Tracker.tracked_events.push({ category: category, action: action, label: label, value: value }); // store the tracked events 

				if(typeof _gaq === "object") {
					return _gaq.push(["_trackEvent", category, action, label, value]); // Google Analytics Async Code
				} else if (typeof pageTracker === "object") {
					return pageTracker._trackEvent(category, action, label, value); // Google Analytics Traditional (ga.js) Code
				}								
			};
		},
		/**
			@description Configures the Snapper.Tracker object and adds events to track. Only call configure once. 
			@param {Object} params
		 */
		configure: function (params) {
			
			var action, objByClass, i, j, max;
			
			if (params) {
				
				this.className = (typeof params.className !== "undefined" && params.className.constructor === Array) ? params.className : [];
				this.toTrack = (typeof params.toTrack === "object") ? params.toTrack : {};			
			
				for (action in this.toTrack) {
					if (this.toTrack.hasOwnProperty(action)) {
						for (i = 0, max = this.className.length; i < max; i += 1) {
						
							if (!this.inArray(this.className[i], this.toTrack[action])) { // only track this action if this class does NOT appear in the action's array
							
								objByClass = this.getElementsByClass(this.className[i]); // get all objects in document by className
							
								for (j = 0; j < objByClass.length; j += 1) { // loop thru objects and add event listener
									this.addEvent(objByClass[j], action, this.eventHandlerRef({ id: objByClass[j].getAttribute("id"), className: this.className[i], action: action, time_start: this.time_start }));
								}
							}
						}
					}
				}			
			}			
		},
		/**
			@description Adds event tracking to single object. Useful if objects are created after the DOM initially loads and calls Snapper.Tracker.configure();
			@param {Object} params
		 */
		trackThisObject: function (params) {
			
			var params_required = {
				obj: "object",
				className: "string",
				action: "string"
			}, time_start;
			
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, "Snapper.Tracker.trackThisObject")) { // interface
				time_start = new Date().getTime();
				this.addEvent(params.obj, params.action, this.eventHandlerRef({ id: params.obj.getAttribute("id"), className: params.className, action: params.action, time_start: this.time_start }));
			}
		},
		inArray: function (needle, haystack) {

		    var key = '';

	        for (key in haystack) {
	            if (haystack[key] === needle) {
	                return true;
	            }
	        }

		    return false;
		},
		addEvent: function (target, eventType, handler) {
			
			if (target.addEventListener) { // WC3
				this.addEvent = function (target, eventType, handler) {
					target.addEventListener(eventType, handler, false);
				};
			} else { // IE
				this.addEvent = function (target, eventType, handler) {
					target.attachEvent("on" + eventType, handler);
				};				
			}
			this.addEvent(target, eventType, handler);
		},
		getElementsByClass: function (searchClass, node, tag) { // adapted from Dustin Diaz; http://www.dustindiaz.com/getelementsbyclass

			var i,
				j,
				els,
				elsLen,
				pattern,
				classElements = [];
				
				node = node || document;
				tag = tag || '*';

			els = node.getElementsByTagName(tag);

			elsLen = els.length;

			pattern = new RegExp("(^|\\s)"+searchClass+"(\\s|$)");

			for (i = 0, j = 0; i < elsLen; i += 1) {
				if (pattern.test(els[i].className)) {
					classElements[j] = els[i];
					j += 1;
				}
			}

			return classElements;
		},
		/**
			@description Highlights all DOM elements carrying tracking events. 
		 */
		showMe: function () {
			
			var i, max;
			
			for (i = 0, max = this.className.length; i < max; i += 1) {
				$("." + this.className[i]).css("border", "1px solid #00ff00");
			}
		}
	};
	
}());

/**
	@namespace A generic preconfigured object.
*/
Snapper.UI.Element = (function () {
	"use strict";
	/**
		@lends Snapper.UI.Element.prototype
	*/
	var alias = "Snapper.UI.Element";

	return {
		state : null,
		disable: function () {
			this.unbindEvents();
			this.state = "disabled";
			this.update();
		},
		enable: function () {
			this.bindEvents();
			this.state = "enabled";
			this.update();
		},
		bindEvents: function () {
			var evt, events;
			if (this.events && typeof this.events === "function") {
				events = this.events();
			}
			this.unbindEvents();
			for (evt in events) {
				if (events.hasOwnProperty(evt)) {
					Snapper.Constructor.addEvent(this, evt, {id: this.id}, events[evt]);
				}
			}
		},
		unbindEvents: function () {
			var evt;
			for (evt in this.events) {
				if (this.events.hasOwnProperty(evt)) {
					Snapper.Constructor.removeEvent(this, evt);
				}
			}
		},
		unbindEvents_: function () {
			$(this.el).unbind(".enable");
		},		
		update : function () {

			var class_names_current = Snapper.Utils.trim(this.el.className);

			switch (this.state) {
				case "enabled":
					this.el.className = Snapper.Utils.trim(class_names_current.replace("disabled", ""));
					break;
				case "disabled":
					this.el.className = Snapper.Utils.trim(class_names_current.replace("enabled", "")) + " disabled";
					break;
			}

		},
		render: function (params) {
			var i, el, params_required = {element_type: "string"};

			if (Snapper.Interface.checkRequiredParams(params, params_required, false, alias)) {

				if (params.element_type === "input" && params.type === "radio") { // ie6 and ie7 cannot handle dynamically created radio buttons
					el = this.createRadioElement({name: params.name, checked: params.checked}); // create a radio button using a fragment
					delete params.name; // do not try to set the name property below
					delete params.checked; // do not try to set the checked property below
					delete params.type; // do not try to set the type property below
				} else {
					el = document.createElement(params.element_type);
				}

				delete params.element_type;

				for (i in params) {
					if (params.hasOwnProperty(i) && params[i]) { // do not set empty attributes
						if (i === "text" && el.appendChild) {
							el.appendChild(document.createTextNode(params[i])); // create text node
						} else if (i === "html") {
							$(el).html(params[i]);
						} else if (i === "className") {
							el.className = params[i];
						} else {
							if (el.setAttribute) {
								el.setAttribute(i, params[i]);
							}
						}
					}
				}
				return el;

			}
		},
		createRadioElement: function (params) {

			var radioHtml, radioFragment, params_required = {name: "string"};

			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) {

			    radioHtml = "<input type='radio' name='" + params.name + "'";
			    if (params.checked) {
			        radioHtml += " checked='checked'";
			    }
			    radioHtml += "/>";

			    radioFragment = document.createElement('div');
			    radioFragment.innerHTML = radioHtml;

			    return radioFragment.firstChild;

			} else {
				return {};
			}
		}
	};
}());

///////////

Snapper.UI.SnapperObject = (function () { // generic template for all objects
	
	"use strict";
	
	var alias = "SnapperObject";
	
	return {
		addEvent: function (type, obj, use_capture) {

			if (obj.el.addEventListener) { // W3C
				this.addEvent = function (type, obj, use_capture) {
					obj.el.addEventListener(type, obj, use_capture);
				};
			} else if (obj.el.attachEvent) { // IE
				this.addEvent = function (type, obj, use_capture) {
					obj.el.attachEvent("on" + type, function (e) {
						obj.handleEvent(e);
					});
				};
			}

			this.addEvent(type, obj, use_capture);

		},
		removeEvent: function (type, obj, use_capture) {

			if (obj.el.removeEventListener) { // W3C
				this.removeEvent = function (type, obj, use_capture) {
					var capture = use_capture || false;
					obj.el.removeEventListener(type, obj, capture);
				};
			} else if (obj.el.detachEvent) { // IE
				this.removeEvent = function (type, obj, use_capture) {
					var capture = use_capture || false;
					obj.el.detachEvent("on" + type, obj, capture);
				};
			}

			this.removeEvent(type, obj, use_capture);
		},
		removeEvents: function (el) {
			if (!el) {
				Snapper.Log.message("SnapperObject.removeEvents(el): Object not found. el = " + el);
			}
			this.removeEvent("mouseover", this, false);
			this.removeEvent("mouseout", this, false);
			this.removeEvent("mousedown", this, false);
			this.removeEvent("mousemove", this, false);
			this.removeEvent("mouseup", this, false);
			this.removeEvent("click", this, false);
			this.removeEvent("dblclick", this, false);
			this.removeEvent("touchstart", this, false);
			this.removeEvent("touchmove", this, false);
			this.removeEvent("touchend", this, false);
		},
		handleEvent: function (e) {
			if (Modernizr.touch) {
				if (e.preventDefault) { 
				    e.preventDefault(); 
				} else {
				    e.returnValue = false;
				}
			}
			switch (e.type) {
				case "mouseover": 
					if (typeof this.beforeMouseOver === "function") {
						this.beforeMouseOver();
					}
					if (typeof this.mouseover === "function") {
						this.mouseover(e);
					}
					if (typeof this.afterMouseOver === "function") {
						this.afterMouseOver();
					}
				break;
				case "mouseout": 
					if (typeof this.beforeMouseOut === "function") {
						this.beforeMouseOut();
					}
					if (typeof this.mouseout === "function") {
						this.mouseout(e);
					}
					if (typeof this.afterMouseOut === "function") {
						this.afterMouseOut();
					}
				break;
				case "mousedown":	
					if (typeof this.beforeMouseDown === "function") {
						this.beforeMouseDown();
					}
					if (typeof this.mousedown === "function") {
						this.mousedown(e);
					}
					if (typeof this.afterMouseDown === "function") {
						this.afterMouseDown();
					}
				break;																		
				case "mousemove":
					if (typeof this.beforeMouseMove === "function") {
						this.beforeMouseMove();
					}
					if (typeof this.mousemove === "function") {
						this.mousemove(e);
					}
					if (typeof this.afterMouseMove === "function") {
						this.afterMouseMove();
					}
				break;
				case "mouseup": 
					if (typeof this.beforeMouseUp === "function") {
						this.beforeMouseUp();
					}
					if (typeof this.mouseup === "function") {
						this.mouseup(e);
					}
					if (typeof this.afterMouseUp === "function") {
						this.afterMouseUp();
					}
				break;
				case "click": 
					if (typeof this.beforeClick === "function") {
						this.beforeClick();
					}
					if (typeof this.click === "function") {
						this.click(e);
					}
					if (typeof this.afterClick === "function") {
						this.afterClick();
					}
				break;						
				case "dblclick":
					if (typeof this.beforeDblClick === "function") {
						this.beforeDblClick();
					}
					if (typeof this.dblclick === "function") {
						this.dblclick(e);
					}					
					if (typeof this.afterDblClick === "function") {
						this.afterDblClick();
					}
				break;
				case "touchstart": 
					if (typeof this.beforeTouchStart === "function") {
						this.beforeTouchStart();
					}
					if (typeof this.touchstart === "function") {
						this.touchstart(e);
					}
					if (typeof this.afterTouchStart === "function") {
						this.afterTouchStart();
					}
				break;
				case "touchmove": 
					if (typeof this.beforeTouchMove === "function") {
						this.beforeTouchMove();
					}
					if (typeof this.touchmove === "function") {
						this.touchmove(e);
					}
					if (typeof this.afterTouchMove === "function") {
						this.afterTouchMove();
					}
				break;
				case "touchend": 
					if (typeof this.beforeTouchEnd === "function") {
						this.beforeTouchEnd();
					}
					if (typeof this.touchend === "function") {
						this.touchend(e);
					}
					if (typeof this.afterTouchEnd === "function") {
						this.afterTouchEnd();
					}
				break;																								
			}
		}
	};
}());				


Snapper.UI.NavigationBar = (function () {
	
	"use strict";
		
	var alias = "NavigationBar",
		contents = Snapper.Collection.Contents,
		SnapperObject = Snapper.UI.SnapperObject;
	
	return {
		configure: function (params) {
		
			var i;
		
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}
			this.init(params);
		},
		init: function (params) {
		
			var params_required = {
				"id": "string",
				"el": "object"
			},
			i,
			me = this;
		
			for (i in params) { // add passed properties
				if (params.hasOwnProperty(i)) {
					this[i] = params[i];
				}
			}
		
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
				this.id = params.id;
				this.el = params.el;
				
				this.removeEvents(this.el);
				
				this.title = params.title || null;
				this.buttonLeft = params.buttonLeft || null;
				this.buttonRight = params.buttonRight || null;
				
				//
				
				this.updateTitle();
				
			}
		},
		updateTitle: function () {
			$(this.el).find(".navigation-bar-title").empty().text(this.title);
		},
		removeAllButtons: function () {
			$(this.el).find(".button").remove();
		},
		removeButton: function (params) {
			var params_required = {
				"type": "string"
			}, type;
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
				type = params.type;
				if (this[type]) {
					$(this.el).find("#" + this[type]).remove();
				}
			}
		},
		addButton: function (params) {
			
		}
	};
}());

Snapper.UI.ButtonNavigation = (function () {
	
	"use strict";
		
	var alias = "ButtonNavigation",
		contents = Snapper.Collection.Contents,
		SnapperObject = Snapper.UI.SnapperObject;
	
	return {
		configure: function (params) {
		
			var i;
		
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}
		
			this.init(params);
		},
		init: function (params) {
		
			var params_required = {
				"id": "string",
				"el": "object",
				"connectedViewController": "object",
				"connectedView": "object"
			},
			i,
			me = this;
		
			for (i in params) { // add passed properties
				if (params.hasOwnProperty(i)) {
					this[i] = params[i];
				}
			}
		
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
			
				this.id = params.id;
				this.el = params.el;
				this.connectedViewController = params.connectedViewController;
				this.connectedView = params.connectedView;
				
				// set bubbling to true for parent to receive event first; false for children to receive event first
				
				this.removeEvents(this.el);
		
				this.addEvent("mouseover", this, false);
				this.addEvent("mouseout", this, false);
				this.addEvent("click", this, false);

				this.addEvent("touchstart", this, false);
				this.addEvent("touchend", this, false);
				
				this.velocity = PVector.create(0, 0);
				this.location = params.initLoc || PVector.create(0, 0);
				this.lastLoc = params.initLoc || PVector.create(0, 0);

				Snapper.Loop.list.push(this); // add this obj to the loop
				
				if (typeof Snapper.Utils.makePublisher !== "undefined") {
					Snapper.Utils.makePublisher(this);
				}
				
			}
		},	
		render: function (params) {

			var params_required = {
				"id": "string",
				"className": "string"
			}, val, obj;

			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface

				obj = document.createElement("div");
				val = params.val || null;

				obj.id = params.id;
				obj.className = params.className;
				obj.appendChild(document.createTextNode(val));

				return obj;
			}
			return false;
		},
		mouseover: function (e) {
		},
		mouseout: function (e) {
		},
		click: function (e) {
			
			//console.log(this.connectedViewController.location.x);
			
			// if xAxis && x !== 0
			// call exit on controller
			// else 
			// call exit on view
			
			var button = this, w = $(window).width(), h = $(window).height(),
			contents,
			viewContentId = $(button.connectedViewController.el).find(".view-container").attr("id");
			
			if (button.connectedViewController.xAxis && button.connectedViewController.location.x !== 0) { // if view is already partially off screen
				return (function () {

					button.connectedViewController.exit({ // tells connected obj to exit

						"callback": function () { // passes a callback function

							//$(button.connectedViewController.el).find(".view-container").empty(); // remove any child elements from DOM element w class = "view-container"
							button.connectedView.render(); // render the connected view

							if (this.resetBounds) { // reset the bounds
								this.resetBounds({
									"location" : this.location
								});
							}

							this.target = PVector.create(0, 0); // send main view back to origin
							
							if (!Modernizr.touch && FloatyApp) {
								FloatyApp.reset(); // only use Floaty on non-touch devices
							}
							$(window).trigger("scroll");
						}
					});

				}());
				
			} else {
				
				// fade out buttons
				$(button.connectedViewController.el).find(".navigation-bar .button").removeClass("fadeIn").addClass("fadeOut");
				
				// fade out title
				$(button.connectedViewController.el).find(".navigation-bar .navigation-bar-title").remove();
				
				Snapper.App.Cover.Collection.Contents.coverView.xAxis = true;
				
				Snapper.App.Cover.Collection.Contents.coverView.exit({
					"callback": function () {
						button.connectedView.render({ // when rendered at w, 0 Android collapses the width
							"initLoc": PVector.create(0, 0) // when rendered at 0, 0 Android displays it correctly
						});
						if (this.resetBounds) { // reset the bounds
							this.resetBounds({
								"location" : this.location
							});
						}

						this.target = PVector.create(0, 0); // send main view back to origin
						
						
						if (!Modernizr.touch) {
							FloatyApp.reset(); // only use Floaty on non-touch devices
						}
						$(window).trigger("scroll");
					}
				});
				
			}

		},
		touchend: function (e) {
			if (this.velocity.mag() === 0) {
				this.click(e);
			}
		},
		update: function () {
			var offset = $(this.el).offset();
			this.location = PVector.create(offset.left, offset.top);
			this.velocity = Snapper.Utils.PVectorSub(this.location, this.lastLoc); // velocity is change in location
			this.lastLoc = PVector.create(this.location.x, this.location.y);
		}
	};
}());

Snapper.UI.ButtonBack = (function () {

	"use strict";
		
	var alias = "ButtonBack",
		contents = Snapper.Collection.Contents,
		SnapperObject = Snapper.UI.SnapperObject;
	
	return {
		configure: function (params) {
		
			var i;
		
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}
		
			this.init(params);
		},
		init: function (params) {
		
			var params_required = {
				"id": "string",
				"el": "object"
			},
			i,
			me = this;
		
			for (i in params) { // add passed properties
				if (params.hasOwnProperty(i)) {
					this[i] = params[i];
				}
			}
		
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
			
				this.id = params.id;
				this.el = params.el;
				
				// set bubbling to true for parent to receive event first; false for children to receive event first
				
				this.removeEvents(this.el);
				
				this.addEvent("mouseover", this, false);
				this.addEvent("mouseout", this, false);
				this.addEvent("click", this, false);
				this.addEvent("touchstart", this, false);
				this.addEvent("touchend", this, false);
				
				if (typeof Snapper.Utils.makePublisher !== "undefined") {
					Snapper.Utils.makePublisher(this);
				}
				
			}
		},
		render: function (params) {
			
			var params_required = {
				"id": "string",
				"className": "string"
			}, val, obj;
			
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
				
				obj = document.createElement("div");
				val = params.val || null;
				
				obj.id = params.id;
				obj.className = params.className;
				obj.appendChild(document.createTextNode(val));
				
				return obj;
			}
			return false;
		},
		mouseover: function (e) {
		},
		mouseout: function (e) {
		},
		click: function (e) {
			contents[this.connectedObjId].shiftViewStack();
			contents[this.connectedObjId].shiftNavigationBarStack();
		},
		touchstart: function (e) {
		},
		touchend: function (e) {
			this.click(e);
		}
	};
}());

Snapper.UI.Button = (function () {

	"use strict";
		
	var alias = "Button",
		contents = Snapper.Collection.Contents,
		SnapperObject = Snapper.UI.SnapperObject;
	
	return {
		configure: function (params) {
		
			var i;
		
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}
		
			this.init(params);
		},
		init: function (params) {
		
			var params_required = {
				"id": "string",
				"el": "object"
			},
			i,
			me = this;
		
			for (i in params) { // add passed properties
				if (params.hasOwnProperty(i)) {
					this[i] = params[i];
				}
			}
		
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
			
				this.id = params.id;
				this.el = params.el;
				
				// set bubbling to true for parent to receive event first; false for children to receive event first
				
				this.removeEvents(this.el);
		
				this.addEvent("mouseover", this, false);
				this.addEvent("mouseout", this, false);
				this.addEvent("click", this, false);
				this.addEvent("touchstart", this, false);
				this.addEvent("touchend", this, false);
				
				if (typeof Snapper.Utils.makePublisher !== "undefined") {
					Snapper.Utils.makePublisher(this);
				}
				
			}
		},
		mouseover: function (e) {
		},
		mouseout: function (e) {
		},
		click: function (e) {
			
		},
		touchstart: function (e) {
		},
		touchend: function (e) {
			this.mouseup(e);
		}
		
	};
}());

Snapper.UI.ViewController = (function () {

	"use strict";
		
	var alias = "ViewController",
		contents = Snapper.Collection.Contents,
		SnapperObject = Snapper.UI.SnapperObject;
		
	return {
		configure: function (params) {
		
			var i;
		
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}
		
			this.init(params);
		},
		init: function (params) {

			var me = this, w = $(Snapper.CONSTANTS.appWindow).width(), h = $(Snapper.CONSTANTS.appWindow).height(); // !! need to set an appWindow
		
			this.id = params.id; // required
			this.el = params.el; // required
						
			// set bubbling to true for parent to receive event first; false for children to receive event first

			this.removeEvents(this.el);

			this.addEvent("mouseover", this, false);
			this.addEvent("mouseout", this, false);
			this.addEvent("mousedown", this, false); 
			this.addEvent("mousemove", this, false);
			this.addEvent("mouseup", this, false);
			this.addEvent("click", this, false);
			this.addEvent("dblclick", this, false);
			this.addEvent("touchstart", this, false);
			this.addEvent("touchmove", this, false);
			this.addEvent("touchend", this, false);
		
			this.width = params.width || this.el.offsetWidth;
			this.height = params.height || this.el.offsetHeight;
			this.parent = params.parent || null;
			this.dockPadding = 75;
			this.initBounds = params.initBounds || [
				-(this.height - this.dockPadding),
				w - this.dockPadding,
				h - this.dockPadding,
				-(this.width - this.dockPadding)
			];
			this.bounds = params.initBounds || [
				-(this.height - this.dockPadding),
				w - this.dockPadding,
				h - this.dockPadding,
				-(this.width - this.dockPadding)
			];
			this.acceleration = PVector.create(0, 0);
			this.velocity = PVector.create(0, 0);
			this.offset = PVector.create(0, 0);
			this.initLoc = params.initLoc || PVector.create(0, 0);
			this.startLoc = params.initLoc || PVector.create(0, 0);
			this.location = params.initLoc || PVector.create(0, 0);
			this.lastLoc = params.initLoc || PVector.create(0, 0);
			this.pressedLoc = null;
			this.target = params.target || false;
			this.dir = PVector.create(0, 0);
			this.maxSpeed = params.maxSpeed || 100;
			this.friction = params.friction || 0.9;
			this.pullThreshold = params.pullThreshold || 70; // the min distance a docked object must be moved before it undocks
			this.isDocked = params.isDocked || false; // boolean; 
			this.isLocked = params.isLocked || false; // boolean; if isLocked, obj will not move on either axis
			this.xAxis = params.xAxis || false; // boolean; if true obj can move on the x-axis; required
			this.yAxis = params.yAxis || false; // boolean; if true obj can move on the y-axis; required
			this.dragThreshold = params.dragThreshold || 30; // the min distance obj the mouse/touch should drag before dragging object
			this.lastTouch = new Date().getTime(); // holds time of last touch for detecting double taps
			this.doubletapSensitivity = params.doubletapSensitivity || 200;
			this.dockedCallback = params.dockedCallback || null;
			this.isDocked = this.checkDocked();
			this.beforeUpdate = params.beforeUpdate || null;
			this.afterUpdate = params.afterUpdate || null;
			this.beforeDraw = params.beforeDraw || null;
			this.afterDraw = params.afterDraw || null;
			this.beforeMouseDown = params.beforeMouseDown || null;
			this.afterMouseDown = params.afterMouseDown || null;
			this.beforeMouseMove = params.beforeMouseMove || null;
			this.afterMouseMove = params.afterMouseMove || null;
			this.beforeMouseUp = params.beforeMouseUp || null;
			this.afterMouseUp = params.afterMouseUp || null;
			this.beforeTouchStart = params.beforeTouchStart || null;
			this.afterTouchStart = params.afterTouchStart || null;
			this.beforeTouchMove = params.beforeTouchMove || null;
			this.afterTouchMove = params.afterTouchMove || null;
			this.beforeTouchEnd = params.beforeTouchEnd || null;
			this.afterTouchEnd = params.afterTouchEnd || null;
			this.beforeSlide = params.beforeSlide || null;
			this.afterSlide = params.afterSlide || null;
							
			this.exitCallback = params.exitCallback || function () {};
			this.resetBounds = params.resetBounds || function () {};
		
			Snapper.Loop.list.push(this); // add this obj to the loop
			
			if (typeof Snapper.Utils.makePublisher !== "undefined") {
				Snapper.Utils.makePublisher(this);
				if (typeof Snapper.setKeyControlStack !== "undefined") {
					this.subscribe("Focus", Snapper.setKeyControlStack, this);
					this.publish("Focus", this); // this panel has focus
					this.subscribe("Bounds", Snapper.Bounds, this);
				}
			}
		
			return this;

		},
		mouseover: function (e) {
			//this.startLoc = PVector.create(this.location.x, this.location.y);
			//this.isDocked = this.checkDocked(); // check if object is currently docked; if so, mouseup will check if obj has moved far enough to undock
		},
		mouseout: function (e) {
			this.mouseup(e);
		},
		mousedown: function (e) {

			var pageX, pageY;
		
			if (e.pageX) {
				pageX = e.pageX;
			} else if (e.clientX) {
				pageX = e.clientX;
			} else if (e.changedTouches) {
				pageX = e.changedTouches[0].pageX;
			}

			if (e.pageY) {
				pageY = e.pageY;
			} else if (e.clientY) {
				pageY = e.clientY;
			} else if (e.changedTouches) {
				pageY = e.changedTouches[0].pageY;
			}

			this.velocity = PVector.create(0, 0); // stop the object
			this.offset = PVector.create(pageX - this.location.x, pageY - this.location.y); // set the offset on mousedown; should not change until mouseup
			this.startLoc = PVector.create(this.location.x, this.location.y);
			this.pressedLoc = PVector.create(pageX, pageY);
			this.isPressed = true;
			this.target = null;
			this.isDocked = this.checkDocked(); // check if object is currently docked; if so, mouseup will check if obj has moved far enough to undock
		
			this.publish("Focus", this); // this panel has focus
		
		},
		mousemove: function (e) {
		
		},
		mouseup: function (e) {
			this.isPressed = false;
			this.pressedLoc = null;
		},
		click: function (e) {
			
			var v;
			
			this.isPressed = false;
			this.pressedLoc = null;
			
			// single tap or click should send obj back to its initial position
			if (this.checkDocked()) { // only if object is docked
				
				v = Snapper.Utils.PVectorSub(this.location, this.startLoc); // check difference bw startLoc and current location
				
				if ((this.xAxis && this.location.x !== 0) || (this.yAxis && this.location.y !== 0)) { // if obj is not in its intial position
					if (v.mag() === 0) { // if vector has no magnitude, was a single click or tap; not a drag
						this.target = PVector.create(this.initLoc.x, this.initLoc.y); // return to initial position
					} else if (v.mag() < this.pullThreshold) { // if the difference is less than the threshold, should snap back
						this.target = PVector.create(this.location.x, this.location.y); // create vector for current location
						this.target[this.isDocked[0]] = this.isDocked[1]; // update the axis that broke the bounds
					}
				}
			}
			this.publish("Unlock", this.el.id); // unlocks any parents or children
		},
		dblclick: function (e) {},
		touchstart: function (e) {
			var t = new Date().getTime();
			if (t - this.lastTouch > this.doubletapSensitivity) {
				this.mousedown(e);
				this.lastTouch = new Date().getTime();
			} else {
				this.doubletap(e);
			}	
		},
		touchmove: function (e) {
			
			// all lock calls should be made here

			var pageX,
				pageY,
				bounds,
				x = this.initLoc.x,
				y = this.initLoc.y,
				curPressedLoc,
				dir;
		
			if (this.isLocked) { // if obj is locked, return false immediately
				return false;
			}
		
			if (e.pageX) {
				pageX = e.pageX;
			} else if (e.clientX) {
				pageX = e.clientX;
			} else if (e.changedTouches) {
				pageX = e.changedTouches[0].pageX;
			}

			if (e.pageY) {
				pageY = e.pageY;
			} else if (e.clientY) {
				pageY = e.clientY;
			} else if (e.changedTouches) {
				pageY = e.changedTouches[0].pageY;
			}

			if (typeof pageX === "undefined") {
				pageX = 0;
			}

			if (typeof pageY === "undefined") {
				pageY = 0;
			}
		
			dir = PVector.create(0, 0);
		
			if (this.pressedLoc) { // find the dir vector bw the start of the press and the current location
				curPressedLoc = PVector.create(pageX, pageY); // get the current loc
				if (this.xAxis) { // if axis is restricted, set it equal to the pressedLoc
					curPressedLoc.y = this.pressedLoc.y;
				}
				if (this.yAxis) {
					curPressedLoc.x = this.pressedLoc.x;
				} 
				dir = Snapper.Utils.PVectorSub(curPressedLoc, this.pressedLoc); // if axis is restricted, dir mag should equal 0
			}

			if (this.isPressed && dir.mag() > this.dragThreshold) {
		
				if (this.xAxis) {
					x = pageX - this.offset.x;
				}
				if (this.yAxis) {
					y = pageY - this.offset.y;
				}
			
				this.location = PVector.create(x, y); // user is dragging obj
									
				bounds = this.checkBounds();
				if (bounds) { // user is dragging a docked object past the bounds
					this.location[bounds[0]] = bounds[1] + ((this.location[bounds[0]] - bounds[1]) * 0.5); // obj should drag half the intended distance
				}
			
				this.publish("Lock", this.id); // lock any parents or children
		
			}
		},
		touchend: function (e) {
			this.click(e);
		},
		singletap: function (e) {
		
		},
		doubletap: function (e) {
			if (this.checkDocked()) {
				if (this.location.x !== 0) {
					this.target = PVector.create(0,0);
				}
			}
		},
		slide: function (type) { // !! rename this function
		
			if (typeof this.beforeSlide === "function") {
				this.beforeSlide();
			}
			if (!this.target) {
				this.target = null; // clear target
				this.isDocked = this.checkDocked(); // check if object is currently docked
				switch (type) {
					case "up":
						this.acceleration = PVector.create(0, 1);
					break;
					case "right":
						this.acceleration = PVector.create(1, 0);
					break;
					case "down":
						this.acceleration = PVector.create(0, -1);
					break;
					case "left":
						this.acceleration = PVector.create(-1, 0);
					break;
					default: // an empty direction resets acceleration
						this.acceleration = PVector.create(0, 0);
					break;						
				}
			}
			if (typeof this.afterSlide === "function") {
				this.afterSlide();
			}
		},
		exit: function (params) {
		
			var params_required = {
				"callback": "function"
			}, callback, w = $(window).width(), h = $(window).height();
			
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, "Snapper.UI.ViewController.exit")) { // interface
				
				callback = params.callback || null;
				
				if (this.xAxis) {
					if (this.location.x > 0) {
						this.bounds = [ 
							this.initBounds[0],
							w + this.width,
							this.initBounds[2],
							w
						];
					} else {
						this.bounds = [ 
							this.initBounds[0],
							-w,
							this.initBounds[2],
							-w - this.width
						];							
					}
				}
				
			/*	if (this.yAxis) {
					if (this.location.y > 0) {
						this.bounds = [ 
							h + this.height,
							this.initBounds[1],
							h,
							this.initBounds[3]
						];
					} else {
						this.bounds = [ 
							-h - this.height,
							this.initBounds[1],
							-h,
							this.initBounds[3]
						];							
					}
				}*/
		
				if (typeof callback === "function") {
					this.exitCallback = params.callback;
				}
			
			}

		},
		back: function (params) {
		
			this.target = PVector.create(-this.width - this.width/4, this.location.y);

			this.exitCallback = function () {
				$("#main").find(".content-main").empty().append(Snapper.ViewStack.pop());
				this.target = PVector.create(0, 0);
			};
		
		},
		lock: function (id) {
			//if (this.id !== id) {
				this.isLocked = true;
				this.acceleration = PVector.create(0,0);
				//this.velocity = PVector.create(0,0);
			//}
		},
		unlock: function (id) {
			this.isLocked = false;
			//contents[id].isLocked = false;
		},
		stop: function () { // set acceleration to 0
			this.acceleration = PVector.create(0,0);
		},
		update: function () {

			var v,
				bounds;
		
			if (typeof this.beforeUpdate === "function") {
				this.beforeUpdate();
			}
							
			if (this.isPressed) {

				v = Snapper.Utils.PVectorSub(this.location, this.lastLoc); // velocity is change in location

				if (v.mag() > 0) { // never assign 0 velocity while object is pressed
					this.velocity.x = v.x; 
					this.velocity.y = v.y;
				}

			} else {

				if (!this.target) {
											
					bounds = this.checkBounds();

					if (bounds) {	
						this.target = PVector.create(this.location.x, this.location.y); // create vector for current location
						this.target[bounds[0]] = bounds[1]; // update the axis that broke the bounds	
					}

					if (this.acceleration.mag() > 0) { // if obj has acceleration
						this.velocity.add(this.acceleration); // add to velocity; do not apply friction
					} else {
						this.velocity.mult(this.friction); // if no acceleration; apply friction
					}
					this.velocity.limit(this.maxSpeed);
					this.location.add(this.velocity);

				} else {

					this.velocity = Snapper.Utils.PVectorSub(this.target, this.location); // difference bw target and current location = velocity	
					this.velocity.mult(0.25); // reduce velocity; !! use var here
					this.location.add(this.velocity);
				
					if (this.velocity.mag() < 0.5) {
						this.location = this.target;
						this.acceleration = PVector.create(0, 0); // reset acceleration
						this.velocity = PVector.create(0, 0); // reset velocity
						this.target = null; // reset target
						if (typeof this.exitCallback === "function") {
							this.exitCallback();
							this.exitCallback = null;
						}
					}
				}
			}

			this.lastLoc = PVector.create(this.location.x, this.location.y);
		
			if (typeof this.afterUpdate === "function") {
				this.afterUpdate();
			}
		},
		draw: function () {
			var x = this.initLoc.x,
				y = this.initLoc.y;

			if (typeof this.beforeDraw === "function") {
				this.beforeDraw();
			}
			if (Modernizr.csstransforms3d) { //  && Modernizr.touch
				if (this.xAxis) {
					x = this.location.x;
				}
				if (this.yAxis) {
					y = this.location.y;
				}	
				this.el.style.webkitTransform = 'translate(' + x + 'px, ' + y + 'px)';	
				this.el.style.MozTransform = 'translate(' + x + 'px, ' + y + 'px)';		
				this.el.style.OTransform = 'translate(' + x + 'px, ' + y + 'px)';
				//this.el.style.zIndex = 100;					
			} else {
				if (this.xAxis) {
					x = this.location.x;
				}
				if (this.yAxis) {
					y = this.location.y;
				}
				$(this.el).css({
					"left": x,
					"top": y
				});
			}
			if (typeof this.afterDraw === "function") {
				this.afterDraw();
			}
		},
		checkBounds: function () {
		
			var top = this.bounds[0],
				right = this.bounds[1],
				bottom = this.bounds[2],
				left = this.bounds[3];
					
			if (this.location.y < top && this.yAxis) {
				this.publish("Bounds", this.el.id);
				return ["y", top];
			}
			if (this.location.x > right && this.xAxis) {
				this.publish("Bounds", this.el.id);
				return ["x", right];
			}
			if (this.location.y > bottom && this.yAxis) {
				this.publish("Bounds", this.el.id);
				return ["y", bottom];
			}
			if (this.location.x < left && this.xAxis) {
				this.publish("Bounds", this.el.id);
				return ["x", left];
			}

			return false;
		},
		checkDocked: function () {

			var top = this.bounds[0],
				right = this.bounds[1],
				bottom = this.bounds[2],
				left = this.bounds[3];

			if (this.velocity.mag() < 1) { // obj is docked only if velocity < 1
				if (Math.abs(this.location.y - top) < 1 && this.yAxis) {
					if (this.dockedCallback) {
						this.dockedCallback(["y", top]);
					}
					return ["y", top];
				}
				if (Math.abs(this.location.x - right) < 1 && this.xAxis) {
					if (this.dockedCallback) {
						this.dockedCallback(["x", right]);
					}
					return ["x", right];
				}
				if (Math.abs(this.location.y - bottom) < 1 && this.yAxis) {
					if (this.dockedCallback) {
						this.dockedCallback(["y", bottom]);
					}
					return ["y", bottom];
				}
				if (Math.abs(this.location.x - left) < 1 && this.xAxis) {
					if (this.dockedCallback) {
						this.dockedCallback(["x", left]);
					}
					return ["x", left];
				}
			}
			return false;
		}
	};
}());

Snapper.UI.ScrollView = (function () {

	"use strict";
		
	var alias = "ScrollView",
	SnapperObject = Snapper.UI.SnapperObject,
	ViewController = Snapper.UI.ViewController;
	
	return {
		configure: function (params) { 
			var i;
	
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}

			for (i in ViewController) { // inherit from ViewController
				if (ViewController.hasOwnProperty(i)) {
					this[i] = ViewController[i];
				}
			}
			this.init(params);
		},
		render: function (params) {
			var params_required = {
				"id": "string",
				"className": "string"
			}, obj;
		
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
			
				obj = document.createElement("div");
			
				obj.id = params.id;
				obj.className = params.className;
			
				return obj;
			}
			return false;
		}
	};
}());

Snapper.UI.ScrollViewHandle = (function () {

	"use strict";
		
	var alias = "ScrollViewHandle",
	SnapperObject = Snapper.UI.SnapperObject;
	
	return {
		configure: function (params) { 
			var i;
	
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}

			this.init(params);
		},
		init: function (params) {
			
			var me = this, w = $(Snapper.CONSTANTS.appWindow).width(), windowHeight = $(window).height(), h = Snapper.CONSTANTS.APP_HEIGHT;
		
			this.id = params.id; // required
			this.el = params.el; // required
			this.parent = params.parent; // required
			
			this.width = params.width || this.el.offsetWidth;
			this.height = params.height || this.el.offsetHeight;
			this.initLoc = params.initLoc || PVector.create(0, 0);
			this.lastLoc = params.initLoc || PVector.create(0, 0);
			this.location = params.initLoc || PVector.create(0, 0);
			this.velocity = PVector.create(0, 0);
			this.xAxis = false;
			this.yAxis = true;
			
			if (Snapper.CONSTANTS.APP_HEIGHT > windowHeight) {
				h = windowHeight;
			}
			if (this.parent.height > h) {
				this.height = Snapper.Utils.map(h, 10, this.parent.height, 10, h - this.initLoc.y);
			} else { // if text is to short to be scrolled; hide handle
				this.height = 0;
				$(this.el).addClass("disabled");
			}
			$(this.el).css({
				"height": this.height
			});
							
			Snapper.Loop.list.push(this); // add this obj to the loop

		},
		render: function (params) {
			var params_required = {
				"id": "string",
				"className": "string"
			}, obj;
		
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
			
				obj = document.createElement("div");
			
				obj.id = params.id;
				obj.className = params.className;
			
				return obj;
			}
			return false;
		},
		update: function () {			
			
			var h = Snapper.CONSTANTS.APP_HEIGHT, windowHeight = $(window).height();

			if (Snapper.CONSTANTS.APP_HEIGHT > windowHeight) {
				h = windowHeight;
			}
			// !! should not hardcode the 44 value		
			this.location.y = Snapper.Utils.map(this.parent.location.y, this.parent.initBounds[2], this.parent.initBounds[0], 44, h - this.height); //Snapper.Utils.map(value, min1, max1, min2, max2);
			
			this.velocity = Snapper.Utils.PVectorSub(this.location, this.lastLoc);
			if (this.velocity.mag() < 0.1 && this.location.y >= 44 && this.location.y <= h - this.height) {
				$(this.el).removeClass("enabled");
			} else {
				$(this.el).addClass("enabled");
			}
			this.lastLoc = PVector.create(this.location.x, this.location.y);
			
			
		},
		draw: function () {
			var y = this.initLoc.y;

			if (typeof this.beforeDraw === "function") {
				this.beforeDraw();
			}
			if (Modernizr.csstransforms3d) { //  && Modernizr.touch
				if (this.yAxis) {
					y = this.location.y;
				}	
				this.el.style.webkitTransform = 'translateY(' + y + 'px)';	
				this.el.style.MozTransform = 'translateY(' + y + 'px)';		
				this.el.style.OTransform = 'translateY(' + y + 'px)';					
			} else {
				if (this.yAxis) {
					y = this.location.y;
				}
				$(this.el).css({
					"top": y
				});
			}
			if (typeof this.afterDraw === "function") {
				this.afterDraw();
			}
		}
	};
}());

Snapper.UI.TableView = (function () {

	"use strict";
		
	var alias = "TableView",
	SnapperObject = Snapper.UI.SnapperObject,
	ViewController = Snapper.UI.ViewController;
	
	return {
		configure: function (params) { // inherit from ViewController
			
			var i;
	
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}
		
			for (i in ViewController) {
				if (ViewController.hasOwnProperty(i)) {
					this[i] = ViewController[i];
				}
			}
			this.init(params);
		}
	};
}());

Snapper.UI.TableCell = (function () {

	"use strict";
		
	var alias = "TableCell",
	SnapperObject = Snapper.UI.SnapperObject,
	ViewController = Snapper.UI.ViewController;
	
	return {
		configure: function (params) {
		
			var i;
		
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}
			this.init(params);
		}
	};
}());

Snapper.UI.TextField = (function () {

	"use strict";
		
	var alias = "TextField",
	SnapperObject = Snapper.UI.SnapperObject;	
	return {
		configure: function (params) {
		
			var i;
		
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}
			this.init(params);
		},
		render: function (params) {
			
			var params_required = {
				"id": "string",
				"className": "string"
			}, val, obj, hasIcon, iconSrc, img, txt;
			
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
				
				obj = document.createElement("div");
				val = params.val || null;
				hasIcon = params.hasIcon || null;
				iconSrc = params.iconSrc || null;
				
				
				obj.id = params.id;
				obj.className = params.className;
				
				if (hasIcon && iconSrc) {
					img = document.createElement("img");
					img.src = iconSrc;
					img.className = "text-field-icon";
					obj.appendChild(img);
				}
				if (val) {
					txt = document.createTextNode(val);
					obj.appendChild(txt);
				}
			
				
				return obj;
			}
			return false;
		}
	};
}());

Snapper.UI.ActivityView = (function () {

	"use strict";
		
	var alias = "ActivityView",
	SnapperObject = Snapper.UI.SnapperObject,
	ViewController = Snapper.UI.ViewController;
	
	return {
		configure: function (params) { // inherit from ViewController
			
			var i;
	
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}
			
			for (i in ViewController) { // inherit from ViewController
				if (ViewController.hasOwnProperty(i)) {
					this[i] = ViewController[i];
				}
			}
			this.init(params);
		},
		render: function (params) {
			var params_required = {
				"id": "string",
				"className": "string"
			}, obj, spinner, i;
		
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
				
				obj = document.createElement("div");
			
				obj.id = params.id;
				obj.className = params.className;
				
				for (i = 0; i < 12; i += 1) {
					spinner = document.createElement("div"); 
					spinner.className = "bar" + (i + 1);
					obj.appendChild(spinner);
				}
			
				return obj;
			}
			return false;
		}
	};
}());

Snapper.UI.WebLink = (function () {

	"use strict";
		
	var alias = "WebLink",
	SnapperObject = Snapper.UI.SnapperObject;	
	return {
		configure: function (params) {
		
			var i;
		
			for (i in SnapperObject) { // inherit from SnapperObject
				if (SnapperObject.hasOwnProperty(i)) {
					this[i] = SnapperObject[i];
				}
			}
			this.init(params);
		},
		init: function (params) {
			
			var params_required = {
				"id": "string",
				"el": "object",
				"parentView": "object"
			};
			
			if (Snapper.Interface.checkRequiredParams(params, params_required, true, alias)) { // interface
				
				this.id = params.id; // required
				this.el = params.el; // required
				this.parentView = params.parentView; // required
				this.openNewWindow = params.openNewWindow || false;
					
				// set bubbling to true for parent to receive event first; false for children to receive event first

				this.removeEvents(this.el);
			
				this.addEvent("click", this, false);
				this.addEvent("touchend", this, false);
			
				this.velocity = PVector.create(0, 0);
				this.location = params.initLoc || PVector.create(0, 0);
				this.lastLoc = params.initLoc || PVector.create(0, 0);
			
				Snapper.Loop.list.push(this); // add this obj to the loop
		
				return this;
			
			}
		},
		click: function (e) {
			if (this.openNewWindow) {
				window.open($(this.el).attr("rel"));
			} else {
				window.location.href = $(this.el).attr("rel");
			}
		},
		touchend: function (e) {
			if (this.velocity.mag() === 0 && this.parentView.velocity.mag() === 0) {
				this.click(e);
			}
		},
		update: function () {
			var offset = $(this.el).offset();
			this.location = PVector.create(offset.left, offset.top);
			this.velocity = Snapper.Utils.PVectorSub(this.location, this.lastLoc); // velocity is change in location
			this.lastLoc = PVector.create(this.location.x, this.location.y);
		}
	};
}());

