function listingAction(action, listing, child, userAction) {
	var afterCallback = function () {
		switch (action) {
		   case 'spam':
				setTimeout(function() { $(child).innerHTML = 'Marked as spam.'; }, 100);
				break;
			case 'scam':
			case 'fraud':
				setTimeout(function() { $(child).innerHTML = 'Thank you for reporting this item as spam/fraud - we will investigate it. (If you clicked the link by mistake, please don\'t worry. It takes more than one complaint to get the item removed.)'; }, 100);
				break;
			case 'abuse':
				setTimeout(function() { $(child).innerHTML = 'Thank you for reporting this item as abusive - we will investigate it. (If you clicked the link by mistake, please don\'t worry. It takes more than one complaint to get the item removed.)'; }, 100);
				break;	
			case 'inappropriate':
				setTimeout(function() { $(child).innerHTML = 'Thank you for reporting this item as inappropriate - we will investigate it.'; }, 100);
				break;
			case 'unavailable':
				setTimeout(function() { $(child).innerHTML = 'Thank you. We will remove the listing after verifying it is unavailable.'; }, 100);
				break;
			case 'suspicious':
				setTimeout(function() { $(child).innerHTML = 'Marked as suspicious. Refresh this page to see the change.'; }, 100);
				break;
			case 'unmarkspam':
			case 'unmarksusp':
				setTimeout(function() { $(child).innerHTML = 'Listing has been unmarked.'; }, 100);
				break;
			case 'adminremove':
				setTimeout(function() { $(child).innerHTML = 'Listing has been administratively removed.'; }, 100);
				break;
		}
	};
	var ajaxRequest = new XMLHttpRequestObject(null, afterCallback, null);
	ajaxRequest.send('/ajax/actions/?action=' + userAction + '&listing=' + listing);
	$(child).innerHTML = 'Marking as ' + action + '...';
}

// Render a listing using an AJAX callback.
function fillListing(divid, listingid) {
	var expandedListing = document.getElementById(divid);
	var fullbody = getElementsByClass('sbody', expandedListing, 'span');
	var vin = getElementsByClass('vin', expandedListing, 'span');
	var frequency = getElementsByClass('listing-frequency', expandedListing, 'strong');
	var map = getElementsByClass('map', expandedListing, 'div'); 
	new XMLHttpRequestObject(
		function () {},
		function () {},
		function (xml) {
			fullbody = fullbody[0];
			if (fullbody) {
				if (fullbody.innerHTML == '') {
					body = null;
					tags = xml.getElementsByTagName('body');
					if (tags.length > 0) {
						body = tags[0];
					}
					if (body &&
					    body.childNodes &&
					    body.childNodes.length > 0 &&
					    body.childNodes[0].nodeValue !=  "") {
						fullbody.innerHTML = body.childNodes[0].nodeValue;
					} else {
						fullbody.innerHTML = "No description available";
					}
				}
			}
			
			vin = vin[0] ? vin[0] : null;
			if (vin) {
				if (vin.innerHTML == '') {
					body = null;
					tags = xml.getElementsByTagName('vin');
					if (tags.length > 0) {
						body = tags[0];
					}
					if (body &&
					    body.childNodes &&
					    body.childNodes.length > 0 &&
					    body.childNodes[0].nodeValue !=  "") {
						vin.innerHTML = body.childNodes[0].nodeValue;
					} else {
						vin.innerHTML = "No VIN available";
					}
				}
			}
			
			if (frequency.length > 0) {
				frequency = frequency[0];
				if (frequency.innerHTML == '' || frequency.innerHTML == 'very rarely') {
					similar = null;
					tags = xml.getElementsByTagName('num_similar');
					var headerText = "Listing Frequency";
					if (tags.length > 0) {
						similar = tags[0];
					}
					if (similar &&
					    similar.childNodes &&
					    similar.childNodes.length > 0 &&
					    similar.childNodes[0].nodeValue !=  "0") {
						var days = parseInt(180/similar.childNodes[0].nodeValue);
						if (days == 0) {
							frequency.innerHTML = "every few hours";
						} else if (days == 1) {
							frequency.innerHTML = 'every day';
						} else {
							frequency.innerHTML = "every " + days + " days";
						}
					} else if (similar &&
					           similar.childNodes &&
					           similar.childNodes.length > 0 &&
					           similar.childNodes[0].nodeValue ==  "0") {
						frequency.innerHTML = "very rarely";
					} else {
						frequency.innerHTML = "very rarely";
					}
				}
			}

			var loadMapFunc = function() {
			
				if (!window.GBrowserIsCompatible ||
				    !window.GControl ||
				    !window.GMap2)
				{
					setTimeout(loadMapFunc, 200);
					return;
				}

				if (map.length > 0 && GBrowserIsCompatible()) {
					map = map[0];
					var lat = xml.getElementsByTagName('latitude');
					if (lat) lat = lat[0];
					if (lat && lat.childNodes && lat.childNodes.length > 0) {
						lat = lat.childNodes[0].nodeValue;
					} else {
						lat = null;
					}
					var lng = xml.getElementsByTagName('longitude');
					if (lng) lng = lng[0];
					if (lng && lng.childNodes && lng.childNodes.length > 0) {
						lng = lng.childNodes[0].nodeValue;
					} else {
						lng = null;
					}

					var loc = xml.getElementsByTagName('location_code');
					if (loc) loc = loc[0];
					if (loc && loc.childNodes && loc.childNodes.length > 0)
					{
						loc = loc.childNodes[0].nodeValue;
					}
					else
					{
						loc = null;
					}
					
					if (lat && lng && loc && loc.substr(0, 5) === 'addr:')
					{
						map.style.width = "150px";
						map.style.height = "150px";
						// map.style.styleFloat = "right";
						// map.style.floatStyle = "right";
						// map.style.cssFloat = "right";
						map.style.overflow = "hidden";
						map.style.marginLeft = "2em";
	
						var gmap = new GMap2(map);
						gmap.setCenter(new GLatLng(lat, lng), 13);
						GEvent.addListener(gmap, "moveend", function() {
							gmap.panTo(new GLatLng(lat, lng));	
						});					
	
						var pin = new GIcon();
						pin.shadow = "http://i.oodleimg.com/a/maps/shadow.png";
						pin.shadowSize = new GSize(22, 24);
						pin.iconSize = new GSize(14, 24);
						pin.iconAnchor = new GPoint(7, 22);
						pin.infoWindowAnchor = new GPoint(7, 5);
						pin.image =  "http://i.oodleimg.com/a/maps/icon.png";
						var marker = new GMarker(new GLatLng(lat, lng), pin);
						gmap.addOverlay(marker);
					}
				}
			};
			
			LazyLoad.loadOnce(['http://img.oodle.com/jaml/lazyloadmap/1216934470'], loadMapFunc, null, null, true);
		},
		"/ajax/listing/?listing_id=" + listingid
	);
}

function toggleListing(id) {
	var element = document.getElementById(id);
	if (element) {
		if (hasClass(element, "normal-listing")) {	
			fillListing(id, id);	
			swapToggleLink(id, "show");
			removeClass(element, "normal-listing");
			addClass(element, "expanded-listing");
		} else if (hasClass(element, "expanded-listing")) {
			swapToggleLink( id, "hide" );
			removeClass(element, "expanded-listing");
			addClass(element, "normal-listing");
		}
	}
}

function swapToggleLink(id, event) {
	var toggleBtn = document.getElementById(id + "_toggleBtn");
	if (toggleBtn) {
		var seeDetails = getElementsByClass('see-details-link', toggleBtn)[0];
		var hideDetails = getElementsByClass('hide-details-link', toggleBtn)[0];
		
		if (event == 'hide') {
			seeDetails.style.display = "inline";
			hideDetails.style.display = "none";
		} 
		else if(event == 'show') 
		{
			seeDetails.style.display = "none";
			hideDetails.style.display = "inline";
		}
	}
}

function doToggle(id, event) {
	//var now = new Date().getTime();
	if (typeof _cs != 'undefined' && _cs && _cs != id) {
		// preserve scroll state
		var curElement = document.getElementById(id);
		var oldOffset = getElementTop(curElement);
		toggleListing(_cs); 
		var newOffset = getElementTop(curElement);
		if (oldOffset - newOffset != 0) {
			// scroll by offset
			window.scrollBy(0, newOffset - oldOffset)
		}
	}
	if (typeof _cs == 'undefined' || _cs == null || _cs != id) {
		_cs = id;
	} else {
		_cs = null;
	}
	toggleListing(id);
	//var later = new Date().getTime();

	//alert(later - now);
}
function Slider(containerElement,
                formElement,
                formHiddenInputName,
                values,
                defaultValue,
                errorText,
                knobOnSrc,
                knobOffSrc,
                leftLabelInnerHTML,
                rightLabelInnerHTML) {
	var index = 0;
	var defaultPosition = 2;
	var ticks = new Array();

	for (var value in values) {
		ticks[index] = value;

		if (values[value] == defaultValue) {
			defaultPosition = index;
		}

		index++;
	}

	// If the tick is a dotted line (i.e. inactive),
	// move right until an active tick is found.
	while (ticks[defaultPosition] == "") {
		defaultPosition++;
	}

	// Offsets.
	var offset = 4; // the slider is slightly smaller than the container.
	var imgOffset = 3; // Move the image over so it's center is aligned with
	                   // the tick (normally the left edge will be aligned).

	// Container element.
	var sliderContainer = document.getElementById(containerElement);
	var sliderAreaWidth = getElementWidth(sliderContainer);
	var sliderWidth = sliderAreaWidth - offset * 2;
	var numTicks = ticks.length;
	var tickDistance = sliderWidth / (numTicks - 1);
	var sliderMinX = getElementPos(sliderContainer)[0];
	var sliderMaxX = parseInt(sliderMinX) + sliderWidth;
	var knobOnSrc = knobOnSrc;
	var knobOffSrc = knobOffSrc;

	// Form element.
	var sliderForm = document.getElementById(formElement);
	var formHiddenInputName = formHiddenInputName;

	// Radius element.
	var sliderInfo;

	// Error element.
	var sliderError;
	var errorText = errorText;
	var errorMessage;

	// Labels.
	var leftLabelInnerHTML = leftLabelInnerHTML;
	var rightLabelInnerHTML = rightLabelInnerHTML;

	// Knob element.
	var sliderKnob;

	// Hidden input element.
	var sliderParam;

	// Other.
	var mouseDown = false;

	_drawSlider();

	function _drawSlider() {
		// Slider cover (transparent div over slider captures all clicks).
		var sliderCover = document.createElement("div");
		sliderCover.id = "slider-cover"; // Pass in to constructor.
		_addListeners(sliderCover);

		// Slider hidden input.
		sliderParam = document.createElement("input");
		sliderParam.type = "hidden";
		sliderParam.name = formHiddenInputName;
		sliderContainer.appendChild(sliderParam);

		// Slider line.
		var sliderLine = document.createElement('div');
		sliderLine.className = "slider-active-tick"; // Pass in to constructor.
		sliderLine.style.width = tickDistance + "px";

		for (i = 0, j = 0; j < ticks.length; i += tickDistance, j++) {
			var sliderLineClone = sliderLine.cloneNode(false);
			sliderLineClone.style.left = i + offset +  "px";

			if (ticks[j] == "") {
				sliderLineClone.className = "slider-inactive-tick"; // Pass in to constructor.
			}

			sliderContainer.appendChild(sliderLineClone);
		}

		sliderLineClone.style.borderBottom = "none";

		// Slider knob.
		sliderKnob = document.createElement('img');
		sliderKnob.id = "slider-knob"; // Pass in to constructor.
		sliderKnob.src = knobOffSrc;
		sliderContainer.appendChild(sliderKnob);

		// Slider info.
		sliderInfo = document.createElement("div");
		sliderInfo.id = "slider-info"; // Pass in to constructor.
	
		// @Teju: Hack to stop the page crashing in IE6/IE7
		// 	      Ideally this should happen in _setKnob
		sliderInfo.innerHTML = ticks[defaultPosition]; 	
		sliderContainer.appendChild(sliderInfo);

		// Slider error.
		sliderError = document.createElement("div");
		sliderError.id = "slider-error"; // Pass in to constructor.
		sliderContainer.appendChild(sliderError);

		// Slider left label.
		var leftLabel = document.createElement("span");
		leftLabel.id = "slider-left-label"; // Pass in to constructor.
		leftLabel.innerHTML = leftLabelInnerHTML;
		sliderContainer.appendChild(leftLabel);

		// Slider right label.
		var rightLabel = document.createElement("span");
		rightLabel.id = "slider-right-label"; // Pass in to constructor.
		rightLabel.innerHTML = rightLabelInnerHTML; 
		sliderContainer.appendChild(rightLabel);

		//
		sliderContainer.appendChild(sliderCover); // Put the slider cover on top of everything else.

		//
		_setKnob(defaultPosition);
	}

	// Move info and err windows under the tick; if the mouse is up,
	// moves the slider knob to the specified tick
	function _setKnob(pos) {
		if (!mouseDown) {
			sliderKnob.src = knobOffSrc;
			sliderKnob.style.left = pos * tickDistance - imgOffset + "px";
		}
		
		sliderParam.value =  values[ticks[pos]];
		//below line crash IE6 and IE7
		//sliderInfo.innerHTML = ticks[pos];
		sliderError.innerHTML = errorMessage ? errorMessage : "";
  	}

	// When slider is active, the cursor is a hand.
	function _activeSlider() {
		sliderContainer.style.cursor = "pointer";
	}

	// When slider is inactive, cursor is normal.
	function _inactiveSlider() {
		sliderContainer.style.cursor = "auto";
	}

	function _knobToPos(e) {
		errorMessage = "";
		mouseDown = true;
		_moveImg(e);
		sliderKnob.src = knobOnSrc;
		return false;
	}

	function _startDrag(e) {
		errorMessage = "";

		if (!mouseDown) {
			return;
		}

		_moveImg(e);
		return false;
	}

	// Move the knob to current mouse position.
	function _moveImg(e) {
		if (!e) {
			var e = window.event;
		}

		if (!_withinBounds(e)) {
			return false;
		}

		if (!e.preventDefault) {
			e.returnValue = false;
		} else {
			e.preventDefault();
		}

		sliderKnob.style.left = e.clientX - sliderMinX - imgOffset*2 + "px";
		_latchToTick(e);
	}

	function _stopDrag(e) {
		if (!e) {
			var e = window.event;
		}

		if (!mouseDown) {
			return false;
		}

		mouseDown = false;
		_latchToTick(e);
	 	_cleanup();
	}

	function _latchToTick(e) {
		if (sliderKnob && sliderKnob.style)
		// && e.clientX < sliderMaxX && e.clientX > sliderMinX)
		{
			var sliderVal =  e.clientX - sliderMinX;
			var sliderPos = Math.round (sliderVal / sliderWidth * (numTicks - 1));

			if (sliderPos >= numTicks) {
				sliderPos = numTicks - 1;
			} else if (sliderPos < 0) {
				sliderPos = 0;
			}

			// If the tick is a dotted line (i.e. inactive), move right until an
			// active tick is found
			while (ticks[sliderPos] == "") {
				sliderPos++;
				errorMessage = errorText; 
			}

			_setKnob(sliderPos);
			// @Teju: Hack to stop the page crashing in IE6/IE7
			// 	      Ideally this should happen in _setKnob
			sliderInfo.innerHTML = ticks[sliderPos];

			// If the slider was moved to a different tick than the current one
			// submit the form.
			if (sliderPos != defaultPosition && !mouseDown) {
				sliderForm.submit();
			}
		}

		return false;
	}

	function _withinBounds(e) {
		// If this function was called because the mouse left the slider boundary,
		// do nothing.
		if (mouseDown && e.clientX > sliderMaxX) {
			sliderKnob.style.left = sliderWidth + "px";
			return false;
		}

		if (mouseDown && e.clientX < sliderMinX) {
			sliderKnob.style.left = "0px";
			return false;
		}

		return true;
	}

	function _addListeners(obj) {
		if (obj.addEventListener) {
			obj.addEventListener('mousedown', _knobToPos, false);
			obj.addEventListener('mouseup', _stopDrag, false);
			obj.addEventListener('mouseout', _stopDrag, false);
			obj.addEventListener('mousemove', _startDrag, false);
			obj.addEventListener('mouseover', _activeSlider, false);
			obj.addEventListener('mouseout', _inactiveSlider, false);
		} else if (obj.attachEvent) {
			obj.attachEvent('onmousedown', _knobToPos);
			obj.attachEvent('onmouseup', _stopDrag);
			obj.attachEvent('onmouseout', _stopDrag);
			obj.attachEvent('onmousemove', _startDrag);
			obj.attachEvent('onmouseover', _activeSlider);
			obj.attachEvent('onmouseout', _inactiveSlider);
		}
	}

	function _removeListeners() {
		// Remove event listeners from 'document' (W3C).
		if (document.removeEventListener) {
			document.removeEventListener('onmousedown', _knobToPos, false);
			document.removeEventListener('mousemove', _startDrag, false);
			document.removeEventListener('mouseup', _stopDrag, false);
			document.removeEventListener('mouseout', _stopDrag, false);
			document.removeEventListener('mousemove', _startDrag, false);
			document.removeEventListener('mouseover', _activeSlider, false);
			document.removeEventListener('mouseout', _inactiveSlider, false);
		// Remove event listeners from 'document' (IE).
		// Hopefully will plug memory leaks.
		} else if (document.detachEvent) {
			document.detachEvent('onmousedown', _knobToPos);
			document.detachEvent('onmousemove', _startDrag);
			document.detachEvent('onmouseup', _stopDrag);
			document.detachEvent('onmouseout', _stopDrag);
			document.detachEvent('onmousemove', _startDrag);
			document.detachEvent('onmouseover', _activeSlider);
			document.detachEvent('onmouseout', _inactiveSlider);
		}
	}

	function _cleanup() {
		_removeListeners();
	}
}

function toggleNarrow(controlObj, toggleObj) {
	toggle(toggleObj);
	if (toggleObj.style.display == 'none') {
		removeClass(controlObj, 'expanded');
	} else {
		addClass(controlObj, 'expanded');
	}
}

// @param CheckBox childObj
// @param UL parentObj
// Check childObj and and uncheck all of
// the CheckBox objects in parentObj.
// Args can be undefined.
function clickAll(childObj, parentObj) {
	//if (childObj.checked)
	//	return;
	if (!childObj || !parentObj)
		return;
	var elems = getElementsByTagNames('input', parentObj);
	for (var i = 1; i < elems.length; i++)
		elems[i].checked = false;
	childObj.checked = true;
}

// @param UL parentObj
// iterate through the CheckBox objects in parentObj
// and check the first object if there are no
// "on" object, otherwise uncheck the first object.
// Args can be undefined.
function fixAll(parentObj) {
	if (!parentObj)
		return;
	var elems = getElementsByTagNames('input', parentObj);
	var on = 0;
	for (var i = 1; i < elems.length; i++) {
		if (elems[i].checked == true) {
			on++;
			break;
		}
	}
	elems[0].checked = !on;
}

// @param CheckBox childObj
// @param UL parentObj
// toggle the state of childObj, then iterate
// through the CheckBox objects in parentObj
// and check the first object if there are no
// "on" object, otherwise uncheck the first object.
// Args can be undefined.
function clickAndFixAll(childObj, parentObj) {
	if (!childObj || !parentObj)
		return;
	childObj.checked = !childObj.checked;
	fixAll(parentObj);
}
function submitAlertPopup()
{
	var email = $('alert-popup-email').value;
	var frequency = $('alert-popup-frequency').value;
	var alertBody = $('alert-popup-body');

	new XMLHttpRequestObject(
		function() { },
		function() { },
		function(response) 
		{
			var tag = response.getElementsByTagName('alert-response');
			alertBody.innerHTML = tag[0].firstChild.nodeValue;
		},
		'/ajax/alert/?email=' + email + '&frequency=' + frequency
	);
	return false;
}

// global counter for listing clicks
var listingClicks = 0;
var triggerPopupCount = 1;

function listingClicked(elementID)
{
//	alert(elementID);
	listingElement = $(elementID);
	
	// increment the global listing clicks counter
	listingClicks++;

	if (listingElement && listingClicks == triggerPopupCount)
	{
		var alertWindow = $('alert-form-popup');
		
		if (alertWindow)
		{
			alertWindow.style.position = 'absolute';
			document.body.appendChild(alertWindow);
			
			var listingPos = getElementPos(listingElement);
			alertWindow.style.left = (listingPos[0] - 150) + 'px';
			alertWindow.style.top = (listingPos[1] - 100) + 'px';
			
			alertWindow.style.display = 'block';

			return true;
		}
	}

	return false;
}
function mouseHL(div)
{
	var childs = div.getElementsByTagName("div");
	if (!childs || !childs[0] || !childs[1])
		return;
	
	childs[0].style.visibility="visible";
	childs[1].style.visibility="hidden";
	
}

function unmouseHL(div)
{
	var childs = div.getElementsByTagName("div");
	if (!childs || !childs[0] || !childs[1])
		return;
	
	childs[0].style.visibility="hidden";
	childs[1].style.visibility="visible";
}

function distInfoBoxHtml(index)
{
	//range
	//last 6 month number
	//today number
}

function makeSubmit(form)
{
	var n = form.model.options.length;
	form.model.options[n] = new Option("","");
	form.model.selectedIndex = n;
	form.submit();
}

function neighborhoodSubmit(form)
{
	if (form.loc)
	{
		form.removeChild(form.loc);
	}
	form.loc_neighborhood.name = "loc";
	form.submit();
}

function citySubmit(form)
{
	if (form.loc)
	{
		form.removeChild(form.loc);
	}
	form.loc_city.name = "loc";
	form.submit();
}

function withLocSubmit(form)
{
	if (form.loc)
	{
		form.removeChild(form.loc);
	}
	if (form.loc_neighborhood && form.loc_neighborhood.value !=  "")
	{
		form.loc_neighborhood.name = "loc";
	}
	else
	{
		form.loc_city.name = "loc";
	}

	form.submit();
}
// Maintained by Lu Wang, lwang@corp

// utility - detecting Opera. We need it because its rendering engine only displays changes properly
// when a div's top style is changed after a change to it's height style.
var isOpera = false;
if (navigator.userAgent.indexOf("Opera") >= 0)
{
	isOpera = true; 
}

var isSafari = false;
if (navigator.userAgent.indexOf("Safari") >= 0)
{
	isSafari = true;
}

function getElementStyle(elem, IEStyleProp, CSSStyleProp) {
	if (elem.currentStyle) {
		return elem.currentStyle[IEStyleProp];
	} else if (document.defaultView && document.defaultView.getComputedStyle) {
		var compStyle = document.defaultView.getComputedStyle(elem, "");
		return compStyle.getPropertyValue(CSSStyleProp);
	}
	return "";
}

// class declaration / definition
// requires standards mode IE to display properly (NOT quirks mode!)
function JSLWBarGraph (w, h, xAxisLabels, yAxisLabels, vals1, vals2, yAxisMargin, xAxisMargin, offset, slice) {
	// sanity check the input:
	if (w === 0 || h === 0 ||
	    xAxisLabels === null || xAxisLabels.length === 0 ||
	    yAxisLabels === null || yAxisLabels.length === 0) {
		return null;
	}

	this.m_viewSlice = slice || 5;
	this.m_scrollPos = 0;
	this.m_barsOffset = offset || 0;
	
	this.m_w = w;
	this.m_h = h;
	this.m_lMargin = yAxisMargin ? yAxisMargin : 20;
	this.m_rMargin = 10;
	this.m_tMargin = 30;
	this.m_bMargin = xAxisMargin ? xAxisMargin : 20;
	this.m_wInner = w - this.m_lMargin - this.m_rMargin;
	this.m_hInner = h - this.m_tMargin - this.m_bMargin;
	this.m_xAxisLabels = xAxisLabels;
	this.m_yAxisLabels = yAxisLabels;
	this.m_vals1 = vals1;
	this.m_vals2 = vals2;
	this.m_xTicks = xAxisLabels.length;
	this.m_yTicks = yAxisLabels.length;
	this.m_oBars1 = [];
	this.m_oBars2 = [];

	this._create(w, h);
}

JSLWBarGraph.prototype.getWnd = function() {
	return this.m_oWnd;
};

JSLWBarGraph.prototype.setVals = function(vals1, vals2, animate) {
	this.m_oldVals1 = this.m_vals1;
	this.m_oldVals2 = this.m_vals2;
	this.m_vals1 = vals1;
	this.m_vals2 = vals2;
	if (animate) {
		if (this._animID) {
			clearInterval(this._animID);
		}
		this._iteration = 0;
		var workingGraph = this;	
		this._animID = setInterval(function(){workingGraph._animateChange()}, 30);
	}
	else {
		for (var i = 0; i < this.m_xTicks; i++) {
			this._setBar(this.m_oBars1[i], this.m_vals1[i] || 0);
		}
		for (var i = 0; i < this.m_xTicks; i++) {
			this._setBar(this.m_oBars2[i], this.m_vals2[i] || 0);
		}
	}
};

JSLWBarGraph.prototype.canScroll = function(delta) {
	var oldScrollPos = this.m_scrollPos;
	var newScrollPos = Math.max(0, Math.min(this.m_xTicks - this.m_viewSlice, oldScrollPos + delta));
	if (newScrollPos == oldScrollPos)
		return false;
	else return true;
};

JSLWBarGraph.prototype.scroll = function(delta, noanimate) {
	this.m_oldScrollPos = this.m_scrollPos;
	this.m_scrollPos = Math.max(0, Math.min(this.m_xTicks - this.m_viewSlice, this.m_oldScrollPos + delta));
	if (noanimate) {
		this.m_oView.style.left = -this.m_scrollPos*(this.m_wInner/this.m_viewSlice) + "px";
	}
	else {
		if (this._scrollID) {
			clearInterval(this._scrollID);
		}
		this._scrollIteration = 0;
		var workingGraph = this;	
		this._scrollID = setInterval(function(){workingGraph._animateScroll()}, 25);
	}
};

JSLWBarGraph.prototype.setHandlers = function(funcClick, funcHover) {
	this.m_clickHandler = funcClick;
	this.m_hoverHandler = funcHover;
};

// create bars of a certain className and returns the array
JSLWBarGraph.prototype._createBars = function (vals, className, reflection, line, offset) {
	if (!offset) offset = 0;
	var count = this.m_xTicks;
	bars = [];
	for (var i = 0; i < count; i++) {
		var bar = document.createElement("div");
		bar.className = className;
		bar.style.overflow = "hidden";
		bar.style.height = (vals[i] || 0)*this.m_hInner/100 + "px";
		bar.style.bottom = "0px";
		bar.index = i;
		bar.graph = this;

		var tip = document.createElement("div");
		tip.className = className + "TopNormal";
		tip.style.top = "0px";
		bar.appendChild(tip);
		bar.tip = tip;
		
		var barBounds = document.createElement("div");
		barBounds.className = "JSLWBarBounds";
		barBounds.style.position = "absolute";
		barBounds.style.overflow = "visible";
		barBounds.style.top = this.m_tMargin + (100-(vals[i] || 0))*this.m_hInner/100 + "px";
		barBounds.style.height = "1px";
		barBounds.style.width = (this.m_wInner/this.m_viewSlice - 1) + "px";
		barBounds.style.left = (i*(this.m_wInner/this.m_viewSlice)) + offset + "px";
		
		barBounds.appendChild(bar);
		bar.bounds = barBounds;		
		
		var workingGraph = this;
		bar.onmouseover = function() { this.className = className + " " + className + "Over"; if (this.line) this.line.className = className + "LineOver"; workingGraph._showInfobox(true, this.index);};
		bar.onmouseout = function() { this.className = className; if (this.line) this.line.className = className + "Line"; workingGraph._showInfobox(false, this.index);};
		bar.onmousedown = function() { if (this.graph.m_clickHandler) this.graph.m_clickHandler(this.graph, this.index); };

		if (reflection)
		{
			var reflBounds = document.createElement("div");
			reflBounds.className = "JSLWBarReflectionBounds";
			reflBounds.style.position = "absolute";
			reflBounds.style.overflow = "hidden";
			reflBounds.style.height = "8px";
			reflBounds.style.width = (this.m_wInner/this.m_viewSlice - 1) + "px";
			reflBounds.style.left = (i*(this.m_wInner/this.m_viewSlice)) + offset + "px";
			reflBounds.style.bottom = (this.m_bMargin - 8 - 1) + "px";		
			for (var j = 0; j < 8; j++) {
				var reflection = document.createElement("div");
				reflection.style.overflow = "hidden";
				reflection.style.height = 1 + "px";
				reflection.style.opacity = 0.4-j*0.05;
				reflection.style.filter = "alpha(opacity=" + (40-j*5.0)+ ")";
				reflection.className = className + "Reflection";
				reflBounds.appendChild(reflection);
			}
			this.m_oView.appendChild(reflBounds);
		}

		this.m_oView.appendChild(barBounds);
		bars[i] = bar;
	}
	return bars;
};

JSLWBarGraph.prototype._createAxis = function(w, h) {
	// create x tick marks
	for (var i = 0; i < this.m_xTicks; i++) {
		var tick = document.createElement("div");
		tick.style.position = "absolute";
		tick.style.height = "3px";
		tick.style.lineHeight = "3px";
		tick.style.clip = "rect(0px, 1px, 3px, 0px)";
		tick.style.width = "1px";
		tick.style.left = ((i+0.5)*(this.m_wInner/this.m_viewSlice)) + "px";
		tick.style.top = (h - this.m_bMargin) + "px";
		tick.className = "JSLWAxis";
		this.m_oView.appendChild(tick); 

		var text = document.createElement("div");
		text.style.position = "absolute";
		text.style.height = "3px";
		text.style.width = (this.m_wInner/this.m_viewSlice) + "px";
		text.style.left = ((i)*(this.m_wInner/this.m_viewSlice)) + "px";
		text.style.top = ((h - this.m_bMargin) + 3) + "px";
		text.className = "JSLWXAxisText";
		text.innerHTML = this.m_xAxisLabels[i];	
		var workingGraph = this;
		text.index = i;
		text.onmousedown = function() { if (workingGraph.m_clickHandler) workingGraph.m_clickHandler(workingGraph, this.index); };
		this.m_oView.appendChild(text);
	}

	// create y tick marks
	for (var i = 0; i < this.m_yTicks; i++) {
		var tick = document.createElement("div");
		tick.style.position = "absolute";
		tick.style.height = "1px";
		tick.style.width = "3px";
		tick.style.left = (this.m_lMargin -3) + "px";
		tick.style.top = (this.m_tMargin + (i)*(this.m_hInner/this.m_yTicks)) + "px";
		// use clipping to solve the IE min div height bug
		tick.style.clip = "rect(0px,3px, 1px, 0px)"; 
		tick.className = "JSLWAxis";
		this.m_oWnd.appendChild(tick); 

		var line = document.createElement("div");
		line.style.position = "absolute";
		line.style.height = "1px";
		line.style.width = this.m_wInner + "px";
		line.style.left = (this.m_lMargin) + "px";
		line.style.top = (this.m_tMargin + (i)*(this.m_hInner/this.m_yTicks)) + "px";
		line.style.lineHeight = "0";
		line.style.fontSize = "0";
		line.style.clip = "rect(0px, " + this.m_wInner + "px, 1px, 0px)"; 
		line.className = "JSLWLine";
		this.m_oWnd.appendChild(line); 
		

		var text = document.createElement("div");
		text.style.position = "absolute";
		text.style.height = "3px";
		text.style.width = (this.m_lMargin - 5) + "px";
		text.style.left = "0px";
		text.style.top = (this.m_tMargin + i*(this.m_hInner/this.m_yTicks)) + "px";
		text.className = "JSLWYAxisText";
		text.innerHTML = this.m_yAxisLabels[this.m_yTicks - i - 1];
		this.m_oWnd.appendChild(text);
	}
};

JSLWBarGraph.prototype._create = function(w, h) {
	this.m_oWnd = document.createElement("div");
	this.m_oWnd.style.position = "relative";
	this.m_oWnd.style.overflow = "visible";
	this.m_oWnd.style.height = h + "px";
	this.m_oWnd.style.width = w + "px";
	this.m_oWnd.className = "JSLWBarGraph";

	this.m_oFrame = document.createElement("div");
	this.m_oFrame.style.position = "relative";
	this.m_oFrame.style.overflow = "hidden";
	this.m_oFrame.style.height = "100%";
	this.m_oFrame.style.width = this.m_wInner + 1 + "px";
	this.m_oFrame.style.left = this.m_lMargin + "px";
	this.m_oFrame.style.zIndex = 1;

	this.m_oView = document.createElement("div");
	this.m_oView.style.position = "absolute";
	this.m_oView.style.overflow = "visible";
	this.m_oView.style.height = "100%";
	this.m_oView.style.width = this.m_wInner + "px";
	this.m_oView.style.left =  "0px";

	this.m_oFrame.appendChild(this.m_oView);

	this.m_oY = document.createElement("div");
	this.m_oY.style.position = "absolute";
	this.m_oY.style.overflow = "hidden";
	this.m_oY.style.height = this.m_hInner + "px";
	this.m_oY.style.width = "1px";
	this.m_oY.style.left = this.m_lMargin + "px";
	this.m_oY.style.top = this.m_tMargin + "px";
	this.m_oY.style.zIndex = 40;
	this.m_oY.className = "JSLWAxis";

	this.m_oX = document.createElement("div");	
	this.m_oX.style.position = "absolute";
	this.m_oX.style.overflow = "hidden";
	this.m_oX.style.height = "1px";
	this.m_oX.style.width = this.m_wInner + 1 + "px";
	this.m_oX.style.left = this.m_lMargin + "px";
	this.m_oX.style.top = (h - this.m_bMargin) + "px";
	this.m_oX.style.zIndex = 40;
	this.m_oX.className = "JSLWAxis";

	this.m_oWnd.appendChild(this.m_oFrame);
	this.m_oWnd.appendChild(this.m_oX);
	this.m_oWnd.appendChild(this.m_oY);

	this._createInfobox();
	this._createAxis(w, h);

	var sampleBar = document.createElement("div");
	sampleBar.className = "JSLWBar";
	sampleBar.visibility = "none";
	document.body.appendChild(sampleBar);
	barWidth = getElementStyle(sampleBar, "width", "width");
	if (barWidth == "auto")
	{
		barWidth = (this.m_wInner/this.m_xTicks - 1) + "px";	
	}
	var leftDelta = ((this.m_wInner/this.m_xTicks)-parseInt(barWidth, 10))/2;
	document.body.removeChild(sampleBar);	
		
	this.m_oBars2 = this._createBars(this.m_vals2, "JSLWBar2", true, true, this.m_barsOffset);
	this.m_oBars1 = this._createBars(this.m_vals1, "JSLWBar1", true, true);
	
};

JSLWBarGraph.prototype._createInfobox = function()
{
	this._infoBox = document.createElement("div");
	this._infoBox.style.display = "none";
	this._infoBox.className = "JSLWBarGraphInfobox";	
	this._infoBox.style.position = "absolute";
	this._infoBox.style.overflow = "hidden";
	this._infoBox.style.width = (this.m_w - 20) + "px";
	this._infoBox.style.left = 0 + "px";
	this._infoBox.style.bottom = this.m_bMargin + this.m_hInner + "px";
	this._infoBox.style.zIndex = 100;
	this.m_oWnd.appendChild(this._infoBox);
	
	// IE6 fix to cover all select boxes
	this._infoBoxHider = document.createElement("iframe");
	this._infoBoxHider.style.display = "none";
	this._infoBoxHider.frameborder = "0";
	this._infoBoxHider.border = "0";
	this._infoBoxHider.scrolling = "no";
	this._infoBoxHider.src = "javascript:false";
	this._infoBoxHider.style.position = "absolute";
	this._infoBoxHider.style.overflow = "hidden";
	this._infoBoxHider.style.width = this._infoBox.style.width;
	this._infoBoxHider.style.height = "0px";
	this._infoBoxHider.style.left = this._infoBox.style.left;
	this._infoBoxHider.style.bottom = this._infoBox.style.bottom;
	this._infoBoxHider.style.zIndex = this._infoBox.style.zIndex - 1;
	this.m_oWnd.appendChild(this._infoBoxHider);
};

JSLWBarGraph.prototype._showInfobox = function(bShow, index)
{
	if (bShow && this.m_hoverHandler)
	{
		this._infoBox.style.display = "block";
		this._infoBoxHider.style.display = "block";
		this._infoBox.innerHTML = this.m_hoverHandler(this, index);
		if (this._infoBox.offsetHeight) this._infoBoxHider.style.height = this._infoBox.offsetHeight;
		if (isOpera) {
			this._infoBox.style.top = 0;
			this._infoBox.style.bottom = "";
			this._infoBoxHider.style.top = 0;
			this._infoBoxHider.style.bottom = "";
		}
	}
	else
	{	
		this._infoBoxHider.style.display = "none";
		this._infoBox.style.display = "none";	
		this._infoBox.innerHTML = "";
	}
};

JSLWBarGraph.prototype._setBar = function(bar, val)
{
	bar.style.height = Math.min(val, 100)*this.m_hInner/100 + 1 + "px";
	bar.bounds.style.top = this.m_tMargin + (100-Math.min(val,100))*this.m_hInner/100 + "px";
	if (bar.tip) {
		if (val > 100) bar.tip.className = bar.className + "TopMaxed";
		else bar.tip.className = bar.className + "TopNormal";
	}
};

JSLWBarGraph.prototype._animateBar = function(bar, oldval, newval)
{
	var changed = false;
	var baseHeight = parseInt(oldval);
	var destHeight = parseInt(newval);
	var currHeight = parseInt(parseInt(bar.style.height, 10)*100/this.m_hInner);
	var dist = (destHeight - baseHeight)/20;
	if (currHeight == 100 && destHeight > 100) {
		return false;
	}
	if (currHeight != destHeight) {
		changed = true;
		if (Math.abs(currHeight - destHeight) <= Math.abs(dist))
			var newHeight = destHeight;
		else
			var newHeight = baseHeight + this._iteration*dist;
		this._setBar(bar, newHeight);
	}
	return changed;
};

JSLWBarGraph.prototype._animateChange = function() {
	var changed = false;

	for (var i = 0; i < this.m_xTicks; i++) {
		changed |= this._animateBar(this.m_oBars1[i], this.m_oldVals1[i] || 0, this.m_vals1[i] || 0);		
	}
	
	for (var i = 0; i < this.m_xTicks; i++) {
		changed |= this._animateBar(this.m_oBars2[i], this.m_oldVals2[i] || 0, this.m_vals2[i] || 0);		
	}

	this._iteration++;
	if (!changed) {
		clearInterval(this._animID);
		this._iteration = 0;
	}
};

JSLWBarGraph.prototype._animateScroll = function() {
	var changed = false;

	var base = parseInt(-this.m_oldScrollPos*(this.m_wInner/this.m_viewSlice));
	var dest = parseInt(-this.m_scrollPos*(this.m_wInner/this.m_viewSlice));
	var curr = parseInt(this.m_oView.style.left, 10);
	var dist = (dest - base)/8;
	if (curr != dest) {
		changed = true;
		if (Math.abs(curr - dest) <= Math.abs(dist))
			var newscroll = dest;
		else
			var newscroll = base + this._scrollIteration*dist;
		this.m_oView.style.left = newscroll + "px";
	}

	this._scrollIteration++;
	if (!changed) {
		clearInterval(this._scrollID);
		this._scrollIteration = 0;
	}
};



// this class comes with its own unit test
var myGraph, myGraph2;

function clickhandler(graph, index) {
	alert(index);
}

function hoverhandler(graph, index) {
	return "Show data for bar <strong>" + index + "</strong>";
}

function JSLWBarGraph_UnitTest() {
	var xArray = ["SJ", "LA", "NY", "SD", "CHI", "SF", "SA", "PHX"];
	var yArray = ["10k", "20k", "30k", "40k", "50k"];
	var vals1 = [10, 10, 10, 10, 10, 10, 10, 10];
	var vals2 = [20, 20, 20, 20, 20, 20, 20, 20];
	myGraph = new JSLWBarGraph(300, 300, xArray, yArray, vals1, vals2, 50, 30, 6);
	myGraph.setHandlers(clickhandler, hoverhandler);
	document.body.appendChild(myGraph.getWnd());

	setTimeout("myGraph.setVals([20, 30], [50, 40, 60, 90, 80, 95, 90, 97], true)", 1000);
	setTimeout("myGraph.setVals([20, 31, 76, 61, 14, 30, 40, 50], [50, 40, 90, 70, 60, 40, 60, 80], true)", 2000);
	setTimeout("myGraph.setVals([20, 45, 40, 64, 0, 67, 12, 73], [50, 70, 60, 80, 90, 70, 80, 90], true)", 3000);
	setTimeout("myGraph.setVals([20, 30, 40, 50, 8, 70, 80, 90], [50, 40, 65, 30, 70, 80, 100, 150], true)", 4000);

	setTimeout("myGraph.scroll(4)", 5000);	
}// Maintained by Lu Wang, lwang@corp

// JSEmptyGraph extends JSLWBarGraph
function JSEmptyGraph (w, h, imgClass, yAxisMargin, xAxisMargin) 
{
	// no constructor chaining is necessary.

	// sanity check the input:
	if (w === 0 || h === 0) {
		return null;
	}

	this.m_viewSlice = 5;
	
	this.m_w = w;
	this.m_h = h;
	this.m_lMargin = yAxisMargin ? yAxisMargin : 20;
	this.m_rMargin = 10;
	this.m_tMargin = 30;
	this.m_bMargin = xAxisMargin ? xAxisMargin : 20;
	this.m_wInner = w - this.m_lMargin - this.m_rMargin;
	this.m_hInner = h - this.m_tMargin - this.m_bMargin;
	this.m_xAxisLabels = ['', '', '', '', ''];
	this.m_yAxisLabels = ['', '', '', '', ''];
	this.m_xTicks = this.m_xAxisLabels.length;
	this.m_yTicks = this.m_yAxisLabels.length;
	this.m_imgClass = imgClass;

	this._create(w, h);
}

JSEmptyGraph.prototype.getWnd = function() 
{
	return this.m_oWnd;
};

JSEmptyGraph.prototype._createAxis = function(w, h) 
{
	// create x tick marks
	for (var i = 0; i < this.m_xTicks; i++) {
		var tick = document.createElement("div");
		tick.style.position = "absolute";
		tick.style.height = "3px";
		tick.style.lineHeight = "3px";
		tick.style.clip = "rect(0px, 1px, 3px, 0px)";
		tick.style.width = "1px";
		tick.style.left = (this.m_lMargin + (i+0.5)*(this.m_wInner/this.m_viewSlice)) + "px";
		tick.style.top = (h - this.m_bMargin) + "px";
		tick.className = "JSLWAxis";
		this.m_oWnd.appendChild(tick); 

		var text = document.createElement("div");
		text.style.position = "absolute";
		text.style.height = "3px";
		text.style.width = (this.m_wInner/this.m_viewSlice) + "px";
		text.style.left = (this.m_lMargin + (i)*(this.m_wInner/this.m_viewSlice)) + "px";
		text.style.top = ((h - this.m_bMargin) + 3) + "px";
		text.className = "JSLWXAxisText";
		text.innerHTML = this.m_xAxisLabels[i];	
		var workingGraph = this;
		text.index = i;
		text.onmousedown = function() { if (workingGraph.m_clickHandler) workingGraph.m_clickHandler(workingGraph, this.index); };
		this.m_oWnd.appendChild(text);
	}

	// create y tick marks
	for (var i = 0; i < this.m_yTicks; i++) {
		var tick = document.createElement("div");
		tick.style.position = "absolute";
		tick.style.height = "1px";
		tick.style.width = "3px";
		tick.style.left = (this.m_lMargin -3) + "px";
		tick.style.top = (this.m_tMargin + (i)*(this.m_hInner/this.m_yTicks)) + "px";
		// use clipping to solve the IE min div height bug
		tick.style.clip = "rect(0px,3px, 1px, 0px)"; 
		tick.className = "JSLWAxis";
		this.m_oWnd.appendChild(tick); 

		var line = document.createElement("div");
		line.style.position = "absolute";
		line.style.height = "1px";
		line.style.width = this.m_wInner + "px";
		line.style.left = (this.m_lMargin) + "px";
		line.style.top = (this.m_tMargin + (i)*(this.m_hInner/this.m_yTicks)) + "px";
		line.style.lineHeight = "0";
		line.style.fontSize = "0";
		line.style.clip = "rect(0px, " + this.m_wInner + "px, 1px, 0px)"; 
		line.className = "JSLWLine";
		this.m_oWnd.appendChild(line); 
		

		var text = document.createElement("div");
		text.style.position = "absolute";
		text.style.height = "3px";
		text.style.width = (this.m_lMargin - 5) + "px";
		text.style.left = "0px";
		text.style.top = (this.m_tMargin + i*(this.m_hInner/this.m_yTicks)) + "px";
		text.className = "JSLWYAxisText";
		text.innerHTML = this.m_yAxisLabels[this.m_yTicks - i - 1];
		this.m_oWnd.appendChild(text);
	}
};

JSEmptyGraph.prototype._create = function(w, h) 
{
	this.m_oWnd = document.createElement("div");
	this.m_oWnd.style.position = "relative";
	this.m_oWnd.style.overflow = "visible";
	this.m_oWnd.style.height = h + "px";
	this.m_oWnd.style.width = w + "px";
	this.m_oWnd.className = "JSLWBarGraph";
	
	this.m_oImgWrapper = document.createElement("div");
	this.m_oImgWrapper.className = this.m_imgClass + "-wrapper";
	this.m_oImgWrapper.style.position = "absolute";
	this.m_oImgWrapper.style.left = this.m_lMargin + "px";
	this.m_oImgWrapper.style.top = this.m_tMargin + "px";
	
	this.m_oImg = document.createElement("div");
	this.m_oImg.className = this.m_imgClass;
	this.m_oImg.style.height = this.m_hInner + "px";
	this.m_oImg.style.width = this.m_wInner + 1 + "px";

	this.m_oY = document.createElement("div");
	this.m_oY.style.position = "absolute";
	this.m_oY.style.overflow = "hidden";
	this.m_oY.style.height = this.m_hInner + "px";
	this.m_oY.style.width = "1px";
	this.m_oY.style.left = this.m_lMargin + "px";
	this.m_oY.style.top = this.m_tMargin + "px";
	this.m_oY.style.zIndex = 40;
	this.m_oY.className = "JSLWAxis";

	this.m_oX = document.createElement("div");	
	this.m_oX.style.position = "absolute";
	this.m_oX.style.overflow = "hidden";
	this.m_oX.style.height = "1px";
	this.m_oX.style.width = this.m_wInner + 1 + "px";
	this.m_oX.style.left = this.m_lMargin + "px";
	this.m_oX.style.top = (h - this.m_bMargin) + "px";
	this.m_oX.style.zIndex = 40;
	this.m_oX.className = "JSLWAxis";


	this.m_oWnd.appendChild(this.m_oX);
	this.m_oWnd.appendChild(this.m_oY);
	this.m_oWnd.appendChild(this.m_oImgWrapper);
	this.m_oImgWrapper.appendChild(this.m_oImg);

	this._createAxis(w, h);	
};

// Maintained by Lu Wang, lwang@corp

// utility - detecting Opera. We need it because its rendering engine only displays changes properly
// when a div's top style is changed after a change to it's height style.
var isOpera = false;
if (navigator.userAgent.indexOf("Opera") >= 0)
{
	isOpera = true; 
}

var isSafari = false;
if (navigator.userAgent.indexOf("Safari") >= 0)
{
	isSafari = true;
}

function getElementStyle(elem, IEStyleProp, CSSStyleProp) {
	if (elem.currentStyle) {
		return elem.currentStyle[IEStyleProp];
	} else if (document.defaultView && document.defaultView.getComputedStyle) {
		var compStyle = document.defaultView.getComputedStyle(elem, "");
		return compStyle.getPropertyValue(CSSStyleProp);
	}
	return "";
}

// class declaration / definition
// requires standards mode IE to display properly (NOT quirks mode!)
function JSLWLineGraph (w, h, xAxisLabels, yAxisLabels, vals, data, yAxisMargin, xAxisMargin, slice, interval) {
	// sanity check the input:
	if (w === 0 || h === 0 ||
	    xAxisLabels === null || xAxisLabels.length === 0 ||
	    yAxisLabels === null || yAxisLabels.length === 0) {
		return null;
	}

	this.m_colors = ["rgba(80, 130, 200, 1.0)", "rgba(200, 130, 80, 1.0)", "rgba(80, 200, 130, 1.0)", "rgba(200, 130, 200, 1.0)"];
	this.m_fills =  ["rgba(128, 190, 255, 0.2)", "rgba(255, 190, 128, 0.2)", "rgba(128, 255, 190, 0.2)", "rgba(255, 128, 255, 0.2)"];

	this.m_viewSlice = slice-1 || 5;
	this.m_scrollPos = 0;
	this.m_interval = interval || 1;
	
	this.m_w = w;
	this.m_h = h;
	this.m_lMargin = yAxisMargin ? yAxisMargin : 20;
	this.m_rMargin = 10;
	this.m_tMargin = 30;
	this.m_bMargin = xAxisMargin ? xAxisMargin : 20;
	this.m_wInner = w - this.m_lMargin - this.m_rMargin;
	this.m_hInner = h - this.m_tMargin - this.m_bMargin;
	this.m_xAxisLabels = xAxisLabels;
	this.m_yAxisLabels = yAxisLabels;
	this.m_vals = vals;
	this.m_data = data;
	this.m_xTicks = xAxisLabels.length * this.m_interval;
	this.m_yTicks = yAxisLabels.length;
	
	this._create(w, h);
}

JSLWLineGraph.prototype.getWnd = function() {
	return this.m_oWnd;
};


JSLWLineGraph.prototype.canScroll = function(delta) {
	var oldScrollPos = this.m_scrollPos;
	var newScrollPos = Math.max(0, Math.min(this.m_xTicks - this.m_viewSlice, oldScrollPos + delta));
	if (newScrollPos == oldScrollPos)
		return false;
	else return true;
};

JSLWLineGraph.prototype.scroll = function(delta, noanimate) {
	this.m_oldScrollPos = this.m_scrollPos;
	this.m_scrollPos = Math.max(0, Math.min(this.m_xTicks - this.m_viewSlice - 1, this.m_oldScrollPos + delta));
	if (noanimate) {
		this.m_oView.style.left = -this.m_scrollPos*(this.m_wInner/(this.m_viewSlice-1)) + "px";
	}
	else {
		if (this._scrollID) {
			clearInterval(this._scrollID);
		}
		this._scrollIteration = 0;
		var workingGraph = this;	
		this._scrollID = setInterval(function(){workingGraph._animateScroll()}, 25);
	}
};

JSLWLineGraph.prototype.setHandlers = function(funcClick, funcHover) {
	this.m_clickHandler = funcClick;
	this.m_hoverHandler = funcHover;
};


JSLWLineGraph.prototype._createAxis = function(w, h) {
	// create x tick marks
	for (var i = 0; i < this.m_xTicks; i++) {
		var tick = document.createElement("div");
		tick.style.position = "absolute";
		tick.style.height = i%this.m_interval == 0 ? "5px" : "3px";
		tick.style.width = "1px";
		tick.style.left = ((i+0.0)*(this.m_wInner/this.m_viewSlice)) + "px";
		tick.style.top = (h - this.m_bMargin - this.m_tMargin) + "px";
		tick.style.lineHeight = "0px";
		tick.style.fontSize = "0pt";
		tick.className = "JSLWAxis";
		this.m_oView.appendChild(tick); 

		if (i%this.m_interval == 0) {
			var text = document.createElement("div");
			text.style.position = "absolute";
			text.style.height = "3px";
			text.style.width = (this.m_wInner/this.m_viewSlice) + "px";
			text.style.left = ((i-1)*(this.m_wInner/this.m_viewSlice)) + "px";
			text.style.top = ((h - this.m_bMargin) + 3) + "px";
			text.className = "JSLWXAxisText";
			text.innerHTML = this.m_xAxisLabels[i/this.m_interval];	
			var workingGraph = this;
			text.onmousedown = function() { if (workingGraph.m_clickHandler) workingGraph.m_clickHandler(workingGraph, i); };
			this.m_oAxisView.appendChild(text);
		}
	}

	// create y tick marks
	for (var i = 0; i < this.m_yTicks; i++) {
		var tick = document.createElement("div");
		tick.style.position = "absolute";
		tick.style.height = "1px";
		tick.style.width = "3px";
		tick.style.left = (this.m_lMargin -3) + "px";
		tick.style.top = (this.m_tMargin + (i)*(this.m_hInner/(this.m_yTicks-1))) + "px";
		tick.style.lineHeight = "0px";
		tick.style.fontSize = "0pt";
		// use clipping to solve the IE min div height bug
		tick.style.clip = "rect(0px,3px, 1px, 0px)"; 
		tick.className = "JSLWAxis";
		this.m_oWnd.appendChild(tick); 

		var line = document.createElement("div");
		line.style.position = "absolute";
		line.style.height = "1px";
		line.style.width = this.m_wInner + "px";
		line.style.left = (this.m_lMargin) + "px";
		line.style.top = (this.m_tMargin + (i)*(this.m_hInner/(this.m_yTicks-1))) + "px";
		line.style.lineHeight = "0px";
		line.style.fontSize = "0pt";
		line.style.clip = "rect(0px, " + this.m_wInner + "px, 1px, 0px)"; 
		line.className = "JSLWLine";
		this.m_oWnd.appendChild(line); 
		

		var text = document.createElement("div");
		text.style.position = "absolute";
		text.style.height = "3px";
		text.style.width = (this.m_lMargin - 5) + "px";
		text.style.left = "0px";
		text.style.top = (this.m_tMargin + (i-0.2)*(this.m_hInner/(this.m_yTicks-1))) + "px";
		text.className = "JSLWYAxisText";
		text.innerHTML = this.m_yAxisLabels[this.m_yTicks - i - 1];
		this.m_oWnd.appendChild(text);
	}
};

JSLWLineGraph.prototype._create = function(w, h) {
	this.m_oWnd = document.createElement("div");
	this.m_oWnd.style.position = "relative";
	this.m_oWnd.style.overflow = "visible";
	this.m_oWnd.style.height = h + "px";
	this.m_oWnd.style.width = w + "px";
	this.m_oWnd.className = "JSLWLineGraph";

	this.m_oFrame = document.createElement("div");
	this.m_oFrame.style.position = "absolute";
	this.m_oFrame.style.overflow = "hidden";
	this.m_oFrame.style.height = "100%";
	this.m_oFrame.style.width = this.m_wInner + 1 + "px";
	this.m_oFrame.style.left = this.m_lMargin + "px";

	this.m_oAxisFrame = document.createElement("div");
	this.m_oAxisFrame.style.position = "absolute";
	this.m_oAxisFrame.style.overflow = "hidden";
	this.m_oAxisFrame.style.height = "100%";
	this.m_oAxisFrame.style.width = this.m_wInner + (this.m_wInner/this.m_viewSlice) + 1 + "px";
	this.m_oAxisFrame.style.left = (this.m_lMargin - (this.m_wInner/this.m_viewSlice)) + "px";

	this.m_oView = document.createElement("div");
	this.m_oView.style.position = "absolute";
	this.m_oView.style.overflow = "visible";
	this.m_oView.style.height = this.m_hInner + this.m_bMargin + "px";
	this.m_oView.style.width = (this.m_wInner/this.m_viewSlice) * this.m_xTicks + "px";
	this.m_oView.style.left =  "0px";
	this.m_oView.style.top = this.m_tMargin + "px";

	var canvas = document.createElement("canvas");
	canvas.style.position = "absolute";
	canvas.style.width = (this.m_wInner/this.m_viewSlice) * this.m_xTicks + "px";
	canvas.style.height = this.m_hInner + "px";
	canvas.height = this.m_hInner;
	canvas.width = (this.m_wInner/this.m_viewSlice) * this.m_xTicks;
	if (window.G_vmlCanvasManager_ && window.G_vmlCanvasManager_.initElement) 
	{
		window.G_vmlCanvasManager_.initElement(canvas);
	}

	this.m_canvas = canvas;
	this.m_canvas.style.cursor = "move";
	this.m_oView.appendChild(this.m_canvas);

	var overlay = document.createElement("div");
	overlay.style.position = "absolute";
	overlay.style.width = (this.m_wInner/this.m_viewSlice) * this.m_xTicks + "px";
	overlay.style.height = this.m_hInner + "px";
	overlay.style.top = this.m_tMargin + "px";
	overlay.width = (this.m_wInner/this.m_viewSlice) * this.m_xTicks;
	overlay.height = this.m_hInner;
	this.m_overlay = overlay;
	this.m_overlay.style.cursor = "move";
	this.m_oView.appendChild(this.m_overlay);

	this.m_oAxisView = document.createElement("div");
	this.m_oAxisView.style.position = "absolute";
	this.m_oAxisView.style.overflow = "visible";
	this.m_oAxisView.style.height = "100%";
	this.m_oAxisView.style.width = (this.m_wInner/this.m_viewSlice) * (this.m_xTicks) + "px";
	this.m_oAxisView.style.left =  (this.m_wInner/this.m_viewSlice) + "px";

	this.m_oAxisFrame.appendChild(this.m_oAxisView);
	this.m_oFrame.appendChild(this.m_oView);

	this.m_oY = document.createElement("div");
	this.m_oY.style.position = "absolute";
	this.m_oY.style.overflow = "hidden";
	this.m_oY.style.height = this.m_hInner + "px";
	this.m_oY.style.width = "1px";
	this.m_oY.style.left = this.m_lMargin + "px";
	this.m_oY.style.top = this.m_tMargin + "px";
	this.m_oY.className = "JSLWAxis";

	this.m_oX = document.createElement("div");	
	this.m_oX.style.position = "absolute";
	this.m_oX.style.overflow = "hidden";
	this.m_oX.style.height = "1px";
	this.m_oX.style.width = this.m_wInner + 1 + "px";
	this.m_oX.style.left = this.m_lMargin + "px";
	this.m_oX.style.top = (h - this.m_bMargin) + "px";
	this.m_oX.className = "JSLWAxis";

	this._createAxis(w, h);
	this.m_oWnd.appendChild(this.m_oAxisFrame);
	this.m_oWnd.appendChild(this.m_oFrame);
	this.m_oWnd.appendChild(this.m_oX);
	this.m_oWnd.appendChild(this.m_oY);

	// we cannot redraw immediately.
	// apparent, in IE6, the initialization of the canvas itself
	// might not yet be ready. Set a timeout to handle this.
	var that = this;
	setTimeout(function() { that.redraw() }, 200);

	var workingGraph = this;
	document.onmousedown = function(e) { return LineGraphDown(workingGraph, e); }
	document.onmouseup = function(e) { return LineGraphUp(workingGraph, e); }

};

function LineGraphDown(graph, e) {
	var dobj = e ? e.target : event.srcElement;
	if (dobj == graph.m_overlay || dobj.parentNode == graph.m_canvas) {
		graph.m_dragging = true;
		graph.m_oldScrollPos = graph.m_scrollPos;
		graph.tx = parseInt(graph.m_oView.style.left+0,10);
		graph.x = e ? e.clientX : event.clientX;
		document.onmousemove = function(e) { return LineGraphMove(graph, e); }; 
		return false;
	}
	return true;
}

function LineGraphUp(graph, e) {
	graph.m_dragging = false;
	return false;
}

function LineGraphMove(graph, e) {
	if (graph.m_dragging)
	{	
		var dist = e ? e.clientX - graph.x : event.clientX - graph.x;
		var deltaScroll = -parseInt(dist/(graph.m_wInner/graph.m_viewSlice));
		graph.m_scrollPos = Math.max(0, Math.min(graph.m_xTicks - graph.m_viewSlice - 1, graph.m_oldScrollPos + deltaScroll));
		var dest = parseInt(-graph.m_scrollPos*(graph.m_wInner/graph.m_viewSlice));
		graph.m_oView.style.left = dest + "px";
		graph.m_oAxisView.style.left = dest + (graph.m_wInner/graph.m_viewSlice) + "px";
		return false;
	}
}

JSLWLineGraph.prototype.redraw = function() {
	for (var i = 0; i < this.m_vals.length; i++) {
		this._drawVals(this.m_vals[i], this.m_data[i] || null, false, this.m_colors[i], this.m_fills[i]);	
	}
};

JSLWLineGraph.prototype._drawVals = function(vals, data, drawFill, color, fill) {
	if (!this.m_canvas.getContext)
		return;

	dc = this.m_canvas.getContext("2d");	

	var x = 0*this.m_wInner/(this.m_viewSlice);
	var y = (100-(vals[0] || 0))*this.m_hInner/100;

	dc.lineWidth = "2";
	dc.strokeStyle = color; //"rgba(80, 130, 200, 1.0)";
	dc.fillStyle = fill; //"rgba(128, 190, 255, 0.2)";

	// fill
	if (drawFill)
	{


	dc.beginPath();
	var i = 0;
	for(i = 0; i < this.m_xTicks; i++)
	{
		if (vals[i] != null) 
		{
			var x = i*this.m_wInner/(this.m_viewSlice);
			var y = (100- (vals[i] || 0))*this.m_hInner/100;
			dc.moveTo(x, y);
			break;
		}
	}

	for(; i < this.m_xTicks; i++)
	{
		x = (i) * this.m_wInner/(this.m_viewSlice);
		y = (100-(vals[i] || 0))*this.m_hInner/100;
		
		if (vals[i] != null) {
			dc.lineTo(x, y);
		}
		
	}

	dc.lineTo(x, this.m_hInner);	
	
	for(i = 0; i < this.m_xTicks; i++)
	{
		if (vals[i] != null) 
		{
			var x = i*this.m_wInner/(this.m_viewSlice);
			var y = (100- (vals[i] || 0))*this.m_hInner/100;
			dc.lineTo(x, this.m_hInner);
			dc.lineTo(x, y);
			break;
		}
	}

	

	dc.fill();
	dc.closePath();



		/*dc.beginPath();
		dc.moveTo(x, this.m_hInner);
		dc.lineTo(x, y);
		for(var i = 1; i < this.m_xTicks; i++)
		{
			x = (i) * this.m_wInner/(this.m_viewSlice);
			y = (100- (vals[i] || 0))*this.m_hInner/100;
			
			dc.lineTo(x, y);
		}
		dc.lineTo(x, this.m_hInner);
		dc.lineTo(0.5*this.m_wInner/(this.m_viewSlice), this.m_hInner);
		dc.fill();
		dc.closePath();*/
	}

	// line
	
	dc.beginPath();
	var i = 0;
	for(i = 0; i < this.m_xTicks; i++)
	{
		if (vals[i] != null) 
		{
			var x = i*this.m_wInner/(this.m_viewSlice);
			var y = (100- (vals[i] || 0))*this.m_hInner/100;
			dc.moveTo(x, y);
			break;
		}
	}

	for(; i < this.m_xTicks; i++)
	{
		x = (i) * this.m_wInner/(this.m_viewSlice);
		y = (100-(vals[i] || 0))*this.m_hInner/100;
		
		if (vals[i] != null) {
			dc.lineTo(x, y);
		}
		
	}
	dc.stroke();
	dc.closePath();

	// points
	dc.fillStyle = color;
	for(i=0; i < this.m_xTicks; i++)
	{
		x = (i) * this.m_wInner/(this.m_viewSlice);
		y = (100-(vals[i] || 0))*this.m_hInner/100;
		
		if (vals[i] != null && data != null && data[i] != null) {
			dc.fillStyle = color;
			dc.fillRect(x-5, y-5, 10, 10);
						
			//dc.fillStyle = fill;
			//dc.fillRect(x-10, y-10, 20, 20);

			var tooltip = document.createElement("div");
			tooltip.className = "JSLWBarGraphInfobox";
			tooltip.style.position = "absolute";
			tooltip.style.display = "none";
			tooltip.style.left = "0px";
			tooltip.style.bottom = this.m_bMargin + this.m_hInner + "px";
			tooltip.innerHTML = data[i];
			tooltip.style.zIndex = 100;
			this.m_oWnd.appendChild(tooltip);		

			var point = document.createElement("div");
			point.className = "graph-point";
			point.style.width = "10px";
			point.style.height = "10px";
			point.style.lineHeight = "10px";
			point.style.position = "absolute";
			point.style.top = y-5 + "px";
			point.style.left = x-5 + "px";
			point.style.fontSize = "0pt";
			this.m_oView.appendChild(point);
			
			point.tooltip = tooltip;
			point.onmouseover = function () { this.tooltip.style.display = "block"; };
			point.onmouseout = function() { this.tooltip.style.display = "none"; };
		}
		
	}
};


JSLWLineGraph.prototype._animateScroll = function() {
	var changed = false;

	var base = parseInt(-this.m_oldScrollPos*(this.m_wInner/(this.m_viewSlice)));
	var dest = parseInt(-this.m_scrollPos*(this.m_wInner/(this.m_viewSlice)));
	var curr = parseInt(this.m_oView.style.left, 10);
	var dist = (dest - base)/8;
	if (curr != dest) {
		changed = true;
		if (Math.abs(curr - dest) <= Math.abs(dist))
			var newscroll = dest;
		else
			var newscroll = base + this._scrollIteration*dist;
		this.m_oView.style.left = newscroll + "px";
		this.m_oAxisView.style.left = newscroll + (this.m_wInner/this.m_viewSlice) + "px";
	}

	this._scrollIteration++;
	if (!changed) {
		clearInterval(this._scrollID);
		this._scrollIteration = 0;
	}
};



// this class comes with its own unit test
var myGraph;

function clickhandler(graph, index) {
	alert(index);
}

function hoverhandler(graph, index) {
	return "Show data for bar <strong>" + index + "</strong>";
}

function JSLWLineGraph_UnitTest() {
	var xArray = [];
	var yArray = ["10k", "20k", "30k", "40k", "50k"];
	var vals = [];
	var vals2 = [];
	vals[0] = 50; 
	vals2[0] = 50;
	for (var i = 0; i < 50; i++ ) xArray[i] = "foo";
	for (var i = 1; i < 500; i++ ) vals[i] = vals[i-1] + Math.random()*10 - 5;
	for (var i = 1; i < 500; i++ ) vals2[i] = vals2[i-1] + Math.random()*10 - 5;
	myGraph = new JSLWLineGraph(700, 300, xArray, yArray, [{5: 80, 6: 70, 7: 50, 8: 40, 9: 35, 10: 32}, {6: 90, 7: 80, 8: 60, 9: 50, 10: 45, 13: 40}, {0: 100, 1: 50, 2: 40}], [{5: "bar", 10: "baz"}, {8: "foo"}, {}], 50, 30, 20, 1);
	myGraph.setHandlers(clickhandler, hoverhandler);
	document.body.appendChild(myGraph.getWnd());
	myGraph.redraw();
}// Edited by Lu Wang for some error fixes

// Copyright 2006 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


// Known Issues:
//
// * Patterns are not implemented.
// * Radial gradient are not implemented. The VML version of these look very
//   different from the canvas one.
// * Clipping paths are not implemented.
// * Coordsize. The width and height attribute have higher priority than the
//   width and height style values which isn't correct.
// * Painting mode isn't implemented.
// * Canvas width/height should is using content-box by default. IE in
//   Quirks mode will draw the canvas using border-box. Either change your
//   doctype to HTML5
//   (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
//   or use Box Sizing Behavior from WebFX
//   (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
// * Optimize. There is always room for speed improvements.

// only add this code if we do not already have a canvas implementation
var G_vmlCanvasManager_ = null;
if (!window.CanvasRenderingContext2D && navigator.userAgent.indexOf("Safari") < 0) {

(function () {

  // alias some functions to make (compiled) code shorter
  var m = Math;
  var mr = m.round;
  var ms = m.sin;
  var mc = m.cos;

  // this is used for sub pixel precision
  var Z = 10;
  var Z2 = Z / 2;

  G_vmlCanvasManager_ = {
    init: function (opt_doc) {
      var doc = opt_doc || document;
      if (/MSIE/.test(navigator.userAgent) && !window.opera) {
        var self = this;
        doc.attachEvent("onreadystatechange", function () {
          self.init_(doc);
        });
      }
    },

    init_: function (doc) {
      if (doc.readyState == "complete") {
        // create xmlns
        if (!doc.namespaces["g_vml_"]) {
          doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml");
        }

        // setup default css
        var ss = doc.createStyleSheet();
        ss.cssText = "canvas{display:inline-block;overflow:hidden;" +
            // default size is 300x150 in Gecko and Opera
            "text-align:left;width:300px;height:150px}" +
            "g_vml_\\:*{behavior:url(#default#VML)}";

        // find all canvas elements
        var els = doc.getElementsByTagName("canvas");
        for (var i = 0; i < els.length; i++) {
          if (!els[i].getContext) {
            this.initElement(els[i]);
          }
        }
      }
    },

    fixElement_: function (el) {
      // in IE before version 5.5 we would need to add HTML: to the tag name
      // but we do not care about IE before version 6
      var outerHTML = el.outerHTML;

      var newEl = el.ownerDocument.createElement(outerHTML);
      // if the tag is still open IE has created the children as siblings and
      // it has also created a tag with the name "/FOO"
      if (outerHTML.slice(-2) != "/>") {
        var tagName = "/" + el.tagName;
        var ns;
        // remove content
        while ((ns = el.nextSibling) && ns.tagName != tagName) {
          ns.removeNode();
        }
        // remove the incorrect closing tag
        if (ns) {
          ns.removeNode();
        }
      }
      el.parentNode.replaceChild(newEl, el);
      return newEl;
    },

    /**
     * Public initializes a canvas element so that it can be used as canvas
     * element from now on. This is called automatically before the page is
     * loaded but if you are creating elements using createElement you need to
     * make sure this is called on the element.
     * @param {HTMLElement} el The canvas element to initialize.
     * @return {HTMLElement} the element that was created.
     */
    initElement: function (el) {
      // el = this.fixElement_(el);
      el.getContext = function () {
        if (this.context_) {
          return this.context_;
        }
        return this.context_ = new CanvasRenderingContext2D_(this);
      };

      // do not use inline function because that will leak memory
      el.attachEvent('onpropertychange', onPropertyChange);
      el.attachEvent('onresize', onResize);

      var attrs = el.attributes;
      if (attrs.width && attrs.width.specified) {
        // TODO: use runtimeStyle and coordsize
        // el.getContext().setWidth_(attrs.width.nodeValue);
        el.style.width = attrs.width.nodeValue + "px";
      } else {
        el.width = el.clientWidth;
      }
      if (attrs.height && attrs.height.specified) {
        // TODO: use runtimeStyle and coordsize
        // el.getContext().setHeight_(attrs.height.nodeValue);
        el.style.height = attrs.height.nodeValue + "px";
      } else {
        el.height = el.clientHeight;
      }
      //el.getContext().setCoordsize_()
      return el;
    }
  };

  function onPropertyChange(e) {
    var el = e.srcElement;

    switch (e.propertyName) {
      case 'width':
        el.style.width = el.attributes.width.nodeValue + "px";
        el.getContext().clearRect();
        break;
      case 'height':
        el.style.height = el.attributes.height.nodeValue + "px";
        el.getContext().clearRect();
        break;
    }
  }

  function onResize(e) {
    var el = e.srcElement;
    if (el.firstChild) {
      el.firstChild.style.width =  el.clientWidth + 'px';
      el.firstChild.style.height = el.clientHeight + 'px';
    }
  }

  G_vmlCanvasManager_.init();

  // precompute "00" to "FF"
  var dec2hex = [];
  for (var i = 0; i < 16; i++) {
    for (var j = 0; j < 16; j++) {
      dec2hex[i * 16 + j] = i.toString(16) + j.toString(16);
    }
  }

  function createMatrixIdentity() {
    return [
      [1, 0, 0],
      [0, 1, 0],
      [0, 0, 1]
    ];
  }

  function matrixMultiply(m1, m2) {
    var result = createMatrixIdentity();

    for (var x = 0; x < 3; x++) {
      for (var y = 0; y < 3; y++) {
        var sum = 0;

        for (var z = 0; z < 3; z++) {
          sum += m1[x][z] * m2[z][y];
        }

        result[x][y] = sum;
      }
    }
    return result;
  }

  function copyState(o1, o2) {
    o2.fillStyle     = o1.fillStyle;
    o2.lineCap       = o1.lineCap;
    o2.lineJoin      = o1.lineJoin;
    o2.lineWidth     = o1.lineWidth;
    o2.miterLimit    = o1.miterLimit;
    o2.shadowBlur    = o1.shadowBlur;
    o2.shadowColor   = o1.shadowColor;
    o2.shadowOffsetX = o1.shadowOffsetX;
    o2.shadowOffsetY = o1.shadowOffsetY;
    o2.strokeStyle   = o1.strokeStyle;
    o2.arcScaleX_    = o1.arcScaleX_;
    o2.arcScaleY_    = o1.arcScaleY_;
  }

  function processStyle(styleString) {
    var str, alpha = 1;

    styleString = String(styleString);
    if (styleString.substring(0, 3) == "rgb") {
      var start = styleString.indexOf("(", 3);
      var end = styleString.indexOf(")", start + 1);
      var guts = styleString.substring(start + 1, end).split(",");

      str = "#";
      for (var i = 0; i < 3; i++) {
        str += dec2hex[Number(guts[i])];
      }

      if ((guts.length == 4) && (styleString.substr(3, 1) == "a")) {
        alpha = guts[3];
      }
    } else {
      str = styleString;
    }

    return [str, alpha];
  }

  function processLineCap(lineCap) {
    switch (lineCap) {
      case "butt":
        return "flat";
      case "round":
        return "round";
      case "square":
      default:
        return "square";
    }
  }

  /**
   * This class implements CanvasRenderingContext2D interface as described by
   * the WHATWG.
   * @param {HTMLElement} surfaceElement The element that the 2D context should
   * be associated with
   */
   function CanvasRenderingContext2D_(surfaceElement) {
    this.m_ = createMatrixIdentity();

    this.mStack_ = [];
    this.aStack_ = [];
    this.currentPath_ = [];

    // Canvas context properties
    this.strokeStyle = "#000";
    this.fillStyle = "#000";

    this.lineWidth = 1;
    this.lineJoin = "miter";
    this.lineCap = "butt";
    this.miterLimit = Z * 1;
    this.globalAlpha = 1;
    this.canvas = surfaceElement;

    var el = surfaceElement.ownerDocument.createElement('div');
    el.style.width =  surfaceElement.clientWidth + 'px';
    el.style.height = surfaceElement.clientHeight + 'px';
    el.style.overflow = 'hidden';
    el.style.position = 'absolute';
    surfaceElement.appendChild(el);

    this.element_ = el;
    this.arcScaleX_ = 1;
    this.arcScaleY_ = 1;
  };

  var contextPrototype = CanvasRenderingContext2D_.prototype;
  contextPrototype.clearRect = function() {
    this.element_.innerHTML = "";
    this.currentPath_ = [];
  };

  contextPrototype.beginPath = function() {
    // TODO: Branch current matrix so that save/restore has no effect
    //       as per safari docs.

    this.currentPath_ = [];
  };

  contextPrototype.moveTo = function(aX, aY) {
    this.currentPath_.push({type: "moveTo", x: aX, y: aY});
    this.currentX_ = aX;
    this.currentY_ = aY;
  };

  contextPrototype.lineTo = function(aX, aY) {
    this.currentPath_.push({type: "lineTo", x: aX, y: aY});
    this.currentX_ = aX;
    this.currentY_ = aY;
  };

  contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
                                            aCP2x, aCP2y,
                                            aX, aY) {
    this.currentPath_.push({type: "bezierCurveTo",
                           cp1x: aCP1x,
                           cp1y: aCP1y,
                           cp2x: aCP2x,
                           cp2y: aCP2y,
                           x: aX,
                           y: aY});
    this.currentX_ = aX;
    this.currentY_ = aY;
  };

  contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
    // the following is lifted almost directly from
    // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
    var cp1x = this.currentX_ + 2.0 / 3.0 * (aCPx - this.currentX_);
    var cp1y = this.currentY_ + 2.0 / 3.0 * (aCPy - this.currentY_);
    var cp2x = cp1x + (aX - this.currentX_) / 3.0;
    var cp2y = cp1y + (aY - this.currentY_) / 3.0;
    this.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, aX, aY);
  };

  contextPrototype.arc = function(aX, aY, aRadius,
                                  aStartAngle, aEndAngle, aClockwise) {
    aRadius *= Z;
    var arcType = aClockwise ? "at" : "wa";

    var xStart = aX + (mc(aStartAngle) * aRadius) - Z2;
    var yStart = aY + (ms(aStartAngle) * aRadius) - Z2;

    var xEnd = aX + (mc(aEndAngle) * aRadius) - Z2;
    var yEnd = aY + (ms(aEndAngle) * aRadius) - Z2;

    // IE won't render arches drawn counter clockwise if xStart == xEnd.
    if (xStart == xEnd && !aClockwise) {
      xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
                       // that can be represented in binary
    }

    this.currentPath_.push({type: arcType,
                           x: aX,
                           y: aY,
                           radius: aRadius,
                           xStart: xStart,
                           yStart: yStart,
                           xEnd: xEnd,
                           yEnd: yEnd});

  };

  contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
    this.moveTo(aX, aY);
    this.lineTo(aX + aWidth, aY);
    this.lineTo(aX + aWidth, aY + aHeight);
    this.lineTo(aX, aY + aHeight);
    this.closePath();
  };

  contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
    // Will destroy any existing path (same as FF behaviour)
    this.beginPath();
    this.moveTo(aX, aY);
    this.lineTo(aX + aWidth, aY);
    this.lineTo(aX + aWidth, aY + aHeight);
    this.lineTo(aX, aY + aHeight);
    this.closePath();
    this.stroke();
  };

  contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
    // Will destroy any existing path (same as FF behaviour)
    this.beginPath();
    this.moveTo(aX, aY);
    this.lineTo(aX + aWidth, aY);
    this.lineTo(aX + aWidth, aY + aHeight);
    this.lineTo(aX, aY + aHeight);
    this.closePath();
    this.fill();
  };

  contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
    var gradient = new CanvasGradient_("gradient");
    return gradient;
  };

  contextPrototype.createRadialGradient = function(aX0, aY0,
                                                   aR0, aX1,
                                                   aY1, aR1) {
    var gradient = new CanvasGradient_("gradientradial");
    gradient.radius1_ = aR0;
    gradient.radius2_ = aR1;
    gradient.focus_.x = aX0;
    gradient.focus_.y = aY0;
    return gradient;
  };

  contextPrototype.drawImage = function (image, var_args) {
    var dx, dy, dw, dh, sx, sy, sw, sh;

    // to find the original width we overide the width and height
    var oldRuntimeWidth = image.runtimeStyle.width;
    var oldRuntimeHeight = image.runtimeStyle.height;
    image.runtimeStyle.width = 'auto';
    image.runtimeStyle.height = 'auto';

    // get the original size
    var w = image.width;
    var h = image.height;

    // and remove overides
    image.runtimeStyle.width = oldRuntimeWidth;
    image.runtimeStyle.height = oldRuntimeHeight;

    if (arguments.length == 3) {
      dx = arguments[1];
      dy = arguments[2];
      sx = sy = 0;
      sw = dw = w;
      sh = dh = h;
    } else if (arguments.length == 5) {
      dx = arguments[1];
      dy = arguments[2];
      dw = arguments[3];
      dh = arguments[4];
      sx = sy = 0;
      sw = w;
      sh = h;
    } else if (arguments.length == 9) {
      sx = arguments[1];
      sy = arguments[2];
      sw = arguments[3];
      sh = arguments[4];
      dx = arguments[5];
      dy = arguments[6];
      dw = arguments[7];
      dh = arguments[8];
    } else {
      throw "Invalid number of arguments";
    }

    var d = this.getCoords_(dx, dy);

    var w2 = sw / 2;
    var h2 = sh / 2;

    var vmlStr = [];

    var W = 10;
    var H = 10;

    // For some reason that I've now forgotten, using divs didn't work
    vmlStr.push(' <g_vml_:group',
                ' coordsize="', Z * W, ',', Z * H, '"',
                ' coordorigin="0,0"' ,
                ' style="width:', W, ';height:', H, ';position:absolute;');

    // If filters are necessary (rotation exists), create them
    // filters are bog-slow, so only create them if abbsolutely necessary
    // The following check doesn't account for skews (which don't exist
    // in the canvas spec (yet) anyway.

    if (this.m_[0][0] != 1 || this.m_[0][1]) {
      var filter = [];

      // Note the 12/21 reversal
      filter.push("M11='", this.m_[0][0], "',",
                  "M12='", this.m_[1][0], "',",
                  "M21='", this.m_[0][1], "',",
                  "M22='", this.m_[1][1], "',",
                  "Dx='", mr(d.x / Z), "',",
                  "Dy='", mr(d.y / Z), "'");

      // Bounding box calculation (need to minimize displayed area so that
      // filters don't waste time on unused pixels.
      var max = d;
      var c2 = this.getCoords_(dx + dw, dy);
      var c3 = this.getCoords_(dx, dy + dh);
      var c4 = this.getCoords_(dx + dw, dy + dh);

      max.x = Math.max(max.x, c2.x, c3.x, c4.x);
      max.y = Math.max(max.y, c2.y, c3.y, c4.y);

      vmlStr.push("padding:0 ", mr(max.x / Z), "px ", mr(max.y / Z),
                  "px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",
                  filter.join(""), ", sizingmethod='clip');")
    } else {
      vmlStr.push("top:", mr(d.y / Z), "px;left:", mr(d.x / Z), "px;")
    }

    vmlStr.push(' ">' ,
                '<g_vml_:image src="', image.src, '"',
                ' style="width:', Z * dw, ';',
                ' height:', Z * dh, ';"',
                ' cropleft="', sx / w, '"',
                ' croptop="', sy / h, '"',
                ' cropright="', (w - sx - sw) / w, '"',
                ' cropbottom="', (h - sy - sh) / h, '"',
                ' />',
                '</g_vml_:group>');

    this.element_.insertAdjacentHTML("BeforeEnd",
                                    vmlStr.join(""));
  };

  contextPrototype.stroke = function(aFill) {
    var lineStr = [];
    var lineOpen = false;
    var a = processStyle(aFill ? this.fillStyle : this.strokeStyle);
    var color = a[0];
    var opacity = a[1] * this.globalAlpha;

    var W = 10;
    var H = 10;

    lineStr.push('<g_vml_:shape',
                 ' fillcolor="', color, '"',
                 ' filled="', Boolean(aFill), '"',
                 ' style="position:absolute;width:', W, ';height:', H, ';"',
                 ' coordorigin="0 0" coordsize="', Z * W, ' ', Z * H, '"',
                 ' stroked="', !aFill, '"',
                 ' strokeweight="', this.lineWidth, '"',
                 ' strokecolor="', color, '"',
                 ' path="');

    var newSeq = false;
    var min = {x: null, y: null};
    var max = {x: null, y: null};

    for (var i = 0; i < this.currentPath_.length; i++) {
      var p = this.currentPath_[i];

      if (p.type == "moveTo") {
        lineStr.push(" m ");
        var c = this.getCoords_(p.x, p.y);
        lineStr.push(mr(c.x), ",", mr(c.y));
      } else if (p.type == "lineTo") {
        lineStr.push(" l ");
        var c = this.getCoords_(p.x, p.y);
        lineStr.push(mr(c.x), ",", mr(c.y));
      } else if (p.type == "close") {
        lineStr.push(" x ");
      } else if (p.type == "bezierCurveTo") {
        lineStr.push(" c ");
        var c = this.getCoords_(p.x, p.y);
        var c1 = this.getCoords_(p.cp1x, p.cp1y);
        var c2 = this.getCoords_(p.cp2x, p.cp2y);
        lineStr.push(mr(c1.x), ",", mr(c1.y), ",",
                     mr(c2.x), ",", mr(c2.y), ",",
                     mr(c.x), ",", mr(c.y));
      } else if (p.type == "at" || p.type == "wa") {
        lineStr.push(" ", p.type, " ");
        var c  = this.getCoords_(p.x, p.y);
        var cStart = this.getCoords_(p.xStart, p.yStart);
        var cEnd = this.getCoords_(p.xEnd, p.yEnd);

        lineStr.push(mr(c.x - this.arcScaleX_ * p.radius), ",",
                     mr(c.y - this.arcScaleY_ * p.radius), " ",
                     mr(c.x + this.arcScaleX_ * p.radius), ",",
                     mr(c.y + this.arcScaleY_ * p.radius), " ",
                     mr(cStart.x), ",", mr(cStart.y), " ",
                     mr(cEnd.x), ",", mr(cEnd.y));
      }


      // TODO: Following is broken for curves due to
      //       move to proper paths.

      // Figure out dimensions so we can do gradient fills
      // properly
      if(c) {
        if (min.x == null || c.x < min.x) {
          min.x = c.x;
        }
        if (max.x == null || c.x > max.x) {
          max.x = c.x;
        }
        if (min.y == null || c.y < min.y) {
          min.y = c.y;
        }
        if (max.y == null || c.y > max.y) {
          max.y = c.y;
        }
      }
    }
    lineStr.push(' ">');

    if (typeof this.fillStyle == "object") {
      var focus = {x: "50%", y: "50%"};
      var width = (max.x - min.x);
      var height = (max.y - min.y);
      var dimension = (width > height) ? width : height;

      focus.x = mr((this.fillStyle.focus_.x / width) * 100 + 50) + "%";
      focus.y = mr((this.fillStyle.focus_.y / height) * 100 + 50) + "%";

      var colors = [];

      // inside radius (%)
      if (this.fillStyle.type_ == "gradientradial") {
        var inside = (this.fillStyle.radius1_ / dimension * 100);

        // percentage that outside radius exceeds inside radius
        var expansion = (this.fillStyle.radius2_ / dimension * 100) - inside;
      } else {
        var inside = 0;
        var expansion = 100;
      }

      var insidecolor = {offset: null, color: null};
      var outsidecolor = {offset: null, color: null};

      // We need to sort 'colors' by percentage, from 0 > 100 otherwise ie
      // won't interpret it correctly
      this.fillStyle.colors_.sort(function (cs1, cs2) {
        return cs1.offset - cs2.offset;
      });

      for (var i = 0; i < this.fillStyle.colors_.length; i++) {
        var fs = this.fillStyle.colors_[i];

        colors.push( (fs.offset * expansion) + inside, "% ", fs.color, ",");

        if (fs.offset > insidecolor.offset || insidecolor.offset == null) {
          insidecolor.offset = fs.offset;
          insidecolor.color = fs.color;
        }

        if (fs.offset < outsidecolor.offset || outsidecolor.offset == null) {
          outsidecolor.offset = fs.offset;
          outsidecolor.color = fs.color;
        }
      }
      colors.pop();

      lineStr.push('<g_vml_:fill',
                   ' color="', outsidecolor.color, '"',
                   ' color2="', insidecolor.color, '"',
                   ' type="', this.fillStyle.type_, '"',
                   ' focusposition="', focus.x, ', ', focus.y, '"',
                   ' colors="', colors.join(""), '"',
                   ' opacity="', opacity, '" />');
    } else if (aFill) {
      lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity, '" />');
    } else {
      lineStr.push(
        '<g_vml_:stroke',
        ' opacity="', opacity,'"',
        ' joinstyle="', this.lineJoin, '"',
        ' miterlimit="', this.miterLimit, '"',
        ' endcap="', processLineCap(this.lineCap) ,'"',
        ' weight="', this.lineWidth, 'px"',
        ' color="', color,'" />'
      );
    }

    lineStr.push("</g_vml_:shape>");

    this.element_.insertAdjacentHTML("beforeEnd", lineStr.join(""));

    this.currentPath_ = [];
  };

  contextPrototype.fill = function() {
    this.stroke(true);
  }

  contextPrototype.closePath = function() {
    this.currentPath_.push({type: "close"});
  };

  /**
   * @private
   */
  contextPrototype.getCoords_ = function(aX, aY) {
    return {
      x: Z * (aX * this.m_[0][0] + aY * this.m_[1][0] + this.m_[2][0]) - Z2,
      y: Z * (aX * this.m_[0][1] + aY * this.m_[1][1] + this.m_[2][1]) - Z2
    }
  };

  contextPrototype.save = function() {
    var o = {};
    copyState(this, o);
    this.aStack_.push(o);
    this.mStack_.push(this.m_);
    this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
  };

  contextPrototype.restore = function() {
    copyState(this.aStack_.pop(), this);
    this.m_ = this.mStack_.pop();
  };

  contextPrototype.translate = function(aX, aY) {
    var m1 = [
      [1,  0,  0],
      [0,  1,  0],
      [aX, aY, 1]
    ];

    this.m_ = matrixMultiply(m1, this.m_);
  };

  contextPrototype.rotate = function(aRot) {
    var c = mc(aRot);
    var s = ms(aRot);

    var m1 = [
      [c,  s, 0],
      [-s, c, 0],
      [0,  0, 1]
    ];

    this.m_ = matrixMultiply(m1, this.m_);
  };

  contextPrototype.scale = function(aX, aY) {
    this.arcScaleX_ *= aX;
    this.arcScaleY_ *= aY;
    var m1 = [
      [aX, 0,  0],
      [0,  aY, 0],
      [0,  0,  1]
    ];

    this.m_ = matrixMultiply(m1, this.m_);
  };

  /******** STUBS ********/
  contextPrototype.clip = function() {
    // TODO: Implement
  };

  contextPrototype.arcTo = function() {
    // TODO: Implement
  };

  contextPrototype.createPattern = function() {
    return new CanvasPattern_;
  };

  // Gradient / Pattern Stubs
  function CanvasGradient_(aType) {
    this.type_ = aType;
    this.radius1_ = 0;
    this.radius2_ = 0;
    this.colors_ = [];
    this.focus_ = {x: 0, y: 0};
  }

  CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
    aColor = processStyle(aColor);
    this.colors_.push({offset: 1-aOffset, color: aColor});
  };

  function CanvasPattern_() {}

  // set up externs
  G_vmlCanvasManager = G_vmlCanvasManager_;
  CanvasRenderingContext2D = CanvasRenderingContext2D_;
  CanvasGradient = CanvasGradient_;
  CanvasPattern = CanvasPattern_;

})();

} // if
