dojo.provide("dojox.gfx.canvas");

dojo.require("dojox.gfx._base");
dojo.require("dojox.gfx.shape");
dojo.require("dojox.gfx.path");

dojo.experimental("dojox.gfx.canvas");

dojox.gfx.canvas.xmlns = {
	xlink: "http://www.w3.org/1999/xlink",
	svg:   "http://www.w3.org/2000/svg"
};

dojox.gfx.canvas.getRef = function(name){
	// summary: returns a DOM Node specified by the name argument or null
	// name: String: an SVG external reference
//TODO: Determine if this is really necessary for html canvas implementation
	if(!name || name == "none"){return null;}
	if(name.match(/^url\(#.+\)$/)){
		return dojo.byId(name.slice(5, -1));	// Node
	}
	// alternative representation of a reference
	if(name.match(/^#dojoUnique\d+$/)){
		// we assume here that a reference was generated by dojox.gfx
		return dojo.byId(name.slice(1));	// Node
	}
	return null;	// Node
};

//Common constants
dojox.gfx.canvas.pi2 = Math.PI / 2;
dojox.gfx.canvas.pi4 = Math.PI / 4;
dojox.gfx.canvas.pi8 = Math.PI / 8;
dojox.gfx.canvas.two_pi = Math.PI * 2;
dojox.gfx.canvas.kappa = 4 * ((Math.sqrt(2) -1) / 3);

dojox.gfx.canvas.dasharray = {
	shortdash:			"ShortDash",
	shortdot:			"ShortDot",
	shortdashdot:		"ShortDashDot",
	shortdashdotdot:	"ShortDashDotDot",
	dot:				"Dot",
	dash:				"Dash",
	longdash:			"LongDash",
	dashdot:			"DashDot",
	longdashdot:		"LongDashDot",
	longdashdotdot:		"LongDashDotDot"
};

dojox.gfx.canvas.shapeExt = {
	// summary: HTML Canvas-specific implementation of dojox.gfx.Shape methods
	_getGC: function(){
		return this.parent.ctx;
	},
	setFill: function(fill){
		// summary: sets a fill into the current graphics context
		// fill: Object: a fill object
		//	(see dojox.gfx.defaultLinearGradient, 
		//	dojox.gfx.defaultRadialGradient, 
		//	dojox.gfx.defaultPattern, 
		//	or dojo.Color)
		if(!fill){
			// fill=none
			this.fillStyle = null;
			return this;
		}
		if(typeof(fill) == "object" && "type" in fill){
			if (fill.type=="linear"){
				this.fillStyle = dojox.gfx.makeParameters(dojox.gfx.defaultLinearGradient, fill);
			}
			if (fill.type=="radial"){
				this.fillStyle = dojox.gfx.makeParameters(dojox.gfx.defaultRadialGradient, fill);
			}
			if (fill.type=="pattern"){
				this.fillStyle = dojox.gfx.makeParameters(dojox.gfx.defaultPattern, fill);
				if(!this.fillStyle.src){return this;} 
				dojox.gfx.canvas.imageLoader.loadImage(this.fillStyle.src);
			}
			return this;
		}
		// Use a color object as fill
		var f = dojox.gfx.normalizeColor(fill);
		this.fillStyle = f;
		return this;	// self
	},
	_fill: function(){
		var ctx = this._getGC();
		if(!this.fillStyle){
			// fill=none
			this.parent.ctx.fillStyle = "rgba(0,0,0,0.0)";
			return;
		}
		if(typeof(this.fillStyle) == "object" && "type" in this.fillStyle){
				// gradient
				// TODO: Fix special fill styles for canvas
			var f = null;
			with (this.fillStyle){
				switch(this.fillStyle.type){
					case "linear":
						f = this._getGC().createLinearGradient(x1,y1,x2,y2);
						for (var csi=0;csi<colors.length;csi++){
							f.addColorStop(colors[csi].offset,this._normalizeColor(colors[csi].color).toString());
						}
						break;
					case "radial":
						f = this._getGC().createRadialGradient(cx,cy,0,cx,cy,r);
						for (var csi=0;csi<colors.length;csi++){
							f.addColorStop(colors[csi].offset,this._normalizeColor(colors[csi].color).toString());
						}
						break;
					case "pattern":
						if (this.fillStyle.src){
							this.fillStyle.img=dojox.gfx.canvas.imageLoader.getImage(this.fillStyle.src);
							if(!this.fillStyle.img){return this;} 
							f=this._getGC().createPattern(this.fillStyle.img,"repeat");
							// TODO: other repeat styles are possible (no-repeat,repeat-x,repeat-y). 
						}
				}
			}
			ctx.fillStyle=f;
		} else {
			// Set fill color using CSS RGBA func style
			ctx.fillStyle=this.fillStyle.toString();
		}
	},
	_normalizeColor: function(color){
		var c = dojox.gfx.normalizeColor(color);
		if (isNaN(c.r)){c.r = 0;}
		if (isNaN(c.g)){c.g = 0;}
		if (isNaN(c.b)){c.b = 0;}
		if (isNaN(c.a)){c.a = 1;}
		return c;
	},
	setStroke: function(stroke){
		// summary: sets a stroke to use for the shape. 
		// stroke: Object: a stroke object
		//	(see dojox.gfx.defaultStroke) 
		if(!stroke){
			// don't stroke
			this.strokeStyle = null;
			return this;
		}
		// normalize the stroke
		if(typeof stroke == "string"){
			stroke = {color: stroke};
		}
		var s = this.strokeStyle;
		s = dojox.gfx.makeParameters(dojox.gfx.defaultStroke, stroke);
		s.color = dojox.gfx.normalizeColor(s.color);
		// generate attributes
		if(s){
			this.strokeStyle = s;
			var da = s.style.toLowerCase();
			if(da in dojox.gfx.canvas.dasharray){
				this.strokeStyle.daurl = dojo.moduleUrl("dojox","gfx/images/" + dojox.gfx.canvas.dasharray[da] + ".JPG");
				dojox.gfx.canvas.imageLoader.loadImage(this.strokeStyle.daurl);
			}
		}
		return this;	// self
	},
	_stroke: function(){
		// summary: Applies the stroke into current graphics context 
		// stroke: Object: a stroke object
		//	(see dojox.gfx.defaultStroke) 
		var ctx = this._getGC();
		if(!this.strokeStyle){
			// no stroke
			ctx.strokeStyle="rgba(0,0,0,0.0)";
			ctx.stroke();
			return;
		}
		var s = this.strokeStyle;
		if (!s.daurl){
			ctx.strokeStyle=s.color.toString();
		}else{
			this.strokeStyle.img=dojox.gfx.canvas.imageLoader.getImage(this.strokeStyle.daurl);
			if(!this.strokeStyle.img){return this;}
			dash = ctx.createPattern(this.strokeStyle.img,"repeat");
			ctx.strokeStyle=dash;
		}
		ctx.lineWidth = s.width;
		ctx.lineCap = s.cap;
		if(typeof s.join == "number"){
			ctx.lineJoin = "miter";
			ctx.miterLimit = s.join;
		}else{
			ctx.lineJoin = s.join;
		}
		ctx.stroke();
	},
	_getParentSurface: function(){
		var surface = this.parent;
		for(; surface && !(surface instanceof dojox.gfx.Surface); surface = surface.parent);
		return surface;
	},
	setRawNode: function(rawNode){
		/* Unnecessary for most canvas scene graph shapes (other than Surface and Group for placeholder divs)
	},

	setShape: function(newShape){
		/* Unnecessary for canvas scene graph
		// summary: sets a shape object (SVG)
		// newShape: Object: a shape object
		//	(see dojox.gfx.defaultPath,
		//	dojox.gfx.defaultPolyline,
		//	dojox.gfx.defaultRect,
		//	dojox.gfx.defaultEllipse,
		//	dojox.gfx.defaultCircle,
		//	dojox.gfx.defaultLine,
		//	or dojox.gfx.defaultImage)
		this.shape = dojox.gfx.makeParameters(this.shape, newShape);
		for(var i in this.shape){
			if(i != "type"){ this.rawNode.setAttribute(i, this.shape[i]); }
		}
		return this;	// self
	*/
	},
	// move family
	_moveToFront: function(){
		// summary: moves a shape to front of its parent (surface or group's) list of shapes
		//this.parent.appendChild(this.rawNode);
		return this;	// self
	},
	_moveToBack: function(){
		// summary: moves a shape to back of its parent's list of shapes 
		//this.parent.insertBefore(this.rawNode, this.rawNode.parentNode.firstChild);
		return this;	// self
	},
	// transformations
	_getRealMatrix: function(){
		var m = this.matrix;
		var p = this.parent;
		while(p){
			if(p.matrix){
				m = dojox.gfx.matrix.multiply(p.matrix, m);
			}
			p = p.parent;
		}
		return m;
	},
	_applyTransform: function() {
		// NOOP'd - render is separate stage for canvas.
		// _transform is used instead.
		return this;
	},
	
	_transform: function() {
		var tm = this._getRealMatrix();
		var ctx = this._getGC();
		if(tm){
			ctx.translate(tm.dx,tm.dy); 
			ctx.rotate(tm.yx);
//			ctx.scale(tm.xx,tm.yy);
//			console.debug("trans("+tm.dx+","+tm.dy+") rot("+tm.yx+") scale("+tm.xx+","+tm.yy+") ");
		}else{
			ctx.translate(0,0);
			ctx.rotate(0);
			ctx.scale(1);
		}
		// TODO: Scaling
		// TODO: Skewing
	}
}
dojo.extend(dojox.gfx.Shape, dojox.gfx.canvas.shapeExt);

dojo.declare("dojox.gfx.Group", dojox.gfx.Shape, {
	// summary: a group shape (a Composite), which can be used 
	//	to logically group shapes (e.g, to propagate matricies)
	constructor: function(){
		dojox.gfx.canvas.Container._init.call(this);
	},
	// apply transformation
	_applyTransform: function(){
		// summary: applies a transformation matrix to a group
		var matrix = this._getRealMatrix();
		for(var i = 0; i < this.children.length; ++i){
			this.children[i]._updateParentMatrix(matrix);
		}
		return this;	// self
	},
	setShape: function(newShape){
		return this.setTransform(this.matrix);	// self
		// NOOP--groups have no shape.
	},
	setRawNode: function(rawNode){
		// summary:
		//	assigns and clears the underlying node that will represent this
		//	shape. Once set, transforms, gradients, etc, can be applied.
		this.rawNode = rawNode;
	},
	_getGC: function(){
		return this.ctx;
	},
	render: function(){
		for (var i=0;i<this.children.length;i++){
			this.children[i].render();
		}
	}
});
dojox.gfx.Group.nodeType = "group";

dojo.declare("dojox.gfx.Rect", dojox.gfx.shape.Rect, {
	// summary: a rectangle shape 
	setShape: function(newShape){
		// summary: sets a rectangle shape object (SVG)
		// newShape: Object: a rectangle shape object
		//this.shape = dojox.gfx.makeParameters(this.shape, newShape);
		this.bbox = null;
		this.shape = dojo.clone(dojox.gfx.defaultRect);
		for(var i in newShape){
			this.shape[i]=newShape[i];
		}
		return this.setTransform(this.matrix);	// self
	},
	render: function(){
		var ctx = this._getGC();
		if (!this.shape.r || this.shape.r==0){
			//Draw the rect without rounded corners the hard way (to allow skew);
			ctx.save(); 
			this._transform();
			ctx.beginPath();
			this._fill();
		 	ctx.translate(this.shape.x,this.shape.y);
		 	ctx.moveTo(0,0);
		 	ctx.lineTo(this.shape.width,0);
		 	ctx.lineTo(this.shape.width,this.shape.height);
		 	ctx.lineTo(0,this.shape.height);
		 	ctx.lineTo(0,0);
		 	ctx.lineTo(this.shape.width,0);
			ctx.fill();
			this._stroke();
			ctx.closePath();
			ctx.restore(); 
		}else{
			//Draw the rect with rounded corners the hard way.
			ctx.save(); // save graphics context prior to rendering shape
			this._transform();
			ctx.beginPath();
			this._fill();
		 	ctx.translate(this.shape.x,this.shape.y);
		 	ctx.moveTo(this.shape.r,0);
		 	ctx.arc(this.shape.width-this.shape.r,this.shape.r,this.shape.r, -dojox.gfx.canvas.pi2, 0, false);
		 	ctx.arc(this.shape.width-this.shape.r,this.shape.height-this.shape.r,this.shape.r, 0, dojox.gfx.canvas.pi2, false);
		 	ctx.arc(this.shape.r,this.shape.height-this.shape.r,this.shape.r, dojox.gfx.canvas.pi2, Math.PI, false);
		 	ctx.arc(this.shape.r,this.shape.r,this.shape.r, Math.PI, dojox.gfx.canvas.pi2, false);
			ctx.fill();
			this._stroke();
			ctx.closePath();
			ctx.restore(); // return graphics context after rendering shape
		}
	}
});
dojox.gfx.Rect.nodeType = "rect";

dojo.declare("dojox.gfx.Ellipse", dojox.gfx.shape.Ellipse, {
	setShape: function(newShape){
		// summary: sets a ellipse shape object 
		// newShape: Object: an ellipse shape object
		this.shape = dojox.gfx.makeParameters(dojox.gfx.defaultEllipse, newShape);
		return this.setTransform(this.matrix);	// self
	},
	render: function(){
		console.debug("========= ellipse ===========");
		console.debug("cx,cy=("+this.shape.cx+","+this.shape.cy+")");
		console.debug("rx="+this.shape.rx+" ry="+this.shape.ry);
		var ctx=this._getGC();
		ctx.save(); 
		this._transform();
		ctx.beginPath();
		this._fill();
	 	ctx.beginPath();
	 	var k = dojox.gfx.canvas.kappa;
	 	with (this.shape){
		 	ctx.moveTo(cx,cy-ry);
		 	ctx.bezierCurveTo(cx+(k*rx),cy-ry,cx+rx,cy-(k*ry),cx+rx,cy);
		 	ctx.bezierCurveTo(cx+rx,cy+(k*ry),cx+(k*rx),cy+ry,cx,cy+ry);
		 	ctx.bezierCurveTo(cx-(k*rx),cy+ry,cx-rx,cy+(k*ry),cx-rx,cy);
		 	ctx.bezierCurveTo(cx-rx,cy-(k*ry),cx-(k*rx),cy-ry,cx,cy-ry);
	 	}
		ctx.fill();
		this._stroke();
		ctx.closePath();
		ctx.restore(); 
	}
});
dojox.gfx.Ellipse.nodeType = "ellipse";

dojo.declare("dojox.gfx.Circle", dojox.gfx.shape.Circle, {
	setShape: function(newShape){
		// summary: sets a circle shape object (SVG)
		// newShape: Object: a circle shape object
		this.shape = dojo.clone(newShape);
		return this.setTransform(this.matrix);	// self
	},
	render: function(){
		var ctx=this._getGC();
		ctx.save(); // save graphics context prior to rendering shape
		this._transform();
		ctx.beginPath();
	 	ctx.arc(this.shape.cx,this.shape.cy,this.shape.r,0,dojox.gfx.canvas.two_pi,true);
		this._fill();
		ctx.fill();
		this._stroke();
		ctx.closePath();
		ctx.restore(); // return graphics context after rendering shape
	}
});
dojox.gfx.Circle.nodeType = "circle";

dojo.declare("dojox.gfx.Line", dojox.gfx.shape.Line, {
	setShape: function(newShape){
		// summary: sets a text shape object, given shape properties
		// newShape: Object: a text shape object containing props to set
		this.shape = dojo.clone(newShape);
		return this.setTransform(this.matrix);	// self
	},
	render: function(){
		var ctx=this._getGC();
		ctx.save(); // save graphics context prior to rendering shape
		this._transform();
		ctx.beginPath();
	 	ctx.moveTo(this.shape.x1,this.shape.y1);
	 	ctx.lineTo(this.shape.x2,this.shape.y2);
		this._fill();
		ctx.fill();
		this._stroke();
		ctx.closePath();
		ctx.restore(); // return graphics context after rendering shape
	}
});
dojox.gfx.Line.nodeType = "line";

dojo.declare("dojox.gfx.Polyline", dojox.gfx.shape.Polyline, {
	// summary: a polyline/polygon shape 
	setShape: function(points, closed){
		// summary: sets a polyline/polygon shape object 
		// points: Object: a polyline/polygon shape object
		if(points && points instanceof Array){
			// branch
			// points: Array: an array of points
			this.shape = dojox.gfx.makeParameters(this.shape, { points: points });
			if(closed && this.shape.points.length){ 
				this.shape.points.push(this.shape.points[0]);
			}
		}else{
			this.shape = dojox.gfx.makeParameters(this.shape, points);
		}
		this.box = null;
		var attr = [];
		var p = this.shape.points;
		for(var i = 0; i < p.length; ++i){
			if(typeof p[i] == "number"){
				attr.push(p[i].toFixed(8));
			}else{
				attr.push(p[i].x.toFixed(8));
				attr.push(p[i].y.toFixed(8));
			}
		}
//		this.rawNode.setAttribute("points", attr.join(" "));
		return this.setTransform(this.matrix);	// self
	},
	render: function(){
		var ctx=this._getGC();
		if (this.shape.points.length==0){return;}
		ctx.save(); // save graphics context prior to rendering shape
		this._transform();
		ctx.beginPath();
		this._fill();
		if (this.shape.points[0] instanceof Object){
			// points is array of points [{x:x0,y:y0},...,{x:xN,y:yN}]
			ctx.moveTo(this.shape.points[0].x,this.shape.points[0].y);
			for (var i=1; i<this.shape.points.length; ++i){
				ctx.lineTo(this.shape.points[i].x,this.shape.points[i].y);
			}
		} else {
			// points is array of points [x0,y0,...,xN,yN]
			ctx.moveTo(this.shape.points[0],this.shape.points[1]);
			for (var i=2; i<this.shape.points.length-1; i=i+2){
				ctx.lineTo(this.shape.points[i],this.shape.points[i+1]);
			}

		}
		this._stroke();
		ctx.fill();
		ctx.closePath();
		ctx.restore(); // return graphics context after rendering shape
	}
});
dojox.gfx.Polyline.nodeType = "polyline";

// Originally taken from vml.js--candidate for _base.js or path.js
dojox.gfx.path._calcArc = function(alpha){
	// return a start point, 1st and 2nd control points, and an end point
	var cosa  = Math.cos(alpha), sina  = Math.sin(alpha),
		p2 = {x: cosa + (4 / 3) * (1 - cosa), y: sina - (4 / 3) * cosa * (1 - cosa) / sina};
	return {
		s:  {x: cosa, y: -sina},
		c1: {x: p2.x, y: -p2.y},
		c2: p2,
		e:  {x: cosa, y: sina}
	};
};

dojo.declare("dojox.gfx.Path", dojox.gfx.path.Path, {
	// summary: A shape defined using an SVG path string.
	
	render: function(){
		// summary: Renders the path onto the HTML canvas, iterating through each path segment.
		var ctx=this._getGC();
		this.last={x:0,y:0};
		this.first=null;
		if (this.segments.length==0){return;}
		ctx.save(); // save graphics context prior to rendering shape
		this._transform();
		ctx.beginPath();
		this._fill();
		//Iterate and render each path segments...
		for (var i=0;i<this.segments.length;i++){
			this._renderSegment(this.segments[i]);
		}
		this._stroke();
		ctx.fill();
		ctx.closePath(); // Ignore closePath, and implicitly close all paths
		ctx.restore(); // return graphics context after rendering shape
	},
	_renderSegment: function(segment){
		// summary: Render the specified segment on the surface of the canvas
		// segment: Object: Segment to be rendered
		var ctx=this._getGC();
		var n = segment.args;
		// update internal variables: bbox, absolute, last
		switch(segment.action){
			case "M": // moveTo (Absolute)
				ctx.moveTo(n[0],n[1]);
				if (!this.first){this.first={x:n[0],y:n[1]};}//If first position not at origin, track it
				this.last = {x:n[0],y:n[1]};
				if (!this.first){this.first=this.last;}
				this.lastControl = {};
				var n2=n;
				while (n2.length>2){
					n2=n2.slice(2,n.length);
					this._renderSegment({action:"L",args:n2.slice(0,2)});
				}
				break;
			case "L": // lineTo (Absolute)
				ctx.lineTo(n[0],n[1]);
				this.last={x:n[0],y:n[1]};
				if (!this.first){this.first=this.last;}
				this.lastControl = {};
				var n2=n;
				while (n2.length>2){
					n2=n2.slice(2,n.length);
					this._renderSegment({action:"L",args:n2.slice(0,2)});
				}
				break;
			case "C": // Bezier curveTo (Absolute)
				ctx.bezierCurveTo(n[0],n[1],n[2],n[3],n[4],n[5]);
				this.last={x:n[4],y:n[5]};
				this.lastControl={x:n[2],y:n[3],type:"C"};
				var n2=n;
				while (n2.length>6){
					n2=n2.slice(6,n.length);
					this._renderSegment({action:"C",args:n2.slice(0,6)});
				}
				break;
			case "S": // Smooth Bezier curveTo (Absolute)
				if(this.lastControl.type == "C"){// VERIFIED - BUT 2*last.x doesnt seem correct
					ctx.bezierCurveTo(2*this.last.x-this.lastControl.x,
									  2*this.last.y-this.lastControl.y,
									  n[0],n[1],n[2],n[3]);
				}else{
					ctx.bezierCurveTo(this.last.x,this.last.y,n[0],n[1],n[2],n[3]);
				}
				this.last={x:n[2],y:n[3]};
				this.lastControl={x:n[0],y:n[1],type:"C"};
				var n2=n;
				while (n2.length>4){
					n2=n2.slice(4,n.length);
					this._renderSegment({action:"C",args:n2.slice(0,4)}); // Curve to absolute for remaining args
				}
				break;
			case "Q": // Quadratic curveTo (Absolute)
				ctx.quadraticCurveTo(n[0],n[1],n[2],n[3]);
				this.last={x:n[2],y:n[3]};
				this.lastControl={x:n[0],y:n[1],type:"Q"};
				var n2=n;
				while (n2.length>4){
					n2=n2.slice(4,n.length);
					this._renderSegment({action:"Q",args:n2.slice(0,4)}); // Curve to absolute for remaining args
				}
				break;
			case "T": // Smooth Quadratic curveTo (Absolute)
				if(this.lastControl.type == "Q"){
					this.lastControl.x=2*this.last.x-this.lastControl.x;
					this.lastControl.y=2*this.last.y-this.lastControl.y;
					ctx.quadraticCurveTo(this.lastControl.x,this.lastControl.y,n[0],n[1]);
				}else{
					ctx.quadraticCurveTo(this.last.x,this.last.y,n[0],n[1]);
					this.lastControl.x=this.last.x;
					this.lastControl.y=this.last.y;
				}
				this.last={x:n[0],y:n[1]};
				this.lastControl.type="Q";
				var n2=n;
				while (n2.length>2){
					n2=n2.slice(2,n.length);
					this._renderSegment({action:"Q",args:n2.slice(0,2)}); // Curve to absolute for remaining args
				}
				break;
			case "H": // Horizontal lineTo(y) (Absolute)
				ctx.lineTo(n[0],this.last.y); 
				this.last.x=n[0];
				this.last.y=this.last.y;
				if (!this.first){this.first=this.last;}
				this.lastControl = {};
				break;
			case "V": // Vertical lineTo(x) (Absolute)
				ctx.lineTo(this.last.x,n[0]);
				this.last.x=this.last.x;
				this.last.y=n[0];
				this.lastControl = {};
				if (!this.first){this.first=this.last;}
				break;
			case "m": // moveTo (Relative)
				ctx.moveTo(this.last.x+n[0],this.last.y+n[1]);
				this.last.x=this.last.x+n[0];
				this.last.y=this.last.y+n[1];
				if (!this.first){this.first=this.last;}
				this.lastControl = {};
				var n2=n;
				while (n2.length>2){
					n2=n2.slice(2,n.length);
					this._renderSegment({action:"l",args:n2.slice(0,2)});
				}
				break;
			case "l": // lineTo (Relative)
				ctx.moveTo(this.last.x,this.last.y);
				ctx.lineTo(this.last.x+n[0],this.last.y+n[1]);
				this.last.x=this.last.x+n[0];
				this.last.y=this.last.y+n[1];
				if (!this.first){this.first=this.last;}
				this.lastControl = {};
				var n2=n;
				while (n2.length>2){
					n2=n2.slice(2,n.length);
					this._renderSegment({action:"l",args:n2.slice(0,2)});
				}
				break;
			case "q": // Quadratic curveTo (Relative)
				ctx.quadraticCurveTo(this.last.x+n[0],this.last.y+n[1],
								     this.last.x+n[2],this.last.y+n[3]);
				this.lastControl={x:this.last.x+n[0],y:this.last.y+n[1],type:"Q"};
				this.last.x=this.last.x+n[2];
				this.last.y=this.last.y+n[3];
				var n2=n;
				while (n2.length>4){
					n2=n2.slice(4,n.length);
					this._renderSegment({action:"Q",args:n2.slice(0,4)});// Quadratic absolute remaining
				}
				break;
			case "t": // Smooth Quadratic curveTo (Relative)
				if(this.lastControl.type == "Q"){
					this.lastControl.x=2*this.last.x-this.lastControl.x;
					this.lastControl.y=2*this.last.y-this.lastControl.y;
					ctx.quadraticCurveTo(this.lastControl.x,this.lastControl.y,
							 this.last.x+n[0],this.last.y+n[1]);
				}else{
					this.lastControl.x=this.last.x;
					this.lastControl.y=this.last.y;
					ctx.quadraticCurveTo(this.lastControl.x,this.lastControl.y,
										 this.last.x+n[0],this.last.y+n[1]);
				}
				this.last.x=this.last.x+n[0];
				this.last.y=this.last.y+n[1];
				this.lastControl.type = "Q";
				var n2=n;
				while (n2.length>2){
					n2=n2.slice(2,n.length);
					this._renderSegment({action:"Q",args:n2.slice(0,2)});// Quadratic absolute remaining
				}
				break;
			case "h": // Horizontal lineTo (Relative)
				ctx.moveTo(this.last.x,this.last.y);
				ctx.lineTo(this.last.x+n[0],this.last.y);
				this.last.x=this.last.x+n[0];
				this.last.y=this.last.y;
				if (!this.first){this.first=this.last;}
				this.lastControl = {};
				break;
			case "v": // Vertical lineTo (Relative)
				ctx.moveTo(this.last.x,this.last.y);
				ctx.lineTo(this.last.x,this.last.y+n[0]);
				this.last.x=this.last.x;
				this.last.y=this.last.y+n[0];
				if (!this.first){this.first=this.last;}
				this.lastControl = {};
				break;
			case "c": // Bezier curveTo (Relative)
				var len=n.length;
				ctx.bezierCurveTo(this.last.x+n[0],this.last.y+n[1],
								  this.last.x+n[2],this.last.y+n[3],
								  this.last.x+n[4],this.last.y+n[5]);
				this.last.x += n[4];
				this.last.y += n[5];
				this.lastControl={x:n[2],y:n[3],type:"C"};  
				var n2=n;
				while (n2.length>6){
					n2=n2.slice(6,n.length);
					this._renderSegment({action:"c",args:n2.slice(0,6)});// curve to relative
				}
				break;
			case "s": // Smooth Bezier curveTo (Relative)
				if(this.lastControl.type == "C"){
					ctx.bezierCurveTo( 
							this.last.x+(this.last.x-this.lastControl.x),this.last.y+(this.last.y-this.lastControl.y),
							this.last.x+n[0],this.last.y+n[1],
							this.last.x+n[2],this.last.y+n[3]);
				}else{
					ctx.bezierCurveTo(
							0,0,
							this.last.x+n[0],this.last.y+n[1],
							this.last.x+n[2],this.last.y+n[3]);
				}
				this.lastControl={x:this.last.x+n[0],y:this.last.y+n[1],type:"C"};
				this.last.x += n[2];
				this.last.y += n[3];
				var n2=n;
				while (n2.length>4){
					n2=n2.slice(4,n.length);
					this._renderSegment({action:"c",args:n2.slice(0,4)});// curve to relative
				}
				break;
			case "A":
			case "a":
				relative = segment.action == "a";
				var x1 = n[5], y1 = n[6];
				if(relative){
					x1 += this.last.x;
					y1 += this.last.y;
				}
				this._renderArcTo(this.last, n[0], n[1], n[2], 
								  n[3] ? 1 : 0, n[4] ? 1 : 0,
								  x1, y1
				);
				this.last = {x: x1, y: y1};
				this.lastControl = {};
				break;
			case "Z":
			case "z": // closePath (SVG closepath action)- draw a line back to first point in segment
				if (this.first){
					ctx.lineTo(this.first.x,this.first.y); // Return to first position
				}else{
					ctx.lineTo(0,0); // Return to origin
				}
				this.lastControl = {};
				break;
		}
	},
	_curvePI4: dojox.gfx.path._calcArc(dojox.gfx.canvas.pi8),
	_renderArcTo: function(last, rx, ry, xRotg, large, sweep, x, y){
		var m = dojox.gfx.matrix, ctx = this._getGC();
		// calculate parameters
		var xRot = m._degToRad(xRotg),
			rx2 = rx * rx, ry2 = ry * ry,
			pa = m.multiplyPoint(
					m.rotate(-xRot), 
					{x: (last.x - x) / 2, y: (last.y - y) / 2}
			),
			pax2 = pa.x * pa.x, pay2 = pa.y * pa.y,
			c1 = Math.sqrt((rx2 * ry2 - rx2 * pay2 - ry2 * pax2) / (rx2 * pay2 + ry2 * pax2));
		if(isNaN(c1)){ c1 = 0; }
		var ca = {
				x:  c1 * rx * pa.y / ry,
				y: -c1 * ry * pa.x / rx
			};
		if(large == sweep){
			ca = {x: -ca.x, y: -ca.y};
		}
		// our center
		var c = m.multiplyPoint(
			[
				m.translate(
					(last.x + x) / 2,
					(last.y + y) / 2
				),
				m.rotate(xRot)
			], 
			ca
		);
		// calculate our elliptic transformation
		var elliptic_transform = m.normalize([
			m.translate(c.x, c.y),
			m.rotate(xRot),
			m.scale(rx, ry)
		]);
		// start, end, and size of our arc
		var inversed = m.invert(elliptic_transform),
			sp = m.multiplyPoint(inversed, last),
			ep = m.multiplyPoint(inversed, x, y),
			startAngle = Math.atan2(sp.y, sp.x),
			endAngle   = Math.atan2(ep.y, ep.x),
			// size of our arc in radians
			theta = sweep ? endAngle - startAngle : startAngle - endAngle;
		if(theta < 0){
			theta += dojox.gfx.canvas.two_pi;
		}else if(theta > dojox.gfx.canvas.two_pi){
			theta = dojox.gfx.canvas.two_pi;
		}
		// draw curve chunks
		var pi4 = Math.PI / 4, alpha = Math.PI / 8, curve = this._curvePI4, 
			step  = sweep ? alpha : -alpha;
		for(var angle = theta; angle > 0; angle -= pi4){
			if(angle < pi4){
				alpha = angle / 2;
				curve = dojox.gfx.path._calcArc(alpha);
				step  = sweep ? alpha : -alpha;
			}
			var c1, c2, e,
				M = m.normalize([elliptic_transform, m.rotate(startAngle + step)]);
			if(sweep){
				c1 = m.multiplyPoint(M, curve.c1);
				c2 = m.multiplyPoint(M, curve.c2);
				e  = m.multiplyPoint(M, curve.e );
			}else{
				c1 = m.multiplyPoint(M, curve.c2);
				c2 = m.multiplyPoint(M, curve.c1);
				e  = m.multiplyPoint(M, curve.s );
			}
			// draw the curve
			ctx.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, e.x, e.y);
			startAngle += 2 * step;
		}
	}
	
});
dojo.extend(dojox.gfx.Path, dojox.gfx.canvas.shapeExt);
dojo.extend(dojox.gfx.Path, {
	// Note: This function is a copy of the function in dojo.gfx.path.Path it has to be mixed-in at this point 
	// because the mixin of the Shape extension functions above overwrite the existing inherited setShape() 
	// function from Path.
	setShape: function(newShape){
		// summary: forms a path using a shape
		// newShape: Object: an SVG path string or a path object (see dojox.gfx.defaultPath)
		this.shape = dojox.gfx.makeParameters(this.shape, typeof newShape == "string" ? {path: newShape} : newShape);
		var path = this.shape.path;
		// switch to non-updating version of path building
		this.shape.path = [];
		this._setPath(path);
		// switch back to the string path
		this.shape.path = this.shape.path.join("");
		return this.setTransform(this.matrix);	// self
	}
});
dojox.gfx.Path.nodeType = "path";

dojo.declare("dojox.gfx.Image", dojox.gfx.shape.Image, {
	// summary: an image 
	setShape: function(newShape){
		// summary: sets an image shape object
		// newShape: Object: an image shape object
		this.shape = dojo.clone(newShape);
		if(!this.shape.x){this.shape.x=0;}
		if(!this.shape.y){this.shape.y=0;}
		if(!this.shape.width){this.useImgWidth=true;}
		if(!this.shape.height){this.useImgHeight=true;}
		dojox.gfx.canvas.imageLoader.loadImage(this.shape.src);
		this.bbox = null;
		return this.setTransform(this.matrix);	// self
		// TODO: wait until image is loaded, or better yet,
		// display loading image and then invalidate scene and re-render upon loaded.
	},
	setStroke: function(){
		// summary: ignore setting a stroke style
		return this;	// self
	},
	setFill: function(){
		// summary: ignore setting a fill style
		return this;	// self
	},
	render: function(){
		this.img = dojox.gfx.canvas.imageLoader.getImage(this.shape.src);
		if (!this.img){return;}
		var ctx=this._getGC();
		ctx.save(); // save graphics context prior to rendering shape
		this._transform();
		// Adjust width and height to the size of the loaded image that we have now.
		if (this.useImgWidth){this.shape.width=this.img.width;}
		if (this.useImgHeight){this.shape.height=this.img.height;}
// TODO: FIXME: When width/height are specified rather than using the src image's w&h, drawImage doesnt seem to work
//		ctx.drawImage(this.img,this.shape.x,this.shape.y,this.shape.width,this.shape.height);
// TODO: FIXME: width and height of rotated images (using transform matrix) appears to clip at original image extent.
		ctx.drawImage(this.img,this.shape.x,this.shape.y);
		ctx.restore(); // return graphics context after rendering shape

	}
});
dojox.gfx.Image.nodeType = "image";

dojo.declare("dojox.gfx.Text", dojox.gfx.shape.Text, {
	// summary: a text shape
	setShape: function(newShape){
		// summary: sets a text shape object (SVG)
		// newShape: Object: a text shape object
		this.shape = dojo.clone(newShape);
		if (!this.shape.text){this.shape.text="";};
		this.bbox = null;
		if(!("fontStyle" in this)){
			this.fontStyle = dojo.clone(dojox.gfx.defaultFont);
		}else{
			this.fontStyle = dojox.gfx.makeParameters(dojox.gfx.defaultFont,this.fontStyle);
		}
		//TODO: deal with other properties like css
		return this.setTransform(this.matrix);	// self
	},
	setRawNode: function(rawNode){
		// summary:
		//	assigns and clears the underlying node that will represent this
		//	shape. 
		this.rawNode = rawNode;
		this.rawNode.innerHtml="";
		this.rawNode.appendChild(this.rawNode.ownerDocument.createTextNode(this.shape.text));
		this.rawNode.style.position="absolute";
		if (this.shape.align){
			switch (this.shape.align){
				case "middle":	this.rawNode.style.textAlign="center";
					break;
				case "end":	this.rawNode.style.textAlign="right";
					break;
				default:	this.rawNode.style.textAlign="left";
			}
		}
		var fontOffset = {x:0,y:0}
		if (this.fontStyle.size){
			// Calculate offset based on estimated font height
			var baseFontHeight = dojox.gfx._base._getCachedFontMeasurements()["12pt"];
			// Use a ratio to estimate...
			//  baseFontHeight        estFontHeight
			// ---------------- = ---------------------------------
			//  12					this.rawNode.style.fontHeight
			var estFontHeight = (baseFontHeight/12) * dojox.gfx.normalizedLength(this.fontStyle.size);
			fontOffset.y=-estFontHeight + 1;
			// Fudge font width using 1/goldenratio 
			//fontOffset.x=-(0.6180339882*estFontHeight/2) + 1
		}
		// TODO: Check to see if the node is nested within a table cell (parent tr).  
		// use the offset for the cell (with insets)
		var tableNode = this._nestedWithin(this.rawNode,"table"); 
		if (tableNode){
			var trNode = this._nestedWithin(this.rawNode,"tr");
			var tdNode = this._nestedWithin(this.rawNode,"td");
			this.rawNode.style.top=tableNode.offsetTop+trNode.offsetTop+this.shape.y+fontOffset.y+"px";
			this.rawNode.style.left=tdNode.offsetLeft+this.shape.x+fontOffset.x+"px";
		}else{
			this.rawNode.style.left=this.rawNode.parentNode.offsetLeft+this.shape.x+fontOffset.x+"px";
			this.rawNode.style.top=this.rawNode.parentNode.offsetTop+this.shape.y+fontOffset.y+"px";
		}
	},
	_nestedWithin: function(node,nodeName){
		while (node.parentNode){
			if(node.parentNode.nodeName.toLowerCase()==nodeName){return node.parentNode;}
			return this._nestedWithin(node.parentNode,nodeName);
		}
		return null;
	},
	getTextWidth: function(){ 
		// summary: get the text width in pixels
		// TODO: Use dojo text functions to compute width of text
		return 50; 
	},
	render: function(){
		// Noop - Text nodes are rendered by the browser using divs
	}
});
dojox.gfx.Text.nodeType = "text";

dojo.declare("dojox.gfx.TextPath", dojox.gfx.path.TextPath, {
	// summary: a textpath shape (SVG)
	_updateWithSegment: function(segment){
		// summary: updates the bounding box of path with new segment
		// segment: Object: a segment
		dojox.gfx.Path.superclass._updateWithSegment.apply(this, arguments);
		this._setTextPath();
	},
	setShape: function(newShape){
		// summary: forms a path using a shape (SVG)
		// newShape: Object: an SVG path string or a path object (see dojox.gfx.defaultPath)
		dojox.gfx.Path.superclass.setShape.apply(this, arguments);
		//this._setTextPath();
		return this.setTransform(this.matrix);	// self
	},
/*	_setTextPath: function(){
		if(typeof this.shape.path != "string"){ return; }
		var r = this.rawNode;
		if(!r.firstChild){
			var tp = document.createElementNS(dojox.gfx.canvas.xmlns.svg, "textPath");
			var tx = document.createTextNode("");
			tp.appendChild(tx);
			r.appendChild(tp);
		}
		var ref  = r.firstChild.getAttributeNS(dojox.gfx.canvas.xmlns.xlink, "href");
		var path = ref && dojox.gfx.canvas.getRef(ref);
		if(!path){
			var surface = this._getParentSurface();
			if(surface){
				var defs = surface.defNode;
				path = document.createElementNS(dojox.gfx.canvas.xmlns.svg, "path");
				var id = dojox.gfx._base._getUniqueId();
				path.setAttribute("id", id);
				defs.appendChild(path);
				r.firstChild.setAttributeNS(dojox.gfx.canvas.xmlns.xlink, "href", "#" + id);
			}
		}
		if(path){
			path.setAttribute("d", this.shape.path);
		}
	},
*/	
	_setText: function(){
		var r = this.rawNode;
		r = r.firstChild;
/*
		var t = this.text;
		r.setAttribute("alignment-baseline", "middle");
		switch(t.align){
			case "middle":
				r.setAttribute("text-anchor", "middle");
				r.setAttribute("startOffset", "50%");
				break;
			case "end":
				r.setAttribute("text-anchor", "end");
				r.setAttribute("startOffset", "100%");
				break;
			default:
				r.setAttribute("text-anchor", "start");
				r.setAttribute("startOffset", "0%");
				break;
		}
		//r.parentNode.setAttribute("alignment-baseline", "central");
		//r.setAttribute("dominant-baseline", "central");
		r.setAttribute("baseline-shift", "0.5ex");
		r.setAttribute("text-decoration", t.decoration);
		r.setAttribute("rotate", t.rotated ? 90 : 0);
		r.setAttribute("kerning", t.kerning ? "auto" : 0);
*/		
		r.text = t.text;
	},
	render: function(){
		// Noop - Text nodes are rendered by the browser using divs
	}
});
dojox.gfx.TextPath.nodeType = "text";

dojo.declare("dojox.gfx.Surface", dojox.gfx.shape.Surface, {
	// summary: a surface object to be used for drawings (html canvas)
	constructor: function(){
		dojox.gfx.canvas.Container._init.call(this);
	},
	setDimensions: function(width, height){
		// summary: sets the width and height of the rawNode
		// width: String: width of surface, e.g., "100px"
		// height: String: height of surface, e.g., "100px"
		if(!this.rawNode){ return this; }
		this.rawNode.width  = width;
		this.rawNode.height = height;
		return this;	// self
	},
	getDimensions: function(){
		// summary: returns an object with properties "width" and "height"
		return this.rawNode ? {width: this.rawNode.width, height: this.rawNode.height} : null; // Object
	},
	render: function(){
		// wait for pending images to load.
		console.debug("In surface render()");
		if(!dojox.gfx.canvas.imageLoader.complete){
			console.debug("Images not loaded yet.");
			setTimeout(dojo.hitch(this, function(){this.render();}),500); // try rendering again in a bit...
		}else{
			console.debug("Images loaded.");
			// clear the canvas;
			this.ctx.clearRect(0,0,this.rawNode.width,this.rawNode.height);
			// render shapes from back (first) to front (last)
			for (var i=0;i<this.children.length;i++){
				this.children[i].render();
			}
		}
	}
});

dojox.gfx.createSurface = function(parentNode, width, height){
	// summary: creates a surface (HTML Canvas)
	// parentNode: Node: a parent node
	// width: String: width of surface, e.g., "100px"
	// height: String: height of surface, e.g., "100px"

	var s = new dojox.gfx.Surface();
	s.rawNode = dojo.byId(parentNode).ownerDocument.createElement("canvas");
	s.ctx = s.rawNode.getContext("2d");

	s.rawNode.width  = width  ? width  : "100%";
	s.rawNode.height = height ? height : "100%";
	// FIXME: Does HTML Canvas make use of CSS?
	/*
	s.rawNode.style.width  = width  ? width  : "100";
	s.rawNode.style.height = height ? height : "100";
	s.rawNode.style.position = "relative";
	s.rawNode.coordsize = (width && height)
		? (parseFloat(width) + " " + parseFloat(height))
		: "100 100";
	s.rawNode.coordorigin = "0 0";
	*/
	
	dojo.byId(parentNode).appendChild(s.rawNode);
	// FIXME: FOR DEBUG, so we can tell that we have a canvas
	s.ctx.fillStyle = "rgb(255,255,255)";
 	s.ctx.fillRect (0, 0, width, height);
	// END FOR DEBUG
 	return s;	// dojox.gfx.Surface
};

/*
dojox.gfx._function draw() {
	  // TODO: the image objects should be created and initialized once for all canvas surfaces
	  // Surface needs to wait until the images have been loaded before constructing stroke patterns
	  // which must be created with a per-surface graphics context during render phase of each shape 
	  // Here's an example (broken)
	  var img = new Image();
	  img.src = 'images/wallpaper.png';
	  img.onload = function(){
	    // create pattern
	    var ptrn = ctx.createPattern(img,'repeat');
	    ctx.fillStyle = ptrn;
	    ctx.fillRect(0,0,150,150);
	  }
};
*/

// Extenders

dojox.gfx.canvas.Font = {
	_setFont: function(){
		// summary: sets a font object (SVG)
		var f = this.fontStyle;
		// next line doesn't work in Firefox 2 or Opera 9
		//this.rawNode.setAttribute("font", dojox.gfx.makeFontString(this.fontStyle));
		this.rawNode.style.fontStyle = f.style;
		this.rawNode.style.fontVariant = f.variant;
		this.rawNode.style.fontWeight = f.weight;
//FIXME: Looks like there's a bug in dojox.gfx.splitFontString for size and family when using canvas
// needs more work.
		this.rawNode.style.fontSize = f.size;
		this.rawNode.style.fontFamily = f.family;
	},
	setFill: function(fillColor){
		this.rawNode.style.color=dojox.gfx.normalizeColor(fillColor).toHex();
	}
};

dojox.gfx.canvas.Container = {
	_init: function(){
		dojox.gfx.shape.Container._init.call(this);
	},
	add: function(shape){
		// summary: adds a shape to a group/surface
		// shape: dojox.gfx.Shape: an VML shape object
		if(this != shape.getParent()){
			dojox.gfx.shape.Container.add.apply(this, arguments);
		}
		return this;	// self
	},
	remove: function(shape, silently){
		// summary: remove a shape from a group/surface
		// shape: dojox.gfx.Shape: an shape object
		// silently: Boolean?: if true, regenerate a picture
		if(this == shape.getParent()){
			dojox.gfx.shape.Container.remove.apply(this, arguments);
		}
		return this;	// self
	},
	clear: function(){
		// summary: removes all shapes from a group/surface
		var r = this.rawNode;
		while(r.lastChild){
			r.removeChild(r.lastChild);
		}
		//return this.inherited(arguments);	// self
		return dojox.gfx.shape.Container.clear.apply(this, arguments);
	},
	_moveChildToFront: dojox.gfx.shape.Container._moveChildToFront,
	_moveChildToBack:  dojox.gfx.shape.Container._moveChildToBack
};

dojox.gfx.canvas.Creator = {
	// summary: Shape creators
	createPath: function(path){
		// summary: creates an SVG path shape
		// path: Object: a path object (see dojox.gfx.defaultPath)
		return this.createObject(dojox.gfx.Path, path);	// dojox.gfx.Path
	},
	createRect: function(rect){
		// summary: creates a rectangle shape
		// rect: Object: a path object (see dojox.gfx.defaultRect)
		return this.createObject(dojox.gfx.Rect, rect);	// dojox.gfx.Rect
	},
	createCircle: function(circle){
		// summary: creates a circle shape
		// circle: Object: a circle object (see dojox.gfx.defaultCircle)
		return this.createObject(dojox.gfx.Circle, circle);	// dojox.gfx.Circle
	},
	createEllipse: function(ellipse){
		// summary: creates an ellipse shape
		// ellipse: Object: an ellipse object (see dojox.gfx.defaultEllipse)
		return this.createObject(dojox.gfx.Ellipse, ellipse);	// dojox.gfx.Ellipse
	},
	createLine: function(line){
		// summary: creates an SVG line shape
		// line: Object: a line object (see dojox.gfx.defaultLine)
		return this.createObject(dojox.gfx.Line, line);	// dojox.gfx.Line
	},
	createPolyline: function(points){
		// summary: creates an SVG polyline/polygon shape
		// points: Object: a points object (see dojox.gfx.defaultPolyline)
		//	or an Array of points
		return this.createObject(dojox.gfx.Polyline, points);	// dojox.gfx.Polyline
	},
	createImage: function(image){
		// summary: creates an SVG image shape
		// image: Object: an image object (see dojox.gfx.defaultImage)
		return this.createObject(dojox.gfx.Image, image);	// dojox.gfx.Image
	},
	createText: function(textObj){
		// summary: Creates an text shape. The text shape for html canvas uses div overlays (with CSS layout)
		// to simulate text shapes.  Moz3 introduces proprietary canvas text functions that may be able to replace
		// this on mozilla browsers in the future; however, there's no portable canvas text solution at this time.
		// One other possible solution (though very compute intensive) would be to use custom path's and render our own fonts
		// textObj: Object: a text object (see dojox.gfx.defaultText) which contains text property and other text attrs
		var shape = new dojox.gfx.Text();
		var node = this.rawNode.ownerDocument.createElement("div");
		this.rawNode.parentNode.appendChild(node); // Make the div a peer of canvas (until we have group divs)
		//shape.fontStyle = dojo.clone(dojox.gfx.defaultFont);
		shape.setShape(textObj);
		shape.setRawNode(node);
		this.add(shape);
		return shape;
	},
	createTextPath: function(text){
		// summary: creates an text path shape (simulated with div overlays)
		// text: Object: a textpath object (see dojox.gfx.defaultTextPath)
		return this.createObject(dojox.gfx.TextPath, {}).setText(text);	// dojox.gfx.TextPath
	},
	createGroup: function(){
		// summary: creates an SVG group shape
		var g = this.createObject(dojox.gfx.Group);	// dojox.gfx.Group
		g.ctx = this.ctx; // Pass the parent GC reference onto the child group
		g.setRawNode(this.rawNode); // Each group shares the surface's rawNode
		return g;
	},
	createObject: function(shapeType, rawShape){
		// summary: creates an instance of the passed shapeType class
		// shapeType: Function: a class constructor to create an instance of
		// rawShape: Object: properties to be passed in to the classes "setShape" method
		var shape = new shapeType();
		shape.setShape(rawShape);
		this.add(shape);
		return shape;	// dojox.gfx.Shape
	},
	createShape: dojox.gfx._createShape
};

dojo.declare("dojox.gfx.canvas.ImageLoader", null, {
	constructor: function(imageUrls,callback){
	   this.callback = callback;
	   this.numLoaded = 0;
	   this.processed = 0;
	   this.complete=false;
	   this.images = [];
	   this.urls = imageUrls?imageUrls:[];
	   if(!imageUrls){
		   this.complete=true;
		   return;
	   }
	   for (var i=0;i<urls.length;i++){
		   this.loadImage(urls[i]);
	   }
	},
	getImage: function(src){
		for(var i=0;i<this.urls.length;i++){
			if (this.urls[i]==src){return this.images[i];}
		}
		return null;
	},
	loadImage: function(src){
		var cached = this.getImage(src);
	   if (cached){
		   return cached;
	   }
	   var image = new Image();
	   this.images.push(image);
	   this.urls.push(src);
	   image.loader = this;
	   image.loaded = false;
	   image.onload = dojo.hitch(this,function(){
		   this.loaded=true;
		   this.numLoaded++;
		   this._imageLoaded();
	   	});
	   image.onerror = dojo.hitch(this,function(){
		   this.error=true;
		   this._imageLoaded();
	   });
	   image.onabort = dojo.hitch(this,function(){
		   this.aborted=true;
		   this._imageLoaded();
	   });
	   image.src = src;
	},
	_imageLoaded: function(){
		this.processed++;
		if(this.processed==this.images.length){
			this.onComplete();
		}
	},
	onComplete: function(){
		this.complete=true;
	}
});
dojox.gfx.canvas.imageLoader = new dojox.gfx.canvas.ImageLoader();

dojo.extend(dojox.gfx.Text, dojox.gfx.canvas.Font);
dojo.extend(dojox.gfx.TextPath, dojox.gfx.canvas.Font);

dojo.extend(dojox.gfx.Group, dojox.gfx.canvas.Container);
dojo.extend(dojox.gfx.Group, dojox.gfx.canvas.Creator);

dojo.extend(dojox.gfx.Surface, dojox.gfx.canvas.Container);
dojo.extend(dojox.gfx.Surface, dojox.gfx.canvas.Creator);
dojo.extend(dojox.gfx.Surface, dojox.gfx.canvas.ImageLoader);
