/**
 * The following line of code is the compact form of XRegExp, a library that
 * adds named group captures (and some other features) to regexs, taken from:
 * 		http://blog.stevenlevithan.com/archives/xregexp-named-capture
 * Full "documentation" on this library can be found at the above URL, but here
 * is a usage example:
 * var urlParser = new XRegExp(
 * 		"^(<protocol>[^:/?]+)://(<host>[^/?]*)(<path>[^?]*)\\?(<query>.*)", "k"
 * );
 * var parts = urlParser.exec("http://microsoft.com/pathto/file?q=1");
 * // parts.protocol == "http";
 * // parts.host == "microsoft.com";
 * // etc.
 * 
 * XRegExp 0.2.2; MIT License
 * By Steven Levithan <http://stevenlevithan.com>
 * ----------
 * Adds support for the following regular expression features:
 * - Free-spacing and comments ("x" flag)
 * - Dot matches all ("s" flag)
 * - Named capture ("k" flag)
 * - Capture: (<name>...)
 * - Backreference: \k<name>
 * - In replacement: ${name}
 * - Stored at: result.name
 */
eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('4(K.t===17){6 t;(5(){6 j={q:q,C:q.p.C,A:z.p.A,7:z.p.7};t=5(a,b){3 j.q(a).P(b)};q.p.P=5(e){6 f=8.13,E=12,y=t.H;e=(e||"")+j.7.m(8.G(),/^[\\S\\s]+\\//,"");4(e.v("x")>-1){f=j.7.m(f,y.R,5(a,b,c){3 b?(c?c:"(?:)"):a})}4(e.v("k")>-1){6 g=[];f=j.7.m(f,y.O,5(a,b){4(/^\\((?!\\?)/.18(a)){4(b)E=16;g.15(b||14);3"("}B{3 a}});4(E){f=j.7.m(f,y.N,5(a,b,c){6 d=b?g.v(b):-1;3 d>-1?"\\\\"+(d+1).G()+(c?"(?:)"+c:""):a})}}f=j.7.m(f,y.M,5(a,b){3 b?j.7.m(a,/^(\\[\\^?)]/,"$1\\\\]"):a});4(e.v("s")>-1){f=j.7.m(f,y.L,5(a){3 a==="."?"[\\\\S\\\\s]":a})}6 h=j.q(f,j.7.m(e,/[11]+/g,""));4(E){h.l=g}B 4(8.l){h.l=8.l.Z()}3 h};z.p.7=5(g,h){4(!(g Y j.q&&g.l)){3 j.7.J(8,u)}4(X h==="5"){3 j.7.m(8,g,5(){u[0]=W z(u[0]);F(6 i=0;i<g.l.o;i++){4(g.l[i])u[0][g.l[i]]=u[i+1]}3 h.J(K,u)})}B{3 j.7.m(8,g,5(){6 f=u;3 j.7.m(h,t.H.I,5(a,b,c){4(b){V(b){D"$":3"$";D"&":3 f[0];D"`":3 f[f.o-1].U(0,f[f.o-2]);D"\'":3 f[f.o-1].U(f[f.o-2]+f[0].o);1d:6 d="";b=+b;1c(b>g.l.o){d=b.G().A(/\\d$/)[0]+d;b=T.1b(b/10)}3(b?f[b]:"$")+d}}B 4(c){6 e=g.l.v(c);3 e>-1?f[e+1]:a}B{3 a}})})}};q.p.C=5(a){6 b=j.C.m(8,a);4(!(8.l&&b&&b.o>1))3 b;F(6 i=1;i<b.o;i++){6 c=8.l[i-1];4(c)b[c]=b[i]}3 b};z.p.A=5(a){4(!a.l||a.1a)3 j.A.m(8,a);3 a.C(8)}})()}t.H={R:/(?:[^[#\\s\\\\]+|\\\\(?:[\\S\\s]|$)|\\[\\^?]?(?:[^\\\\\\]]+|\\\\(?:[\\S\\s]|$))*]?)+|(\\s*#[^\\n\\r]*\\s*|\\s+)([?*+]|{\\d+(?:,\\d*)?})?/g,L:/(?:[^[\\\\.]+|\\\\(?:[\\S\\s]|$)|\\[\\^?]?(?:[^\\\\\\]]+|\\\\(?:[\\S\\s]|$))*]?)+|\\./g,M:/(?:[^\\\\[]+|\\\\(?:[\\S\\s]|$))+|\\[\\^?(]?)(?:[^\\\\\\]]+|\\\\(?:[\\S\\s]|$))*]?/g,O:/(?:[^[(\\\\]+|\\\\(?:[\\S\\s]|$)|\\[\\^?]?(?:[^\\\\\\]]+|\\\\(?:[\\S\\s]|$))*]?|\\((?=\\?))+|\\((?:<([$\\w]+)>)?/g,N:/(?:[^\\\\[]+|\\\\(?:[^k]|$)|\\[\\^?]?(?:[^\\\\\\]]+|\\\\(?:[\\S\\s]|$))*]?|\\\\k(?!<[$\\w]+>))+|\\\\k<([$\\w]+)>(\\d*)/g,I:/(?:[^$]+|\\$(?![1-9$&`\']|{[$\\w]+}))+|\\$(?:([1-9]\\d*|[$&`\'])|{([$\\w]+)})/g};t.19=5(){q=t};Q.p.v=Q.p.v||5(a,b){6 c=8.o;F(6 i=(b<0)?T.1e(0,c+b):b||0;i<c;i++){4(8[i]===a)3 i}3-1};',62,77,'|||return|if|function|var|replace|this|||||||||||||_captureNames|call||length|prototype|RegExp|||XRegExp|arguments|indexOf|||re|String|match|else|exec|case|useNamedCapture|for|toString|_re|replacementVariable|apply|window|singleline|characterClass|namedBackreference|capturingGroup|addFlags|Array|extended||Math|substring|switch|new|typeof|instanceof|valueOf||sxk|false|source|null|push|true|undefined|test|overrideNative|global|floor|while|default|max'.split('|'),0,{}))


// NOTE: This version changes the named group syntax from <namedGroup> to 
// ?<namedGroup>, and removes the need for the k flag (to do named group
// captures) so js_cardReader.js has to be revised before it can be uncommented
// 
//XRegExp 0.5.2, <stevenlevithan.com>, MIT License
//if(!window.XRegExp){(function(){var D={exec:RegExp.prototype.exec,match:String.prototype.match,replace:String.prototype.replace,split:String.prototype.split},C={part:/(?:[^\\([#\s.]+|\\(?!k<[\w$]+>)[\S\s]?|\((?=\?(?!#|<[\w$]+>)))+|(\()(?:\?(?:(#)[^)]*\)|<([$\w]+)>))?|\\k<([\w$]+)>|(\[\^?)|([\S\s])/g,replaceVar:/(?:[^$]+|\$(?![1-9$&`']|{[$\w]+}))+|\$(?:([1-9]\d*|[$&`'])|{([$\w]+)})/g,extended:/^(?:\s+|#.*)+/,quantifier:/^(?:[?*+]|{\d+(?:,\d*)?})/,classLeft:/&&\[\^?/g,classRight:/]/g},A=function(H,F,G){for(var E=G||0;E<H.length;E++){if(H[E]===F){return E}}return -1},B=/()??/.exec("")[1]!==undefined;XRegExp=function(N,H){if(N instanceof RegExp){if(H!==undefined){throw TypeError("can't supply flags when constructing one RegExp from another")}return N.addFlags()}var H=H||"",E=H.indexOf("s")>-1,J=H.indexOf("x")>-1,O=false,Q=[],G=[],F=C.part,K,I,M,L,P;F.lastIndex=0;while(K=D.exec.call(F,N)){if(K[2]){if(!C.quantifier.test(N.slice(F.lastIndex))){G.push("(?:)")}}else{if(K[1]){Q.push(K[3]||null);if(K[3]){O=true}G.push("(")}else{if(K[4]){L=A(Q,K[4]);G.push(L>-1?"\\"+(L+1)+(isNaN(N.charAt(F.lastIndex))?"":"(?:)"):K[0])}else{if(K[5]){if(N.charAt(F.lastIndex)==="]"){G.push(K[5]==="["?"(?!)":"[\\S\\s]");F.lastIndex++}else{I=XRegExp.matchRecursive("&&"+N.slice(K.index),C.classLeft,C.classRight,"",{escapeChar:"\\"})[0];G.push(K[5]+I+"]");F.lastIndex+=I.length+1}}else{if(K[6]){if(E&&K[6]==="."){G.push("[\\S\\s]")}else{if(J&&C.extended.test(K[6])){M=D.exec.call(C.extended,N.slice(F.lastIndex-1))[0].length;if(!C.quantifier.test(N.slice(F.lastIndex-1+M))){G.push("(?:)")}F.lastIndex+=M-1}else{G.push(K[6])}}}else{G.push(K[0])}}}}}}P=RegExp(G.join(""),D.replace.call(H,/[sx]+/g,""));P._x={source:N,captureNames:O?Q:null};return P};RegExp.prototype.exec=function(I){var G=D.exec.call(this,I),F,H,E;if(G){if(B&&G.length>1){E=new RegExp("^"+this.source+"$(?!\\s)",this.getNativeFlags());D.replace.call(G[0],E,function(){for(H=1;H<arguments.length-2;H++){if(arguments[H]===undefined){G[H]=undefined}}})}if(this._x&&this._x.captureNames){for(H=1;H<G.length;H++){F=this._x.captureNames[H-1];if(F){G[F]=G[H]}}}if(this.global&&this.lastIndex>(G.index+G[0].length)){this.lastIndex--}}return G};String.prototype.match=function(E){if(!(E instanceof RegExp)){E=new XRegExp(E)}if(E.global){return D.match.call(this,E)}return E.exec(this)};String.prototype.replace=function(F,G){var E=(F._x||{}).captureNames;if(!(F instanceof RegExp&&E)){return D.replace.apply(this,arguments)}if(typeof G==="function"){return D.replace.call(this,F,function(){arguments[0]=new String(arguments[0]);for(var H=0;H<E.length;H++){if(E[H]){arguments[0][E[H]]=arguments[H+1]}}return G.apply(window,arguments)})}else{return D.replace.call(this,F,function(){var H=arguments;return D.replace.call(G,C.replaceVar,function(J,I,M){if(I){switch(I){case"$":return"$";case"&":return H[0];case"`":return H[H.length-1].slice(0,H[H.length-2]);case"'":return H[H.length-1].slice(H[H.length-2]+H[0].length);default:var K="";I=+I;while(I>E.length){K=D.split.call(I,"").pop()+K;I=Math.floor(I/10)}return(I?H[I]:"$")+K}}else{if(M){var L=A(E,M);return L>-1?H[L+1]:J}else{return J}}})})}};String.prototype.split=function(J,F){if(!(J instanceof RegExp)){return D.split.apply(this,arguments)}var G=[],E=J.lastIndex,K=0,I=0,H;if(F===undefined||+F<0){F=false}else{F=Math.floor(+F);if(!F){return[]}}if(!J.global){J=J.addFlags("g")}else{J.lastIndex=0}while((!F||I++<=F)&&(H=J.exec(this))){if(J.lastIndex>K){G=G.concat(this.slice(K,H.index),(H.index===this.length?[]:H.slice(1)));K=J.lastIndex}if(!H[0].length){J.lastIndex++}}G=K===this.length?(J.test("")?G:G.concat("")):(F?G:G.concat(this.slice(K)));J.lastIndex=E;return G}})()}RegExp.prototype.getNativeFlags=function(){return(this.global?"g":"")+(this.ignoreCase?"i":"")+(this.multiline?"m":"")+(this.extended?"x":"")+(this.sticky?"y":"")};RegExp.prototype.addFlags=function(A){var B=new XRegExp(this.source,(A||"")+this.getNativeFlags());if(this._x){B._x={source:this._x.source,captureNames:this._x.captureNames?this._x.captureNames.slice(0):null}}return B};RegExp.prototype.call=function(A,B){return this.exec(B)};RegExp.prototype.apply=function(B,A){return this.exec(A[0])};XRegExp.cache=function(C,A){var B="/"+C+"/"+(A||"");return XRegExp.cache[B]||(XRegExp.cache[B]=new XRegExp(C,A))};XRegExp.escape=function(A){return A.replace(/[-[\]{}()*+?.\\^$|,#\s]/g,"\\$&")};XRegExp.matchRecursive=function(P,D,S,F,B){var B=B||{},V=B.escapeChar,K=B.valueNames,F=F||"",Q=F.indexOf("g")>-1,C=F.indexOf("i")>-1,H=F.indexOf("m")>-1,U=F.indexOf("y")>-1,F=F.replace(/y/g,""),D=D instanceof RegExp?(D.global?D:D.addFlags("g")):new XRegExp(D,"g"+F),S=S instanceof RegExp?(S.global?S:S.addFlags("g")):new XRegExp(S,"g"+F),I=[],A=0,J=0,N=0,L=0,M,E,O,R,G,T;if(V){if(V.length>1){throw SyntaxError("can't supply more than one escape character")}if(H){throw TypeError("can't supply escape character when using the multiline flag")}G=XRegExp.escape(V);T=new RegExp("^(?:"+G+"[\\S\\s]|(?:(?!"+D.source+"|"+S.source+")[^"+G+"])+)+",C?"i":"")}while(true){D.lastIndex=S.lastIndex=N+(V?(T.exec(P.slice(N))||[""])[0].length:0);O=D.exec(P);R=S.exec(P);if(O&&R){if(O.index<=R.index){R=null}else{O=null}}if(O||R){J=(O||R).index;N=(O?D:S).lastIndex}else{if(!A){break}}if(U&&!A&&J>L){break}if(O){if(!A++){M=J;E=N}}else{if(R&&A){if(!--A){if(K){if(K[0]&&M>L){I.push([K[0],P.slice(L,M),L,M])}if(K[1]){I.push([K[1],P.slice(M,E),M,E])}if(K[2]){I.push([K[2],P.slice(E,J),E,J])}if(K[3]){I.push([K[3],P.slice(J,N),J,N])}}else{I.push(P.slice(E,J))}L=N;if(!Q){break}}}else{D.lastIndex=S.lastIndex=0;throw Error("subject data contains unbalanced delimiters")}}if(J===N){N++}}if(Q&&!U&&K&&K[0]&&P.length>L){I.push([K[0],P.slice(L),L,P.length])}D.lastIndex=S.lastIndex=0;return I};




/**
 * The following two functions add memoizing capabilities to Javascript
 * functions (although only functions which have array/scalar-only arguments;
 * functions with arguments that are objects can't be memoized). Here is an
 * example of how to use this capability:
 *
 * OSM.util.someFunction = function(){
 * 		var a = 1; var b = 2; return a + b; }.memoize()
 * }.memoize();
 *
 * The function OSM.util.someFunction() can now be called as many times as is
 * necessary, but it will only calculate "1 + 2" the first time it is called;
 * every time after that it will return 3 without recalculating 
 *
 * This code was based on code found at:
 * http://talideon.com/weblog/2005/07/javascript-memoization.cfm 
 */
Function.prototype.memoize = function() {
	var pad  = {};
	var self = this;
	var obj  = arguments.length > 0 ? arguments[i] : null;
	var memoizedFn = function() {
		// Copy the arguments object into an array, to be used as a cache key.
        var args = [];
        for (var i = 0; i < arguments.length; i++) {
            args[i] = arguments[i];
        }
        // Evaluate the function, if it hasn't been evaluated (with these args).
        if(!(args in pad)) pad[args] = self.apply(obj, arguments);
        return pad[args];
    };
    memoizedFn.unmemoize = function() { return self; };
    return memoizedFn;
}
Function.prototype.unmemoize = function() {
    return console.log("Attempt to unmemoize an unmemoized function.");
}


/**
 * MethodProfiler Class
 * (Originally taken from code in "Pro Javascript Design Patterns" (chapter 12))
 * @author <a href"mailto:jeremy@on-site.com">Jeremy Walker</a>
 * 
 * The MethodProfiler class can be used to profile (ie. time) any JS method.  To
 * use it, simply create your object (the one with the method you want to
 * profile) as normal, then reassign it to be a new MethodProfiler, with the
 * original object as the only constructor argument.  From that point on,
 * anytime that object uses any of it's methods, the profiler will use
 * console.log to output the number of milliseconds the method took to perform. 
 * 
 * NOTE: This class can't be used to profile free-standing/class-less functions.
 * (However, you may be able to do something like this:
 * 		function myFreeStandingFunction() { return true }
 * 		var a = {"myFreeStandingFunction": myFreeStandingFunction};
 * 		myFreeStandingFunction = new MethodProfiler(a).myFreeStandingFunction;
 * I just haven't tested it myself - Jeremy)
 * 
 * Usage Example:
 * var list = new ListBuilder('list-container', 5000);
 * list = new MethodProfiler(list);
 * list.buildList('ol'); // Displays "buildList: 301 ms".
 * list.buildList('ul'); // Displays "buildList: 287 ms".
 * list.removeLists('ul'); // Displays "removeLists: 10 ms".
 * list.removeLists('ol'); // Displays "removeLists: 12 ms".
 * 
 * @see <a href="http://jsdesignpatterns.com/">Pro Javascript Design Patterns
 * Site</a>
 */
var MethodProfiler = function(component) {
  this.component = component;
  this.timers = {};

  for(var key in this.component) {
    // Ensure that the property is a function.
    if(typeof this.component[key] !== 'function') {
      continue;
    }

    // Add the method.
    var that = this;
    (function(methodName) {
      that[methodName] = function() {
        that.startTimer(methodName);
        var returnValue = that.component[methodName].apply(that.component, 
          arguments);
        that.displayTime(methodName, that.getElapsedTime(methodName));
        return returnValue;
      };
    })(key); }
};
/** */
MethodProfiler.prototype.startTimer = function(methodName) {
	this.timers[methodName] = (new Date()).getTime();
};
/** */
MethodProfiler.prototype.getElapsedTime = function(methodName) {
	return (new Date()).getTime() - this.timers[methodName];
};
/** */
MethodProfiler.prototype.displayTime = function(methodName, time) {
	console.log(methodName + ': ' + time + ' ms');
};
var littleWindow = 0;
function noPopUpOpenWindow(URL) {
	window.location = URL + (URL.indexOf("?") == -1 ? "?" : "&") + "popup=no";
}
function windowStr(height, width) {
	return 'resizable=yes,scrollbars=yes,width=' + (width ? width : 620) +
	',height=' + (height ? height:  345);
}
function popUpOpenWindow(URL, width, height) {
	littleWindow = window.open(URL, 'littleWindow', windowStr(width, height));
	littleWindow.focus();
}
var openWindow = window.noPopUps ? noPopUpOpenWindow : popUpOpenWindow;

function eLeft(eElement) {
    var nLeftPos = eElement.offsetLeft;
    var eParElement = eElement.offsetParent;
    while (eParElement != null) {
        nLeftPos += eParElement.offsetLeft;
        eParElement = eParElement.offsetParent;
    }
    return nLeftPos;                            
}

function eTop(eElement) {
    var nTopPos = eElement.offsetTop;
    var eParElement = eElement.offsetParent;
    while (eParElement != null) {                                           
        nTopPos += eParElement.offsetTop;
        eParElement = eParElement.offsetParent; 
    }
    return nTopPos;                              
}
var timeOut;
var inCount = 0;
var killTimeOut;
var openMenu;
var errorFound = false;
var openedTime;
try {
	document.onclick = documentClick;
} catch (e) {
	errorFound = true;
}
function max (x, y) { return (x > y) ? x : y; }
function min (x, y) { return (x < y) ? x : y; }
//** Remove this line due to conflicts with RichFaces Modal component!!!!
//function $(id){return document.getElementById(id);}

function rectIntersectsRect(left, top, right, bottom,
			    left2, top2, right2, bottom2) {
	var l = max (left, left2);
	var t = max (top, top2);
	var r = min (right, right2);
	var b = min (bottom, bottom2);

	if (l <= r && t <= b)
		return true;
	else
		return false;
}

function rectIntersectsMenu(left, top, right, bottom) {
	var curid = openMenu;
	while (curid && curid != '') {
		var e = document.getElementById(curid);

		var l = eLeft(e);
		var t = eTop(e);

		if (rectIntersectsRect(
			l, t, 
			e.offsetWidth + l, e.offsetHeight + t,
			left, top, right, bottom)) {
			return true;
		}

		curid = e.parentmenu;
	}
	return false;
}
function elementIntersectsPopup(e) {
	var l = eLeft(e);
	var t = eTop(e);

	try {
		if (rectIntersectsMenu(l, t, l + e.offsetWidth, t + e.offsetHeight))
			return true;
	} catch (e) { }

	try {
		if (rectIntersectsDateSelector(l, t, l + e.offsetWidth, t + e.offsetHeight)) {
			return true;
		}
	} catch (e) { }

	try {
		if (rectIntersectsRangeSelector(l, t, l + e.offsetWidth, t + e.offsetHeight)) {
			return true;
		}
	} catch (e) { }

	return false;
}
function pointIntersectsMenu(x, y) {
	return rectIntersectsMenu(x, y, x, y);
}
function documentClick(e) {
	var posx = 0;
	var posy = 0;

	if (document.all) {
		posx = event.clientX + document.body.scrollLeft;
		posy = event.clientY + document.body.scrollTop;		
	} else {
		posx = e.pageX;
		posy = e.pageY;
	}
	var showforms = true;

	if (!pointIntersectsMenu(posx, posy)) {
		hideAll('');
	} else {
		showforms = false;
	}
	try {
		if (!pointIntersectsDateSelector(posx, posy)) {
			hideDateSelector();
		} else {
			showforms = false;
		}
	} catch (e) {}
	try {
		if (!pointIntersectsRangeSelector(posx, posy)) {
			hideRangeSelector();
		} else {
			showforms = false;
		}
	} catch (e) {}

	if (showforms) showForms(true);
}

function showForms(show) {
	if (document.forms && document.all) {
		for (j = 0; j < document.forms.length; j++) {
			theElems = document.forms[j].elements;
			for (i = 0; i < theElems.length; i++) {
				if (theElems[i].type == 'select-one') {
					
					if (show) {
						if (theElems[i].dirty) {
							theElems[i].style.visibility = theElems[i].oldvisibility;
							theElems[i].dirty = false;
						}
					} else if (elementIntersectsPopup(theElems[i]) &&
						   theElems[i].style.visibility != 'hidden') {
						theElems[i].dirty = true;
						theElems[i].oldvisibility = theElems[i].style.visibility;
						theElems[i].style.visibility = 'hidden';
					}
				}
			}
		}
	}
}
function hideAll(id) {
	openMenu = null;
	if (document.idlist) {
		for (i = 0; i < document.idlist.length; i++) {
			var keep = false;
			var curid = id;
			while (curid && curid != '') {
				if (document.idlist[i] == curid) {
					keep = true;
					break;
				}
				curid = document.getElementById(curid).parentmenu;
			}
			if (!keep) {
				hideSub(document.idlist[i]);
			}
		}
	}
}
function hideSub(id) {
	if (document.getElementById(id) && document.getElementById(id).style.position == 'absolute') {
		document.getElementById(id).style.visibility = 'hidden';
		document.getElementById(id).style.display = 'none';
	}
}
function toggleSub(id, parentid, left, top) {
	if (document.getElementById(id)) {
		if (document.getElementById(id).style.visibility != 'hidden') {
			if ((new Date() - openedTime) > 250)
				hideAll(id);
		} else {
			showSub(id, parentid, left, top);
		}
	}
}
function showSub(id, parentid, left, top) {
	// hide any other popups
	try {
		hideDateSelector();
	} catch (e) {}
	try {
		hideRangeSelector();
	} catch (e) {}
	
	document.getElementById(id).parentmenu = parentid;
	hideAll(id);
	openMenu = id;
	var e = document.getElementById(id);
	var parent = document.getElementById(parentid);

	if (parent.style.position != 'absolute') {
		top += parent.offsetHeight;
		left += 1;
	} else {
		left += parent.offsetWidth - 3;
		if (left + e.offsetWidth > document.body.clientWidth) {
			left -= e.offsetWidth + parent.offsetWidth - 5;
		}
		if (left < 0) left = 0;
		top += 3;
	}
	e.style.left = left;
	e.style.top = top;

	var curid = id;
	while (curid && curid != '') {
		document.getElementById(curid).style.visibility = 'visible';
		document.getElementById(id).style.display = '';
		curid = document.getElementById(curid).parentmenu;
	}
	showForms(false);
	openedTime = new Date();
}
function highLight(item) {
	image = document.getElementById("img" + item.id);
	if (image) {
		image.src = image.src.replace("_off", "_on");
	} else {
		if (item.className.indexOf("main") != -1)
			item.className = "mainSubHover";
		else if (item.className.indexOf("sub") != -1)
			item.className = "subMenuHover";
		else
			item.className = "menuHover";
	}		
}
function unhighLight(item) {
	image = document.getElementById("img" + item.id);
	if (image) {
		image.src = image.src.replace("_on", "_off");
	} else {
		if (item.className.indexOf("main") != -1)
			item.className = "mainSub";
		else if (item.className.indexOf("sub") != -1)
			item.className = "sub";
		else
			item.className = "";
	}
}
function mouseOver(id, parentid, left, top) {
	if (errorFound)
		return;
	timeOut = setTimeout("showSub('" + id + "', '" + parentid + "', " + left + ", " + top + ");", 350);
	inCount++;
}
function mouseOverLink(id) {
	if (errorFound)
		return;
	timeOut = setTimeout("hideAll('" + id + "');", 300);
	inCount++;
}
function mouseOut() {
	if (errorFound)
		return;
	killTimeOut = clearTimeout(killTimeOut);
	killTimeOut = setTimeout("killMenu()", 5000);
	timeOut = clearTimeout(timeOut);
	inCount--;
}
function killMenu() {
	if (inCount == 0) {
		hideAll('');
		showForms(true);
	}
}

// Get the document object inside an iframe in a portable manner
function getIframeDocument(id) {
	if(document.frames)
		return document.frames[id].document;

	var ifrm = document.getElementById(id);
	if(ifrm.contentDocument)
		return ifrm.contentDocument;
	else if(ifrm.contentWindow)
		return ifrm.contentWindow.document;
	else if(ifrm.document)
		return ifrm.document;
}



// Turn an array into a readable string. 
function arrayToReadableList(array) {
    var tempString = "";
    for (var i=0; i < array.length; i++) {
	tempString += array[i];
	if (array.length - i > 2) {
	    tempString += ", ";
	} else if (array.length - i == 2) {
	    tempString += " and ";
	}
    }
    return tempString;
}



/**
 * Calculate the number of each type of parking space, (reserved,
 * unreserved, garage, carport)
 *
 * This has been added to the standard js file as we have noticed some
 * cases where the the function is being called when it has not been
 * defined. We should extend this fix by factoring out all the
 * standard vehicle js functions, or by ensuring that all cqs only use
 * it when it has defined. This function should not stay in this file
 * permanently.
 * 
 */
function calcNumSpaces() { 

    var defined = document.getElementById("vehicleNumUnreservedSpaces");
    if (defined == null){
        return;
    }
    var numVehicles = parseInt(document.getElementById("numVehicles").value);  
    var numUnreserved = 0; 
    var numReserved = 0; 
    var numCarports = 0; 
    var numGarages = 0; 
    for (var i=1;  i<=numVehicles; i++) {    
	var e = document.getElementById("vehicleParkingType" + i);   
	var type = (e != null ? e.value : document.getElementById('vehicleDefaultParkingTypeHidden').value);   
	switch (type) {    
	case "0": 
	    numUnreserved++; 
	    break; 
	case "1": 
	    numReserved++; 
	    break; 
	case "2": 
	    numCarports++; 
	    break; 
	case "3": 
	    numGarages++; 
	    break; 
	} 
    } 
    document.getElementById("vehicleNumUnreservedSpaces").value = numUnreserved;     
    document.getElementById("vehicleNumReservedSpaces").value = numReserved;     
    document.getElementById("vehicleNumCarports").value = numCarports;     
    document.getElementById("vehicleNumGarages").value = numGarages; 
}    


var TaxIdRegionCodes=new Array("10", "12", "60", "67", "50", "53", "01", "02", "03", "04", "05", "06", "11", "13", "14", "16", "21", "22", "23", "25", "34", "51", "52", "54", "55", "56", "57", "58", "59", "65", "30", "32", "35", "36", "37", "38", "61",	"15", "24", "40", "44", "94", "95", "80", "90",
"33", "39", "41", "42", "43", "45", "46", "47", "48", "62", "63", "64", "66", "68", "71", "72", "73", "74", "75", "76", "77", "81", "82", "83", "84", "85", "86", "87", "88", "91", "92", "93", "98", "99", "20", "26", "27");

// Declare the On-Site "Package", if none already exists
var OSM = window.OSM || {};

/**
 * This function is basically a console.log alias on Firefox, but in IE it
 * checks whether it's being used on a devsite or a live site, and if it is 
 * on a devsite it sets up a pseudo-log function (on live it does nothing)
 */
OSM.log = function() {
	if (window.console && console.log)
		return console.log;
	else if ((window.location + "").indexOf("dev.on-site") != -1) {
		return function(x) {
			if (!$("pseudoLog"))
				document.body.appendChild(DIV({"id":"pseudoLog"}, "LOG", BR()));
			$("pseudoLog").innerHTML += x +"<br/>";
		};
	} else
		return noop;
}();
// If there is no console object (ie. if the user doesn't have Firebug)
// make a fake one that does nothing, to prevent errors from code that uses it
if (!window.console)
	window.console = {log:OSM.log};

function reload() { 
  if (validate(true)) { 
    if (document.forms[0].action.indexOf('?') == -1) 
      document.forms[0].action+='?'; 
    else 
      document.forms[0].action+='&'; document.forms[0].action+='reload=yes&scroll_to='+(self.pageYOffset ? self.pageYOffset:document.body.scrollTop); 
      document.forms[0].action+='&scroll_down='+(self.pageYOffset ? self.pageYOffset:document.body.scrollTop); 
      document.forms[0].submit(); 
  } 
}

/**
 */
function checkSSN (element, name){
	return checkSSNs (element, null, name);
}


/**
 * Check a SSN to see that it is either censored and matches the old
 * ssn, or in the standard format.
 * @param sselt an input element holding the new ssn
 * @param oldssnelt an input element holding the old or existing ssn 
 * (this may be null)
 * @param name the human readable name of the ssn you are
 * checking. (i.e. if you are checking a tenant's name, this will be
 * "tenant's")
 * @return true if the ssnelt's value is a valid standard-format ssn
 * or if the ssnelt's value equals the old ssn.
 */
function checkSSNs (ssnelt, oldssnelt, name){
    if (checkStandardSSN(ssnelt)){
	return true;
    } else if (oldssnelt != null && 
	checkCensoredSSN(oldssnelt) && 
	oldssnelt.value == ssnelt.value){
        return true;
    } else {
	alert ("Please check that you have entered the " + name + " social security number correctly.");	
	ssnelt.focus();
	ssnelt.select();
	return false;
     }
}


/**
 * Check a SSN in the standard format.  These SSN's should be 11
 * characters long with the format 'DDD-DD-DDDD' where 'D' is a digit.
 */
function checkStandardSSN (element) {
	var temp = element.value;
	var SSN = "";
	for (var i=0; i<temp.length; i++) {
		var current = temp.charAt(i);
		if (!isNaN(parseInt(current)))
			// append it to the String
			SSN += current;
	}
	if (SSN.length == 9) {
		element.value = formatSSN(SSN);
		return true;
	} else {
		return false;
	}
}


/**
 * Check a censored SSN.  These SSN's should be 11 characters long
 * with the format 'DDDD-DD-****' where 'D' is a digit.
 */
function checkCensoredSSN (element) {
	var temp = element.value;
	var SSN = "";
        
	if (temp.length != 11){
         	return false;
        }
        if (isNaN(parseInt(temp.charAt(0))) ||
	    isNaN(parseInt(temp.charAt(1))) ||
	    isNaN(parseInt(temp.charAt(2))) ||
	    isNaN(parseInt(temp.charAt(4))) ||
	    isNaN(parseInt(temp.charAt(5)))){
	    return false;
        }    
        if ( temp.charAt(3)  != '-' ||
            temp.charAt(6)  != '-' ||
            temp.charAt(7)  != '*' ||
            temp.charAt(8)  != '*' ||
            temp.charAt(9)  != '*' ||
            temp.charAt(10) != '*'){
        	return false;
        }
        return true;
}
	
function formatSSN (rawSSN) {
	return (rawSSN.substring(0,3) + "-" + rawSSN.substring(3,5) + "-" + rawSSN.substring(5));
}

/**
 * Check a federal tax id in the standard format.  These taxId's should be 10
 * characters long with the format 'DD-DDDDDDD' where 'D' is a digit.
 */
function checkStandardTaxId (element) {
	var temp = element.value;
	var taxId = "";
	if (temp.length == 0){
		return true;
	}
	for (var i=0; i<temp.length; i++) {
		var current = temp.charAt(i);
		if (!isNaN(parseInt(current)))
			// append it to the String
			taxId += current;
	}
	if (taxId.length == 9 && validRegionCode(taxId.substring(0,2))) {
		element.value = formatTaxId(taxId);
		return true;
	} else {
		alert ("Please check that you have entered the federal tax id correctly.");	
		element.focus();
		element.select();
		return false;
	}
}
function formatTaxId (rawTaxId) {
	return (rawTaxId.substring(0,2) + "-" + rawTaxId.substring(2));
}

function validRegionCode(regionCode){
    var len = TaxIdRegionCodes.length;
    for (var i = 0; i < len; i++){
      if(TaxIdRegionCodes[i]===regionCode){ return true;}
    }
    return false;
}


function checkPercentage(element, name) {
	var amount = parseDouble(element.value);
	if (isNaN(amount)) {
		alert ("Please enter a numeric amount for the " + name + ".");
		element.focus();
		element.select();
		return false;
	}
	if (amount > 100) {
		alert ("The " + name + " may not exceed 100%.");
		element.focus();
		element.select();
		return false;
	}
	element.value=(amount + "%");
	return true;
}

function checkPercentageIfRequired(element, name, required) {
	if (element.value.length == 0 && !required)
		return true;
	else
		return checkPercentage(element, name);
}

function checkInt(element, name) {
	var temp = parseInt(element.value)
	if (isNaN(temp)) {
		alert("Please enter a number for the " + name + ".");
		element.focus();
		element.select();
		return false;
	}
	return true;
}

function checkIntIfRequired(element, name, required) {
	if (element.value.length == 0 && !required)
		return true;
	else
		return checkInt(element, name);
}

function checkDouble(element, name) {
	var temp = parseDouble(element.value)
	if (isNaN(temp)) {
		alert("Please enter a number for the " + name + ".");
		element.focus();
		element.select();
		return false;
	}
	return true;
}

function checkDoubleIfRequired(element, name, required) {
	if (element.value.length == 0 && !required)
		return true;
	else
		return checkDouble(element, name);
}

function checkText(element, name, notText) {
	// make sure there is non-empty text in the text element.
	trimElement(element);
	if (!element.value.length || (notText && element.value == notText)) {
		alert("Please enter a value for the " + name + ".");
		element.focus();
		element.select();
		return false;
	}
	return true;
}

function checkEmail(element, name) {
	// make sure there is a valid email addres
	trimElement(element);
	var i = element.value.indexOf("@");
	if (i == -1 || element.value.indexOf(".", i) == -1) {
		if (name)
			alert("Please enter a valid " + name);
		else
			alert("Please enter a valid email address.");
		element.focus();
		element.select();
		return false;
	}
	return true;
}

/**
 * Check the format of a phone number.
 *
 * This javascript function did not enforce very many restrictions.
 * It has since been renamed and replaced.
 * @param element the element containign the phone number.
 * @param name the name of the entity for which the phone number belongs
 * @return true if the phone number provided valid, false otherwise
 * @depricated 
 * @see checkPhone(element, name)
 */
function checkPhoneX (element, name) {
	trimElement(element);
	var countNumbers = 0;
	var str = element.value;
	for (var i=0; i<str.length; i++) {
		var ch = str.charAt(i);
		if (ch >= '0' && ch <= '9')
			countNumbers ++;
	}
	if (countNumbers < 7) {
		alert("Please enter a valid " + name + " phone number.");
		element.focus();
		element.select();
		return false;
	} 
	return true;
}

/**
 * Determines whether an HTML input element contains a valid phone
 * number. To be valid, the phone number must contain 10 digits,
 * and can be entered in formats '(DDD) DDD-DDDD', 'DDD-DDD-DDDD', or
 * 'DDDDDDDDDD'. If the input element contains a valid phone number,
 * it will be reformatted to the '(DDD) DDD-DDDD' format.
 * @param element the element containing the phone number
 * @param isRequired true if the phone number is required, or false
 * if a blank phone number is allowed
 * @return true if the input element contains a valid phone number, or
 * is empty and isRequired is false. Otherwise returns false.
 */
function isValidPhoneNumber(element, isRequired) {
    var regEx = '\\(?\\d\\d\\d\\)?[ -\\.]*?\\d\\d\\d[ -\\.]*?\\d\\d\\d\\d';
    trimElement(element);
    var str = element.value;
	if (!isRequired && str.length == 0) {
		return true;
	}
    if (str.match(regEx + '.*') == str) {
        if (str.match(regEx) == str) {
            element.value = formatPhone(str);
        }
        return true;
    } 
	return false;
}

/**
 * Check the format of a phone number.
 *
 * a valid phone number is defined as 10 digits, with a valid area
 * code in the format 'DDD-DDD-DDDD' or '(DDD) DDD-DDDD'.
 * @param element the element containing the phone number
 * @param name the name of the entity for which the phone number belongs
 * @param empty true if the value 'n/a' or 'N/A' may be accepted,
 * false otherwise.
 * @return true if the phone number provided valid, false otherwise
 */
function checkPhoneFull(element, name, empty) {
	if (isValidPhoneNumber(element, !empty)) {
		return true;
	}
  var str = element.value;
	if (empty && str.match('\\s*[Nn]\\s?/?\\s?[Aa]\\s*') == str){
		element.value = 'N/A';
		return true;
	}
	alert("Please enter a valid " + name + " phone number" + 
		  (empty ? " or enter the value 'N/A'" : "") +
	  ".\n  All phone numbers MUST include an area code.\n If you have an extension please include it after the phone number.  (e.g. '(555) 123-4567 ext. 347')");
	element.focus();
	element.select();
	return false;            
}

/**
 * Check the format of a phone number.
 *
 * a valid phone number is defined as 10 digits, with a valid area
 * code in the format 'DDD-DDD-DDDD' or (DDD) DDD-DDDD.
 * @param element the element containign the phone number.
 * @param name the name of the entity for which the phone number belongs
 * @return true if the phone number provided valid, false otherwise
 */
function checkPhone(element, name) {
    return checkPhoneFull(element, name, false);
}

/**
 * Check the format of a phone number. 
 *
 * a valid phone number is defined as 10 digits, with a valid area
 * code in the format 'DDD-DDD-DDDD' or (DDD) DDD-DDDD.
 * @param element the element containign the phone number.
 * @param name the name of the entity for which the phone number belongs
 * @return true if the phone number provided is valid or null, false
 * otherwise
 */
function checkPhoneIfNotNull(element, name) {
	trimElement(element);
	return (element.value == null || 
			element.value.length == 0 || 
			checkPhoneFull(element, name, false));
}


/**
 * Format the given phone number into the format (DDD) DDD-DDDD
 * @param rawPhone a 10 digit number.
 * @return a string in the format (DDD) DDD-DDDD or the empty string
 * if the phone number could not be formatted.
 */
function formatPhone (rawPhone) {
        var formatted = '';
        if (rawPhone == null || rawPhone.length < 10){
            return '';
        }
        for (var i=0; i<rawPhone.length; i++){
            var ch = rawPhone.charAt(i);
            if (ch >= '0' && ch <= '9'){
                formatted += ch;
            } else if (ch == ' ' ||
                       ch == '(' ||
                       ch == ')' ||
                       ch == '.' ||
                       ch == '-'){
                continue;
            } else {
                //unexpected character
	        return '';
            }
        }
	return ('(' + formatted.substring(0,3) + ") " + formatted.substring(3,6) + "-" + formatted.substring(6));
}

function trim(str) {
	while (str.indexOf(" ") == 0) {
		if (str.length > 1)
			str = str.substring(1);
		else
			return ("");
	}
	while (str.length && str.lastIndexOf(" ") == str.length -1) {
		str = str.substring(0, str.length-1);
	}
	var newStr = "";
	var i = str.indexOf(" ");
	while (i != -1) {
		// check to make sure it's in a valid place
		if (str.length > i) {
			newStr += str.substring(0, i);
			var ch = str.charAt(i + 1);
			var prevCh = '';
			if (newStr.length)
				prevChar = newStr.charAt(newStr.length-1);
			if ((prevCh != ' ' && prevCh != '-' && prevCh != '\'')
				&& (ch != ' ' && ch != '-' && ch != '\'')) {
				newStr += " ";
			}
			str = str.substring(i+1);
		} else {
			str = "";
		}
		i = str.indexOf(" ");
	}
	newStr += str;
	return newStr;
}

/**
 * Test whether or not the provided string contains numerals or the # character
 * @param str the string to be evaluated
 * @return true (if the string contains numbers or "#") or false (if it doesn't)
 */
function containsNumbers (str) {
	return /[0-9#]/.test(str);
}

function trimElement(element) {
	element.value = trim(element.value);
}

function split(str) {
	var words = new Array();
	
	return words;
}

function isHawaian(str, follows) {
	if (str.length > 1 && follows != 's') {
		var ch = str.charAt(str.length - 2);
		return (str.charAt(str.length - 1) == '\'' 
			&& (ch == 'a'
				|| ch == 'e'
				|| ch == 'i'
				|| ch == 'o'
				|| ch == 'u'));
	} else {
		return (str.length && str.charAt(str.length - 1) == '\'' && follows.toLowerCase() == 's');
	}
}

function doTitleCase(str) {
	if (str.toLowerCase().indexOf("mc") == 0 && str.length > 2) {
		return "Mc" + str.charAt(2).toUpperCase() + str.substring(3).toLowerCase();
	} else if (str.indexOf("Mac") == 0 && str.length > 3 
		&& str.charAt(3).toUpperCase() == str.charAt(3)) {
		return str;
	} 
	return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase();
}

function checkTitleCase(element) {
	var str = trim(element.value);
	var newStr = str;
	if (newStr == newStr.toLowerCase()) {
		var done = false;
		newStr = "";
		while (!done) {
			var apos = str.indexOf("'");
			var dash = str.indexOf("-");
			var space = str.indexOf(" ");
			var i = str.length - 1;
			var ch = '';
			if (apos != -1 && apos < i) {
				i = apos;
				ch = '\'';
			}
			if (dash != -1 && dash < i) {
				i = dash;
				ch = '-';
			}
			if (space != -1 && space < i) {
				i = space;
				ch = '';
			}
			var temp = str;
			if (i < str.length - 1)
				temp = trim(str.substring(0, i));
			else {
				done = true;
			}	
			if (temp.length) {
				if (newStr.length) {
					var prevChar = newStr.charAt(newStr.length - 1);
					if (prevChar != '\'' 
						&& prevChar != '-')
						newStr += " ";
				}
				if (temp.toLowerCase() != 'de' 
					&& (newStr.toLowerCase() + temp.toLowerCase() != 'de la')
					&& (temp + ch).toLowerCase() != 'l\'' 
					&& (temp + ch).toLowerCase() != 'd\'' 
					//&& (temp.toLowerCase() != 'el' || !newStr.length) 
					&& temp.toLowerCase() != 'le' 
					&& temp.toLowerCase() != 'al' 
					&& temp.toLowerCase() != 'izz' 
					&& temp.toLowerCase() != 'van'
					&& temp.toLowerCase() != 'von'
					&& temp.toLowerCase() != 'der' 
					&& temp.toLowerCase() != 'nee' 
					&& temp.toLowerCase() != 'da' 
					&& temp.toLowerCase() != 'ibn' 
					&& temp.toLowerCase() != 'bin'
					&& (!isHawaian(newStr, temp))) {
					if (temp != temp.toUpperCase() && !containsNumbers(temp)) {
						temp = doTitleCase(temp) + ch;
					} else
						temp += ch;
				} else {
					temp = temp.toLowerCase() + ch;
				}
				newStr += temp;
			}
			if (!done)
				str = trim(str.substring(i + 1));
		}
	}
	element.value = newStr;
}

/**
 * format a textarea to a certain length
 */
function formatTextArea(elt, msg){
	var answer = true;
	with (document){
		var maxlen = elt.getAttribute('maxlength');
		var value = elt.value;
		if (value.length > maxlen){
			var confirmMsg = 'Warning, your answer ';
			confirmMsg += (msg ? 'to "' + msg + '" ' : "");
			confirmMsg += 'is too long; it can be up to ' + maxlen + ' ';
			confirmMsg += 'characters.  Please click "OK" if it is alright to ';
			confirmMsg += 'trim your answer to fit.';
			answer = confirm(confirmMsg);
			if(answer) {
				elt.value = value.substring(0, maxlen-1);
			} else {
				elt.focus();
				elt.select();
			}
		}
	}
	return answer;
}

// Stuff Jeremy Added
/*
 Hide all visibile traces of a CQ (the TR that it's in, the next TR if it has additional instructions, etc.)
 */
function hideCQ(cqLabel){
	var cqNode = $(cqLabel);
	if (cqNode == null) return;
	if (cqNode.tagName == 'TH'){
		var nodeToHide = cqNode.parentNode;
	} else {
		var nodeToHide = cqNode.parentNode.parentNode;
	}
	nodeToHide.style.display = 'none';
	if($(cqLabel+"Description"))
		$(cqLabel+"Description").parentNode.style.display = 'none';
}
//Aliases to avoid capitalization problems
var hideCq = hideCQ;
var hidecq = hideCQ;

/**
 * Restores a CQ hidden by hideCQ() to visibility
 */
function showCQ(cqLabel){
	var cqNode = $(cqLabel);
	if (cqNode == null) return;
	if (cqNode.tagName == 'TH'){
		var nodeToHide = cqNode.parentNode;
	} else {
		var nodeToHide = cqNode.parentNode.parentNode;
	}
	nodeToHide.style.display = '';
	if($(cqLabel+"Description"))
		$(cqLabel+"Description").parentNode.style.display = '';
}

//Aliases to avoid capitalization problems
var showCq = showCQ;
var showcq = showCQ;


/**
 * Deprecated - Hide all visibile traces of a Header CQ (use hideCQ instead)
 */
function hideHeader(headerText){
	var allTRs = document.getElementsByTagName("tr");
	for (thisTR in allTRs){
		if (allTRs[thisTR].className == "header"){
			if (allTRs[thisTR].childNodes[0].innerHTML == headerText)
				allTRs[thisTR].style.display = "none";
		}
	}
}

/**
 * Deprecated - Restores a Header CQ hidden by hideHeader() to visibility (use
 * showCQ instead)
 */
function showHeader(headerText){
	var allTRs = document.getElementsByTagName("tr");
	for (thisTR in allTRs){
		if (allTRs[thisTR].className == "header"){
			if (allTRs[thisTR].childNodes[0].innerHTML == headerText)
				allTRs[thisTR].style.display = "";
		}
	}
}

/** Determine the current value of a set of radio buttons */
function getRadioValue(name){
	var options = document.getElementsByName(name);
	for(var index = 0; index < options.length; index++){
		if(options[index].checked) return options[index].value; 
	}
	return null;
}

/** Hide all elements of the provided tag and class */
function hideAllElements(tagName, className) {
	tagName = tagName ? tagName : "*";
	className = className ? "." + className : "";
	$$(tagName + className).hide();
}

/** Hide all elements of the provided tag and class */
function showAllElements(tagName, className){
	tagName = tagName ? tagName : "*";
	className = className ? "." + className : "";
	$$(tagName + className).shpw();
}

// Declare the regexp sub-package
OSM.regexp = {};
/** Shorthand function for a named group regular expression */
OSM.regexp.namedGroup = function(regexp) {
	return new XRegExp(regexp, "k");
}

// Declare the build sub-package
OSM.build = {};

OSM.build.NormalTextbox = function(id, name, size, value) {
	// $$("<input type='text' class='NormalInput' id='" + id + "' " +
	//		"name='" + name + "' size='" +
	//		(typeof(size) == "undefined" ? 10: size ) + "' value='" +
	//		value + "'/>");
	return INPUT({
		"type":"text", "class":"NormalInput", "id":id, "name":name,
		"size":typeof(size) == "undefined" ? 10: size, "value":value
	});
}

/**
 * This function builds a standard cancel button, using the provided id for both
 * the buttons name and id attributes
 */
OSM.build.cancelButton = function(id) {
	// $$("<input type='button' class='CancelButton NormalButton' " +
	//		"id='" + id + "' name='" + id + "' value='Cancel'/>");
	return FORMBUTTON({
		"id":id, "name":id, "class":"CancelButton NormalButton",
		"value":"Cancel"
	});
}
/**
 * This method generates a series of LIs using by mapping the provided items
 * array to the provided function (func), and then appending the results to
 * the provided list
 */
OSM.build.list = function(list, func, items) {
	// $$.each(list, function(index, value) {
	//		var li = $$("<li>").append(func(value, index));
	//		list.appendChild(li);
	//	}
	//	return list;
	for(var i = 0; i < items.length; i++){
		list.appendChild(LI({}, func(items[i], i)));
	}
	return list;
}

// Declare the util sub-package
OSM.util = {};
OSM.util.build = {};
OSM.util.isMSIE = function(){
	return /*@cc_on!@*/false;
}

/**
 * Capitalizes (the first character of) a single provided word
 * NOTE: If the provided string contains multiple words, only the first
 * (character of the first) word will be capitalized
 */
OSM.util.capitalizeWord = function(word){
	return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase();
}

/**
 * Capitalizes the first letter of every word in the provided string
 */
OSM.util.capitalize = function(str){
	return str.replace(/\w+/g, OSM.util.capitalizeWord);
}

/**
 * Converts a word to plural form if needed, depending upon a specified count. This isn't
 * perfect, but it handles the most common form of plural words. For example:
 * OSM.util.pluralize("lease", 1) returns "lease".
 * OSM.util.pluralize("lease", 2) returns "leases".
 * OSM.util.pluralize("company", 2) returns "companies".
 */
OSM.util.pluralize = function(word, count){
	if (count == 1)
		return word;
	if (word.charAt(word.length - 1) == 'y' && word != "day")
		word = word.substr(0, word.length-1) + "ie";
	return word + "s";
}

OSM.util.concatAndPluralize = function(word, count, convertToWords) {
	if (convertToWords)
		throw("This version of concatAndPluralize has not yet been " + 
			"implemented; when it is implemented (and true) this function " +
			"will return 'four companies' instead of '4 companies' (but " +
			"someone has to write a 4=>four conversion function first)"); 
	return count + " " + OSM.util.pluralize(word, count);
}

/**
 * Converts the provided array of strings in to a single comma-separated string
 * @param strings an array of strings to be combined
 * @param lastSep (optional) the string (presumably "and" || "or") that will
 * separate the final two items (in place of a comma); if no lastSep is
 * provided, a simple comma-seperated list will be returned
 * 
 * Examples:
 * OSM.util.stringList(["bills", "bills", "more bills"]);
 * >>>"bills, bills, more bills"
 * 
 * OSM.util.stringList(["bills", "bills", "more bills"], "or");
 * >>>"bills, bills or more bills"
 * 
 * OSM.util.stringList(["green eggs", "ham"], "and");
 * >>>"green eggs and ham"
 */
OSM.util.stringList = function(strings, lastSep) {
	// Join the strings with commas and spaces
	var ret = strings.join(", ");
	// If a simple comma list is enough, stop here
	if (!lastSep)
		return ret;
	// Otherwise, determine where the last comma is
	var lastComma = ret.lastIndexOf(",");
	// If there is no last comma, there was only one item (and we're done)
	if (lastComma == -1)
		return ret;
	// Otherwise, replace the last comma with the provided lastSep
	return ret.slice(0, lastComma) + " " + lastSep + ret.slice(lastComma + 1);
}

OSM.util.addToDoc = function(elem){
	appendChildNodes(document.body, elem);
}
/**
 * Centers the provided element on the page using Mochikit's Style library
 * 
 * NOTE: The provided element must have a fixed height/width.
 */
OSM.util.centerOnPage = function(theElem){
	// TODO: Replace these MochiKit functions with jQuery ones
	if (typeof theElem == "string")
		theElem = $(theElem);
	var vPosition = getViewportPosition();
	var vDimensions = getViewportDimensions();
	var eDimensions = getElementDimensions(theElem);
	var topOffset = vPosition.y + vDimensions.h/2 - eDimensions.h/2;
	var leftOffset = vPosition.x + vDimensions.w/2 - eDimensions.w/2;
	setStyle(theElem, {
		"left": leftOffset + "px",
		"position": "absolute",
		"top": topOffset + "px"
	});
}
/** Sets the styles of the provided element to the provided styles */ 
OSM.util.elementWithStyle = function(elem, style){
	setStyle(elem, style);
	return elem;
}
OSM.util.showOverlay = function(name, callback) {
	$$("#" + name + "OverlayWindow").show();
	$$("#" + name + "OverlayFrame").show();
	$$("#" + name + "OverlayMessage").show();
	setTimeout(function() {
		scroll(0,0);
		document.body.style.overflow = 'hidden'; // Don't let the user scroll
		if (callback)
			callback();
	}, 50);
}
OSM.util.hideOverlay = function(name) {
	$$("#" + name + "OverlayWindow").hide();
	$$("#" + name + "OverlayFrame").hide();
	$$("#" + name + "OverlayMessage").hide();
	document.body.style.overflow = '';
}

// DEPRECATED
OSM.util.coverDialog = function(contents, w, h){
	var dialogContainerAttrs = {"id":"dialogContainer", "style":"display:none"};
	var dialogContainer = DIV(dialogContainerAttrs, contents);
	if(h && w) setElementDimensions(dialogContainer, {"h":h, "w":w});
	OSM.util.coverDisplay(dialogContainer);
	OSM.util.centerOnPage(dialogContainer);
	dialogContainer.style.display = "";
}
//DEPRECATED
OSM.util.uncoverDialog = function(){
	removeElement("dialogContainer");
	OSM.util.uncoverDisplay();
}

// Build the cover DIV (memoized function)
//DEPRECATED
OSM.util.build.coverDiv = function(top, left, contents){
	var coverDiv = DIV({id:"coverDiv"}, contents);
	setStyle(coverDiv, {
		"background-image": "url('/images/cover_bg.png')",
		"display":"block",
		"height":"100%",
		"left":left,
		"position":"absolute",
		"top":top,
		"width":"100%"
	});
	return coverDiv;
}

// Build the cover IFrame (memoized function)
//DEPRECATED
OSM.util.build.coverIFrame = function(top, left){
	var iframe = createDOM("IFRAME", { id:"coverFrame", src:"javascript:'';" });
	setStyle(iframe, {
		"position":"absolute",
		"left":left,
		"opacity":"0",
		"top":top,
		"height":"100%",
		"width":"100%"
	});
	return iframe;
}.memoize();

// Creates a semi-transparent(30%) black overlay that covers the entire page
//DEPRECATED
OSM.util.coverDisplay = function(contents){
	var build = OSM.util.build; // convenience alias
	document.body.style.overflow = "hidden";
	var viewPortPosition = getViewportPosition();
	var top = viewPortPosition.x + "px";
	var left = viewPortPosition.y + "px";
	var coverDiv = build.coverDiv(top, left, contents);
	if(OSM.util.isMSIE()) {
		var coverIFrame = build.coverIFrame(top, left);
		OSM.util.addToDoc(coverIFrame);
	}
	OSM.util.addToDoc(coverDiv);
}

// Removes the overlay created by OSM.util.coverDisplay
//DEPRECATED
OSM.util.uncoverDisplay = function(){
	removeElement("coverDiv");
	if(OSM.util.isMSIE()) removeElement("coverFrame");
}
/**
 * Collection of utility objects for converting between month names and numerals
 */
OSM.util.monthNames = {
	1:'January', 2:'February', 3:'March', 4:'April', 5:'May', 6:'June',
	7:'July', 8:'August', 9:'September', 10:'October', 11:'November',
	12:'December'
}
OSM.util.monthNamesReversed = {
	'January':1, 'February':2, 'March':3, 'April':4, 'May':5, 'June':6,
	'July':7, 'August':8, 'September':9, 'October':10, 'November':11,
	'December':12
}
OSM.util.monthNamesShort = {
	1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun',
	7:'Jul', 8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'
}
OSM.util.monthNamesShortReversed = {
	'Jan':1, 'Feb':2, 'Mar':3, 'Apr':4, 'May':5, 'Jun':6,
	'Jul':7, 'Aug':8, 'Sep':9, 'Oct':10, 'Nov':11, 'Dec':12
}

/**
 * Normally SELECTs can only be "set" by changing their selectedIndex property;
 * sometimes however you only know the value of the option you want to select
 * (not the index).  You can use this function for those cases, to select the
 * FIRST option with the specified value.
 * @param id the id of the SELECT element
 * @param value the value of the OPTION you wish to select
 * @return the index of the OPTION (in case you care), or null if no OPTION had
 * the provided value 
*/
OSM.util.setSelectByValue = function(id, value) {
	var select = $(id);
	var options = select.childNodes;
	for(var i = 0; i < options.length; i++) {
		if(options[i].value == value) {
			select.selectedIndex = options[i].index;
			return options[i].index;
		}
	}
	return null;
}

/**
 * Queues up the provided function to run as soon as the DOM is loaded
 */
OSM.util.addOnLoad = function(func) {
	connect(window, "onload", func);
};

/**
 * Hide the element with the provided ID
 * 
 */
OSM.util.hide = function(elementId) {
	$$("#" + elementId).hide();
}

/**
 * Shows (un-hides) the element with the provided ID
 */
OSM.util.show = function(elementId) {
	$$("#" + elementId).show();
}

/**
 * This function just throws an error with the provided message (as an alias,
 * this function only saves 3 characters vs. just throwing the error directly;
 * the main reason to use it is if you want a *function* for throw errors
 * (to use with Map or something similar))
 */
OSM.util.err = function(message) {
	throw new Error(message);
}
/**
 * Throws an error with a message that at least one of the provided parameters
 * is required
 * 
 * Example:
 * OSM.util.errParamsMissingOneOf("adddressState", "zipCode")
 * Error: One of the following parameters is required: addressState or zipCode!
 * 
 * @param params an array of names (of the parameters that are required)
 */
OSM.util.errParamsMissingOneOf = function(params) {
	var message = "One of the following parameters is required: ";
	OSM.util.err(message + OSM.util.stringList(params, "or") + "!");
}

OSM.util.ensureParam = function(config, param) {
	if(!config[param]) OSM.util.err("The parameter " + param + " is required!");
}

OSM.util.ensureOneOfParams = function(config, params, exclusive) {
	// Get an array of all of the parameter names that are in config
	var hasParams = filter(partial(operator.contains, config), params);
	if(hasParams.length == 0) {
		OSM.util.errMissingOneOf(params);
	}
	if(hasParams.length > 1 && exclusive) {
		var message = "Only one of the following parameters may be provided: ";
		OSM.util.err(message + OSM.util.stringList(hasParams, "or") +"!");
	}
}

/**
 * <p>
 *   Obtain a flipped version of the given 2 argument function such that the
 *   argument order is reversed.
 * </p>
 * <p>
 *   For example, given the function subtract(a, b) which returns a - b,
 *   then the following:<br/>
 *   <code>
 *     (flip(subtract))(1, 2);<br/>
 *   </code>
 *   will return 1 rather than -1.
 * </p>
 *
 * @param fn
 *     The function to obtain a flipped function for.
 * @return A function that will call fn with the arguments reversed.
 */
OSM.util.flip = function(fn) {
	return function(a, b) {
		return fn(b, a);
	};
}

/**
 * <p>
 *   Format all arguments into the first argument.  This is a convenience
 *   function similar to the C sprintf function, though only with simple
 *   replacements.  Replacements are formatted like #{i} where i is a zero
 *   based index into the additional arguments passed in to format beyond
 *   the first.
 * </p>
 * <p>
 *   Example usage:<br/>
 *   <code>
 *     format("#{2} #{0} #{1}", "hello", "world", 3);<br/>
 *   </code>
 *   will return the string "3 hello world".
 * </p>
 * <p>
 *   Additional parameters not used will be ignored.  For example:<br/>
 *   <code>
 *     format("#{0} #{1}", "hello", "world", 3);<br/>
 *   </code>
 *   will return the string "hello world".
 * </p>
 * <p>
 *   Including formatting requests for parameters that don't exist will
 *   throw an exception.  For example:<br/>
 *   <code>
 *     format("#{2} #{0} #{1}", "hello", "world");<br/>
 *   </code>
 *   throws an exception.
 * </p>
 * @params
 *     The first argument must be the string that needs to be formatted.
 *     Additional arguments are formatted into that string.
 * @return A string formatted to include the additional parameters.
 */
OSM.util.format = function() {
	var str = arguments[0];

	if (arguments.length > 1) {
		for (var i = 1; i < arguments.length; i++) {
			var regex = "\\#\\{" + (i - 1) + "\\}";
			str = str.replace(new RegExp(regex, "g"), arguments[i]);
		}
	}

	if (/\#\{\d+\}/.test(str)) {
		throw new Error("Unmatched formatting found!");
	}

	return str;
}

/**
 * <p>
 *   Requires Libraries:
 *   <ul>
 *     <li>MochiKit.Base</li>
 *   </ul>
 * </p>
 * <p>
 *   This is a simple abstraction to take a list of objects and assign the
 *   same value to each one.  The assignments parameter can either be a
 *   single Assignment object, or a list of Assignment objects
 *   (flattenArguments is called on it).  This is equivalent to taking each
 *   Assignment object in the parameter and doing the following:<br/>
 *   for (var i; i < assignment.objects.length; i++) {<br/>
 *       assignment.objects[i][assignment.key] = assignment.value;<br/>
 *   }<br/>
 *   The benefit of which means you can take the following code:<br/>
 *   <code>
 *     forEach(textBoxes, function(textBox) {<br/>
 *         textBox.value = "abc";<br/>
 *     });<br/>
 *     forEach(buttons, function(button) {<br/>
 *         button.value = "Cancel";<br/>
 *     });<br/>
 *   </code>
 *   And turn it into 1 assignToAll call:<br/>
 *   <code>
 *     assignToAll([new Assignment(textBoxes, "value", "abc"),
 *         new Assignment(buttons, "value", "Cancel")]);<br/>
 *   </code>
 *   Note that each extra loop of assignments equates to a single new
 *   Assignment object added to the assignToAll call.
 * </p>
 * <p>
 *   To avoid the extra syntax of constructing an Assignment object, this
 *   function alternatively can be called with the 3 objects consisting of
 *   the Assignment object constructor parameters for the case that only 1
 *   batch assignment is required.  For example:<br/>
 *   <code>
 *     assignToAll(textBoxes, "value", "abc");
 *   </code>
 *   is equivalent to:<br/>
 *   <code>
 *     assignToAll(new Assignment(textBoxes, "value", "abc"));
 *   </code>
 * </p>
 *
 * @params
 *     Either a single Assignment object, multiple Assignment objects, an
 *     array of Assignment objects to assign the values for.
 */
OSM.util.assignToAll = function() {
	var assignments = flattenArray(arguments);

	// If the argument length matches Assignment parameter length, and the
	// first element of the result of flattenArray is not an Assignment,
	// then we can safely assume they are using the
	// assignToAll(textBoxes, "value", "abc") form.
	if (arguments.length == 3 && assignments[0].constructor != OSM.util.Assignment) {
		assignments = [new OSM.util.Assignment(arguments[0], arguments[1], arguments[2])];
	}

	forEach(assignments, function(assignment) {
		forEach(assignment.objects, function(obj) {
			obj[assignment.key] = assignment.value;
		});
	});
}

/**
 * <p>
 *   Construct an Assignment object, to be used with the assignToAll method.
 *   This object sets up the following properties:<br/>
 *   <br/>
 *   objects: The list of source objects that each must be assigned
 *       value.<br/>
 *   <br/>
 *   key: The object property key that must be assigned on each of the
 *       objects.<br/>
 *   <br/>
 *   value: The value that must be assigned to each of the objects.<br/>
 * </p>
 *
 * @param objects
 *     Assigned as the objects property.
 * @param key
 *     Assigned as the key property.
 * @param value
 *     Assigned as the value property.
 */
OSM.util.Assignment = function(objects, key, value) {
	this.objects = objects;
	this.key = key;
	this.value = value;
}

/**
 * <p>
 *   Requires Libraries:
 *   <ul>
 *     <li>MochiKit.Base</li>
 *   </ul>
 * </p>
 * <p>
 *   The eventTriggered method is designed to trigger an event that
 *   originated on a DOM object.  This was created so even though each event
 *   type calls a different function, they can all be called in the same
 *   manner, avoiding a big ugly error prone switch.
 * </p>
 * <p>
 *   As an example, consider the following regular event call:<br/>
 *   <code>
 *     textBox.onkeyup = function(e) { keyUp(table, textBox.value); };<br/>
 *   </code>
 *   This becomes:<br/>
 *   <code>
 *     textBox.onkeyup = function(e) { eventTriggered(textBox, keyUp, table, ["value"], e); };<br/>
 *   </code>
 *   The benefit of which is that now every event handler is called in the
 *   same exact manner, and so all them can be hooked up in a loop, reducing
 *   code duplication and chances for errors when a new event handler is
 *   needed.
 * </p>
 *
 * @param obj
 *     The source of the event and (more importantly) where the late bound
 *     parameter keys reside.
 * @param fn
 *     The function to be called when the event is triggered, and it must
 *     have the signature fn(table[, late bound parameters...]);
 * @param parameters
 *     The parameters that are passed in to fn as the first parameters.
 * @param lateBoundParameters
 *     A list of string keys that correspond to object properties of obj
 *     that are retrieved at the time the event is triggered.
 * @param passEvent
 *     Whether or not the event argument should be passed in to fn as the
 *     last argument.
 * @param e
 *     The event object passed in from the event being triggered.  If
 *     passEvent is true, this will be passed in as the last argument.
 */
OSM.util.eventTriggered = function(obj, fn, parameters, lateBoundParameters, passEvent, e) {
	// obtain the arguments other than the table 
	var params = map(function(key) {
			return obj[key];
		}, lateBoundParameters);

	// first partially apply the regular parameters
	fn = partial.apply(this, concat([fn], parameters));

	if (passEvent) {
		params.push(e);
	}

	// then apply with the late bound parameters
	fn.apply(this, params);
}

/**
 * <p>
 *   Requires Libraries:
 *   <ul>
 *     <li>MochiKit.Base</li>
 *     <li>MochiKit.Iter</li>
 *     <li>MochiKit.DOM</li>
 *     <li>MochiKit.Style</li>
 *     <li>MochiKit.Signal</li>
 *   </ul>
 * </p>
 * <p>
 *   This method is used to connect a bunch of events with event handlers
 *   sing the eventTriggered method as a common event handler.  This
 *   leverages the fact that eventTriggered is normalized so that every
 *   event hookup looks the same.
 * </p>
 * <p>
 *   This method is essentially the same as obtaining each source object in
 *   each hookup object in the hookupsArray and setting up the corresponding
 *   event handler.  So for example, the following hookup object:<br/>
 *   <code>
 *     new Hookup([textBox], "onkeyup", keyUp, [table], ["value"], false);<br/>
 *   </code>
 *   is essentially the same as:<br/>
 *   <code>
 *     textBox.onkeyup = function(e) { keyUp(table, textBox.value); };<br/>
 *   </code>
 *   However the benefit of using this method is it will hook up the event
 *   on every object given for every set of Hookup passed in, so a sequence
 *   of 5 loops setting up a different kind of event in each loop to a set
 *   of objects can be reduced to a single method call to this method, with
 *   a list of the desired Hookup specifications.
 * </p>
 *
 * @param hookups
 *     The list of Hookup objects to hook up events for.
 */
OSM.util.hookupEvents = function(hookups) {
	// for each set of hookups...
	forEach(hookups, function(hookup) {
		// iterate on each object in that hookup list...
		forEach(hookup.objects, function(obj) {
			// and connect a signal to call eventTriggered
			connect(obj, hookup.event, partial(OSM.util.eventTriggered, obj, hookup.fn,
				hookup.parameters, hookup.lateBoundParameters, hookup.eventObjectRequired));
		});
	});
}

/**
 * <p>
 *   Construct a Hookup object, to be used with the hookupEvents method.
 *   This object sets up the following properties:<br/>
 *   <br/>
 *   objects: The list of source objects to hook up an event for.<br/>
 *   <br/>
 *   event: The event (or signal) that is being set up on the source
 *       objects.<br/>
 *   <br/>
 *   eventHandler: The function called when the event is triggered, and it
 *       must have the signature
 *       eventHandler(table[, late bound parameters...]);<br/>
 *   <br/>
 *   parameters: The set of objects that are passed in to the event handler
 *       before the late bound parameters.<br/>
 *   <br/>
 *   lateBoundParameters: The set of object property keys that are obtained
 *       and passed in to the eventHandler when it is called.<br/>
 *   <br/>
 *   eventObjectRequired: Whether or not the event object is needed as the
 *       last parameter to the event handler.
 * </p>
 *
 * @param sourceList
 *     Assigned as the objects property.
 * @param event
 *     Assigned as the event property.
 * @param eventHandler
 *     Assigned as the fn property.
 * @param parameters
 *     Assigned as the parameters property.
 * @param lateBoundParameters
 *     Assigned as the lateBoundParameters property.
 * @param eventObjectRequired
 *     Assigned as the eventObjectRequired property.
 */
OSM.util.Hookup = function(sourceList, event, eventHandler, parameters, lateBoundParameters, eventObjectRequired) {
	this.objects = sourceList;
	this.event = event;
	this.fn = eventHandler;
	this.parameters = parameters;
	this.lateBoundParameters = lateBoundParameters;
	this.eventObjectRequired = eventObjectRequired;
}

//--------------------------- getNestedProperty ---------------------------
/**
 * <p>
 *   Obtain deeply nested properties from obj in a safe manner, checking for
 *   null or undefined at each step.  If no arguments are passed, obj is
 *   null or undefined, or any of the nested properties are null or
 *   undefined, this function will return null.  If no additional arguments
 *   are passed beyond obj, then obj is just returned (except if it is
 *   undefined, in which case null will be returned).
 * </p>
 * <p>
 *   For example, the call:<br/>
 *   <code>
 *     var display = getNestedProperty(obj, "array", 1, "display");<br/>
 *   </code>
 *   is equivalent to:<br/>
 *   <code>
 *     var display = null;<br/>
 *     if (obj && obj.array && obj.array[1] && obj.array[1].display) {<br/>
 *       display = obj.array[1].display;<br/>
 *     }<br/>
 *   </code>
 * </p>
 * @param obj
 *     The object in question to obtain the nested property.
 * @param arguments
 *     Additional arguments beyond the first are the nested properties to be
 *     obtained from obj.
 */
OSM.util.getNestedProperty = function(obj) {
	if (arguments.length == 0) {
		return null;
	}

	if (isUndefinedOrNull(obj)) {
		return null;
	}

	if (arguments.length == 1) {
		return obj;
	}

	var args = [];
	args.push(obj[arguments[1]]);

	for (var i = 2; i < arguments.length; i++) {
		args.push(arguments[i]);
	}

	return getNestedProperty.apply(this, args);
}

// ---------------------------- applyDuplicates ----------------------------
/**
 * <p>
 *   This function is used to apply a given function on every item in the
 *   given list where they key is the same.  This is done by using the value
 *   of the key on each item as a hash key onto a new object.  Thus, if
 *   object[list[0][key]] will assign to the same key on "object" as
 *   object[list[1][key]], then the items at index 0 and 1 will be passed to
 *   the function.
 * </p>
 * <p>
 *   For example, the following code:<br/>
 *   <code>
 *     var obj1 = {a: "name 1"}, obj2 = {a: "name 2"}, obj3 = {a: "name 1"};<br/>
 *     var list = [obj1, obj2, obj3];<br/>
 *     applyDuplicates(list, "a", function(dup) { dup.a = dup.a + " DUP"; });<br/>
 *   </code>
 *   will result in obj1.a being changed to "name 1 DUP" and obj3.a being
 *   changed to "name 1 DUP".
 * </p>
 * <p>
 *   Nothing will happen for a null list.  Null and undefined key values are
 *   treated as a special key that will match with eachother, triggering the
 *   function call for them.
 * </p>
 * @param list
 *     The list to apply the function on duplicate elements.
 * @param key
 *     The key used on each item to determine duplicity.
 * @param fn
 *     The function to apply on all items that have duplicate values.
 */
OSM.util.applyDuplicates = function(list, key, fn) {
	if (isUndefinedOrNull(list)) {
		return;
	}

	var nullOrUndefined = [];
	var duplicates = {};

	// Store each item in an array within the duplicates hash, but append to
	// the array any duplicate key values.  Store items with null or
	// undefined key values in a separate list.
	forEach(list, function(item) {
		var value = item[key];

		if (isUndefinedOrNull(value)) {
			nullOrUndefined.push(item);
			return;
		}

		if (isUndefinedOrNull(duplicates[value])) {
			duplicates[value] = [item];
		} else {
			duplicates[value].push(item);
		}
	});

	// For each duplicate array with more than 1 element, apply the passed
	// in function.  Apply the function on the list with null or undefined
	// keys first.
	var applyFn = function(duplicatesArray) {
		if (duplicatesArray.length > 1) {
			forEach(duplicatesArray, fn);
		}
	};
	applyFn(nullOrUndefined);
	forEach(values(duplicates), applyFn);
}

/**
 * This very simple function takes a frame argument and then sets the height of
 * that frame to be 100% of its framed page's height.  It does this by setting
 * the frame's height based on the innerHeight + scrollMaxY of its content
 * window, + 30 pixels (for a little extra padding at the bottom)
 * 
 * @author Jeremy
 */
OSM.util.resizeFrameToFillPage = function(frame) {
	if (isFinite(frame))
		frame = this;
	var win = frame.contentWindow;
	if (win && win.innerHeight) {
		height = win.innerHeight +  win.scrollMaxY;
	} else if (win.document && win.document.body && win.document.body.scrollHeight) {
	    height = win.document.body.scrollHeight;
	} 
	// window.status = height;
	if (isNaN(height))
		height = 1000;
	frame.style.height = (height + 30) + "px";
}

OSM.util.showDialog = function(name, callback) {
	OSM.util.show(name + "Dialog");
	$$("#" + name + "Dialog").dialog("open");
	if (callback)
		callback();
}

OSM.util.hideDialog = function(name) {
	$$("#" + name + "Dialog").dialog("close");
	OSM.util.hide(name + "Dialog");
}

/**
 * Simple utility method which returns the parameters that would have come from
 * the page's INPUTs/SELECTs (if their parent FORM had been submitted), appended
 * to the provided URL
 * 
 * @param {Object} url
 */
OSM.util.addParams = function(url) {
  return url + $$("INPUT, SELECT").serialize();
}

// Dialog Event Hook-ups
jQuery(function() {
	// Re-size any full-page IFRAMEs
	jQuery("IFRAME.frame_smart").each(OSM.util.resizeFrameToFillPage);
	
	// Hook-up JS for any dialogs on the page
	jQuery(".dialog").each(function() {
		var $this = $$(this);
		$this.dialog({
			"autoOpen":false,
			"bgiframe":true,
			"closeOnEscape":true,
			"dialogClass":$this.is(".waiting") ? "waiting" : null,
			"draggable":false,
			"modal":true,
			"resizable":false,
			"width":500 // Change to "-1" for 100% width
		});
	});
	
	// Hook-up JS for any dialog triggers on the page
	jQuery(".dialog_trigger").click(function (e) {
		var name = this.id.substring(0, this.id.length -13);
		OSM.util.showDialog(name);
	});

});

(function($) {
    function areYouSureify(href) {
        var link = $(this);
        if (link.attr("areYouSureified")) {
            return;
        }

        link.attr("areYouSureified", true);
        link.after(
                // please don't re-format the spacing in here, it will
                // affect the rendered html which is tricky with the
                // non-breaking spaces that are formatting this
                // message
                '<span class="areyousure" style="display: none;">' +
                '<span class="hot">Are&nbsp;you&nbsp;sure?</span>' +
                ' <a href="' + href + '">yes</a>' +
                '&nbsp;/&nbsp;' +
                '<a href="#">no</a>' +
                '</span>');
        var areyousureSpan = link.next();
        $("a", areyousureSpan).eq(1).click(function() {
            link.show();
            areyousureSpan.hide();
            return false;
        });

        link.click(function() {
            areyousureSpan.show();
            link.hide();
            return false;
        });
    }

    $.fn.areYouSure = function(options) {
        return $(this).each(function(i) {
            var href = null;

            if ($(this).is("a")) {
                href = $(this).attr("href");
            } else {
                // The href needs to be set manually if the element
                // isn't a link... this will fail (intentionally) if
                // options is null.
                href = options["href"];
            }

            areYouSureify.call(this, href);
        });
    };
})(jQuery);

function setCookie(c_name,value,expiredays)
{
    var exdate=new Date();
    exdate.setDate(exdate.getDate()+expiredays);
    document.cookie=c_name+ "=" +escape(value)+
    ((expiredays==null) ? "" : ";expires="+exdate.toUTCString());
}

function getCookie(c_name)
{
    if (document.cookie.length>0)
    {
        c_start=document.cookie.indexOf(c_name + "=");
        if (c_start!=-1)
        {
        c_start=c_start + c_name.length+1;
        c_end=document.cookie.indexOf(";",c_start);
        if (c_end==-1) c_end=document.cookie.length;
        return unescape(document.cookie.substring(c_start,c_end));
        }
    }
    return "";
}
     
/**
 * Adds a "this is required" star to the label TD for the provided CQ names/IDs
 * @param cqNameId one or more custom question names/IDs
 */
function addStar(cqNameId) {
	$$.each(cqNameId, function(i, current) {
		var $cqNode = $$('#' + current);
		// Check by name if not found by ID
		$cqNode = $cqNode.length ? $cqNode : $$('[name=' + current + ']:eq(0)');
		if (!$cqNode.length) {
			return null;
		}
		var $nodeToStar = $cqNode.closest('TR');
		var $nodeToStarTD = $nodeToStar.find('TD[class~=label]');
		while (!$nodeToStar.is(':visible') || !$nodeToStarTD.length) {
			$nodeToStar = $nodeToStar.prev();
			$nodeToStarTD = $nodeToStar.find('TD[class~=label]');
		}
		$nodeToStarTD.removeClass().addClass('form_label_required');
	});
}
/**
 * js_forms.js
 * A function library for Acrobat plug-in checking and cookie manipulation
 */

var osmWinParams = "width=600,height=365,resizable=yes,scrollbars=yes";

/**
 * Determine whether the user has the Acrobat Reader ActiveX control by 
 * attempting to instantiate it
 * 
 * @return true if the ActiveX control was successfully instantiated,
 * false otherwise
 * 
 */
function tryActiveXAcro() {
	try {
		acro = new ActiveXObject("PDF.PdfCtrl.1");
		if(!acro) return;
		acro = null;
		return true;
	} catch (e) { return false; }
}

function checkAcrobat(url, doPopup) {
	if(getCookie('HasAcrobat') != 1) {
		if(tryActiveXAcro()) {
			setThreeYearCookie('HasAcrobat');
			window.location = url;
		} else {
			var acroUrl = "/acrobat_help.jsp?url=" + escape(url);
			if(doPopup) {
				var zwindow = window.open(acroUrl, "usePolicy", osmWinParams);
				zwindow.focus();
			} else
				window.location = acroUrl + "&popup=no";
		}
	} else window.location = url;
}

function checkAcrobatAlt(url, alt, doPopup) {
	if(getCookie('HasAcrobat') == 1) window.location = url;
	else if(getCookie('HasAcrobat') == 0) window.location = alt;
	else {
		if(!document.layers && tryActiveXAcro()) {
			setThreeYearCookie('HasAcrobat');
			window.location = url;
		} else {
			var acroUrl =
				"/acrobat_help.jsp?url=" + escape(url) + "&alt=" + escape(alt);
			if(doPopup) window.open(acroUrl, "usePolicy", osmWinParams).focus();
			else window.location = acroUrl + "&popup=no";
		}
	}
}

function changeCookie (name, isChecked) {
	if (isChecked) setThreeYearCookie(name);
	else deleteCookie(name);
}

function setThreeYearCookieVal(name, value) {
	var expireDate = new Date();
	expireDate.setTime(expireDate.getTime() + 1000*60*60*24*365*3);
	document.cookie = name + "=" + value + ";expires="+expireDate.toGMTString();
}

function setThreeYearCookie(name) {
	setThreeYearCookieVal(name, 1);
}

function deleteCookie (name) {
	var oldDate = new Date();
	oldDate.setFullYear(1970); // the expiredate is set to Jan. 1st 1970
	document.cookie = name + "=0" + "; expires="+oldDate.toGMTString();
}

/**
 * read a cookie value
 */
function getCookie (name) {
	var cookieString = document.cookie;
	var startIndex = cookieString.indexOf(name);
	if(startIndex == -1) return (-1);	// the cookie doesn't exist!
	startIndex += name.length + 1;
	endIndex = cookieString.indexOf(";", startIndex);
	if (endIndex == -1) endIndex = cookieString.length;
	return unescape(cookieString.substring(startIndex, endIndex));
}

/**
	js_dateFunctions.js
	===================
	Author:  Scott Jones
	Date:  10/5/2000
	===========================
	Date manipulation functions.
*/
var sixDigitDateRegExp = /^(\d{2})(\d{2})(\d{2})$/;
var eightDigitDateRegExp = /^(\d{2})(\d{2})(\d{4})$/;
var slashDelineatedDateRegExp = /^(\d{1,2})\/(\d{1,2})\/(\d{2,4})$/;
var dashDelineatedDateRegExp = /^(\d{1,2})\-(\d{1,2})\-(\d{2,4})$/;
var ONE_DAY = 24 * 60 * 60 * 1000;

/**
 * Takes a string in the form "10/05/2000" and returns a JS Date object of it
 * @return a JS Date object corresponding to the provided string (unless the
 * provided string is unparseable, in which case null is returned)
 */
function parseDate(dateString) {
	// Try to find a match using the various regular expressions 
	var match = sixDigitDateRegExp.exec(dateString);
	if(!match) match = eightDigitDateRegExp.exec(dateString);
	if(!match) match = slashDelineatedDateRegExp.exec(dateString);
	if(!match) match = dashDelineatedDateRegExp.exec(dateString);
	// If no match was found, return null
	if(!match) return null;
	
	// Get the month/day/year from the regular expression match groups
	var month = parseInt(match[1], 10);
	var day = parseInt(match[2], 10);
	var year = parseInt(match[3], 10);
	
	// If the month or day are impossible, return null
	if(month > 12 || day > 31) return null;
	
	// If the year is 2-digits, add the appropriate amount to make it 4-digits
	// ("appropriate" means that if the date is 20-99 we assume it's 1920-1999,
	// whereas if it's anything else we assume it is 20-whatever else) 
	if(year < 100) year += year < 20 ? 2000 : 1900;
	
	// Build the actual Date object, and then return it
	var theDate = new Date();
	theDate.setYear(year);
	theDate.setDate(1); // so that 31 day month doesn't mess things up!!!
	theDate.setMonth(month-1);
	theDate.setDate(day);
	return theDate;
}

/**
 * Adds the provided number of months to the provided date
 * @return a JS Date object numMonths months after theDate
 * 
 */
function addMonths (theDate, numMonths) {
	var newDay = theDate.getDate();
	var newMonth = theDate.getMonth() + numMonths;
	var newYear = theDate.getFullYear();
	if (newMonth > 11) {
		// we're moving into a new year!
		newYear += Math.floor(newMonth/12);
		newMonth = newMonth % 12;
	}
	// find the last day of the month that we're moving to
	var maxDaysInMonth = numDaysInMonth(newMonth, newYear);
	
	if (maxDaysInMonth < newDay) {
		newDay = maxDaysInMonth;
	} else {
		newDay -= 1;
	}
	return new Date (newYear, newMonth, newDay);
}

/** 
 * Gives the number of days in a given month
 * @param month
 * @param year
 * @return the number of days in the month
 */
function numDaysInMonth (month, year) {
	var monthLengths = [31,28,31,30,31,30,31,31,30,31,30,31];
	// if NOT February
	if(month != 1) return monthLengths[month];
	// if February
	return ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) ? 29 : 28;
}

/**
 * Adds the provided number of days to the provided date
 * @return a JS Date object numDays days after theDate
 * 
 * Note: This actually adds (numDays * 24 hours) + 1 hour, to solve DST bugs
 */
function addDays (theDate, numDays) {
	var anHour = 60*60*1000;
	var newDate = new Date(theDate.getTime() + numDays*24*anHour + anHour);
	return newDate;
}

/**
 * Finds the date of the last day of the month for the provided date's month
 */
function dateToMonthEnd(theDate) {
	var month = theDate.getMonth();
	while (theDate.getMonth() == month) {
		theDate.setDate(theDate.getDate() + 1);
	}
	theDate.setDate(theDate.getDate() - 1);
	return theDate;
}

/**
 * Converts a JS Date object in to a string in the form MM/DD/YYYY
 */
function formatDate(theDate) {
	var month = (theDate.getMonth() + 1);
	return month + "/" + theDate.getDate() + "/" + theDate.getFullYear();
}

/**
 * Determine the number of days in the provided date's month
 * @return the length of the provided date's month
 */
function getMonthLength(theDate) {
	var monthLengths = new Array(31,28,31,30,31,30,31,31,30,31,30,31);
	var month = theDate.getMonth();
	// if NOT February
	if(month != 1) return monthLengths[month];
	// if February
	if((theYear % 4 == 0 && theYear % 100 != 0) || theYear % 400 == 0)
		return 29;
	else return 28;
}

/**
 * Determine whether the provided date string can be parsed in to a date object,
 * and alert the user if it is not
 */
function checkDate(element, name, noAlert) {
	var zDate = parseDate(element.value);
	if (zDate != null) {
		element.value = formatDate(zDate);
		return true;
	} else {
		if(name == null) name = "";
		if(name.indexOf("date") == -1) name += " date";
		if (!noAlert)
			alert("Please check that the " + name + " is in\n the format M/D/YYYY.");
		element.focus();
		element.select();
		return false;
	}
}

/**
 * Like checkDate, except that it takes an extra "required" argument, and 
 * returns true automatically if that argument is false and the provided element
 * has no value 
 */
function checkDateIfRequired(element, name, required) {
	if (!required && element.value.length == 0) return true;
	else return checkDate(element, name);
}

/**
 * Compares two dates.
 * 
 * Parameters dateString1 and dateString2 must be valid date strings.
 * Standard short date formats such as "11/26/07" and "11-26-2007" are
 * acceptable, as are longer date formats such as "November 26, 2007 5:50 PM".
 * 
 * Returns a negative integer if dateString1 represents an earlier date than
 * dateString2,  a positive integer if dateString1 represents a later date than
 * dateString2, and zero if they both represent the same date.
*/
function compareDates(endDateString, startDateString) {
    /* note that default parse will set to beginning of date 0:00:00
     * need to set to endDate to end of day, 23:59:59 for proper
     * length calculation
     */
    var endDate = new Date(Date.parse(endDateString));
    endDate.setHours(23);
    endDate.setMinutes(59,59,999);

    // make startDate a date object for symmetry
    var startDate = new Date(Date.parse(startDateString));
    
    /* add back in one millisecond to allow for proper division in dependent code */
    return ((endDate.getTime() + 1) - startDate.getTime());
}


/**
 * This function will return false if and only if the element contains
 * a properly formatted date that is not at least 1 year in the past.
 * 
 * Note that this method does not take into account the time of day. 
 */
function checkDateOneYearInPast(element, name) {
	// Allow null values
	if (element == null || element.value == null || element.length == 0)
		return true;
	
	var checkDate = parseDate(element.value);
	// This method only rejects properly formatted dates that are
	// not in the past. If we are unable to parse the date, then
	// this method will not reject it.
	if (checkDate == null) return true;
	
	var now = new Date();
	var currentYear = now.getFullYear() - 1;
	var checkYear = checkDate.getFullYear();
	// If this date is at least 1 year in the past, return true
	if(checkYear < currentYear) return true;
	if(checkYear == currentYear) {
		var currentMonth = now.getMonth();
		var checkMonth = checkDate.getMonth();
		if(checkMonth < currentMonth) return true;
		if(checkMonth == currentMonth && checkDate.getDate() < now.getDate())
			return true;
	}
	
	// This is a well-formatted date that is not in the past. Display
	// an alert message and focus on this element.
	if (name == null) name = "";
	if (name.indexOf("date") == -1) name += " date";
	alert ("Please check that the " + name + " is at least 1 year in the past.");
	element.focus();
	element.select();
	return false;
}


/**
 * Accepts a JavaScript Date object and returns another Date object representing the same
 * day, but with the time set to 00:00:00:000, which represents the start of the day.
 */
function setToStartOfDay(date) {
	var newDate = new Date();
	newDate.setTime(date.getTime());
    newDate.setHours(0);
    newDate.setMinutes(0);
    newDate.setSeconds(0);
    newDate.setMilliseconds(0);
	return newDate;
}


/**
 * Accepts a JavaScript Date object and returns another Date object representing the first day
 * of the original date's month. The time of the returned date is set to 00:00:00:000, which 
 * represents the start of the day.
 */
function firstDayOfMonth(date) {
	var newDate = new Date();
	newDate.setTime(date.getTime());
	newDate.setDate(1);
    newDate.setHours(0);
    newDate.setMinutes(0);
    newDate.setSeconds(0);
    newDate.setMilliseconds(0);
	return newDate;
}


/**
 * Creates a new date using specified day, month, and year parameters. Month is
 * zero-based, so to create date January 15, 2008, call createDate(15, 0, 2008). 
 * The returned date is set to the start of the specified day.
 */
function createDate(day, month, year) {
    var date = new Date();
    date.setFullYear(year, month, day);
    date.setHours(0);
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
    return date;
}


/**
 * Returns a boolean value indicating whether two JavaScript Date objects are on the same day.
 */
function areSameDay(date1, date2) {
	var d1 = setToStartOfDay(date1);
	var d2 = setToStartOfDay(date2);
	return d1.getTime() == d2.getTime();
}

function parseDouble(numString) {
	var negative = false;
	if (numString.indexOf("-") != -1) {
		numString = numString.substring(numString.indexOf('-')+1);
		negative = true;
	} else if (numString.indexOf("(") != -1 && numString.indexOf(")") != -1) {
		numString = numString.substring(numString.indexOf('(')+1, numString.indexOf(')'));
		negative = true;
        }        
	if (numString.indexOf("$") != -1)
		numString = numString.substring(numString.indexOf('$')+1);
	while (numString.indexOf(",") != -1) {
		var index = numString.indexOf(",");
		numString = numString.substring(0, index) + numString.substring(index + 1);
	}
	num = parseInt(numString);			
	if (isNaN(num))
		return num;	// return NaN for use there...
	pointIndex = numString.indexOf(".")+1;
	if (pointIndex > 0 && pointIndex < numString.length) {
		dec = parseInt(numString.substring(pointIndex, pointIndex+1));
		if (isNaN(dec))
			return dec;
		num += dec/10;
		cents = parseInt(numString.substring(pointIndex+1, pointIndex+2));
		if (!isNaN(cents)) {
			num += cents/100;
			num = Math.round(num*100)/100;
		}
	}
	if (negative)
		num = -num;
	return num;
}

function moneyFormat(num) {
	var prefix="$";
	if (num < -0.005)
		prefix = "-$";
	num = Math.abs(num);
	firstPart = Math.floor(num);
	secondPart = Math.round((num-firstPart)*100);
        if (secondPart == 100){
	  firstPart += 1;
	  secondPart = 0;
        }
	var result = "";
	var isFirst = true;
	while (firstPart > 0 || isFirst) {
		isFirst = false;
		remainder = firstPart % 1000;
		firstPart -= remainder;
		firstPart = firstPart / 1000;
		if (remainder >= 100 || firstPart <= 0)
			result = remainder + result;
		else if (remainder >= 10)
			result = "0" + remainder + result;
		else 
			result = "00" + remainder + result;
		if (firstPart > 0)
			result = "," + result;
	}
	if (secondPart < 10) 
		return (prefix+result + ".0" + secondPart);
	else
		return (prefix+result + "." + secondPart);
}

function moneyFormatSafe(num) {
	num = parseDouble(num);

	if (isNaN(num)) {
		return moneyFormat(0);
	}

	return moneyFormat(num);
}

function checkMoney(element, name) {
	var amount = parseDouble(element.value);
	if (isNaN(amount)) {
		alert ("Please enter a numeric amount for the " + name + ".");
		element.focus();
		element.select();
		return false;
	}
	element.value=moneyFormat(amount);
	return true;
}

function checkMoneyIfRequired(element, name, required) {
	if (!required && element.value.length == 0) {
		return true;
	} else {
		return checkMoney(element, name);
	}
}

/**
 * Verifies that the value is a properly-formatted dollar-amount
 * (using checkMoney()) and also verifies that the value is at or
 * above the specified minimum.
 */
function checkMoneyMinimum(element, name, minValue, minFailureExplanation) {
	if (!checkMoney(element, name))
		return false;
	
	if (minValue != null) {
		var amount = parseDouble(element.value);
		if (amount < minValue) {
			if (minFailureExplanation != null)
				alert (minFailureExplanation);
			else
				alert ("The " + name + " must be at least " + moneyFormat(minValue));
			element.focus();
			element.select();
			return false;
		}
	}
	return true;
}


/**
 * Verifies that the value is a properly-formatted dollar-amount
 * (using checkMoney()) and also verifies that the value is at or
 * above the specified minimum. If the value is not required, then
 * this method will return TRUE for NULL values.
 */
function checkMoneyMinimumIfRequired(element, name, minValue, minFailureExplanation, required) {
	if (!required && element.value.length == 0)
		return true;
	else 
		return checkMoneyMinimum(element, name, minValue, minFailureExplanation);
}


function periodToMonthly(amount, period) {
	if (isNaN(amount))
		return amount;
	switch (period) {
	case 1:
		return amount * 4.3;
	case 3:
		return amount / 12.0;
	default:
		return amount;
	}
}	

/* Various utility functions used by billing .jsp and .js files */

/*
Requires:
  js_util.js
  js_moneyFunctions.js
*/

function areMoniesEqual(money1, money2) {
    var m1 = isNaN(money1) ? parseDouble(money1) : money1;
    var m2 = isNaN(money2) ? parseDouble(money2) : money2;
    if (isNaN(m1) || isNaN(m2))
        return false;
    return Math.abs(m1 - m2) < 0.00001;
}

function getParent(element) {
	if (element.parentNode) {
		return element.parentNode;
	}

	if (element.parentElement) {
		return element.parentElement;
	}

	return null;
}

function findErrorSpan(element) {
	var parent = getParent(element);

	if (parent == null) {
		return null;
	}

	parent = getParent(parent);

	if (parent == null) {
		return null;
	}

	var node = parent.firstChild;
	while (node != null) {
		if (node.className == "error") {
			return node;
		}

		node = node.nextSibling;
	}

	return null;
}

function checkMoneyValue(element, fieldDescription, allowZero) {
    var fieldValue = element.value;
    if (!checkMoney(element, fieldDescription))
        return false;
        
    var amount = parseDouble(fieldValue);
    var cutoffAmount = allowZero ? -0.00001 : 0.00001;
   	var errorSpan = findErrorSpan(element);

    if (!isNaN(amount) && amount < cutoffAmount) {
    	if (errorSpan == null) {
            alert ("Please enter an amount greater than zero for the " + fieldDescription + ".");
        } else {
        	errorSpan.innerHTML = "Please enter an amount greater than zero.";
        }

        element.focus();
        element.select();
        return false;
    }

    if (errorSpan != null) {
    	errorSpan.innerHTML = "";
    }

    return true;
}

function checkMoneyValueIsPositive(element, fieldDescription) {
    return checkMoneyValue(element, fieldDescription, false);
}

function checkMoneyValueIsNonNegative(element, fieldDescription) {
    return checkMoneyValue(element, fieldDescription, true);
}

function checkMoneyIsValidOrBlank(element, fieldDescription) {
    if (trim(element.value) == "")
        return true;
    if (!checkMoney(element, fieldDescription))
        return false;
    return true;
}

function setMoneyCellValue(cell, amount) {
    if (isNaN(amount)) {
        cell.style.color="";
        amount = "******";
    } else {
        if (amount < -0.00001)
            cell.style.color="red";
        else
            cell.style.color="";
        amount = moneyFormat(amount);
    }
    cell.innerHTML = amount;       
}

// Creates a new using specified day, month, and year parameters. The date is set to the start of the specified day.
function createDate(day, month, year) {
    var date = new Date();
    date.setFullYear(year, month, day);
    date.setHours(0);
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
    return date;
}

function validateAccountingDate(accountingDateInput, firstOpenDate, fieldDescription) {
    var accountingDate = Date.parse(accountingDateInput.value);
    // Date cannot be within a closed accounting month
    if (accountingDate < firstOpenDate) {
        alert("The " + fieldDescription + " is within an accounting month that has been closed.\n\n" +
              "Please select a more recent date.");
        accountingDateInput.focus();
        return false;
    }
    // Date cannot be more than 45 days in the future
    var maxDate = new Date();
    maxDate.setDate(maxDate.getDate() + 45);
    if (accountingDate > maxDate) {
        alert("The " + fieldDescription + " is too far in the future.\n\n" +
              "Please select another date.");
        accountingDateInput.focus();
        return false;
    }
    return true;
}

