/* 
 * heatmap.js 1.0 -	JavaScript Heatmap Library
 *
 * Copyright (c) 2011, Patrick Wied (http://www.patrick-wied.at)
 * Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * and the Beerware (http://en.wikipedia.org/wiki/Beerware) license.
 */ 

(function(w){
	// the heatmapFactory creates heatmap instances
	var heatmapFactory = (function(){
	
	// store object constructor
	// a heatmap contains a store
	// the store has to know about the heatmap in order to trigger heatmap updates when datapoints get added
	function store(hmap){

		var _ = {
			// data is a two dimensional array 
			// a datapoint gets saved as data[point-x-value][point-y-value]
			// the value at [point-x-value][point-y-value] is the occurrence of the datapoint
			data: [],
			// tight coupling of the heatmap object
			heatmap: hmap
		};
		// the max occurrence - the heatmaps radial gradient alpha transition is based on it
		this.max = 0;
		
		this.get = function(key){
			return _[key];
		},
		this.set = function(key, value){
			_[key] = value;
		};
	};
	
	store.prototype = {
		// function for adding datapoints to the store
		// datapoints are usually defined by x and y but could also contain a third parameter which represents the occurrence
		addDataPoint: function(x, y){
			if(x < 0 || y < 0)
				return;
				
			var heatmap = this.get("heatmap"),
			data = this.get("data");
			
			if(!data[x]) data[x] = [];
			if(!data[x][y]) data[x][y] = 1;
			// if count parameter is set increment by count otherwise by 1
			data[x][y]+=(arguments.length<3)?1:arguments[2];
			
			// do we have a new maximum?
			if(this.max < data[x][y]){
				this.max = data[x][y];
				// max changed, we need to redraw all existing(lower) datapoints
				heatmap.get("actx").clearRect(0,0,heatmap.get("width"),heatmap.get("height"));
				for(var one in data)					
					for(var two in data[one])
						heatmap.drawAlpha(one, two, data[one][two]);

				// @TODO
				// implement feature
				// heatmap.drawLegend(); ? 
				return;
			}
			heatmap.drawAlpha(x, y, data[x][y]);
		},
		setDataSet: function(obj){
			
			this.max = obj.max;
			var heatmap = this.get("heatmap"),
			data = this.get("data"),
			d = obj.data,
			dlen = d.length;
			// clear the heatmap before the data set gets drawn
			heatmap.clear();
			
			while(dlen--){
				var point = d[dlen];
				heatmap.drawAlpha(point.x, point.y, point.count);
				if(!data[point.x]) data[point.x] = [];
				if(!data[point.x][point.y]) data[point.x][point.y] = 1;
				data[point.x][point.y]+=point.count;
			}
			// Add event after all done
			// 10-22-2011 by Jeffri Hong
			if ( typeof(obj.callback) == 'function' )
				obj.callback();
		},
		exportDataSet: function(){
			var data = this.get("data");
			var exportData = [];
			for(var one in data){
				// jump over undefined indexes
				if(one === undefined)
					continue;
				for(var two in data[one]){
					if(two === undefined)
						continue;
					// if both indexes are defined, push the values into the array
					exportData.push({x: parseInt(one, 10), y: parseInt(two, 10), count: data[one][two]});
				}
			}
					
			return exportData;
		},
		generateRandomDataSet: function(points){
			var heatmap = this.get("heatmap"),
			w = heatmap.get("width"),
			h = heatmap.get("height");
			var randomset = {},
			max = Math.floor(Math.random()*1000+1);
			randomset.max = max;
			var data = [];
			while(points--){
				data.push({x: Math.floor(Math.random()*w+1), y: Math.floor(Math.random()*h+1), count: Math.floor(Math.random()*max+1)});
			}
			randomset.data = data;
			this.setDataSet(randomset);
		}
	};
	
	
	// heatmap object constructor
	function heatmap(config){
		// private variables
		var _ = {
			radiusIn : 20,
			radiusOut : 40,
			element : {},
			canvas : {},
			acanvas: {},
			ctx : {},
			actx : {},
			visible : true,
			width : 0,
			height : 0,
			max : false,
			gradient : false,
			opacity: 180
		};
		// heatmap store containing the datapoints and information about the maximum
		// accessible via instance.store
		this.store = new store(this);
		
		this.get = function(key){
			return _[key];
		},
		this.set = function(key, value){
			_[key] = value;
		};
		// configure the heatmap when an instance gets created
		this.configure(config);
		// and initialize it
		this.init();
	};
	
	// public functions
	heatmap.prototype = {
		configure: function(config){
				if(config.radius){
					var rout = config.radius,
					rin = parseInt(rout/2);					
				}
				this.set("radiusIn", rin || 15),
				this.set("radiusOut", rout || 40),
				this.set("element", (config.element instanceof Object)?config.element:document.getElementById(config.element));
				this.set("visible", config.visible);
				this.set("max", config.max || false);
				this.set("gradient", config.gradient || { 0.45: "rgb(0,0,255)", 0.55: "rgb(0,255,255)", 0.65: "rgb(0,255,0)", 0.95: "yellow", 1.0: "rgb(255,0,0)"});	// default is the common blue to red gradient
				this.set("opacity", parseInt(255/(100/config.opacity), 10) || 180);
				this.set("width", config.width || 0);
				this.set("height", config.height || 0);
		},
		init: function(){
				this.initColorPalette();
				var canvas = document.createElement("canvas"),
				acanvas = document.createElement("canvas"),
				element = this.get("element");
				this.set("canvas", canvas);
				this.set("acanvas", acanvas);
				canvas.width = acanvas.width = element.style.width.replace(/px/,"") || this.getWidth(element);
				this.set("width", canvas.width);
				canvas.height = acanvas.height = element.style.height.replace(/px/,"") || this.getHeight(element);
				this.set("height", canvas.height);
				canvas.style.position = acanvas.style.position = "absolute";
				canvas.style.top = acanvas.style.top = "0";
				canvas.style.left = acanvas.style.left = "0";
				canvas.style.zIndex = 1000000;
				if(!this.get("visible"))
					canvas.style.display = "none";

				this.get("element").appendChild(canvas);
				this.set("ctx", canvas.getContext("2d"));
				this.set("actx", acanvas.getContext("2d"));
		},
		initColorPalette: function(){
				
			var canvas = document.createElement("canvas");
			canvas.width = "1";
			canvas.height = "256";
			var ctx = canvas.getContext("2d");
			var grad = ctx.createLinearGradient(0,0,1,256),
			gradient = this.get("gradient");
			for(var x in gradient){
				grad.addColorStop(x, gradient[x]);
			}
			
			ctx.fillStyle = grad;
			ctx.fillRect(0,0,1,256);
			
			this.set("gradient", ctx.getImageData(0,0,1,256).data);
			delete canvas;
			delete grad;
			delete ctx;
		},
		getWidth: function(element){
			var width = element.offsetWidth;
			if(element.style.paddingLeft)
				width+=element.style.paddingLeft;
			if(element.style.paddingRight)
				width+=element.style.paddingRight;
			
			return width;
		},
		getHeight: function(element){
			var height = element.offsetHeight;
			if(element.style.paddingTop)
				height+=element.style.paddingTop;
			if(element.style.paddingBottom)
				height+=element.style.paddingBottom;
			
			return height;
		},
		colorize: function(x, y){
				// get the private variables
				var width = this.get("width"),
				radiusOut = this.get("radiusOut"),
				height = this.get("height"),
				actx = this.get("actx"),
				ctx = this.get("ctx");
				
				var x2 = radiusOut*2;
				
				if(x+x2>width)
					x=width-x2;
				if(x<0)
					x=0;
				if(y<0)
					y=0;
				if(y+x2>height)
					y=height-x2;
				// get the image data for the mouse movement area
				var image = actx.getImageData(x,y,x2,x2),
				// some performance tweaks
					imageData = image.data,
					length = imageData.length,
					palette = this.get("gradient"),
					opacity = this.get("opacity");
				// loop thru the area
				for(var i=3; i < length; i+=4){
					
					// [0] -> r, [1] -> g, [2] -> b, [3] -> alpha
					var alpha = imageData[i],
					offset = alpha*4;
					
					if(!offset)
						continue;
	
					// we ve started with i=3
					// set the new r, g and b values
					imageData[i-3]=palette[offset];
					imageData[i-2]=palette[offset+1];
					imageData[i-1]=palette[offset+2];
					// we want the heatmap to have a gradient from transparent to the colors
					// as long as alpha is lower than the defined opacity (maximum), we'll use the alpha value
					imageData[i] = (alpha < opacity)?alpha:opacity;
				}
				// the rgb data manipulation didn't affect the ImageData object(defined on the top)
				// after the manipulation process we have to set the manipulated data to the ImageData object
				image.data = imageData;
				ctx.putImageData(image,x,y);	
		},
		drawAlpha: function(x, y, count){
				// storing the variables because they will be often used
				var r1 = this.get("radiusIn"),
				r2 = this.get("radiusOut"),
				ctx = this.get("actx"),
				max = this.get("max"),
				// create a radial gradient with the defined parameters. we want to draw an alphamap
				rgr = ctx.createRadialGradient(x,y,r1,x,y,r2),
				xb = x-r2, yb = y-r2, mul = 2*r2;
				// the center of the radial gradient has .1 alpha value
				rgr.addColorStop(0, 'rgba(0,0,0,'+((count)?(count/this.store.max):'0.1')+')');  
				// and it fades out to 0
				rgr.addColorStop(1, 'rgba(0,0,0,0)');
				// drawing the gradient
				ctx.fillStyle = rgr;  
				ctx.fillRect(xb,yb,mul,mul);
				// finally colorize the area
				this.colorize(xb,yb);

		},
		toggleDisplay: function(){
				var visible = this.get("visible"),
				canvas = this.get("canvas");
				
				if(!visible)
					canvas.style.display = "block";
				else
					canvas.style.display = "none";
					
				this.set("visible", !visible);
		},
		// dataURL export
		getImageData: function(){
				return this.get("canvas").toDataURL();
		},
		clear: function(){
			var w = this.get("width"),
			h = this.get("height");
			this.store.set("data",[]);
			// @TODO: reset stores max to 1 
			//this.store.max = 1;
			this.get("ctx").clearRect(0,0,w,h);
			this.get("actx").clearRect(0,0,w,h);
		}
	};
		
	return {
			create: function(config){
				return new heatmap(config);
			},
			util: {
				mousePosition: function(ev){
					// this doesn't work right
					// rather use 
					/*
						// this = element to observe
						var x = ev.pageX - this.offsetLeft;
						var y = ev.pageY - this.offsetTop;
						
					*/
					var x, y;
					
					if (ev.layerX) { // Firefox
						x = ev.layerX;
						y = ev.layerY;
					} else if (ev.offsetX) { // Opera
						x = ev.offsetX;
						y = ev.offsetY;
					}
					if(typeof(x)=='undefined')
						return;
					
					return [x,y];
				}
			}
		};
	})();
	w.h337 = w.heatmapFactory = heatmapFactory;
})(window);
