/*
 2008-12-08 JFC Added imageAnchorBuilder function for slideshows
 2008-11-16 JFC Fixed "no empty cells at end of month" bug in SSCalendar
 2008-10-24 JFC Added SSCalendar
 2008-06-15 JFC Converted to jQuery
*/
String.prototype.trim = function() {
	return this.replace(/^\s+|\s+$/g,"");
};
String.prototype.ltrim = function() {
	return this.replace(/^\s+/,"");
};
String.prototype.rtrim = function() {
	return this.replace(/\s+$/,"");
};
String.prototype.right = function(len) {
	return this.substr(this.length-len);
};
String.prototype.left = function(len) {
	return this.substr(0,len);
};

// Namespace placeholder
//ss = {};

// Initialize things
$(document).ready(
	function() {
		initPup();
		initCalendars();
		if (initPageToc()>0) $('#onthispage').show();
		initQuote();
	}
);

function initPup() {
	// initPup() installs handlers the Pop-Up Pedigree buttons
	$(".pupbutton").click( pup );
};

function pup(e) {
	// pup() handles clicks on Pop-Up Pedigree buttons

	// Get the ID of the data element
	var $button = $(e.target);
	var partsID = $button.attr('id').replace(/pup/i, 'pupd');

	// Get the data and convert it into a table
	var sContent = getPupContent(partsID);

	// Add the new HTML to the pup element,
	// move it relative to button, and show it.
	var pos = $button.offset();
	$("#pup").html(sContent).css(
				{left: pos.left+'px', top: (pos.top+$button.height())+'px'}
			).show();
};

function getPupContent(partsID) {
	// Initialize the arrays
	var msg = "";
	var names = new Array(7);
	var stats = new Array(7);
	var links = new Array(7);
	var i, j;

	for (j=0; j<7; j++) {
		names[j] = "";
		stats[j] = "";
		links[j] = "";
	};

	// Get the data from the hidden DIV
	var sParts = $('#'+partsID).text();
	
	// Fix some potential encoding issues
	sParts = sParts.replace(/\\n/gi, '<br>');
	sParts = sParts.replace(/&lt;/gi, '<');
	sParts = sParts.replace(/&gt;/gi, '>');

	// Parse the sParts into the things we need
	var args = sParts.split("|");

	for (j=0, i=0; i<args.length && j<7;i++, j++) {
		names[j] = args[i++];
		if (names[j] == "") names[j]="&nbsp;";

		stats[j] = args[i++];

		if (args[i] != "") {
			links[j] = '<a href="'+args[i]+'">'+names[j]+'</A>';
		} else {
			links[j] = names[j];
		};
	};

	// Start the content
	var s1='<table><tbody>\n';

	// Build the table from the parts

	// Row 1: 3 cells (0, 1, 3)
	s1 += '<tr><td width="30%" rowspan="4" class="pupsubject">'+links[0]+'<br>'+stats[0]+'</td>';
	s1 += '<td width="30%" rowspan="2" class="pupmale">';
	s1 += links[1]+'<br>'+stats[1]+'</td>';
	s1 += '<td width="40%" class="pupmale">';
	s1 += links[3]+'<br>'+stats[3]+'</td></tr>\n';

	// Row 2: 1 cell (4)
	s1 += '<tr><td width="40%" class="pupfemale">'+links[4]+'<br>'+stats[4]+'</td></tr>\n';

	// Row 3: 2 cells (2, 5)
	s1 += '<tr><td width="30%" rowspan="2" class="pupfemale">'+links[2]+'<br>'+stats[2]+'</td>';
	s1 += '<td width="40%" class="pupmale">';
	s1 += links[5]+'<br>'+stats[5]+'</td></tr>\n';

	// Row 4: 1 cell (6)
	s1 += '<tr><td width="40%" class="pupfemale">'+links[6]+'<br>'+stats[6]+'</td></tr>\n';

	s1 += '<tr><td colspan="3"><input type="button" class="pupbutton" value="Close"'+
			' onclick="$(\'#pup\').hide();"></td></tr>\n';

	// End the content
	s1 += '</tbody></table>\n';

	return s1;
};

function initPageToc() {
	// The page "table of contents" creates a list of
	// h2 tags on the page if it finds an element with
	// the id "pagetoc". It only functions if
	// JavaScript is supported, and enabled, and if
	// the user has added HTML similar to the
	// following in one of the static content
	// sections:
	
	// <div id="pagetoc"></div>
	//    or
	// <ul id="pagetoc"></ul>
	
	// 2007-04-06 JFC Now allows pagetoc on UL or OL, or on
	//                container DIV

	var $outerElement = $('#pagetoc');
	var nElements = 0;

	if ($outerElement.length>0) {
		var $tocElement = $outerElement.children("ul,ol").filter(":first");

		// Toggling display from none to block speeds
		// up execution, presumably because the browser
		// ignores the new content as it is being
		// added, and only computes locations, size, etc.
		// once at the end. That's just a guess based
		// on observing Firefox. If it actually isn't
		// faster, it seems like it, and that's good
		// enough for me in this case.
		$outerElement.hide();

		if ($tocElement.length>0) {
			// ul or ol is outer element
			$tocElement = $outerElement;
			// If ul or ol has elements, remove the first.
			// That way, user can provide
			//     <ul id="pagetoc"><li></li></ul>
			// to avoid validation errors.
			$tocElement.find(':first-child').remove();
		} else {
			$tocElement = ($outerElement).append("<ul></ul>").children();
		};
		nElements = addPageToc($tocElement, $('#content'), 0);
		$outerElement.show();
	};
	return nElements;
};

function addPageToc($tocElement, $parent, tags) {
	$parent.children().each( function(index) {
		tags = addPageToc($tocElement, $(this), tags);
		if (ss.pageTocElements.indexOf(this.tagName.toLowerCase()) != -1) {
			var eListElement = document.createElement('li');
			eListElement.className = 'toc'+this.tagName.toLowerCase();

			// Get text of H2 and add to TOC as link

			// Remove all child elements; we want H2 text only
			var sItemText = $(this).clone().children().remove().end().text();
			if (sItemText=='') sItemText = $(this).text();
			var nText = document.createTextNode(sItemText);
			var eLinkElement = document.createElement('a');
			eLinkElement.appendChild(nText);
			eListElement.appendChild(eLinkElement);

			// Set ID of H2 node if it doesn't have one
			if (!this.id) this.id = 't'+tags;

			// Set target of the A tag we are adding to ID of H2
			eLinkElement.href = '#' + this.id;
			$tocElement.append(eListElement);
			tags++;
		};
	});
	return tags;
};

function initQuote() {
	// Initializes the "random quote" facility
	
	// <ul class="randomquote"><li> ... </li> etc. </ul>
	//      or
	// <div class="randomquote"><ul><li> ... </li> etc. </ul></div>

	$('ul.randomquote,div.randomquote ul').each( function() {
		pickQuote(this);
	});
	$('ul.randomquote,div.randomquote,div.randomquote ul').show();
};

function pickQuote(eUL) {
	// Hide all but one child (LI) element
	var $listItems = $(eUL).children('li');
	var iIndex = Math.round(Math.random()*($listItems.length-1));
	$listItems.hide();
	$listItems.eq(iIndex).show();
};

function hemlink(part1, part2) {
	var loc = '';

	loc = 'm'+"A"+"i";loc=loc+"l"+"to"+":";
	loc = loc.toLowerCase() + part1+"@"+part2;
	loc = loc+'?SUBJECT='+document.title;
	location.href=loc;
};

function hemlinknc(part1, part2) {
	var loc = '';

	loc = 'm'+"A"+"i";loc=loc+"l"+"to"+":";
	loc = loc.toLowerCase() + part1+"@"+part2;
	location.href=loc;
};

function hemx() {
	hemlinknc('John','JohnCardinal.com');
};

function tip(on_this, on_event, content) {
	return makeTrue(domTT_activate(on_this, on_event, 'content', content));
};

function tipcap(on_this, on_event, content, caption) {
	return makeTrue(domTT_activate(on_this, on_event, 'content', content, 'caption', caption));
};

/*
	parseUri 1.2.1
	(c) 2007 Steven Levithan <stevenlevithan.com>
	MIT License
*/
function parseUri (str) {
	var o = parseUri.options,
		m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
		uri = {},
		i = 14;

	while (i--) uri[o.key[i]] = m[i] || "";

	uri[o.q.name] = {};
	uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
		if ($1) uri[o.q.name][$1] = $2;
	});

	return uri;
};

parseUri.options = {
	strictMode: false,
	key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
	q:   {
		name:   "queryKey",
		parser: /(?:^|&)([^&=]*)=?([^&]*)/g
	},
	parser: {
		strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
		loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
	}
};

// Load framed Second Site pages in proper frame 

function ssFramer(destFrame) {
	var sDestFrame = destFrame;
	$(document).ready( function() {
		var kFrameset = "index.htm";
		var parentUri = parseUri(parent.location.href);
		var windowUri = parseUri(window.location.href);

		if (windowUri.file == '' ||
				windowUri.file.toLowerCase() == kFrameset) {
			// Handle parent page
			// Argument format: ?framename1=uri;anchor&framename2=uri;anchor...
			// Ex: ?ssmain=p1.htm;i32&ssindex=i1.htm;s37
			// Loads: p1.htm#i32 in frame "ssmain"
			// Loads: i1.htm#s37 in frame "ssindex"

			// Process frame arguments
			for (var sArg in windowUri.queryKey) {
				// Get frame URIs from frame name/anchor arguments
				var parts = windowUri.queryKey[sArg].split(';');
				window.frames[sArg].location.href = parts[0]+(parts.length>1 ? '#'+parts[1] : '');
			};

		} else {
			// Handle child page
			if (window.location.href == parent.location.href) {
				// Not in parent frame; reload parent
				// with arguments set to load child
				var sUrl = kFrameset+'?'+sDestFrame+'='+windowUri.file+';'+windowUri.anchor;
				window.location.href=sUrl;
			};
		};
	});
};

var ss = function() {
	var bMapEditor = false;
	var bHaveConsole = false;

	/**
	 * parseColor parses CSS color values. It is based
	 * on RGBColor by Stoyan Stefanov <sstoo@gmail.com>,
	 * rewritten by John Cardinal to improve performance and
	 * make it more compatible with my applications and coding
	 * style.
	 */

	function parseColor(sColor) {
		// Constructor for parseColor object.
		// sColor is optional; if provided,
		// it will be parsed. Otherwise, call the
		// parseColor.parse(sColor) method.

		var self = this;
		this.r = this.g = this.b = 0;
		this.color_names = {
			'aliceblue': 'f0f8ff',
			'antiquewhite': 'faebd7',
			'aqua': '00ffff',
			'aquamarine': '7fffd4',
			'azure': 'f0ffff',
			'beige': 'f5f5dc',
			'bisque': 'ffe4c4',
			'black': '000000',
			'blanchedalmond': 'ffebcd',
			'blue': '0000ff',
			'blueviolet': '8a2be2',
			'brown': 'a52a2a',
			'burlywood': 'deb887',
			'cadetblue': '5f9ea0',
			'chartreuse': '7fff00',
			'chocolate': 'd2691e',
			'coral': 'ff7f50',
			'cornflowerblue': '6495ed',
			'cornsilk': 'fff8dc',
			'crimson': 'dc143c',
			'cyan': '00ffff',
			'darkblue': '00008b',
			'darkcyan': '008b8b',
			'darkgoldenrod': 'b8860b',
			'darkgray': 'a9a9a9',
			'darkgrey': 'a9a9a9',
			'darkgreen': '006400',
			'darkkhaki': 'bdb76b',
			'darkmagenta': '8b008b',
			'darkolivegreen': '556b2f',
			'darkorange': 'ff8c00',
			'darkorchid': '9932cc',
			'darkred': '8b0000',
			'darksalmon': 'e9967a',
			'darkseagreen': '8fbc8b',
			'darkslateblue': '483d8b',
			'darkslategray': '2f4f4f',
			'darkslategrey': '2f4f4f',
			'darkturquoise': '00ced1',
			'darkviolet': '9400d3',
			'deeppink': 'ff1493',
			'deepskyblue': '00bfff',
			'dimgray': '696969',
			'dimgrey': '696969',
			'dodgerblue': '1e90ff',
			'firebrick': 'b22222',
			'floralwhite': 'fffaf0',
			'forestgreen': '228b22',
			'fuchsia': 'ff00ff',
			'gainsboro': 'dcdcdc',
			'ghostwhite': 'f8f8ff',
			'gold': 'ffd700',
			'goldenrod': 'daa520',
			'gray': '808080',
			'grey': '808080',
			'green': '008000',
			'greenyellow': 'adff2f',
			'honeydew': 'f0fff0',
			'hotpink': 'ff69b4',
			'indianred': 'cd5c5c',
			'indigo': '4b0082',
			'ivory': 'fffff0',
			'khaki': 'f0e68c',
			'lavender': 'e6e6fa',
			'lavenderblush': 'fff0f5',
			'lawngreen': '7cfc00',
			'lemonchiffon': 'fffacd',
			'lightblue': 'add8e6',
			'lightcoral': 'f08080',
			'lightcyan': 'e0ffff',
			'lightgoldenrodyellow': 'fafad2',
			'lightgreen': '90ee90',
			'lightgray': 'd3d3d3',
			'lightgrey': 'd3d3d3',
			'lightpink': 'ffb6c1',
			'lightsalmon': 'ffa07a',
			'lightseagreen': '20b2aa',
			'lightskyblue': '87cefa',
			'lightslategray': '778899',
			'lightslategrey': '778899',
			'lightsteelblue': 'b0c4de',
			'lightyellow': 'ffffe0',
			'lime': '00ff00',
			'limegreen': '32cd32',
			'linen': 'faf0e6',
			'magenta': 'ff00ff',
			'maroon': '800000',
			'mediumaquamarine': '66cdaa',
			'mediumblue': '0000cd',
			'mediumorchid': 'ba55d3',
			'mediumpurple': '9370db',
			'mediumseagreen': '3cb371',
			'mediumslateblue': '7b68ee',
			'mediumspringgreen': '00fa9a',
			'mediumturquoise': '48d1cc',
			'mediumvioletred': 'c71585',
			'midnightblue': '191970',
			'mintcream': 'f5fffa',
			'mistyrose': 'ffe4e1',
			'moccasin': 'ffe4b5',
			'navajowhite': 'ffdead',
			'navy': '000080',
			'oldlace': 'fdf5e6',
			'olive': '808000',
			'olivedrab': '6b8e23',
			'orange': 'ffa500',
			'orangered': 'ff4500',
			'orchid': 'da70d6',
			'palegoldenrod': 'eee8aa',
			'palegreen': '98fb98',
			'paleturquoise': 'afeeee',
			'palevioletred': 'db7093',
			'papayawhip': 'ffefd5',
			'peachpuff': 'ffdab9',
			'peru': 'cd853f',
			'pink': 'ffc0cb',
			'plum': 'dda0dd',
			'powderblue': 'b0e0e6',
			'purple': '800080',
			'red': 'ff0000',
			'rosybrown': 'bc8f8f',
			'royalblue': '4169e1',
			'saddlebrown': '8b4513',
			'salmon': 'fa8072',
			'sandybrown': 'f4a460',
			'seagreen': '2e8b57',
			'seashell': 'fff5ee',
			'sienna': 'a0522d',
			'silver': 'c0c0c0',
			'skyblue': '87ceeb',
			'slateblue': '6a5acd',
			'slategray': '708090',
			'slategrey': '708090',
			'snow': 'fffafa',
			'springgreen': '00ff7f',
			'steelblue': '4682b4',
			'tan': 'd2b48c',
			'teal': '008080',
			'thistle': 'd8bfd8',
			'tomato': 'ff6347',
			'turquoise': '40e0d0',
			'violet': 'ee82ee',
			'wheat': 'f5deb3',
			'white': 'ffffff',
			'whitesmoke': 'f5f5f5',
			'yellow': 'ffff00',
			'yellowgreen': '9acd32'
		};

		// Array of functions to parse CSS color strings
		this.parsers = [
			// Handles "ffffff" form
			function(sColor) {
				if (sColor.length==6) {
					self.r = parseInt(sColor.substr(0,2), 16);
					self.g = parseInt(sColor.substr(2,2), 16);
					self.b = parseInt(sColor.substr(4,2), 16);
					return true;
				};
			},
			// Handles "fff" form
			function(sColor) {
				if (sColor.length==3) {
					var c = sColor.charAt(0);
					self.r = parseInt(c+c, 16);
					c = sColor.charAt(1);
					self.g = parseInt(c+c, 16);
					c = sColor.charAt(2);
					self.b = parseInt(c+c, 16);
					return true;
				};
			},
			// Handles "rgb(255,255,255)" form
			function(sColor) {
				var parts = sColor.match(/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/);
				if (parts) {
					self.r = parseInt(parts[1]);
					self.g = parseInt(parts[2]);
					self.b = parseInt(parts[3]);
					return true;
				};
			},
			// Last variation sets values to 0
			function(sColor) {
				self.r = self.g = self.b = 0;
				return false;
			}
		];

		if (sColor) {
			this.parse(sColor);
		};
	};

	parseColor.prototype.toHex = function() {
		var rX = ('0'+this.r.toString(16)).right(2);
		var gX = ('0'+this.g.toString(16)).right(2);
		var bX = ('0'+this.b.toString(16)).right(2);
		return '#' + rX + gX + bX;
	};

	parseColor.prototype.toRGB = function() {
		return 'rgb('+this.r+','+this.g+','+this.b+')';
	};

	parseColor.prototype.parse = function(sColor) {
		var result = false;

		// Remove #s and spaces
		sColor = sColor.toLowerCase().replace(/[# ]/g,'');

		// Check for named color
		if (this.color_names[sColor]) {
			sColor = this.color_names[sColor];
		};

		// Call the parsers until one indicates success
		for (var i = 0; i < this.parsers.length && !result; i++) {
			result = this.parsers[i](sColor);
		};

		if (result) {
			// validate/cleanup values
			this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);
			this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);
			this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);
		};

		return result;
	};

	return {
		enableMapEditor: function(enable) {	bMapEditor=enable; },
		isMapEditor: function() { return (bMapEditor); },

		extend: function(baseClass, subClass) {
			// Copied from Kevin Lindsey
			// http://www.kevlindev.com/tutorials/javascript/inheritance/index.htm
			function inheritance() {};
			inheritance.prototype = baseClass.prototype;

			subClass.prototype = new inheritance();
			subClass.prototype.constructor = subClass;
			subClass.baseConstructor = baseClass;
			subClass.superClass = baseClass.prototype;
		},

		fpdd: function(float) { return (float.toFixed(8)) },

		dMsg: function(sMessage) {
			if (!bHaveConsole) {
        $("body").append(
            '<div id="ssConsole">'+
            '<div id="ssConsoleHandle">'+
            '<button onclick="$(\'#ssConsoleContents\').empty()">'+
            		'Clear Messages</button>Messages<div style="clear: both;"></div></div>'+
            '<ol id="ssConsoleContents"></ol></div>');
        $("#ssConsole").css(
          { border: '1px solid black',
            background: '#aaa',
            padding: '1px',
            position: 'absolute',
            top: '0px',
            right: '0px',
            width: '25em'
          }
        );
        $("#ssConsole button").css(
        	{	float: 'right'
        	}
        );
        $("#ssConsoleHandle").css(
          { background: '#00f',
            color: '#fff',
            'font-family': 'sans-serif',
            'font-size': 'small',
            'font-weight': 'bold',
            cursor: 'move',
            padding: '2px'
          }
        );
        $("#ssConsoleContents").css(
          { background: '#fff',
          	clear: 'right',
            margin: '1px',
            overflow: 'auto',
            'max-height': '30em'
          }
        );
        $("#ssConsole").draggable(
          { handle: '#ssConsoleHandle',
            dragPrevention: "ol,li"
          }
        );
        bHaveConsole=true;
			};
			$("#ssConsoleContents").append('<li>'+sMessage + '</li>');
		},

		propsToString: function(theObject, prefix, suffix) {
			var sProps = '';
			var sDelim = '';

			for (var prop in theObject) {
				if (typeof(theObject[prop]) != 'function' &&
						typeof(theObject[prop]) != 'object') {
					sProps += '<span title="'+typeof(theObject[prop])+'">';
					sProps +=	(prefix ? prefix : sDelim) + '&nbsp;' + prop + ':' + theObject[prop] + (suffix ? suffix : '')+'</span>';
					sDelim = ', ';
				};
			};
			return sProps;
		},

		parseColor: parseColor,

		imageAnchorBuilder: function(index, slide, width, height) {
			return '<li><a href="#"><img src="' + slide.src + '" width="' + width + '" height="' + height + '"/></a></li>';
		},

		divAnchorBuilder: function(index, slide, width, height) {
			var src = $('img',slide).attr('src');
			return '<li><a href="#"><img src="' + src + '" width="' + width + '" height="' + height + '"/></a></li>';
		},

		pageTocElements: 'h2'
	}
}();

function initCalendars() {
	$("div.calendar").each( function() {
		var id=$(this).attr('id');
		var oCalendar = new SSCalendar(id);
		// Store object off the element so we
		// can access it via element
		$('#'+id).data('obj', oCalendar);
		oCalendar.getData();
	});
};

function SSCalendar(id) {
	this.id = id;
	this.cookieName = id+'|month|year';
	this.maxDays = 32;		// days in longest month, plus one
	this.calendarDate = new Date();

	var sCookie = $.cookie(this.cookieName);
	if (sCookie != null) {
		var sParts = sCookie.split('|');
		this.calendarDate = new Date(sParts[1], sParts[0], 1);
	};
};

SSCalendar.prototype.getData = function() {
	// Save data and make calendar
	this.data = eval('('+$('#caldata'+this.id).html()+')');
	$('#caldata'+this.id).html('');
	this.days = this.data.days;
	this.months = this.data.months;
	this.makeCalendar();
};

SSCalendar.prototype.makeCalendar = function() {
	var sW='';
	var iDay;
	this.calendarDate = new Date(this.calendarDate.getFullYear(), this.calendarDate.getMonth(), 1);

//for (var iMonth=0; iMonth<12; iMonth++) {
//this.calendarDate = new Date(2008,iMonth,1);

	var days = this.getDaysInMonth(this.calendarDate.getMonth());
	var sDays = this.getEventsForMonth(this.calendarDate.getMonth());
	var oCells = this.getCalendarCells(sDays, days, this.calendarDate.getDay());
	sW += this.getHTML(sDays, oCells);
//}
	$('#'+this.id+' div.caltable').html(sW);

	this.setHandlers();
};

SSCalendar.prototype.getHTML = function(sDays, oCells) {
	// getHTML creates the HTML to display a month.

	var nWeeks = oCells.length/7;
	var iMonth = this.calendarDate.getMonth();
	var iYear = this.calendarDate.getFullYear();
	var iCell = 0;
	var oCell;
	var sW = '';

	// Heading row with controls, month, year
	sW += '<table class="caltable"><thead>' +
			'<tr class="calmth"><th colspan="7">' +
			'<div style="margin: 0 auto; width: 18em;">' +
			'<button class="calprev"></button>' +
			'<button class="calnext"></button>' + this.months[iMonth]+' '+iYear +
			'</div></th></tr>';

	// Heading row with days of week
	sW += '<tr class="caldow">';
	for (iDay=0; iDay<7; iDay++) {
		sW += '<th>'+this.days[iDay]+'</th>';
	};
	sW += '</tr></thead>';

	// Rows with event data
	sW += '<tbody>';
	for (var iWeek=0; iWeek<nWeeks; iWeek++) {
		sW += '<tr>';
		for (var iDay=0; iDay<7; iDay++) {
			oCell = oCells[iCell];
			if (oCell.css != '') {
				sW += '<td class="'+oCell.css+'"';
				if (oCell.colspan>1) sW += ' colspan="'+oCell.colspan+'"';
				sW += '>';
				if (oCell.dayNumber>0) {
					sW += '<div class="caldn';
					if (iMonth==1 && oCell.dayNumber==29) {
						if (!this.isLeapYear(iYear)) sW += ' caldn29';
					};
					sW += '">'+oCell.dayNumber+'</div>';
				};
				if (oCell.daysIndex != -1) sW += sDays[oCell.daysIndex];
				sW += '</td>';
			};
			iCell++;
		};
		sW += '</tr>';
	};
	sW += '</tbody></table>';
	return sW;
};

SSCalendar.prototype.getEventsForMonth = function(nMonth) {
	/* Load event text into sDays array based on day number.
	   Note that months and days are 1-origin in JSON data.
	   Month events (no day) are stored in sDays[0].
	*/
	var sDays = new Array(this.maxDays);
	var iDay;
	var oCalEvt;
	var p, n;

	// Initialize the array
	for (iDay=0; iDay<this.maxDays; iDay++) sDays[iDay]='';

	for (iEvent=0; iEvent<this.data.events.length; iEvent++) {
		oCalEvt = this.data.events[iEvent];
		if (oCalEvt.m==nMonth+1) {
			iDay = oCalEvt.d;
			sDays[iDay] += '<div class="calevt">' +
					'<span class="calppl">'+oCalEvt.p+'</span>' +
					'<span class="calnot">('+oCalEvt.n+')</span>' +
					'</div>';
		};
	};

	// Fix HTML characters
	for (iDay=0; iDay<this.maxDays; iDay++) {
		sDays[iDay]=sDays[iDay].replace(/&gt;/gi, '>');
		sDays[iDay]=sDays[iDay].replace(/&lt;/gi, '<');
		sDays[iDay]=sDays[iDay].replace(/&amp;/gi, '&');
	};
	return sDays;
};

SSCalendar.prototype.getCalendarCells = function(sDays, days, firstDay) {
	/* Create cells for the grid that will represent the month.
	   Cells are used during rendering stage (getHTML()) to
	   control which days are in which cells, colspans, etc.
	*/
	var oCells = new Array;
	var iDay;
	var iCell;
	var extraCells;

	// Adjust February if no 29th day events
	// and not leap year
	if ((days==29) && (sDays[29].length==0)) {
		iYear = this.calendarDate.getFullYear();
		if (!this.isLeapYear(iYear)) days=28;
	};

	if (firstDay>2) {
		// We have at least 3 empty days at
		// front, so put month events there

		// Cell for month events
		oCells.push ( {
			css: (sDays[0].length==0) ? 'calemp':'caloth',
			colspan: (firstDay),
			dayNumber: 0,
			daysIndex: 0 } );

		// Placeholder cells
		for (iCell=1; iCell<firstDay; iCell++) {
			oCells.push ( {
				css: '',
				colspan: 1,
				dayNumber: 0,
				daysIndex: -1 } );
		};

		// Cells for days in month
		for (iDay=1; iDay<=days; iDay++) {
			oCells.push ( {
				css: 'calday',
				colspan: 1,
				dayNumber: iDay,
				daysIndex: iDay } );
		};

		// Empty cells after end of month
		extraCells = 7-(oCells.length%7);
		if (extraCells==7) extraCells=0;
		for (iCell=0; iCell<extraCells; iCell++) {
			oCells.push ( {
				css: "calemp",
				colspan: 1,
				dayNumber: 0,
				daysIndex: -1 } );
		};
	} else {
		// Put month events at end
		
		// Empty cells before start of month
		for (iCell=0; iCell<firstDay; iCell++) {
			oCells.push ( {
				css: "calemp",
				colspan: 1,
				dayNumber: 0,
				daysIndex: -1 } );
		};

		// Cells for days in month
		for (iDay=1; iDay<=days; iDay++) {
			oCells.push ( {
				css: "calday",
				colspan: 1,
				dayNumber: iDay,
				daysIndex: iDay } );
		};

		// Room for other events in last
		// week of month?
		extraCells = 7-(oCells.length%7);
		if (extraCells<=2) {
			for (iCell=0; iCell<extraCells; iCell++) {
				oCells.push ( {
					css: "calemp",
					colspan: 1,
					dayNumber: 0,
					daysIndex: -1 } );
			};
			extraCells=7;
		};

		// Write other cell and extra cells unless
		// it's a whole row and no month events
		if ((extraCells!=7) || (sDays[0].length>0)) {
			// Cell for month events
			oCells.push ( {
				css: (sDays[0].length==0) ? 'calemp':'caloth',
				colspan: extraCells,
				dayNumber: 0,
				daysIndex: 0 } );

			// Placeholder cells
			for (iCell=1; iCell<extraCells; iCell++) {
				oCells.push ( {
					css: '',
					colspan: 1,
					dayNumber: 0,
					daysIndex: -1 } );
			};
		};
	};
	return oCells;
};

SSCalendar.prototype.setHandlers = function() {
	/* setHandlers adds handlers for controls
	   that adjust current month.
	*/
	var oCal = this;

	$('#'+this.id+' button.calprev').click(function() {
		oCal.adjustMonth(-1);
		oCal.makeCalendar();
		return false;
	});

	$('#'+this.id+' button.calnext').click(function() {
		oCal.adjustMonth(1);
		oCal.makeCalendar();
		return false;
	});
};

SSCalendar.prototype.adjustMonth = function(delta) {
	/* adjustMonth is called by control handlers
	   to adjust the month up or down.
	*/
	var iMonth = this.calendarDate.getMonth()+delta;
	var iYear = this.calendarDate.getFullYear();
	$.cookie(this.cookieName, iMonth+'|'+iYear);
	this.calendarDate = new Date(iYear, iMonth, 1);
};

SSCalendar.prototype.getDaysInMonth = function(monthNo) {
	var days=[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
	return days[monthNo];
};

SSCalendar.prototype.isLeapYear = function(yearNo) {
	return (new Date(yearNo, 1, 29).getMonth() == 1);
};
