

/*

 * When considering jsLint, we need to allow eval() as it it is used for reading cookies and 

 * building the dynamic multi-column sort functions.

 */

/*jslint evil: true, undef: true, browser: true */

/*globals $, jQuery,_fnExternApiFunc,_fnInitalise,_fnLanguageProcess,_fnAddColumn,_fnColumnOptions,_fnAddData,_fnGatherData,_fnDrawHead,_fnDraw,_fnReDraw,_fnAjaxUpdate,_fnAjaxUpdateDraw,_fnAddOptionsHtml,_fnFeatureHtmlTable,_fnScrollDraw,_fnAjustColumnSizing,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnBuildSearchArray,_fnBuildSearchRow,_fnFilterCreateSearch,_fnDataToSearch,_fnSort,_fnSortAttachListener,_fnSortingClasses,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnFeatureHtmlLength,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnNodeToDataIndex,_fnVisbleColumns,_fnCalculateEnd,_fnConvertToWidth,_fnCalculateColumnWidths,_fnScrollingWidthAdjust,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnArrayCmp,_fnDetectType,_fnSettingsFromNode,_fnGetDataMaster,_fnGetTrNodes,_fnGetTdNodes,_fnEscapeRegex,_fnDeleteIndex,_fnReOrderIndex,_fnColumnOrdering,_fnLog,_fnClearTable,_fnSaveState,_fnLoadState,_fnCreateCookie,_fnReadCookie,_fnGetUniqueThs,_fnScrollBarWidth,_fnApplyToChildren,_fnMap*/



(function($, window, document) {

	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

	 * Section - DataTables variables

	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

	

	/*

	 * Variable: dataTableSettings

	 * Purpose:  Store the settings for each dataTables instance

	 * Scope:    jQuery.fn

	 */

	$.fn.dataTableSettings = [];

	var _aoSettings = $.fn.dataTableSettings; /* Short reference for fast internal lookup */

	

	/*

	 * Variable: dataTableExt

	 * Purpose:  Container for customisable parts of DataTables

	 * Scope:    jQuery.fn

	 */

	$.fn.dataTableExt = {};

	var _oExt = $.fn.dataTableExt;

	

	

	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

	 * Section - DataTables extensible objects

	 * 

	 * The _oExt object is used to provide an area where user dfined plugins can be 

	 * added to DataTables. The following properties of the object are used:

	 *   oApi - Plug-in API functions

	 *   aTypes - Auto-detection of types

	 *   oSort - Sorting functions used by DataTables (based on the type)

	 *   oPagination - Pagination functions for different input styles

	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

	

	/*

	 * Variable: sVersion

	 * Purpose:  Version string for plug-ins to check compatibility

	 * Scope:    jQuery.fn.dataTableExt

	 * Notes:    Allowed format is a.b.c.d.e where:

	 *   a:int, b:int, c:int, d:string(dev|beta), e:int. d and e are optional

	 */

	_oExt.sVersion = "1.7.4";

	

	/*

	 * Variable: sErrMode

	 * Purpose:  How should DataTables report an error. Can take the value 'alert' or 'throw'

	 * Scope:    jQuery.fn.dataTableExt

	 */

	_oExt.sErrMode = "alert";

	

	/*

	 * Variable: iApiIndex

	 * Purpose:  Index for what 'this' index API functions should use

	 * Scope:    jQuery.fn.dataTableExt

	 */

	_oExt.iApiIndex = 0;

	

	/*

	 * Variable: oApi

	 * Purpose:  Container for plugin API functions

	 * Scope:    jQuery.fn.dataTableExt

	 */

	_oExt.oApi = { };

	

	/*

	 * Variable: aFiltering

	 * Purpose:  Container for plugin filtering functions

	 * Scope:    jQuery.fn.dataTableExt

	 */

	_oExt.afnFiltering = [ ];

	

	/*

	 * Variable: aoFeatures

	 * Purpose:  Container for plugin function functions

	 * Scope:    jQuery.fn.dataTableExt

	 * Notes:    Array of objects with the following parameters:

	 *   fnInit: Function for initialisation of Feature. Takes oSettings and returns node

	 *   cFeature: Character that will be matched in sDom - case sensitive

	 *   sFeature: Feature name - just for completeness :-)

	 */

	_oExt.aoFeatures = [ ];

	

	/*

	 * Variable: ofnSearch

	 * Purpose:  Container for custom filtering functions

	 * Scope:    jQuery.fn.dataTableExt

	 * Notes:    This is an object (the name should match the type) for custom filtering function,

	 *   which can be used for live DOM checking or formatted text filtering

	 */

	_oExt.ofnSearch = { };

	

	/*

	 * Variable: afnSortData

	 * Purpose:  Container for custom sorting data source functions

	 * Scope:    jQuery.fn.dataTableExt

	 * Notes:    Array (associative) of functions which is run prior to a column of this 

	 *   'SortDataType' being sorted upon.

	 *   Function input parameters:

	 *     object:oSettings-  DataTables settings object

	 *     int:iColumn - Target column number

	 *   Return value: Array of data which exactly matched the full data set size for the column to

	 *     be sorted upon

	 */

	_oExt.afnSortData = [ ];

	

	/*

	 * Variable: oStdClasses

	 * Purpose:  Storage for the various classes that DataTables uses

	 * Scope:    jQuery.fn.dataTableExt

	 */

	_oExt.oStdClasses = {

		/* Two buttons buttons */

		"sPagePrevEnabled": "paginate_enabled_previous",

		"sPagePrevDisabled": "paginate_disabled_previous",

		"sPageNextEnabled": "paginate_enabled_next",

		"sPageNextDisabled": "paginate_disabled_next",

		"sPageJUINext": "",

		"sPageJUIPrev": "",

		

		/* Full numbers paging buttons */

		"sPageButton": "paginate_button",

		"sPageButtonActive": "paginate_active",

		"sPageButtonStaticDisabled": "paginate_button",

		"sPageFirst": "first",

		"sPagePrevious": "previous",

		"sPageNext": "next",

		"sPageLast": "last",

		

		/* Stripping classes */

		"sStripOdd": "odd",

		"sStripEven": "even",

		

		/* Empty row */

		"sRowEmpty": "dataTables_empty",

		

		/* Features */

		"sWrapper": "dataTables_wrapper",

		"sFilter": "dataTables_filter",

		"sInfo": "dataTables_info",

		"sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */

		"sLength": "dataTables_length",

		"sProcessing": "dataTables_processing",

		

		/* Sorting */

		"sSortAsc": "sorting_asc",

		"sSortDesc": "sorting_desc",

		"sSortable": "sorting", /* Sortable in both directions */

		"sSortableAsc": "sorting_asc_disabled",

		"sSortableDesc": "sorting_desc_disabled",

		"sSortableNone": "sorting_disabled",

		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */

		"sSortJUIAsc": "",

		"sSortJUIDesc": "",

		"sSortJUI": "",

		"sSortJUIAscAllowed": "",

		"sSortJUIDescAllowed": "",

		"sSortJUIWrapper": "",

		

		/* Scrolling */

		"sScrollWrapper": "dataTables_scroll",

		"sScrollHead": "dataTables_scrollHead",

		"sScrollHeadInner": "dataTables_scrollHeadInner",

		"sScrollBody": "dataTables_scrollBody",

		"sScrollFoot": "dataTables_scrollFoot",

		"sScrollFootInner": "dataTables_scrollFootInner",

		

		/* Misc */

		"sFooterTH": ""

	};

	

	/*

	 * Variable: oJUIClasses

	 * Purpose:  Storage for the various classes that DataTables uses - jQuery UI suitable

	 * Scope:    jQuery.fn.dataTableExt

	 */

	_oExt.oJUIClasses = {

		/* Two buttons buttons */

		"sPagePrevEnabled": "fg-button ui-button ui-state-default ui-corner-left",

		"sPagePrevDisabled": "fg-button ui-button ui-state-default ui-corner-left ui-state-disabled",

		"sPageNextEnabled": "fg-button ui-button ui-state-default ui-corner-right",

		"sPageNextDisabled": "fg-button ui-button ui-state-default ui-corner-right ui-state-disabled",

		"sPageJUINext": "ui-icon ui-icon-circle-arrow-e",

		"sPageJUIPrev": "ui-icon ui-icon-circle-arrow-w",

		

		/* Full numbers paging buttons */

		"sPageButton": "fg-button ui-button ui-state-default",

		"sPageButtonActive": "fg-button ui-button ui-state-default ui-state-disabled",

		"sPageButtonStaticDisabled": "fg-button ui-button ui-state-default ui-state-disabled",

		"sPageFirst": "first ui-corner-tl ui-corner-bl",

		"sPagePrevious": "previous",

		"sPageNext": "next",

		"sPageLast": "last ui-corner-tr ui-corner-br",

		

		/* Stripping classes */

		"sStripOdd": "odd",

		"sStripEven": "even",

		

		/* Empty row */

		"sRowEmpty": "dataTables_empty",

		

		/* Features */

		"sWrapper": "dataTables_wrapper",

		"sFilter": "dataTables_filter",

		"sInfo": "dataTables_info",

		"sPaging": "dataTables_paginate fg-buttonset ui-buttonset fg-buttonset-multi "+

			"ui-buttonset-multi paging_", /* Note that the type is postfixed */

		"sLength": "dataTables_length",

		"sProcessing": "dataTables_processing",

		

		/* Sorting */

		"sSortAsc": "ui-state-default",

		"sSortDesc": "ui-state-default",

		"sSortable": "ui-state-default",

		"sSortableAsc": "ui-state-default",

		"sSortableDesc": "ui-state-default",

		"sSortableNone": "ui-state-default",

		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */

		"sSortJUIAsc": "css_right ui-icon ui-icon-triangle-1-n",

		"sSortJUIDesc": "css_right ui-icon ui-icon-triangle-1-s",

		"sSortJUI": "css_right ui-icon ui-icon-carat-2-n-s",

		"sSortJUIAscAllowed": "css_right ui-icon ui-icon-carat-1-n",

		"sSortJUIDescAllowed": "css_right ui-icon ui-icon-carat-1-s",

		"sSortJUIWrapper": "DataTables_sort_wrapper",

		

		/* Scrolling */

		"sScrollWrapper": "dataTables_scroll",

		"sScrollHead": "dataTables_scrollHead ui-state-default",

		"sScrollHeadInner": "dataTables_scrollHeadInner",

		"sScrollBody": "dataTables_scrollBody",

		"sScrollFoot": "dataTables_scrollFoot ui-state-default",

		"sScrollFootInner": "dataTables_scrollFootInner",

		

		/* Misc */

		"sFooterTH": "ui-state-default"

	};

	

	/*

	 * Variable: oPagination

	 * Purpose:  Container for the various type of pagination that dataTables supports

	 * Scope:    jQuery.fn.dataTableExt

	 */

	_oExt.oPagination = {

		/*

		 * Variable: two_button

		 * Purpose:  Standard two button (forward/back) pagination

	 	 * Scope:    jQuery.fn.dataTableExt.oPagination

		 */

		"two_button": {

			/*

			 * Function: oPagination.two_button.fnInit

			 * Purpose:  Initalise dom elements required for pagination with forward/back buttons only

			 * Returns:  -

	 		 * Inputs:   object:oSettings - dataTables settings object

	     *           node:nPaging - the DIV which contains this pagination control

			 *           function:fnCallbackDraw - draw function which must be called on update

			 */

			"fnInit": function ( oSettings, nPaging, fnCallbackDraw )

			{

				var nPrevious, nNext, nPreviousInner, nNextInner;

				

				/* Store the next and previous elements in the oSettings object as they can be very

				 * usful for automation - particularly testing

				 */

				if ( !oSettings.bJUI )

				{

					nPrevious = document.createElement( 'div' );

					nNext = document.createElement( 'div' );

				}

				else

				{

					nPrevious = document.createElement( 'a' );

					nNext = document.createElement( 'a' );

					

					nNextInner = document.createElement('span');

					nNextInner.className = oSettings.oClasses.sPageJUINext;

					nNext.appendChild( nNextInner );

					

					nPreviousInner = document.createElement('span');

					nPreviousInner.className = oSettings.oClasses.sPageJUIPrev;

					nPrevious.appendChild( nPreviousInner );

				}

				

				nPrevious.className = oSettings.oClasses.sPagePrevDisabled;

				nNext.className = oSettings.oClasses.sPageNextDisabled;

				

				nPrevious.title = oSettings.oLanguage.oPaginate.sPrevious;

				nNext.title = oSettings.oLanguage.oPaginate.sNext;

				

				nPaging.appendChild( nPrevious );

				nPaging.appendChild( nNext );

				

				$(nPrevious).click( function() {

					if ( oSettings.oApi._fnPageChange( oSettings, "previous" ) )

					{

						/* Only draw when the page has actually changed */

						fnCallbackDraw( oSettings );

					}

				} );

				

				$(nNext).click( function() {

					if ( oSettings.oApi._fnPageChange( oSettings, "next" ) )

					{

						fnCallbackDraw( oSettings );

					}

				} );

				

				/* Take the brutal approach to cancelling text selection */

				$(nPrevious).bind( 'selectstart', function () { return false; } );

				$(nNext).bind( 'selectstart', function () { return false; } );

				

				/* ID the first elements only */

				if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.p == "undefined" )

				{

					nPaging.setAttribute( 'id', oSettings.sTableId+'_paginate' );

					nPrevious.setAttribute( 'id', oSettings.sTableId+'_previous' );

					nNext.setAttribute( 'id', oSettings.sTableId+'_next' );

				}

			},

			

			/*

			 * Function: oPagination.two_button.fnUpdate

			 * Purpose:  Update the two button pagination at the end of the draw

			 * Returns:  -

	 		 * Inputs:   object:oSettings - dataTables settings object

			 *           function:fnCallbackDraw - draw function to call on page change

			 */

			"fnUpdate": function ( oSettings, fnCallbackDraw )

			{

				if ( !oSettings.aanFeatures.p )

				{

					return;

				}

				

				/* Loop over each instance of the pager */

				var an = oSettings.aanFeatures.p;

				for ( var i=0, iLen=an.length ; i<iLen ; i++ )

				{

					if ( an[i].childNodes.length !== 0 )

					{

						an[i].childNodes[0].className = 

							( oSettings._iDisplayStart === 0 ) ? 

							oSettings.oClasses.sPagePrevDisabled : oSettings.oClasses.sPagePrevEnabled;

						

						an[i].childNodes[1].className = 

							( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() ) ? 

							oSettings.oClasses.sPageNextDisabled : oSettings.oClasses.sPageNextEnabled;

					}

				}

			}

		},

		

		

		/*

		 * Variable: iFullNumbersShowPages

		 * Purpose:  Change the number of pages which can be seen

	 	 * Scope:    jQuery.fn.dataTableExt.oPagination

		 */

		"iFullNumbersShowPages": 5,

		

		/*

		 * Variable: full_numbers

		 * Purpose:  Full numbers pagination

	 	 * Scope:    jQuery.fn.dataTableExt.oPagination

		 */

		"full_numbers": {

			/*

			 * Function: oPagination.full_numbers.fnInit

			 * Purpose:  Initalise dom elements required for pagination with a list of the pages

			 * Returns:  -

	 		 * Inputs:   object:oSettings - dataTables settings object

	     *           node:nPaging - the DIV which contains this pagination control

			 *           function:fnCallbackDraw - draw function which must be called on update

			 */

			"fnInit": function ( oSettings, nPaging, fnCallbackDraw )

			{

				var nFirst = document.createElement( 'span' );

				var nPrevious = document.createElement( 'span' );

				var nList = document.createElement( 'span' );

				var nNext = document.createElement( 'span' );

				var nLast = document.createElement( 'span' );

				

				nFirst.innerHTML = oSettings.oLanguage.oPaginate.sFirst;

				nPrevious.innerHTML = oSettings.oLanguage.oPaginate.sPrevious;

				nNext.innerHTML = oSettings.oLanguage.oPaginate.sNext;

				nLast.innerHTML = oSettings.oLanguage.oPaginate.sLast;

				

				var oClasses = oSettings.oClasses;

				nFirst.className = oClasses.sPageButton+" "+oClasses.sPageFirst;

				nPrevious.className = oClasses.sPageButton+" "+oClasses.sPagePrevious;

				nNext.className= oClasses.sPageButton+" "+oClasses.sPageNext;

				nLast.className = oClasses.sPageButton+" "+oClasses.sPageLast;

				

				nPaging.appendChild( nFirst );

				nPaging.appendChild( nPrevious );

				nPaging.appendChild( nList );

				nPaging.appendChild( nNext );

				nPaging.appendChild( nLast );

				

				$(nFirst).click( function () {

					if ( oSettings.oApi._fnPageChange( oSettings, "first" ) )

					{

						fnCallbackDraw( oSettings );

					}

				} );
				
				$(nList).click( function () {
					alert(nList);
					alert('ggg');
					/*if ( oSettings.oApi._fnPageChange( oSettings, "first" ) )

					{

						fnCallbackDraw( oSettings );

					}*/

				} );

				

				$(nPrevious).click( function() {

					if ( oSettings.oApi._fnPageChange( oSettings, "previous" ) )

					{

						fnCallbackDraw( oSettings );

					}

				} );

				

				$(nNext).click( function() {

					if ( oSettings.oApi._fnPageChange( oSettings, "next" ) )

					{

						fnCallbackDraw( oSettings );

					}

				} );

				

				$(nLast).click( function() {
					
					if ( oSettings.oApi._fnPageChange( oSettings, "last" ) )

					{
									
						fnCallbackDraw( oSettings );

					}

				} );

				

				/* Take the brutal approach to cancelling text selection */

				$('span', nPaging)

					.bind( 'mousedown', function () { return false; } )

					.bind( 'selectstart', function () { return false; } );

				

				/* ID the first elements only */

				if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.p == "undefined" )

				{

					nPaging.setAttribute( 'id', oSettings.sTableId+'_paginate' );

					nFirst.setAttribute( 'id', oSettings.sTableId+'_first' );

					nPrevious.setAttribute( 'id', oSettings.sTableId+'_previous' );

					nNext.setAttribute( 'id', oSettings.sTableId+'_next' );

					nLast.setAttribute( 'id', oSettings.sTableId+'_last' );

				}

			},

			

			/*

			 * Function: oPagination.full_numbers.fnUpdate

			 * Purpose:  Update the list of page buttons shows

			 * Returns:  -

	 		 * Inputs:   object:oSettings - dataTables settings object

			 *           function:fnCallbackDraw - draw function to call on page change

			 */

			"fnUpdate": function ( oSettings, fnCallbackDraw )

			{

				if ( !oSettings.aanFeatures.p )

				{

					return;

				}

				

				var iPageCount = _oExt.oPagination.iFullNumbersShowPages;

				var iPageCountHalf = Math.floor(iPageCount / 2);

				var iPages = Math.ceil((oSettings.fnRecordsDisplay()) / oSettings._iDisplayLength);

				var iCurrentPage = Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength) + 1;

				var sList = "";

				var iStartButton, iEndButton, i, iLen;

				var oClasses = oSettings.oClasses;

				

				/* Pages calculation */

				if (iPages < iPageCount)

				{

					iStartButton = 1;

					iEndButton = iPages;

				}

				else

				{

					if (iCurrentPage <= iPageCountHalf)

					{

						iStartButton = 1;

						iEndButton = iPageCount;

					}

					else

					{

						if (iCurrentPage >= (iPages - iPageCountHalf))

						{

							iStartButton = iPages - iPageCount + 1;

							iEndButton = iPages;

						}

						else

						{

							iStartButton = iCurrentPage - Math.ceil(iPageCount / 2) + 1;

							iEndButton = iStartButton + iPageCount - 1;

						}

					}

				}

				

				/* Build the dynamic list */

				for ( i=iStartButton ; i<=iEndButton ; i++ )

				{

					if ( iCurrentPage != i )

					{

						sList += '<span class="'+oClasses.sPageButton+'">'+i+'</span>';

					}

					else

					{

						sList += '<span class="'+oClasses.sPageButtonActive+'">'+i+'</span>';
						//------ yingwey code
						window.scroll(0,0);
						parent.scrollTo(0,0);
						//-------------------------//

					}
					

				}				

				/* Loop over each instance of the pager */

				var an = oSettings.aanFeatures.p;

				var anButtons, anStatic, nPaginateList;

				var fnClick = function() {

					/* Use the information in the element to jump to the required page */

					var iTarget = (this.innerHTML * 1) - 1;

					oSettings._iDisplayStart = iTarget * oSettings._iDisplayLength;

					fnCallbackDraw( oSettings );

					return false;

				};

				var fnFalse = function () { return false; };

				

				for ( i=0, iLen=an.length ; i<iLen ; i++ )

				{

					if ( an[i].childNodes.length === 0 )

					{

						continue;

					}

					

					/* Build up the dynamic list forst - html and listeners */

					var qjPaginateList = $('span:eq(2)', an[i]);

					qjPaginateList.html( sList );

					$('span', qjPaginateList).click( fnClick ).bind( 'mousedown', fnFalse )

						.bind( 'selectstart', fnFalse );

					

					/* Update the 'premanent botton's classes */

					anButtons = an[i].getElementsByTagName('span');

					anStatic = [

						anButtons[0], anButtons[1], 

						anButtons[anButtons.length-2], anButtons[anButtons.length-1]

					];

					$(anStatic).removeClass( oClasses.sPageButton+" "+oClasses.sPageButtonActive+" "+oClasses.sPageButtonStaticDisabled );

					if ( iCurrentPage == 1 )

					{

						anStatic[0].className += " "+oClasses.sPageButtonStaticDisabled;

						anStatic[1].className += " "+oClasses.sPageButtonStaticDisabled;

					}

					else

					{

						anStatic[0].className += " "+oClasses.sPageButton;

						anStatic[1].className += " "+oClasses.sPageButton;

					}

					

					if ( iPages === 0 || iCurrentPage == iPages || oSettings._iDisplayLength == -1 )

					{

						anStatic[2].className += " "+oClasses.sPageButtonStaticDisabled;

						anStatic[3].className += " "+oClasses.sPageButtonStaticDisabled;

					}

					else

					{

						anStatic[2].className += " "+oClasses.sPageButton;

						anStatic[3].className += " "+oClasses.sPageButton;

					}

				}

			}

		}

	};

	

	/*

	 * Variable: oSort

	 * Purpose:  Wrapper for the sorting functions that can be used in DataTables

	 * Scope:    jQuery.fn.dataTableExt

	 * Notes:    The functions provided in this object are basically standard javascript sort

	 *   functions - they expect two inputs which they then compare and then return a priority

	 *   result. For each sort method added, two functions need to be defined, an ascending sort and

	 *   a descending sort.

	 */

	_oExt.oSort = {

		/*

		 * text sorting

		 */

		"string-asc": function ( a, b )

		{

			var x = a.toLowerCase();

			var y = b.toLowerCase();

			return ((x < y) ? -1 : ((x > y) ? 1 : 0));

		},

		

		"string-desc": function ( a, b )

		{

			var x = a.toLowerCase();

			var y = b.toLowerCase();

			return ((x < y) ? 1 : ((x > y) ? -1 : 0));

		},

		

		

		/*

		 * html sorting (ignore html tags)

		 */

		"html-asc": function ( a, b )

		{

			var x = a.replace( /<.*?>/g, "" ).toLowerCase();

			var y = b.replace( /<.*?>/g, "" ).toLowerCase();

			return ((x < y) ? -1 : ((x > y) ? 1 : 0));

		},

		

		"html-desc": function ( a, b )

		{

			var x = a.replace( /<.*?>/g, "" ).toLowerCase();

			var y = b.replace( /<.*?>/g, "" ).toLowerCase();

			return ((x < y) ? 1 : ((x > y) ? -1 : 0));

		},

		

		

		/*

		 * date sorting

		 */

		"date-asc": function ( a, b )

		{

			var x = Date.parse( a );

			var y = Date.parse( b );

			

			if ( isNaN(x) || x==="" )

			{

    		x = Date.parse( "01/01/1970 00:00:00" );

			}

			if ( isNaN(y) || y==="" )

			{

				y =	Date.parse( "01/01/1970 00:00:00" );

			}

			

			return x - y;

		},

		

		"date-desc": function ( a, b )

		{

			var x = Date.parse( a );

			var y = Date.parse( b );

			

			if ( isNaN(x) || x==="" )

			{

    		x = Date.parse( "01/01/1970 00:00:00" );

			}

			if ( isNaN(y) || y==="" )

			{

				y =	Date.parse( "01/01/1970 00:00:00" );

			}

			

			return y - x;

		},

		

		

		/*

		 * numerical sorting

		 */

		"numeric-asc": function ( a, b )

		{

			var x = (a=="-" || a==="") ? 0 : a*1;

			var y = (b=="-" || b==="") ? 0 : b*1;

			return x - y;

		},

		

		"numeric-desc": function ( a, b )

		{

			var x = (a=="-" || a==="") ? 0 : a*1;

			var y = (b=="-" || b==="") ? 0 : b*1;

			return y - x;

		}

	};

	

	

	/*

	 * Variable: aTypes

	 * Purpose:  Container for the various type of type detection that dataTables supports

	 * Scope:    jQuery.fn.dataTableExt

	 * Notes:    The functions in this array are expected to parse a string to see if it is a data

	 *   type that it recognises. If so then the function should return the name of the type (a

	 *   corresponding sort function should be defined!), if the type is not recognised then the

	 *   function should return null such that the parser and move on to check the next type.

	 *   Note that ordering is important in this array - the functions are processed linearly,

	 *   starting at index 0.

	 *   Note that the input for these functions is always a string! It cannot be any other data

	 *   type

	 */

	_oExt.aTypes = [

		/*

		 * Function: -

		 * Purpose:  Check to see if a string is numeric

		 * Returns:  string:'numeric' or null

		 * Inputs:   string:sText - string to check

		 */

		function ( sData )

		{

			/* Allow zero length strings as a number */

			if ( sData.length === 0 )

			{

				return 'numeric';

			}

			

			var sValidFirstChars = "0123456789-";

			var sValidChars = "0123456789.";

			var Char;

			var bDecimal = false;

			

			/* Check for a valid first char (no period and allow negatives) */

			Char = sData.charAt(0); 

			if (sValidFirstChars.indexOf(Char) == -1) 

			{

				return null;

			}

			

			/* Check all the other characters are valid */

			for ( var i=1 ; i<sData.length ; i++ ) 

			{

				Char = sData.charAt(i); 

				if (sValidChars.indexOf(Char) == -1) 

				{

					return null;

				}

				

				/* Only allowed one decimal place... */

				if ( Char == "." )

				{

					if ( bDecimal )

					{

						return null;

					}

					bDecimal = true;

				}

			}

			

			return 'numeric';

		},

		

		/*

		 * Function: -

		 * Purpose:  Check to see if a string is actually a formatted date

		 * Returns:  string:'date' or null

		 * Inputs:   string:sText - string to check

		 */

		function ( sData )

		{

			var iParse = Date.parse(sData);

			if ( (iParse !== null && !isNaN(iParse)) || sData.length === 0 )

			{

				return 'date';

			}

			return null;

		},

		

		/*

		 * Function: -

		 * Purpose:  Check to see if a string should be treated as an HTML string

		 * Returns:  string:'html' or null

		 * Inputs:   string:sText - string to check

		 */

		function ( sData )

		{

			if ( sData.indexOf('<') != -1 && sData.indexOf('>') != -1 )

			{

				return 'html';

			}

			return null;

		}

	];

	

	/*

	 * Function: fnVersionCheck

	 * Purpose:  Check a version string against this version of DataTables. Useful for plug-ins

	 * Returns:  bool:true -this version of DataTables is greater or equal to the required version

	 *                false -this version of DataTales is not suitable

	 * Inputs:   string:sVersion - the version to check against. May be in the following formats:

	 *             "a", "a.b" or "a.b.c"

	 * Notes:    This function will only check the first three parts of a version string. It is

	 *   assumed that beta and dev versions will meet the requirements. This might change in future

	 */

	_oExt.fnVersionCheck = function( sVersion )

	{

		/* This is cheap, but very effective */

		var fnZPad = function (Zpad, count)

		{

			while(Zpad.length < count) {

				Zpad += '0';

			}

			return Zpad;

		};

		var aThis = _oExt.sVersion.split('.');

		var aThat = sVersion.split('.');

		var sThis = '', sThat = '';

		

		for ( var i=0, iLen=aThat.length ; i<iLen ; i++ )

		{

			sThis += fnZPad( aThis[i], 3 );

			sThat += fnZPad( aThat[i], 3 );

		}

		

		return parseInt(sThis, 10) >= parseInt(sThat, 10);

	};

	

	/*

	 * Variable: _oExternConfig

	 * Purpose:  Store information for DataTables to access globally about other instances

	 * Scope:    jQuery.fn.dataTableExt

	 */

	_oExt._oExternConfig = {

		/* int:iNextUnique - next unique number for an instance */

		"iNextUnique": 0

	};

	

	

	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

	 * Section - DataTables prototype

	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

	

	/*

	 * Function: dataTable

	 * Purpose:  DataTables information

	 * Returns:  -

	 * Inputs:   object:oInit - initalisation options for the table

	 */

	$.fn.dataTable = function( oInit )

	{

		/*

		 * Function: classSettings

		 * Purpose:  Settings container function for all 'class' properties which are required

		 *   by dataTables

		 * Returns:  -

		 * Inputs:   -

		 */

		function classSettings ()

		{

			this.fnRecordsTotal = function ()

			{

				if ( this.oFeatures.bServerSide ) {

					return parseInt(this._iRecordsTotal, 10);

				} else {

					return this.aiDisplayMaster.length;

				}

			};

			

			this.fnRecordsDisplay = function ()

			{

				if ( this.oFeatures.bServerSide ) {

					return parseInt(this._iRecordsDisplay, 10);

				} else {

					return this.aiDisplay.length;

				}

			};

			

			this.fnDisplayEnd = function ()

			{

				if ( this.oFeatures.bServerSide ) {

					if ( this.oFeatures.bPaginate === false || this._iDisplayLength == -1 ) {

						return this._iDisplayStart+this.aiDisplay.length;

					} else {

						return Math.min( this._iDisplayStart+this._iDisplayLength, 

							this._iRecordsDisplay );

					}

				} else {

					return this._iDisplayEnd;

				}

			};

			

			/*

			 * Variable: oInstance

			 * Purpose:  The DataTables object for this table

			 * Scope:    jQuery.dataTable.classSettings 

			 */

			this.oInstance = null;

			

			/*

			 * Variable: sInstance

			 * Purpose:  Unique idendifier for each instance of the DataTables object

			 * Scope:    jQuery.dataTable.classSettings 

			 */

			this.sInstance = null;

			

			/*

			 * Variable: oFeatures

			 * Purpose:  Indicate the enablement of key dataTable features

			 * Scope:    jQuery.dataTable.classSettings 

			 */

			this.oFeatures = {

				"bPaginate": true,

				"bLengthChange": true,

				"bFilter": true,

				"bSort": true,

				"bInfo": true,

				"bAutoWidth": true,

				"bProcessing": false,

				"bSortClasses": true,

				"bStateSave": false,

				"bServerSide": false

			};

			

			/*

			 * Variable: oScroll

			 * Purpose:  Container for scrolling options

			 * Scope:    jQuery.dataTable.classSettings 

			 */

			this.oScroll = {

				"sX": "",

				"sXInner": "",

				"sY": "",

				"bCollapse": false,

				"bInfinite": false,

				"iLoadGap": 100,

				"iBarWidth": 0

			};

			

			/*

			 * Variable: aanFeatures

			 * Purpose:  Array referencing the nodes which are used for the features

			 * Scope:    jQuery.dataTable.classSettings 

			 * Notes:    The parameters of this object match what is allowed by sDom - i.e.

			 *   'l' - Length changing

			 *   'f' - Filtering input

			 *   't' - The table!

			 *   'i' - Information

			 *   'p' - Pagination

			 *   'r' - pRocessing

			 */

			this.aanFeatures = [];

			

			/*

			 * Variable: oLanguage

			 * Purpose:  Store the language strings used by dataTables

			 * Scope:    jQuery.dataTable.classSettings

			 * Notes:    The words in the format _VAR_ are variables which are dynamically replaced

			 *   by javascript

			 */

			this.oLanguage = {

				"sProcessing": "Processing...",

				"sLengthMenu": "_MENU_ records per page",

				"sZeroRecords": "No matching records found",

				"sEmptyTable": "No record available",

				"sInfo": " _START_ to _END_ of _TOTAL_ records",

				"sInfoEmpty": " no record",

				"sInfoFiltered": "(filtered from _MAX_ total entries)",

				"sInfoPostFix": "",

				"sSearch": "Quick Search:",

				"sUrl": "",

				"oPaginate": {

					"sFirst":    "First",

					"sPrevious": "Prev",

					"sNext":     "Next",

					"sLast":     "Last"

				},

				"fnInfoCallback": null

			};

			

			/*

			 * Variable: aoData

			 * Purpose:  Store data information

			 * Scope:    jQuery.dataTable.classSettings 

			 * Notes:    This is an array of objects with the following parameters:

			 *   int: _iId - internal id for tracking

			 *   array: _aData - internal data - used for sorting / filtering etc

			 *   node: nTr - display node

			 *   array node: _anHidden - hidden TD nodes

			 *   string: _sRowStripe

			 */

			this.aoData = [];

			

			/*

			 * Variable: aiDisplay

			 * Purpose:  Array of indexes which are in the current display (after filtering etc)

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.aiDisplay = [];

			

			/*

			 * Variable: aiDisplayMaster

			 * Purpose:  Array of indexes for display - no filtering

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.aiDisplayMaster = [];

							

			/*

			 * Variable: aoColumns

			 * Purpose:  Store information about each column that is in use

			 * Scope:    jQuery.dataTable.classSettings 

			 */

			this.aoColumns = [];

			

			/*

			 * Variable: iNextId

			 * Purpose:  Store the next unique id to be used for a new row

			 * Scope:    jQuery.dataTable.classSettings 

			 */

			this.iNextId = 0;

			

			/*

			 * Variable: asDataSearch

			 * Purpose:  Search data array for regular expression searching

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.asDataSearch = [];

			

			/*

			 * Variable: oPreviousSearch

			 * Purpose:  Store the previous search incase we want to force a re-search

			 *   or compare the old search to a new one

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.oPreviousSearch = {

				"sSearch": "",

				"bRegex": false,

				"bSmart": true

			};

			

			/*

			 * Variable: aoPreSearchCols

			 * Purpose:  Store the previous search for each column

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.aoPreSearchCols = [];

			

			/*

			 * Variable: aaSorting

			 * Purpose:  Sorting information

			 * Scope:    jQuery.dataTable.classSettings

			 * Notes:    Index 0 - column number

			 *           Index 1 - current sorting direction

			 *           Index 2 - index of asSorting for this column

			 */

			this.aaSorting = [ [0, 'asc', 0] ];

			

			/*

			 * Variable: aaSortingFixed

			 * Purpose:  Sorting information that is always applied

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.aaSortingFixed = null;

			

			/*

			 * Variable: asStripClasses

			 * Purpose:  Classes to use for the striping of a table

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.asStripClasses = [];

			

			/*

			 * Variable: asDestoryStrips

			 * Purpose:  If restoring a table - we should restore it's striping classes as well

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.asDestoryStrips = [];

			

			/*

			 * Variable: sDestroyWidth

			 * Purpose:  If restoring a table - we should restore it's width

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.sDestroyWidth = 0;

			

			/*

			 * Variable: fnRowCallback

			 * Purpose:  Call this function every time a row is inserted (draw)

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.fnRowCallback = null;

			

			/*

			 * Variable: fnHeaderCallback

			 * Purpose:  Callback function for the header on each draw

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.fnHeaderCallback = null;

			

			/*

			 * Variable: fnFooterCallback

			 * Purpose:  Callback function for the footer on each draw

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.fnFooterCallback = null;

			

			/*

			 * Variable: aoDrawCallback

			 * Purpose:  Array of callback functions for draw callback functions

			 * Scope:    jQuery.dataTable.classSettings

			 * Notes:    Each array element is an object with the following parameters:

			 *   function:fn - function to call

			 *   string:sName - name callback (feature). useful for arranging array

			 */

			this.aoDrawCallback = [];

			

			/*

			 * Variable: fnInitComplete

			 * Purpose:  Callback function for when the table has been initalised

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.fnInitComplete = null;

			

			/*

			 * Variable: sTableId

			 * Purpose:  Cache the table ID for quick access

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.sTableId = "";

			

			/*

			 * Variable: nTable

			 * Purpose:  Cache the table node for quick access

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.nTable = null;

			

			/*

			 * Variable: nTHead

			 * Purpose:  Permanent ref to the thead element

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.nTHead = null;

			

			/*

			 * Variable: nTFoot

			 * Purpose:  Permanent ref to the tfoot element - if it exists

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.nTFoot = null;

			

			/*

			 * Variable: nTBody

			 * Purpose:  Permanent ref to the tbody element

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.nTBody = null;

			

			/*

			 * Variable: nTableWrapper

			 * Purpose:  Cache the wrapper node (contains all DataTables controlled elements)

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.nTableWrapper = null;

			

			/*

			 * Variable: bInitialised

			 * Purpose:  Indicate if all required information has been read in

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.bInitialised = false;

			

			/*

			 * Variable: aoOpenRows

			 * Purpose:  Information about open rows

			 * Scope:    jQuery.dataTable.classSettings

			 * Notes:    Has the parameters 'nTr' and 'nParent'

			 */

			this.aoOpenRows = [];

			

			/*

			 * Variable: sDom

			 * Purpose:  Dictate the positioning that the created elements will take

			 * Scope:    jQuery.dataTable.classSettings

			 * Notes:    

			 *   The following options are allowed:

			 *     'l' - Length changing

			 *     'f' - Filtering input

			 *     't' - The table!

			 *     'i' - Information

			 *     'p' - Pagination

			 *     'r' - pRocessing

			 *   The following constants are allowed:

			 *     'H' - jQueryUI theme "header" classes

			 *     'F' - jQueryUI theme "footer" classes

			 *   The following syntax is expected:

			 *     '<' and '>' - div elements

			 *     '<"class" and '>' - div with a class

			 *   Examples:

			 *     '<"wrapper"flipt>', '<lf<t>ip>'

			 */

			this.sDom = 'lfrtip';

			

			/*

			 * Variable: sPaginationType

			 * Purpose:  Note which type of sorting should be used

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.sPaginationType = "two_button";

			

			/*

			 * Variable: iCookieDuration

			 * Purpose:  The cookie duration (for bStateSave) in seconds - default 2 hours

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.iCookieDuration = 60 * 60 * 2;

			

			/*

			 * Variable: sCookiePrefix

			 * Purpose:  The cookie name prefix

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.sCookiePrefix = "SpryMedia_DataTables_";

			

			/*

			 * Variable: fnCookieCallback

			 * Purpose:  Callback function for cookie creation

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.fnCookieCallback = null;

			

			/*

			 * Variable: aoStateSave

			 * Purpose:  Array of callback functions for state saving

			 * Scope:    jQuery.dataTable.classSettings

			 * Notes:    Each array element is an object with the following parameters:

			 *   function:fn - function to call. Takes two parameters, oSettings and the JSON string to

			 *     save that has been thus far created. Returns a JSON string to be inserted into a 

			 *     json object (i.e. '"param": [ 0, 1, 2]')

			 *   string:sName - name of callback

			 */

			this.aoStateSave = [];

			

			/*

			 * Variable: aoStateLoad

			 * Purpose:  Array of callback functions for state loading

			 * Scope:    jQuery.dataTable.classSettings

			 * Notes:    Each array element is an object with the following parameters:

			 *   function:fn - function to call. Takes two parameters, oSettings and the object stored.

			 *     May return false to cancel state loading.

			 *   string:sName - name of callback

			 */

			this.aoStateLoad = [];

			

			/*

			 * Variable: oLoadedState

			 * Purpose:  State that was loaded from the cookie. Useful for back reference

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.oLoadedState = null;

			

			/*

			 * Variable: sAjaxSource

			 * Purpose:  Source url for AJAX data for the table

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.sAjaxSource = null;

			

			/*

			 * Variable: bAjaxDataGet

			 * Purpose:  Note if draw should be blocked while getting data

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.bAjaxDataGet = true;

			

			/*

			 * Variable: fnServerData

			 * Purpose:  Function to get the server-side data - can be overruled by the developer

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.fnServerData = function ( url, data, callback ) {

				$.ajax( {

					"url": url,

					"data": data,

					"success": callback,

					"dataType": "json",

					"cache": false,

					"error": function (xhr, error, thrown) {

						if ( error == "parsererror" ) {

							alert( "DataTables warning: JSON data from server could not be parsed. "+

								"This is caused by a JSON formatting error." );

						}

					}

				} );

			};

			

			/*

			 * Variable: fnFormatNumber

			 * Purpose:  Format numbers for display

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.fnFormatNumber = function ( iIn )

			{

				if ( iIn < 1000 )

				{

					/* A small optimisation for what is likely to be the vast majority of use cases */

					return iIn;

				}

				else

				{

					var s=(iIn+""), a=s.split(""), out="", iLen=s.length;

					

					for ( var i=0 ; i<iLen ; i++ )

					{

						if ( i%3 === 0 && i !== 0 )

						{

							out = ','+out;

						}

						out = a[iLen-i-1]+out;

					}

				}

				return out;

			};

			

			/*

			 * Variable: aLengthMenu

			 * Purpose:  List of options that can be used for the user selectable length menu

			 * Scope:    jQuery.dataTable.classSettings

			 * Note:     This varaible can take for form of a 1D array, in which case the value and the 

			 *   displayed value in the menu are the same, or a 2D array in which case the value comes

			 *   from the first array, and the displayed value to the end user comes from the second

			 *   array. 2D example: [ [ 10, 25, 50, 100, -1 ], [ 10, 25, 50, 100, 'All' ] ];

			 */

			this.aLengthMenu = [ 10, 25, 50, 100 ];

			

			/*

			 * Variable: iDraw

			 * Purpose:  Counter for the draws that the table does. Also used as a tracker for

			 *   server-side processing

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.iDraw = 0;

			

			/*

			 * Variable: bDrawing

			 * Purpose:  Indicate if a redraw is being done - useful for Ajax

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.bDrawing = 0;

			

			/*

			 * Variable: iDrawError

			 * Purpose:  Last draw error

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.iDrawError = -1;

			

			/*

			 * Variable: _iDisplayLength, _iDisplayStart, _iDisplayEnd

			 * Purpose:  Display length variables

			 * Scope:    jQuery.dataTable.classSettings

			 * Notes:    These variable must NOT be used externally to get the data length. Rather, use

			 *   the fnRecordsTotal() (etc) functions.

			 */

			this._iDisplayLength = 10;

			this._iDisplayStart = 0;

			this._iDisplayEnd = 10;

			

			/*

			 * Variable: _iRecordsTotal, _iRecordsDisplay

			 * Purpose:  Display length variables used for server side processing

			 * Scope:    jQuery.dataTable.classSettings

			 * Notes:    These variable must NOT be used externally to get the data length. Rather, use

			 *   the fnRecordsTotal() (etc) functions.

			 */

			this._iRecordsTotal = 0;

			this._iRecordsDisplay = 0;

			

			/*

			 * Variable: bJUI

			 * Purpose:  Should we add the markup needed for jQuery UI theming?

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.bJUI = false;

			

			/*

			 * Variable: bJUI

			 * Purpose:  Should we add the markup needed for jQuery UI theming?

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.oClasses = _oExt.oStdClasses;

			

			/*

			 * Variable: bFiltered and bSorted

			 * Purpose:  Flags to allow callback functions to see what actions have been performed

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.bFiltered = false;

			this.bSorted = false;

			

			/*

			 * Variable: oInit

			 * Purpose:  Initialisation object that is used for the table

			 * Scope:    jQuery.dataTable.classSettings

			 */

			this.oInit = null;

		}

		

		/*

		 * Variable: oApi

		 * Purpose:  Container for publicly exposed 'private' functions

		 * Scope:    jQuery.dataTable

		 */

		this.oApi = {};

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - API functions

		 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

		

		/*

		 * Function: fnDraw

		 * Purpose:  Redraw the table

		 * Returns:  -

		 * Inputs:   bool:bComplete - Refilter and resort (if enabled) the table before the draw.

		 *             Optional: default - true

		 */

		this.fnDraw = function( bComplete )

		{

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			if ( typeof bComplete != 'undefined' && bComplete === false )

			{

				_fnCalculateEnd( oSettings );

				_fnDraw( oSettings );

			}

			else

			{

				_fnReDraw( oSettings );

			}

		};

		

		/*

		 * Function: fnFilter

		 * Purpose:  Filter the input based on data

		 * Returns:  -

		 * Inputs:   string:sInput - string to filter the table on

		 *           int:iColumn - optional - column to limit filtering to

		 *           bool:bRegex - optional - treat as regular expression or not - default false

		 *           bool:bSmart - optional - perform smart filtering or not - default true

		 *           bool:bShowGlobal - optional - show the input global filter in it's input box(es)

		 *              - default true

		 */

		this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal )

		{

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			

			if ( !oSettings.oFeatures.bFilter )

			{

				return;

			}

			

			if ( typeof bRegex == 'undefined' )

			{

				bRegex = false;

			}

			

			if ( typeof bSmart == 'undefined' )

			{

				bSmart = true;

			}

			

			if ( typeof bShowGlobal == 'undefined' )

			{

				bShowGlobal = true;

			}

			

			if ( typeof iColumn == "undefined" || iColumn === null )

			{

				/* Global filter */

				_fnFilterComplete( oSettings, {

					"sSearch":sInput,

					"bRegex": bRegex,

					"bSmart": bSmart

				}, 1 );

				

				if ( bShowGlobal && typeof oSettings.aanFeatures.f != 'undefined' )

				{

					var n = oSettings.aanFeatures.f;

					for ( var i=0, iLen=n.length ; i<iLen ; i++ )

					{

						$('input', n[i]).val( sInput );

					}

				}

			}

			else

			{

				/* Single column filter */

				oSettings.aoPreSearchCols[ iColumn ].sSearch = sInput;

				oSettings.aoPreSearchCols[ iColumn ].bRegex = bRegex;

				oSettings.aoPreSearchCols[ iColumn ].bSmart = bSmart;

				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );

			}

		};

		

		/*

		 * Function: fnSettings

		 * Purpose:  Get the settings for a particular table for extern. manipulation

		 * Returns:  -

		 * Inputs:   -

		 */

		this.fnSettings = function( nNode  )

		{

			return _fnSettingsFromNode( this[_oExt.iApiIndex] );

		};

		

		/*

		 * Function: fnVersionCheck

		 * Notes:    The function is the same as the 'static' function provided in the ext variable

		 */

		this.fnVersionCheck = _oExt.fnVersionCheck;

		

		/*

		 * Function: fnSort

		 * Purpose:  Sort the table by a particular row

		 * Returns:  -

		 * Inputs:   int:iCol - the data index to sort on. Note that this will

		 *   not match the 'display index' if you have hidden data entries

		 */

		this.fnSort = function( aaSort )

		{

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			oSettings.aaSorting = aaSort;

			_fnSort( oSettings );

		};

		

		/*

		 * Function: fnSortListener

		 * Purpose:  Attach a sort listener to an element for a given column

		 * Returns:  -

		 * Inputs:   node:nNode - the element to attach the sort listener to

		 *           int:iColumn - the column that a click on this node will sort on

		 *           function:fnCallback - callback function when sort is run - optional

		 */

		this.fnSortListener = function( nNode, iColumn, fnCallback )

		{

			_fnSortAttachListener( _fnSettingsFromNode( this[_oExt.iApiIndex] ), nNode, iColumn,

			 	fnCallback );

		};

		

		/*

		 * Function: fnAddData

		 * Purpose:  Add new row(s) into the table

		 * Returns:  array int: array of indexes (aoData) which have been added (zero length on error)

		 * Inputs:   array:mData - the data to be added. The length must match

		 *               the original data from the DOM

		 *             or

		 *             array array:mData - 2D array of data to be added

		 *           bool:bRedraw - redraw the table or not - default true

		 * Notes:    Warning - the refilter here will cause the table to redraw

		 *             starting at zero

		 * Notes:    Thanks to Yekimov Denis for contributing the basis for this function!

		 */

		this.fnAddData = function( mData, bRedraw )

		{

			if ( mData.length === 0 )

			{

				return [];

			}

			

			var aiReturn = [];

			var iTest;

			

			/* Find settings from table node */

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			

			/* Check if we want to add multiple rows or not */

			if ( typeof mData[0] == "object" )

			{

				for ( var i=0 ; i<mData.length ; i++ )

				{

					iTest = _fnAddData( oSettings, mData[i] );

					if ( iTest == -1 )

					{

						return aiReturn;

					}

					aiReturn.push( iTest );

				}

			}

			else

			{

				iTest = _fnAddData( oSettings, mData );

				if ( iTest == -1 )

				{

					return aiReturn;

				}

				aiReturn.push( iTest );

			}

			

			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();

			

			if ( typeof bRedraw == 'undefined' || bRedraw )

			{

				_fnReDraw( oSettings );

			}

			return aiReturn;

		};

		

		/*

		 * Function: fnDeleteRow

		 * Purpose:  Remove a row for the table

		 * Returns:  array:aReturn - the row that was deleted

		 * Inputs:   mixed:mTarget - 

		 *             int: - index of aoData to be deleted, or

		 *             node(TR): - TR element you want to delete

		 *           function:fnCallBack - callback function - default null

		 *           bool:bRedraw - redraw the table or not - default true

		 */

		this.fnDeleteRow = function( mTarget, fnCallBack, bRedraw )

		{

			/* Find settings from table node */

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			var i, iAODataIndex;

			

			iAODataIndex = (typeof mTarget == 'object') ? 

				_fnNodeToDataIndex(oSettings, mTarget) : mTarget;

			

			/* Return the data array from this row */

			var oData = oSettings.aoData.splice( iAODataIndex, 1 );

			

			/* Remove the target row from the search array */

			var iDisplayIndex = $.inArray( iAODataIndex, oSettings.aiDisplay );

			oSettings.asDataSearch.splice( iDisplayIndex, 1 );

			

			/* Delete from the display arrays */

			_fnDeleteIndex( oSettings.aiDisplayMaster, iAODataIndex );

			_fnDeleteIndex( oSettings.aiDisplay, iAODataIndex );

			

			/* If there is a user callback function - call it */

			if ( typeof fnCallBack == "function" )

			{

				fnCallBack.call( this, oSettings, oData );

			}

			

			/* Check for an 'overflow' they case for dislaying the table */

			if ( oSettings._iDisplayStart >= oSettings.aiDisplay.length )

			{

				oSettings._iDisplayStart -= oSettings._iDisplayLength;

				if ( oSettings._iDisplayStart < 0 )

				{

					oSettings._iDisplayStart = 0;

				}

			}

			

			if ( typeof bRedraw == 'undefined' || bRedraw )

			{

				_fnCalculateEnd( oSettings );

				_fnDraw( oSettings );

			}

			

			return oData;

		};

		

		/*

		 * Function: fnClearTable

		 * Purpose:  Quickly and simply clear a table

		 * Returns:  -

		 * Inputs:   bool:bRedraw - redraw the table or not - default true

		 * Notes:    Thanks to Yekimov Denis for contributing the basis for this function!

		 */

		this.fnClearTable = function( bRedraw )

		{

			/* Find settings from table node */

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			_fnClearTable( oSettings );

			

			if ( typeof bRedraw == 'undefined' || bRedraw )

			{

				_fnDraw( oSettings );

			}

		};

		

		/*

		 * Function: fnOpen

		 * Purpose:  Open a display row (append a row after the row in question)

		 * Returns:  node:nNewRow - the row opened

		 * Inputs:   node:nTr - the table row to 'open'

		 *           string:sHtml - the HTML to put into the row

		 *           string:sClass - class to give the new TD cell

		 */

		this.fnOpen = function( nTr, sHtml, sClass )

		{

			/* Find settings from table node */

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			

			/* the old open one if there is one */

			this.fnClose( nTr );

			

			var nNewRow = document.createElement("tr");

			var nNewCell = document.createElement("td");

			nNewRow.appendChild( nNewCell );

			nNewCell.className = sClass;

			nNewCell.colSpan = _fnVisbleColumns( oSettings );

			nNewCell.innerHTML = sHtml;

			

			/* If the nTr isn't on the page at the moment - then we don't insert at the moment */

			var nTrs = $('tr', oSettings.nTBody);

			if ( $.inArray(nTr, nTrs) != -1 )

			{

				$(nNewRow).insertAfter(nTr);

			}

			

			oSettings.aoOpenRows.push( {

				"nTr": nNewRow,

				"nParent": nTr

			} );

			

			return nNewRow;

		};

		

		/*

		 * Function: fnClose

		 * Purpose:  Close a display row

		 * Returns:  int: 0 (success) or 1 (failed)

		 * Inputs:   node:nTr - the table row to 'close'

		 */

		this.fnClose = function( nTr )

		{

			/* Find settings from table node */

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			

			for ( var i=0 ; i<oSettings.aoOpenRows.length ; i++ )

			{

				if ( oSettings.aoOpenRows[i].nParent == nTr )

				{

					var nTrParent = oSettings.aoOpenRows[i].nTr.parentNode;

					if ( nTrParent )

					{

						/* Remove it if it is currently on display */

						nTrParent.removeChild( oSettings.aoOpenRows[i].nTr );

					}

					oSettings.aoOpenRows.splice( i, 1 );

					return 0;

				}

			}

			return 1;

		};

		

		/*

		 * Function: fnGetData

		 * Purpose:  Return an array with the data which is used to make up the table

		 * Returns:  array array string: 2d data array ([row][column]) or array string: 1d data array

		 *           or

		 *           array string (if iRow specified)

		 * Inputs:   mixed:mRow - optional - if not present, then the full 2D array for the table 

		 *             if given then:

		 *               int: - return 1D array for aoData entry of this index

		 *               node(TR): - return 1D array for this TR element

		 * Inputs:   int:iRow - optional - if present then the array returned will be the data for

		 *             the row with the index 'iRow'

		 */

		this.fnGetData = function( mRow )

		{

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			

			if ( typeof mRow != 'undefined' )

			{

				var iRow = (typeof mRow == 'object') ? 

					_fnNodeToDataIndex(oSettings, mRow) : mRow;

				return oSettings.aoData[iRow]._aData;

			}

			return _fnGetDataMaster( oSettings );

		};

		

		/*

		 * Function: fnGetNodes

		 * Purpose:  Return an array with the TR nodes used for drawing the table

		 * Returns:  array node: TR elements

		 *           or

		 *           node (if iRow specified)

		 * Inputs:   int:iRow - optional - if present then the array returned will be the node for

		 *             the row with the index 'iRow'

		 */

		this.fnGetNodes = function( iRow )

		{

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			

			if ( typeof iRow != 'undefined' )

			{

				return oSettings.aoData[iRow].nTr;

			}

			return _fnGetTrNodes( oSettings );

		};

		

		/*

		 * Function: fnGetPosition

		 * Purpose:  Get the array indexes of a particular cell from it's DOM element

		 * Returns:  int: - row index, or array[ int, int, int ]: - row index, column index (visible)

		 *             and column index including hidden columns

		 * Inputs:   node:nNode - this can either be a TR or a TD in the table, the return is

		 *             dependent on this input

		 */

		this.fnGetPosition = function( nNode )

		{

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			var i;

			

			if ( nNode.nodeName.toUpperCase() == "TR" )

			{

				return _fnNodeToDataIndex(oSettings, nNode);

			}

			else if ( nNode.nodeName.toUpperCase() == "TD" )

			{

				var iDataIndex = _fnNodeToDataIndex(oSettings, nNode.parentNode);

				var iCorrector = 0;

				for ( var j=0 ; j<oSettings.aoColumns.length ; j++ )

				{

					if ( oSettings.aoColumns[j].bVisible )

					{

						if ( oSettings.aoData[iDataIndex].nTr.getElementsByTagName('td')[j-iCorrector] == nNode )

						{

							return [ iDataIndex, j-iCorrector, j ];

						}

					}

					else

					{

						iCorrector++;

					}

				}

			}

			return null;

		};

		

		/*

		 * Function: fnUpdate

		 * Purpose:  Update a table cell or row

		 * Returns:  int: 0 okay, 1 error

		 * Inputs:   array string 'or' string:mData - data to update the cell/row with

		 *           mixed:mRow - 

		 *             int: - index of aoData to be updated, or

		 *             node(TR): - TR element you want to update

		 *           int:iColumn - the column to update - optional (not used of mData is 2D)

		 *           bool:bRedraw - redraw the table or not - default true

		 *           bool:bAction - perform predraw actions or not (you will want this as 'true' if

		 *             you have bRedraw as true) - default true

		 */

		this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction )

		{

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			var iVisibleColumn;

			var sDisplay;

			var iRow = (typeof mRow == 'object') ? 

				_fnNodeToDataIndex(oSettings, mRow) : mRow;

			

			if ( typeof mData != 'object' )

			{

				sDisplay = mData;

				oSettings.aoData[iRow]._aData[iColumn] = sDisplay;

				

				if ( oSettings.aoColumns[iColumn].fnRender !== null )

				{

					sDisplay = oSettings.aoColumns[iColumn].fnRender( {

						"iDataRow": iRow,

						"iDataColumn": iColumn,

						"aData": oSettings.aoData[iRow]._aData,

						"oSettings": oSettings

					} );

					

					if ( oSettings.aoColumns[iColumn].bUseRendered )

					{

						oSettings.aoData[iRow]._aData[iColumn] = sDisplay;

					}

				}

				

				iVisibleColumn = _fnColumnIndexToVisible( oSettings, iColumn );

				if ( iVisibleColumn !== null )

				{

					oSettings.aoData[iRow].nTr.getElementsByTagName('td')[iVisibleColumn].innerHTML = 

						sDisplay;

				}

			}

			else

			{

				if ( mData.length != oSettings.aoColumns.length )

				{

					_fnLog( oSettings, 0, 'An array passed to fnUpdate must have the same number of '+

						'columns as the table in question - in this case '+oSettings.aoColumns.length );

					return 1;

				}

				

				for ( var i=0 ; i<mData.length ; i++ )

				{

					sDisplay = mData[i];

					oSettings.aoData[iRow]._aData[i] = sDisplay;

					

					if ( oSettings.aoColumns[i].fnRender !== null )

					{

						sDisplay = oSettings.aoColumns[i].fnRender( {

							"iDataRow": iRow,

							"iDataColumn": i,

							"aData": oSettings.aoData[iRow]._aData,

							"oSettings": oSettings

						} );

						

						if ( oSettings.aoColumns[i].bUseRendered )

						{

							oSettings.aoData[iRow]._aData[i] = sDisplay;

						}

					}

					

					iVisibleColumn = _fnColumnIndexToVisible( oSettings, i );

					if ( iVisibleColumn !== null )

					{

						oSettings.aoData[iRow].nTr.getElementsByTagName('td')[iVisibleColumn].innerHTML = 

							sDisplay;

					}

				}

			}

			

			/* Modify the search index for this row (strictly this is likely not needed, since fnReDraw

			 * will rebuild the search array - however, the redraw might be disabled by the user)

			 */

			var iDisplayIndex = $.inArray( iRow, oSettings.aiDisplay );

			oSettings.asDataSearch[iDisplayIndex] = _fnBuildSearchRow( oSettings, 

				oSettings.aoData[iRow]._aData );

			

			/* Perform pre-draw actions */

			if ( typeof bAction == 'undefined' || bAction )

			{

				_fnAjustColumnSizing( oSettings );

			}

			

			/* Redraw the table */

			if ( typeof bRedraw == 'undefined' || bRedraw )

			{

				_fnReDraw( oSettings );

			}

			return 0;

		};

		

		

		/*

		 * Function: fnShowColoumn

		 * Purpose:  Show a particular column

		 * Returns:  -

		 * Inputs:   int:iCol - the column whose display should be changed

		 *           bool:bShow - show (true) or hide (false) the column

		 *           bool:bRedraw - redraw the table or not - default true

		 */

		this.fnSetColumnVis = function ( iCol, bShow, bRedraw )

		{

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			var i, iLen;

			var iColumns = oSettings.aoColumns.length;

			var nTd, anTds;

			

			/* No point in doing anything if we are requesting what is already true */

			if ( oSettings.aoColumns[iCol].bVisible == bShow )

			{

				return;

			}

			

			var nTrHead = $('>tr', oSettings.nTHead)[0];

			var nTrFoot = $('>tr', oSettings.nTFoot)[0];

			var anTheadTh = [];

			var anTfootTh = [];

			for ( i=0 ; i<iColumns ; i++ )

			{

				anTheadTh.push( oSettings.aoColumns[i].nTh );

				anTfootTh.push( oSettings.aoColumns[i].nTf );

			}

			

			/* Show the column */

			if ( bShow )

			{

				var iInsert = 0;

				for ( i=0 ; i<iCol ; i++ )

				{

					if ( oSettings.aoColumns[i].bVisible )

					{

						iInsert++;

					}

				}

				

				/* Need to decide if we should use appendChild or insertBefore */

				if ( iInsert >= _fnVisbleColumns( oSettings ) )

				{

					nTrHead.appendChild( anTheadTh[iCol] );

					if ( nTrFoot )

					{

						nTrFoot.appendChild( anTfootTh[iCol] );

					}

					

					for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )

					{

						nTd = oSettings.aoData[i]._anHidden[iCol];

						oSettings.aoData[i].nTr.appendChild( nTd );

					}

				}

				else

				{

					/* Which coloumn should we be inserting before? */

					var iBefore;

					for ( i=iCol ; i<iColumns ; i++ )

					{

						iBefore = _fnColumnIndexToVisible( oSettings, i );

						if ( iBefore !== null )

						{

							break;

						}

					}

					

					nTrHead.insertBefore( anTheadTh[iCol], nTrHead.getElementsByTagName('th')[iBefore] );

					if ( nTrFoot )

					{

						nTrFoot.insertBefore( anTfootTh[iCol], nTrFoot.getElementsByTagName('th')[iBefore] );

					}

					

					anTds = _fnGetTdNodes( oSettings );

					for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )

					{

						nTd = oSettings.aoData[i]._anHidden[iCol];

						oSettings.aoData[i].nTr.insertBefore( nTd, $('>td:eq('+iBefore+')', 

							oSettings.aoData[i].nTr)[0] );

					}

				}

				

				oSettings.aoColumns[iCol].bVisible = true;

			}

			else

			{

				/* Remove a column from display */

				nTrHead.removeChild( anTheadTh[iCol] );

				if ( nTrFoot )

				{

					nTrFoot.removeChild( anTfootTh[iCol] );

				}

				

				anTds = _fnGetTdNodes( oSettings );

				for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )

				{

					nTd = anTds[ ( i*oSettings.aoColumns.length) + (iCol*1) ];

					oSettings.aoData[i]._anHidden[iCol] = nTd;

					nTd.parentNode.removeChild( nTd );

				}

				

				oSettings.aoColumns[iCol].bVisible = false;

			}

			

			/* If there are any 'open' rows, then we need to alter the colspan for this col change */

			for ( i=0, iLen=oSettings.aoOpenRows.length ; i<iLen ; i++ )

			{

				oSettings.aoOpenRows[i].nTr.colSpan = _fnVisbleColumns( oSettings );

			}

			

			/* Do a redraw incase anything depending on the table columns needs it 

			 * (built-in: scrolling) 

			 */

			if ( typeof bRedraw == 'undefined' || bRedraw )

			{

				_fnAjustColumnSizing( oSettings );

				_fnDraw( oSettings );

			}

			

			_fnSaveState( oSettings );

		};

		

		/*

		 * Function: fnPageChange

		 * Purpose:  Change the pagination

		 * Returns:  -

		 * Inputs:   string:sAction - paging action to take: "first", "previous", "next" or "last"

		 *           bool:bRedraw - redraw the table or not - optional - default true

		 */

		this.fnPageChange = function ( sAction, bRedraw )

		{

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			_fnPageChange( oSettings, sAction );

			_fnCalculateEnd( oSettings );

			

			if ( typeof bRedraw == 'undefined' || bRedraw )

			{

				_fnDraw( oSettings );

			}

		};

		

		/*

		 * Function: fnDestroy

		 * Purpose:  Destructor for the DataTable

		 * Returns:  -

		 * Inputs:   -

		 */

		this.fnDestroy = function ( )

		{

			var oSettings = _fnSettingsFromNode( this[_oExt.iApiIndex] );

			var nOrig = oSettings.nTableWrapper.parentNode;

			var nBody = oSettings.nTBody;

			var i, iLen;

			

			/* Flag to note that the table is currently being destoryed - no action should be taken */

			oSettings.bDestroying = true;

			

			/* Restore hidden columns */

			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )

			{

				if ( oSettings.aoColumns[i].bVisible === false )

				{

					this.fnSetColumnVis( i, true );

				}

			}

			

			/* If there is an 'empty' indicator row, remove it */

			$('tbody>tr>td.'+oSettings.oClasses.sRowEmpty, oSettings.nTable).parent().remove();

			

			/* When scrolling we had to break the table up - restore it */

			if ( oSettings.nTable != oSettings.nTHead.parentNode )

			{

				$('>thead', oSettings.nTable).remove();

				oSettings.nTable.appendChild( oSettings.nTHead );

			}

			

			if ( oSettings.nTFoot && oSettings.nTable != oSettings.nTFoot.parentNode )

			{

				$('>tfoot', oSettings.nTable).remove();

				oSettings.nTable.appendChild( oSettings.nTFoot );

			}

			

			/* Remove the DataTables generated nodes, events and classes */

			oSettings.nTable.parentNode.removeChild( oSettings.nTable );

			$(oSettings.nTableWrapper).remove();

			

			oSettings.aaSorting = [];

			oSettings.aaSortingFixed = [];

			_fnSortingClasses( oSettings );

			

			$(_fnGetTrNodes( oSettings )).removeClass( oSettings.asStripClasses.join(' ') );

			

			if ( !oSettings.bJUI )

			{

				$('th', oSettings.nTHead).removeClass( [ _oExt.oStdClasses.sSortable,

					_oExt.oStdClasses.sSortableAsc,

					_oExt.oStdClasses.sSortableDesc,

					_oExt.oStdClasses.sSortableNone ].join(' ')

				);

			}

			else

			{

				$('th', oSettings.nTHead).removeClass( [ _oExt.oStdClasses.sSortable,

					_oExt.oJUIClasses.sSortableAsc,

					_oExt.oJUIClasses.sSortableDesc,

					_oExt.oJUIClasses.sSortableNone ].join(' ')

				);

				$('th span', oSettings.nTHead).remove();

			}

			

			/* Add the TR elements back into the table in their original order */

			nOrig.appendChild( oSettings.nTable );

			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )

			{

				nBody.appendChild( oSettings.aoData[i].nTr );

			}

			

			/* Restore the width of the original table */

			oSettings.nTable.style.width = _fnStringToCss(oSettings.sDestroyWidth);

			

			/* If the were originally odd/even type classes - then we add them back here. Note

			 * this is not fool proof (for example if not all rows as odd/even classes - but 

			 * it's a good effort without getting carried away

			 */

			$('>tr:even', nBody).addClass( oSettings.asDestoryStrips[0] );

			$('>tr:odd', nBody).addClass( oSettings.asDestoryStrips[1] );

			

			/* Remove the settings object from the settings array */

			for ( i=0, iLen=_aoSettings.length ; i<iLen ; i++ )

			{

				if ( _aoSettings[i] == oSettings )

				{

					_aoSettings.splice( i, 1 );

				}

			}

			

			/* End it all */

			oSettings = null;

		};

		

		/*

		 * Function: _fnAjustColumnSizing

		 * Purpose:  Update tale sizing based on content. This would most likely be used for scrolling

		 *   and will typically need a redraw after it.

		 * Returns:  -

		 * Inputs:   bool:bRedraw - redraw the table or not, you will typically want to - default true

		 */

		this.fnAdjustColumnSizing = function ( bRedraw )

		{

			_fnAjustColumnSizing( _fnSettingsFromNode( this[_oExt.iApiIndex] ) );

			

			if ( typeof bRedraw == 'undefined' || bRedraw )

			{

				this.fnDraw( false );

			}

		};

		

		/*

		 * Plugin API functions

		 * 

		 * This call will add the functions which are defined in _oExt.oApi to the

		 * DataTables object, providing a rather nice way to allow plug-in API functions. Note that

		 * this is done here, so that API function can actually override the built in API functions if

		 * required for a particular purpose.

		 */

		

		/*

		 * Function: _fnExternApiFunc

		 * Purpose:  Create a wrapper function for exporting an internal func to an external API func

		 * Returns:  function: - wrapped function

		 * Inputs:   string:sFunc - API function name

		 */

		function _fnExternApiFunc (sFunc)

		{

			return function() {

					var aArgs = [_fnSettingsFromNode(this[_oExt.iApiIndex])].concat( 

						Array.prototype.slice.call(arguments) );

					return _oExt.oApi[sFunc].apply( this, aArgs );

				};

		}

		

		for ( var sFunc in _oExt.oApi )

		{

			if ( sFunc )

			{

				/*

				 * Function: anon

				 * Purpose:  Wrap the plug-in API functions in order to provide the settings as 1st arg 

				 *   and execute in this scope

				 * Returns:  -

				 * Inputs:   -

				 */

				this[sFunc] = _fnExternApiFunc(sFunc);

			}

		}

		

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Local functions

		 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Initalisation

		 */

		

		/*

		 * Function: _fnInitalise

		 * Purpose:  Draw the table for the first time, adding all required features

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnInitalise ( oSettings )

		{

			var i, iLen;

			

			/* Ensure that the table data is fully initialised */

			if ( oSettings.bInitialised === false )

			{

				setTimeout( function(){ _fnInitalise( oSettings ); }, 200 );

				return;

			}

			

			/* Show the display HTML options */

			_fnAddOptionsHtml( oSettings );

			

			/* Draw the headers for the table */

			_fnDrawHead( oSettings );

			

			/* Okay to show that something is going on now */

			_fnProcessingDisplay( oSettings, true );

			

			/* Calculate sizes for columns */

			if ( oSettings.oFeatures.bAutoWidth )

			{

				_fnCalculateColumnWidths( oSettings );

			}

			

			for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )

			{

				if ( oSettings.aoColumns[i].sWidth !== null )

				{

					oSettings.aoColumns[i].nTh.style.width = _fnStringToCss( oSettings.aoColumns[i].sWidth );

				}

			}

			

			/* If there is default sorting required - let's do it. The sort function

			 * will do the drawing for us. Otherwise we draw the table

			 */

			if ( oSettings.oFeatures.bSort )

			{

				_fnSort( oSettings );

			}

			else

			{

				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();

				_fnCalculateEnd( oSettings );

				_fnDraw( oSettings );

			}

			

			/* if there is an ajax source */

			if ( oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide )

			{

				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, [], function(json) {

					/* Got the data - add it to the table */

					for ( i=0 ; i<json.aaData.length ; i++ )

					{

						_fnAddData( oSettings, json.aaData[i] );

					}

					

					/* Reset the init display for cookie saving. We've already done a filter, and

					 * therefore cleared it before. So we need to make it appear 'fresh'

					 */

					oSettings.iInitDisplayStart = oSettings._iDisplayStart;

					

					if ( oSettings.oFeatures.bSort )

					{

						_fnSort( oSettings );

					}

					else

					{

						oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();

						_fnCalculateEnd( oSettings );

						_fnDraw( oSettings );

					}

					

					_fnProcessingDisplay( oSettings, false );

					

					/* Run the init callback if there is one - done here for ajax source for json obj */

					if ( typeof oSettings.fnInitComplete == 'function' )

					{

						oSettings.fnInitComplete.call( oSettings.oInstance, oSettings, json );

					}

				} );

				return;

			}

			

			if ( !oSettings.oFeatures.bServerSide )

			{

				_fnProcessingDisplay( oSettings, false );

			}

		}

		

		/*

		 * Function: _fnLanguageProcess

		 * Purpose:  Copy language variables from remote object to a local one

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           object:oLanguage - Language information

		 *           bool:bInit - init once complete

		 */

		function _fnLanguageProcess( oSettings, oLanguage, bInit )

		{

			_fnMap( oSettings.oLanguage, oLanguage, 'sProcessing' );

			_fnMap( oSettings.oLanguage, oLanguage, 'sLengthMenu' );

			_fnMap( oSettings.oLanguage, oLanguage, 'sEmptyTable' );

			_fnMap( oSettings.oLanguage, oLanguage, 'sZeroRecords' );

			_fnMap( oSettings.oLanguage, oLanguage, 'sInfo' );

			_fnMap( oSettings.oLanguage, oLanguage, 'sInfoEmpty' );

			_fnMap( oSettings.oLanguage, oLanguage, 'sInfoFiltered' );

			_fnMap( oSettings.oLanguage, oLanguage, 'sInfoPostFix' );

			_fnMap( oSettings.oLanguage, oLanguage, 'sSearch' );

			

			if ( typeof oLanguage.oPaginate != 'undefined' )

			{

				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sFirst' );

				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sPrevious' );

				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sNext' );

				_fnMap( oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sLast' );

			}

			

			/* Backwards compatibility - if there is no sEmptyTable given, then use the same as

			 * sZeroRecords - assuming that is given.

			 */

			if ( typeof oLanguage.sEmptyTable == 'undefined' && 

			     typeof oLanguage.sZeroRecords != 'undefined' )

			{

				_fnMap( oSettings.oLanguage, oLanguage, 'sZeroRecords', 'sEmptyTable' );

			}

			

			if ( bInit )

			{

				_fnInitalise( oSettings );

			}

		}

		

		/*

		 * Function: _fnAddColumn

		 * Purpose:  Add a column to the list used for the table with default values

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           node:nTh - the th element for this column

		 */

		function _fnAddColumn( oSettings, nTh )

		{

			oSettings.aoColumns[ oSettings.aoColumns.length++ ] = {

				"sType": null,

				"_bAutoType": true,

				"bVisible": true,

				"bSearchable": true,

				"bSortable": true,

				"asSorting": [ 'asc', 'desc' ],

				"sSortingClass": oSettings.oClasses.sSortable,

				"sSortingClassJUI": oSettings.oClasses.sSortJUI,

				"sTitle": nTh ? nTh.innerHTML : '',

				"sName": '',

				"sWidth": null,

				"sWidthOrig": null,

				"sClass": null,

				"fnRender": null,

				"bUseRendered": true,

				"iDataSort": oSettings.aoColumns.length-1,

				"sSortDataType": 'std',

				"nTh": nTh ? nTh : document.createElement('th'),

				"nTf": null

			};

			

			var iCol = oSettings.aoColumns.length-1;

			var oCol = oSettings.aoColumns[ iCol ];

			

			/* Add a column specific filter */

			if ( typeof oSettings.aoPreSearchCols[ iCol ] == 'undefined' ||

			     oSettings.aoPreSearchCols[ iCol ] === null )

			{

				oSettings.aoPreSearchCols[ iCol ] = {

					"sSearch": "",

					"bRegex": false,

					"bSmart": true

				};

			}

			else

			{

				/* Don't require that the user must specify bRegex and / or bSmart */

				if ( typeof oSettings.aoPreSearchCols[ iCol ].bRegex == 'undefined' )

				{

					oSettings.aoPreSearchCols[ iCol ].bRegex = true;

				}

				

				if ( typeof oSettings.aoPreSearchCols[ iCol ].bSmart == 'undefined' )

				{

					oSettings.aoPreSearchCols[ iCol ].bSmart = true;

				}

			} 

			

			/* Use the column options function to initialise classes etc */

			_fnColumnOptions( oSettings, iCol, null );

		}

		

		/*

		 * Function: _fnColumnOptions

		 * Purpose:  Apply options for a column

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           int:iCol - column index to consider

		 *           object:oOptions - object with sType, bVisible and bSearchable

		 */

		function _fnColumnOptions( oSettings, iCol, oOptions )

		{

			var oCol = oSettings.aoColumns[ iCol ];

			

			/* User specified column options */

			if ( typeof oOptions != 'undefined' && oOptions !== null )

			{

				if ( typeof oOptions.sType != 'undefined' )

				{

					oCol.sType = oOptions.sType;

					oCol._bAutoType = false;

				}

				

				_fnMap( oCol, oOptions, "bVisible" );

				_fnMap( oCol, oOptions, "bSearchable" );

				_fnMap( oCol, oOptions, "bSortable" );

				_fnMap( oCol, oOptions, "sTitle" );

				_fnMap( oCol, oOptions, "sName" );

				_fnMap( oCol, oOptions, "sWidth" );

				_fnMap( oCol, oOptions, "sWidth", "sWidthOrig" );

				_fnMap( oCol, oOptions, "sClass" );

				_fnMap( oCol, oOptions, "fnRender" );

				_fnMap( oCol, oOptions, "bUseRendered" );

				_fnMap( oCol, oOptions, "iDataSort" );

				_fnMap( oCol, oOptions, "asSorting" );

				_fnMap( oCol, oOptions, "sSortDataType" );

			}

			

			/* Feature sorting overrides column specific when off */

			if ( !oSettings.oFeatures.bSort )

			{

				oCol.bSortable = false;

			}

			

			/* Check that the class assignment is correct for sorting */

			if ( !oCol.bSortable ||

					 ($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1) )

			{

				oCol.sSortingClass = oSettings.oClasses.sSortableNone;

				oCol.sSortingClassJUI = "";

			}

			else if ( $.inArray('asc', oCol.asSorting) != -1 && $.inArray('desc', oCol.asSorting) == -1 )

			{

				oCol.sSortingClass = oSettings.oClasses.sSortableAsc;

				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIAscAllowed;

			}

			else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) != -1 )

			{

				oCol.sSortingClass = oSettings.oClasses.sSortableDesc;

				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIDescAllowed;

			}

		}

		

		/*

		 * Function: _fnAddData

		 * Purpose:  Add a data array to the table, creating DOM node etc

		 * Returns:  int: - >=0 if successful (index of new aoData entry), -1 if failed

		 * Inputs:   object:oSettings - dataTables settings object

		 *           array:aData - data array to be added

		 * Notes:    There are two basic methods for DataTables to get data to display - a JS array

		 *   (which is dealt with by this function), and the DOM, which has it's own optimised

		 *   function (_fnGatherData). Be careful to make the same changes here as there and vice-versa

		 */

		function _fnAddData ( oSettings, aDataSupplied )

		{

			/* Sanity check the length of the new array */

			if ( aDataSupplied.length != oSettings.aoColumns.length &&

				oSettings.iDrawError != oSettings.iDraw )

			{

				_fnLog( oSettings, 0, "Added data (size "+aDataSupplied.length+") does not match known "+

					"number of columns ("+oSettings.aoColumns.length+")" );

				oSettings.iDrawError = oSettings.iDraw;

				return -1;

			}

			

			

			/* Create the object for storing information about this new row */

			var aData = aDataSupplied.slice();

			var iThisIndex = oSettings.aoData.length;

			oSettings.aoData.push( {

				"nTr": document.createElement('tr'),

				"_iId": oSettings.iNextId++,

				"_aData": aData,

				"_anHidden": [],

				"_sRowStripe": ''

			} );

			

			/* Create the cells */

			var nTd, sThisType;

			for ( var i=0 ; i<aData.length ; i++ )

			{

				nTd = document.createElement('td');

				

				/* Allow null data (from a data array) - simply deal with it as a blank string */

				if ( aData[i] === null )

				{

					aData[i] = '';

				}

				

				if ( typeof oSettings.aoColumns[i].fnRender == 'function' )

				{

					var sRendered = oSettings.aoColumns[i].fnRender( {

							"iDataRow": iThisIndex,

							"iDataColumn": i,

							"aData": aData,

							"oSettings": oSettings

						} );

					nTd.innerHTML = sRendered;

					if ( oSettings.aoColumns[i].bUseRendered )

					{

						/* Use the rendered data for filtering/sorting */

						oSettings.aoData[iThisIndex]._aData[i] = sRendered;

					}

				}

				else

				{

					nTd.innerHTML = aData[i];

				}

				

				/* Cast everything as a string - so we can treat everything equally when sorting */

				if ( typeof aData[i] != 'string' )

				{

					aData[i] += "";

				}

				aData[i] = $.trim(aData[i]);

				

				/* Add user defined class */

				if ( oSettings.aoColumns[i].sClass !== null )

				{

					nTd.className = oSettings.aoColumns[i].sClass;

				}

				

				/* See if we should auto-detect the column type */

				if ( oSettings.aoColumns[i]._bAutoType && oSettings.aoColumns[i].sType != 'string' )

				{

					/* Attempt to auto detect the type - same as _fnGatherData() */

					sThisType = _fnDetectType( oSettings.aoData[iThisIndex]._aData[i] );

					if ( oSettings.aoColumns[i].sType === null )

					{

						oSettings.aoColumns[i].sType = sThisType;

					}

					else if ( oSettings.aoColumns[i].sType != sThisType )

					{

						/* String is always the 'fallback' option */

						oSettings.aoColumns[i].sType = 'string';

					}

				}

					

				if ( oSettings.aoColumns[i].bVisible )

				{

					oSettings.aoData[iThisIndex].nTr.appendChild( nTd );

					oSettings.aoData[iThisIndex]._anHidden[i] = null;

				}

				else

				{

					oSettings.aoData[iThisIndex]._anHidden[i] = nTd;

				}

			}

			

			/* Add to the display array */

			oSettings.aiDisplayMaster.push( iThisIndex );

			return iThisIndex;

		}

		

		/*

		 * Function: _fnGatherData

		 * Purpose:  Read in the data from the target table from the DOM

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 * Notes:    This is a optimised version of _fnAddData (more or less) for reading information

		 *   from the DOM. The basic actions must be identical in the two functions.

		 */

		function _fnGatherData( oSettings )

		{

			var iLoop, i, iLen, j, jLen, jInner,

			 	nTds, nTrs, nTd, aLocalData, iThisIndex,

				iRow, iRows, iColumn, iColumns;

			

			/*

			 * Process by row first

			 * Add the data object for the whole table - storing the tr node. Note - no point in getting

			 * DOM based data if we are going to go and replace it with Ajax source data.

			 */

			if ( oSettings.sAjaxSource === null )

			{

				nTrs = oSettings.nTBody.childNodes;

				for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )

				{

					if ( nTrs[i].nodeName.toUpperCase() == "TR" )

					{

						iThisIndex = oSettings.aoData.length;

						oSettings.aoData.push( {

							"nTr": nTrs[i],

							"_iId": oSettings.iNextId++,

							"_aData": [],

							"_anHidden": [],

							"_sRowStripe": ''

						} );

						

						oSettings.aiDisplayMaster.push( iThisIndex );

						

						aLocalData = oSettings.aoData[iThisIndex]._aData;

						nTds = nTrs[i].childNodes;

						jInner = 0;

						

						for ( j=0, jLen=nTds.length ; j<jLen ; j++ )

						{

							if ( nTds[j].nodeName.toUpperCase() == "TD" )

							{

								aLocalData[jInner] = $.trim(nTds[j].innerHTML);

								jInner++;

							}

						}

					}

				}

			}

			

			/* Gather in the TD elements of the Table - note that this is basically the same as

			 * fnGetTdNodes, but that function takes account of hidden columns, which we haven't yet

			 * setup!

			 */

			nTrs = _fnGetTrNodes( oSettings );

			nTds = [];

			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )

			{

				for ( j=0, jLen=nTrs[i].childNodes.length ; j<jLen ; j++ )

				{

					nTd = nTrs[i].childNodes[j];

					if ( nTd.nodeName.toUpperCase() == "TD" )

					{

						nTds.push( nTd );

					}

				}

			}

			

			/* Sanity check */

			if ( nTds.length != nTrs.length * oSettings.aoColumns.length )

			{

				_fnLog( oSettings, 1, "Unexpected number of TD elements. Expected "+

					(nTrs.length * oSettings.aoColumns.length)+" and got "+nTds.length+". DataTables does "+

					"not support rowspan / colspan in the table body, and there must be one cell for each "+

					"row/column combination." );

			}

			

			/* Now process by column */

			for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )

			{

				/* Get the title of the column - unless there is a user set one */

				if ( oSettings.aoColumns[iColumn].sTitle === null )

				{

					oSettings.aoColumns[iColumn].sTitle = oSettings.aoColumns[iColumn].nTh.innerHTML;

				}

				

				var

					bAutoType = oSettings.aoColumns[iColumn]._bAutoType,

					bRender = typeof oSettings.aoColumns[iColumn].fnRender == 'function',

					bClass = oSettings.aoColumns[iColumn].sClass !== null,

					bVisible = oSettings.aoColumns[iColumn].bVisible,

					nCell, sThisType, sRendered;

				

				/* A single loop to rule them all (and be more efficient) */

				if ( bAutoType || bRender || bClass || !bVisible )

				{

					for ( iRow=0, iRows=oSettings.aoData.length ; iRow<iRows ; iRow++ )

					{

						nCell = nTds[ (iRow*iColumns) + iColumn ];

						

						/* Type detection */

						if ( bAutoType )

						{

							if ( oSettings.aoColumns[iColumn].sType != 'string' )

							{

								sThisType = _fnDetectType( oSettings.aoData[iRow]._aData[iColumn] );

								if ( oSettings.aoColumns[iColumn].sType === null )

								{

									oSettings.aoColumns[iColumn].sType = sThisType;

								}

								else if ( oSettings.aoColumns[iColumn].sType != sThisType )

								{

									/* String is always the 'fallback' option */

									oSettings.aoColumns[iColumn].sType = 'string';

								}

							}

						}

						

						/* Rendering */

						if ( bRender )

						{

							sRendered = oSettings.aoColumns[iColumn].fnRender( {

									"iDataRow": iRow,

									"iDataColumn": iColumn,

									"aData": oSettings.aoData[iRow]._aData,

									"oSettings": oSettings

								} );

							nCell.innerHTML = sRendered;

							if ( oSettings.aoColumns[iColumn].bUseRendered )

							{

								/* Use the rendered data for filtering/sorting */

								oSettings.aoData[iRow]._aData[iColumn] = sRendered;

							}

						}

						

						/* Classes */

						if ( bClass )

						{

							nCell.className += ' '+oSettings.aoColumns[iColumn].sClass;

						}

						

						/* Column visability */

						if ( !bVisible )

						{

							oSettings.aoData[iRow]._anHidden[iColumn] = nCell;

							nCell.parentNode.removeChild( nCell );

						}

						else

						{

							oSettings.aoData[iRow]._anHidden[iColumn] = null;

						}

					}

				}

			}

		}

		

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Drawing functions

		 */

		

		/*

		 * Function: _fnDrawHead

		 * Purpose:  Create the HTML header for the table

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnDrawHead( oSettings )

		{

			var i, nTh, iLen, j, jLen;

			var iThs = oSettings.nTHead.getElementsByTagName('th').length;

			var iCorrector = 0;

			

			/* If there is a header in place - then use it - otherwise it's going to get nuked... */

			if ( iThs !== 0 )

			{

				/* We've got a thead from the DOM, so remove hidden columns and apply width to vis cols */

				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )

				{

					nTh = oSettings.aoColumns[i].nTh;

					

					if ( oSettings.aoColumns[i].bVisible )

					{

						/* Set the title of the column if it is user defined (not what was auto detected) */

						if ( oSettings.aoColumns[i].sTitle != nTh.innerHTML )

						{

							nTh.innerHTML = oSettings.aoColumns[i].sTitle;

						}

					}

					else

					{

						nTh.parentNode.removeChild( nTh );

						iCorrector++;

					}

				}

			}

			else

			{

				/* We don't have a header in the DOM - so we are going to have to create one */

				var nTr = document.createElement( "tr" );

				

				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )

				{

					nTh = oSettings.aoColumns[i].nTh;

					nTh.innerHTML = oSettings.aoColumns[i].sTitle;

					

					if ( oSettings.aoColumns[i].bVisible )

					{

						if ( oSettings.aoColumns[i].sClass !== null )

						{

							nTh.className = oSettings.aoColumns[i].sClass;

						}

						

						nTr.appendChild( nTh );

					}

				}

				$(oSettings.nTHead).html( '' )[0].appendChild( nTr );

			}

			

			/* Add the extra markup needed by jQuery UI's themes */

			if ( oSettings.bJUI )

			{

				for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )

				{

					nTh = oSettings.aoColumns[i].nTh;

					

					var nDiv = document.createElement('div');

					nDiv.className = oSettings.oClasses.sSortJUIWrapper;

					$(nTh).contents().appendTo(nDiv);

					

					nDiv.appendChild( document.createElement('span') );

					nTh.appendChild( nDiv );

				}

			}

			

			/* Add sort listener */

			var fnNoSelect = function (e) {

				this.onselectstart = function() { return false; };

				return false;

			};

			

			if ( oSettings.oFeatures.bSort )

			{

				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )

				{

					if ( oSettings.aoColumns[i].bSortable !== false )

					{

						_fnSortAttachListener( oSettings, oSettings.aoColumns[i].nTh, i );

						

						/* Take the brutal approach to cancelling text selection in header */

						$(oSettings.aoColumns[i].nTh).mousedown( fnNoSelect );

					}

					else

					{

						$(oSettings.aoColumns[i].nTh).addClass( oSettings.oClasses.sSortableNone );

					}

				}

			}

			

			/* Cache the footer elements */

			if ( oSettings.nTFoot !== null )

			{

				iCorrector = 0;

				var nTfs = oSettings.nTFoot.getElementsByTagName('th');

				for ( i=0, iLen=nTfs.length ; i<iLen ; i++ )

				{

					if ( typeof oSettings.aoColumns[i] != 'undefined' )

					{

						oSettings.aoColumns[i].nTf = nTfs[i-iCorrector];

						

						if ( oSettings.oClasses.sFooterTH !== "" )

						{

							oSettings.aoColumns[i].nTf.className += " "+oSettings.oClasses.sFooterTH;

						}

						

						if ( !oSettings.aoColumns[i].bVisible )

						{

							nTfs[i-iCorrector].parentNode.removeChild( nTfs[i-iCorrector] );

							iCorrector++;

						}

					}

				}

			}

		}

		

		/*

		 * Function: _fnDraw

		 * Purpose:  Insert the required TR nodes into the table for display

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnDraw( oSettings )

		{

			var i, iLen;

			var anRows = [];

			var iRowCount = 0;

			var bRowError = false;

			var iStrips = oSettings.asStripClasses.length;

			var iOpenRows = oSettings.aoOpenRows.length;

			

			oSettings.bDrawing = true;

			

			/* Check and see if we have an initial draw position from state saving */

			if ( typeof oSettings.iInitDisplayStart != 'undefined' && oSettings.iInitDisplayStart != -1 )

			{

				if ( oSettings.oFeatures.bServerSide )

				{

					oSettings._iDisplayStart = oSettings.iInitDisplayStart;

				}

				else

				{

					oSettings._iDisplayStart = (oSettings.iInitDisplayStart >= oSettings.fnRecordsDisplay()) ?

						0 : oSettings.iInitDisplayStart;

				}

				oSettings.iInitDisplayStart = -1;

				_fnCalculateEnd( oSettings );

			}

			

			/* If we are dealing with Ajax - do it here */

			if ( oSettings.oFeatures.bServerSide && 

			     !_fnAjaxUpdate( oSettings ) )

			{

				return;

			}

			else if ( !oSettings.oFeatures.bServerSide )

			{

				oSettings.iDraw++;

			}

			

			if ( oSettings.aiDisplay.length !== 0 )

			{

				var iStart = oSettings._iDisplayStart;

				var iEnd = oSettings._iDisplayEnd;

				

				if ( oSettings.oFeatures.bServerSide )

				{

					iStart = 0;

					iEnd = oSettings.aoData.length;

				}

				

				for ( var j=iStart ; j<iEnd ; j++ )

				{

					var aoData = oSettings.aoData[ oSettings.aiDisplay[j] ];

					var nRow = aoData.nTr;

					

					/* Remove the old stripping classes and then add the new one */

					if ( iStrips !== 0 )

					{

						var sStrip = oSettings.asStripClasses[ iRowCount % iStrips ];

						if ( aoData._sRowStripe != sStrip )

						{

							$(nRow).removeClass( aoData._sRowStripe ).addClass( sStrip );

							aoData._sRowStripe = sStrip;

						}

					}

					

					/* Custom row callback function - might want to manipule the row */

					if ( typeof oSettings.fnRowCallback == "function" )

					{

						nRow = oSettings.fnRowCallback.call( oSettings.oInstance, nRow, 

							oSettings.aoData[ oSettings.aiDisplay[j] ]._aData, iRowCount, j );

						if ( !nRow && !bRowError )

						{

							_fnLog( oSettings, 0, "A node was not returned by fnRowCallback" );

							bRowError = true;

						}

					}

					

					anRows.push( nRow );

					iRowCount++;

					

					/* If there is an open row - and it is attached to this parent - attach it on redraw */

					if ( iOpenRows !== 0 )

					{

						for ( var k=0 ; k<iOpenRows ; k++ )

						{

							if ( nRow == oSettings.aoOpenRows[k].nParent )

							{

								anRows.push( oSettings.aoOpenRows[k].nTr );

							}

						}

					}

				}

			}

			else

			{

				/* Table is empty - create a row with an empty message in it */

				anRows[ 0 ] = document.createElement( 'tr' );

				

				if ( typeof oSettings.asStripClasses[0] != 'undefined' )

				{

					anRows[ 0 ].className = oSettings.asStripClasses[0];

				}

				

				var nTd = document.createElement( 'td' );

				nTd.setAttribute( 'valign', "top" );

				nTd.colSpan = _fnVisbleColumns( oSettings );

				nTd.className = oSettings.oClasses.sRowEmpty;

				if ( typeof oSettings.oLanguage.sEmptyTable != 'undefined' &&

				     oSettings.fnRecordsTotal() === 0 )

				{

					nTd.innerHTML = oSettings.oLanguage.sEmptyTable;

				}

				else

				{

					nTd.innerHTML = oSettings.oLanguage.sZeroRecords.replace(

						'_MAX_', oSettings.fnFormatNumber(oSettings.fnRecordsTotal()) );

				}

				

				anRows[ iRowCount ].appendChild( nTd );

			}

			

			/* Callback the header and footer custom funcation if there is one */

			if ( typeof oSettings.fnHeaderCallback == 'function' )

			{

				oSettings.fnHeaderCallback.call( oSettings.oInstance, $('>tr', oSettings.nTHead)[0], 

					_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(),

					oSettings.aiDisplay );

			}

			

			if ( typeof oSettings.fnFooterCallback == 'function' )

			{

				oSettings.fnFooterCallback.call( oSettings.oInstance, $('>tr', oSettings.nTFoot)[0], 

					_fnGetDataMaster( oSettings ), oSettings._iDisplayStart, oSettings.fnDisplayEnd(),

					oSettings.aiDisplay );

			}

			

			/* 

			 * Need to remove any old row from the display - note we can't just empty the tbody using

			 * $().html('') since this will unbind the jQuery event handlers (even although the node 

			 * still exists!) - equally we can't use innerHTML, since IE throws an exception.

			 */

			var

				nAddFrag = document.createDocumentFragment(),

				nRemoveFrag = document.createDocumentFragment(),

				nBodyPar, nTrs;

			

			if ( oSettings.nTBody )

			{

				nBodyPar = oSettings.nTBody.parentNode;

				nRemoveFrag.appendChild( oSettings.nTBody );

				

				/* When doing infinite scrolling, only remove child rows when sorting, filtering or start

				 * up. When not infinite scroll, always do it.

				 */

				if ( !oSettings.oScroll.bInfinite || !oSettings._bInitComplete ||

				 	oSettings.bSorted || oSettings.bFiltered )

				{

					nTrs = oSettings.nTBody.childNodes;

					for ( i=nTrs.length-1 ; i>=0 ; i-- )

					{

						nTrs[i].parentNode.removeChild( nTrs[i] );

					}

				}

				

				/* Put the draw table into the dom */

				for ( i=0, iLen=anRows.length ; i<iLen ; i++ )

				{

					nAddFrag.appendChild( anRows[i] );

				}

				

				oSettings.nTBody.appendChild( nAddFrag );

				if ( nBodyPar !== null )

				{

					nBodyPar.appendChild( oSettings.nTBody );

				}

			}

			

			/* Call all required callback functions for the end of a draw */

			for ( i=0, iLen=oSettings.aoDrawCallback.length ; i<iLen ; i++ )

			{

				oSettings.aoDrawCallback[i].fn.call( oSettings.oInstance, oSettings );

			}

			

			/* Draw is complete, sorting and filtering must be as well */

			oSettings.bSorted = false;

			oSettings.bFiltered = false;

			oSettings.bDrawing = false;

				

			/* On first draw, initilaisation is now complete */

			if ( typeof oSettings._bInitComplete == "undefined" )

			{

				oSettings._bInitComplete = true;

				

				if ( typeof oSettings.fnInitComplete == 'function' &&

					   (oSettings.oFeatures.bServerSide || oSettings.sAjaxSource === null) )

				{

					oSettings.fnInitComplete.call( oSettings.oInstance, oSettings );

				}

			}

		}

		

		/*

		 * Function: _fnReDraw

		 * Purpose:  Redraw the table - taking account of the various features which are enabled

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnReDraw( oSettings )

		{

			if ( oSettings.oFeatures.bSort )

			{

				/* Sorting will refilter and draw for us */

				_fnSort( oSettings, oSettings.oPreviousSearch );

			}

			else if ( oSettings.oFeatures.bFilter )

			{

				/* Filtering will redraw for us */

				_fnFilterComplete( oSettings, oSettings.oPreviousSearch );

			}

			else

			{

				_fnCalculateEnd( oSettings );

				_fnDraw( oSettings );

			}

		}

		

		/*

		 * Function: _fnAjaxUpdate

		 * Purpose:  Update the table using an Ajax call

		 * Returns:  bool: block the table drawing or not

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnAjaxUpdate( oSettings )

		{

			if ( oSettings.bAjaxDataGet )

			{

				_fnProcessingDisplay( oSettings, true );

				var iColumns = oSettings.aoColumns.length;

				var aoData = [];

				var i;

				

				/* Paging and general */

				oSettings.iDraw++;

				aoData.push( { "name": "sEcho",          "value": oSettings.iDraw } );

				aoData.push( { "name": "iColumns",       "value": iColumns } );

				aoData.push( { "name": "sColumns",       "value": _fnColumnOrdering(oSettings) } );

				aoData.push( { "name": "iDisplayStart",  "value": oSettings._iDisplayStart } );

				aoData.push( { "name": "iDisplayLength", "value": oSettings.oFeatures.bPaginate !== false ?

					oSettings._iDisplayLength : -1 } );

				

				/* Column names */

				var aNames = [];

				for ( i=0 ; i<iColumns ; i++ )

				{

					aNames.push( oSettings.aoColumns[i].sName );

				}

				aoData.push( { "name": "sNames", "value": aNames.join(',') } );

				

				/* Filtering */

				if ( oSettings.oFeatures.bFilter !== false )

				{

					aoData.push( { "name": "sSearch", "value": oSettings.oPreviousSearch.sSearch } );

					aoData.push( { "name": "bRegex",  "value": oSettings.oPreviousSearch.bRegex } );

					for ( i=0 ; i<iColumns ; i++ )

					{

						aoData.push( { "name": "sSearch_"+i,     "value": oSettings.aoPreSearchCols[i].sSearch } );

						aoData.push( { "name": "bRegex_"+i,      "value": oSettings.aoPreSearchCols[i].bRegex } );

						aoData.push( { "name": "bSearchable_"+i, "value": oSettings.aoColumns[i].bSearchable } );

					}

				}

				

				/* Sorting */

				if ( oSettings.oFeatures.bSort !== false )

				{

					var iFixed = oSettings.aaSortingFixed !== null ? oSettings.aaSortingFixed.length : 0;

					var iUser = oSettings.aaSorting.length;

					aoData.push( { "name": "iSortingCols",   "value": iFixed+iUser } );

					for ( i=0 ; i<iFixed ; i++ )

					{

						aoData.push( { "name": "iSortCol_"+i,  "value": oSettings.aaSortingFixed[i][0] } );

						aoData.push( { "name": "sSortDir_"+i,  "value": oSettings.aaSortingFixed[i][1] } );

					}

					

					for ( i=0 ; i<iUser ; i++ )

					{

						aoData.push( { "name": "iSortCol_"+(i+iFixed),  "value": oSettings.aaSorting[i][0] } );

						aoData.push( { "name": "sSortDir_"+(i+iFixed),  "value": oSettings.aaSorting[i][1] } );

					}

					

					for ( i=0 ; i<iColumns ; i++ )

					{

						aoData.push( { "name": "bSortable_"+i,  "value": oSettings.aoColumns[i].bSortable } );

					}

				}

				

				oSettings.fnServerData.call( oSettings.oInstance, oSettings.sAjaxSource, aoData,

					function(json) {

						_fnAjaxUpdateDraw( oSettings, json );

					} );

				return false;

			}

			else

			{

				return true;

			}

		}

		

		/*

		 * Function: _fnAjaxUpdateDraw

		 * Purpose:  Data the data from the server (nuking the old) and redraw the table

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           object:json - json data return from the server.

		 *             The following must be defined:

		 *               iTotalRecords, iTotalDisplayRecords, aaData

		 *             The following may be defined:

		 *               sColumns

		 */

		function _fnAjaxUpdateDraw ( oSettings, json )

		{

			if ( typeof json.sEcho != 'undefined' )

			{

				/* Protect against old returns over-writing a new one. Possible when you get

				 * very fast interaction, and later queires are completed much faster

				 */

				if ( json.sEcho*1 < oSettings.iDraw )

				{

					return;

				}

				else

				{

					oSettings.iDraw = json.sEcho * 1;

				}

			}

			

			if ( !oSettings.oScroll.bInfinite ||

				   (oSettings.oScroll.bInfinite && (oSettings.bSorted || oSettings.bFiltered)) )

			{

				_fnClearTable( oSettings );

			}

			oSettings._iRecordsTotal = json.iTotalRecords;

			oSettings._iRecordsDisplay = json.iTotalDisplayRecords;

			

			/* Determine if reordering is required */

			var sOrdering = _fnColumnOrdering(oSettings);

			var bReOrder = (typeof json.sColumns != 'undefined' && sOrdering !== "" && json.sColumns != sOrdering );

			if ( bReOrder )

			{

				var aiIndex = _fnReOrderIndex( oSettings, json.sColumns );

			}

			

			for ( var i=0, iLen=json.aaData.length ; i<iLen ; i++ )

			{

				if ( bReOrder )

				{

					/* If we need to re-order, then create a new array with the correct order and add it */

					var aData = [];

					for ( var j=0, jLen=oSettings.aoColumns.length ; j<jLen ; j++ )

					{

						aData.push( json.aaData[i][ aiIndex[j] ] );

					}

					_fnAddData( oSettings, aData );

				}

				else

				{

					/* No re-order required, sever got it "right" - just straight add */

					_fnAddData( oSettings, json.aaData[i] );

				}

			}

			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();

			

			oSettings.bAjaxDataGet = false;

			_fnDraw( oSettings );

			oSettings.bAjaxDataGet = true;

			_fnProcessingDisplay( oSettings, false );

		}

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Options (features) HTML

		 */

		

		/*

		 * Function: _fnAddOptionsHtml

		 * Purpose:  Add the options to the page HTML for the table

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnAddOptionsHtml ( oSettings )

		{

			/*

			 * Create a temporary, empty, div which we can later on replace with what we have generated

			 * we do it this way to rendering the 'options' html offline - speed :-)

			 */

			var nHolding = document.createElement( 'div' );

			oSettings.nTable.parentNode.insertBefore( nHolding, oSettings.nTable );

			

			/* 

			 * All DataTables are wrapped in a div - this is not currently optional - backwards 

			 * compatability. It can be removed if you don't want it.

			 */

			oSettings.nTableWrapper = document.createElement( 'div' );

			oSettings.nTableWrapper.className = oSettings.oClasses.sWrapper;

			if ( oSettings.sTableId !== '' )

			{

				oSettings.nTableWrapper.setAttribute( 'id', oSettings.sTableId+'_wrapper' );

			}

			

			/* Track where we want to insert the option */

			var nInsertNode = oSettings.nTableWrapper;

			

			/* Loop over the user set positioning and place the elements as needed */

			var aDom = oSettings.sDom.split('');

			var nTmp, iPushFeature, cOption, nNewNode, cNext, sAttr, j;

			for ( var i=0 ; i<aDom.length ; i++ )

			{

				iPushFeature = 0;

				cOption = aDom[i];

				

				if ( cOption == '<' )

				{

					/* New container div */

					nNewNode = document.createElement( 'div' );

					

					/* Check to see if we should append an id and/or a class name to the container */

					cNext = aDom[i+1];

					if ( cNext == "'" || cNext == '"' )

					{

						sAttr = "";

						j = 2;

						while ( aDom[i+j] != cNext )

						{

							sAttr += aDom[i+j];

							j++;

						}

						

						/* Replace jQuery UI constants */

						if ( sAttr == "H" )

						{

							sAttr = "fg-toolbar ui-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix";

						}

						else if ( sAttr == "F" )

						{

							sAttr = "fg-toolbar ui-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix";

						}

						

						/* The attribute can be in the format of "#id.class", "#id" or "class" This logic

						 * breaks the string into parts and applies them as needed

						 */

						if ( sAttr.indexOf('.') != -1 )

						{

							var aSplit = sAttr.split('.');

							nNewNode.setAttribute('id', aSplit[0].substr(1, aSplit[0].length-1) );

							nNewNode.className = aSplit[1];

						}

						else if ( sAttr.charAt(0) == "#" )

						{

							nNewNode.setAttribute('id', sAttr.substr(1, sAttr.length-1) );

						}

						else

						{

							nNewNode.className = sAttr;

						}

						

						i += j; /* Move along the position array */

					}

					

					nInsertNode.appendChild( nNewNode );

					nInsertNode = nNewNode;

				}

				else if ( cOption == '>' )

				{

					/* End container div */

					nInsertNode = nInsertNode.parentNode;

				}

				else if ( cOption == 'l' && oSettings.oFeatures.bPaginate && oSettings.oFeatures.bLengthChange )

				{

					/* Length */

					nTmp = _fnFeatureHtmlLength( oSettings );

					iPushFeature = 1;

				}

				else if ( cOption == 'f' && oSettings.oFeatures.bFilter )

				{

					/* Filter */

					nTmp = _fnFeatureHtmlFilter( oSettings );

					iPushFeature = 1;

				}

				else if ( cOption == 'r' && oSettings.oFeatures.bProcessing )

				{

					/* pRocessing */

					nTmp = _fnFeatureHtmlProcessing( oSettings );

					iPushFeature = 1;

				}

				else if ( cOption == 't' )

				{

					/* Table */

					nTmp = _fnFeatureHtmlTable( oSettings );

					iPushFeature = 1;

				}

				else if ( cOption ==  'i' && oSettings.oFeatures.bInfo )

				{

					/* Info */

					nTmp = _fnFeatureHtmlInfo( oSettings );

					iPushFeature = 1;

				}

				else if ( cOption == 'p' && oSettings.oFeatures.bPaginate )

				{

					/* Pagination */

					nTmp = _fnFeatureHtmlPaginate( oSettings );

					iPushFeature = 1;

				}

				else if ( _oExt.aoFeatures.length !== 0 )

				{

					/* Plug-in features */

					var aoFeatures = _oExt.aoFeatures;

					for ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ )

					{

						if ( cOption == aoFeatures[k].cFeature )

						{

							nTmp = aoFeatures[k].fnInit( oSettings );

							if ( nTmp )

							{

								iPushFeature = 1;

							}

							break;

						}

					}

				}

				

				/* Add to the 2D features array */

				if ( iPushFeature == 1 && nTmp !== null )

				{

					if ( typeof oSettings.aanFeatures[cOption] != 'object' )

					{

						oSettings.aanFeatures[cOption] = [];

					}

					oSettings.aanFeatures[cOption].push( nTmp );

					nInsertNode.appendChild( nTmp );

				}

			}

			

			/* Built our DOM structure - replace the holding div with what we want */

			nHolding.parentNode.replaceChild( oSettings.nTableWrapper, nHolding );

		}

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Feature: Filtering

		 */

		

		/*

		 * Function: _fnFeatureHtmlTable

		 * Purpose:  Add any control elements for the table - specifically scrolling

		 * Returns:  node: - Node to add to the DOM

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnFeatureHtmlTable ( oSettings )

		{

			/* Chack if scrolling is enabled or not - if not then leave the DOM unaltered */

			if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "" )

			{

				return oSettings.nTable;

			}

			

			/*

			 * The HTML structure that we want to generate in this function is:

			 *  div - nScroller

			 *    div - nScrollHead

			 *      div - nScrollHeadInner

			 *        table - nScrollHeadTable

			 *          thead - nThead

			 *    div - nScrollBody

			 *      table - oSettings.nTable

			 *        thead - nTheadSize

			 *        tbody - nTbody

			 *    div - nScrollFoot

			 *      div - nScrollFootInner

			 *        table - nScrollFootTable

			 *          tfoot - nTfoot

			 */

			var

			 	nScroller = document.createElement('div'),

			 	nScrollHead = document.createElement('div'),

			 	nScrollHeadInner = document.createElement('div'),

			 	nScrollBody = document.createElement('div'),

			 	nScrollFoot = document.createElement('div'),

			 	nScrollFootInner = document.createElement('div'),

			 	nScrollHeadTable = oSettings.nTable.cloneNode(false),

			 	nScrollFootTable = oSettings.nTable.cloneNode(false),

				nThead = oSettings.nTable.getElementsByTagName('thead')[0],

			 	nTfoot = oSettings.nTable.getElementsByTagName('tfoot').length === 0 ? null : 

					oSettings.nTable.getElementsByTagName('tfoot')[0],

				oClasses = (typeof oInit.bJQueryUI != 'undefined' && oInit.bJQueryUI) ?

			 		_oExt.oJUIClasses : _oExt.oStdClasses;

			

			nScrollHead.appendChild( nScrollHeadInner );

			nScrollFoot.appendChild( nScrollFootInner );

			nScrollBody.appendChild( oSettings.nTable );

			nScroller.appendChild( nScrollHead );

			nScroller.appendChild( nScrollBody );

			nScrollHeadInner.appendChild( nScrollHeadTable );

			nScrollHeadTable.appendChild( nThead );

			if ( nTfoot !== null )

			{

				nScroller.appendChild( nScrollFoot );

				nScrollFootInner.appendChild( nScrollFootTable );

				nScrollFootTable.appendChild( nTfoot );

			}

			

			nScroller.className = oClasses.sScrollWrapper;

			nScrollHead.className = oClasses.sScrollHead;

			nScrollHeadInner.className = oClasses.sScrollHeadInner;

			nScrollBody.className = oClasses.sScrollBody;

			nScrollFoot.className = oClasses.sScrollFoot;

			nScrollFootInner.className = oClasses.sScrollFootInner;

			

			nScrollHead.style.overflow = "hidden";

			nScrollHead.style.position = "relative";

			nScrollFoot.style.overflow = "hidden";

			nScrollBody.style.overflow = "auto";

			nScrollHead.style.border = "0";

			nScrollFoot.style.border = "0";

			nScrollHeadInner.style.width = "150%"; /* will be overwritten */

			

			/* Modify attributes to respect the clones */

			nScrollHeadTable.removeAttribute('id');

			nScrollHeadTable.style.marginLeft = "0";

			oSettings.nTable.style.marginLeft = "0";

			if ( nTfoot !== null )

			{

				nScrollFootTable.removeAttribute('id');

				nScrollFootTable.style.marginLeft = "0";

			}

			

			/* Move any caption elements from the body to the header */

			var nCaptions = $('>caption', oSettings.nTable);

			for ( var i=0, iLen=nCaptions.length ; i<iLen ; i++ )

			{

				nScrollHeadTable.appendChild( nCaptions[i] );

			}

			

			/*

			 * Sizing

			 */

			/* When xscrolling add the width and a scroller to move the header with the body */

			if ( oSettings.oScroll.sX !== "" )

			{

				nScrollHead.style.width = _fnStringToCss( oSettings.oScroll.sX );

				nScrollBody.style.width = _fnStringToCss( oSettings.oScroll.sX );

				

				if ( nTfoot !== null )

				{

					nScrollFoot.style.width = _fnStringToCss( oSettings.oScroll.sX );	

				}

				

				/* When the body is scrolled, then we also want to scroll the headers */

				$(nScrollBody).scroll( function (e) {

					nScrollHead.scrollLeft = this.scrollLeft;

					

					if ( nTfoot !== null )

					{

						nScrollFoot.scrollLeft = this.scrollLeft;

					}

				} );

			}

			

			/* When yscrolling, add the height */

			if ( oSettings.oScroll.sY !== "" )

			{

				nScrollBody.style.height = _fnStringToCss( oSettings.oScroll.sY );

			}

			

			/* Redraw - align columns across the tables */

			oSettings.aoDrawCallback.push( {

				"fn": _fnScrollDraw,

				"sName": "scrolling"

			} );

			

			/* Infinite scrolling event handlers */

			if ( oSettings.oScroll.bInfinite )

			{

				$(nScrollBody).scroll( function() {

					/* Use a blocker to stop scrolling from loading more data while other data is still loading */

					if ( !oSettings.bDrawing )

					{

						/* Check if we should load the next data set */

						if ( $(this).scrollTop() + $(this).height() > 

							$(oSettings.nTable).height() - oSettings.oScroll.iLoadGap )

						{

							/* Only do the redraw if we have to - we might be at the end of the data */

							if ( oSettings.fnDisplayEnd() < oSettings.fnRecordsDisplay() )

							{

								_fnPageChange( oSettings, 'next' );

								_fnCalculateEnd( oSettings );

								_fnDraw( oSettings );

							}

						}

					}

				} );

			}

			

			oSettings.nScrollHead = nScrollHead;

			oSettings.nScrollFoot = nScrollFoot;

			

			return nScroller;

		}

		

		/*

		 * Function: _fnScrollDraw

		 * Purpose:  Update the various tables for resizing

		 * Returns:  node: - Node to add to the DOM

		 * Inputs:   object:o - dataTables settings object

		 * Notes:    It's a bit of a pig this function, but basically the idea to:

		 *   1. Re-create the table inside the scrolling div

		 *   2. Take live measurements from the DOM

		 *   3. Apply the measurements

		 *   4. Clean up

		 */

		function _fnScrollDraw ( o )

		{

			var

				nScrollHeadInner = o.nScrollHead.getElementsByTagName('div')[0],

				nScrollHeadTable = nScrollHeadInner.getElementsByTagName('table')[0],

				nScrollBody = o.nTable.parentNode,

				i, iLen, j, jLen, anHeadToSize, anHeadSizers, anFootSizers, anFootToSize, oStyle, iVis,

				iWidth, aApplied=[], iSanityWidth;

			

			/*

			 * 1. Re-create the table inside the scrolling div

			 */

			

			/* Remove the old minimised thead and tfoot elements in the inner table */

			var nTheadSize = o.nTable.getElementsByTagName('thead');

			if ( nTheadSize.length > 0 )

			{

				o.nTable.removeChild( nTheadSize[0] );

			}

			

			if ( o.nTFoot !== null )

			{

				/* Remove the old minimised footer element in the cloned header */

				var nTfootSize = o.nTable.getElementsByTagName('tfoot');

				if ( nTfootSize.length > 0 )

				{

					o.nTable.removeChild( nTfootSize[0] );

				}

			}

			

			/* Clone the current header and footer elements and then place it into the inner table */

			nTheadSize = o.nTHead.cloneNode(true);

			o.nTable.insertBefore( nTheadSize, o.nTable.childNodes[0] );

			

			if ( o.nTFoot !== null )

			{

				nTfootSize = o.nTFoot.cloneNode(true);

				o.nTable.insertBefore( nTfootSize, o.nTable.childNodes[1] );

			}

			

			/*

			 * 2. Take live measurements from the DOM - do not alter the DOM itself!

			 */

			

			/* Remove old sizing and apply the calculated column widths

			 * Get the unique column headers in the newly created (cloned) header. We want to apply the

			 * calclated sizes to this header

			 */

			var nThs = _fnGetUniqueThs( nTheadSize );

			for ( i=0, iLen=nThs.length ; i<iLen ; i++ )

			{

				iVis = _fnVisibleToColumnIndex( o, i );

				nThs[i].style.width = o.aoColumns[iVis].sWidth;

			}

			

			if ( o.nTFoot !== null )

			{

				_fnApplyToChildren( function(n) {

					n.style.width = "";

				}, nTfootSize.getElementsByTagName('tr') );

			}

			

			/* Size the table as a whole */

			iSanityWidth = $(o.nTable).outerWidth();

			if ( o.oScroll.sX === "" )

			{

				/* No x scrolling */

				o.nTable.style.width = "100%";

				

				/* I know this is rubbish - but IE7 will make the width of the table when 100% include

				 * the scrollbar - which is shouldn't. This needs feature detection in future - to do

				 */

				if ( $.browser.msie && $.browser.version <= 7 )

				{

					o.nTable.style.width = _fnStringToCss( $(o.nTable).outerWidth()-o.oScroll.iBarWidth );

				}

			}

			else

			{

				if ( o.oScroll.sXInner !== "" )

				{

					/* x scroll inner has been given - use it */

					o.nTable.style.width = _fnStringToCss(o.oScroll.sXInner);

				}

				else if ( iSanityWidth == $(nScrollBody).width() &&

				   $(nScrollBody).height() < $(o.nTable).height() )

				{

					/* There is y-scrolling - try to take account of the y scroll bar */

					o.nTable.style.width = _fnStringToCss( iSanityWidth-o.oScroll.iBarWidth );

					if ( $(o.nTable).outerWidth() > iSanityWidth-o.oScroll.iBarWidth )

					{

						/* Not possible to take account of it */

						o.nTable.style.width = _fnStringToCss( iSanityWidth );

					}

				}

				else

				{

					/* All else fails */

					o.nTable.style.width = _fnStringToCss( iSanityWidth );

				}

			}

			

			/* Recalculate the sanity width - now that we've applied the required width, before it was

			 * a temporary variable. This is required because the column width calculation is done

			 * before this table DOM is created.

			 */

			iSanityWidth = $(o.nTable).outerWidth();

			

			/* We want the hidden header to have zero height, so remove padding and borders. Then

			 * set the width based on the real headers

			 */

			anHeadToSize = o.nTHead.getElementsByTagName('tr');

			anHeadSizers = nTheadSize.getElementsByTagName('tr');

			

			_fnApplyToChildren( function(nSizer, nToSize) {

				oStyle = nSizer.style;

				oStyle.paddingTop = "0";

				oStyle.paddingBottom = "0";

				oStyle.borderTopWidth = "0";

				oStyle.borderBottomWidth = "0";

				oStyle.height = 0;

				

				iWidth = $(nSizer).width();

				nToSize.style.width = _fnStringToCss( iWidth );

				aApplied.push( iWidth );

			}, anHeadSizers, anHeadToSize );

			$(anHeadSizers).height(0);

			

			if ( o.nTFoot !== null )

			{

				/* Clone the current footer and then place it into the body table as a "hidden header" */

				anFootSizers = nTfootSize.getElementsByTagName('tr');

				anFootToSize = o.nTFoot.getElementsByTagName('tr');

				

				_fnApplyToChildren( function(nSizer, nToSize) {

					oStyle = nSizer.style;

					oStyle.paddingTop = "0";

					oStyle.paddingBottom = "0";

					oStyle.borderTopWidth = "0";

					oStyle.borderBottomWidth = "0";

					

					iWidth = $(nSizer).width();

					nToSize.style.width = _fnStringToCss( iWidth );

					aApplied.push( iWidth );

				}, anFootSizers, anFootToSize );

				$(anFootSizers).height(0);

			}

			

			/*

			 * 3. Apply the measurements

			 */

			

			/* "Hide" the header and footer that we used for the sizing. We want to also fix their width

			 * to what they currently are

			 */

			_fnApplyToChildren( function(nSizer) {

				nSizer.innerHTML = "";

				nSizer.style.width = _fnStringToCss( aApplied.shift() );

			}, anHeadSizers );

			

			if ( o.nTFoot !== null )

			{

				_fnApplyToChildren( function(nSizer) {

					nSizer.innerHTML = "";

					nSizer.style.width = _fnStringToCss( aApplied.shift() );

				}, anFootSizers );

			}

			

			/* Sanity check that the table is of a sensible width. If not then we are going to get

			 * misalignment

			 */

			if ( $(o.nTable).outerWidth() < iSanityWidth )

			{

				if ( o.oScroll.sX === "" )

				{

					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+

						" misalignment. It is suggested that you enable x-scrolling or increase the width"+

						" the table has in which to be drawn" );

				}

				else if ( o.oScroll.sXInner !== "" )

				{

					_fnLog( o, 1, "The table cannot fit into the current element which will cause column"+

						" misalignment. It is suggested that you increase the sScrollXInner property to"+

						" allow it to draw in a larger area, or simply remove that parameter to allow"+

						" automatic calculation" );

				}

			}

			

			

			/*

			 * 4. Clean up

			 */

			

			if ( o.oScroll.sY === "" )

			{

				/* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting

				 * the scrollbar height from the visible display, rather than adding it on. We need to

				 * set the height in order to sort this. Don't want to do it in any other browsers.

				 */

				if ( $.browser.msie && $.browser.version <= 7 )

				{

					nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+o.oScroll.iBarWidth );

				}

			}

			

			if ( o.oScroll.sY !== "" && o.oScroll.bCollapse )

			{

				nScrollBody.style.height = _fnStringToCss( o.oScroll.sY );

				

				var iExtra = (o.oScroll.sX !== "" && o.nTable.offsetWidth > nScrollBody.offsetWidth) ?

				 	o.oScroll.iBarWidth : 0;

				if ( o.nTable.offsetHeight < nScrollBody.offsetHeight )

				{

					nScrollBody.style.height = _fnStringToCss( $(o.nTable).height()+iExtra );

				}

			}

			

			/* Finally set the width's of the header and footer tables */

			var iOuterWidth = $(o.nTable).outerWidth();

			nScrollHeadTable.style.width = _fnStringToCss( iOuterWidth );

			nScrollHeadInner.style.width = _fnStringToCss( iOuterWidth+o.oScroll.iBarWidth );

			nScrollHeadInner.parentNode.style.width = _fnStringToCss( $(nScrollBody).width() );

			

			if ( o.nTFoot !== null )

			{

				var

					nScrollFootInner = o.nScrollFoot.getElementsByTagName('div')[0],

					nScrollFootTable = nScrollFootInner.getElementsByTagName('table')[0];

				

				nScrollFootInner.style.width = _fnStringToCss( o.nTable.offsetWidth+o.oScroll.iBarWidth );

				nScrollFootTable.style.width = _fnStringToCss( o.nTable.offsetWidth );

			}

			

			/* If sorting or filtering has occured, jump the scrolling back to the top */

			if ( o.bSorted || o.bFiltered )

			{

				nScrollBody.scrollTop = 0;

			}

		}

		

		/*

		 * Function: _fnAjustColumnSizing

		 * Purpose:  Ajust the table column widths for new data

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 * Notes:    You would probably want to do a redraw after calling this function!

		 */

		function _fnAjustColumnSizing ( oSettings )

		{

			/* Not interested in doing column width calculation if autowidth is disabled */

			if ( oSettings.oFeatures.bAutoWidth === false )

			{

				return false;

			}

			

			_fnCalculateColumnWidths( oSettings );

			for ( var i=0 , iLen=oSettings.aoColumns.length ; i<iLen ; i++ )

			{

				oSettings.aoColumns[i].nTh.style.width = oSettings.aoColumns[i].sWidth;

			}

		}

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Feature: Filtering

		 */

		

		/*

		 * Function: _fnFeatureHtmlFilter

		 * Purpose:  Generate the node required for filtering text

		 * Returns:  node

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnFeatureHtmlFilter ( oSettings )

		{

			var nFilter = document.createElement( 'div' );

			if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.f == "undefined" )

			{

				nFilter.setAttribute( 'id', oSettings.sTableId+'_filter' );

			}

			nFilter.className = oSettings.oClasses.sFilter;

			var sSpace = oSettings.oLanguage.sSearch==="" ? "" : " ";

			nFilter.innerHTML = oSettings.oLanguage.sSearch+sSpace+'<input type="text" />';

			

			var jqFilter = $("input", nFilter);

			jqFilter.val( oSettings.oPreviousSearch.sSearch.replace('"','&quot;') );

			jqFilter.keyup( function(e) {

				/* Update all other filter input elements for the new display */

				var n = oSettings.aanFeatures.f;

				for ( var i=0, iLen=n.length ; i<iLen ; i++ )

				{

					if ( n[i] != this.parentNode )

					{

						$('input', n[i]).val( this.value );

					}

				}

				

				/* Now do the filter */

				if ( this.value != oSettings.oPreviousSearch.sSearch )

				{

					_fnFilterComplete( oSettings, { 

						"sSearch": this.value, 

						"bRegex":  oSettings.oPreviousSearch.bRegex,

						"bSmart":  oSettings.oPreviousSearch.bSmart 

					} );

				}

			} );

			

			jqFilter.keypress( function(e) {

				/* Prevent default */

				if ( e.keyCode == 13 )

				{

					return false;

				}

			} );

			

			return nFilter;

		}

		

		/*

		 * Function: _fnFilterComplete

		 * Purpose:  Filter the table using both the global filter and column based filtering

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           object:oSearch: search information

		 *           int:iForce - optional - force a research of the master array (1) or not (undefined or 0)

		 */

		function _fnFilterComplete ( oSettings, oInput, iForce )

		{

			/* Filter on everything */

			_fnFilter( oSettings, oInput.sSearch, iForce, oInput.bRegex, oInput.bSmart );

			

			/* Now do the individual column filter */

			for ( var i=0 ; i<oSettings.aoPreSearchCols.length ; i++ )

			{

				_fnFilterColumn( oSettings, oSettings.aoPreSearchCols[i].sSearch, i, 

					oSettings.aoPreSearchCols[i].bRegex, oSettings.aoPreSearchCols[i].bSmart );

			}

			

			/* Custom filtering */

			if ( _oExt.afnFiltering.length !== 0 )

			{

				_fnFilterCustom( oSettings );

			}

			

			/* Tell the draw function we have been filtering */

			oSettings.bFiltered = true;

			

			/* Redraw the table */

			oSettings._iDisplayStart = 0;

			_fnCalculateEnd( oSettings );

			_fnDraw( oSettings );

			

			/* Rebuild search array 'offline' */

			_fnBuildSearchArray( oSettings, 0 );

		}

		

		/*

		 * Function: _fnFilterCustom

		 * Purpose:  Apply custom filtering functions

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnFilterCustom( oSettings )

		{

			var afnFilters = _oExt.afnFiltering;

			for ( var i=0, iLen=afnFilters.length ; i<iLen ; i++ )

			{

				var iCorrector = 0;

				for ( var j=0, jLen=oSettings.aiDisplay.length ; j<jLen ; j++ )

				{

					var iDisIndex = oSettings.aiDisplay[j-iCorrector];

					

					/* Check if we should use this row based on the filtering function */

					if ( !afnFilters[i]( oSettings, oSettings.aoData[iDisIndex]._aData, iDisIndex ) )

					{

						oSettings.aiDisplay.splice( j-iCorrector, 1 );

						iCorrector++;

					}

				}

			}

		}

		

		/*

		 * Function: _fnFilterColumn

		 * Purpose:  Filter the table on a per-column basis

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           string:sInput - string to filter on

		 *           int:iColumn - column to filter

		 *           bool:bRegex - treat search string as a regular expression or not

		 *           bool:bSmart - use smart filtering or not

		 */

		function _fnFilterColumn ( oSettings, sInput, iColumn, bRegex, bSmart )

		{

			if ( sInput === "" )

			{

				return;

			}

			

			var iIndexCorrector = 0;

			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart );

			

			for ( var i=oSettings.aiDisplay.length-1 ; i>=0 ; i-- )

			{

				var sData = _fnDataToSearch( oSettings.aoData[ oSettings.aiDisplay[i] ]._aData[iColumn],

					oSettings.aoColumns[iColumn].sType );

				if ( ! rpSearch.test( sData ) )

				{

					oSettings.aiDisplay.splice( i, 1 );

					iIndexCorrector++;

				}

			}

		}

		

		/*

		 * Function: _fnFilter

		 * Purpose:  Filter the data table based on user input and draw the table

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           string:sInput - string to filter on

		 *           int:iForce - optional - force a research of the master array (1) or not (undefined or 0)

		 *           bool:bRegex - treat as a regular expression or not

		 *           bool:bSmart - perform smart filtering or not

		 */

		function _fnFilter( oSettings, sInput, iForce, bRegex, bSmart )

		{

			var i;

			var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart );

			

			/* Check if we are forcing or not - optional parameter */

			if ( typeof iForce == 'undefined' || iForce === null )

			{

				iForce = 0;

			}

			

			/* Need to take account of custom filtering functions - always filter */

			if ( _oExt.afnFiltering.length !== 0 )

			{

				iForce = 1;

			}

			

			/*

			 * If the input is blank - we want the full data set

			 */

			if ( sInput.length <= 0 )

			{

				oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);

				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();

			}

			else

			{

				/*

				 * We are starting a new search or the new search string is smaller 

				 * then the old one (i.e. delete). Search from the master array

			 	 */

				if ( oSettings.aiDisplay.length == oSettings.aiDisplayMaster.length ||

					   oSettings.oPreviousSearch.sSearch.length > sInput.length || iForce == 1 ||

					   sInput.indexOf(oSettings.oPreviousSearch.sSearch) !== 0 )

				{

					/* Nuke the old display array - we are going to rebuild it */

					oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length);

					

					/* Force a rebuild of the search array */

					_fnBuildSearchArray( oSettings, 1 );

					

					/* Search through all records to populate the search array

					 * The the oSettings.aiDisplayMaster and asDataSearch arrays have 1 to 1 

					 * mapping

					 */

					for ( i=0 ; i<oSettings.aiDisplayMaster.length ; i++ )

					{

						if ( rpSearch.test(oSettings.asDataSearch[i]) )

						{

							oSettings.aiDisplay.push( oSettings.aiDisplayMaster[i] );

						}

					}

			  }

			  else

				{

			  	/* Using old search array - refine it - do it this way for speed

			  	 * Don't have to search the whole master array again

			 		 */

			  	var iIndexCorrector = 0;

			  	

			  	/* Search the current results */

			  	for ( i=0 ; i<oSettings.asDataSearch.length ; i++ )

					{

			  		if ( ! rpSearch.test(oSettings.asDataSearch[i]) )

						{

			  			oSettings.aiDisplay.splice( i-iIndexCorrector, 1 );

			  			iIndexCorrector++;

			  		}

			  	}

			  }

			}

			oSettings.oPreviousSearch.sSearch = sInput;

			oSettings.oPreviousSearch.bRegex = bRegex;

			oSettings.oPreviousSearch.bSmart = bSmart;

		}

		

		/*

		 * Function: _fnBuildSearchArray

		 * Purpose:  Create an array which can be quickly search through

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           int:iMaster - use the master data array - optional

		 */

		function _fnBuildSearchArray ( oSettings, iMaster )

		{

			/* Clear out the old data */

			oSettings.asDataSearch.splice( 0, oSettings.asDataSearch.length );

			

			var aArray = (typeof iMaster != 'undefined' && iMaster == 1) ?

			 	oSettings.aiDisplayMaster : oSettings.aiDisplay;

			

			for ( var i=0, iLen=aArray.length ; i<iLen ; i++ )

			{

				oSettings.asDataSearch[i] = _fnBuildSearchRow( oSettings, 

					oSettings.aoData[ aArray[i] ]._aData );

			}

		}

		

		/*

		 * Function: _fnBuildSearchRow

		 * Purpose:  Create a searchable string from a single data row

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           array:aData - aoData[]._aData array to use for the data to search

		 */

		function _fnBuildSearchRow( oSettings, aData )

		{

			var sSearch = '';

			var nTmp = document.createElement('div');

			

			for ( var j=0, jLen=oSettings.aoColumns.length ; j<jLen ; j++ )

			{

				if ( oSettings.aoColumns[j].bSearchable )

				{

					var sData = aData[j];

					sSearch += _fnDataToSearch( sData, oSettings.aoColumns[j].sType )+'  ';

				}

			}

			

			/* If it looks like there is an HTML entity in the string, attempt to decode it */

			if ( sSearch.indexOf('&') !== -1 )

			{

				nTmp.innerHTML = sSearch;

				sSearch = nTmp.textContent ? nTmp.textContent : nTmp.innerText;

				

				/* IE and Opera appear to put an newline where there is a <br> tag - remove it */

				sSearch = sSearch.replace(/\n/g," ").replace(/\r/g,"");

			}

			

			return sSearch;

		}

		

		/*

		 * Function: _fnFilterCreateSearch

		 * Purpose:  Build a regular expression object suitable for searching a table

		 * Returns:  RegExp: - constructed object

		 * Inputs:   string:sSearch - string to search for

		 *           bool:bRegex - treat as a regular expression or not

		 *           bool:bSmart - perform smart filtering or not

		 */

		function _fnFilterCreateSearch( sSearch, bRegex, bSmart )

		{

			var asSearch, sRegExpString;

			

			if ( bSmart )

			{

				/* Generate the regular expression to use. Something along the lines of:

				 * ^(?=.*?\bone\b)(?=.*?\btwo\b)(?=.*?\bthree\b).*$

				 */

				asSearch = bRegex ? sSearch.split( ' ' ) : _fnEscapeRegex( sSearch ).split( ' ' );

				sRegExpString = '^(?=.*?'+asSearch.join( ')(?=.*?' )+').*$';

				return new RegExp( sRegExpString, "i" );

			}

			else

			{

				sSearch = bRegex ? sSearch : _fnEscapeRegex( sSearch );

				return new RegExp( sSearch, "i" );

			}

		}

		

		/*

		 * Function: _fnDataToSearch

		 * Purpose:  Convert raw data into something that the user can search on

		 * Returns:  string: - search string

		 * Inputs:   string:sData - data to be modified

		 *           string:sType - data type

		 */

		function _fnDataToSearch ( sData, sType )

		{

			if ( typeof _oExt.ofnSearch[sType] == "function" )

			{

				return _oExt.ofnSearch[sType]( sData );

			}

			else if ( sType == "html" )

			{

				return sData.replace(/\n/g," ").replace( /<.*?>/g, "" );

			}

			else if ( typeof sData == "string" )

			{

				return sData.replace(/\n/g," ");

			}

			return sData;

		}

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Feature: Sorting

		 */

		

		/*

	 	 * Function: _fnSort

		 * Purpose:  Change the order of the table

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           bool:bApplyClasses - optional - should we apply classes or not

		 * Notes:    We always sort the master array and then apply a filter again

		 *   if it is needed. This probably isn't optimal - but atm I can't think

		 *   of any other way which is (each has disadvantages). we want to sort aiDisplayMaster - 

		 *   but according to aoData[]._aData

		 */

		function _fnSort ( oSettings, bApplyClasses )

		{

			var aaSort = [];

			var oSort = _oExt.oSort;

			var aoData = oSettings.aoData;

			var iDataSort;

			var iDataType;

			var i, j, jLen;

			

			/* No sorting required if server-side or no sorting array */

			if ( !oSettings.oFeatures.bServerSide && 

				(oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null) )

			{

				if ( oSettings.aaSortingFixed !== null )

				{

					aaSort = oSettings.aaSortingFixed.concat( oSettings.aaSorting );

				}

				else

				{

					aaSort = oSettings.aaSorting.slice();

				}

				

				/* If there is a sorting data type, and a fuction belonging to it, then we need to

				 * get the data from the developer's function and apply it for this column

				 */

				for ( i=0 ; i<aaSort.length ; i++ )

				{

					var iColumn = aaSort[i][0];

					var iVisColumn = _fnColumnIndexToVisible( oSettings, iColumn );

					var sDataType = oSettings.aoColumns[ iColumn ].sSortDataType;

					if ( typeof _oExt.afnSortData[sDataType] != 'undefined' )

					{

						var aData = _oExt.afnSortData[sDataType]( oSettings, iColumn, iVisColumn );

						for ( j=0, jLen=aoData.length ; j<jLen ; j++ )

						{

							aoData[j]._aData[iColumn] = aData[j];

						}

					}

				}

				

				/* DataTables offers two different methods for doing the 2D array sorting over multiple

				 * columns. The first is to construct a function dynamically, and then evaluate and run

				 * the function, while the second has no need for evalulation, but is a little bit slower.

				 * This is used for environments which do not allow eval() for code execuation such as AIR

				 */

				if ( !window.runtime )

				{

					/* Dynamically created sorting function. Based on the information that we have, we can

					 * create a sorting function as if it were specifically written for this sort. Here we

					 * want to build a function something like (for two column sorting):

					 *  fnLocalSorting = function(a,b){

					 *  	var iTest;

					 *  	iTest = oSort['string-asc']('data11', 'data12');

					 *  	if (iTest === 0)

					 *  		iTest = oSort['numeric-desc']('data21', 'data22');

					 *  		if (iTest === 0)

					 *  			return oSort['numeric-desc'](1,2);

					 *  	return iTest;

					 *  }

					 * So basically we have a test for each column, and if that column matches, test the

					 * next one. If all columns match, then we use a numeric sort on the position the two

					 * row have in the original data array in order to provide a stable sort. In order to

					 * get the position for the numeric stablisation, we need to take a clone of the current

					 * display array and then get the position of the sorting value from that during the

					 * sort.

					 *

					 * Note that for use with the Closure compiler, we need to be very careful how we deal 

					 * with this eval. Closure will rename all of our local variables, resutling in breakage

					 * if the variables in the eval don't also reflect this. For this reason, we need to use

					 * 'this' to store the variables we need in the eval, so we can control them. A little

					 * nasty, but well worth it for using Closure.

					 */

					this.ClosureDataTables = {

						"fn": function(){},

						"data": aoData,

						"sort": _oExt.oSort,

						"master": oSettings.aiDisplayMaster.slice()

					};

					var sDynamicSort = "this.ClosureDataTables.fn = function(a,b){"+

						"var iTest, oSort=this.ClosureDataTables.sort, "+

						"aoData=this.ClosureDataTables.data, "+

						"aiOrig=this.ClosureDataTables.master;";

					

					for ( i=0 ; i<aaSort.length-1 ; i++ )

					{

						iDataSort = oSettings.aoColumns[ aaSort[i][0] ].iDataSort;

						iDataType = oSettings.aoColumns[ iDataSort ].sType;

						sDynamicSort += "iTest = oSort['"+iDataType+"-"+aaSort[i][1]+"']"+

							"( aoData[a]._aData["+iDataSort+"], aoData[b]._aData["+iDataSort+"] ); if ( iTest === 0 )";

					}

					

					if ( aaSort.length > 0 )

					{

						iDataSort = oSettings.aoColumns[ aaSort[aaSort.length-1][0] ].iDataSort;

						iDataType = oSettings.aoColumns[ iDataSort ].sType;

						sDynamicSort += "iTest = oSort['"+iDataType+"-"+aaSort[aaSort.length-1][1]+"']"+

							"( aoData[a]._aData["+iDataSort+"], aoData[b]._aData["+iDataSort+"] );"+

							"if (iTest===0) "+

								"return oSort['numeric-asc'](jQuery.inArray(a,aiOrig), jQuery.inArray(b,aiOrig)); "+

							"return iTest;}";

						

						/* The eval has to be done to a variable for IE */

						eval( sDynamicSort );

						oSettings.aiDisplayMaster.sort( this.ClosureDataTables.fn );

					}

					this.ClosureDataTables = undefined;

				}

				else

				{

					/*

					 * Non-eval() sorting (AIR and other environments which doesn't allow code in eval()

					 * Note that for reasonable sized data sets this method is around 1.5 times slower than

					 * the eval above (hence why it is not used all the time). Oddly enough, it is ever so

					 * slightly faster for very small sets (presumably the eval has overhead).

					 *   Single column (1083 records) - eval: 32mS   AIR: 38mS

					 *   Two columns (1083 records) -   eval: 55mS   AIR: 66mS

					 */

					

					/* Build a cached array so the sort doesn't have to process this stuff on every call */

					var aAirSort = [];

					var iLen = aaSort.length;

					for ( i=0 ; i<iLen ; i++ )

					{

						iDataSort = oSettings.aoColumns[ aaSort[i][0] ].iDataSort;

						aAirSort.push( [

							iDataSort,

							oSettings.aoColumns[ iDataSort ].sType+'-'+aaSort[i][1]

						] );

					}

					

					oSettings.aiDisplayMaster.sort( function (a,b) {

						var iTest;

						for ( var i=0 ; i<iLen ; i++ )

						{

							iTest = oSort[ aAirSort[i][1] ]( aoData[a]._aData[aAirSort[i][0]], aoData[b]._aData[aAirSort[i][0]] );

							if ( iTest !== 0 )

							{

								return iTest;

							}

						}

						return 0;

					} );

				}

			}

			

			/* Alter the sorting classes to take account of the changes */

			if ( typeof bApplyClasses == 'undefined' || bApplyClasses )

			{

				_fnSortingClasses( oSettings );

			}

			

			/* Tell the draw function that we have sorted the data */

			oSettings.bSorted = true;

			

			/* Copy the master data into the draw array and re-draw */

			if ( oSettings.oFeatures.bFilter )

			{

				/* _fnFilter() will redraw the table for us */

				_fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 );

			}

			else

			{

				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();

				oSettings._iDisplayStart = 0; /* reset display back to page 0 */

				_fnCalculateEnd( oSettings );

				_fnDraw( oSettings );

			}

		}

		

		/*

		 * Function: _fnSortAttachListener

		 * Purpose:  Attach a sort handler (click) to a node

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           node:nNode - node to attach the handler to

		 *           int:iDataIndex - column sorting index

		 *           function:fnCallback - callback function - optional

		 */

		function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback )

		{

			$(nNode).click( function (e) {

				/* If the column is not sortable - don't to anything */

				if ( oSettings.aoColumns[iDataIndex].bSortable === false )

				{

					return;

				}

				

				/*

				 * This is a little bit odd I admit... I declare a temporary function inside the scope of

				 * _fnDrawHead and the click handler in order that the code presented here can be used 

				 * twice - once for when bProcessing is enabled, and another time for when it is 

				 * disabled, as we need to perform slightly different actions.

				 *   Basically the issue here is that the Javascript engine in modern browsers don't 

				 * appear to allow the rendering engine to update the display while it is still excuting

				 * it's thread (well - it does but only after long intervals). This means that the 

				 * 'processing' display doesn't appear for a table sort. To break the js thread up a bit

				 * I force an execution break by using setTimeout - but this breaks the expected 

				 * thread continuation for the end-developer's point of view (their code would execute

				 * too early), so we on;y do it when we absolutely have to.

				 */

				var fnInnerSorting = function () {

					var iColumn, iNextSort;

					

					/* If the shift key is pressed then we are multipe column sorting */

					if ( e.shiftKey )

					{

						/* Are we already doing some kind of sort on this column? */

						var bFound = false;

						for ( var i=0 ; i<oSettings.aaSorting.length ; i++ )

						{

							if ( oSettings.aaSorting[i][0] == iDataIndex )

							{

								bFound = true;

								iColumn = oSettings.aaSorting[i][0];

								iNextSort = oSettings.aaSorting[i][2]+1;

								

								if ( typeof oSettings.aoColumns[iColumn].asSorting[iNextSort] == 'undefined' )

								{

									/* Reached the end of the sorting options, remove from multi-col sort */

									oSettings.aaSorting.splice( i, 1 );

								}

								else

								{

									/* Move onto next sorting direction */

									oSettings.aaSorting[i][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];

									oSettings.aaSorting[i][2] = iNextSort;

								}

								break;

							}

						}

						

						/* No sort yet - add it in */

						if ( bFound === false )

						{

							oSettings.aaSorting.push( [ iDataIndex, 

								oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] );

						}

					}

					else

					{

						/* If no shift key then single column sort */

						if ( oSettings.aaSorting.length == 1 && oSettings.aaSorting[0][0] == iDataIndex )

						{

							iColumn = oSettings.aaSorting[0][0];

							iNextSort = oSettings.aaSorting[0][2]+1;

							if ( typeof oSettings.aoColumns[iColumn].asSorting[iNextSort] == 'undefined' )

							{

								iNextSort = 0;

							}

							oSettings.aaSorting[0][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];

							oSettings.aaSorting[0][2] = iNextSort;

						}

						else

						{

							oSettings.aaSorting.splice( 0, oSettings.aaSorting.length );

							oSettings.aaSorting.push( [ iDataIndex, 

								oSettings.aoColumns[iDataIndex].asSorting[0], 0 ] );

						}

					}

					

					/* Run the sort */

					_fnSort( oSettings );

				}; /* /fnInnerSorting */

				

				if ( !oSettings.oFeatures.bProcessing )

				{

					fnInnerSorting();

				}

				else

				{

					_fnProcessingDisplay( oSettings, true );

					setTimeout( function() {

						fnInnerSorting();

						if ( !oSettings.oFeatures.bServerSide )

						{

							_fnProcessingDisplay( oSettings, false );

						}

					}, 0 );

				}

				

				/* Call the user specified callback function - used for async user interaction */

				if ( typeof fnCallback == 'function' )

				{

					fnCallback( oSettings );

				}

			} );

		}

		

		/*

		 * Function: _fnSortingClasses

		 * Purpose:  Set the sortting classes on the header

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 * Notes:    It is safe to call this function when bSort and bSortClasses are false

		 */

		function _fnSortingClasses( oSettings )

		{

			var i, iLen, j, jLen, iFound;

			var aaSort, sClass;

			var iColumns = oSettings.aoColumns.length;

			var oClasses = oSettings.oClasses;

			

			for ( i=0 ; i<iColumns ; i++ )

			{

				if ( oSettings.aoColumns[i].bSortable )

				{

					$(oSettings.aoColumns[i].nTh).removeClass( oClasses.sSortAsc +" "+ oClasses.sSortDesc +

				 		" "+ oSettings.aoColumns[i].sSortingClass );

				}

			}

			

			if ( oSettings.aaSortingFixed !== null )

			{

				aaSort = oSettings.aaSortingFixed.concat( oSettings.aaSorting );

			}

			else

			{

				aaSort = oSettings.aaSorting.slice();

			}

			

			/* Apply the required classes to the header */

			for ( i=0 ; i<oSettings.aoColumns.length ; i++ )

			{

				if ( oSettings.aoColumns[i].bSortable )

				{

					sClass = oSettings.aoColumns[i].sSortingClass;

					iFound = -1;

					for ( j=0 ; j<aaSort.length ; j++ )

					{

						if ( aaSort[j][0] == i )

						{

							sClass = ( aaSort[j][1] == "asc" ) ?

								oClasses.sSortAsc : oClasses.sSortDesc;

							iFound = j;

							break;

						}

					}

					$(oSettings.aoColumns[i].nTh).addClass( sClass );

					

					if ( oSettings.bJUI )

					{

						/* jQuery UI uses extra markup */

						var jqSpan = $("span", oSettings.aoColumns[i].nTh);

						jqSpan.removeClass(oClasses.sSortJUIAsc +" "+ oClasses.sSortJUIDesc +" "+ 

							oClasses.sSortJUI +" "+ oClasses.sSortJUIAscAllowed +" "+ oClasses.sSortJUIDescAllowed );

						

						var sSpanClass;

						if ( iFound == -1 )

						{

						 	sSpanClass = oSettings.aoColumns[i].sSortingClassJUI;

						}

						else if ( aaSort[iFound][1] == "asc" )

						{

							sSpanClass = oClasses.sSortJUIAsc;

						}

						else

						{

							sSpanClass = oClasses.sSortJUIDesc;

						}

						

						jqSpan.addClass( sSpanClass );

					}

				}

				else

				{

					/* No sorting on this column, so add the base class. This will have been assigned by

					 * _fnAddColumn

					 */

					$(oSettings.aoColumns[i].nTh).addClass( oSettings.aoColumns[i].sSortingClass );

				}

			}

			

			/* 

			 * Apply the required classes to the table body

			 * Note that this is given as a feature switch since it can significantly slow down a sort

			 * on large data sets (adding and removing of classes is always slow at the best of times..)

			 * Further to this, note that this code is admitadly fairly ugly. It could be made a lot 

			 * simpiler using jQuery selectors and add/removeClass, but that is significantly slower

			 * (on the order of 5 times slower) - hence the direct DOM manipulation here.

			 */

			sClass = oClasses.sSortColumn;

			

			if ( oSettings.oFeatures.bSort && oSettings.oFeatures.bSortClasses )

			{

				var nTds = _fnGetTdNodes( oSettings );

				

				/* Remove the old classes */

				if ( nTds.length >= iColumns )

				{

					for ( i=0 ; i<iColumns ; i++ )

					{

						if ( nTds[i].className.indexOf(sClass+"1") != -1 )

						{

							for ( j=0, jLen=(nTds.length/iColumns) ; j<jLen ; j++ )

							{

								nTds[(iColumns*j)+i].className = 

									$.trim( nTds[(iColumns*j)+i].className.replace( sClass+"1", "" ) );

							}

						}

						else if ( nTds[i].className.indexOf(sClass+"2") != -1 )

						{

							for ( j=0, jLen=(nTds.length/iColumns) ; j<jLen ; j++ )

							{

								nTds[(iColumns*j)+i].className = 

									$.trim( nTds[(iColumns*j)+i].className.replace( sClass+"2", "" ) );

							}

						}

						else if ( nTds[i].className.indexOf(sClass+"3") != -1 )

						{

							for ( j=0, jLen=(nTds.length/iColumns) ; j<jLen ; j++ )

							{

								nTds[(iColumns*j)+i].className = 

									$.trim( nTds[(iColumns*j)+i].className.replace( " "+sClass+"3", "" ) );

							}

						}

					}

				}

				

				/* Add the new classes to the table */

				var iClass = 1, iTargetCol;

				for ( i=0 ; i<aaSort.length ; i++ )

				{

					iTargetCol = parseInt( aaSort[i][0], 10 );

					for ( j=0, jLen=(nTds.length/iColumns) ; j<jLen ; j++ )

					{

						nTds[(iColumns*j)+iTargetCol].className += " "+sClass+iClass;

					}

					

					if ( iClass < 3 )

					{

						iClass++;

					}

				}

			}

		}

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Feature: Pagination. Note that most of the paging logic is done in 

		 * _oExt.oPagination

		 */

		

		/*

		 * Function: _fnFeatureHtmlPaginate

		 * Purpose:  Generate the node required for default pagination

		 * Returns:  node

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnFeatureHtmlPaginate ( oSettings )

		{

			if ( oSettings.oScroll.bInfinite )

			{

				return null;

			}

			

			var nPaginate = document.createElement( 'div' );

			nPaginate.className = oSettings.oClasses.sPaging+oSettings.sPaginationType;

			

			_oExt.oPagination[ oSettings.sPaginationType ].fnInit( oSettings, nPaginate, 

				function( oSettings ) {

					_fnCalculateEnd( oSettings );

					_fnDraw( oSettings );

				}

			);

			

			/* Add a draw callback for the pagination on first instance, to update the paging display */

			if ( typeof oSettings.aanFeatures.p == "undefined" )

			{

				oSettings.aoDrawCallback.push( {

					"fn": function( oSettings ) {

						_oExt.oPagination[ oSettings.sPaginationType ].fnUpdate( oSettings, function( oSettings ) {

							_fnCalculateEnd( oSettings );

							_fnDraw( oSettings );

						} );

					},

					"sName": "pagination"

				} );

			}

			return nPaginate;

		}

		

		/*

		 * Function: _fnPageChange

		 * Purpose:  Alter the display settings to change the page

		 * Returns:  bool:true - page has changed, false - no change (no effect) eg 'first' on page 1

		 * Inputs:   object:oSettings - dataTables settings object

		 *           string:sAction - paging action to take: "first", "previous", "next" or "last"

		 */

		function _fnPageChange ( oSettings, sAction )

		{

			var iOldStart = oSettings._iDisplayStart;

			

			if ( sAction == "first" )

			{

				oSettings._iDisplayStart = 0;

			}

			else if ( sAction == "previous" )

			{

				oSettings._iDisplayStart = oSettings._iDisplayLength>=0 ?

					oSettings._iDisplayStart - oSettings._iDisplayLength :

					0;

				

				/* Correct for underrun */

				if ( oSettings._iDisplayStart < 0 )

				{

				  oSettings._iDisplayStart = 0;

				}

			}

			else if ( sAction == "next" )

			{

				if ( oSettings._iDisplayLength >= 0 )

				{

					/* Make sure we are not over running the display array */

					if ( oSettings._iDisplayStart + oSettings._iDisplayLength < oSettings.fnRecordsDisplay() )

					{

						oSettings._iDisplayStart += oSettings._iDisplayLength;

					}

				}

				else

				{

					oSettings._iDisplayStart = 0;

				}

			}

			else if ( sAction == "last" )

			{

				if ( oSettings._iDisplayLength >= 0 )

				{

					var iPages = parseInt( (oSettings.fnRecordsDisplay()-1) / oSettings._iDisplayLength, 10 ) + 1;

					oSettings._iDisplayStart = (iPages-1) * oSettings._iDisplayLength;

				}

				else

				{

					oSettings._iDisplayStart = 0;

				}

			}

			else

			{

				_fnLog( oSettings, 0, "Unknown paging action: "+sAction );

			}

			

			return iOldStart != oSettings._iDisplayStart;

		}

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Feature: HTML info

		 */

		

		/*

		 * Function: _fnFeatureHtmlInfo

		 * Purpose:  Generate the node required for the info display

		 * Returns:  node

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnFeatureHtmlInfo ( oSettings )

		{

			var nInfo = document.createElement( 'div' );

			nInfo.className = oSettings.oClasses.sInfo;

			

			/* Actions that are to be taken once only for this feature */

			if ( typeof oSettings.aanFeatures.i == "undefined" )

			{

				/* Add draw callback */

				oSettings.aoDrawCallback.push( {

					"fn": _fnUpdateInfo,

					"sName": "information"

				} );

				

				/* Add id */

				if ( oSettings.sTableId !== '' )

				{

					nInfo.setAttribute( 'id', oSettings.sTableId+'_info' );

				}

			}

			

			return nInfo;

		}

		

		/*

		 * Function: _fnUpdateInfo

		 * Purpose:  Update the information elements in the display

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnUpdateInfo ( oSettings )

		{

			/* Show information about the table */

			if ( !oSettings.oFeatures.bInfo || oSettings.aanFeatures.i.length === 0 )

			{

				return;

			}

			

			var

				iStart = oSettings._iDisplayStart+1, iEnd = oSettings.fnDisplayEnd(),

				iMax = oSettings.fnRecordsTotal(), iTotal = oSettings.fnRecordsDisplay(),

				sStart = oSettings.fnFormatNumber( iStart ), sEnd = oSettings.fnFormatNumber( iEnd ),

				sMax = oSettings.fnFormatNumber( iMax ), sTotal = oSettings.fnFormatNumber( iTotal ),

				sOut;

			

			/* When infinite scrolling, we are always starting at 1. _iDisplayStart is used only

			 * internally

			 */

			if ( oSettings.oScroll.bInfinite )

			{

				sStart = oSettings.fnFormatNumber( 1 );

			}

			

			if ( oSettings.fnRecordsDisplay() === 0 && 

				   oSettings.fnRecordsDisplay() == oSettings.fnRecordsTotal() )

			{

				/* Empty record set */

				sOut = oSettings.oLanguage.sInfoEmpty+ oSettings.oLanguage.sInfoPostFix;

			}

			else if ( oSettings.fnRecordsDisplay() === 0 )

			{

				/* Rmpty record set after filtering */

				sOut = oSettings.oLanguage.sInfoEmpty +' '+ 

					oSettings.oLanguage.sInfoFiltered.replace('_MAX_', sMax)+

						oSettings.oLanguage.sInfoPostFix;

			}

			else if ( oSettings.fnRecordsDisplay() == oSettings.fnRecordsTotal() )

			{

				/* Normal record set */

				sOut = oSettings.oLanguage.sInfo.

						replace('_START_', sStart).

						replace('_END_',   sEnd).

						replace('_TOTAL_', sTotal)+ 

					oSettings.oLanguage.sInfoPostFix;

			}

			else

			{

				/* Record set after filtering */

				sOut = oSettings.oLanguage.sInfo.

						replace('_START_', sStart).

						replace('_END_',   sEnd).

						replace('_TOTAL_', sTotal) +' '+ 

					oSettings.oLanguage.sInfoFiltered.replace('_MAX_', 

						oSettings.fnFormatNumber(oSettings.fnRecordsTotal()))+ 

					oSettings.oLanguage.sInfoPostFix;

			}

			

			if ( oSettings.oLanguage.fnInfoCallback !== null )

			{

				sOut = oSettings.oLanguage.fnInfoCallback( oSettings, iStart, iEnd, iMax, iTotal, sOut );

			}

			

			var n = oSettings.aanFeatures.i;

			for ( var i=0, iLen=n.length ; i<iLen ; i++ )

			{

				$(n[i]).html( sOut );

			}

		}

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Feature: Length change

		 */

		

		/*

		 * Function: _fnFeatureHtmlLength

		 * Purpose:  Generate the node required for user display length changing

		 * Returns:  node

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnFeatureHtmlLength ( oSettings )

		{

			if ( oSettings.oScroll.bInfinite )

			{

				return null;

			}

			

			/* This can be overruled by not using the _MENU_ var/macro in the language variable */

			var sName = (oSettings.sTableId === "") ? "" : 'name="'+oSettings.sTableId+'_length"';

			var sStdMenu = '<select size="1" '+sName+'>';

			var i, iLen;

			

			if ( oSettings.aLengthMenu.length == 2 && typeof oSettings.aLengthMenu[0] == 'object' && 

					typeof oSettings.aLengthMenu[1] == 'object' )

			{

				for ( i=0, iLen=oSettings.aLengthMenu[0].length ; i<iLen ; i++ )

				{

					sStdMenu += '<option value="'+oSettings.aLengthMenu[0][i]+'">'+

						oSettings.aLengthMenu[1][i]+'</option>';

				}

			}

			else

			{

				for ( i=0, iLen=oSettings.aLengthMenu.length ; i<iLen ; i++ )

				{

					sStdMenu += '<option value="'+oSettings.aLengthMenu[i]+'">'+

						oSettings.aLengthMenu[i]+'</option>';

				}

			}

			sStdMenu += '</select>';

			

			var nLength = document.createElement( 'div' );

			if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.l == "undefined" )

			{

				nLength.setAttribute( 'id', oSettings.sTableId+'_length' );

			}

			nLength.className = oSettings.oClasses.sLength;

			nLength.innerHTML = oSettings.oLanguage.sLengthMenu.replace( '_MENU_', sStdMenu );

			

			/*

			 * Set the length to the current display length - thanks to Andrea Pavlovic for this fix,

			 * and Stefan Skopnik for fixing the fix!

			 */

			$('select option[value="'+oSettings._iDisplayLength+'"]',nLength).attr("selected",true);

			

			$('select', nLength).change( function(e) {

				var iVal = $(this).val();

				

				/* Update all other length options for the new display */

				var n = oSettings.aanFeatures.l;

				for ( i=0, iLen=n.length ; i<iLen ; i++ )

				{

					if ( n[i] != this.parentNode )

					{

						$('select', n[i]).val( iVal );

					}

				}

				

				/* Redraw the table */

				oSettings._iDisplayLength = parseInt(iVal, 10);

				_fnCalculateEnd( oSettings );

				

				/* If we have space to show extra rows (backing up from the end point - then do so */

				if ( oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay() )

				{

					oSettings._iDisplayStart = oSettings.fnDisplayEnd() - oSettings._iDisplayLength;

					if ( oSettings._iDisplayStart < 0 )

					{

						oSettings._iDisplayStart = 0;

					}

				}

				

				if ( oSettings._iDisplayLength == -1 )

				{

					oSettings._iDisplayStart = 0;

				}

				

				_fnDraw( oSettings );

			} );

			

			return nLength;

		}

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Feature: Processing incidator

		 */

		

		/*

		 * Function: _fnFeatureHtmlProcessing

		 * Purpose:  Generate the node required for the processing node

		 * Returns:  node

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnFeatureHtmlProcessing ( oSettings )

		{

			var nProcessing = document.createElement( 'div' );

			

			if ( oSettings.sTableId !== '' && typeof oSettings.aanFeatures.r == "undefined" )

			{

				nProcessing.setAttribute( 'id', oSettings.sTableId+'_processing' );

			}

			nProcessing.innerHTML = oSettings.oLanguage.sProcessing;

			nProcessing.className = oSettings.oClasses.sProcessing;

			oSettings.nTable.parentNode.insertBefore( nProcessing, oSettings.nTable );

			

			return nProcessing;

		}

		

		/*

		 * Function: _fnProcessingDisplay

		 * Purpose:  Display or hide the processing indicator

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           bool:

		 *   true - show the processing indicator

		 *   false - don't show

		 */

		function _fnProcessingDisplay ( oSettings, bShow )

		{

			if ( oSettings.oFeatures.bProcessing )

			{

				var an = oSettings.aanFeatures.r;

				for ( var i=0, iLen=an.length ; i<iLen ; i++ )

				{

					an[i].style.visibility = bShow ? "visible" : "hidden";

				}

			}

		}

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Support functions

		 */

		

		/*

		 * Function: _fnVisibleToColumnIndex

		 * Purpose:  Covert the index of a visible column to the index in the data array (take account

		 *   of hidden columns)

		 * Returns:  int:i - the data index

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnVisibleToColumnIndex( oSettings, iMatch )

		{

			var iColumn = -1;

			

			for ( var i=0 ; i<oSettings.aoColumns.length ; i++ )

			{

				if ( oSettings.aoColumns[i].bVisible === true )

				{

					iColumn++;

				}

				

				if ( iColumn == iMatch )

				{

					return i;

				}

			}

			

			return null;

		}

		

		/*

		 * Function: _fnColumnIndexToVisible

		 * Purpose:  Covert the index of an index in the data array and convert it to the visible

		 *   column index (take account of hidden columns)

		 * Returns:  int:i - the data index

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnColumnIndexToVisible( oSettings, iMatch )

		{

			var iVisible = -1;

			for ( var i=0 ; i<oSettings.aoColumns.length ; i++ )

			{

				if ( oSettings.aoColumns[i].bVisible === true )

				{

					iVisible++;

				}

				

				if ( i == iMatch )

				{

					return oSettings.aoColumns[i].bVisible === true ? iVisible : null;

				}

			}

			

			return null;

		}

		

		

		/*

		 * Function: _fnNodeToDataIndex

		 * Purpose:  Take a TR element and convert it to an index in aoData

		 * Returns:  int:i - index if found, null if not

		 * Inputs:   object:s - dataTables settings object

		 *           node:n - the TR element to find

		 */

		function _fnNodeToDataIndex( s, n )

		{

			var i, iLen;

			

			/* Optimisation - see if the nodes which are currently visible match, since that is

			 * the most likely node to be asked for (a selector or event for example)

			 */

			for ( i=s._iDisplayStart, iLen=s._iDisplayEnd ; i<iLen ; i++ )

			{

				if ( s.aoData[ s.aiDisplay[i] ].nTr == n )

				{

					return s.aiDisplay[i];

				}

			}

			

			/* Otherwise we are in for a slog through the whole data cache */

			for ( i=0, iLen=s.aoData.length ; i<iLen ; i++ )

			{

				if ( s.aoData[i].nTr == n )

				{

					return i;

				}

			}

			return null;

		}

		

		/*

		 * Function: _fnVisbleColumns

		 * Purpose:  Get the number of visible columns

		 * Returns:  int:i - the number of visible columns

		 * Inputs:   object:oS - dataTables settings object

		 */

		function _fnVisbleColumns( oS )

		{

			var iVis = 0;

			for ( var i=0 ; i<oS.aoColumns.length ; i++ )

			{

				if ( oS.aoColumns[i].bVisible === true )

				{

					iVis++;

				}

			}

			return iVis;

		}

		

		/*

		 * Function: _fnCalculateEnd

		 * Purpose:  Rcalculate the end point based on the start point

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnCalculateEnd( oSettings )

		{

			if ( oSettings.oFeatures.bPaginate === false )

			{

				oSettings._iDisplayEnd = oSettings.aiDisplay.length;

			}

			else

			{

				/* Set the end point of the display - based on how many elements there are

				 * still to display

				 */

				if ( oSettings._iDisplayStart + oSettings._iDisplayLength > oSettings.aiDisplay.length ||

					   oSettings._iDisplayLength == -1 )

				{

					oSettings._iDisplayEnd = oSettings.aiDisplay.length;

				}

				else

				{

					oSettings._iDisplayEnd = oSettings._iDisplayStart + oSettings._iDisplayLength;

				}

			}

		}

		

		/*

		 * Function: _fnConvertToWidth

		 * Purpose:  Convert a CSS unit width to pixels (e.g. 2em)

		 * Returns:  int:iWidth - width in pixels

		 * Inputs:   string:sWidth - width to be converted

		 *           node:nParent - parent to get the with for (required for

		 *             relative widths) - optional

		 */

		function _fnConvertToWidth ( sWidth, nParent )

		{

			if ( !sWidth || sWidth === null || sWidth === '' )

			{

				return 0;

			}

			

			if ( typeof nParent == "undefined" )

			{

				nParent = document.getElementsByTagName('body')[0];

			}

			

			var iWidth;

			var nTmp = document.createElement( "div" );

			nTmp.style.width = sWidth;

			

			nParent.appendChild( nTmp );

			iWidth = nTmp.offsetWidth;

			nParent.removeChild( nTmp );

			

			return ( iWidth );

		}

		

		/*

		 * Function: _fnCalculateColumnWidths

		 * Purpose:  Calculate the width of columns for the table

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnCalculateColumnWidths ( oSettings )

		{

			var iTableWidth = oSettings.nTable.offsetWidth;

			var iUserInputs = 0;

			var iTmpWidth;

			var iVisibleColumns = 0;

			var iColums = oSettings.aoColumns.length;

			var i;

			var oHeaders = $('th', oSettings.nTHead);

			

			/* Convert any user input sizes into pixel sizes */

			for ( i=0 ; i<iColums ; i++ )

			{

				if ( oSettings.aoColumns[i].bVisible )

				{

					iVisibleColumns++;

					

					if ( oSettings.aoColumns[i].sWidth !== null )

					{

						iTmpWidth = _fnConvertToWidth( oSettings.aoColumns[i].sWidthOrig, 

							oSettings.nTable.parentNode );

						if ( iTmpWidth !== null )

						{

							oSettings.aoColumns[i].sWidth = _fnStringToCss( iTmpWidth );

						}

							

						iUserInputs++;

					}

				}

			}

			

			/* If the number of columns in the DOM equals the number that we have to process in 

			 * DataTables, then we can use the offsets that are created by the web-browser. No custom 

			 * sizes can be set in order for this to happen, nor scrolling used

			 */

			if ( iColums == oHeaders.length && iUserInputs === 0 && iVisibleColumns == iColums &&

				oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "" )

			{

				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )

				{

					iTmpWidth = $(oHeaders[i]).width();

					if ( iTmpWidth !== null )

					{

						oSettings.aoColumns[i].sWidth = _fnStringToCss( iTmpWidth );

					}

				}

			}

			else

			{

				/* Otherwise we are going to have to do some calculations to get the width of each column.

				 * Construct a 1 row table with the widest node in the data, and any user defined widths,

				 * then insert it into the DOM and allow the browser to do all the hard work of

				 * calculating table widths.

				 */

				var

					nCalcTmp = oSettings.nTable.cloneNode( false ),

					nBody = document.createElement( 'tbody' ),

					nTr = document.createElement( 'tr' ),

					nDivSizing;

				

				nCalcTmp.removeAttribute( "id" );

				nCalcTmp.appendChild( oSettings.nTHead.cloneNode(true) );

				if ( oSettings.nTFoot !== null )

				{

					nCalcTmp.appendChild( oSettings.nTFoot.cloneNode(true) );

					_fnApplyToChildren( function(n) {

						n.style.width = "";

					}, nCalcTmp.getElementsByTagName('tr') );

				}

				

				nCalcTmp.appendChild( nBody );

				nBody.appendChild( nTr );

				

				/* Remove any sizing that was previously applied by the styles */

				var jqColSizing = $('thead th', nCalcTmp);

				if ( jqColSizing.length === 0 )

				{

					jqColSizing = $('tbody tr:eq(0)>td', nCalcTmp);

				}

				jqColSizing.each( function (i) {

					this.style.width = "";

					

					var iIndex = _fnVisibleToColumnIndex( oSettings, i );

					if ( iIndex !== null && oSettings.aoColumns[iIndex].sWidthOrig !== "" )

					{

						this.style.width = oSettings.aoColumns[iIndex].sWidthOrig;

					}

				} );

				

				/* Find the biggest td for each column and put it into the table */

				for ( i=0 ; i<iColums ; i++ )

				{

					if ( oSettings.aoColumns[i].bVisible )

					{

						var nTd = _fnGetWidestNode( oSettings, i );

						if ( nTd !== null )

						{

							nTd = nTd.cloneNode(true);

							nTr.appendChild( nTd );

						}

					}

				}

				

				/* Build the table and 'display' it */

				var nWrapper = oSettings.nTable.parentNode;

				nWrapper.appendChild( nCalcTmp );

				

				/* When scrolling (X or Y) we want to set the width of the table as appropriate. However,

				 * when not scrolling leave the table width as it is. This results in slightly different,

				 * but I think correct behaviour

				 */

				if ( oSettings.oScroll.sX !== "" && oSettings.oScroll.sXInner !== "" )

				{

					nCalcTmp.style.width = _fnStringToCss(oSettings.oScroll.sXInner);

				}

				else if ( oSettings.oScroll.sX !== "" )

				{

					nCalcTmp.style.width = "";

					if ( $(nCalcTmp).width() < nWrapper.offsetWidth )

					{

						nCalcTmp.style.width = _fnStringToCss( nWrapper.offsetWidth );

					}

				}

				else if ( oSettings.oScroll.sY !== "" )

				{

					nCalcTmp.style.width = _fnStringToCss( nWrapper.offsetWidth );

				}

				nCalcTmp.style.visibility = "hidden";

				

				/* Scrolling considerations */

				_fnScrollingWidthAdjust( oSettings, nCalcTmp );

				

				/* Read the width's calculated by the browser and store them for use by the caller. We

				 * first of all try to use the elements in the body, but it is possible that there are

				 * no elements there, under which circumstances we use the header elements

				 */

				var oNodes = $("tbody tr:eq(0)>td", nCalcTmp);

				if ( oNodes.length === 0 )

				{

					oNodes = $("thead tr:eq(0)>th", nCalcTmp);

				}

				

				var iIndex, iCorrector = 0, iWidth;

				for ( i=0 ; i<oSettings.aoColumns.length ; i++ )

				{

					if ( oSettings.aoColumns[i].bVisible )

					{

						iWidth = $(oNodes[iCorrector]).width();

						if ( iWidth !== null && iWidth > 0 )

						{

							oSettings.aoColumns[i].sWidth = _fnStringToCss( iWidth );

						}

						iCorrector++;

					}

				}

				

				oSettings.nTable.style.width = _fnStringToCss( $(nCalcTmp).outerWidth() );

				nCalcTmp.parentNode.removeChild( nCalcTmp );

			}

		}

		

		/*

		 * Function: _fnScrollingWidthAdjust

		 * Purpose:  Adjust a table's width to take account of scrolling

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           node:n - table node

		 */

		function _fnScrollingWidthAdjust ( oSettings, n )

		{

			if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY !== "" )

			{

				/* When y-scrolling only, we want to remove the width of the scroll bar so the table

				 * + scroll bar will fit into the area avaialble.

				 */

				var iOrigWidth = $(n).width();

				n.style.width = _fnStringToCss( $(n).outerWidth()-oSettings.oScroll.iBarWidth );

			}

			else if ( oSettings.oScroll.sX !== "" )

			{

				/* When x-scrolling both ways, fix the table at it's current size, without adjusting */

				n.style.width = _fnStringToCss( $(n).outerWidth() );

			}

		}

		

		/*

		 * Function: _fnGetWidestNode

		 * Purpose:  Get the widest node

		 * Returns:  string: - max strlens for each column

		 * Inputs:   object:oSettings - dataTables settings object

		 *           int:iCol - column of interest

		 *           boolean:bFast - Should we use fast (but non-accurate) calculation - optional,

		 *             default true

		 * Notes:    This operation is _expensive_ (!!!). It requires a lot of DOM interaction, but

		 *   this is the only way to reliably get the widest string. For example 'mmm' would be wider

		 *   than 'iiii' so we can't just ocunt characters. If this can be optimised it would be good

		 *   to do so!

		 */

		function _fnGetWidestNode( oSettings, iCol, bFast )

		{

			/* Use fast not non-accurate calculate based on the strlen */

			if ( typeof bFast == 'undefined' || bFast )

			{

				var iMaxLen = _fnGetMaxLenString( oSettings, iCol );

				var iFastVis = _fnColumnIndexToVisible( oSettings, iCol);

				if ( iMaxLen < 0 )

				{

					return null;

				}

				return oSettings.aoData[iMaxLen].nTr.getElementsByTagName('td')[iFastVis];

			}

			

			/* Use the slow approach, but get high quality answers - note that this code is not actually

			 * used by DataTables by default. If you want to use it you can alter the call to 

			 * _fnGetWidestNode to pass 'false' as the third argument

			 */

			var

				iMax = -1, i, iLen,

				iMaxIndex = -1,

				n = document.createElement('div');

			

			n.style.visibility = "hidden";

			n.style.position = "absolute";

			document.body.appendChild( n );

			

			for ( i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )

			{

				n.innerHTML = oSettings.aoData[i]._aData[iCol];

				if ( n.offsetWidth > iMax )

				{

					iMax = n.offsetWidth;

					iMaxIndex = i;

				}

			}

			document.body.removeChild( n );

			

			if ( iMaxIndex >= 0 )

			{

				var iVis = _fnColumnIndexToVisible( oSettings, iCol);

				var nRet = oSettings.aoData[iMaxIndex].nTr.getElementsByTagName('td')[iVis];

				if ( nRet )

				{

					return nRet;

				}

			}

			return null;

		}

		

		/*

		 * Function: _fnGetMaxLenString

		 * Purpose:  Get the maximum strlen for each data column

		 * Returns:  string: - max strlens for each column

		 * Inputs:   object:oSettings - dataTables settings object

		 *           int:iCol - column of interest

		 */

		function _fnGetMaxLenString( oSettings, iCol )

		{

			var iMax = -1;

			var iMaxIndex = -1;

			

			for ( var i=0 ; i<oSettings.aoData.length ; i++ )

			{

				var s = oSettings.aoData[i]._aData[iCol];

				if ( s.length > iMax )

				{

					iMax = s.length;

					iMaxIndex = i;

				}

			}

			

			return iMaxIndex;

		}

		

		/*

		 * Function: _fnStringToCss

		 * Purpose:  Append a CSS unit (only if required) to a string

		 * Returns:  0 if match, 1 if length is different, 2 if no match

		 * Inputs:   array:aArray1 - first array

		 *           array:aArray2 - second array

		 */

		function _fnStringToCss( s )

		{

			if ( s === null )

			{

				return "0px";

			}

			

			if ( typeof s == 'number' )

			{

				if ( s < 0 )

				{

					return "0px";

				}

				return s+"px";

			}

			

			/* Check if the last character is not 0-9 */

			var c = s.charCodeAt( s.length-1 );

			if (c < 0x30 || c > 0x39)

			{

				return s;

			}

			return s+"px";

		}

		

		/*

		 * Function: _fnArrayCmp

		 * Purpose:  Compare two arrays

		 * Returns:  0 if match, 1 if length is different, 2 if no match

		 * Inputs:   array:aArray1 - first array

		 *           array:aArray2 - second array

		 */

		function _fnArrayCmp( aArray1, aArray2 )

		{

			if ( aArray1.length != aArray2.length )

			{

				return 1;

			}

			

			for ( var i=0 ; i<aArray1.length ; i++ )

			{

				if ( aArray1[i] != aArray2[i] )

				{

					return 2;

				}

			}

			

			return 0;

		}

		

		/*

		 * Function: _fnDetectType

		 * Purpose:  Get the sort type based on an input string

		 * Returns:  string: - type (defaults to 'string' if no type can be detected)

		 * Inputs:   string:sData - data we wish to know the type of

		 * Notes:    This function makes use of the DataTables plugin objct _oExt 

		 *   (.aTypes) such that new types can easily be added.

		 */

		function _fnDetectType( sData )

		{

			var aTypes = _oExt.aTypes;

			var iLen = aTypes.length;

			

			for ( var i=0 ; i<iLen ; i++ )

			{

				var sType = aTypes[i]( sData );

				if ( sType !== null )

				{

					return sType;

				}

			}

			

			return 'string';

		}

		

		/*

		 * Function: _fnSettingsFromNode

		 * Purpose:  Return the settings object for a particular table

		 * Returns:  object: Settings object - or null if not found

		 * Inputs:   node:nTable - table we are using as a dataTable

		 */

		function _fnSettingsFromNode ( nTable )

		{

			for ( var i=0 ; i<_aoSettings.length ; i++ )

			{

				if ( _aoSettings[i].nTable == nTable )

				{

					return _aoSettings[i];

				}

			}

			

			return null;

		}

		

		/*

		 * Function: _fnGetDataMaster

		 * Purpose:  Return an array with the full table data

		 * Returns:  array array:aData - Master data array

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnGetDataMaster ( oSettings )

		{

			var aData = [];

			var iLen = oSettings.aoData.length;

			for ( var i=0 ; i<iLen; i++ )

			{

				aData.push( oSettings.aoData[i]._aData );

			}

			return aData;

		}

		

		/*

		 * Function: _fnGetTrNodes

		 * Purpose:  Return an array with the TR nodes for the table

		 * Returns:  array: - TR array

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnGetTrNodes ( oSettings )

		{

			var aNodes = [];

			var iLen = oSettings.aoData.length;

			for ( var i=0 ; i<iLen ; i++ )

			{

				aNodes.push( oSettings.aoData[i].nTr );

			}

			return aNodes;

		}

		

		/*

		 * Function: _fnGetTdNodes

		 * Purpose:  Return an array with the TD nodes for the table

		 * Returns:  array: - TD array

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnGetTdNodes ( oSettings )

		{

			var nTrs = _fnGetTrNodes( oSettings );

			var nTds = [], nTd;

			var anReturn = [];

			var iCorrector;

			var iRow, iRows, iColumn, iColumns;

			

			for ( iRow=0, iRows=nTrs.length ; iRow<iRows ; iRow++ )

			{

				nTds = [];

				for ( iColumn=0, iColumns=nTrs[iRow].childNodes.length ; iColumn<iColumns ; iColumn++ )

				{

					nTd = nTrs[iRow].childNodes[iColumn];

					if ( nTd.nodeName.toUpperCase() == "TD" )

					{

						nTds.push( nTd );

					}

				}

				

				iCorrector = 0;

				for ( iColumn=0, iColumns=oSettings.aoColumns.length ; iColumn<iColumns ; iColumn++ )

				{

					if ( oSettings.aoColumns[iColumn].bVisible )

					{

						anReturn.push( nTds[iColumn-iCorrector] );

					}

					else

					{

						anReturn.push( oSettings.aoData[iRow]._anHidden[iColumn] );

						iCorrector++;

					}

				}

			}

			return anReturn;

		}

		

		/*

		 * Function: _fnEscapeRegex

		 * Purpose:  scape a string stuch that it can be used in a regular expression

		 * Returns:  string: - escaped string

		 * Inputs:   string:sVal - string to escape

		 */

		function _fnEscapeRegex ( sVal )

		{

			var acEscape = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^' ];

		  var reReplace = new RegExp( '(\\' + acEscape.join('|\\') + ')', 'g' );

		  return sVal.replace(reReplace, '\\$1');

		}

		

		/*

		 * Function: _fnDeleteIndex

		 * Purpose:  Take an array of integers (index array) and remove a target integer (value - not 

		 *             the key!)

		 * Returns:  -

		 * Inputs:   a:array int - Index array to target

		 *           int:iTarget - value to find

		 */

		function _fnDeleteIndex( a, iTarget )

		{

			var iTargetIndex = -1;

			

			for ( var i=0, iLen=a.length ; i<iLen ; i++ )

			{

				if ( a[i] == iTarget )

				{

					iTargetIndex = i;

				}

				else if ( a[i] > iTarget )

				{

					a[i]--;

				}

			}

			

			if ( iTargetIndex != -1 )

			{

				a.splice( iTargetIndex, 1 );

			}

		}

		

		/*

		 * Function: _fnReOrderIndex

		 * Purpose:  Figure out how to reorder a display list

		 * Returns:  array int:aiReturn - index list for reordering

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnReOrderIndex ( oSettings, sColumns )

		{

			var aColumns = sColumns.split(',');

			var aiReturn = [];

			

			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )

			{

				for ( var j=0 ; j<iLen ; j++ )

				{

					if ( oSettings.aoColumns[i].sName == aColumns[j] )

					{

						aiReturn.push( j );

						break;

					}

				}

			}

			

			return aiReturn;

		}

		

		/*

		 * Function: _fnColumnOrdering

		 * Purpose:  Get the column ordering that DataTables expects

		 * Returns:  string: - comma separated list of names

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnColumnOrdering ( oSettings )

		{

			var sNames = '';

			for ( var i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ )

			{

				sNames += oSettings.aoColumns[i].sName+',';

			}

			if ( sNames.length == iLen )

			{

				return "";

			}

			return sNames.slice(0, -1);

		}

		

		/*

		 * Function: _fnLog

		 * Purpose:  Log an error message

		 * Returns:  -

		 * Inputs:   int:iLevel - log error messages, or display them to the user

		 *           string:sMesg - error message

		 */

		function _fnLog( oSettings, iLevel, sMesg )

		{

			var sAlert = oSettings.sTableId === "" ?

			 	"DataTables warning: " +sMesg :

			 	"DataTables warning (table id = '"+oSettings.sTableId+"'): " +sMesg;

			

			if ( iLevel === 0 )

			{

				if ( _oExt.sErrMode == 'alert' )

				{

					alert( sAlert );

				}

				else

				{

					throw sAlert;

				}

				return;

			}

			else if ( typeof console != 'undefined' && typeof console.log != 'undefined' )

			{

				console.log( sAlert );

			}

		}

		

		/*

		 * Function: _fnClearTable

		 * Purpose:  Nuke the table

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnClearTable( oSettings )

		{

			oSettings.aoData.splice( 0, oSettings.aoData.length );

			oSettings.aiDisplayMaster.splice( 0, oSettings.aiDisplayMaster.length );

			oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length );

			_fnCalculateEnd( oSettings );

		}

		

		/*

		 * Function: _fnSaveState

		 * Purpose:  Save the state of a table in a cookie such that the page can be reloaded

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 */

		function _fnSaveState ( oSettings )

		{

			if ( !oSettings.oFeatures.bStateSave || typeof oSettings.bDestroying != 'undefined' )

			{

				return;

			}

			

			/* Store the interesting variables */

			var i, iLen, sTmp;

			var sValue = "{";

			sValue += '"iCreate":'+ new Date().getTime()+',';

			sValue += '"iStart":'+ oSettings._iDisplayStart+',';

			sValue += '"iEnd":'+ oSettings._iDisplayEnd+',';

			sValue += '"iLength":'+ oSettings._iDisplayLength+',';

			sValue += '"sFilter":"'+ encodeURIComponent(oSettings.oPreviousSearch.sSearch)+'",';

			sValue += '"sFilterEsc":'+ !oSettings.oPreviousSearch.bRegex+',';

			

			sValue += '"aaSorting":[ ';

			for ( i=0 ; i<oSettings.aaSorting.length ; i++ )

			{

				sValue += '['+oSettings.aaSorting[i][0]+',"'+oSettings.aaSorting[i][1]+'"],';

			}

			sValue = sValue.substring(0, sValue.length-1);

			sValue += "],";

			

			sValue += '"aaSearchCols":[ ';

			for ( i=0 ; i<oSettings.aoPreSearchCols.length ; i++ )

			{

				sValue += '["'+encodeURIComponent(oSettings.aoPreSearchCols[i].sSearch)+

					'",'+!oSettings.aoPreSearchCols[i].bRegex+'],';

			}

			sValue = sValue.substring(0, sValue.length-1);

			sValue += "],";

			

			sValue += '"abVisCols":[ ';

			for ( i=0 ; i<oSettings.aoColumns.length ; i++ )

			{

				sValue += oSettings.aoColumns[i].bVisible+",";

			}

			sValue = sValue.substring(0, sValue.length-1);

			sValue += "]";

			

			/* Save state from any plug-ins */

			for ( i=0, iLen=oSettings.aoStateSave.length ; i<iLen ; i++ )

			{

				sTmp = oSettings.aoStateSave[i].fn( oSettings, sValue );

				if ( sTmp !== "" )

				{

					sValue = sTmp;

				}

			}

			

			sValue += "}";

			

			_fnCreateCookie( oSettings.sCookiePrefix+oSettings.sInstance, sValue, 

				oSettings.iCookieDuration, oSettings.sCookiePrefix, oSettings.fnCookieCallback );

		}

		

		/*

		 * Function: _fnLoadState

		 * Purpose:  Attempt to load a saved table state from a cookie

		 * Returns:  -

		 * Inputs:   object:oSettings - dataTables settings object

		 *           object:oInit - DataTables init object so we can override settings

		 */

		function _fnLoadState ( oSettings, oInit )

		{

			if ( !oSettings.oFeatures.bStateSave )

			{

				return;

			}

			

			var oData, i, iLen;

			var sData = _fnReadCookie( oSettings.sCookiePrefix+oSettings.sInstance );

			if ( sData !== null && sData !== '' )

			{

				/* Try/catch the JSON eval - if it is bad then we ignore it - note that 1.7.0 and before

				 * incorrectly used single quotes for some strings - hence the replace below

				 */

				try

				{

					oData = (typeof $.parseJSON == 'function') ? 

						$.parseJSON( sData.replace(/'/g, '"') ) : eval( '('+sData+')' );

				}

				catch( e )

				{

					return;

				}

				

				/* Allow custom and plug-in manipulation functions to alter the data set which was

				 * saved, and also reject any saved state by returning false

				 */

				for ( i=0, iLen=oSettings.aoStateLoad.length ; i<iLen ; i++ )

				{

					if ( !oSettings.aoStateLoad[i].fn( oSettings, oData ) )

					{

						return;

					}

				}

				

				/* Store the saved state so it might be accessed at any time (particualrly a plug-in */

				oSettings.oLoadedState = $.extend( true, {}, oData );

				

				/* Restore key features */

				oSettings._iDisplayStart = oData.iStart;

				oSettings.iInitDisplayStart = oData.iStart;

				oSettings._iDisplayEnd = oData.iEnd;

				oSettings._iDisplayLength = oData.iLength;

				oSettings.oPreviousSearch.sSearch = decodeURIComponent(oData.sFilter);

				oSettings.aaSorting = oData.aaSorting.slice();

				oSettings.saved_aaSorting = oData.aaSorting.slice();

				

				/*

				 * Search filtering - global reference added in 1.4.1

				 * Note that we use a 'not' for the value of the regular expression indicator to maintain

				 * compatibility with pre 1.7 versions, where this was basically inverted. Added in 1.7.0

				 */

				if ( typeof oData.sFilterEsc != 'undefined' )

				{

					oSettings.oPreviousSearch.bRegex = !oData.sFilterEsc;

				}

				

				/* Column filtering - added in 1.5.0 beta 6 */

				if ( typeof oData.aaSearchCols != 'undefined' )

				{

					for ( i=0 ; i<oData.aaSearchCols.length ; i++ )

					{

						oSettings.aoPreSearchCols[i] = {

							"sSearch": decodeURIComponent(oData.aaSearchCols[i][0]),

							"bRegex": !oData.aaSearchCols[i][1]

						};

					}

				}

				

				/* Column visibility state - added in 1.5.0 beta 10 */

				if ( typeof oData.abVisCols != 'undefined' )

				{

					/* Pass back visibiliy settings to the init handler, but to do not here override

					 * the init object that the user might have passed in

					 */

					oInit.saved_aoColumns = [];

					for ( i=0 ; i<oData.abVisCols.length ; i++ )

					{

						oInit.saved_aoColumns[i] = {};

						oInit.saved_aoColumns[i].bVisible = oData.abVisCols[i];

					}

				}

			}

		}

		

		/*

		 * Function: _fnCreateCookie

		 * Purpose:  Create a new cookie with a value to store the state of a table

		 * Returns:  -

		 * Inputs:   string:sName - name of the cookie to create

		 *           string:sValue - the value the cookie should take

		 *           int:iSecs - duration of the cookie

		 *           string:sBaseName - sName is made up of the base + file name - this is the base

		 *           function:fnCallback - User definable function to modify the cookie

		 */

		function _fnCreateCookie ( sName, sValue, iSecs, sBaseName, fnCallback )

		{

			var date = new Date();

			date.setTime( date.getTime()+(iSecs*1000) );

			

			/* 

			 * Shocking but true - it would appear IE has major issues with having the path not having

			 * a trailing slash on it. We need the cookie to be available based on the path, so we

			 * have to append the file name to the cookie name. Appalling. Thanks to vex for adding the

			 * patch to use at least some of the path

			 */

			var aParts = window.location.pathname.split('/');

			var sNameFile = sName + '_' + aParts.pop().replace(/[\/:]/g,"").toLowerCase();

			var sFullCookie, oData;

			

			if ( fnCallback !== null )

			{

				oData = (typeof $.parseJSON == 'function') ? 

					$.parseJSON( sValue ) : eval( '('+sValue+')' );

				sFullCookie = fnCallback( sNameFile, oData, date.toGMTString(),

					aParts.join('/')+"/" );

			}

			else

			{

				sFullCookie = sNameFile + "=" + encodeURIComponent(sValue) +

					"; expires=" + date.toGMTString() +"; path=" + aParts.join('/')+"/";

			}

			

			/* Are we going to go over the cookie limit of 4KiB? If so, try to delete a cookies

			 * belonging to DataTables. This is FAR from bullet proof

			 */

			var sOldName="", iOldTime=9999999999999;

			var iLength = _fnReadCookie( sNameFile )!==null ? document.cookie.length : 

				sFullCookie.length + document.cookie.length;

			

			if ( iLength+10 > 4096 ) /* Magic 10 for padding */

			{

				var aCookies =document.cookie.split(';');

				for ( var i=0, iLen=aCookies.length ; i<iLen ; i++ )

				{

					if ( aCookies[i].indexOf( sBaseName ) != -1 )

					{

						/* It's a DataTables cookie, so eval it and check the time stamp */

						var aSplitCookie = aCookies[i].split('=');

						try { oData = eval( '('+decodeURIComponent(aSplitCookie[1])+')' ); }

						catch( e ) { continue; }

						

						if ( typeof oData.iCreate != 'undefined' && oData.iCreate < iOldTime )

						{

							sOldName = aSplitCookie[0];

							iOldTime = oData.iCreate;

						}

					}

				}

				

				if ( sOldName !== "" )

				{

					document.cookie = sOldName+"=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path="+

						aParts.join('/') + "/";

				}

			}

			

			document.cookie = sFullCookie;

		}

		

		/*

		 * Function: _fnReadCookie

		 * Purpose:  Read an old cookie to get a cookie with an old table state

		 * Returns:  string: - contents of the cookie - or null if no cookie with that name found

		 * Inputs:   string:sName - name of the cookie to read

		 */

		function _fnReadCookie ( sName )

		{

			var

				aParts = window.location.pathname.split('/'),

				sNameEQ = sName + '_' + aParts[aParts.length-1].replace(/[\/:]/g,"").toLowerCase() + '=',

			 	sCookieContents = document.cookie.split(';');

			

			for( var i=0 ; i<sCookieContents.length ; i++ )

			{

				var c = sCookieContents[i];

				

				while (c.charAt(0)==' ')

				{

					c = c.substring(1,c.length);

				}

				

				if (c.indexOf(sNameEQ) === 0)

				{

					return decodeURIComponent( c.substring(sNameEQ.length,c.length) );

				}

			}

			return null;

		}

		

		/*

		 * Function: _fnGetUniqueThs

		 * Purpose:  Get an array of unique th elements, one for each column

		 * Returns:  array node:aReturn - list of unique ths

		 * Inputs:   node:nThead - The thead element for the table

		 */

		function _fnGetUniqueThs ( nThead )

		{

			var nTrs = nThead.getElementsByTagName('tr');

			

			/* Nice simple case */

			if ( nTrs.length == 1 )

			{

				return nTrs[0].getElementsByTagName('th');

			}

			

			/* Otherwise we need to figure out the layout array to get the nodes */

			var aLayout = [], aReturn = [];

			var ROWSPAN = 2, COLSPAN = 3, TDELEM = 4;

			var i, j, k, iLen, jLen, iColumnShifted;

			var fnShiftCol = function ( a, i, j ) {

				while ( typeof a[i][j] != 'undefined' ) {

					j++;

				}

				return j;

			};

			var fnAddRow = function ( i ) {

				if ( typeof aLayout[i] == 'undefined' ) {

					aLayout[i] = [];

				}

			};

			

			/* Calculate a layout array */

			for ( i=0, iLen=nTrs.length ; i<iLen ; i++ )

			{

				fnAddRow( i );

				var iColumn = 0;

				var nTds = [];

				

				for ( j=0, jLen=nTrs[i].childNodes.length ; j<jLen ; j++ )

				{

					if ( nTrs[i].childNodes[j].nodeName.toUpperCase() == "TD" ||

					     nTrs[i].childNodes[j].nodeName.toUpperCase() == "TH" )

					{

						nTds.push( nTrs[i].childNodes[j] );

					}

				}

				

				for ( j=0, jLen=nTds.length ; j<jLen ; j++ )

				{

					var iColspan = nTds[j].getAttribute('colspan') * 1;

					var iRowspan = nTds[j].getAttribute('rowspan') * 1;

					

					if ( !iColspan || iColspan===0 || iColspan===1 )

					{

						iColumnShifted = fnShiftCol( aLayout, i, iColumn );

						aLayout[i][iColumnShifted] = (nTds[j].nodeName.toUpperCase()=="TD") ? TDELEM : nTds[j];

						if ( iRowspan || iRowspan===0 || iRowspan===1 )

						{

							for ( k=1 ; k<iRowspan ; k++ )

							{

								fnAddRow( i+k );

								aLayout[i+k][iColumnShifted] = ROWSPAN;

							}

						}

						iColumn++;

					}

					else

					{

						iColumnShifted = fnShiftCol( aLayout, i, iColumn );

						for ( k=0 ; k<iColspan ; k++ )

						{

							aLayout[i][iColumnShifted+k] = COLSPAN;

						}

						iColumn += iColspan;

					}

				}

			}

			

			/* Convert the layout array into a node array */

			for ( i=0, iLen=aLayout.length ; i<iLen ; i++ )

			{

				for ( j=0, jLen=aLayout[i].length ; j<jLen ; j++ )

				{

					if ( typeof aLayout[i][j] == 'object' )

					{

						aReturn[j] = aLayout[i][j];

					}

				}

			}

			

			return aReturn;

		}

		

		/*

		 * Function: _fnScrollBarWidth

		 * Purpose:  Get the width of a scroll bar in this browser being used

		 * Returns:  int: - width in pixels

		 * Inputs:   -

		 * Notes:    All credit for this function belongs to Alexandre Gomes. Thanks for sharing!

		 *   http://www.alexandre-gomes.com/?p=115

		 */

		function _fnScrollBarWidth ()

		{  

			var inner = document.createElement('p');  

			var style = inner.style;

			style.width = "100%";  

			style.height = "200px";  

			

			var outer = document.createElement('div');  

			style = outer.style;

			style.position = "absolute";  

			style.top = "0px";  

			style.left = "0px";  

			style.visibility = "hidden";  

			style.width = "200px";  

			style.height = "150px";  

			style.overflow = "hidden";  

			outer.appendChild(inner);  

			

			document.body.appendChild(outer);  

			var w1 = inner.offsetWidth;  

			outer.style.overflow = 'scroll';  

			var w2 = inner.offsetWidth;  

			if ( w1 == w2 )

			{

				w2 = outer.clientWidth;  

			}

			

			document.body.removeChild(outer); 

			return (w1 - w2);  

		}

		

		/*

		 * Function: _fnApplyToChildren

		 * Purpose:  Apply a given function to the display child nodes of an element array (typically

		 *   TD children of TR rows

		 * Returns:  - (done by reference)

		 * Inputs:   function:fn - Method to apply to the objects

		 *           array nodes:an1 - List of elements to look through for display children

		 *           array nodes:an2 - Another list (identical structure to the first) - optional

		 */

		function _fnApplyToChildren( fn, an1, an2 )

		{

			for ( var i=0, iLen=an1.length ; i<iLen ; i++ )

			{

				for ( var j=0, jLen=an1[i].childNodes.length ; j<jLen ; j++ )

				{

					if ( an1[i].childNodes[j].nodeType == 1 )

					{

						if ( typeof an2 != 'undefined' )

						{

							fn( an1[i].childNodes[j], an2[i].childNodes[j] );

						}

						else

						{

							fn( an1[i].childNodes[j] );

						}

					}

				}

			}

		}

		

		/*

		 * Function: _fnMap

		 * Purpose:  See if a property is defined on one object, if so assign it to the other object

		 * Returns:  - (done by reference)

		 * Inputs:   object:oRet - target object

		 *           object:oSrc - source object

		 *           string:sName - property

		 *           string:sMappedName - name to map too - optional, sName used if not given

		 */

		function _fnMap( oRet, oSrc, sName, sMappedName )

		{

			if ( typeof sMappedName == 'undefined' )

			{

				sMappedName = sName;

			}

			if ( typeof oSrc[sName] != 'undefined' )

			{

				oRet[sMappedName] = oSrc[sName];

			}

		}

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - API

		 * 

		 * I'm not overly happy with this solution - I'd much rather that there was a way of getting

		 * a list of all the private functions and do what we need to dynamically - but that doesn't

		 * appear to be possible. Bonkers. A better solution would be to provide a 'bind' type object

		 * To do - bind type method in DTs 2.x.

		 */

		this.oApi._fnExternApiFunc = _fnExternApiFunc;

		this.oApi._fnInitalise = _fnInitalise;

		this.oApi._fnLanguageProcess = _fnLanguageProcess;

		this.oApi._fnAddColumn = _fnAddColumn;

		this.oApi._fnColumnOptions = _fnColumnOptions;

		this.oApi._fnAddData = _fnAddData;

		this.oApi._fnGatherData = _fnGatherData;

		this.oApi._fnDrawHead = _fnDrawHead;

		this.oApi._fnDraw = _fnDraw;

		this.oApi._fnReDraw = _fnReDraw;

		this.oApi._fnAjaxUpdate = _fnAjaxUpdate;

		this.oApi._fnAjaxUpdateDraw = _fnAjaxUpdateDraw;

		this.oApi._fnAddOptionsHtml = _fnAddOptionsHtml;

		this.oApi._fnFeatureHtmlTable = _fnFeatureHtmlTable;

		this.oApi._fnScrollDraw = _fnScrollDraw;

		this.oApi._fnAjustColumnSizing = _fnAjustColumnSizing;

		this.oApi._fnFeatureHtmlFilter = _fnFeatureHtmlFilter;

		this.oApi._fnFilterComplete = _fnFilterComplete;

		this.oApi._fnFilterCustom = _fnFilterCustom;

		this.oApi._fnFilterColumn = _fnFilterColumn;

		this.oApi._fnFilter = _fnFilter;

		this.oApi._fnBuildSearchArray = _fnBuildSearchArray;

		this.oApi._fnBuildSearchRow = _fnBuildSearchRow;

		this.oApi._fnFilterCreateSearch = _fnFilterCreateSearch;

		this.oApi._fnDataToSearch = _fnDataToSearch;

		this.oApi._fnSort = _fnSort;

		this.oApi._fnSortAttachListener = _fnSortAttachListener;

		this.oApi._fnSortingClasses = _fnSortingClasses;

		this.oApi._fnFeatureHtmlPaginate = _fnFeatureHtmlPaginate;

		this.oApi._fnPageChange = _fnPageChange;

		this.oApi._fnFeatureHtmlInfo = _fnFeatureHtmlInfo;

		this.oApi._fnUpdateInfo = _fnUpdateInfo;

		this.oApi._fnFeatureHtmlLength = _fnFeatureHtmlLength;

		this.oApi._fnFeatureHtmlProcessing = _fnFeatureHtmlProcessing;

		this.oApi._fnProcessingDisplay = _fnProcessingDisplay;

		this.oApi._fnVisibleToColumnIndex = _fnVisibleToColumnIndex;

		this.oApi._fnColumnIndexToVisible = _fnColumnIndexToVisible;

		this.oApi._fnNodeToDataIndex = _fnNodeToDataIndex;

		this.oApi._fnVisbleColumns = _fnVisbleColumns;

		this.oApi._fnCalculateEnd = _fnCalculateEnd;

		this.oApi._fnConvertToWidth = _fnConvertToWidth;

		this.oApi._fnCalculateColumnWidths = _fnCalculateColumnWidths;

		this.oApi._fnScrollingWidthAdjust = _fnScrollingWidthAdjust;

		this.oApi._fnGetWidestNode = _fnGetWidestNode;

		this.oApi._fnGetMaxLenString = _fnGetMaxLenString;

		this.oApi._fnStringToCss = _fnStringToCss;

		this.oApi._fnArrayCmp = _fnArrayCmp;

		this.oApi._fnDetectType = _fnDetectType;

		this.oApi._fnSettingsFromNode = _fnSettingsFromNode;

		this.oApi._fnGetDataMaster = _fnGetDataMaster;

		this.oApi._fnGetTrNodes = _fnGetTrNodes;

		this.oApi._fnGetTdNodes = _fnGetTdNodes;

		this.oApi._fnEscapeRegex = _fnEscapeRegex;

		this.oApi._fnDeleteIndex = _fnDeleteIndex;

		this.oApi._fnReOrderIndex = _fnReOrderIndex;

		this.oApi._fnColumnOrdering = _fnColumnOrdering;

		this.oApi._fnLog = _fnLog;

		this.oApi._fnClearTable = _fnClearTable;

		this.oApi._fnSaveState = _fnSaveState;

		this.oApi._fnLoadState = _fnLoadState;

		this.oApi._fnCreateCookie = _fnCreateCookie;

		this.oApi._fnReadCookie = _fnReadCookie;

		this.oApi._fnGetUniqueThs = _fnGetUniqueThs;

		this.oApi._fnScrollBarWidth = _fnScrollBarWidth;

		this.oApi._fnApplyToChildren = _fnApplyToChildren;

		this.oApi._fnMap = _fnMap;

		

		

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

		 * Section - Constructor

		 */

		

		/* Want to be able to reference "this" inside the this.each function */

		var _that = this;

		return this.each(function()

		{

			var i=0, iLen, j, jLen, k, kLen;

			

			/* Check to see if we are re-initalising a table */

			for ( i=0, iLen=_aoSettings.length ; i<iLen ; i++ )

			{

				/* Base check on table node */

				if ( _aoSettings[i].nTable == this )

				{

					if ( typeof oInit == 'undefined' || 

					   ( typeof oInit.bRetrieve != 'undefined' && oInit.bRetrieve === true ) )

					{

						return _aoSettings[i].oInstance;

					}

					else if ( typeof oInit.bDestroy != 'undefined' && oInit.bDestroy === true )

					{

						_aoSettings[i].oInstance.fnDestroy();

						break;

					}

					else

					{

						_fnLog( _aoSettings[i], 0, "Cannot reinitialise DataTable.\n\n"+

							"To retrieve the DataTables object for this table, please pass either no arguments "+

							"to the dataTable() function, or set bRetrieve to true. Alternatively, to destory "+

							"the old table and create a new one, set bDestroy to true (note that a lot of "+

							"changes to the configuration can be made through the API which is usually much "+

							"faster)." );

						return;

					}

				}

				

				/* If the element we are initialising has the same ID as a table which was previously

				 * initialised, but the table nodes don't match (from before) then we destory the old

				 * instance by simply deleting it. This is under the assumption that the table has been

				 * destroyed by other methods. Anyone using non-id selectors will need to do this manually

				 */

				if ( _aoSettings[i].sTableId !== "" && _aoSettings[i].sTableId == this.getAttribute('id') )

				{

					_aoSettings.splice( i, 1 );

					break;

				}

			}

			

			/* Make a complete and independent copy of the settings object */

			var oSettings = new classSettings();

			_aoSettings.push( oSettings );

			

			var bInitHandedOff = false;

			var bUsePassedData = false;

			

			/* Set the id */

			var sId = this.getAttribute( 'id' );

			if ( sId !== null )

			{

				oSettings.sTableId = sId;

				oSettings.sInstance = sId;

			}

			else

			{

				oSettings.sInstance = _oExt._oExternConfig.iNextUnique ++;

			}

			

			/* Sanity check */

			if ( this.nodeName.toLowerCase() != 'table' )

			{

				_fnLog( oSettings, 0, "Attempted to initialise DataTables on a node which is not a "+

					"table: "+this.nodeName );

				return;

			}

			

			/* Store 'this' in the settings object for later retrieval */

			oSettings.oInstance = _that;

			

			/* Set the table node */

			oSettings.nTable = this;

			

			/* Bind the API functions to the settings, so we can perform actions whenever oSettings is

			 * available

			 */

			oSettings.oApi = _that.oApi;

			

			/* State the table's width for if a destroy is called at a later time */

			oSettings.sDestroyWidth = $(this).width();

			

			/* Store the features that we have available */

			if ( typeof oInit != 'undefined' && oInit !== null )

			{

				oSettings.oInit = oInit;

				_fnMap( oSettings.oFeatures, oInit, "bPaginate" );

				_fnMap( oSettings.oFeatures, oInit, "bLengthChange" );

				_fnMap( oSettings.oFeatures, oInit, "bFilter" );

				_fnMap( oSettings.oFeatures, oInit, "bSort" );

				_fnMap( oSettings.oFeatures, oInit, "bInfo" );

				_fnMap( oSettings.oFeatures, oInit, "bProcessing" );

				_fnMap( oSettings.oFeatures, oInit, "bAutoWidth" );

				_fnMap( oSettings.oFeatures, oInit, "bSortClasses" );

				_fnMap( oSettings.oFeatures, oInit, "bServerSide" );

				_fnMap( oSettings.oScroll, oInit, "sScrollX", "sX" );

				_fnMap( oSettings.oScroll, oInit, "sScrollXInner", "sXInner" );

				_fnMap( oSettings.oScroll, oInit, "sScrollY", "sY" );

				_fnMap( oSettings.oScroll, oInit, "bScrollCollapse", "bCollapse" );

				_fnMap( oSettings.oScroll, oInit, "bScrollInfinite", "bInfinite" );

				_fnMap( oSettings.oScroll, oInit, "iScrollLoadGap", "iLoadGap" );

				_fnMap( oSettings, oInit, "asStripClasses" );

				_fnMap( oSettings, oInit, "fnRowCallback" );

				_fnMap( oSettings, oInit, "fnHeaderCallback" );

				_fnMap( oSettings, oInit, "fnFooterCallback" );

				_fnMap( oSettings, oInit, "fnCookieCallback" );

				_fnMap( oSettings, oInit, "fnInitComplete" );

				_fnMap( oSettings, oInit, "fnServerData" );

				_fnMap( oSettings, oInit, "fnFormatNumber" );

				_fnMap( oSettings, oInit, "aaSorting" );

				_fnMap( oSettings, oInit, "aaSortingFixed" );

				_fnMap( oSettings, oInit, "aLengthMenu" );

				_fnMap( oSettings, oInit, "sPaginationType" );

				_fnMap( oSettings, oInit, "sAjaxSource" );

				_fnMap( oSettings, oInit, "iCookieDuration" );

				_fnMap( oSettings, oInit, "sCookiePrefix" );

				_fnMap( oSettings, oInit, "sDom" );

				_fnMap( oSettings, oInit, "oSearch", "oPreviousSearch" );

				_fnMap( oSettings, oInit, "aoSearchCols", "aoPreSearchCols" );

				_fnMap( oSettings, oInit, "iDisplayLength", "_iDisplayLength" );

				_fnMap( oSettings, oInit, "bJQueryUI", "bJUI" );

				_fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" );

				

				/* Callback functions which are array driven */

				if ( typeof oInit.fnDrawCallback == 'function' )

				{

					oSettings.aoDrawCallback.push( {

						"fn": oInit.fnDrawCallback,

						"sName": "user"

					} );

				}

				

				if ( typeof oInit.fnStateSaveCallback == 'function' )

				{

					oSettings.aoStateSave.push( {

						"fn": oInit.fnStateSaveCallback,

						"sName": "user"

					} );

				}

				

				if ( typeof oInit.fnStateLoadCallback == 'function' )

				{

					oSettings.aoStateLoad.push( {

						"fn": oInit.fnStateLoadCallback,

						"sName": "user"

					} );

				}

				

				if ( oSettings.oFeatures.bServerSide && oSettings.oFeatures.bSort &&

					   oSettings.oFeatures.bSortClasses )

				{

					/* Enable sort classes for server-side processing. Safe to do it here, since server-side

					 * processing must be enabled by the developer

					 */

					oSettings.aoDrawCallback.push( {

						"fn": _fnSortingClasses,

						"sName": "server_side_sort_classes"

					} );

				}

				

				if ( typeof oInit.bJQueryUI != 'undefined' && oInit.bJQueryUI )

				{

					/* Use the JUI classes object for display. You could clone the oStdClasses object if 

					 * you want to have multiple tables with multiple independent classes 

					 */

					oSettings.oClasses = _oExt.oJUIClasses;

					

					if ( typeof oInit.sDom == 'undefined' )

					{

						/* Set the DOM to use a layout suitable for jQuery UI's theming */

						oSettings.sDom = '<"H"lfr>t<"F"ip>';

					}

				}

				

				/* Calculate the scroll bar width and cache it for use later on */

				if ( oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "" )

				{

					oSettings.oScroll.iBarWidth = _fnScrollBarWidth();

				}

				

				if ( typeof oInit.iDisplayStart != 'undefined' && 

				     typeof oSettings.iInitDisplayStart == 'undefined' )

				{

					/* Display start point, taking into account the save saving */

					oSettings.iInitDisplayStart = oInit.iDisplayStart;

					oSettings._iDisplayStart = oInit.iDisplayStart;

				}

				

				/* Must be done after everything which can be overridden by a cookie! */

				if ( typeof oInit.bStateSave != 'undefined' )

				{

					oSettings.oFeatures.bStateSave = oInit.bStateSave;

					_fnLoadState( oSettings, oInit );

					oSettings.aoDrawCallback.push( {

						"fn": _fnSaveState,

						"sName": "state_save"

					} );

				}

				

				if ( typeof oInit.aaData != 'undefined' )

				{

					bUsePassedData = true;

				}

				

				/* Backwards compatability */

				/* aoColumns / aoData - remove at some point... */

				if ( typeof oInit != 'undefined' && typeof oInit.aoData != 'undefined' )

				{

					oInit.aoColumns = oInit.aoData;

				}

				

				/* Language definitions */

				if ( typeof oInit.oLanguage != 'undefined' )

				{

					if ( typeof oInit.oLanguage.sUrl != 'undefined' && oInit.oLanguage.sUrl !== "" )

					{

						/* Get the language definitions from a file */

						oSettings.oLanguage.sUrl = oInit.oLanguage.sUrl;

						$.getJSON( oSettings.oLanguage.sUrl, null, function( json ) { 

							_fnLanguageProcess( oSettings, json, true ); } );

						bInitHandedOff = true;

					}

					else

					{

						_fnLanguageProcess( oSettings, oInit.oLanguage, false );

					}

				}

				/* Warning: The _fnLanguageProcess function is async to the remainder of this function due

				 * to the XHR. We use _bInitialised in _fnLanguageProcess() to check this the processing 

				 * below is complete. The reason for spliting it like this is optimisation - we can fire

				 * off the XHR (if needed) and then continue processing the data.

				 */

			}

			else

			{

				/* Create a dummy object for quick manipulation later on. */

				oInit = {};

			}

			

			/*

			 * Stripes

			 * Add the strip classes now that we know which classes to apply - unless overruled

			 */

			if ( typeof oInit.asStripClasses == 'undefined' )

			{

				oSettings.asStripClasses.push( oSettings.oClasses.sStripOdd );

				oSettings.asStripClasses.push( oSettings.oClasses.sStripEven );

			}

			

			/* Remove row stripe classes if they are already on the table row */

			var bStripeRemove = false;

			var anRows = $('tbody>tr', this);

			for ( i=0, iLen=oSettings.asStripClasses.length ; i<iLen ; i++ )

			{

				if ( anRows.filter(":lt(2)").hasClass( oSettings.asStripClasses[i]) )

				{

					bStripeRemove = true;

					break;

				}

			}

					

			if ( bStripeRemove )

			{

				/* Store the classes which we are about to remove so they can be readded on destory */

				oSettings.asDestoryStrips = [ '', '' ];

				if ( $(anRows[0]).hasClass(oSettings.oClasses.sStripOdd) )

				{

					oSettings.asDestoryStrips[0] += oSettings.oClasses.sStripOdd+" ";

				}

				if ( $(anRows[0]).hasClass(oSettings.oClasses.sStripEven) )

				{

					oSettings.asDestoryStrips[0] += oSettings.oClasses.sStripEven;

				}

				if ( $(anRows[1]).hasClass(oSettings.oClasses.sStripOdd) )

				{

					oSettings.asDestoryStrips[1] += oSettings.oClasses.sStripOdd+" ";

				}

				if ( $(anRows[1]).hasClass(oSettings.oClasses.sStripEven) )

				{

					oSettings.asDestoryStrips[1] += oSettings.oClasses.sStripEven;

				}

				

				anRows.removeClass( oSettings.asStripClasses.join(' ') );

			}

			

			/*

			 * Columns

			 * See if we should load columns automatically or use defined ones

			 */

			var nThead = this.getElementsByTagName('thead');

			var anThs = nThead.length===0 ? [] : _fnGetUniqueThs( nThead[0] );

			var aoColumnsInit;

			

			/* If not given a column array, generate one with nulls */

			if ( typeof oInit.aoColumns == 'undefined' )

			{

				aoColumnsInit = [];

				for ( i=0, iLen=anThs.length ; i<iLen ; i++ )

				{

					aoColumnsInit.push( null );

				}

			}

			else

			{

				aoColumnsInit = oInit.aoColumns;

			}

			

			/* Add the columns */

			for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ )

			{

				/* Check if we have column visibilty state to restore */

				if ( typeof oInit.saved_aoColumns != 'undefined' && oInit.saved_aoColumns.length == iLen )

				{

					if ( aoColumnsInit[i] === null )

					{

						aoColumnsInit[i] = {};

					}

					aoColumnsInit[i].bVisible = oInit.saved_aoColumns[i].bVisible;

				}

				

				_fnAddColumn( oSettings, anThs ? anThs[i] : null );

			}

			

			/* Add options from column definations */

			if ( typeof oInit.aoColumnDefs != 'undefined' )

			{

				/* Loop over the column defs array - loop in reverse so first instace has priority */

				for ( i=oInit.aoColumnDefs.length-1 ; i>=0 ; i-- )

				{

					/* Each column def can target multiple columns, as it is an array */

					var aTargets = oInit.aoColumnDefs[i].aTargets;

					if ( !$.isArray( aTargets ) )

					{

						_fnLog( oSettings, 1, 'aTargets must be an array of targets, not a '+(typeof aTargets) );

					}

					for ( j=0, jLen=aTargets.length ; j<jLen ; j++ )

					{

						if ( typeof aTargets[j] == 'number' && aTargets[j] >= 0 )

						{

							/* 0+ integer, left to right column counting. We add columns which are unknown

							 * automatically. Is this the right behaviour for this? We should at least

							 * log it in future. We cannot do this for the negative or class targets, only here.

							 */

							while( oSettings.aoColumns.length <= aTargets[j] )

							{

								_fnAddColumn( oSettings );

							}

							_fnColumnOptions( oSettings, aTargets[j], oInit.aoColumnDefs[i] );

						}

						else if ( typeof aTargets[j] == 'number' && aTargets[j] < 0 )

						{

							/* Negative integer, right to left column counting */

							_fnColumnOptions( oSettings, oSettings.aoColumns.length+aTargets[j], 

								oInit.aoColumnDefs[i] );

						}

						else if ( typeof aTargets[j] == 'string' )

						{

							/* Class name matching on TH element */

							for ( k=0, kLen=oSettings.aoColumns.length ; k<kLen ; k++ )

							{

								if ( aTargets[j] == "_all" ||

								     oSettings.aoColumns[k].nTh.className.indexOf( aTargets[j] ) != -1 )

								{

									_fnColumnOptions( oSettings, k, oInit.aoColumnDefs[i] );

								}

							}

						}

					}

				}

			}

			

			/* Add options from column array - after the defs array so this has priority */

			if ( typeof aoColumnsInit != 'undefined' )

			{

				for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ )

				{

					_fnColumnOptions( oSettings, i, aoColumnsInit[i] );

				}

			}

			

			/*

			 * Sorting

			 * Check the aaSorting array

			 */

			for ( i=0, iLen=oSettings.aaSorting.length ; i<iLen ; i++ )

			{

				if ( oSettings.aaSorting[i][0] >= oSettings.aoColumns.length )

				{

					oSettings.aaSorting[i][0] = 0;

				}

				var oColumn = oSettings.aoColumns[ oSettings.aaSorting[i][0] ];

				

				/* Add a default sorting index */

				if ( typeof oSettings.aaSorting[i][2] == 'undefined' )

				{

					oSettings.aaSorting[i][2] = 0;

				}

				

				/* If aaSorting is not defined, then we use the first indicator in asSorting */

				if ( typeof oInit.aaSorting == "undefined" && 

						 typeof oSettings.saved_aaSorting == "undefined" )

				{

					oSettings.aaSorting[i][1] = oColumn.asSorting[0];

				}

				

				/* Set the current sorting index based on aoColumns.asSorting */

				for ( j=0, jLen=oColumn.asSorting.length ; j<jLen ; j++ )

				{

					if ( oSettings.aaSorting[i][1] == oColumn.asSorting[j] )

					{

						oSettings.aaSorting[i][2] = j;

						break;

					}

				}

			}

				

			/* Do a first pass on the sorting classes (allows any size changes to be taken into

			 * account, and also will apply sorting disabled classes if disabled

			 */

			_fnSortingClasses( oSettings );

			

			/*

			 * Final init

			 * Sanity check that there is a thead and tbody. If not let's just create them

			 */

			if ( this.getElementsByTagName('thead').length === 0 )

			{

				this.appendChild( document.createElement( 'thead' ) );

			}

			

			if ( this.getElementsByTagName('tbody').length === 0 )

			{

				this.appendChild( document.createElement( 'tbody' ) );

			}

			

			oSettings.nTHead = this.getElementsByTagName('thead')[0];

			oSettings.nTBody = this.getElementsByTagName('tbody')[0];

			if ( this.getElementsByTagName('tfoot').length > 0 )

			{

				oSettings.nTFoot = this.getElementsByTagName('tfoot')[0];

			}

			

			/* Check if there is data passing into the constructor */

			if ( bUsePassedData )

			{

				for ( i=0 ; i<oInit.aaData.length ; i++ )

				{

					_fnAddData( oSettings, oInit.aaData[ i ] );

				}

			}

			else

			{

				/* Grab the data from the page */

				_fnGatherData( oSettings );

			}

			

			/* Copy the data index array */

			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();

			

			/* Initialisation complete - table can be drawn */

			oSettings.bInitialised = true;

			

			/* Check if we need to initialise the table (it might not have been handed off to the

			 * language processor)

			 */

			if ( bInitHandedOff === false )

			{

				_fnInitalise( oSettings );

			}

		});

	};

})(jQuery, window, document);


