/*
	A circular page indicator using the canvas tag.
	
	Requires jQuery.
	
	Usage
	=====
	
	API
	===
		var progInd = new CircularPageIndicator([Object initObject]) // initialise the inidicator and draw it on the canvas. Config object optional.
		.getName() - returns _canvasID
		.getCurrentSector() - returns the index number of the current sector (0-based).
		.setCurrentSector(pSector, willSuppressEvent) - makes the passed sector index the active sector and redraws indicator. Callback is not fired. Can suppress firing of custom event with second argument.
		
		When its state changes (setCurrentSector updates _currentIndex) a custom jQuery event is triggered:
		$(_canvas).trigger('updated.circPI', [_currentIndex, _canvasID]);
		
		You can respond to this event like so:
		$('#canvas_element').bind('updated.circPI', function (e, currSector) { log(e, currSector); } )
		
		or:
		function sectorClicked(e, pSector, id) {
			log('callback function sectorClicked: ' + pSector, id);
		}
		$('#canvas_element').bind('updated.circPI', sectorClicked) } )

		
	Defaults:
	=========
		In HTML define a canvas with ID 'canvas'
		In JS $(document).ready( var progInd = new CircularPageIndicator() );
		This will create a page indicator with default settings that fills the height of the canvas.

		
	Configured, with callback:
	==========================
		In HTML define a canvas with ID 'circInd' (any valid custom ID - add it into piInitObj )
		Pass an object to CircularPageIndicator() with settings. The callback function will fire when a sector is clicked, 
		and should take 2 argument of the sector number (0-based index) and HTML ID of canvas element that was clicked.
		eg:
			var progInd,
				piInitObj = {	
								canvasID:		'circInd',
								totPages:		8,				// number of sectors
								startPage:		0,				// initially activated sector
								lineWidth:		9,				// thickness of each sector
								lineCapStyle:	'round',		// style of line cap - pixels of round cap added to path are compensated for
								gap:			6,				// transparent gap in degrees between sectors
								defaultColour:	'#CCC',
								hoverColour:	'#FFF',
								activeColour:	'#29ABE2',
								radius:			50;
								centreX:		100;			// canvas co-ordinates
								centreY:		100;			// canvas co-ordinates
								callback:		sectorClicked	// an optional function name to call when a sector is clicked
							};
			$(document).ready( progInd = new CircularPageIndicator(piInitObj) );
*/
function CircularPageIndicator(initObj) {
	
	
	// CONSTRUCTOR STARTS //////////////////////////////////////////////////////////
	
	
	// declare vars ////////////////////////////////////////////////////////////////
	
	
	var _numPages,					// total number of pages to be indicated (sectors to be drawn)
		_currentIndex,				// integer containing the sector number (array index) of the currently displayed page
		_prevIndex,					// (currently unused) index containing the previously active sector - for use in transitions
		_hoverIndex,				// integer containing the sector number (array index) that is being hovered over,
									// -1 if none are being hovered over
		_centreX,					// X origin of circle in canvas co-ordiantes
		_centreY,					// Y origin of circle in canvas co-ordiantes
									// If undefined, _centreX and _centreY is the centre of the canvas element - 
									// can define one of _centreX and _centreY to have the undefined co-ord centred in canvas height/width
		_lineWidth,					// each sector is actually a path (arc) with a user-definable thickness, measured in pixels.
		_lineCapStyle,				// a string of either 'butt' or 'round'. Defaults to 'butt'. 
									// If changed to round the distance that the round cap protrudes beyond the path is automatically added to _gapSize.
		_radius,					// user-definable radius of the circle, measured in pixels. 
									// If undefined, _radius is half canvas *height* minus half the linewidth minus 1
									// If user-defined take into account the width of the line.
		_circumference,				// used while compensating for _gapSize if lineCap is 'round'
		_gapSize,					// user-definable transparent area between sectors, measured in degrees
		_canvasID,					// ID of canvas element, passed to init() so it can define _canvas
		_canvas,					// canvas element
		_ctx,						// context of the canvas
		_degsPerSector,				// calculated from _numPages for getSectorUnderMouse function
		_defaultColour,				// colour of sectors in normal state
		_hoverColour,				// colour of a sector when hovered over
		_activeColour,				// colour of the current page
		_onClickCallback,			// this callback should take 1 argument of the sector number (0-based index) that was clicked
		PI_TO_8DP = 3.14159265;		// hard-coded to avoid Math calls as it's used in an intensive part of the script

	
	// END OF VARIABLE DECLARATIONS /////////////////////////////////////////////////////
	
	
	
	
	// ensure that new has been used ////////////////////////////////////////////////////////
	if ( !(this instanceof CircularPageIndicator) ) {
		if (typeof window.log === 'function') {
			log('CircularPageIndicator not called with new!');
		}
		return new CircularPageIndicator(initObj);
	}
	
	
	
	
	
	// define vars /////////////////////////////////////////////////////////////////////////
	
	
	initObj =			initObj					||		{};
	_canvasID =			initObj.canvasID		||		'canvas';

	_canvas = document.getElementById(_canvasID);
	_ctx = _canvas.getContext("2d");
	
	_numPages =			initObj.totPages		||		4;
	_currentIndex =		initObj.startPage		||		0;
	_prevIndex =		initObj.startPage		||		0;
	_centreX =			initObj.centreX			||		Math.round(_canvas.width * 0.5);
	_centreY =			initObj.centreY			||		Math.round(_canvas.height * 0.5);
	_lineWidth =		initObj.lineWidth		||		5;
	_lineCapStyle =		initObj.lineCapStyle	||		'butt';
	_radius =			initObj.radius			||		Math.floor(_canvas.height * 0.5 - (_lineWidth * 0.5) - 1);
	_gapSize =			initObj.gap				||		5;
	_defaultColour =	initObj.defaultColour	||		'#CCC';
	_hoverColour =		initObj.hoverColour		||		'#FFF';
	_activeColour =		initObj.activeColour	||		'#29ABE2';
	_onClickCallback =	initObj.callback		||		undefined;

	_circumference = _radius * 2 * PI_TO_8DP;
	_degsPerSector = (360 / _numPages);
	_hoverIndex = -1;
	
	// A round end cap protrudes beyond the end of the drawn path. It extends beyond the path by half the stroke width:
	// http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-linecap
	// To keep the visible gap for round caps the same size as the visible gap for a default end cap, adjust _gapSize.
	if (_lineCapStyle === 'round') {
		// The formula is (arc length divided by the circumference) multiplied by 360 degrees
		// It is derived from:    x degrees / 360 degrees == arc length / circumference
		// Here arc length == half the _lineWidth.
		// See Fig. 2 on  http://www.cliffsnotes.com/study_guide/Arc-Length-and-Sectors.topicArticleId-18851,articleId-18829.html
		// Also multiply by 2 as drawArc() adjusts the start and end of the path it draws by halving the gapSize (which centres the arc in its sector).
		_gapSize += ((_lineWidth/2) / _circumference) * 360 * 2;
	}
	
	// END OF VARIABLE DEFINITIONS /////////////////////////////////////////////////////
	
	
	
	
	// define functions ////////////////////////////////////////////////////////////////
	
	
	function radToDeg(pRad) {
		return pRad * 180 / PI_TO_8DP;
	}
		
	function degToRad(pDeg) {
		return pDeg * PI_TO_8DP / 180;
	}
	
	
	
	
	/**
	 * Draws one sector of a given a colour
	 */
	function drawArc(index, colour) {
		var startAngle,
			endAngle;
			
		// -90 == 12 o'clock
		// halve the gapSize to centre the arc in its sector
		startAngle = ((360 / _numPages) * index) - 90 + (_gapSize * 0.5);
		endAngle = (360 / _numPages) * (index + 1) - 90 - (_gapSize * 0.5);

		_ctx.beginPath();
		_ctx.strokeStyle = colour;
		_ctx.lineWidth = _lineWidth;
		_ctx.lineCap = _lineCapStyle;
		_ctx.arc(_centreX, _centreY, _radius, degToRad(startAngle), degToRad(endAngle), false);
		_ctx.stroke();
	}
	
	
	
	
	/**
	 * Draws indicator.
	 */
	function drawIndicator() {
		var i;
		
		//log(my);
		// clear canvas
		// needs testing in all browsers, see http://stackoverflow.com/questions/2142535/how-to-clear-the-canvas-for-redrawing
		_canvas.width = _canvas.width; 

		// draw each segment using the correct colour
		for (i = 0; i < _numPages; i++) {
			switch (i) {
				case _currentIndex:
					drawArc(i, _activeColour);
					break;
				case _hoverIndex:
					drawArc(i, _hoverColour);
					break;
				default:
					drawArc(i, _defaultColour);
			}
		}
	}
	
	
	/**
	 * Getter for _currentIndex
	 */
	function getName() {
		return _canvasID;
	}
	
	/**
	 * Getter for _currentIndex
	 */
	function getCurrentSector() {
		return _currentIndex;
	}
	
	/**
	 * Setter for _currentIndex.
	 * If pSector is less than 0, pSector is converted to 0;
	 * If pSector is more than than _numPages, pSector is converted to _numPages;
	 */
	function setCurrentSector(pSector, willSuppressEvent) {
		// validate input
		if (!isNaN(pSector)) {
			if (pSector < 0) {
				pSector = 0;
			} else if (pSector > _numPages) {
				pSector = _numPages;
			}
			Math.floor(pSector); // make sure pSector is an integer
			_prevIndex = _currentIndex;
			_currentIndex = pSector;
			drawIndicator();
			
			if (willSuppressEvent === true) {
				//log('setCurrentSector not firing event');
			} else {
				// fire custom event
				//log('setCurrentSector is firing event');
				$(_canvas).trigger('updated.circPI', [_currentIndex, _canvasID]);
			}
		}
	}
	
	
	
	
	
	
	
	/**
	 * Assumptions: the co-ordinate system passed places 0 degrees at 3 o'clock.
	 * Calculates the angle of a line drawn from a co-ordinate inside the circle to the circle's origin.
	 * Conversion is done to ensure that 0 degrees is at 12 o'clock - HTML canvas 0 degrees is at 3 o'clock.
	 * Divides a circle into equal sectors starting at 12 o'clock and progressing clockwise. 
	 * (The sectors are numbered from 0 and the numbering progresses clockwise.)
	 * Returns the sector that the co-ordinate is within as an integer. 
	 * Dependencies: requires function radToDeg(pRad) which requires var PI_TO_8DP
	 */
	function getSectorUnderMouse(pX, pY, originX, originY, degreesPerSector) {
		var clickAngleRads,
			clickAngleDegs;
			
		// arctangent is used to calculate the angles of a right triangle http://www.arctangent.net/atan.html
		clickAngleRads = Math.atan2(pY - originY, pX - originX); // convert click to angle of click from centre of circle
		clickAngleDegs = radToDeg(clickAngleRads);
		clickAngleDegs += 90; // compensate for canvas circle orientation so 0 degrees is at 12 o'clock
		// compensate for minus figures so all figures are in range  0 to (nearly) 360.
		if (clickAngleDegs < 0) {
			clickAngleDegs += 360;
		}
		return Math.floor( clickAngleDegs / degreesPerSector );
	}
	
	
	

	function onBlur() {
		$(_canvas).css('cursor', '');
		_hoverIndex = -1;
		drawIndicator();
	}
	
	
	
	function onMouseMove(e) {
		var data,
			alpha,
			mouseSector,
			xPos,
			yPos;
			
		//log('onMouseMove: ' + e.pageX, e.pageY, e.currentTarget, e);
		//log(e);
		
		// calculate the offset of the mouse over the canvas 
		xPos = e.layerX; //e.pageX - e.currentTarget.offsetLeft;
		yPos = e.layerY; //e.pageY - e.currentTarget.offsetTop;
		
		//log(xPos, yPos);
		
		// get the colour data of the pixel under the mouse, compensating for the canvas's offset from top left of the page
		// the alpha value is the 4th item in the data array
		try{
			// data = _ctx.getImageData(e.pageX - _ctx.canvas.offsetLeft, e.pageY - _ctx.canvas.offsetTop, 1, 1).data;
			data = _ctx.getImageData(xPos, yPos, 1, 1).data;
			alpha = data[3];
			if (alpha) {
				// getting the mouseSector is a potientially costly function 
				// so only call it if we're sure the mouse is over an opaque area of the canvas
				// that's why we avoid getting a value for mouseSector before the enclosing if block
				mouseSector = getSectorUnderMouse(xPos, yPos, _centreX, _centreY, _degsPerSector);
				
				// don't do anything if user has moused over the currently active sector
				if (mouseSector !== _currentIndex) {
					$(_canvas).css('cursor', 'pointer');
					_hoverIndex = mouseSector;
					drawIndicator();
				} else {
					onBlur();
				}
			} else {
				onBlur();
			}
		} catch (e) {
			//log(e);
		}
	}
	
	
	
	
	function onClick(e) {
		var data,
			alpha,
			mouseSector;
		
		// get the colour data of the pixel under the mouse, compensating for the canvas's offset from top left of the page
		// the alpha value is the 4th item in the data array
		data = _ctx.getImageData(e.layerX, e.layerY, 1, 1).data;
		alpha = data[3];
		if (alpha) {
			// getting the mouseSector is a potientially costly function 
			// so only call it if we're sure the mouse is over an opaque area of the canvas
			// that's why we avoid getting a value for mouseSector before the enclosing if block
			mouseSector = getSectorUnderMouse(e.layerX, e.layerY, _centreX, _centreY, _degsPerSector);
			
			// don't do anything if user has moused over the currently active sector
			if (mouseSector !== _currentIndex) {
				//$(_canvas).css('cursor', 'pointer');
				setCurrentSector(mouseSector);
				if (typeof _onClickCallback === 'function') {
					_onClickCallback(_currentIndex, _canvasID);
				} else {
					if (typeof window.log === 'function') {
						//log('No callback. _onClickCallback: ' + _onClickCallback);
					}
				}
			}
		}
		
		//return false;
		
	}
	
	
	// END OF FUNCTION DEFINITIONS /////////////////////////////////////////////////////
	
	
	// prepare document / other available objects/vars //////////////////////////////////////
	
	// set up event handlers on target canvas
	// needs to be (window) otherwise changes to the page can disable the event
	$(window).mousemove(onMouseMove);
	$(window).mouseout(onBlur);
	$(window).click(onClick);
		
	// draw initial state on canvas
	drawIndicator();
	

	// assign properties and methods to newly created object ///////////////////////////
	this.getName = getName;
	this.getCurrentSector = getCurrentSector;
	this.setCurrentSector = setCurrentSector;
	
	// for clarity, explicitly return this /////////////////////////////////////////////
	return this;
	
	// CONSTRUCTOR ENDS ////////////////////////////////////////////////////////////////
}

